add duplicate, copy previous day/week, and repeat entry
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
<div class="p-6">
|
||||
<!-- Hero timer display -->
|
||||
<div class="text-center pt-4 pb-8">
|
||||
<p class="text-[3.5rem] font-medium font-[family-name:var(--font-heading)] tracking-tighter mb-6">
|
||||
<p class="text-[3.5rem] font-medium font-[family-name:var(--font-heading)] tracking-tighter mb-2">
|
||||
<span class="text-text-primary">{{ timerParts.hours }}</span>
|
||||
<span :class="timerStore.isRunning ? 'text-accent-text animate-pulse-colon' : 'text-text-primary'">:</span>
|
||||
<span class="text-text-primary">{{ timerParts.minutes }}</span>
|
||||
@@ -10,14 +10,22 @@
|
||||
<span class="text-text-primary">{{ timerParts.seconds }}</span>
|
||||
</p>
|
||||
|
||||
<!-- Paused indicator -->
|
||||
<p
|
||||
v-if="timerStore.isPaused"
|
||||
class="text-[0.75rem] font-medium mb-4"
|
||||
:class="timerStore.timerState === 'PAUSED_IDLE' ? 'text-status-warning' : 'text-status-info'"
|
||||
>
|
||||
{{ timerStore.timerState === 'PAUSED_IDLE' ? 'Paused (idle)' : 'Paused (app not visible)' }}
|
||||
</p>
|
||||
<div v-else class="mb-4" />
|
||||
|
||||
<button
|
||||
@click="toggleTimer"
|
||||
class="px-10 py-3 text-sm font-medium rounded-lg transition-colors duration-150"
|
||||
:class="timerStore.isRunning
|
||||
? 'bg-status-error text-white hover:bg-status-error/80'
|
||||
: 'bg-accent text-bg-base hover:bg-accent-hover'"
|
||||
:class="buttonClass"
|
||||
>
|
||||
{{ timerStore.isRunning ? 'Stop' : 'Start' }}
|
||||
{{ buttonLabel }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +41,7 @@
|
||||
value-key="id"
|
||||
placeholder="Select project"
|
||||
:placeholder-value="null"
|
||||
:disabled="timerStore.isRunning"
|
||||
:disabled="!timerStore.isStopped"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -45,7 +53,7 @@
|
||||
value-key="id"
|
||||
placeholder="Select task"
|
||||
:placeholder-value="null"
|
||||
:disabled="timerStore.isRunning || !selectedProject"
|
||||
:disabled="!timerStore.isStopped || !selectedProject"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,7 +62,7 @@
|
||||
<input
|
||||
v-model="description"
|
||||
type="text"
|
||||
:disabled="timerStore.isRunning"
|
||||
:disabled="!timerStore.isStopped"
|
||||
class="w-full px-3 py-2 bg-bg-inset border border-border-subtle rounded-lg text-[0.8125rem] text-text-primary placeholder-text-tertiary focus:outline-none focus:border-border-visible disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
placeholder="What are you working on?"
|
||||
/>
|
||||
@@ -85,9 +93,19 @@
|
||||
<p class="text-[0.6875rem] text-text-tertiary">{{ entry.description || 'No description' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-[0.75rem] font-mono text-text-primary">{{ formatDuration(entry.duration) }}</p>
|
||||
<p class="text-[0.6875rem] text-text-tertiary">{{ formatDateTime(entry.start_time) }}</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="text-right">
|
||||
<p class="text-[0.75rem] font-mono text-text-primary">{{ formatDuration(entry.duration) }}</p>
|
||||
<p class="text-[0.6875rem] text-text-tertiary">{{ formatDateTime(entry.start_time) }}</p>
|
||||
</div>
|
||||
<button
|
||||
v-if="timerStore.isStopped"
|
||||
@click="repeatEntry(entry)"
|
||||
class="p-1 text-text-tertiary hover:text-text-secondary transition-colors"
|
||||
title="Repeat"
|
||||
>
|
||||
<RotateCcw class="w-3 h-3" :stroke-width="2" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -99,6 +117,22 @@
|
||||
<p class="text-xs text-text-tertiary mt-1">Select a project and hit Start to begin tracking</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Idle Prompt Dialog -->
|
||||
<IdlePromptDialog
|
||||
:show="timerStore.showIdlePrompt"
|
||||
:idle-seconds="timerStore.idleDurationSeconds"
|
||||
@continue-keep="onIdleContinueKeep"
|
||||
@continue-subtract="onIdleContinueSubtract"
|
||||
@stop-timer="onIdleStop"
|
||||
/>
|
||||
|
||||
<!-- App Tracking Prompt Dialog -->
|
||||
<AppTrackingPromptDialog
|
||||
:show="timerStore.showAppPrompt"
|
||||
@continue-timer="onAppContinue"
|
||||
@stop-timer="onAppStop"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -107,14 +141,18 @@ import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useTimerStore } from '../stores/timer'
|
||||
import { useProjectsStore, type Task } from '../stores/projects'
|
||||
import { useEntriesStore } from '../stores/entries'
|
||||
import { useSettingsStore } from '../stores/settings'
|
||||
import { useToastStore } from '../stores/toast'
|
||||
import { Timer as TimerIcon } from 'lucide-vue-next'
|
||||
import { Timer as TimerIcon, RotateCcw } from 'lucide-vue-next'
|
||||
import AppSelect from '../components/AppSelect.vue'
|
||||
import IdlePromptDialog from '../components/IdlePromptDialog.vue'
|
||||
import AppTrackingPromptDialog from '../components/AppTrackingPromptDialog.vue'
|
||||
import { formatDateTime } from '../utils/locale'
|
||||
|
||||
const timerStore = useTimerStore()
|
||||
const projectsStore = useProjectsStore()
|
||||
const entriesStore = useEntriesStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const toastStore = useToastStore()
|
||||
|
||||
// Local state for inputs
|
||||
@@ -144,6 +182,18 @@ const recentEntries = computed(() => {
|
||||
return entriesStore.entries.slice(0, 5)
|
||||
})
|
||||
|
||||
// Button appearance
|
||||
const buttonLabel = computed(() => {
|
||||
if (timerStore.isStopped) return 'Start'
|
||||
if (timerStore.isPaused) return 'Stop'
|
||||
return 'Stop'
|
||||
})
|
||||
|
||||
const buttonClass = computed(() => {
|
||||
if (timerStore.isStopped) return 'bg-accent text-bg-base hover:bg-accent-hover'
|
||||
return 'bg-status-error text-white hover:bg-status-error/80'
|
||||
})
|
||||
|
||||
// Watch project selection and fetch tasks
|
||||
watch(selectedProject, async (newProjectId) => {
|
||||
timerStore.setProject(newProjectId)
|
||||
@@ -165,20 +215,89 @@ watch(description, (newDesc) => {
|
||||
timerStore.setDescription(newDesc)
|
||||
})
|
||||
|
||||
// Bring window to front when prompts appear
|
||||
async function bringToFront() {
|
||||
try {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window')
|
||||
const win = getCurrentWindow()
|
||||
await win.show()
|
||||
await win.setFocus()
|
||||
} catch (e) {
|
||||
// Ignore if not in Tauri context
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => timerStore.showIdlePrompt, (show) => {
|
||||
if (show) bringToFront()
|
||||
})
|
||||
|
||||
watch(() => timerStore.showAppPrompt, (show) => {
|
||||
if (show) bringToFront()
|
||||
})
|
||||
|
||||
// Send OS notification when entering PAUSED_APP in "notify" mode
|
||||
watch(() => timerStore.timerState, async (newState, oldState) => {
|
||||
if (newState === 'PAUSED_APP' && oldState === 'RUNNING') {
|
||||
const mode = settingsStore.settings.app_tracking_mode || 'auto'
|
||||
if (mode === 'notify') {
|
||||
try {
|
||||
const { sendNotification } = await import('@tauri-apps/plugin-notification')
|
||||
sendNotification({
|
||||
title: 'ZeroClock',
|
||||
body: 'Tracked app is no longer visible. Timer paused.',
|
||||
})
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Toggle timer
|
||||
function toggleTimer() {
|
||||
if (timerStore.isRunning) {
|
||||
timerStore.stop()
|
||||
entriesStore.fetchEntries()
|
||||
} else {
|
||||
if (timerStore.isStopped) {
|
||||
if (!selectedProject.value) {
|
||||
toastStore.info('Please select a project before starting the timer')
|
||||
return
|
||||
}
|
||||
timerStore.start()
|
||||
} else {
|
||||
timerStore.stop()
|
||||
entriesStore.fetchEntries()
|
||||
}
|
||||
}
|
||||
|
||||
// Idle prompt handlers
|
||||
function onIdleContinueKeep() {
|
||||
timerStore.handleIdleContinueKeep()
|
||||
}
|
||||
|
||||
function onIdleContinueSubtract() {
|
||||
timerStore.handleIdleContinueSubtract()
|
||||
}
|
||||
|
||||
async function onIdleStop() {
|
||||
await timerStore.handleIdleStop()
|
||||
entriesStore.fetchEntries()
|
||||
}
|
||||
|
||||
// App prompt handlers
|
||||
function onAppContinue() {
|
||||
timerStore.handleAppContinue()
|
||||
}
|
||||
|
||||
async function onAppStop() {
|
||||
await timerStore.handleAppStop()
|
||||
entriesStore.fetchEntries()
|
||||
}
|
||||
|
||||
// Repeat an entry (fill in project/task/description)
|
||||
function repeatEntry(entry: { project_id: number; task_id?: number; description?: string }) {
|
||||
selectedProject.value = entry.project_id
|
||||
selectedTask.value = entry.task_id || null
|
||||
description.value = entry.description || ''
|
||||
}
|
||||
|
||||
// Get project name by ID
|
||||
function getProjectName(projectId: number): string {
|
||||
const project = projectsStore.projects.find(p => p.id === projectId)
|
||||
@@ -205,7 +324,8 @@ function formatDuration(seconds: number): string {
|
||||
onMounted(async () => {
|
||||
await Promise.all([
|
||||
projectsStore.fetchProjects(),
|
||||
entriesStore.fetchEntries()
|
||||
entriesStore.fetchEntries(),
|
||||
settingsStore.fetchSettings()
|
||||
])
|
||||
|
||||
// Restore timer state
|
||||
|
||||
Reference in New Issue
Block a user