refactor: enhance TimerPage layout and functionality; update theme and tokens for improved design consistency

- Refactored TimerPage component to improve layout and user experience.
- Updated theme configuration to utilize design tokens for better maintainability.
- Introduced new tokens for colors and typography to align with design principles.
- Simplified state management and improved timer functionality.
- Enhanced accessibility and responsiveness of UI elements.
This commit is contained in:
timka123 committed 2026-10-18 23:57:36 +03:00
1 parent 17a0a3c4c9
commit 77b26757bf
84 files changed
+4501 -2682

No files matched your search

+103 -6
View File
@@ -25,7 +25,7 @@ from PyQt6.QtGui import QDesktopServices
from kadence_desktop.api import Api
from kadence_desktop.blocking.manager import BlockerManager
from kadence_desktop.config import DesktopConfig
from kadence_desktop.integrations import autostart
from kadence_desktop.integrations import autostart, keep_above
from kadence_desktop.integrations.inhibit import DoNotDisturb, ScreensaverInhibitor
from kadence_desktop.integrations.launcher import LauncherEntry
from kadence_desktop.integrations.mpris import MediaController
@@ -53,6 +53,8 @@ class Controller(QObject):
blockerChanged = pyqtSignal()
connectionChanged = pyqtSignal()
configChanged = pyqtSignal()
miniChanged = pyqtSignal()
miniPlacementChanged = pyqtSignal()
toast = pyqtSignal(str)
showWindowRequested = pyqtSignal()
@@ -77,6 +79,7 @@ class Controller(QObject):
self._last_status = ""
self._completing = False
self._paused_by_lock = False
self._mini_visible = False
# ---- system integrations -------------------------------------------
self._notifier = Notifier()
@@ -119,6 +122,9 @@ class Controller(QObject):
self._tick.timeout.connect(self._on_tick)
self._tick.start()
# A Wayland compositor ignores "stay on top", so ask KWin properly.
self.ensure_keep_above()
self._idle_check = QTimer(self)
self._idle_check.setInterval(30_000)
self._idle_check.timeout.connect(self._check_idle)
@@ -219,6 +225,14 @@ class Controller(QObject):
def nextKindLabel(self) -> str:
return KIND_LABELS.get(str(self._state.get("next_kind", "work")), "Focus")
@pyqtProperty(bool, notify=settingsChanged)
def timerEnabled(self) -> bool:
"""When False the app is in view-only mode — no timer controls."""
if self._config.view_only:
return False
pomodoro = self._settings.get("pomodoro", {})
return bool(pomodoro.get("timer_enabled", True))
@pyqtProperty("QVariantList", notify=tasksChanged)
def tasks(self) -> list[dict[str, Any]]:
return self._tasks
@@ -263,6 +277,11 @@ class Controller(QObject):
def blockerError(self) -> str:
return self._blocker.last_error or ""
@pyqtProperty(bool, notify=miniChanged)
def miniVisible(self) -> bool:
"""Whether the floating mini timer is pinned open by the user."""
return self._mini_visible
@pyqtProperty("QVariantMap", notify=configChanged)
def config(self) -> dict[str, Any]:
from dataclasses import asdict
@@ -270,6 +289,8 @@ class Controller(QObject):
data = asdict(self._config)
data["autostart"] = autostart.autostart_enabled()
data["service_enabled"] = autostart.service_enabled()
data["keep_above_supported"] = keep_above.supported()
data["keep_above_installed"] = keep_above.installed()
return data
# ======================================================================
@@ -289,6 +310,8 @@ class Controller(QObject):
@pyqtSlot()
@pyqtSlot(int)
def startWork(self, minutes: int = 0) -> None:
if not self.timerEnabled:
return
args: dict[str, Any] = {"kind": "work"}
if minutes:
args["minutes"] = minutes
@@ -301,12 +324,16 @@ class Controller(QObject):
@pyqtSlot()
def startBreak(self) -> None:
if not self.timerEnabled:
return
kind = str(self._state.get("next_kind", "short_break"))
self._send("start", {"kind": "long_break" if kind == "long_break" else "short_break"})
@pyqtSlot()
def toggle(self) -> None:
"""One verb for a global hotkey: start → pause → resume."""
if not self.timerEnabled:
return
if not self.running:
self.startWork()
elif self.status == "paused":
@@ -314,6 +341,14 @@ class Controller(QObject):
else:
self.pause()
@pyqtSlot(bool)
def setTimerEnabled(self, enabled: bool) -> None:
"""Toggle view-only mode on or off."""
self._config.update(view_only=not enabled)
self.configChanged.emit()
self.settingsChanged.emit()
self.patchSettings({"pomodoro": {"timer_enabled": enabled}})
@pyqtSlot()
def pause(self) -> None:
self._send("pause")
@@ -345,8 +380,9 @@ class Controller(QObject):
self._send("attach_task", {"task_id": task_id if task_id >= 0 else None})
@pyqtSlot(int, bool)
def setTaskDone(self, task_id: int, done: bool) -> None:
self._send("task_done", {"task_id": task_id, "done": done})
def setTaskPinned(self, task_id: int, pinned: bool) -> None:
"""Pinning is the only task edit this app makes — closing one is not."""
self._send("patch_task", {"task_id": task_id, "patch": {"pinned": pinned}})
@pyqtSlot(str, str)
def createTask(self, title: str, due_date: str) -> None:
@@ -413,10 +449,66 @@ class Controller(QObject):
self._blocker.enforce_hosts = bool(value)
elif key == "block_apps":
self._blocker.enforce_apps = bool(value)
elif key == "mini_timer_keep_above":
ok, message = keep_above.apply() if value else keep_above.remove()
self.toast.emit(message)
if not ok:
self._config.update(mini_timer_keep_above=not value)
elif key in ("mini_timer_corner", "mini_timer_margin"):
self.miniPlacementChanged.emit()
elif key == "enforce_blocking" and not value:
self._blocker.release()
self.configChanged.emit()
@pyqtSlot()
@pyqtSlot(bool)
def setMiniVisible(self, visible: bool = True) -> None:
if visible == self._mini_visible:
return
self._mini_visible = visible
self.miniChanged.emit()
@pyqtSlot(result=str)
def toggleMini(self) -> str:
"""Tray menu, Ctrl+M and the D-Bus verb all land here."""
if not self._config.mini_timer:
self._config.update(mini_timer=True)
self.configChanged.emit()
self.setMiniVisible(not self._mini_visible)
if self._mini_visible:
self.ensure_keep_above()
return "shown" if self._mini_visible else "hidden"
def ensure_keep_above(
self, position: tuple[int, int] | None = None, *, announce: bool = False
) -> None:
"""Install or refresh the KWin rule, including where to place the window.
A no-op when the rule already says exactly this, so it can be called
freely — it only ever touches KWin when something really changed.
"""
config = self._config
if not (config.mini_timer and config.mini_timer_keep_above):
return
if not keep_above.supported() or keep_above.up_to_date(position):
return
ok, message = keep_above.apply(position)
log.info("keep-above rule: %s", message)
if announce or not ok:
self.toast.emit(message)
self.configChanged.emit()
@pyqtSlot(int, int)
def saveMiniPosition(self, x: int, y: int) -> None:
"""Remember where it was dragged to — only in "remember" mode.
In corner mode the app places the window itself, and recording that
placement would quietly overwrite the position you had dragged it to.
"""
if self._config.mini_timer_corner != "remember":
return
self._config.update(mini_timer_x=int(x), mini_timer_y=int(y))
@pyqtSlot()
def openWebUi(self) -> None:
QDesktopServices.openUrl(QUrl(self._config.server_url))
@@ -431,6 +523,8 @@ class Controller(QObject):
"""One line for the tray tooltip and the D-Bus `Status` method."""
if not self._connected:
return "Kadence — backend unreachable"
if not self.timerEnabled:
return f"Kadence — view mode · {self.completedToday}/{self.goal} today"
if not self.running:
return f"Kadence — idle · {self.completedToday}/{self.goal} today"
return (
@@ -503,11 +597,14 @@ class Controller(QObject):
self.toast.emit("Daily report sent" if result.get("sent") else "Report not sent — check Telegram settings")
elif name == "refresh_sources":
failed = [item for item in result.get("sources", []) if not item.get("ok")]
closed = result.get("closed", 0)
self.toast.emit(
f"{len(failed)} source(s) failed" if failed else f"{result.get('total', 0)} tasks pulled"
f"{len(failed)} source(s) failed"
if failed
else f"{result.get('total', 0)} open · {closed} closed since last refresh"
)
elif name == "task_done" and result.get("warning"):
self.toast.emit(str(result["warning"]))
elif name == "refresh_sources" and result.get("closed"):
self.toast.emit(f"{result['closed']} task(s) closed in your tools")
# ======================================================================
# Transitions → the system integrations