feat: auto-backup UI and window close hook
This commit is contained in:
17
src/App.vue
17
src/App.vue
@@ -188,6 +188,23 @@ onMounted(async () => {
|
|||||||
|
|
||||||
registerShortcuts()
|
registerShortcuts()
|
||||||
|
|
||||||
|
// Auto-backup on window close
|
||||||
|
try {
|
||||||
|
const { getCurrentWindow } = await import('@tauri-apps/api/window')
|
||||||
|
const win = getCurrentWindow()
|
||||||
|
win.onCloseRequested(async () => {
|
||||||
|
if (settingsStore.settings.auto_backup === 'true' && settingsStore.settings.backup_path) {
|
||||||
|
try {
|
||||||
|
await invoke('auto_backup', { backupDir: settingsStore.settings.backup_path })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Auto-backup failed:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to register close handler:', e)
|
||||||
|
}
|
||||||
|
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||||
if (settingsStore.settings.theme_mode === 'system') applyTheme()
|
if (settingsStore.settings.theme_mode === 'system') applyTheme()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1198,6 +1198,45 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Last exported -->
|
||||||
|
<div v-if="lastExported" class="flex items-center justify-between" role="status">
|
||||||
|
<div>
|
||||||
|
<p class="text-[0.8125rem] text-text-primary">Last exported</p>
|
||||||
|
<p class="text-[0.6875rem] text-text-tertiary mt-0.5">{{ lastExportedFormatted }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-border-subtle" />
|
||||||
|
|
||||||
|
<!-- Auto-backup on close -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-[0.8125rem] text-text-primary">Auto-backup on close</p>
|
||||||
|
<p class="text-[0.6875rem] text-text-tertiary mt-0.5">Save a backup when the app closes</p>
|
||||||
|
</div>
|
||||||
|
<button @click="toggleAutoBackup" role="switch" :aria-checked="autoBackupEnabled" aria-label="Auto-backup on close"
|
||||||
|
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
|
||||||
|
:class="autoBackupEnabled ? 'bg-status-running' : 'bg-bg-elevated'">
|
||||||
|
<span :class="['inline-block h-3.5 w-3.5 transform rounded-full bg-text-primary transition-transform duration-150',
|
||||||
|
autoBackupEnabled ? 'translate-x-[18px]' : 'translate-x-[3px]']" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Backup path -->
|
||||||
|
<div v-if="autoBackupEnabled" class="flex items-center justify-between pl-4 border-l-2 border-border-subtle ml-1">
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="text-[0.8125rem] text-text-primary">Backup directory</p>
|
||||||
|
<p class="text-[0.6875rem] text-text-tertiary mt-0.5 truncate">{{ backupPath || 'Not set' }}</p>
|
||||||
|
</div>
|
||||||
|
<button @click="chooseBackupPath"
|
||||||
|
class="px-3 py-1.5 text-[0.75rem] border border-border-subtle text-text-secondary rounded-lg hover:bg-bg-elevated transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
|
||||||
|
aria-label="Choose backup directory">
|
||||||
|
{{ backupPath ? 'Change' : 'Choose' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-border-subtle" />
|
||||||
|
|
||||||
<!-- Import Data -->
|
<!-- Import Data -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h3 class="text-[0.8125rem] font-medium text-text-primary mb-4">Import Data</h3>
|
<h3 class="text-[0.8125rem] font-medium text-text-primary mb-4">Import Data</h3>
|
||||||
@@ -1685,6 +1724,34 @@ watch(showClearDataDialog, (val) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Auto-backup state
|
||||||
|
const autoBackupEnabled = ref(false)
|
||||||
|
const backupPath = ref('')
|
||||||
|
const lastExported = ref('')
|
||||||
|
|
||||||
|
const lastExportedFormatted = computed(() => {
|
||||||
|
if (!lastExported.value) return ''
|
||||||
|
return new Date(lastExported.value).toLocaleString()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function toggleAutoBackup() {
|
||||||
|
autoBackupEnabled.value = !autoBackupEnabled.value
|
||||||
|
await settingsStore.updateSetting('auto_backup', autoBackupEnabled.value ? 'true' : 'false')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function chooseBackupPath() {
|
||||||
|
try {
|
||||||
|
const { open } = await import('@tauri-apps/plugin-dialog')
|
||||||
|
const selected = await open({ directory: true, title: 'Choose backup directory' })
|
||||||
|
if (selected && typeof selected === 'string') {
|
||||||
|
backupPath.value = selected
|
||||||
|
await settingsStore.updateSetting('backup_path', selected)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// User cancelled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Import state
|
// Import state
|
||||||
const importFormat = ref('toggl')
|
const importFormat = ref('toggl')
|
||||||
const importFileContent = ref('')
|
const importFileContent = ref('')
|
||||||
@@ -2103,6 +2170,10 @@ async function exportData() {
|
|||||||
a.download = `zeroclock-export-${new Date().toISOString().split('T')[0]}.json`
|
a.download = `zeroclock-export-${new Date().toISOString().split('T')[0]}.json`
|
||||||
a.click()
|
a.click()
|
||||||
URL.revokeObjectURL(url)
|
URL.revokeObjectURL(url)
|
||||||
|
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
await settingsStore.updateSetting('last_exported', now)
|
||||||
|
lastExported.value = now
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to export data:', error)
|
console.error('Failed to export data:', error)
|
||||||
toastStore.error('Failed to export data')
|
toastStore.error('Failed to export data')
|
||||||
@@ -2158,6 +2229,11 @@ onMounted(async () => {
|
|||||||
// Notification settings
|
// Notification settings
|
||||||
persistentNotifications.value = settingsStore.settings.persistent_notifications === 'true'
|
persistentNotifications.value = settingsStore.settings.persistent_notifications === 'true'
|
||||||
|
|
||||||
|
// Auto-backup settings
|
||||||
|
autoBackupEnabled.value = settingsStore.settings.auto_backup === 'true'
|
||||||
|
backupPath.value = settingsStore.settings.backup_path || ''
|
||||||
|
lastExported.value = settingsStore.settings.last_exported || ''
|
||||||
|
|
||||||
// Sound settings
|
// Sound settings
|
||||||
soundEnabled.value = settingsStore.settings.sound_enabled === 'true'
|
soundEnabled.value = settingsStore.settings.sound_enabled === 'true'
|
||||||
soundMode.value = settingsStore.settings.sound_mode || 'synthesized'
|
soundMode.value = settingsStore.settings.sound_mode || 'synthesized'
|
||||||
|
|||||||
Reference in New Issue
Block a user