import { writable, get } from "svelte/store"; import { listen } from "@tauri-apps/api/event"; import { invoke } from "@tauri-apps/api/core"; import { playSound, playBreakEndSound } from "../utils/sounds"; import { config } from "./config"; export interface TimerSnapshot { state: "running" | "paused" | "breakActive"; currentView: "dashboard" | "breakScreen" | "settings" | "stats"; timeRemaining: number; totalDuration: number; progress: number; hasHadBreak: boolean; secondsSinceLastBreak: number; prebreakWarning: boolean; snoozesUsed: number; canSnooze: boolean; breakTitle: string; breakMessage: string; breakProgress: number; breakTimeRemaining: number; breakTotalDuration: number; breakPastHalf: boolean; settingsModified: boolean; idlePaused: boolean; naturalBreakOccurred: boolean; smartBreaksEnabled: boolean; smartBreakThreshold: number; // F1: Microbreaks microbreakEnabled: boolean; microbreakActive: boolean; microbreakTimeRemaining: number; microbreakTotalDuration: number; microbreakCountdown: number; microbreakFrequency: number; // F3: Pomodoro pomodoroEnabled: boolean; pomodoroCyclePosition: number; pomodoroTotalInCycle: number; pomodoroIsLongBreak: boolean; pomodoroNextIsLong: boolean; // F5: Screen dimming screenDimActive: boolean; screenDimProgress: number; // F2: Presentation mode presentationModeActive: boolean; deferredBreakPending: boolean; // F10: Gamification isLongBreak: boolean; } const defaultSnapshot: TimerSnapshot = { state: "paused", currentView: "dashboard", timeRemaining: 1500, totalDuration: 1500, progress: 1.0, hasHadBreak: false, secondsSinceLastBreak: 0, prebreakWarning: false, snoozesUsed: 0, canSnooze: true, breakTitle: "Rest your eyes", breakMessage: "Look away from the screen. Stretch and relax.", breakProgress: 0, breakTimeRemaining: 0, breakTotalDuration: 0, breakPastHalf: false, settingsModified: false, idlePaused: false, naturalBreakOccurred: false, smartBreaksEnabled: true, smartBreakThreshold: 300, microbreakEnabled: false, microbreakActive: false, microbreakTimeRemaining: 0, microbreakTotalDuration: 0, microbreakCountdown: 0, microbreakFrequency: 1200, pomodoroEnabled: false, pomodoroCyclePosition: 0, pomodoroTotalInCycle: 4, pomodoroIsLongBreak: false, pomodoroNextIsLong: false, screenDimActive: false, screenDimProgress: 0, presentationModeActive: false, deferredBreakPending: false, isLongBreak: false, }; export const timer = writable(defaultSnapshot); // Track the current view separately so UI can switch views optimistically export const currentView = writable< "dashboard" | "breakScreen" | "settings" | "stats" >("dashboard"); let initialized = false; export async function initTimerStore() { if (initialized) return; initialized = true; // Get initial state try { const snapshot = await invoke("get_timer_state"); timer.set(snapshot); currentView.set(snapshot.currentView); } catch (e) { console.error("Failed to get initial timer state:", e); } // Listen for tick events await listen("timer-tick", (event) => { timer.set(event.payload); // Sync view from backend (backend is authoritative for break transitions) currentView.set(event.payload.currentView); }); // Listen for pre-break warning await listen("prebreak-warning", () => { const cfg = get(config); if (cfg.sound_enabled) { playSound(cfg.sound_preset as any, cfg.sound_volume * 0.6); } }); // Listen for break started await listen("break-started", () => { currentView.set("breakScreen"); const cfg = get(config); if (cfg.sound_enabled) { playSound(cfg.sound_preset as any, cfg.sound_volume); } }); // Listen for break ended await listen("break-ended", () => { currentView.set("dashboard"); const cfg = get(config); if (cfg.sound_enabled) { playBreakEndSound(cfg.sound_preset as any, cfg.sound_volume); } }); // Listen for natural break detected await listen("natural-break-detected", (event) => { const cfg = get(config); if (cfg.sound_enabled) { playSound(cfg.sound_preset as any, cfg.sound_volume * 0.5); } }); // F1: Microbreak events await listen("microbreak-started", () => { const cfg = get(config); if (cfg.microbreak_sound_enabled && cfg.sound_enabled) { playSound(cfg.sound_preset as any, cfg.sound_volume * 0.4); } }); await listen("microbreak-ended", () => { const cfg = get(config); if (cfg.microbreak_sound_enabled && cfg.sound_enabled) { playBreakEndSound(cfg.sound_preset as any, cfg.sound_volume * 0.3); } }); // F10: Milestone and daily goal events await listen<{ streak: number }>("milestone-reached", (event) => { milestoneEvent.set(event.payload.streak); setTimeout(() => milestoneEvent.set(null), 4000); }); await listen("daily-goal-met", () => { dailyGoalEvent.set(true); setTimeout(() => dailyGoalEvent.set(false), 4000); }); // F2: Break deferred await listen("break-deferred", () => { // Dashboard will show deferred status from snapshot }); } // F10: Gamification event stores export const milestoneEvent = writable(null); export const dailyGoalEvent = writable(false); // Helper: format seconds as MM:SS export function formatTime(secs: number): string { const m = Math.floor(secs / 60); const s = secs % 60; return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`; } // Helper: format "X ago" string export function formatDurationAgo(secs: number): string { if (secs < 60) return `${secs} sec ago`; const m = Math.floor(secs / 60); if (m < 60) return m === 1 ? "1 min ago" : `${m} min ago`; const h = Math.floor(secs / 3600); return h === 1 ? "1 hr ago" : `${h} hrs ago`; }