Files
Zaine Qayyum 9e075ec941 Testing
2026-08-19 09:56:06 +01:00

293 lines
8.7 KiB
JavaScript

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();
});