Skip to content

Commit 2d74fb4

Browse files
committed
Enhance timer UX with grouped profiles and phase-specific sounds
1 parent 2e75c59 commit 2d74fb4

9 files changed

Lines changed: 173 additions & 12 deletions

File tree

assets/sounds/break_end.mp3

41.1 KB
Binary file not shown.

assets/sounds/focus_end.mp3

41.1 KB
Binary file not shown.

assets/sounds/notification.mp3

-41.1 KB
Binary file not shown.

assets/sounds/reminder.mp3

41.1 KB
Binary file not shown.

user/settings/Timer_Profiles.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ classic_pomodoro:
55
points_per_focus: 0
66
short_break_minutes: 5
77
type: pomodoro
8+
classic_pomodoro_25_5:
9+
focus_minutes: 25
10+
long_break_every: 4
11+
long_break_minutes: 15
12+
points_per_focus: 0
13+
short_break_minutes: 5
14+
type: pomodoro
815
deep_work_60_10:
916
focus_minutes: 60
1017
long_break_every: 3
@@ -61,3 +68,45 @@ sprint_90_30:
6168
points_per_focus: 0
6269
short_break_minutes: 10
6370
type: pomodoro
71+
reset_1_1:
72+
focus_minutes: 1
73+
long_break_every: 4
74+
long_break_minutes: 2
75+
points_per_focus: 0
76+
short_break_minutes: 1
77+
type: pomodoro
78+
two_minute_2_1:
79+
focus_minutes: 2
80+
long_break_every: 4
81+
long_break_minutes: 2
82+
points_per_focus: 0
83+
short_break_minutes: 1
84+
type: pomodoro
85+
starter_5_1:
86+
focus_minutes: 5
87+
long_break_every: 4
88+
long_break_minutes: 3
89+
points_per_focus: 0
90+
short_break_minutes: 1
91+
type: pomodoro
92+
burst_10_2:
93+
focus_minutes: 10
94+
long_break_every: 4
95+
long_break_minutes: 5
96+
points_per_focus: 0
97+
short_break_minutes: 2
98+
type: pomodoro
99+
focus_15_3:
100+
focus_minutes: 15
101+
long_break_every: 4
102+
long_break_minutes: 7
103+
points_per_focus: 0
104+
short_break_minutes: 3
105+
type: pomodoro
106+
ultralight_20_5:
107+
focus_minutes: 20
108+
long_break_every: 4
109+
long_break_minutes: 10
110+
points_per_focus: 0
111+
short_break_minutes: 5
112+
type: pomodoro

user/settings/Timer_Settings.yml

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,27 @@ default_profile: classic_pomodoro
22
auto_advance: true
33
bind_default_type: task
44
poll_interval_ms: 1000
5+
profile_groups:
6+
Micro:
7+
- reset_1_1
8+
- two_minute_2_1
9+
- starter_5_1
10+
- burst_10_2
11+
- focus_15_3
12+
Classic productivity:
13+
- classic_pomodoro
14+
- classic_pomodoro_25_5
15+
- ultralight_20_5
16+
- deep_work_50_10
17+
- deep_work_60_10
18+
Long cycles:
19+
- sprint_75_15
20+
- sprint_90_30
21+
- desk_time_112_26
22+
Rhythm timers:
23+
- microbreak_25_2
24+
- hourly_55_5
25+
- desk_time_52_17
526
sounds:
6-
focus_end: sounds/alarm.mp3
7-
break_end: sounds/alarm.mp3
27+
focus_end: sounds/focus_end.mp3
28+
break_end: sounds/break_end.mp3
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
default_enabled: true
22
default_recurrence: ["daily"]
33
default_label: "A friendly reminder from Chronos."
4-
default_sound: "sounds/notification.mp3"
4+
default_sound: "sounds/reminder.mp3"
55
Type: reminder

