various fixes

This commit is contained in:
timka123 committed 2026-10-19 09:19:47 +03:00
1 parent 77b26757bf
commit 4bdbc8bfa8
5 files changed
+148 -3

No files matched your search

+24
View File
@@ -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."""
+6 -1
View File
@@ -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:
+19
View File
@@ -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:
+12 -2
View File
@@ -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