206 lines
5.8 KiB
TypeScript
206 lines
5.8 KiB
TypeScript
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<TimerSnapshot>(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<TimerSnapshot>("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<TimerSnapshot>("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<number>("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<number | null>(null);
|
|
export const dailyGoalEvent = writable<boolean>(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`;
|
|
}
|