Major feature release (v0.1.3) adding 15 new features to the break timer: Backend (Rust): - Pomodoro cycle tracking with configurable short/long break pattern - Microbreak scheduling (20-20-20 rule) with independent timer - Screen dimming events with gradual opacity progression - Presentation mode detection (fullscreen app deferral) - Smart break detection (natural idle breaks counting toward goals) - Daily goal tracking and streak milestone events - Multi-monitor break overlay support - Working hours enforcement with per-day schedules - Weekly summary and natural break stats queries - Config expanded to 71 validated fields Frontend (Svelte): - 6 new components: BreathingGuide, ActivityManager, BreakOverlay, MicrobreakOverlay, DimOverlay, Celebration - Breathing guide with 5 patterns and animated pulsing halo - Activity manager with favorites, custom activities, momentum scroll - Confetti celebrations on milestones and goal completion - Dashboard indicators (pomodoro/microbreak/goal) moved inside ring - Settings reorganized into 18 logical cards - Breathing pattern selector redesigned with timing descriptions - Break activities expanded from 40 to 71 curated exercises - Sound presets expanded from 4 to 8 - Stats view with weekly summary and natural break tracking Also: version bump to 0.1.3, CHANGELOG, README and CLAUDE.md updates
139 lines
4.6 KiB
Svelte
139 lines
4.6 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from "svelte";
|
|
import { fly, scale, fade } from "svelte/transition";
|
|
import { cubicOut } from "svelte/easing";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
|
import { initTimerStore, currentView } from "./lib/stores/timer";
|
|
import { loadConfig, config } from "./lib/stores/config";
|
|
import Titlebar from "./lib/components/Titlebar.svelte";
|
|
import Dashboard from "./lib/components/Dashboard.svelte";
|
|
import BreakScreen from "./lib/components/BreakScreen.svelte";
|
|
import Settings from "./lib/components/Settings.svelte";
|
|
import StatsView from "./lib/components/StatsView.svelte";
|
|
import BackgroundBlobs from "./lib/components/BackgroundBlobs.svelte";
|
|
import Celebration from "./lib/components/Celebration.svelte";
|
|
|
|
const appWindow = getCurrentWebviewWindow();
|
|
|
|
onMount(async () => {
|
|
await loadConfig();
|
|
await initTimerStore();
|
|
|
|
// Save window position on move/resize (debounced)
|
|
let posTimer: ReturnType<typeof setTimeout>;
|
|
const savePos = () => {
|
|
clearTimeout(posTimer);
|
|
posTimer = setTimeout(async () => {
|
|
try {
|
|
const pos = await appWindow.outerPosition();
|
|
const size = await appWindow.outerSize();
|
|
await invoke("save_window_position", {
|
|
label: "main", x: pos.x, y: pos.y,
|
|
width: size.width, height: size.height,
|
|
});
|
|
} catch {}
|
|
}, 500);
|
|
};
|
|
appWindow.onMoved(savePos);
|
|
appWindow.onResized(savePos);
|
|
});
|
|
|
|
// Apply UI zoom from config
|
|
const zoomScale = $derived($config.ui_zoom / 100);
|
|
|
|
// Track previous view for directional transitions
|
|
let previousView = $state<string>("dashboard");
|
|
|
|
$effect(() => {
|
|
const view = $currentView;
|
|
// Store previous for determining transition direction
|
|
return () => {
|
|
previousView = view;
|
|
};
|
|
});
|
|
|
|
// Reduced motion preference
|
|
let reducedMotion = $state(window.matchMedia("(prefers-reduced-motion: reduce)").matches);
|
|
$effect(() => {
|
|
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
const handler = (e: MediaQueryListEvent) => { reducedMotion = e.matches; };
|
|
mq.addEventListener("change", handler);
|
|
return () => mq.removeEventListener("change", handler);
|
|
});
|
|
|
|
// Transition parameters — zero when reduced motion active
|
|
const DURATION = $derived(reducedMotion ? 0 : 700);
|
|
const easing = cubicOut;
|
|
|
|
// Focus management: move focus to new view's heading on view change
|
|
$effect(() => {
|
|
const _view = effectiveView;
|
|
requestAnimationFrame(() => {
|
|
const heading = document.querySelector("h1[tabindex='-1']") as HTMLElement | null;
|
|
heading?.focus({ preventScroll: true });
|
|
});
|
|
});
|
|
|
|
// When fullscreen_mode is OFF, the separate break window handles breaks,
|
|
// so the main window should keep showing whatever view it was on (dashboard).
|
|
const effectiveView = $derived(
|
|
$currentView === "breakScreen" && !$config.fullscreen_mode
|
|
? "dashboard"
|
|
: $currentView
|
|
);
|
|
</script>
|
|
|
|
<main class="relative h-full bg-black">
|
|
{#if $config.background_blobs_enabled}
|
|
<BackgroundBlobs accentColor={$config.accent_color} breakColor={$config.break_color} />
|
|
{/if}
|
|
<Titlebar />
|
|
<div
|
|
class="relative h-full overflow-hidden origin-top-left"
|
|
style="
|
|
transform: scale({zoomScale});
|
|
width: {100 / zoomScale}%;
|
|
height: {100 / zoomScale}%;
|
|
"
|
|
>
|
|
{#if effectiveView === "dashboard"}
|
|
<div
|
|
class="absolute inset-0"
|
|
in:fly={{ x: previousView === "settings" ? -200 : 0, duration: DURATION, easing, opacity: 0 }}
|
|
out:fly={{ x: previousView === "dashboard" ? -200 : 0, duration: DURATION * 0.6, easing, opacity: 0 }}
|
|
>
|
|
<Dashboard />
|
|
</div>
|
|
{/if}
|
|
{#if effectiveView === "breakScreen"}
|
|
<div
|
|
class="absolute inset-0"
|
|
in:scale={{ start: 1.08, duration: DURATION, easing, opacity: 0 }}
|
|
out:scale={{ start: 0.95, duration: DURATION * 0.6, easing, opacity: 0 }}
|
|
>
|
|
<BreakScreen />
|
|
</div>
|
|
{/if}
|
|
{#if effectiveView === "settings"}
|
|
<div
|
|
class="absolute inset-0"
|
|
in:fly={{ x: 200, duration: DURATION, easing, opacity: 0 }}
|
|
out:fly={{ x: 200, duration: DURATION * 0.6, easing, opacity: 0 }}
|
|
>
|
|
<Settings />
|
|
</div>
|
|
{/if}
|
|
{#if effectiveView === "stats"}
|
|
<div
|
|
class="absolute inset-0"
|
|
in:fly={{ y: 200, duration: DURATION, easing, opacity: 0 }}
|
|
out:fly={{ y: 200, duration: DURATION * 0.6, easing, opacity: 0 }}
|
|
>
|
|
<StatsView />
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
<Celebration />
|
|
</main>
|