feat: tooltips, two-column timer, font selector, tray behavior, icons, readme
- Custom tooltip directive (WCAG AAA) on every button in the app - Two-column timer layout with sticky hero and recent entries sidebar - Timer font selector with 16 monospace Google Fonts and live preview - UI font selector with 15+ Google Fonts - Close-to-tray and minimize-to-tray settings - New app icons (no-glow variants), platform icon set - Mini timer pop-out window - Favorites strip with drag-reorder and inline actions - Comprehensive README with feature documentation - Remove tracked files that belong in gitignore
This commit is contained in:
133
src/App.vue
133
src/App.vue
@@ -17,6 +17,8 @@ import TourOverlay from './components/TourOverlay.vue'
|
||||
import RecurringPromptDialog from './components/RecurringPromptDialog.vue'
|
||||
import TimerSaveDialog from './components/TimerSaveDialog.vue'
|
||||
import QuickEntryDialog from './components/QuickEntryDialog.vue'
|
||||
import KeyboardShortcutsDialog from './components/KeyboardShortcutsDialog.vue'
|
||||
import GlobalSearchDialog from './components/GlobalSearchDialog.vue'
|
||||
import { useOnboardingStore } from './stores/onboarding'
|
||||
import { useProjectsStore } from './stores/projects'
|
||||
import { useInvoicesStore } from './stores/invoices'
|
||||
@@ -26,6 +28,8 @@ const recurringStore = useRecurringStore()
|
||||
const timerStore = useTimerStore()
|
||||
const { announcement } = useAnnouncer()
|
||||
const showQuickEntry = ref(false)
|
||||
const showShortcuts = ref(false)
|
||||
const showSearch = ref(false)
|
||||
|
||||
function getProjectName(projectId?: number): string {
|
||||
if (!projectId) return ''
|
||||
@@ -39,7 +43,10 @@ function getProjectColor(projectId?: number): string {
|
||||
return projectsStore.projects.find(p => p.id === projectId)?.color || '#6B7280'
|
||||
}
|
||||
|
||||
let shortcutRegistering = false
|
||||
async function registerShortcuts() {
|
||||
if (shortcutRegistering) return
|
||||
shortcutRegistering = true
|
||||
try {
|
||||
const { unregisterAll, register } = await import('@tauri-apps/plugin-global-shortcut')
|
||||
await unregisterAll()
|
||||
@@ -72,6 +79,8 @@ async function registerShortcuts() {
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Failed to register shortcuts:', e)
|
||||
} finally {
|
||||
shortcutRegistering = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +98,39 @@ function applyTheme() {
|
||||
el.setAttribute('data-accent', accent)
|
||||
}
|
||||
|
||||
function daysDiff(a: string, b: string): number {
|
||||
const ms = new Date(b).getTime() - new Date(a).getTime()
|
||||
return Math.floor(ms / 86400000)
|
||||
}
|
||||
|
||||
async function checkScheduledBackup() {
|
||||
const s = settingsStore.settings
|
||||
if (s.auto_backup !== 'true' || !s.backup_path) return
|
||||
const lastBackup = s.auto_backup_last || ''
|
||||
const frequency = s.auto_backup_frequency || 'daily'
|
||||
const retention = parseInt(s.auto_backup_retention || '7')
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
|
||||
const isDue = !lastBackup || (frequency === 'daily' && lastBackup < today) ||
|
||||
(frequency === 'weekly' && daysDiff(lastBackup, today) >= 7)
|
||||
if (!isDue) return
|
||||
|
||||
try {
|
||||
await invoke('auto_backup', { backupDir: s.backup_path })
|
||||
await settingsStore.updateSetting('auto_backup_last', today)
|
||||
const toastStore = useToastStore()
|
||||
const files = await invoke<any[]>('list_backup_files', { backupDir: s.backup_path })
|
||||
if (files.length > retention) {
|
||||
for (const old of files.slice(retention)) {
|
||||
await invoke('delete_backup_file', { path: old.path })
|
||||
}
|
||||
}
|
||||
toastStore.success('Auto-backup completed')
|
||||
} catch (e) {
|
||||
console.error('Scheduled backup failed:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function applyMotion() {
|
||||
const setting = settingsStore.settings.reduce_motion || 'system'
|
||||
const el = document.documentElement
|
||||
@@ -184,15 +226,94 @@ onMounted(async () => {
|
||||
|
||||
const invoicesStore = useInvoicesStore()
|
||||
await invoicesStore.fetchInvoices()
|
||||
await invoicesStore.checkOverdue()
|
||||
const overdueCount = await invoicesStore.checkOverdue()
|
||||
if (overdueCount > 0) {
|
||||
const toastStore = useToastStore()
|
||||
toastStore.info(`${overdueCount} invoice(s) now overdue`)
|
||||
}
|
||||
|
||||
await checkScheduledBackup()
|
||||
|
||||
// End-of-day reminder and weekly summary checks
|
||||
const reminderState = { eodShownToday: '', weeklySummaryShownWeek: '' }
|
||||
|
||||
async function checkReminders() {
|
||||
const now = new Date()
|
||||
const todayStr = now.toISOString().split('T')[0]
|
||||
const currentTime = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
|
||||
|
||||
// End-of-day reminder
|
||||
if (settingsStore.settings.eod_reminder_enabled === 'true' && reminderState.eodShownToday !== todayStr) {
|
||||
const reminderTime = settingsStore.settings.eod_reminder_time || '17:00'
|
||||
if (currentTime >= reminderTime) {
|
||||
reminderState.eodShownToday = todayStr
|
||||
try {
|
||||
const entries = await invoke<any[]>('get_time_entries', { startDate: todayStr, endDate: todayStr })
|
||||
const totalSeconds = entries.reduce((sum: number, e: any) => sum + (e.duration || 0), 0)
|
||||
const totalHours = totalSeconds / 3600
|
||||
const goalHours = parseFloat(settingsStore.settings.daily_goal_hours) || 8
|
||||
if (totalHours < goalHours) {
|
||||
const remaining = (goalHours - totalHours).toFixed(1)
|
||||
const toastStore = useToastStore()
|
||||
toastStore.info(`End of day: ${totalHours.toFixed(1)}h logged today, ${remaining}h remaining to reach your ${goalHours}h goal`)
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Weekly summary (Monday check)
|
||||
if (settingsStore.settings.weekly_summary_enabled === 'true' && now.getDay() === 1) {
|
||||
const weekId = todayStr
|
||||
if (reminderState.weeklySummaryShownWeek !== weekId && now.getHours() >= 9) {
|
||||
reminderState.weeklySummaryShownWeek = weekId
|
||||
try {
|
||||
const lastMonday = new Date(now)
|
||||
lastMonday.setDate(now.getDate() - 7)
|
||||
const lastSunday = new Date(now)
|
||||
lastSunday.setDate(now.getDate() - 1)
|
||||
const entries = await invoke<any[]>('get_time_entries', {
|
||||
startDate: lastMonday.toISOString().split('T')[0],
|
||||
endDate: lastSunday.toISOString().split('T')[0],
|
||||
})
|
||||
const totalSeconds = entries.reduce((sum: number, e: any) => sum + (e.duration || 0), 0)
|
||||
const totalHours = totalSeconds / 3600
|
||||
const goalHours = parseFloat(settingsStore.settings.weekly_goal_hours) || 40
|
||||
const toastStore = useToastStore()
|
||||
toastStore.info(`Weekly summary: ${totalHours.toFixed(1)}h logged last week (goal: ${goalHours}h)`)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkReminders()
|
||||
setInterval(checkReminders, 60000)
|
||||
|
||||
registerShortcuts()
|
||||
|
||||
// Auto-backup on window close
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||||
e.preventDefault()
|
||||
showSearch.value = true
|
||||
return
|
||||
}
|
||||
if (e.key === '?' && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
||||
const tag = (e.target as HTMLElement)?.tagName
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return
|
||||
if ((e.target as HTMLElement)?.isContentEditable) return
|
||||
e.preventDefault()
|
||||
showShortcuts.value = !showShortcuts.value
|
||||
}
|
||||
})
|
||||
|
||||
// Handle window close - backup and optionally hide to tray
|
||||
try {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window')
|
||||
const win = getCurrentWindow()
|
||||
win.onCloseRequested(async () => {
|
||||
win.onCloseRequested(async (event) => {
|
||||
if (settingsStore.settings.auto_backup === 'true' && settingsStore.settings.backup_path) {
|
||||
try {
|
||||
await invoke('auto_backup', { backupDir: settingsStore.settings.backup_path })
|
||||
@@ -200,6 +321,10 @@ onMounted(async () => {
|
||||
console.error('Auto-backup failed:', e)
|
||||
}
|
||||
}
|
||||
if (settingsStore.settings.close_to_tray === 'true') {
|
||||
event.preventDefault()
|
||||
await win.hide()
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Failed to register close handler:', e)
|
||||
@@ -320,4 +445,6 @@ watch(() => settingsStore.settings.persistent_notifications, (val) => {
|
||||
<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 />
|
||||
<KeyboardShortcutsDialog :show="showShortcuts" @close="showShortcuts = false" />
|
||||
<GlobalSearchDialog :show="showSearch" @close="showSearch = false" />
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user