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

+40
View File
@@ -261,6 +261,24 @@ class Controller(QObject):
def settings(self) -> dict[str, Any]:
return self._settings
@pyqtProperty(int, notify=settingsChanged)
def activePresetIndex(self) -> int:
"""Which saved preset the live Focus/Short/Long minutes match, or -1.
One source of truth for SettingsPage (the full switcher, with the
numbers editable below it) and TimerPage (a quick switcher next to
the dial) so the two never compute "which preset is this" differently.
"""
pomodoro = self._settings.get("pomodoro", {})
for index, preset in enumerate(pomodoro.get("presets", [])):
if (
preset.get("work_minutes") == pomodoro.get("work_minutes")
and preset.get("short_break_minutes") == pomodoro.get("short_break_minutes")
and preset.get("long_break_minutes") == pomodoro.get("long_break_minutes")
):
return index
return -1
@pyqtProperty(bool, notify=blockerChanged)
def blockerActive(self) -> bool:
return bool(self._policy.get("active"))
@@ -426,6 +444,28 @@ class Controller(QObject):
def patchSettings(self, patch: dict[str, Any]) -> None:
self._send("patch_settings", {"patch": patch})
@pyqtSlot(int)
def applyPreset(self, index: int) -> None:
"""Copy a saved preset's minutes onto the live timer settings.
The backend resizes whatever interval is already running or paused
to match, so this takes effect at once — not just on the next one.
"""
presets = self._settings.get("pomodoro", {}).get("presets", [])
if not (0 <= index < len(presets)):
return
preset = presets[index]
self.patchSettings(
{
"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"],
}
}
)
@pyqtSlot(str, "QVariant")
def setConfig(self, key: str, value: Any) -> None:
"""Change a desktop-local setting and apply it immediately."""
+35 -77
View File
@@ -29,27 +29,21 @@ ScrollView {
controller.patchSettings({ notifications: { telegram: patch } })
}
// Presets are a saved list on the server (`pomodoro.presets`), each one a
// name plus its own work/short/long minutes — editable here, not baked
// into the UI. `_merge` on the backend replaces list fields wholesale, so
// editing one preset's number means resending the whole list.
function patchPreset(index, key, value) {
if (!page.pomodoro || !page.pomodoro.presets) return
var presets = page.pomodoro.presets.map(function (p) { return Object.assign({}, p) })
presets[index][key] = value
controller.patchSettings({ pomodoro: { presets: presets } })
}
function applyPreset(index) {
if (!page.pomodoro || !page.pomodoro.presets) return
var preset = page.pomodoro.presets[index]
controller.patchSettings({
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
}
})
// The Focus/Short break/Long break fields always show and edit the live
// settings. When they still match a saved preset, the edit also rewrites
// that preset's own numbers, so picking it again later gets you back to
// where you left off rather than to some older saved value.
function patchDuration(key, value) {
if (!page.pomodoro) return
var patch = {}; patch[key] = value
var idx = controller.activePresetIndex
if (idx >= 0 && page.pomodoro.presets) {
var presets = page.pomodoro.presets.map(function (p) { return Object.assign({}, p) })
presets[idx] = Object.assign(presets[idx], patch)
controller.patchSettings({ pomodoro: Object.assign({ presets: presets }, patch) })
} else {
controller.patchSettings({ pomodoro: patch })
}
}
component Field: ColumnLayout {
@@ -127,68 +121,32 @@ ScrollView {
spacing: 12
SectionTitle { text: "Timer" }
// Presets: a saved list of work/short/long combos. "Apply" copies
// one onto the live settings below; the numbers on each row are
// the preset's own and edit it in place.
ColumnLayout {
// The preset switcher: pick one and it applies at once. The fields
// below always belong to whichever one is lit — there is nothing
// to show for the others until you switch to them.
RowLayout {
Layout.fillWidth: true
spacing: 8
Repeater {
model: page.pomodoro && page.pomodoro.presets ? page.pomodoro.presets : []
RowLayout {
id: presetRow
delegate: PresetChip {
required property var modelData
required property int index
Layout.fillWidth: true
spacing: 16
readonly property bool isActive: page.pomodoro
&& page.pomodoro.work_minutes === modelData.work_minutes
&& page.pomodoro.short_break_minutes === modelData.short_break_minutes
&& page.pomodoro.long_break_minutes === modelData.long_break_minutes
Label {
text: presetRow.modelData.name
color: presetRow.isActive ? Theme.ember : Theme.ink
font.pixelSize: 13
font.weight: presetRow.isActive ? Font.DemiBold : Font.Medium
font.family: Theme.sansFamily
Layout.preferredWidth: 70
}
Field {
label: "Focus"
SpinBox {
from: 1; to: 240
value: presetRow.modelData.work_minutes
onValueModified: page.patchPreset(presetRow.index, "work_minutes", value)
}
}
Field {
label: "Short break"
SpinBox {
from: 1; to: 120
value: presetRow.modelData.short_break_minutes
onValueModified: page.patchPreset(presetRow.index, "short_break_minutes", value)
}
}
Field {
label: "Long break"
SpinBox {
from: 1; to: 240
value: presetRow.modelData.long_break_minutes
onValueModified: page.patchPreset(presetRow.index, "long_break_minutes", value)
}
}
GhostButton {
text: presetRow.isActive ? "Active" : "Apply"
enabled: !presetRow.isActive
onClicked: page.applyPreset(presetRow.index)
}
Item { Layout.fillWidth: true }
text: modelData.name
active: controller.activePresetIndex === index
onClicked: controller.applyPreset(index)
}
}
Label {
visible: controller.activePresetIndex === -1
text: "Custom"
color: Theme.muted
font.pixelSize: 12
font.family: Theme.sansFamily
leftPadding: 6
}
Item { Layout.fillWidth: true }
}
Flow {
@@ -199,7 +157,7 @@ ScrollView {
SpinBox {
from: 1; to: 240
value: page.pomodoro ? page.pomodoro.work_minutes : 25
onValueModified: page.patchPomodoro("work_minutes", value)
onValueModified: page.patchDuration("work_minutes", value)
}
}
Field {
@@ -207,7 +165,7 @@ ScrollView {
SpinBox {
from: 1; to: 120
value: page.pomodoro ? page.pomodoro.short_break_minutes : 5
onValueModified: page.patchPomodoro("short_break_minutes", value)
onValueModified: page.patchDuration("short_break_minutes", value)
}
}
Field {
@@ -215,7 +173,7 @@ ScrollView {
SpinBox {
from: 1; to: 240
value: page.pomodoro ? page.pomodoro.long_break_minutes : 15
onValueModified: page.patchPomodoro("long_break_minutes", value)
onValueModified: page.patchDuration("long_break_minutes", value)
}
}
Field {
+23
View File
@@ -102,6 +102,29 @@ ScrollView {
idle: !controller.timerEnabled || !controller.running
}
// Switching here takes effect at once — it resizes whatever
// is already running or paused too, not just the next one.
RowLayout {
visible: controller.timerEnabled
&& controller.settings.pomodoro
&& controller.settings.pomodoro.presets
&& controller.settings.pomodoro.presets.length > 0
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 14
spacing: 8
Repeater {
model: controller.settings.pomodoro ? controller.settings.pomodoro.presets : []
delegate: PresetChip {
required property var modelData
required property int index
text: modelData.name
active: controller.activePresetIndex === index
onClicked: controller.applyPreset(index)
}
}
}
RowLayout {
visible: controller.timerEnabled && controller.running
Layout.alignment: Qt.AlignHCenter
@@ -0,0 +1,39 @@
// A pill in a preset switcher: filled ember when it is the one the live
// settings currently match, outlined otherwise. Clicking any pill applies it
// at once — there is no separate "Apply" step. Shared by SettingsPage (the
// full switcher, with the numbers editable below it) and TimerPage (a quick
// way to change the length of the *next* interval without leaving the dial).
import QtQuick
import QtQuick.Controls
import ".."
Rectangle {
id: chip
property string text: ""
property bool active: false
signal clicked()
implicitWidth: label.implicitWidth + 28
implicitHeight: 32
radius: 16
color: chip.active ? Theme.ember : (chipHover.hovered ? Qt.alpha(Theme.ember, 0.10) : "transparent")
border.width: chip.active ? 0 : 1
border.color: chipHover.hovered ? Theme.ember : Theme.rule
Behavior on color { ColorAnimation { duration: 110 } }
Label {
id: label
anchors.centerIn: parent
text: chip.text
color: chip.active ? "white" : Theme.ink
font.pixelSize: 12
font.weight: chip.active ? Font.DemiBold : Font.Medium
font.family: Theme.sansFamily
}
HoverHandler { id: chipHover; cursorShape: Qt.PointingHandCursor }
TapHandler {
grabPermissions: PointerHandler.TakeOverForbidden
onTapped: chip.clicked()
}
}