From d9d3ddb724e0b7b87bb13d2c31c48f3fcb8b4130 Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Thu, 18 Jun 2026 22:53:46 +0200 Subject: [PATCH] feat: add document name --- src/App.svelte | 104 +++++++++++++++++++++++++++++--- src/lib/export/pdf.ts | 9 ++- src/lib/storage/documentName.ts | 24 ++++++++ 3 files changed, 127 insertions(+), 10 deletions(-) create mode 100644 src/lib/storage/documentName.ts diff --git a/src/App.svelte b/src/App.svelte index 228fa88..5335a66 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -14,6 +14,7 @@ import { loadPageMargins, savePageMargins, DEFAULT_MARGINS, type PageMargins } from './lib/storage/pageMargins'; import { loadOrientation, saveOrientation, type Orientation } from './lib/storage/pageOrientation'; import { loadHfDoc, saveHfDoc, loadHfDistances, saveHfDistances, hfIsEmpty, DEFAULT_HF_DISTANCES, type HfDoc, type HfZone, type HfDistances } from './lib/storage/headerFooter'; + import { loadDocName, saveDocName, stripOdtExtension, sanitizeNameForFile } from './lib/storage/documentName'; let editor: Editor | null = $state(null); let tick: number = $state(0); @@ -40,6 +41,24 @@ let pageMargins: PageMargins = $state(loadPageMargins()); let pageOrientation: Orientation = $state(loadOrientation()); + // The document name (without .odt). Source of truth for the save filename; + // set on open, editable in the header, blank → heading-derived fallback. + let documentName: string = $state(loadDocName()); + + // Shown in the empty title field: what an actual save would name the file. + // Only computed while the field is blank (otherwise the placeholder is hidden, + // so we skip the per-transaction getJSON). + let namePlaceholder = $derived.by(() => { + if (documentName.trim() || tick < 0 || !editor) return 'Untitled document'; + const base = stripOdtExtension(deriveFilename(editor.getJSON() as Parameters[0])); + return base === 'document' ? 'Untitled document' : base; + }); + + function suggestedFilename(json: Parameters[0]): string { + const n = documentName.trim(); + return n ? `${sanitizeNameForFile(n)}.odt` : deriveFilename(json); + } + $effect(() => { saveFormattingMarks(showFormattingMarks); }); @@ -64,6 +83,10 @@ saveHfDistances(hfDistances); }); + $effect(() => { + saveDocName(documentName); + }); + function setZoom(value: number) { zoom = Math.max(20, Math.min(300, value)); localStorage.setItem('odf-editor-zoom', String(zoom)); @@ -122,13 +145,14 @@ pageMargins = { ...DEFAULT_MARGINS }; pageOrientation = 'portrait'; hfDistances = { ...DEFAULT_HF_DISTANCES }; + documentName = ''; fileHandle = null; editor.commands.focus(); } // Replace the document with a parsed .odt; adopt its geometry/header/footer and // track the source handle (null for the fallback file input) so Save overwrites it. - function applyImport(bytes: Uint8Array, handle: FileSystemFileHandle | null) { + function applyImport(bytes: Uint8Array, handle: FileSystemFileHandle | null, sourceName?: string) { if (!editor) return; try { const result = importOdt(bytes); @@ -139,6 +163,8 @@ } editor.commands.setContent(result.content); // onUpdate fires → autosave + // Adopt the opened file's name as the document name (drives the save filename). + if (sourceName) documentName = stripOdtExtension(sourceName); // Adopt the document's page geometry; the $effects persist it and // Editor.svelte re-paginates. if (result.margins) pageMargins = result.margins; @@ -168,7 +194,7 @@ if (fsSupported) { try { const r = await openOdt(); - if (r) applyImport(r.bytes, r.handle); + if (r) applyImport(r.bytes, r.handle, r.name); } catch (err) { if ((err as DOMException)?.name !== 'AbortError') { console.error('[open] Failed to open file:', err); @@ -185,7 +211,7 @@ const file = input.files?.[0]; input.value = ''; // allow re-selecting the same file if (!file) return; - applyImport(new Uint8Array(await file.arrayBuffer()), null); + applyImport(new Uint8Array(await file.arrayBuffer()), null, file.name); } async function handleSave() { @@ -194,7 +220,7 @@ const json = editor.getJSON() as Parameters[0]; try { const bytes = await buildOdt(json, pageMargins, pageOrientation, hfOpts()); - fileHandle = await saveOdt(bytes, deriveFilename(json), fileHandle); + fileHandle = await saveOdt(bytes, suggestedFilename(json), fileHandle); } catch (err) { if ((err as DOMException)?.name === 'AbortError') return; // A stored handle may have lost permission; re-prompt via Save As. @@ -210,7 +236,7 @@ const json = editor.getJSON() as Parameters[0]; try { const bytes = await buildOdt(json, pageMargins, pageOrientation, hfOpts()); - fileHandle = await saveAsOdt(bytes, deriveFilename(json)); + fileHandle = await saveAsOdt(bytes, suggestedFilename(json)); } catch (err) { if ((err as DOMException)?.name === 'AbortError') return; console.error('[save] Failed to save file:', err); @@ -225,9 +251,11 @@ exportMenuOpen = false; pdfBusy = true; try { + const json = editor.getJSON() as Parameters[0]; await exportPdf({ source: editor.view.dom as HTMLElement, - json: editor.getJSON(), + json, + fileName: suggestedFilename(json), orientation: pageOrientation, numPages, }); @@ -245,8 +273,10 @@ if (!editor) return; exportMenuOpen = false; try { + const json = editor.getJSON() as Parameters[0]; printPdf({ - json: editor.getJSON(), + json, + fileName: suggestedFilename(json), margins: pageMargins, orientation: pageOrientation, headerDoc, @@ -314,6 +344,24 @@ {/snippet} +
+ + + e.key === 'Enter' && (e.currentTarget as HTMLInputElement).blur()} + onblur={() => (documentName = documentName.trim())} + /> +