Polish the migration

This commit is contained in:
Zerebos
2026-05-23 21:03:22 -04:00
parent b3fca70f27
commit 5e2114810e
12 changed files with 767 additions and 60 deletions
+71
View File
@@ -1,9 +1,53 @@
<script lang="ts">
import pkg from '../../../../package.json'
import { appState } from '$lib/state/app.svelte'
import { settingsState } from '$lib/state/settings.svelte'
import { trackingState } from '$lib/state/tracking.svelte'
import { checkForAppUpdate, installAppUpdate, isSupported } from '$lib/platform-service'
import type { AppUpdateInfo } from '$lib/platform-adapters/types'
const appVersion = pkg.version as string
let updateInfo = $state<AppUpdateInfo | null>(null)
let updateChecking = $state(false)
let updateInstalling = $state(false)
let updateError = $state<string | null>(null)
let updateDone = $state(false)
const canCheckUpdates = typeof window !== 'undefined' && (() => {
try { return isSupported('app-updates') } catch { return false }
})()
async function handleCheckUpdate() {
updateChecking = true
updateError = null
updateInfo = null
updateDone = false
try {
updateInfo = await checkForAppUpdate()
} catch (error: unknown) {
updateError = error instanceof Error ? error.message : String(error)
} finally {
updateChecking = false
}
}
async function handleInstallUpdate() {
if (!updateInfo) return
updateInstalling = true
updateError = null
try {
await installAppUpdate()
updateDone = true
} catch (error: unknown) {
updateError = error instanceof Error ? error.message : String(error)
} finally {
updateInstalling = false
}
}
</script>
<svelte:head>
@@ -23,8 +67,35 @@
<div class="settings-label">Moku</div>
<div class="settings-desc">Version {appVersion}</div>
</div>
{#if canCheckUpdates}
<button class="settings-button" type="button" onclick={handleCheckUpdate} disabled={updateChecking}>
{updateChecking ? 'Checking…' : 'Check for updates'}
</button>
{/if}
</div>
{#if updateInfo}
<div class="settings-row">
<div>
<div class="settings-label">Update available</div>
<div class="settings-desc">v{updateInfo.version}</div>
</div>
<button class="settings-button" type="button" onclick={handleInstallUpdate} disabled={updateInstalling}>
{updateInstalling ? 'Installing…' : 'Install now'}
</button>
</div>
{:else if updateChecking === false && updateError === null && updateInfo === null && updateDone === false && canCheckUpdates}
<!-- idle, no explicit "up to date" message unless user just clicked -->
{/if}
{#if updateDone}
<p class="settings-feedback-ok">Update installed — please restart Moku.</p>
{/if}
{#if updateError}
<p class="settings-feedback-error">{updateError}</p>
{/if}
<div class="settings-row settings-grid-2">
<div>
<div class="settings-label">Server URL</div>
+120
View File
@@ -1,5 +1,100 @@
<script lang="ts">
import { appState } from '$lib/state/app.svelte'
import { settingsState, updateSettings } from '$lib/state/settings.svelte'
import { historyState } from '$lib/state/history.svelte'
import {
buildAppDataBackup,
downloadAppDataBackup,
parseAppDataBackup,
pickAppDataBackupFile,
} from '$lib/core/backup'
import { isSupported } from '$lib/platform-service'
import { savePersistentState } from '$lib/core/persistence/persist'
let exportBusy = $state(false)
let importBusy = $state(false)
let backupError = $state<string | null>(null)
let backupMsg = $state<string | null>(null)
const isTauri = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window
async function handleExport() {
exportBusy = true
backupError = null
backupMsg = null
try {
if (isTauri && isSupported('filesystem')) {
// Tauri-native export via invoke not yet wired — fall through to web path
const backup = buildAppDataBackup(settingsState, {
history: historyState.history,
bookmarks: historyState.bookmarks,
markers: historyState.markers,
readLog: historyState.readLog,
readingStats: historyState.readingStats as unknown as Record<string, unknown>,
dailyReadCounts: historyState.dailyReadCounts,
})
downloadAppDataBackup(backup)
backupMsg = 'Backup downloaded.'
} else {
const backup = buildAppDataBackup(settingsState, {
history: historyState.history,
bookmarks: historyState.bookmarks,
markers: historyState.markers,
readLog: historyState.readLog,
readingStats: historyState.readingStats as unknown as Record<string, unknown>,
dailyReadCounts: historyState.dailyReadCounts,
})
downloadAppDataBackup(backup)
backupMsg = 'Backup downloaded.'
}
setTimeout(() => (backupMsg = null), 3000)
} catch (error: unknown) {
if (String(error).includes('Cancelled') || String(error).includes('AbortError')) {
// user cancelled
} else {
backupError = error instanceof Error ? error.message : String(error)
}
} finally {
exportBusy = false
}
}
async function handleImport() {
importBusy = true
backupError = null
backupMsg = null
try {
// Tauri-native import handled below — same web path works
const file = await pickAppDataBackupFile()
if (!file) return
const text = await file.text()
const backup = parseAppDataBackup(text)
await Promise.all([
savePersistentState('settings', {
settings: backup.settings,
storeVersion: 1,
}),
savePersistentState('history', backup.history),
])
backupMsg = 'Import complete — reloading in 3 seconds…'
setTimeout(() => window.location.reload(), 3000)
} catch (error: unknown) {
if (String(error).includes('Cancelled') || String(error).includes('AbortError')) {
// user cancelled
} else {
backupError = error instanceof Error ? error.message : String(error)
}
} finally {
importBusy = false
}
}
</script>
<svelte:head>
@@ -34,4 +129,29 @@
</label>
</div>
</div>
<div class="settings-card">
<div class="settings-row">
<div>
<div class="settings-label">App data backup</div>
<div class="settings-desc">Export or import Moku settings and reading history.</div>
</div>
<div class="settings-inline-control">
<button class="settings-button" type="button" onclick={handleExport} disabled={exportBusy}>
{exportBusy ? 'Exporting…' : 'Export backup'}
</button>
<button class="settings-button" type="button" onclick={handleImport} disabled={importBusy}>
{importBusy ? 'Importing…' : 'Import backup'}
</button>
</div>
</div>
{#if backupMsg}
<p class="settings-feedback-ok">{backupMsg}</p>
{/if}
{#if backupError}
<p class="settings-feedback-error">{backupError}</p>
{/if}
</div>
</section>