add blocker presets
This commit is contained in:
12 files changed
+426
-83
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in new issue
Block a user