From ef8d219cdd1619ea5109e5fa65ca33b91f5dc707 Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Fri, 4 Sep 2026 00:15:43 +0200 Subject: [PATCH] feat: the autosave keeps the last three versions of the document One version every five minutes goes into an IndexedDB store, pictures inline, and both file menus list them by time; a click puts that text back and leaves the page setup, the styles and the zones alone. The browser copy is one document deep, so a bad edit had nothing behind it. --- README.md | 3 +- src/App.svelte | 36 ++++++++++++++- src/lib/components/ribbon/Ribbon.svelte | 13 +++++- src/lib/i18n/locales/de.ts | 4 ++ src/lib/i18n/locales/en.ts | 4 ++ src/lib/storage/CLAUDE.md | 1 + src/lib/storage/autosave.ts | 4 ++ src/lib/storage/snapshots.svelte.ts | 58 +++++++++++++++++++++++++ tests/unit/snapshots.test.ts | 54 +++++++++++++++++++++++ 9 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 src/lib/storage/snapshots.svelte.ts create mode 100644 tests/unit/snapshots.test.ts diff --git a/README.md b/README.md index 3ef187a..922aff5 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,8 @@ EdenText is a web-based, powerful word processor for everything from quick notes > EdenText is young, in **beta** and actively developed — more features are on > the way. It is tested — the full suite plus LibreOffice round-trip checks run > on every commit — but expect occasional bugs, and keep backups of documents -> you care about. +> you care about. The browser copy also keeps the last three versions of the open +> document — File ▸ Earlier versions puts one back. [![EdenText editing a multi-column document](docs/screenshot.png)](https://edentext.app) Sample File from https://sample-files.com/ diff --git a/src/App.svelte b/src/App.svelte index 4a63c32..9f3beb5 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -46,6 +46,7 @@ import { loadLineNumbering, saveLineNumbering, DEFAULT_LINE_NUMBERING, type LineNumbering } from './lib/storage/lineNumbering'; import { loadFoldMarks, saveFoldMarks } from './lib/storage/foldMarks'; import { printMarkup } from './lib/storage/printMarkup.svelte'; + import { snapshots, loadSnapshots, readSnapshot } from './lib/storage/snapshots.svelte'; import { commentsInPane, changesInPane, markupAttrs, setShowChanges, setShowComments } from './lib/storage/markup.svelte'; import { loadDocumentLanguage, saveDocumentLanguage, odfFromLanguage, type DocumentLanguage } from './lib/storage/documentLanguage'; import { setTableLanguage } from './lib/storage/tableOptions.svelte'; @@ -68,7 +69,7 @@ import { OPEN_THESAURUS_EVENT } from './lib/spell/thesaurus'; import StyleManagerDialog from './lib/components/StyleManagerDialog.svelte'; import NoteOptionsDialog from './lib/components/NoteOptionsDialog.svelte'; - import { t } from './lib/i18n/i18n.svelte'; + import { t, locale } from './lib/i18n/i18n.svelte'; import { withShortcut } from './lib/i18n/shortcut'; import { DEFAULT_SHORTCUTS, matchesEvent, shortcutHint } from './lib/editor/shortcuts'; import { OPEN_LINK_DIALOG_EVENT } from './lib/editor/extensions/link'; @@ -1040,6 +1041,29 @@ } } + // The versions the autosave keeps aside, and the reader's label for one. + void loadSnapshots(); + function snapshotLabel(at: number): string { + return new Date(at).toLocaleString(locale(), { dateStyle: 'short', timeStyle: 'short' }); + } + + // Put an earlier version's text back. The page setup, the styles and the zones + // belong to the open document, so only the text is replaced. + async function handleRestoreSnapshot(at: number) { + exportMenuOpen = false; + if (!editor || !confirm(t().dialogs.confirmRestore(snapshotLabel(at)))) return; + const json = await readSnapshot(at); + if (!json) { + alert(t().dialogs.snapshotGone); + void loadSnapshots(); + return; + } + loadContent(json as Content); + documentEpoch++; + resetHistory(); + editor.commands.focus(); + } + function handleForgetRecent() { forgetRecentFiles(); recentFiles = []; @@ -1317,6 +1341,8 @@ recentFiles={recentFiles} onOpenRecent={(id) => { const f = recentFiles.find((r) => r.id === id); if (f) void handleOpenRecent(f); }} onForgetRecent={handleForgetRecent} + snapshots={snapshots().map((at) => ({ at, label: snapshotLabel(at) }))} + onRestoreSnapshot={(at) => void handleRestoreSnapshot(at)} onExportPdf={handleExportPdf} onPrintPdf={handlePrintPdf} onPrint={handlePrint} @@ -1454,6 +1480,14 @@ {t().app.clearRecentFiles} {/if} + {#if snapshots().length} +
{t().app.versions}
+ {#each snapshots() as at (at)} + + {/each} + {/if} {/if} + {#if snapshots.length} +
+ {#each snapshots as s (s.at)} + + {/each} + {/if}