add blocker presets

This commit is contained in:
timka123 committed 2026-10-19 11:54:15 +03:00
1 parent 2be074f191
commit 772fdbfded
12 files changed
+426 -83

No files matched your search

+7
View File
@@ -166,6 +166,13 @@ export interface AppSettings {
short_break_minutes: number;
long_break_minutes: number;
cycles_before_long_break: number;
presets: {
name: string;
work_minutes: number;
short_break_minutes: number;
long_break_minutes: number;
cycles_before_long_break: number;
}[];
auto_start_breaks: boolean;
auto_start_work: boolean;
daily_goal_pomodoros: number;
+64 -1
View File
@@ -17,6 +17,8 @@ import {
Stack,
Switch,
TextField,
ToggleButton,
ToggleButtonGroup,
Typography,
} from "@mui/material";
@@ -44,6 +46,48 @@ export default function SettingsPage({ onToast }: { onToast: (message: string) =
if (!settings) return <Typography>Loading…</Typography>;
// Which saved preset the live Focus/Short/Long minutes currently match, if
// any — "Custom" the moment they drift from every one of them.
const presets = settings.pomodoro.presets;
const activePresetIndex = presets.findIndex(
(preset) =>
preset.work_minutes === settings.pomodoro.work_minutes &&
preset.short_break_minutes === settings.pomodoro.short_break_minutes &&
preset.long_break_minutes === settings.pomodoro.long_break_minutes,
);
const applyPreset = (index: number) => {
const preset = presets[index];
void patch(
{
pomodoro: {
work_minutes: preset.work_minutes,
short_break_minutes: preset.short_break_minutes,
long_break_minutes: preset.long_break_minutes,
cycles_before_long_break: preset.cycles_before_long_break,
},
},
`${preset.name} applied`,
);
};
// Editing Focus/Short/Long always changes the live settings; when they
// still match a saved preset it also rewrites that preset's own numbers,
// so applying it again later lands where you left off, not on a stale value.
const patchDuration = (
key: "work_minutes" | "short_break_minutes" | "long_break_minutes",
value: number,
) => {
if (activePresetIndex === -1) {
void patch({ pomodoro: { [key]: value } });
return;
}
const updated = presets.map((preset, index) =>
index === activePresetIndex ? { ...preset, [key]: value } : preset,
);
void patch({ pomodoro: { presets: updated, [key]: value } });
};
const testSource = async (name: string) => {
setTesting(name);
try {
@@ -65,6 +109,21 @@ export default function SettingsPage({ onToast }: { onToast: (message: string) =
/>
<Stack spacing={5}>
<Section title="Timer" subtitle="Shared with the desktop app and the Telegram bot.">
<Stack direction="row" spacing={1.5} alignItems="center" sx={{ mb: 2, flexWrap: "wrap", rowGap: 1 }}>
<ToggleButtonGroup
size="small"
exclusive
value={activePresetIndex === -1 ? null : activePresetIndex}
onChange={(_event, value) => value !== null && applyPreset(value as number)}
>
{presets.map((preset, index) => (
<ToggleButton key={preset.name} value={index}>
{preset.name}
</ToggleButton>
))}
</ToggleButtonGroup>
{activePresetIndex === -1 && <Chip size="small" variant="outlined" label="Custom" />}
</Stack>
<Grid container spacing={2}>
{(
[
@@ -81,7 +140,11 @@ export default function SettingsPage({ onToast }: { onToast: (message: string) =
value={settings.pomodoro[key] as number}
min={min}
max={max}
onCommit={(value) => void patch({ pomodoro: { [key]: value } })}
onCommit={(value) =>
key === "work_minutes" || key === "short_break_minutes" || key === "long_break_minutes"
? patchDuration(key, value)
: void patch({ pomodoro: { [key]: value } })
}
/>
</Grid>
))}
+53 -1
View File
@@ -22,6 +22,8 @@ import {
MenuItem,
Stack,
TextField,
ToggleButton,
ToggleButtonGroup,
Tooltip,
Typography,
} from "@mui/material";
@@ -136,6 +138,39 @@ export default function TimerPage({
const total = current?.planned_seconds ?? (state ? state.durations[kind] * 60 : 1500);
const accent = kindColor(kind, palette);
// Which saved preset the live Focus/Short/Long minutes match, if any.
const presets = settings?.pomodoro.presets ?? [];
const activePresetIndex = settings
? presets.findIndex(
(preset) =>
preset.work_minutes === settings.pomodoro.work_minutes &&
preset.short_break_minutes === settings.pomodoro.short_break_minutes &&
preset.long_break_minutes === settings.pomodoro.long_break_minutes,
)
: -1;
// The backend resizes whatever interval is already running or paused to
// match, so a fresh `/api/timer/state` right away makes it feel instant
// instead of waiting out this page's own 10s poll.
const applyPreset = async (index: number) => {
const preset = presets[index];
if (!preset) return;
try {
await save({
pomodoro: {
work_minutes: preset.work_minutes,
short_break_minutes: preset.short_break_minutes,
long_break_minutes: preset.long_break_minutes,
cycles_before_long_break: preset.cycles_before_long_break,
},
});
await timer.reload();
onToast(`${preset.name} applied`);
} catch (exc) {
onToast(exc instanceof Error ? exc.message : String(exc));
}
};
return (
<>
<PageHeader
@@ -220,8 +255,25 @@ export default function TimerPage({
idle={!timerEnabled || !current}
/>
{timerEnabled && presets.length > 0 && (
<Stack direction="row" justifyContent="center" sx={{ mt: 3 }}>
<ToggleButtonGroup
size="small"
exclusive
value={activePresetIndex === -1 ? null : activePresetIndex}
onChange={(_event, value) => value !== null && void applyPreset(value as number)}
>
{presets.map((preset, index) => (
<ToggleButton key={preset.name} value={index}>
{preset.name}
</ToggleButton>
))}
</ToggleButtonGroup>
</Stack>
)}
{current && timerEnabled && (
<Stack direction="row" spacing={1} justifyContent="center" sx={{ mt: 3 }}>
<Stack direction="row" spacing={1} justifyContent="center" sx={{ mt: 2 }}>
<Tooltip title="Add five minutes">
<IconButton size="small" disabled={busy} onClick={() => void call("/api/timer/extend", { minutes: 5 })}>
<AddAlarmOutlinedIcon fontSize="small" />