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

+161 -135
View File
@@ -1,46 +1,40 @@
// The window.
//
// Qt Quick Controls' Material style, following the session's own light/dark
// preference, with a navigation rail on the left and the pages stacked on the
// right. Everything it shows comes from `controller`, the Python object bound
// into the QML context — there is no second copy of the state here.
// A quiet left column — wordmark, navigation as words, and the two things worth
// knowing from anywhere (what the timer is doing, whether the blocker is on) —
// with the pages on the right. Qt Quick Controls supply the behaviour; the
// chrome comes from `Theme`, so the desktop app and the web app are the same
// product rather than two takes on it.
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Material
import QtQuick.Layouts
import QtQuick.Window
import "."
import "components"
ApplicationWindow {
id: window
width: 1100
height: 760
minimumWidth: 420
minimumHeight: 560
width: 1180
height: 820
minimumWidth: 460
minimumHeight: 600
visible: false
color: Theme.paper
title: controller.running
? controller.clockText + " · " + controller.kindLabel + " — Kadence"
: "Kadence"
Material.theme: Material.System
Material.primary: "#6750A4"
Material.accent: "#6750A4"
Material.theme: Theme.dark ? Material.Dark : Material.Light
Material.primary: Theme.ember
Material.accent: Theme.ember
Material.background: Theme.paper
Material.foreground: Theme.ink
readonly property color workColor: "#2a78d6"
readonly property color shortBreakColor: "#eb6834"
readonly property color longBreakColor: "#1baf7a"
readonly property bool wide: width >= 820
property int page: 0
function kindColor(kind) {
if (kind === "short_break") return shortBreakColor
if (kind === "long_break") return longBreakColor
return workColor
}
// Closing the window keeps the app alive in the tray — the timer is still
// running, and that is the whole point of a tray icon.
onClosing: function (close) {
if (controller.config.close_to_tray && controller.config.tray_icon) {
close.accepted = false
@@ -54,10 +48,9 @@ ApplicationWindow {
function onShowWindowRequested() { window.show(); window.raise(); window.requestActivate() }
}
// ---- shortcuts ---------------------------------------------------------
Shortcut { sequence: "Space"; onActivated: controller.toggle() }
Shortcut { sequence: "Ctrl+Return"; onActivated: controller.complete() }
Shortcut { sequence: "Ctrl+E"; onActivated: controller.extend(5) }
Shortcut { sequence: "Space"; onActivated: if (controller.timerEnabled) controller.toggle() }
Shortcut { sequence: "Ctrl+Return"; onActivated: if (controller.timerEnabled) controller.complete() }
Shortcut { sequence: "Ctrl+E"; onActivated: if (controller.timerEnabled) controller.extend(5) }
Shortcut { sequence: "Ctrl+R"; onActivated: controller.refreshSources() }
Shortcut { sequence: "Ctrl+1"; onActivated: window.page = 0 }
Shortcut { sequence: "Ctrl+2"; onActivated: window.page = 1 }
@@ -65,143 +58,167 @@ ApplicationWindow {
Shortcut { sequence: "Ctrl+4"; onActivated: window.page = 3 }
Shortcut { sequence: "Ctrl+5"; onActivated: window.page = 4 }
Shortcut { sequence: "Ctrl+W"; onActivated: window.hide() }
header: ToolBar {
visible: !window.wide
Material.background: Material.backgroundColor
RowLayout {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
Label {
text: ["Timer", "Tasks", "Blocker", "Report", "Settings"][window.page]
font.pixelSize: 18
font.weight: Font.Medium
Layout.fillWidth: true
}
StatusPill {
running: controller.running
paused: controller.status === "paused"
connected: controller.connected
text: controller.connected ? controller.clockText : "offline"
accent: window.kindColor(controller.kind)
}
}
}
Shortcut { sequence: "Ctrl+M"; onActivated: controller.toggleMini() }
RowLayout {
anchors.fill: parent
spacing: 0
// ---- navigation rail ------------------------------------------------
Pane {
// ---- the rail --------------------------------------------------------
Item {
visible: window.wide
Layout.fillHeight: true
Layout.preferredWidth: 212
Material.elevation: 0
padding: 12
Rectangle {
anchors.right: parent.right
width: 1
height: parent.height
color: Theme.rule
}
ColumnLayout {
anchors.fill: parent
spacing: 4
anchors.margins: 22
spacing: 2
RowLayout {
Layout.bottomMargin: 16
Layout.leftMargin: 8
spacing: 10
Rectangle {
width: 36; height: 36; radius: 18
color: Material.accent
Label { anchors.centerIn: parent; text: "🍅"; font.pixelSize: 18 }
ColumnLayout {
Layout.bottomMargin: 26
spacing: 0
Label {
text: "Kadence"
color: Theme.ink
font.pixelSize: 23
font.family: Theme.displayFamily
font.letterSpacing: -0.5
}
ColumnLayout {
spacing: 0
Label { text: "Kadence"; font.pixelSize: 17; font.weight: Font.DemiBold }
Label {
text: controller.connected ? "connected" : "offline"
font.pixelSize: 11
opacity: 0.6
}
Label {
text: "find your rhythm"
color: Theme.muted
font.pixelSize: 11
font.family: Theme.sansFamily
}
}
Repeater {
model: [
{ label: "Timer", icon: "⏱" },
{ label: "Tasks", icon: "✓" },
{ label: "Blocker", icon: "🛡" },
{ label: "Report", icon: "📊" },
{ label: "Settings", icon: "⚙" }
]
delegate: ItemDelegate {
model: ["Timer", "Tasks", "Blocker", "Report", "Settings"]
delegate: Item {
required property int index
required property var modelData
required property string modelData
Layout.fillWidth: true
height: 44
highlighted: window.page === index
onClicked: window.page = index
implicitHeight: 34
background: Rectangle {
radius: 22
color: parent.highlighted ? Material.accent
: (parent.hovered ? Material.listHighlightColor : "transparent")
opacity: parent.highlighted ? 0.18 : 1
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 2
height: 16
radius: 1
color: Theme.ember
visible: window.page === index
}
contentItem: RowLayout {
spacing: 12
Label { text: modelData.icon; font.pixelSize: 16 }
Label {
text: modelData.label
font.weight: parent.parent.highlighted ? Font.DemiBold : Font.Normal
Layout.fillWidth: true
}
Label {
anchors.left: parent.left
anchors.leftMargin: 14
anchors.verticalCenter: parent.verticalCenter
text: modelData
color: window.page === index ? Theme.ink : (mouse.containsMouse ? Theme.ink : Theme.muted)
font.pixelSize: 14
font.weight: window.page === index ? Font.DemiBold : Font.Normal
font.family: Theme.sansFamily
}
MouseArea {
id: mouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: window.page = index
}
}
}
Item { Layout.fillHeight: true }
StatusPill {
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.rule; Layout.bottomMargin: 10 }
RowLayout {
Layout.fillWidth: true
running: controller.running
paused: controller.status === "paused"
connected: controller.connected
text: controller.connected ? (controller.running ? controller.clockText : "idle") : "offline"
accent: window.kindColor(controller.kind)
spacing: 8
Rectangle {
width: 7; height: 7; radius: 3.5
color: controller.running ? Theme.kindColor(controller.kind) : Theme.faint
SequentialAnimation on opacity {
running: controller.running && controller.status === "running"
loops: Animation.Infinite
NumberAnimation { to: 0.35; duration: 900 }
NumberAnimation { to: 1.0; duration: 900 }
}
}
Label {
text: controller.connected
? (controller.timerEnabled
? (controller.running ? controller.clockText : "Idle")
: "View mode")
: "Offline"
color: Theme.ink
font.pixelSize: 13
font.weight: Font.Medium
font.family: Theme.monoFamily
}
Item { Layout.fillWidth: true }
Label {
text: controller.completedToday + "/" + controller.goal
color: Theme.muted
font.pixelSize: 12
font.family: Theme.sansFamily
}
}
Label {
Layout.fillWidth: true
Layout.topMargin: 6
text: controller.completedToday + "/" + controller.goal + " today"
font.pixelSize: 12
opacity: 0.7
horizontalAlignment: Text.AlignHCenter
}
Label {
Layout.fillWidth: true
visible: controller.blockerActive
text: "🛡 blocking is on"
font.pixelSize: 12
color: "#e34948"
horizontalAlignment: Text.AlignHCenter
text: "● BLOCKING"
color: Theme.clay
font.pixelSize: 10
font.letterSpacing: 1.1
font.weight: Font.DemiBold
font.family: Theme.sansFamily
}
}
}
// ---- pages ----------------------------------------------------------
// ---- pages -----------------------------------------------------------
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 0
Banner {
ToolBar {
visible: !window.wide
Layout.fillWidth: true
visible: !controller.connected
text: controller.connectionError !== ""
? controller.connectionError
: "Cannot reach the backend. Start it with `docker compose up`."
actionText: "Settings"
onActionClicked: window.page = 4
background: Rectangle {
color: Theme.paper
Rectangle { anchors.bottom: parent.bottom; width: parent.width; height: 1; color: Theme.rule }
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 16
anchors.rightMargin: 16
Label {
text: "Kadence"
color: Theme.ink
font.pixelSize: 18
font.family: Theme.displayFamily
Layout.fillWidth: true
}
Label {
text: controller.connected
? (controller.timerEnabled
? (controller.running ? controller.clockText : "Idle")
: "View mode")
: "Offline"
color: Theme.ink
font.pixelSize: 13
font.family: Theme.monoFamily
}
}
}
StackLayout {
@@ -215,20 +232,29 @@ ApplicationWindow {
ReportPage {}
SettingsPage {}
}
TabBar {
visible: !window.wide
Layout.fillWidth: true
currentIndex: window.page
onCurrentIndexChanged: window.page = currentIndex
background: Rectangle {
color: Theme.paper
Rectangle { width: parent.width; height: 1; color: Theme.rule }
}
TabButton { text: "Timer" }
TabButton { text: "Tasks" }
TabButton { text: "Blocker" }
TabButton { text: "Report" }
TabButton { text: "Settings" }
}
}
}
// ---- footer: bottom navigation on a narrow window ----------------------
footer: TabBar {
visible: !window.wide
currentIndex: window.page
onCurrentIndexChanged: window.page = currentIndex
TabButton { text: "⏱" }
TabButton { text: "✓" }
TabButton { text: "🛡" }
TabButton { text: "📊" }
TabButton { text: "⚙" }
}
Snackbar { id: snackbar }
// The floating mini timer is created by app.py as its own top-level window:
// nested inside this one it would be transient, and a transient window
// cannot be shown while its parent is hidden — which is precisely when the
// mini timer earns its keep.
}