const { app, BrowserWindow, ipcMain, shell, dialog } = require("electron"); const fs = require("fs"); const path = require("path"); const { importPdfFiles } = require("./pdf-import"); const EMPTY_CATALOG = { folders: [], books: [], resources: [], }; function settingsPath() { return path.join(app.getPath("userData"), "settings.json"); } function readSettings() { try { const data = JSON.parse(fs.readFileSync(settingsPath(), "utf8")); return { vaults: Array.isArray(data.vaults) ? data.vaults : [], activeVaultId: data.activeVaultId || "", }; } catch { return { vaults: [], activeVaultId: "" }; } } function writeSettings(settings) { const payload = { vaults: Array.isArray(settings.vaults) ? settings.vaults : [], activeVaultId: settings.activeVaultId || "", }; fs.mkdirSync(path.dirname(settingsPath()), { recursive: true }); fs.writeFileSync(settingsPath(), JSON.stringify(payload, null, 2)); return payload; } function activeVault(settings = readSettings()) { return settings.vaults.find((vault) => vault.id === settings.activeVaultId) || null; } function makeVaultId(name) { const slug = String(name || "vault") .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-|-$/g, "") .slice(0, 40); return `${slug || "vault"}-${Date.now().toString(36)}`; } function addOrSelectVault(folderPath, name) { const settings = readSettings(); const resolved = path.resolve(folderPath); let vault = settings.vaults.find((item) => path.resolve(item.path) === resolved); if (!vault) { vault = { id: makeVaultId(name || path.basename(resolved)), name: (name || path.basename(resolved)).trim() || path.basename(resolved), path: resolved, }; settings.vaults.push(vault); } settings.activeVaultId = vault.id; return { settings: writeSettings(settings), vault }; } function ensureVaultFiles(folderPath) { if (!fs.existsSync(folderPath)) { return { ok: false, reason: "missing-folder", path: folderPath }; } const libraryFile = path.join(folderPath, "library.json"); fs.mkdirSync(path.join(folderPath, "pdfs"), { recursive: true }); if (!fs.existsSync(libraryFile)) { fs.writeFileSync(libraryFile, JSON.stringify(EMPTY_CATALOG, null, 2)); } return { ok: true, libraryPath: libraryFile, path: folderPath }; } function parseCatalog(raw) { const data = JSON.parse(raw); return { folders: Array.isArray(data.folders) ? data.folders : [], books: Array.isArray(data.books) ? data.books : [], resources: Array.isArray(data.resources) ? data.resources : [], }; } function loadLibrary() { const settings = readSettings(); const vault = activeVault(settings); if (!vault) { return { ok: false, reason: "no-vault", settings }; } const ensured = ensureVaultFiles(vault.path); if (!ensured.ok) { return { ...ensured, settings, vault }; } try { return { ok: true, settings, vault, catalog: parseCatalog(fs.readFileSync(ensured.libraryPath, "utf8")), libraryPath: ensured.libraryPath, }; } catch (err) { return { ok: false, reason: "unreadable", error: err.message, settings, vault, path: vault.path, }; } } function saveLibrary(data) { const loaded = loadLibrary(); if (!loaded.ok) return loaded; const payload = { folders: Array.isArray(data.folders) ? data.folders : [], books: Array.isArray(data.books) ? data.books : [], resources: Array.isArray(data.resources) ? data.resources : [], }; fs.writeFileSync(loaded.libraryPath, JSON.stringify(payload, null, 2)); return { ...loaded, catalog: payload }; } function resolvePdfPath(vaultRoot, pdfPath) { if (!pdfPath) return ""; if (path.isAbsolute(pdfPath) && fs.existsSync(pdfPath)) return pdfPath; const joined = path.join(vaultRoot, pdfPath); if (fs.existsSync(joined)) return joined; return ""; } function toPosixRelative(from, to) { return path.relative(from, to).split(path.sep).join("/"); } async function importIntoActiveVault(filePaths, existingNames) { const loaded = loadLibrary(); if (!loaded.ok) { return { imported: [], skipped: [], errors: [], cancelled: false, ...loaded }; } const destDir = path.join(loaded.vault.path, "pdfs"); const result = await importPdfFiles(filePaths || [], destDir, existingNames || []); result.imported = result.imported.map((item) => ({ ...item, pdfPath: toPosixRelative(loaded.vault.path, item.pdfPath), })); return result; } function focusedWindow() { return BrowserWindow.getFocusedWindow(); } function createWindow() { const win = new BrowserWindow({ width: 1280, height: 800, minWidth: 900, minHeight: 600, backgroundColor: "#f3ead3", autoHideMenuBar: true, webPreferences: { preload: path.join(__dirname, "preload.js"), contextIsolation: true, nodeIntegration: false, }, }); win.loadFile(path.join(__dirname, "..", "index.html")); win.maximize(); win.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url); return { action: "deny" }; }); } app.whenReady().then(() => { ipcMain.handle("library:load", () => loadLibrary()); ipcMain.handle("library:save", (_event, data) => saveLibrary(data)); ipcMain.handle("settings:load", () => { const settings = readSettings(); return { settings, vault: activeVault(settings) }; }); ipcMain.handle("vault:open", async () => { const result = await dialog.showOpenDialog(focusedWindow(), { title: "Open vault folder", properties: ["openDirectory"], }); if (result.canceled || !result.filePaths[0]) { return { ok: false, cancelled: true, ...loadLibrary() }; } const folderPath = result.filePaths[0]; if (!fs.existsSync(folderPath)) { return { ok: false, reason: "missing-folder", path: folderPath, settings: readSettings(), }; } addOrSelectVault(folderPath, path.basename(folderPath)); return loadLibrary(); }); ipcMain.handle("vault:create", async (_event, name) => { const folderName = String(name || "").trim(); if (!folderName) { return { ok: false, reason: "no-name", settings: readSettings() }; } const result = await dialog.showOpenDialog(focusedWindow(), { title: "Choose parent folder for the new vault", properties: ["openDirectory", "createDirectory"], }); if (result.canceled || !result.filePaths[0]) { return { ok: false, cancelled: true, ...loadLibrary() }; } const folderPath = path.join(result.filePaths[0], folderName); fs.mkdirSync(folderPath, { recursive: true }); addOrSelectVault(folderPath, folderName); return loadLibrary(); }); ipcMain.handle("vault:switch", (_event, id) => { const settings = readSettings(); if (!settings.vaults.some((vault) => vault.id === id)) { return loadLibrary(); } writeSettings({ ...settings, activeVaultId: id }); return loadLibrary(); }); ipcMain.handle("vault:remove", (_event, id) => { const settings = readSettings(); const vaults = settings.vaults.filter((vault) => vault.id !== id); const activeVaultId = settings.activeVaultId === id ? vaults[0]?.id || "" : settings.activeVaultId; writeSettings({ vaults, activeVaultId }); return loadLibrary(); }); ipcMain.handle("pdf:import", async (_event, existingNames) => { const loaded = loadLibrary(); if (!loaded.ok) { return { imported: [], skipped: [], errors: [], cancelled: false, ...loaded }; } const result = await dialog.showOpenDialog(focusedWindow(), { title: "Import PDFs", properties: ["openFile", "multiSelections"], filters: [{ name: "PDF documents", extensions: ["pdf"] }], }); if (result.canceled || !result.filePaths.length) { return { imported: [], skipped: [], errors: [], cancelled: true }; } return importIntoActiveVault(result.filePaths, existingNames); }); ipcMain.handle("pdf:importPaths", async (_event, filePaths, existingNames) => { return importIntoActiveVault(filePaths, existingNames); }); ipcMain.handle("pdf:open", async (_event, pdfPath) => { const vault = activeVault(); if (!vault) return { ok: false, error: "No vault open." }; const resolved = resolvePdfPath(vault.path, pdfPath); if (!resolved) { return { ok: false, error: "File not found. Wait for Google Drive to finish syncing, then try again.", }; } const error = await shell.openPath(resolved); return { ok: !error, error: error || "" }; }); createWindow(); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on("window-all-closed", () => { if (process.platform !== "darwin") app.quit(); });