feat: add global keyboard shortcuts for timer toggle and show app
Register CmdOrCtrl+Shift+T (toggle timer) and CmdOrCtrl+Shift+Z (show app) via tauri-plugin-global-shortcut. Shortcut keys are configurable in Settings Timer tab. Shortcuts re-register on change.
This commit is contained in:
33
src/App.vue
33
src/App.vue
@@ -4,9 +4,37 @@ import TitleBar from './components/TitleBar.vue'
|
|||||||
import NavRail from './components/NavRail.vue'
|
import NavRail from './components/NavRail.vue'
|
||||||
import ToastNotification from './components/ToastNotification.vue'
|
import ToastNotification from './components/ToastNotification.vue'
|
||||||
import { useSettingsStore } from './stores/settings'
|
import { useSettingsStore } from './stores/settings'
|
||||||
|
import { useTimerStore } from './stores/timer'
|
||||||
|
|
||||||
const settingsStore = useSettingsStore()
|
const settingsStore = useSettingsStore()
|
||||||
|
|
||||||
|
async function registerShortcuts() {
|
||||||
|
try {
|
||||||
|
const { unregisterAll, register } = await import('@tauri-apps/plugin-global-shortcut')
|
||||||
|
await unregisterAll()
|
||||||
|
const toggleKey = settingsStore.settings.shortcut_toggle_timer || 'CmdOrCtrl+Shift+T'
|
||||||
|
const showKey = settingsStore.settings.shortcut_show_app || 'CmdOrCtrl+Shift+Z'
|
||||||
|
|
||||||
|
await register(toggleKey, () => {
|
||||||
|
const timerStore = useTimerStore()
|
||||||
|
if (timerStore.isStopped) {
|
||||||
|
if (timerStore.selectedProjectId) timerStore.start()
|
||||||
|
} else {
|
||||||
|
timerStore.stop()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await register(showKey, async () => {
|
||||||
|
const { getCurrentWindow } = await import('@tauri-apps/api/window')
|
||||||
|
const win = getCurrentWindow()
|
||||||
|
await win.show()
|
||||||
|
await win.setFocus()
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to register shortcuts:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function applyTheme() {
|
function applyTheme() {
|
||||||
const el = document.documentElement
|
const el = document.documentElement
|
||||||
const mode = settingsStore.settings.theme_mode || 'dark'
|
const mode = settingsStore.settings.theme_mode || 'dark'
|
||||||
@@ -29,6 +57,7 @@ onMounted(async () => {
|
|||||||
(app.style as any).zoom = `${zoom}%`
|
(app.style as any).zoom = `${zoom}%`
|
||||||
}
|
}
|
||||||
applyTheme()
|
applyTheme()
|
||||||
|
registerShortcuts()
|
||||||
|
|
||||||
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()
|
||||||
@@ -38,6 +67,10 @@ onMounted(async () => {
|
|||||||
watch(() => [settingsStore.settings.theme_mode, settingsStore.settings.accent_color], () => {
|
watch(() => [settingsStore.settings.theme_mode, settingsStore.settings.accent_color], () => {
|
||||||
applyTheme()
|
applyTheme()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(() => [settingsStore.settings.shortcut_toggle_timer, settingsStore.settings.shortcut_show_app], () => {
|
||||||
|
registerShortcuts()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -230,6 +230,40 @@
|
|||||||
@update:model-value="saveSettings"
|
@update:model-value="saveSettings"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<div class="border-t border-border-subtle" />
|
||||||
|
|
||||||
|
<!-- Keyboard Shortcuts -->
|
||||||
|
<h3 class="text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] font-medium">Keyboard Shortcuts</h3>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-[0.8125rem] text-text-primary">Toggle Timer</p>
|
||||||
|
<p class="text-[0.6875rem] text-text-tertiary mt-0.5">Start/stop the active timer</p>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-model="shortcutToggleTimer"
|
||||||
|
type="text"
|
||||||
|
class="w-48 px-3 py-1.5 bg-bg-inset border border-border-subtle rounded-lg text-[0.75rem] text-text-primary font-mono text-center focus:outline-none focus:border-border-visible"
|
||||||
|
placeholder="CmdOrCtrl+Shift+T"
|
||||||
|
@change="saveShortcutSettings"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-[0.8125rem] text-text-primary">Show App</p>
|
||||||
|
<p class="text-[0.6875rem] text-text-tertiary mt-0.5">Bring the app to front</p>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-model="shortcutShowApp"
|
||||||
|
type="text"
|
||||||
|
class="w-48 px-3 py-1.5 bg-bg-inset border border-border-subtle rounded-lg text-[0.75rem] text-text-primary font-mono text-center focus:outline-none focus:border-border-visible"
|
||||||
|
placeholder="CmdOrCtrl+Shift+Z"
|
||||||
|
@change="saveShortcutSettings"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -272,6 +306,55 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Import Data -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h3 class="text-[0.8125rem] font-medium text-text-primary mb-4">Import Data</h3>
|
||||||
|
|
||||||
|
<div class="flex items-end gap-3 mb-4">
|
||||||
|
<div class="w-48">
|
||||||
|
<label class="block text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1.5">Format</label>
|
||||||
|
<AppSelect
|
||||||
|
v-model="importFormat"
|
||||||
|
:options="importFormats"
|
||||||
|
label-key="label"
|
||||||
|
value-key="value"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="handleImportFile"
|
||||||
|
class="flex items-center gap-2 px-4 py-2 border border-border-visible text-text-primary text-xs rounded-lg hover:bg-bg-elevated transition-colors duration-150"
|
||||||
|
>
|
||||||
|
<Upload class="w-3.5 h-3.5" />
|
||||||
|
Choose File
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="importFileName" class="mb-4">
|
||||||
|
<p class="text-[0.75rem] text-text-secondary mb-2">File: {{ importFileName }}</p>
|
||||||
|
|
||||||
|
<!-- CSV Preview -->
|
||||||
|
<div v-if="importPreview.length > 0" class="overflow-x-auto mb-3">
|
||||||
|
<table class="text-[0.6875rem]">
|
||||||
|
<tr v-for="(row, i) in importPreview" :key="i" :class="i === 0 ? 'text-text-tertiary font-medium' : 'text-text-secondary'">
|
||||||
|
<td v-for="(cell, j) in row" :key="j" class="pr-4 py-0.5 whitespace-nowrap">{{ cell }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="executeImport"
|
||||||
|
:disabled="isImporting"
|
||||||
|
class="px-4 py-2 bg-accent text-bg-base text-xs font-medium rounded-lg hover:bg-accent-hover transition-colors duration-150 disabled:opacity-40"
|
||||||
|
>
|
||||||
|
{{ isImporting ? 'Importing...' : 'Import' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="importStatus" class="text-[0.75rem] mt-2" :class="importStatus.startsWith('Error') ? 'text-status-error' : 'text-status-running'">
|
||||||
|
{{ importStatus }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Danger Zone -->
|
<!-- Danger Zone -->
|
||||||
<div class="mt-8 rounded-xl border border-status-error/20 p-5">
|
<div class="mt-8 rounded-xl border border-status-error/20 p-5">
|
||||||
<h3 class="text-xs text-status-error uppercase tracking-[0.08em] font-medium mb-4">Danger Zone</h3>
|
<h3 class="text-xs text-status-error uppercase tracking-[0.08em] font-medium mb-4">Danger Zone</h3>
|
||||||
@@ -328,12 +411,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, markRaw } from 'vue'
|
import { ref, onMounted, markRaw } from 'vue'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import { Settings as SettingsIcon, Clock, Receipt, Database, Plus, Minus } from 'lucide-vue-next'
|
import { Settings as SettingsIcon, Clock, Receipt, Database, Plus, Minus, Upload } from 'lucide-vue-next'
|
||||||
import { useSettingsStore } from '../stores/settings'
|
import { useSettingsStore } from '../stores/settings'
|
||||||
import { useToastStore } from '../stores/toast'
|
import { useToastStore } from '../stores/toast'
|
||||||
import AppNumberInput from '../components/AppNumberInput.vue'
|
import AppNumberInput from '../components/AppNumberInput.vue'
|
||||||
import AppSelect from '../components/AppSelect.vue'
|
import AppSelect from '../components/AppSelect.vue'
|
||||||
import { LOCALES, getCurrencies, getCurrencySymbol } from '../utils/locale'
|
import { LOCALES, getCurrencies, getCurrencySymbol } from '../utils/locale'
|
||||||
|
import { parseCSV, mapTogglCSV, mapGenericCSV, type ImportEntry } from '../utils/import'
|
||||||
|
|
||||||
const settingsStore = useSettingsStore()
|
const settingsStore = useSettingsStore()
|
||||||
const toastStore = useToastStore()
|
const toastStore = useToastStore()
|
||||||
@@ -361,6 +445,8 @@ const currency = ref('USD')
|
|||||||
const currencyOptions = getCurrencies()
|
const currencyOptions = getCurrencies()
|
||||||
const themeMode = ref('dark')
|
const themeMode = ref('dark')
|
||||||
const accentColor = ref('amber')
|
const accentColor = ref('amber')
|
||||||
|
const shortcutToggleTimer = ref('CmdOrCtrl+Shift+T')
|
||||||
|
const shortcutShowApp = ref('CmdOrCtrl+Shift+Z')
|
||||||
|
|
||||||
const themeModes = [
|
const themeModes = [
|
||||||
{ value: 'dark', label: 'Dark' },
|
{ value: 'dark', label: 'Dark' },
|
||||||
@@ -387,6 +473,21 @@ const appTrackingModes = [
|
|||||||
// Dialog state
|
// Dialog state
|
||||||
const showClearDataDialog = ref(false)
|
const showClearDataDialog = ref(false)
|
||||||
|
|
||||||
|
// Import state
|
||||||
|
const importFormat = ref('toggl')
|
||||||
|
const importFileContent = ref('')
|
||||||
|
const importFileName = ref('')
|
||||||
|
const importPreview = ref<string[][]>([])
|
||||||
|
const importStatus = ref('')
|
||||||
|
const isImporting = ref(false)
|
||||||
|
|
||||||
|
const importFormats = [
|
||||||
|
{ label: 'Toggl CSV', value: 'toggl' },
|
||||||
|
{ label: 'Clockify CSV', value: 'clockify' },
|
||||||
|
{ label: 'Generic CSV', value: 'generic' },
|
||||||
|
{ label: 'ZeroClock JSON', value: 'json' },
|
||||||
|
]
|
||||||
|
|
||||||
// Zoom steps
|
// Zoom steps
|
||||||
const zoomSteps = [80, 90, 100, 110, 120, 130, 150]
|
const zoomSteps = [80, 90, 100, 110, 120, 130, 150]
|
||||||
|
|
||||||
@@ -453,6 +554,74 @@ async function saveThemeSettings() {
|
|||||||
await settingsStore.updateSetting('accent_color', accentColor.value)
|
await settingsStore.updateSetting('accent_color', accentColor.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save shortcut settings
|
||||||
|
async function saveShortcutSettings() {
|
||||||
|
await settingsStore.updateSetting('shortcut_toggle_timer', shortcutToggleTimer.value)
|
||||||
|
await settingsStore.updateSetting('shortcut_show_app', shortcutShowApp.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import file handling
|
||||||
|
async function handleImportFile() {
|
||||||
|
try {
|
||||||
|
const { open } = await import('@tauri-apps/plugin-dialog')
|
||||||
|
const selected = await open({
|
||||||
|
filters: [
|
||||||
|
{ name: 'Data Files', extensions: ['csv', 'json'] }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
if (!selected) return
|
||||||
|
|
||||||
|
const filePath = selected as string
|
||||||
|
// @ts-ignore - plugin-fs types are available at runtime
|
||||||
|
const { readTextFile } = await import('@tauri-apps/plugin-fs')
|
||||||
|
const content: string = await readTextFile(filePath)
|
||||||
|
importFileContent.value = content
|
||||||
|
importFileName.value = filePath.split(/[/\\]/).pop() || 'file'
|
||||||
|
|
||||||
|
if (importFormat.value === 'json') {
|
||||||
|
importPreview.value = []
|
||||||
|
} else {
|
||||||
|
importPreview.value = parseCSV(content).slice(0, 6)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to read file:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeImport() {
|
||||||
|
if (!importFileContent.value) return
|
||||||
|
isImporting.value = true
|
||||||
|
importStatus.value = 'Importing...'
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (importFormat.value === 'json') {
|
||||||
|
const data = JSON.parse(importFileContent.value)
|
||||||
|
await invoke('import_json_data', { data })
|
||||||
|
importStatus.value = 'Import complete!'
|
||||||
|
} else {
|
||||||
|
const rows = parseCSV(importFileContent.value)
|
||||||
|
let entries: ImportEntry[]
|
||||||
|
|
||||||
|
if (importFormat.value === 'toggl' || importFormat.value === 'clockify') {
|
||||||
|
entries = mapTogglCSV(rows)
|
||||||
|
} else {
|
||||||
|
entries = mapGenericCSV(rows, { project: 0, description: 1, start_time: 2, duration: 3, client: -1, task: -1 })
|
||||||
|
}
|
||||||
|
|
||||||
|
await invoke('import_entries', { entries })
|
||||||
|
importStatus.value = `Imported ${entries.length} entries!`
|
||||||
|
}
|
||||||
|
|
||||||
|
importFileContent.value = ''
|
||||||
|
importFileName.value = ''
|
||||||
|
importPreview.value = []
|
||||||
|
} catch (e) {
|
||||||
|
importStatus.value = `Error: ${e}`
|
||||||
|
} finally {
|
||||||
|
isImporting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Export all data
|
// Export all data
|
||||||
async function exportData() {
|
async function exportData() {
|
||||||
try {
|
try {
|
||||||
@@ -499,5 +668,7 @@ onMounted(async () => {
|
|||||||
currency.value = settingsStore.settings.currency || 'USD'
|
currency.value = settingsStore.settings.currency || 'USD'
|
||||||
themeMode.value = settingsStore.settings.theme_mode || 'dark'
|
themeMode.value = settingsStore.settings.theme_mode || 'dark'
|
||||||
accentColor.value = settingsStore.settings.accent_color || 'amber'
|
accentColor.value = settingsStore.settings.accent_color || 'amber'
|
||||||
|
shortcutToggleTimer.value = settingsStore.settings.shortcut_toggle_timer || 'CmdOrCtrl+Shift+T'
|
||||||
|
shortcutShowApp.value = settingsStore.settings.shortcut_show_app || 'CmdOrCtrl+Shift+Z'
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user