const { app, BrowserWindow, dialog, ipcMain } = require("electron"); const fs = require("node:fs/promises"); const path = require("node:path"); const { indexFolder } = require("./folderIndex.cjs"); const autoUpdate = require("./autoUpdate.cjs"); const { version: appVersion } = require("../package.json"); const DOCUMENT_EXTENSIONS = new Set([".pdf", ".tif", ".tiff", ".png", ".jpg", ".jpeg"]); const APP_ICON = path.join(__dirname, "icon.png"); /** @type {BrowserWindow | null} */ let mainWindow = null; function createWindow() { const win = new BrowserWindow({ width: 1280, height: 860, minWidth: 1080, minHeight: 720, title: `IPSExtractor Desktop v${appVersion}`, icon: APP_ICON, backgroundColor: "#f6f7f9", webPreferences: { preload: path.join(__dirname, "preload.cjs"), contextIsolation: true, nodeIntegration: false, }, }); mainWindow = win; win.on("closed", () => { if (mainWindow === win) mainWindow = null; }); if (!app.isPackaged) { win.loadURL("http://127.0.0.1:5173"); win.webContents.openDevTools({ mode: "detach" }); } else { win.loadFile(path.join(app.getAppPath(), "dist", "index.html")); } } function sendUpdateProgress(payload) { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send("updater:progress", payload); } } ipcMain.handle("updater:check", async () => { return autoUpdate.checkForUpdates(); }); ipcMain.handle("updater:release-meta", async () => { return autoUpdate.getCurrentReleaseMeta(); }); ipcMain.handle("updater:list-versions", async () => { return autoUpdate.listVersions(); }); ipcMain.handle("updater:install", async (_event, manifest) => { if (!app.isPackaged) { throw new Error("Instalar actualizaciones solo está disponible en la app instalada"); } return autoUpdate.downloadAndInstall(manifest, sendUpdateProgress); }); ipcMain.handle("net:download-url", async (event, urlStr) => { const raw = String(urlStr || "").trim(); let parsed; try { parsed = new URL(raw); } catch { throw new Error("URL de descarga inválida"); } if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { throw new Error("Solo se permiten descargas http(s)"); } const win = BrowserWindow.fromWebContents(event.sender) || mainWindow; if (!win || win.isDestroyed()) { throw new Error("Ventana no disponible para iniciar la descarga"); } win.webContents.downloadURL(parsed.toString()); return { ok: true }; }); app.whenReady().then(() => { if (process.platform === "win32") { app.setAppUserModelId("com.ipsextractor.desktop"); } 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("dialog:save-file", async (_event, defaultPath, filters) => { const result = await dialog.showSaveDialog({ defaultPath: defaultPath || "documento.pdf", filters: Array.isArray(filters) && filters.length > 0 ? filters : [{ name: "PDF", extensions: ["pdf"] }], }); 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("file:write-bytes", async (_event, filePath, data) => { const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data instanceof ArrayBuffer ? data : new Uint8Array(data)); await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, buffer); return { ok: true, filePath }; }); ipcMain.handle("path:join", async (_event, parts) => { const list = Array.isArray(parts) ? parts.map(String) : []; return path.join(...list); }); ipcMain.handle("fs:first-document", async (_event, rootPath, keywords) => { const matches = await findDocuments(rootPath, Array.isArray(keywords) ? keywords : []); return matches[0] || null; }); ipcMain.handle("fs:index-folder", async (_event, rootPath, structureMode, docTypes, keywords) => { if (!rootPath) return []; return indexFolder(rootPath, structureMode, Array.isArray(docTypes) ? docTypes : [], Array.isArray(keywords) ? keywords : []); }); ipcMain.handle("fs:read-file", async (_event, filePath) => { const buffer = await fs.readFile(filePath); return { filename: path.basename(filePath), size: buffer.byteLength, data: buffer, }; }); 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; }