Testing
This commit is contained in:
292
electron/main.js
Normal file
292
electron/main.js
Normal file
@@ -0,0 +1,292 @@
|
||||
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();
|
||||
});
|
||||
Reference in New Issue
Block a user