โ All Skills Knowledge History
Memo .txt backup/restore with VueUse useFileSystemAccess
memobackup-restoreuseFileSystemAccessvueusefile-system-access-apitxt-serializationbrowser-support-fallbackcomposable-helpers
Request
- Add a downloadable .txt backup of all memos, and import a .txt that recreates the memos.
- Use VueUse useFileSystemAccess.
- Write the finding to Mono Skills.
Summary
Memo .txt backup/restore with VueUse useFileSystemAccess
Add "download all memos to a .txt" and "import a .txt" to the localStorage memo app,
using the native file pickers via VueUse, with a fallback for non-Chromium browsers.
useFileSystemAccess โ the shape that works
Auto-imported from @vueuse/core. Create it once in setup, typed for text:
const fsa = useFileSystemAccess({
dataType: 'Text',
types: [{ description: 'Berkas teks', accept: { 'text/plain': ['.txt'] } }],
})
- Save / download: set the data, then open the save picker.
fsa.data.value = text await fsa.saveAs({ suggestedName: 'memo-backup-2026-06-20.txt' }) - Open / import:
await fsa.open()then read the populated ref.await fsa.open() const text = String(fsa.data.value ?? '') fsa.isSupportedis a computed ref โ checkfsa.isSupported.value.fsa.datais a ShallowRef โfsa.data.value.
Two gotchas
- Chromium-only.
showSaveFilePicker/showOpenFilePickerdon't exist in Firefox/Safari, sofsa.isSupported.valueisfalsethere. Guard on it and fall back:- export โ
URL.createObjectURL(new Blob([text], {type:'text/plain'}))+ a temporary<a download>click +revokeObjectURL. - import โ a hidden
<input type="file" accept=".txt,text/plain">; on changeawait file.text(). Resetinput.value = ''afterward so re-selecting the same file fireschangeagain.
- export โ
- Cancelling the picker rejects with
AbortError. WrapsaveAs()/open()in try/catch and swallow(e as DOMException).name === 'AbortError'so a user cancel isn't reported as failure.
Layering (mono Rules 8 / 15)
- Pure serialize/parse in a feature composable
src/composables/use-memo-utils.ts, exported as one uniquely-named composableuseMemoUtilsMyMemo()(auto-imported; Rule 8 unique name):serializeMemos,parseMemos,backupFileName. - Store (
use-memo.ts) getsimportMemos(incoming): numberโ merge (append),persist(), notif the count. The parser assigns a freshcrypto.randomUUID()to every imported memo so a re-import can't collide ids. - Page owns only the file I/O (FSA + fallback) and the toolbar buttons โ it serializes
store.memos, saves, or parses the opened text intostore.importMemos.
Round-trip .txt format (human-readable, full fidelity)
# Memo Backup โ 2026-06-20 (2 memo)
===== MEMO =====
Judul: Belanja
Kategori: Pribadi
Disematkan: ya
Dibuat: 2026-06-19T10:00:00.000Z
Diperbarui: 2026-06-19T11:00:00.000Z
----- ISI -----
beli telur
beli susu
Parse = split on ===== MEMO ===== (drop the file-header chunk), read Key: value lines until
----- ISI -----, then the rest is content verbatim (multiline preserved). Robustness:
- invalid
Kategoriโ falls back to the first category;Disematkanaccepts ya/true/yes/1. - a plain
.txtwith no markers โ one memo (first non-empty line = title, rest = content), so arbitrary text files import too. - Known limit (documented in code): content containing the literal marker lines isn't escaped.
A standalone round-trip test confirmed multiline+blank-line content, timestamps, pinned/category, invalid-category fallback, empty-title skip, and plain-text import all behave.
Files
src/composables/use-memo-utils.ts(new) โ serialize/parse + filename.src/stores/use-memo.tsโimportMemos(incoming)merge action.src/pages/memo/index.vueโuseFileSystemAccess,exportBackup()/importBackup(), the Blob/anchor + hidden-input fallbacks, and two toolbar buttons.
Outcome
successvite module transform + standalone round-trip test /memo, the page, the store, and use-memo-utils.ts all transform 200 UnoCSS generates i-mdi-file-import and i-mdi-content-save-outline Round-trip test passed: multiline content (incl. blank lines) preserved, timestamps/category/pinned restored, invalid category -> default, empty title skipped, plain .txt (no markers) -> one memo File-picker UI is Chromium-only and not clickable headless; logic guarded by fsa.isSupported with Blob/anchor + file-input fallback
Decisions
useFileSystemAccess with isSupported guard + fallback
Primary path uses VueUse useFileSystemAccess (saveAs/open); when fsa.isSupported.value is false, fall back to Blob+anchor download and a hidden <input type=file>.
Why: The File System Access API is Chromium-only; the user asked for useFileSystemAccess but the app must still export/import in Firefox/Safari. One guard keeps both paths.
Readable round-trip .txt format, not JSON
Human-readable marker format (===== MEMO ===== header lines + ----- ISI ----- body); parser also accepts a plain .txt as a single memo.
Why: User chose readable text for a memo backup. Markers keep multiline content verbatim and round-trip every field; plain-text fallback lets any .txt import as a memo.
Merge on import with fresh uuids
importMemos appends; parseMemos assigns a new crypto.randomUUID() to every imported memo.
Why: User chose merge (no data loss). Fresh ids prevent collisions/overwrites when re-importing a backup that originated from this same app.
Layering: pure helpers in a composable, file I/O in the page
serialize/parse live in use-memo-utils.ts (useMemoUtilsMyMemo); the store gets importMemos; the page owns useFileSystemAccess + buttons.
Why: Rule 15 (feature helpers in use-<name>-utils) + Rule 8 (unique auto-imported names). Browser file pickers belong in the component setup, not the Pinia store.
Files changed
| File | Operation | Note |
|---|---|---|
src/composables/use-memo-utils.ts | added | useMemoUtilsMyMemo(): serializeMemos / parseMemos (round-trip .txt + plain-text fallback, fresh uuids) / backupFileName |
src/stores/use-memo.ts | modified | Added importMemos(incoming): number โ merge/append + persist + notif count; exported it |
src/pages/memo/index.vue | modified | useFileSystemAccess (dataType Text); exportBackup()/importBackup() with isSupported guard + Blob/anchor & hidden file-input fallback + AbortError swallow; Impor .txt / Unduh cadangan toolbar buttons |
Commands
pnpm dev
curl /memo, page, store, /src/composables/use-memo-utils.ts (all 200)
node round-trip test of serialize/parse (all passed)
MONO_SKILLS=true MONO_SKILLS_GITHUB_TOKEN=[REDACTED] pnpm exec mono skills save --app my-memo --dir .mono/skills/pending/<id>Validation
Tests: not run Build: passed