fix: improve saving where the File System Access API is unavailable
The fallback download shows a one-time hint about the browser's ask-where-to-save setting, keeps its anchor in the DOM and the blob URL alive past the click (Gecko). Failed saves and exports now report the underlying error, with a clear message when a script blocker prevented a lazy chunk from loading. A full or blocked localStorage no longer fails the save when recording the recent-files entry.
This commit is contained in:
1 parent
f1b63c722c
commit
fc20e0a89c
7 files changed
+46
-10
No files matched your search
+15
-7
@@ -754,6 +754,14 @@
|
||||
await applyImport(new Uint8Array(await file.arrayBuffer()), null, file.name);
|
||||
}
|
||||
|
||||
// The reason belongs in the message: a failure on someone else's browser is
|
||||
// otherwise unreportable. A lazy chunk a script blocker ate says so in plain words.
|
||||
function failed(what: string, err: unknown): void {
|
||||
const detail = (err as Error)?.message ?? String(err);
|
||||
const blocked = /dynamically imported module|Importing a module script failed/i.test(detail);
|
||||
alert(`${what}\n\n${blocked ? t().dialogs.scriptBlocked : detail}`);
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!editor) return;
|
||||
exportMenuOpen = false;
|
||||
@@ -776,7 +784,7 @@
|
||||
// A stored handle may have lost permission; re-prompt via Save As.
|
||||
if (fileHandle) { fileHandle = null; await handleSaveAs(); return; }
|
||||
console.error('[save] Failed to save file:', err);
|
||||
alert(t().dialogs.couldNotSave);
|
||||
failed(t().dialogs.couldNotSave, err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -792,7 +800,7 @@
|
||||
} catch (err) {
|
||||
if ((err as DOMException)?.name === 'AbortError') return;
|
||||
console.error('[save] Failed to save file:', err);
|
||||
alert(t().dialogs.couldNotSave);
|
||||
failed(t().dialogs.couldNotSave, err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -816,7 +824,7 @@
|
||||
} catch (err) {
|
||||
if ((err as DOMException)?.name === 'AbortError') return;
|
||||
console.error('[save] Failed to save template:', err);
|
||||
alert(t().dialogs.couldNotSave);
|
||||
failed(t().dialogs.couldNotSave, err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -861,7 +869,7 @@
|
||||
} catch (err) {
|
||||
if ((err as DOMException)?.name === 'AbortError') return;
|
||||
console.error('[docx] Export failed:', err);
|
||||
alert(t().dialogs.couldNotExportDocx);
|
||||
failed(t().dialogs.couldNotExportDocx, err);
|
||||
} finally {
|
||||
docxBusy = false;
|
||||
}
|
||||
@@ -885,7 +893,7 @@
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[pdf] Export failed:', err);
|
||||
alert(t().dialogs.couldNotExportPdf);
|
||||
failed(t().dialogs.couldNotExportPdf, err);
|
||||
} finally {
|
||||
pdfBusy = false;
|
||||
}
|
||||
@@ -910,7 +918,7 @@
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[pdf] Print failed:', err);
|
||||
alert(t().dialogs.couldNotPrint);
|
||||
failed(t().dialogs.couldNotPrint, err);
|
||||
} finally {
|
||||
pdfBusy = false;
|
||||
}
|
||||
@@ -940,7 +948,7 @@
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[pdf] Print failed:', err);
|
||||
alert(t().dialogs.couldNotPrintPdf);
|
||||
failed(t().dialogs.couldNotPrintPdf, err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
`odt.ts` (ODF), `docx.ts` (Word), `pdf.ts` (html2canvas + jsPDF), `saveFile.ts`.
|
||||
Frames, tables and pagination have their own deep-dives in `docs/architecture/`.
|
||||
|
||||
`saveFile.ts` picks the path: `showSaveFilePicker` where there is one, else a download —
|
||||
Gecko has none and Brave disables the whole File System Access API by default. The first
|
||||
fallback download shows a one-time hint (`edentext-download-hint`) that the browser's
|
||||
"always ask where to save" setting picks the location.
|
||||
|
||||
## Heading defaults (`HEADING_STYLE_OVERRIDES`, `export/odt.ts`)
|
||||
|
||||
- **Headings** follow LibreOffice too (`HEADING_STYLE_OVERRIDES`): 18 / 16 / 14 / 13 / 12 / 12pt (levels 4 and 6 italic) with
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
// File System Access API helpers for saving/opening .odt files. Falls back to a
|
||||
// plain browser download / no-op where the API is unavailable (Firefox/Safari).
|
||||
|
||||
import { t } from '../i18n/i18n.svelte';
|
||||
|
||||
// Shown once per browser, at the first save that goes the download route — the
|
||||
// moment the missing save dialog is actually felt.
|
||||
const HINT_KEY = 'edentext-download-hint';
|
||||
|
||||
const ODT_MIME = 'application/vnd.oasis.opendocument.text';
|
||||
const OTT_MIME = 'application/vnd.oasis.opendocument.text-template';
|
||||
const DOCX_MIME = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
||||
@@ -37,14 +43,24 @@ export function supportsFsAccess(): boolean {
|
||||
return typeof (window as WinFs).showSaveFilePicker === 'function';
|
||||
}
|
||||
|
||||
// Where the browser saves is the browser's business (its download folder, or its own
|
||||
// dialog where that is switched on). Gecko needs the anchor in the document and the
|
||||
// blob URL alive past the click.
|
||||
function download(bytes: Uint8Array, name: string, mime: string = ODT_MIME): void {
|
||||
// Before the click: the browser's own save dialog would open on top of it.
|
||||
if (!localStorage.getItem(HINT_KEY)) {
|
||||
localStorage.setItem(HINT_KEY, '1');
|
||||
alert(t().ribbon.saveAsHint);
|
||||
}
|
||||
const blob = new Blob([bytes as Uint8Array<ArrayBuffer>], { type: mime });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = name;
|
||||
document.body.append(a);
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 30_000);
|
||||
}
|
||||
|
||||
async function writeHandle(handle: FileSystemFileHandle, bytes: Uint8Array): Promise<void> {
|
||||
|
||||
@@ -741,6 +741,7 @@ const de: Messages = {
|
||||
shapeFormat: 'Formformat',
|
||||
},
|
||||
saveAs: 'Speichern unter…',
|
||||
saveAsHint: 'In den Download-Einstellungen des Browsers aktivieren: „Immer nachfragen, wo Dateien gespeichert werden sollen“.',
|
||||
collapse: 'Menüband minimieren',
|
||||
expand: 'Menüband erweitern',
|
||||
export: 'Exportieren',
|
||||
@@ -910,6 +911,7 @@ const de: Messages = {
|
||||
openedWithLimitations: (list) => `Mit Einschränkungen geöffnet:\n• ${list}`,
|
||||
couldNotOpen: 'Diese Datei konnte nicht geöffnet werden.',
|
||||
couldNotSave: 'Diese Datei konnte nicht gespeichert werden.',
|
||||
scriptBlocked: 'Ein Teil der App konnte nicht geladen werden — ein Skript-Blocker (uBlock Origin, NoScript, strenger Schutzmodus) verhindert das. Erlaube diese Seite und versuche es erneut.',
|
||||
couldNotExportDocx: 'Export nach Word (.docx) nicht möglich.',
|
||||
couldNotExportPdf: 'PDF-Export nicht möglich.',
|
||||
recentUnavailable: (name) =>
|
||||
|
||||
@@ -747,6 +747,7 @@ const en = {
|
||||
shapeFormat: 'Shape Format',
|
||||
},
|
||||
saveAs: 'Save as…',
|
||||
saveAsHint: 'Turn this on in the browser\'s download settings: "Always ask you where to save files".',
|
||||
collapse: 'Collapse the ribbon',
|
||||
expand: 'Expand the ribbon',
|
||||
export: 'Export',
|
||||
@@ -916,6 +917,7 @@ const en = {
|
||||
openedWithLimitations: (list: string) => `Opened with limitations:\n• ${list}`,
|
||||
couldNotOpen: 'Could not open this file.',
|
||||
couldNotSave: 'Could not save this file.',
|
||||
scriptBlocked: 'Part of the app could not be loaded — a script blocker (uBlock Origin, NoScript, strict privacy mode) is blocking it. Allow this site and try again.',
|
||||
couldNotExportDocx: 'Could not export to Word (.docx).',
|
||||
couldNotExportPdf: 'Could not export to PDF.',
|
||||
recentUnavailable: (name: string) =>
|
||||
|
||||
@@ -43,6 +43,7 @@ All keys share the `edentext-` prefix — a namespace, not a description: on Git
|
||||
- **Tab interval:** `edentext-tab-interval` — cm as a decimal string.
|
||||
- **Spacing model:** `edentext-spacing-model` — `'add' | 'max'`.
|
||||
- **Recent fonts:** `edentext-recent-fonts` — JSON string array (ToolbarExpanded).
|
||||
- **Download hint shown:** `edentext-download-hint` — set by the first fallback download (`export/saveFile.ts`); the one-time "always ask where to save" hint.
|
||||
- **Header/footer:** `edentext-header` / `-footer` (HfDoc), `edentext-hf-distances`.
|
||||
- **Styles:** `edentext-styles` — the style registry (`styles/sheet.svelte.ts`).
|
||||
- **Document properties:** `edentext-doc-properties` — title/subject/author/keywords/comments (`docProperties.ts`); the key is removed when every field is empty, so a fresh document writes nothing into `meta.xml`.
|
||||
|
||||
@@ -28,8 +28,10 @@ export function loadRecentFiles(): RecentFile[] {
|
||||
}
|
||||
|
||||
function write(list: RecentFile[]): void {
|
||||
if (list.length) localStorage.setItem(KEY, JSON.stringify(list));
|
||||
else localStorage.removeItem(KEY);
|
||||
try {
|
||||
if (list.length) localStorage.setItem(KEY, JSON.stringify(list));
|
||||
else localStorage.removeItem(KEY);
|
||||
} catch { /* a full or blocked localStorage costs the list, not the save */ }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user