feat: Introduce a new desktop application for IPSExtractor, enabling extraction workflows through a React-based UI. This commit includes the application structure, API client integration, and initial components for user interaction. Enhance documentation to guide setup and usage, and update .gitignore to accommodate new files.

This commit is contained in:
erik
2026-07-05 15:07:11 -05:00
parent b55ad74539
commit 9aca926988
31 changed files with 11870 additions and 66 deletions
+110
View File
@@ -0,0 +1,110 @@
const { app, BrowserWindow, dialog, ipcMain } = require("electron");
const fs = require("node:fs/promises");
const path = require("node:path");
const DOCUMENT_EXTENSIONS = new Set([".pdf", ".tif", ".tiff", ".png", ".jpg", ".jpeg"]);
function createWindow() {
const win = new BrowserWindow({
width: 1280,
height: 860,
minWidth: 1080,
minHeight: 720,
title: "IPSExtractor Desktop",
backgroundColor: "#f6f7f9",
webPreferences: {
preload: path.join(__dirname, "preload.cjs"),
contextIsolation: true,
nodeIntegration: false,
},
});
if (!app.isPackaged) {
win.loadURL("http://127.0.0.1:5173");
win.webContents.openDevTools({ mode: "detach" });
} else {
win.loadFile(path.join(__dirname, "../dist/index.html"));
}
}
app.whenReady().then(() => {
createWindow();
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") app.quit();
});
ipcMain.handle("dialog:select-directory", async () => {
const result = await dialog.showOpenDialog({
properties: ["openDirectory"],
});
if (result.canceled || result.filePaths.length === 0) return null;
return result.filePaths[0];
});
ipcMain.handle("runtime:config", async () => {
return {
documentRootLocal: process.env.IPS_DOCUMENT_ROOT || "",
documentRootContainer: process.env.IPS_DOCUMENT_ROOT_CONTAINER || "/data",
};
});
ipcMain.handle("dialog:save-csv", async (_event, defaultPath) => {
const result = await dialog.showSaveDialog({
defaultPath: defaultPath || "extraccion.csv",
filters: [{ name: "CSV", extensions: ["csv"] }],
});
if (result.canceled || !result.filePath) return null;
return result.filePath;
});
ipcMain.handle("file:write-text", async (_event, filePath, content) => {
await fs.writeFile(filePath, content, "utf8");
return { ok: true, filePath };
});
ipcMain.handle("fs:first-document", async (_event, rootPath, keywords) => {
const matches = await findDocuments(rootPath, Array.isArray(keywords) ? keywords : []);
return matches[0] || null;
});
async function findDocuments(rootPath, keywords) {
const normalizedKeywords = keywords
.map((item) => String(item || "").trim().toLowerCase())
.filter(Boolean);
const out = [];
async function walk(dir) {
let entries;
try {
entries = await fs.readdir(dir, { withFileTypes: true });
} catch {
return;
}
entries.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }));
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
await walk(fullPath);
if (out.length > 0) return;
continue;
}
const ext = path.extname(entry.name).toLowerCase();
if (!DOCUMENT_EXTENSIONS.has(ext)) continue;
const lowerName = entry.name.toLowerCase();
if (normalizedKeywords.length > 0 && !normalizedKeywords.some((kw) => lowerName.includes(kw))) {
continue;
}
out.push(fullPath);
return;
}
}
await walk(rootPath);
return out;
}