Skip to content

โ† All Skills Knowledge History

Memo .txt backup/restore with VueUse useFileSystemAccess

completed๐Ÿ“ฆ my-memo๐Ÿ‘ค YCEJ9999๐Ÿ—“ 2026-06-20 23:30:00

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.isSupported is a computed ref โ†’ check fsa.isSupported.value.
  • fsa.data is a ShallowRef โ†’ fsa.data.value.

Two gotchas

  1. Chromium-only. showSaveFilePicker / showOpenFilePicker don't exist in Firefox/Safari, so fsa.isSupported.value is false there. 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 change await file.text(). Reset input.value = '' afterward so re-selecting the same file fires change again.
  2. Cancelling the picker rejects with AbortError. Wrap saveAs()/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 composable useMemoUtilsMyMemo() (auto-imported; Rule 8 unique name): serializeMemos, parseMemos, backupFileName.
  • Store (use-memo.ts) gets importMemos(incoming): number โ€” merge (append), persist(), notif the count. The parser assigns a fresh crypto.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 into store.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; Disematkan accepts ya/true/yes/1.
  • a plain .txt with 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

FileOperationNote
src/composables/use-memo-utils.tsaddeduseMemoUtilsMyMemo(): serializeMemos / parseMemos (round-trip .txt + plain-text fallback, fresh uuids) / backupFileName
src/stores/use-memo.tsmodifiedAdded importMemos(incoming): number โ€” merge/append + persist + notif count; exported it
src/pages/memo/index.vuemodifieduseFileSystemAccess (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


โ† All Skills Knowledge History