1.1.1: fullbak

This commit is contained in:
th committed 2026-04-25 00:40:53 +02:00
1 parent d59201100b
commit 01c13981c4
6 files changed
+443

No files matched your search

+131
View File
@@ -1,6 +1,8 @@
<script setup lang="ts">
import { nextTick, onMounted, ref, watch } from 'vue'
import {
PhArchive,
PhArrowsClockwise,
PhFileArrowUp,
PhFolderOpen,
PhPlus,
@@ -9,9 +11,16 @@ import {
import ruwioLogo from '@/assets/logo.png'
import {
deleteStoredProject,
listAllProjects,
listRecentProjects,
saveProjectToStore,
type StoredProject,
} from '@/lib/projectStore'
import {
downloadBackup,
pickBackupFile,
readBackupFile,
} from '@/lib/backupFile'
import { confirmDialog, Modal } from '@/components/ui'
import { t } from '@/i18n'
@@ -149,6 +158,90 @@ function importFile() {
emit('update:modelValue', false)
}
const backupBusy = ref(false)
const backupError = ref<string | null>(null)
/** Dump every project in IDB into a single .ruwbak file. No-ops
* with a friendly toast when the store is empty — users expect a
* signal, not a mystery zero-byte file. */
async function backupAll() {
if (backupBusy.value) return
backupBusy.value = true
backupError.value = null
try {
const all = await listAllProjects()
const records = all
.filter((r) => !!r.project)
.map((r) => ({
id: r.id,
name: r.name,
savedAt: r.savedAt,
thumbnail: r.thumbnail,
project: r.project!,
}))
if (!records.length) {
backupError.value = t('welcome.backupEmpty')
return
}
downloadBackup(records)
} catch (err) {
backupError.value =
err instanceof Error ? err.message : String(err)
} finally {
backupBusy.value = false
}
}
/** Pick a .ruwbak file and write every project it contains into
* IDB, preserving ids so the next save overwrites cleanly. The
* existing recents list is NOT cleared — users typically want to
* merge rather than wipe, and we want the opportunity to refuse
* a confusing clobber. */
async function restoreBackup() {
if (backupBusy.value) return
const file = await pickBackupFile()
if (!file) return
backupBusy.value = true
backupError.value = null
try {
const records = await readBackupFile(file)
if (records.length === 0) {
backupError.value = t('welcome.backupEmpty')
return
}
// Check for id collisions so the user knows they'll be
// overwriting existing projects before we start writing.
const existing = await listAllProjects()
const existingIds = new Set(existing.map((r) => r.id))
const overlaps = records.filter((r) => existingIds.has(r.id)).length
if (overlaps > 0) {
const ok = await confirmDialog({
title: t('welcome.restoreConfirmTitle'),
message: t('welcome.restoreConfirmMessage', {
total: records.length,
overlap: overlaps,
}),
confirmLabel: t('welcome.restoreConfirmButton'),
variant: 'danger',
})
if (!ok) return
}
for (const rec of records) {
await saveProjectToStore(rec.project, {
id: rec.id,
name: rec.name,
thumbnail: rec.thumbnail,
})
}
await reload()
} catch (err) {
backupError.value =
err instanceof Error ? err.message : String(err)
} finally {
backupBusy.value = false
}
}
function close() {
emit('update:modelValue', false)
}
@@ -219,6 +312,7 @@ watch(
</li>
</ul>
</div>
<div v-if="backupError" class="backup-error">{{ backupError }}</div>
<template #footer>
<button ref="newBtnRef" class="ftr-btn" @click="startFresh">
<PhPlus :size="13" weight="bold" />
@@ -228,6 +322,25 @@ watch(
<PhFileArrowUp :size="13" />
{{ t('welcome.importFile') }}
</button>
<span class="ftr-sep"></span>
<button
class="ftr-btn"
:disabled="backupBusy"
v-tooltip="t('welcome.backupAllHelp')"
@click="backupAll"
>
<PhArchive :size="13" />
{{ t('welcome.backupAll') }}
</button>
<button
class="ftr-btn"
:disabled="backupBusy"
v-tooltip="t('welcome.restoreBackupHelp')"
@click="restoreBackup"
>
<PhArrowsClockwise :size="13" />
{{ t('welcome.restoreBackup') }}
</button>
</template>
</Modal>
</template>
@@ -389,4 +502,22 @@ watch(
border-color: var(--accent);
box-shadow: 0 0 0 2px var(--accent-soft);
}
.ftr-btn:disabled {
opacity: 0.5;
cursor: default;
}
.ftr-sep {
flex: 1;
}
.backup-error {
margin: 0 12px 8px;
padding: 8px 10px;
font-size: 11px;
background: var(--danger-soft);
color: var(--danger);
border-radius: var(--radius-sm);
}
</style>