utilities/dashboard/server.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2851,9 +2851,46 @@ def _rolling_avg(days_back):
28512851
from modules.timer import main as Timer
28522852
Timer.ensure_default_profiles()
28532853
profiles = {}
2854-
for name in (Timer.profiles_list() or []):
2854+
names = Timer.profiles_list() or []
2855+
for name in names:
28552856
profiles[name] = Timer.profiles_view(name)
2856-
self._write_json(200, {"ok": True, "profiles": profiles})
2857+
2858+
groups = {}
2859+
settings_path = os.path.join(ROOT_DIR, 'user', 'settings', 'timer_settings.yml')
2860+
settings_data = {}
2861+
if os.path.exists(settings_path):
2862+
try:
2863+
with open(settings_path, 'r', encoding='utf-8') as fh:
2864+
settings_data = yaml.safe_load(fh) or {}
2865+
except Exception:
2866+
settings_data = {}
2867+
2868+
raw_groups = {}
2869+
if isinstance(settings_data, dict):
2870+
maybe_groups = settings_data.get('profile_groups')
2871+
if isinstance(maybe_groups, dict):
2872+
raw_groups = maybe_groups
2873+
2874+
known = set(str(n) for n in names)
2875+
assigned = set()
2876+
for label, raw_list in (raw_groups.items() if isinstance(raw_groups, dict) else []):
2877+
if not isinstance(raw_list, list):
2878+
continue
2879+
ordered = []
2880+
for item in raw_list:
2881+
name = str(item or '').strip()
2882+
if not name or name not in known or name in assigned:
2883+
continue
2884+
ordered.append(name)
2885+
assigned.add(name)
2886+
if ordered:
2887+
groups[str(label)] = ordered
2888+
2889+
remaining = [n for n in names if n not in assigned]
2890+
if remaining:
2891+
groups['Other'] = remaining
2892+
2893+
self._write_json(200, {"ok": True, "profiles": profiles, "profile_groups": groups})
28572894
except Exception as e:
28582895
self._write_json(500, {"ok": False, "error": f"Timer profiles error: {e}"})
28592896
return

utilities/dashboard/widgets/timer/index.js

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export function mount(el, context) {
115115
const queueMetaEl = el.querySelector('#twQueueMeta');
116116

117117
let profiles = {};
118+
let profileGroups = {};
118119
let pendingConfirmation = null;
119120
let lastTimerStatus = 'idle';
120121
let lastPendingConfirmVisible = null;
@@ -186,17 +187,70 @@ export function mount(el, context) {
186187
const selectedBefore = profSel.value || '';
187188
const r = await fetch(apiBase() + '/api/timer/profiles?_=' + Date.now(), { cache: 'no-store' }); const d = await r.json();
188189
profiles = d.profiles || {};
189-
profSel.innerHTML = '';
190-
const names = Object.keys(profiles);
191-
names.forEach(n => { const opt = document.createElement('option'); opt.value = n; opt.textContent = n; profSel.appendChild(opt); });
192-
if (selectedBefore) {
193-
const opt = Array.from(profSel.options).find(o => o.value === selectedBefore);
194-
if (opt) profSel.value = selectedBefore;
195-
}
190+
profileGroups = d.profile_groups || {};
191+
renderProfileOptions(selectedBefore);
196192
if (!profSel.value) applyProfileSelection();
197193
} catch { }
198194
}
199195

196+
function renderProfileOptions(selectedBefore = '') {
197+
profSel.innerHTML = '';
198+
const names = Object.keys(profiles || {});
199+
const known = new Set(names);
200+
const groups = (profileGroups && typeof profileGroups === 'object') ? profileGroups : {};
201+
const assigned = new Set();
202+
let hasGroups = false;
203+
204+
Object.entries(groups).forEach(([label, entries]) => {
205+
if (!Array.isArray(entries)) return;
206+
const groupNames = [];
207+
entries.forEach((raw) => {
208+
const name = String(raw || '').trim();
209+
if (!name || !known.has(name) || assigned.has(name)) return;
210+
groupNames.push(name);
211+
assigned.add(name);
212+
});
213+
if (!groupNames.length) return;
214+
hasGroups = true;
215+
const optgroup = document.createElement('optgroup');
216+
optgroup.label = label;
217+
groupNames.forEach((name) => {
218+
const opt = document.createElement('option');
219+
opt.value = name;
220+
opt.textContent = name;
221+
optgroup.appendChild(opt);
222+
});
223+
profSel.appendChild(optgroup);
224+
});
225+
226+
const remaining = names.filter((name) => !assigned.has(name));
227+
if (hasGroups && remaining.length) {
228+
const other = document.createElement('optgroup');
229+
other.label = 'Other';
230+
remaining.forEach((name) => {
231+
const opt = document.createElement('option');
232+
opt.value = name;
233+
opt.textContent = name;
234+
other.appendChild(opt);
235+
});
236+
profSel.appendChild(other);
237+
}
238+
239+
if (!hasGroups) {
240+
names.forEach((name) => {
241+
const opt = document.createElement('option');
242+
opt.value = name;
243+
opt.textContent = name;
244+
profSel.appendChild(opt);
245+
});
246+
}
247+
248+
if (selectedBefore) {
249+
const opt = Array.from(profSel.options).find((o) => o.value === selectedBefore);
250+
if (opt) profSel.value = selectedBefore;
251+
}
252+
}
253+
200254
async function loadSettings() {
201255
try {
202256
const r = await fetch(apiBase() + '/api/timer/settings'); const d = await r.json();

0 commit comments

Comments
 (0)