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
|
# 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):
|
class PomodoroSettings(BaseModel):
|
||||||
"""Lengths and automation of the timer itself. All durations in minutes."""
|
"""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)
|
long_break_minutes: int = Field(default=15, ge=1, le=240)
|
||||||
cycles_before_long_break: int = Field(default=4, ge=1, le=12)
|
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
|
auto_start_breaks: bool = True
|
||||||
"""Roll straight into the break when a work interval ends."""
|
"""Roll straight into the break when a work interval ends."""
|
||||||
|
|
||||||
|
|||||||
@@ -128,7 +128,12 @@ class KadenceApp(QObject):
|
|||||||
config = self._config
|
config = self._config
|
||||||
wanted = bool(config.mini_timer) and (
|
wanted = bool(config.mini_timer) and (
|
||||||
self._controller.miniVisible
|
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 bool(self._mini.property("visible")) != wanted:
|
||||||
if wanted:
|
if wanted:
|
||||||
|
|||||||
@@ -80,6 +80,14 @@ class Controller(QObject):
|
|||||||
self._completing = False
|
self._completing = False
|
||||||
self._paused_by_lock = False
|
self._paused_by_lock = False
|
||||||
self._mini_visible = 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 -------------------------------------------
|
# ---- system integrations -------------------------------------------
|
||||||
self._notifier = Notifier()
|
self._notifier = Notifier()
|
||||||
@@ -282,6 +290,11 @@ class Controller(QObject):
|
|||||||
"""Whether the floating mini timer is pinned open by the user."""
|
"""Whether the floating mini timer is pinned open by the user."""
|
||||||
return self._mini_visible
|
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)
|
@pyqtProperty("QVariantMap", notify=configChanged)
|
||||||
def config(self) -> dict[str, Any]:
|
def config(self) -> dict[str, Any]:
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
@@ -463,6 +476,7 @@ class Controller(QObject):
|
|||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
@pyqtSlot(bool)
|
@pyqtSlot(bool)
|
||||||
def setMiniVisible(self, visible: bool = True) -> None:
|
def setMiniVisible(self, visible: bool = True) -> None:
|
||||||
|
self._mini_dismissed = not visible
|
||||||
if visible == self._mini_visible:
|
if visible == self._mini_visible:
|
||||||
return
|
return
|
||||||
self._mini_visible = visible
|
self._mini_visible = visible
|
||||||
@@ -610,6 +624,11 @@ class Controller(QObject):
|
|||||||
# Transitions → the system integrations
|
# Transitions → the system integrations
|
||||||
# ======================================================================
|
# ======================================================================
|
||||||
def _on_transition(self, previous: dict[str, Any], current: dict[str, Any]) -> None:
|
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"
|
focusing = current.get("kind") == "work" and current.get("status") == "running"
|
||||||
|
|
||||||
if focusing:
|
if focusing:
|
||||||
|
|||||||
@@ -78,7 +78,10 @@ Window {
|
|||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
}
|
}
|
||||||
HoverHandler { id: buttonHover; cursorShape: Qt.PointingHandCursor }
|
HoverHandler { id: buttonHover; cursorShape: Qt.PointingHandCursor }
|
||||||
TapHandler { onTapped: button.clicked() }
|
TapHandler {
|
||||||
|
grabPermissions: PointerHandler.TakeOverForbidden
|
||||||
|
onTapped: button.clicked()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -151,7 +154,14 @@ Window {
|
|||||||
opacity: hover.hovered ? 1 : 0.35
|
opacity: hover.hovered ? 1 : 0.35
|
||||||
Behavior on opacity { NumberAnimation { duration: 120 } }
|
Behavior on opacity { NumberAnimation { duration: 120 } }
|
||||||
HoverHandler { id: closeHover; cursorShape: Qt.PointingHandCursor }
|
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 } })
|
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 {
|
component Field: ColumnLayout {
|
||||||
property string label: ""
|
property string label: ""
|
||||||
default property alias content: holder.data
|
default property alias content: holder.data
|
||||||
@@ -104,6 +127,70 @@ ScrollView {
|
|||||||
spacing: 12
|
spacing: 12
|
||||||
SectionTitle { text: "Timer" }
|
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 {
|
Flow {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: 20
|
spacing: 20
|
||||||
|
|||||||
Reference in new issue
Block a user