feat(settings): the storage folder now decides where books are saved — new books, .md imports, and the export dialog default all land there (was: always Documents); label clarifies it covers books + snapshots

This commit is contained in:
avi 2026-10-04 11:34:10 -05:00
commit 283bb7c772
3 changed files with 36 additions and 14 deletions

View file

@ -246,13 +246,30 @@ async function openBookAt(p: string): Promise<BookResult | ErrResult> {
} }
} }
// Create a new book as a dedicated folder <name>/ inside the user's Documents // Create a new book as a dedicated folder <name>/ inside the user's chosen
// directory, then open it. The folder name is the sanitized book title; if a // storage folder (Settings → Folder), falling back to Documents when none is
// folder with that name already exists we append a numeric suffix. // set or the stored one no longer exists, then open it. The folder name is
// the sanitized book title; if a folder with that name already exists we
// append a numeric suffix.
function getBooksDir(): string {
// One storage folder drives everything (snapshots, recovery, new books).
// snapshotLocation is the one the Settings modal writes; backupLocation is
// the older key — honor both so an existing choice keeps working.
for (const key of ["snapshotLocation", "backupLocation"]) {
const v = String(getSetting<string>(key, "") || "").trim();
try {
if (v && fs.statSync(v).isDirectory()) return v;
} catch {
// stale or unreadable path — fall through to the next candidate
}
}
return app.getPath("documents");
}
handleIpc("folio:newBookNamed", async (_e, name: string) => { handleIpc("folio:newBookNamed", async (_e, name: string) => {
const title = (name || "").trim(); const title = (name || "").trim();
if (!title) return { error: "Please provide a book name." }; if (!title) return { error: "Please provide a book name." };
const base = app.getPath("documents"); const base = getBooksDir();
const folder = sanitizeBookFolderName(title) || "Untitled"; const folder = sanitizeBookFolderName(title) || "Untitled";
let target = path.join(base, folder); let target = path.join(base, folder);
let n = 2; let n = 2;
@ -591,7 +608,7 @@ function exportDefaultPath(filename: string): string {
const dir = const dir =
last && fs.existsSync(last) && fs.statSync(last).isDirectory() last && fs.existsSync(last) && fs.statSync(last).isDirectory()
? last ? last
: app.getPath("documents"); : getBooksDir();
return path.join(dir, filename); return path.join(dir, filename);
} }
@ -972,6 +989,7 @@ function snapshotSettingsSnapshot() {
keep: clampInt(getSetting("snapshotKeep", SNAPSHOT_DEFAULTS.keep), SNAPSHOT_DEFAULTS.keep, 0, 200), keep: clampInt(getSetting("snapshotKeep", SNAPSHOT_DEFAULTS.keep), SNAPSHOT_DEFAULTS.keep, 0, 200),
location, location,
resolvedRoot: resolved, resolvedRoot: resolved,
booksDir: getBooksDir(),
snapshotCount: snaps.length, snapshotCount: snaps.length,
lastSnapshot: snaps.length ? snaps[snaps.length - 1] : null, lastSnapshot: snaps.length ? snaps[snaps.length - 1] : null,
}; };
@ -1008,7 +1026,7 @@ handleIpc("folio:setSnapshotSettings", (_e, patch: unknown) => {
handleIpc("folio:pickSnapshotFolder", async () => { handleIpc("folio:pickSnapshotFolder", async () => {
const { canceled, filePaths } = await showOpenDialog({ const { canceled, filePaths } = await showOpenDialog({
properties: ["openDirectory"], properties: ["openDirectory"],
title: "Choose snapshot folder", title: "Choose storage folder (new books + snapshots)",
}); });
return { canceled, filePaths }; return { canceled, filePaths };
}); });
@ -1089,14 +1107,14 @@ handleIpc("folio:importMarkdown", async () => {
filters: [{ name: "Markdown and text", extensions: ["md", "markdown", "txt"] }], filters: [{ name: "Markdown and text", extensions: ["md", "markdown", "txt"] }],
}); });
if (canceled || !filePaths.length) return { canceled: true } as const; if (canceled || !filePaths.length) return { canceled: true } as const;
return importMarkdownFiles(filePaths, app.getPath("documents")); return importMarkdownFiles(filePaths, getBooksDir());
}); });
// Open a path that may be either a Folio book directory or a Markdown file // Open a path that may be either a Folio book directory or a Markdown file
// (the desktop launcher can be handed either via file associations). // (the desktop launcher can be handed either via file associations).
handleIpc("folio:openPathOrImport", async (_e, p: string) => { handleIpc("folio:openPathOrImport", async (_e, p: string) => {
if (p && isImportableMarkdown(p)) { if (p && isImportableMarkdown(p)) {
const results = importMarkdownFiles([p], app.getPath("documents")); const results = importMarkdownFiles([p], getBooksDir());
const r = results[0]; const r = results[0];
if (!r.ok) return { error: r.error }; if (!r.ok) return { error: r.error };
return openBookAt(r.bookPath as string); return openBookAt(r.bookPath as string);
@ -1300,7 +1318,7 @@ function fileArgFrom(argv: string[]): string | null {
function openFromArg(p: string): void { function openFromArg(p: string): void {
if (isImportableMarkdown(p)) { if (isImportableMarkdown(p)) {
const results = importMarkdownFiles([p], app.getPath("documents")); const results = importMarkdownFiles([p], getBooksDir());
const r = results[0]; const r = results[0];
if (r.ok) { if (r.ok) {
openBookAt(r.bookPath as string); openBookAt(r.bookPath as string);

View file

@ -23,6 +23,8 @@ interface SnapshotSettings {
keep: number; keep: number;
location: string; location: string;
resolvedRoot: string; resolvedRoot: string;
/** Where new books are saved (Settings → Storage folder). */
booksDir?: string;
snapshotCount: number; snapshotCount: number;
lastSnapshot: string | null; lastSnapshot: string | null;
} }
@ -59,8 +61,10 @@ export function createSettingsFeature(ctx: BookCtx): SettingsFeature {
intervalInput.value = String(s.intervalMin ?? 5); intervalInput.value = String(s.intervalMin ?? 5);
keepInput.value = String(s.keep ?? 20); keepInput.value = String(s.keep ?? 20);
locationInput.value = s.location || ""; locationInput.value = s.location || "";
locationInput.placeholder = "Default: <book>/Folio-Backups"; locationInput.placeholder = "Default: Documents (books) · <book>/Folio-Backups (snapshots)";
resolvedEl.textContent = `Saving to: ${shortPath(s.resolvedRoot || "—")}`; resolvedEl.textContent = s.booksDir
? `New books save to: ${shortPath(s.booksDir)}`
: `Saving to: ${shortPath(s.resolvedRoot || "—")}`;
lastSnapEl.textContent = s.lastSnapshot lastSnapEl.textContent = s.lastSnapshot
? `${s.snapshotCount} snapshot${s.snapshotCount === 1 ? "" : "s"} · latest ${s.lastSnapshot.split("/").pop()}` ? `${s.snapshotCount} snapshot${s.snapshotCount === 1 ? "" : "s"} · latest ${s.lastSnapshot.split("/").pop()}`
: "No snapshots yet."; : "No snapshots yet.";

View file

@ -343,9 +343,9 @@
<label>Keep last <input type="number" id="settingKeep" class="modal-input setting-num" min="0" max="200" value="20" /></label> <label>Keep last <input type="number" id="settingKeep" class="modal-input setting-num" min="0" max="200" value="20" /></label>
</div> </div>
<div class="backup-control"> <div class="backup-control">
<span class="backup-label">Folder</span> <span class="backup-label">Storage folder</span>
<input type="text" id="settingLocation" class="backup-input" readonly placeholder="Default: <book>/Folio-Backups" title="Snapshot folder (empty = default)" /> <input type="text" id="settingLocation" class="backup-input" readonly placeholder="Default: Documents" title="Where new books and snapshots are saved (empty = Documents / &lt;book&gt;/Folio-Backups)" />
<button type="button" class="button button-ghost backup-browse" id="settingBrowse" title="Choose snapshot folder">…</button> <button type="button" class="button button-ghost backup-browse" id="settingBrowse" title="Choose storage folder">…</button>
<button type="button" class="button button-ghost" id="settingResetLoc" title="Back to the default folder">Reset</button> <button type="button" class="button button-ghost" id="settingResetLoc" title="Back to the default folder">Reset</button>
</div> </div>
<p class="setting-meta" id="settingResolved"></p> <p class="setting-meta" id="settingResolved"></p>