293 lines
8.7 KiB
JavaScript
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();
|
|
});
|