various fixes
This commit is contained in:
5 files changed
+148
-3
No files matched your search
@@ -20,6 +20,27 @@ from pydantic import BaseModel, Field, field_validator
|
||||
# --------------------------------------------------------------------------
|
||||
# Timer
|
||||
# --------------------------------------------------------------------------
|
||||
class TimerPreset(BaseModel):
|
||||
"""A named, user-editable recipe of durations the UI can apply in one tap."""
|
||||
|
||||
name: str
|
||||
work_minutes: int = Field(default=25, ge=1, le=240)
|
||||
short_break_minutes: int = Field(default=5, ge=1, le=120)
|
||||
long_break_minutes: int = Field(default=15, ge=1, le=240)
|
||||
cycles_before_long_break: int = Field(default=4, ge=1, le=12)
|
||||
|
||||
|
||||
def _default_timer_presets() -> list[TimerPreset]:
|
||||
return [
|
||||
TimerPreset(
|
||||
name="Classic", work_minutes=45, short_break_minutes=15, long_break_minutes=30
|
||||
),
|
||||
TimerPreset(
|
||||
name="Пара", work_minutes=85, short_break_minutes=10, long_break_minutes=10
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class PomodoroSettings(BaseModel):
|
||||
"""Lengths and automation of the timer itself. All durations in minutes."""
|
||||
|
||||
@@ -28,6 +49,9 @@ class PomodoroSettings(BaseModel):
|
||||
long_break_minutes: int = Field(default=15, ge=1, le=240)
|
||||
cycles_before_long_break: int = Field(default=4, ge=1, le=12)
|
||||
|
||||
presets: list[TimerPreset] = Field(default_factory=_default_timer_presets)
|
||||
"""Saved work/break combinations, applied to the fields above in one tap."""
|
||||
|
||||
auto_start_breaks: bool = True
|
||||
"""Roll straight into the break when a work interval ends."""
|
||||
|
||||
|
||||
@@ -128,7 +128,12 @@ class KadenceApp(QObject):
|
||||
config = self._config
|
||||
wanted = bool(config.mini_timer) and (
|
||||
self._controller.miniVisible
|
||||
or (config.mini_timer_auto and self._controller.running and not self._window.isVisible())
|
||||
or (
|
||||
config.mini_timer_auto
|
||||
and self._controller.running
|
||||
and not self._window.isVisible()
|
||||
and not self._controller.mini_dismissed
|
||||
)
|
||||
)
|
||||
if bool(self._mini.property("visible")) != wanted:
|
||||
if wanted:
|
||||
|
||||
@@ -80,6 +80,14 @@ class Controller(QObject):
|
||||
self._completing = False
|
||||
self._paused_by_lock = False
|
||||
self._mini_visible = False
|
||||
self._mini_dismissed = False
|
||||
"""Set when the ✕ on the mini timer was clicked during a running interval.
|
||||
|
||||
`app._update_mini()` would otherwise bring it straight back up through
|
||||
its "auto-show while running and the window is away" rule, making the
|
||||
close button look broken. Cleared on the next explicit show and on the
|
||||
next interval, so a dismissal only lasts for the one you closed it on.
|
||||
"""
|
||||
|
||||
# ---- system integrations -------------------------------------------
|
||||
self._notifier = Notifier()
|
||||
@@ -282,6 +290,11 @@ class Controller(QObject):
|
||||
"""Whether the floating mini timer is pinned open by the user."""
|
||||
return self._mini_visible
|
||||
|
||||
@property
|
||||
def mini_dismissed(self) -> bool:
|
||||
"""For `app._update_mini()`, not QML: see `_mini_dismissed` above."""
|
||||
return self._mini_dismissed
|
||||
|
||||
@pyqtProperty("QVariantMap", notify=configChanged)
|
||||
def config(self) -> dict[str, Any]:
|
||||
from dataclasses import asdict
|
||||
@@ -463,6 +476,7 @@ class Controller(QObject):
|
||||
@pyqtSlot()
|
||||
@pyqtSlot(bool)
|
||||
def setMiniVisible(self, visible: bool = True) -> None:
|
||||
self._mini_dismissed = not visible
|
||||
if visible == self._mini_visible:
|
||||
return
|
||||
self._mini_visible = visible
|
||||
@@ -610,6 +624,11 @@ class Controller(QObject):
|
||||
# Transitions → the system integrations
|
||||
# ======================================================================
|
||||
def _on_transition(self, previous: dict[str, Any], current: dict[str, Any]) -> None:
|
||||
if current.get("id") and current.get("id") != previous.get("id"):
|
||||
# A new interval — pause/resume of the same one must not do this —
|
||||
# gets its own chance to auto-show, even if the last one was closed.
|
||||
self._mini_dismissed = False
|
||||
|
||||
focusing = current.get("kind") == "work" and current.get("status") == "running"
|
||||
|
||||
if focusing:
|
||||
|
||||
@@ -78,7 +78,10 @@ Window {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
HoverHandler { id: buttonHover; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler { onTapped: button.clicked() }
|
||||
TapHandler {
|
||||
grabPermissions: PointerHandler.TakeOverForbidden
|
||||
onTapped: button.clicked()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@@ -151,7 +154,14 @@ Window {
|
||||
opacity: hover.hovered ? 1 : 0.35
|
||||
Behavior on opacity { NumberAnimation { duration: 120 } }
|
||||
HoverHandler { id: closeHover; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler { onTapped: controller.setMiniVisible(false) }
|
||||
// The card's DragHandler has CanTakeOverFromAnything so the whole
|
||||
// pill is draggable; without refusing the take-over here, the tiny
|
||||
// press-move jitter of a real click steals this tap into a
|
||||
// startSystemMove() and the close button never fires.
|
||||
TapHandler {
|
||||
grabPermissions: PointerHandler.TakeOverForbidden
|
||||
onTapped: controller.setMiniVisible(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,29 @@ 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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
component Field: ColumnLayout {
|
||||
property string label: ""
|
||||
default property alias content: holder.data
|
||||
@@ -104,6 +127,70 @@ 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 {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Repeater {
|
||||
model: page.pomodoro && page.pomodoro.presets ? page.pomodoro.presets : []
|
||||
|
||||
RowLayout {
|
||||
id: presetRow
|
||||
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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 20
|
||||
|
||||
Reference in new issue
Block a user