feat: persistent notifications toggle in settings
This commit is contained in:
227
src/App.vue
227
src/App.vue
@@ -1,16 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, watch } from 'vue'
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import TitleBar from './components/TitleBar.vue'
|
||||
import NavRail from './components/NavRail.vue'
|
||||
import MiniTimer from './views/MiniTimer.vue'
|
||||
import ToastNotification from './components/ToastNotification.vue'
|
||||
import { useSettingsStore } from './stores/settings'
|
||||
import { useToastStore } from './stores/toast'
|
||||
import { useTimerStore } from './stores/timer'
|
||||
import { useRecurringStore } from './stores/recurring'
|
||||
import { loadAndApplyTimerFont } from './utils/fonts'
|
||||
import { loadAndApplyUIFont } from './utils/uiFonts'
|
||||
import { useAnnouncer } from './composables/useAnnouncer'
|
||||
import { audioEngine, DEFAULT_EVENTS } from './utils/audio'
|
||||
import type { SoundEvent } from './utils/audio'
|
||||
import TourOverlay from './components/TourOverlay.vue'
|
||||
import RecurringPromptDialog from './components/RecurringPromptDialog.vue'
|
||||
import { useOnboardingStore } from './stores/onboarding'
|
||||
import { useProjectsStore } from './stores/projects'
|
||||
import { useInvoicesStore } from './stores/invoices'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
// Detect mini timer by Tauri window label — no routing needed
|
||||
const isMiniTimer = getCurrentWindow().label === 'mini-timer'
|
||||
const recurringStore = useRecurringStore()
|
||||
const { announcement } = useAnnouncer()
|
||||
|
||||
function getProjectName(projectId?: number): string {
|
||||
if (!projectId) return ''
|
||||
const projectsStore = useProjectsStore()
|
||||
return projectsStore.projects.find(p => p.id === projectId)?.name || ''
|
||||
}
|
||||
|
||||
function getProjectColor(projectId?: number): string {
|
||||
if (!projectId) return '#6B7280'
|
||||
const projectsStore = useProjectsStore()
|
||||
return projectsStore.projects.find(p => p.id === projectId)?.color || '#6B7280'
|
||||
}
|
||||
|
||||
async function registerShortcuts() {
|
||||
try {
|
||||
@@ -53,51 +76,205 @@ function applyTheme() {
|
||||
el.setAttribute('data-accent', accent)
|
||||
}
|
||||
|
||||
function applyMotion() {
|
||||
const setting = settingsStore.settings.reduce_motion || 'system'
|
||||
const el = document.documentElement
|
||||
if (setting === 'on') {
|
||||
el.classList.add('reduce-motion')
|
||||
} else if (setting === 'off') {
|
||||
el.classList.remove('reduce-motion')
|
||||
} else {
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
el.classList.add('reduce-motion')
|
||||
} else {
|
||||
el.classList.remove('reduce-motion')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await settingsStore.fetchSettings()
|
||||
|
||||
const onboardingStore = useOnboardingStore()
|
||||
await onboardingStore.load()
|
||||
|
||||
const timerStore = useTimerStore()
|
||||
await timerStore.restoreState()
|
||||
|
||||
const zoom = parseInt(settingsStore.settings.ui_zoom) || 100
|
||||
const app = document.getElementById('app')
|
||||
if (app) {
|
||||
(app.style as any).zoom = `${zoom}%`
|
||||
}
|
||||
applyTheme()
|
||||
applyMotion()
|
||||
loadAndApplyTimerFont(settingsStore.settings.timer_font || 'JetBrains Mono')
|
||||
|
||||
const dyslexiaMode = settingsStore.settings.dyslexia_mode === 'true'
|
||||
if (dyslexiaMode) {
|
||||
loadAndApplyUIFont('OpenDyslexic')
|
||||
} else {
|
||||
const uiFont = settingsStore.settings.ui_font
|
||||
if (uiFont && uiFont !== 'Inter') {
|
||||
loadAndApplyUIFont(uiFont)
|
||||
}
|
||||
}
|
||||
|
||||
// Load audio settings
|
||||
const soundEnabled = settingsStore.settings.sound_enabled === 'true'
|
||||
const soundMode = (settingsStore.settings.sound_mode || 'synthesized') as 'synthesized' | 'system' | 'custom'
|
||||
const soundVolume = parseInt(settingsStore.settings.sound_volume) || 70
|
||||
let soundEvents: Record<string, boolean> = {}
|
||||
try {
|
||||
soundEvents = JSON.parse(settingsStore.settings.sound_events || '{}')
|
||||
} catch { /* use defaults */ }
|
||||
|
||||
audioEngine.updateSettings({
|
||||
enabled: soundEnabled,
|
||||
mode: soundMode,
|
||||
volume: soundVolume,
|
||||
events: { ...DEFAULT_EVENTS, ...soundEvents } as Record<SoundEvent, boolean>,
|
||||
})
|
||||
|
||||
// Initialize persistent notifications setting
|
||||
const toastStore = useToastStore()
|
||||
toastStore.setPersistentNotifications(settingsStore.settings.persistent_notifications === 'true')
|
||||
|
||||
await recurringStore.fetchEntries()
|
||||
recurringStore.checkRecurrences()
|
||||
setInterval(() => recurringStore.checkRecurrences(), 60000)
|
||||
|
||||
// Background calendar sync
|
||||
async function syncCalendars() {
|
||||
try {
|
||||
const sources = await invoke<any[]>('get_calendar_sources')
|
||||
for (const source of sources) {
|
||||
if (source.source_type === 'url' && source.enabled && source.url) {
|
||||
try {
|
||||
const resp = await fetch(source.url)
|
||||
if (resp.ok) {
|
||||
const content = await resp.text()
|
||||
await invoke('import_ics_file', { sourceId: source.id, content })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Calendar sync failed for', source.name, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to sync calendars:', e)
|
||||
}
|
||||
}
|
||||
|
||||
syncCalendars()
|
||||
setInterval(syncCalendars, 30 * 60000)
|
||||
|
||||
const invoicesStore = useInvoicesStore()
|
||||
await invoicesStore.fetchInvoices()
|
||||
await invoicesStore.checkOverdue()
|
||||
|
||||
registerShortcuts()
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||
if (settingsStore.settings.theme_mode === 'system') applyTheme()
|
||||
})
|
||||
|
||||
window.matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', () => {
|
||||
if (settingsStore.settings.reduce_motion === 'system' || !settingsStore.settings.reduce_motion) {
|
||||
applyMotion()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
watch(() => [settingsStore.settings.theme_mode, settingsStore.settings.accent_color], () => {
|
||||
applyTheme()
|
||||
})
|
||||
|
||||
watch(() => settingsStore.settings.reduce_motion, () => {
|
||||
applyMotion()
|
||||
})
|
||||
|
||||
watch(() => settingsStore.settings.timer_font, (newFont) => {
|
||||
if (newFont) loadAndApplyTimerFont(newFont)
|
||||
})
|
||||
|
||||
watch(() => settingsStore.settings.dyslexia_mode, (val) => {
|
||||
if (val === 'true') {
|
||||
loadAndApplyUIFont('OpenDyslexic')
|
||||
} else {
|
||||
const uiFont = settingsStore.settings.ui_font || 'Inter'
|
||||
loadAndApplyUIFont(uiFont)
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => settingsStore.settings.ui_font, (newFont) => {
|
||||
if (settingsStore.settings.dyslexia_mode !== 'true' && newFont) {
|
||||
loadAndApplyUIFont(newFont)
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => [settingsStore.settings.shortcut_toggle_timer, settingsStore.settings.shortcut_show_app], () => {
|
||||
registerShortcuts()
|
||||
})
|
||||
|
||||
watch(() => settingsStore.settings.sound_enabled, (val) => {
|
||||
audioEngine.updateSettings({ enabled: val === 'true' })
|
||||
})
|
||||
|
||||
watch(() => settingsStore.settings.sound_mode, (val) => {
|
||||
if (val) audioEngine.updateSettings({ mode: val as 'synthesized' | 'system' | 'custom' })
|
||||
})
|
||||
|
||||
watch(() => settingsStore.settings.sound_volume, (val) => {
|
||||
audioEngine.updateSettings({ volume: parseInt(val) || 70 })
|
||||
})
|
||||
|
||||
watch(() => settingsStore.settings.sound_events, (val) => {
|
||||
if (val) {
|
||||
try {
|
||||
const parsed = JSON.parse(val)
|
||||
audioEngine.updateSettings({ events: { ...DEFAULT_EVENTS, ...parsed } as Record<SoundEvent, boolean> })
|
||||
} catch { /* ignore parse errors */ }
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => settingsStore.settings.persistent_notifications, (val) => {
|
||||
const toastStore = useToastStore()
|
||||
toastStore.setPersistentNotifications(val === 'true')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Mini timer: detected by window label, rendered directly (no routing) -->
|
||||
<div v-if="isMiniTimer" class="h-full w-full bg-bg-base">
|
||||
<MiniTimer />
|
||||
</div>
|
||||
|
||||
<!-- Normal app shell -->
|
||||
<template v-else>
|
||||
<div class="h-full w-full flex flex-col bg-bg-base">
|
||||
<TitleBar />
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
<NavRail />
|
||||
<main class="flex-1 overflow-auto">
|
||||
<router-view v-slot="{ Component }">
|
||||
<Transition name="page" mode="out-in" :duration="{ enter: 250, leave: 150 }">
|
||||
<component :is="Component" :key="$route.path" />
|
||||
</Transition>
|
||||
</router-view>
|
||||
</main>
|
||||
</div>
|
||||
<a href="#main-content" class="sr-only sr-only-focusable fixed top-0 left-0 z-[200] bg-accent text-white px-4 py-2 rounded-br-lg">
|
||||
Skip to main content
|
||||
</a>
|
||||
<div class="h-full w-full flex flex-col bg-bg-base">
|
||||
<TitleBar />
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
<NavRail />
|
||||
<main id="main-content" class="flex-1 overflow-auto" tabindex="-1">
|
||||
<router-view v-slot="{ Component }">
|
||||
<Transition name="page" mode="out-in" :duration="{ enter: 250, leave: 150 }">
|
||||
<component :is="Component" :key="$route.path" />
|
||||
</Transition>
|
||||
</router-view>
|
||||
</main>
|
||||
</div>
|
||||
<ToastNotification />
|
||||
</template>
|
||||
</div>
|
||||
<ToastNotification />
|
||||
<RecurringPromptDialog
|
||||
:show="recurringStore.pendingPrompt !== null"
|
||||
:project-name="getProjectName(recurringStore.pendingPrompt?.project_id)"
|
||||
:project-color="getProjectColor(recurringStore.pendingPrompt?.project_id)"
|
||||
:task-name="''"
|
||||
:description="recurringStore.pendingPrompt?.description || ''"
|
||||
:duration="recurringStore.pendingPrompt?.duration || 0"
|
||||
:time-of-day="recurringStore.pendingPrompt?.time_of_day || ''"
|
||||
@confirm="recurringStore.confirmPrompt()"
|
||||
@snooze="recurringStore.snoozePrompt()"
|
||||
@skip="recurringStore.skipPrompt()"
|
||||
/>
|
||||
<div id="route-announcer" class="sr-only" aria-live="polite" aria-atomic="true"></div>
|
||||
<div id="announcer" class="sr-only" aria-live="assertive" aria-atomic="true">{{ announcement }}</div>
|
||||
<TourOverlay />
|
||||
</template>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user