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