1.1.1: fullbak
This commit is contained in:
1 parent
d59201100b
commit
01c13981c4
6 files changed
+443
No files matched your search
@@ -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>
|
||||
Reference in new issue
Block a user