summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src-tauri/src/lib.rs40
-rw-r--r--src/App.tsx62
-rw-r--r--src/components/AmbientControl.tsx9
-rw-r--r--src/components/TimerView.tsx13
4 files changed, 82 insertions, 42 deletions
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index aaa9cd2..ae8ad13 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -55,6 +55,10 @@ fn sync_audio_ducking(audio: &AudioState, phase: TimerPhase) {
}
}
+fn clear_session_count_only(timer: &mut TimerState) {
+ timer.session_count = 0;
+}
+
// ── Timer commands ──────────────────────────────────────────────────────────
#[tauri::command]
@@ -114,6 +118,17 @@ fn reset_timer(
}
#[tauri::command]
+fn clear_session_count(
+ timer: State<'_, TimerStateWrapper>,
+ data: State<'_, AppDataWrapper>,
+ app: AppHandle,
+) -> Result<(), String> {
+ let mut ts = timer.0.lock().unwrap();
+ clear_session_count_only(&mut ts);
+ persist_timer_snapshot(&ts, &data, &app)
+}
+
+#[tauri::command]
fn skip_phase(
timer: State<'_, TimerStateWrapper>,
data: State<'_, AppDataWrapper>,
@@ -455,6 +470,7 @@ pub fn run() {
start_timer,
pause_timer,
reset_timer,
+ clear_session_count,
skip_phase,
set_current_task,
get_settings,
@@ -474,10 +490,32 @@ pub fn run() {
#[cfg(test)]
mod tests {
- use super::fallback_bundled_audio_root;
+ use super::{clear_session_count_only, fallback_bundled_audio_root};
+ use crate::state::{TimerPhase, TimerState};
#[test]
fn dev_audio_fallback_points_at_src_tauri() {
assert_eq!(fallback_bundled_audio_root(), std::path::PathBuf::from("src-tauri"));
}
+
+ #[test]
+ fn clear_session_count_preserves_phase_and_duration() {
+ let mut state = TimerState {
+ phase: TimerPhase::ShortBreak,
+ remaining_secs: 120,
+ total_secs: 300,
+ running: false,
+ session_count: 4,
+ current_task_id: Some("task-1".to_string()),
+ };
+
+ clear_session_count_only(&mut state);
+
+ assert_eq!(state.phase, TimerPhase::ShortBreak);
+ assert_eq!(state.remaining_secs, 120);
+ assert_eq!(state.total_secs, 300);
+ assert!(!state.running);
+ assert_eq!(state.session_count, 0);
+ assert_eq!(state.current_task_id.as_deref(), Some("task-1"));
+ }
}
diff --git a/src/App.tsx b/src/App.tsx
index 29ac64f..b4aaec7 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -3,7 +3,6 @@ import { TimerView } from './components/TimerView';
import { TaskList } from './components/TaskList';
import { SettingsPanel } from './components/SettingsPanel';
import { NotificationOverlay } from './components/NotificationOverlay';
-import { AmbientControl } from './components/AmbientControl';
import { useTimerEvents } from './hooks/useTimerEvents';
import { useTaskStore } from './store/taskStore';
import { useAudioStore } from './store/audioStore';
@@ -65,48 +64,35 @@ function App() {
<TaskList />
{/* Main area */}
- <div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
- {/* Top bar */}
- <div
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', position: 'relative' }}>
+ <button
+ onClick={() => setSettingsOpen(true)}
+ title="Settings"
style={{
+ position: 'absolute',
+ top: '14px',
+ right: '16px',
display: 'flex',
- alignItems: 'flex-start',
- flexWrap: 'wrap',
- justifyContent: 'space-between',
- gap: '16px',
- padding: '12px 16px',
- borderBottom: '1px solid var(--line-1)',
+ alignItems: 'center',
+ padding: '6px',
+ border: 'none',
+ background: 'transparent',
+ color: 'var(--fg-3)',
+ cursor: 'pointer',
+ borderRadius: 'var(--r-2)',
+ transition: 'color 0.15s ease',
+ zIndex: 2,
+ }}
+ onMouseEnter={(e) => {
+ (e.currentTarget as HTMLButtonElement).style.color = 'var(--fg-1)';
+ }}
+ onMouseLeave={(e) => {
+ (e.currentTarget as HTMLButtonElement).style.color = 'var(--fg-3)';
}}
>
- <AmbientControl />
-
- <button
- onClick={() => setSettingsOpen(true)}
- title="Settings"
- style={{
- display: 'flex',
- alignItems: 'center',
- padding: '6px',
- border: 'none',
- background: 'transparent',
- color: 'var(--fg-3)',
- cursor: 'pointer',
- borderRadius: 'var(--r-2)',
- marginLeft: 'auto',
- transition: 'color 0.15s ease',
- }}
- onMouseEnter={(e) => {
- (e.currentTarget as HTMLButtonElement).style.color = 'var(--fg-1)';
- }}
- onMouseLeave={(e) => {
- (e.currentTarget as HTMLButtonElement).style.color = 'var(--fg-3)';
- }}
- >
- <GearIcon />
- </button>
- </div>
+ <GearIcon />
+ </button>
- {/* Timer */}
<TimerView />
</div>
diff --git a/src/components/AmbientControl.tsx b/src/components/AmbientControl.tsx
index dd58235..56a2340 100644
--- a/src/components/AmbientControl.tsx
+++ b/src/components/AmbientControl.tsx
@@ -56,16 +56,21 @@ export function AmbientControl() {
onChange={handleSoundChange}
disabled={!available}
style={{
+ appearance: 'none',
+ WebkitAppearance: 'none',
+ MozAppearance: 'none',
+ colorScheme: 'dark',
fontFamily: 'var(--font-sans)',
fontSize: '13px',
color: available ? 'var(--fg-1)' : 'var(--fg-4)',
- background: 'var(--ink-3)',
+ background: 'var(--ink-2)',
border: '1px solid var(--line-2)',
borderRadius: 'var(--r-2)',
- padding: '6px 10px',
+ padding: '8px 32px 8px 10px',
outline: 'none',
minWidth: '132px',
flex: '0 0 auto',
+ lineHeight: 1.3,
}}
>
{SOUND_OPTIONS.map((option) => (
diff --git a/src/components/TimerView.tsx b/src/components/TimerView.tsx
index b43973c..47c104a 100644
--- a/src/components/TimerView.tsx
+++ b/src/components/TimerView.tsx
@@ -2,6 +2,7 @@ import type { CSSProperties } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { useTimerStore, TimerPhase } from '../store/timerStore';
import { useTaskStore } from '../store/taskStore';
+import { AmbientControl } from './AmbientControl';
const RING_SIZE = 280;
const STROKE = 8;
@@ -78,6 +79,10 @@ export function TimerView() {
await invoke('reset_timer');
await syncFromBackend();
};
+ const handleClearSessions = async () => {
+ await invoke('clear_session_count');
+ await syncFromBackend();
+ };
return (
<div
@@ -88,9 +93,14 @@ export function TimerView() {
justifyContent: 'center',
gap: '20px',
flex: 1,
- padding: '24px 20px',
+ padding: '24px 20px 32px',
+ overflow: 'auto',
}}
>
+ <div style={{ width: 'min(100%, 520px)' }}>
+ <AmbientControl />
+ </div>
+
{/* Eyebrow */}
<span className="eyebrow" style={{ letterSpacing: '0.18em' }}>
{eyebrowText(phase, sessionCount)}
@@ -204,6 +214,7 @@ export function TimerView() {
<GhostButton onClick={handleSkip}>Skip</GhostButton>
<GhostButton onClick={handleReset}>Reset</GhostButton>
+ <GhostButton onClick={handleClearSessions}>Clear Streak</GhostButton>
</div>
</div>
);