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

+194 -147
View File
@@ -1,8 +1,9 @@
// The timer page: the dial, the controls, and what you are working on.
// The timer: the dial, the controls, and what you are working on.
import QtQuick
import QtQuick.Controls.Material
import QtQuick.Controls
import QtQuick.Layouts
import "."
import "components"
ScrollView {
@@ -10,149 +11,165 @@ ScrollView {
contentWidth: availableWidth
clip: true
property var openTasks: {
readonly property var openTasks: {
var list = []
for (var i = 0; i < controller.tasks.length; i++)
if (!controller.tasks[i].is_done) list.push(controller.tasks[i])
return list
}
function suggestion() {
var buckets = ["overdue", "today", "upcoming", "someday"]
for (var b = 0; b < buckets.length; b++)
for (var i = 0; i < openTasks.length; i++)
if (openTasks[i].pinned || openTasks[i].bucket === buckets[b]) return openTasks[i]
return null
}
ColumnLayout {
width: page.availableWidth
spacing: 16
anchors.margins: 20
width: page.availableWidth - 56
x: 28
y: 26
spacing: 22
PageHeader {
eyebrow: Qt.formatDate(new Date(), "dddd, d MMMM")
title: !controller.timerEnabled ? "View mode" : (controller.running ? controller.kindLabel : "Ready when you are")
caption: !controller.timerEnabled
? "Timer is disabled · view-only mode"
: (controller.taskTitle !== ""
? "Tracking “" + controller.taskTitle + "”"
: "Cycle " + controller.cyclePosition + " of " + controller.cycleLength
+ " before a long break")
GhostButton {
visible: controller.timerEnabled && controller.running && controller.status !== "paused"
text: "Pause"
onClicked: controller.pause()
}
GhostButton {
visible: controller.timerEnabled && controller.running && controller.status === "paused"
text: "Resume"
onClicked: controller.resume()
}
PrimaryButton {
visible: controller.timerEnabled && controller.running
text: "Finish"
onClicked: controller.complete()
}
PrimaryButton {
visible: controller.timerEnabled && !controller.running
text: "Start focus"
onClicked: controller.startWork()
}
GhostButton {
visible: controller.timerEnabled && !controller.running
text: controller.nextKindLabel
onClicked: controller.startBreak()
}
GhostButton {
text: controller.timerEnabled ? "Turn off timer" : "Turn on timer"
onClicked: controller.setTimerEnabled(!controller.timerEnabled)
}
}
GridLayout {
Layout.fillWidth: true
Layout.margins: 20
columns: page.width > 900 ? 2 : 1
columnSpacing: 16
rowSpacing: 16
columns: page.width > 940 ? 2 : 1
columnSpacing: 22
rowSpacing: 22
// ---- dial + controls --------------------------------------------
Card {
Panel {
Layout.fillWidth: true
Layout.preferredWidth: 1
padding: 28
TimerDial {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: Math.min(300, page.availableWidth * 0.7)
Layout.preferredWidth: Math.min(290, page.availableWidth * 0.62)
Layout.preferredHeight: Layout.preferredWidth
progress: controller.progress
clockText: controller.clockText
label: controller.running ? controller.kindLabel : "Next: " + controller.nextKindLabel
sublabel: controller.running
? (controller.taskTitle !== "" ? controller.taskTitle : " ")
: "Press start when you are ready"
accent: window.kindColor(controller.kind)
progress: controller.timerEnabled ? controller.progress : 0
clockText: controller.timerEnabled ? controller.clockText : "—"
minutes: Math.max(1, Math.round(controller.plannedSeconds / 60))
label: !controller.timerEnabled ? "View mode" : (controller.running ? controller.kindLabel : "Next · " + controller.nextKindLabel)
sublabel: !controller.timerEnabled
? "Timer disabled"
: (controller.taskTitle !== ""
? controller.taskTitle
: (controller.running ? "" : "Pick something below, or just begin"))
accent: controller.timerEnabled ? Theme.kindColor(controller.kind) : Theme.muted
paused: controller.status === "paused"
idle: !controller.timerEnabled || !controller.running
}
Flow {
RowLayout {
visible: controller.timerEnabled && controller.running
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 8
spacing: 8
Layout.topMargin: 14
spacing: 6
PrimaryButton {
visible: !controller.running
text: "▶ Start focus"
onClicked: controller.startWork()
}
Button {
visible: !controller.running
text: "☕ " + controller.nextKindLabel
onClicked: controller.startBreak()
}
Button {
visible: controller.running && controller.status !== "paused"
text: "⏸ Pause"
onClicked: controller.pause()
}
PrimaryButton {
visible: controller.running && controller.status === "paused"
text: "▶ Resume"
onClicked: controller.resume()
}
PrimaryButton {
visible: controller.running
text: "✓ Done"
onClicked: controller.complete()
}
Button {
visible: controller.running
text: "+5"
GhostButton {
text: "+5 min"
onClicked: controller.extend(5)
ToolTip.visible: hovered
ToolTip.text: "Add five minutes"
}
Button {
visible: controller.running
text: "🔔"
GhostButton {
text: "Interruption"
onClicked: controller.interrupt()
ToolTip.visible: hovered
ToolTip.text: "Count an interruption without stopping"
ToolTip.text: "Count a distraction without stopping the clock"
}
Button {
visible: controller.running
text: "⏹"
flat: true
Material.foreground: "#e34948"
GhostButton {
text: "Abandon"
onClicked: abandonDialog.open()
ToolTip.visible: hovered
ToolTip.text: "Abandon this interval"
}
}
Label {
Layout.alignment: Qt.AlignHCenter
visible: controller.interruptions > 0
text: controller.interruptions + " interruption(s) this interval"
visible: controller.timerEnabled && controller.interruptions > 0
text: controller.interruptions + " interruption(s) so far"
color: Theme.faint
font.pixelSize: 11
opacity: 0.6
font.family: Theme.sansFamily
}
Label {
Layout.alignment: Qt.AlignHCenter
text: "Space start/pause · Ctrl+Enter finish · Ctrl+E +5 min"
visible: controller.timerEnabled
text: "Space start/pause · Ctrl+Enter finish · Ctrl+E +5 min"
color: Theme.faint
font.pixelSize: 10
opacity: 0.45
font.family: Theme.sansFamily
}
}
// ---- today + task picker ----------------------------------------
ColumnLayout {
Layout.fillWidth: true
Layout.preferredWidth: 1
Layout.alignment: Qt.AlignTop
spacing: 16
spacing: 20
Card {
title: "Today"
subtitle: "Cycle " + controller.cyclePosition + " of " + controller.cycleLength
+ " before a long break"
ColumnLayout {
Layout.fillWidth: true
spacing: 10
SectionTitle { text: "Today" }
RowLayout {
Layout.fillWidth: true
spacing: 8
Label {
text: controller.completedToday
font.pixelSize: 34
font.weight: Font.Light
color: Theme.ink
font.pixelSize: 40
font.family: Theme.displayFamily
font.letterSpacing: -1
}
Label {
text: "/ " + controller.goal + " pomodoros"
font.pixelSize: 16
opacity: 0.7
text: "of " + controller.goal + " pomodoros"
color: Theme.muted
font.pixelSize: 14
font.family: Theme.sansFamily
Layout.alignment: Qt.AlignBottom
Layout.bottomMargin: 6
Layout.bottomMargin: 7
}
Item { Layout.fillWidth: true }
}
ProgressBar {
Layout.fillWidth: true
from: 0
to: Math.max(controller.goal, 1)
value: Math.min(controller.completedToday, controller.goal)
}
Flow {
@@ -162,27 +179,45 @@ ScrollView {
model: Math.max(controller.goal, controller.completedToday)
delegate: Rectangle {
required property int index
width: 14; height: 14; radius: 7
color: index < controller.completedToday ? window.workColor : "transparent"
width: 11; height: 11; radius: 5.5
color: index < controller.completedToday ? Theme.work : "transparent"
border.width: index < controller.completedToday ? 0 : 1
border.color: Material.hintTextColor
border.color: Theme.rule
}
}
}
Label {
Layout.fillWidth: true
text: controller.stats.today
? controller.stats.today.focus_minutes + "m focused · "
+ controller.stats.today.break_minutes + "m rested"
+ (controller.stats.today.longest_stretch_minutes > 0
? " · longest run " + controller.stats.today.longest_stretch_minutes + "m" : "")
: ""
color: Theme.muted
font.pixelSize: 12
font.family: Theme.sansFamily
wrapMode: Text.WordWrap
}
}
Card {
title: "What are you working on?"
ColumnLayout {
Layout.fillWidth: true
spacing: 10
SectionTitle { text: "Working on" }
ComboBox {
id: taskPicker
Layout.fillWidth: true
font.pixelSize: 13
font.family: Theme.sansFamily
model: {
var names = ["No task"]
var names = ["Nothing in particular"]
for (var i = 0; i < page.openTasks.length; i++)
names.push((page.openTasks[i].pinned ? "📌 " : "")
names.push((page.openTasks[i].pinned ? "★ " : "")
+ page.openTasks[i].title
+ (page.openTasks[i].due_date ? " · " + page.openTasks[i].due_date : ""))
+ (page.openTasks[i].due_date ? " · " + page.openTasks[i].due_date : ""))
return names
}
onActivated: function (index) {
@@ -190,65 +225,76 @@ ScrollView {
}
}
Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
font.pixelSize: 12
opacity: 0.7
text: controller.taskTitle !== ""
? "This interval is tracked against “" + controller.taskTitle + "”."
: "Pick a task so the daily report knows where the time went."
}
RowLayout {
Layout.fillWidth: true
spacing: 8
spacing: 14
Label {
text: "🔴 " + (controller.taskCounts.overdue || 0) + " overdue"
font.pixelSize: 12
text: (controller.taskCounts.overdue || 0) + " overdue"
color: (controller.taskCounts.overdue || 0) > 0 ? Theme.clay : Theme.muted
font.pixelSize: 11
font.family: Theme.sansFamily
}
Label {
text: "🟡 " + (controller.taskCounts.today || 0) + " due today"
font.pixelSize: 12
text: (controller.taskCounts.today || 0) + " due today"
color: Theme.muted
font.pixelSize: 11
font.family: Theme.sansFamily
}
Item { Layout.fillWidth: true }
Button { text: "Tasks"; flat: true; onClicked: window.page = 1 }
GhostButton { text: "All tasks"; onClicked: window.page = 1 }
}
}
}
}
// ---- the day's numbers ------------------------------------------------
GridLayout {
Layout.fillWidth: true
Layout.leftMargin: 20
Layout.rightMargin: 20
Layout.bottomMargin: 20
columns: page.width > 760 ? 4 : 2
columnSpacing: 12
rowSpacing: 12
// ---- the day's numbers -------------------------------------------------
Panel {
padding: 0
RowLayout {
Layout.fillWidth: true
spacing: 0
StatTile {
label: "Focus today"
value: controller.stats.today ? controller.stats.today.focus_minutes + "m" : "—"
caption: controller.completedToday + " pomodoros"
}
StatTile {
label: "Longest stretch"
value: controller.stats.today ? controller.stats.today.longest_stretch_minutes + "m" : "—"
caption: "unbroken focus"
}
StatTile {
label: "Streak"
value: controller.stats.streak ? controller.stats.streak.current + "d" : "—"
caption: controller.stats.streak ? "best " + controller.stats.streak.best : ""
}
StatTile {
label: "Blocked"
value: controller.stats.today ? controller.stats.today.blocked_attempts : "—"
caption: "temptations turned away"
Repeater {
model: [
{ label: "Focus today",
value: controller.stats.today ? controller.stats.today.focus_minutes + "m" : "—",
caption: controller.completedToday + " intervals" },
{ label: "Interruptions",
value: controller.stats.today ? "" + controller.stats.today.interruptions : "—",
caption: controller.stats.today && controller.stats.today.abandoned > 0
? controller.stats.today.abandoned + " abandoned" : "none dropped" },
{ label: "Closed today",
value: controller.stats.today ? "" + controller.stats.today.tasks_completed_count : "—",
caption: "in your own tools" },
{ label: "Blocked",
value: controller.stats.today ? "" + controller.stats.today.blocked_attempts : "—",
caption: "temptations turned away" }
]
delegate: RowLayout {
required property int index
required property var modelData
Layout.fillWidth: true
spacing: 0
Rectangle {
visible: index > 0
width: 1
Layout.fillHeight: true
Layout.preferredHeight: 70
color: Theme.rule
}
Stat {
Layout.fillWidth: true
Layout.margins: 18
label: modelData.label
value: modelData.value
caption: modelData.caption
}
}
}
}
}
Item { Layout.preferredHeight: 10 }
}
Dialog {
@@ -261,9 +307,12 @@ ScrollView {
ColumnLayout {
anchors.fill: parent
spacing: 10
spacing: 12
Label {
text: controller.clockText + " left. The interval is recorded as abandoned."
text: controller.clockText + " left. It will be recorded as abandoned."
color: Theme.muted
font.pixelSize: 12
font.family: Theme.sansFamily
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
@@ -272,10 +321,8 @@ ScrollView {
Layout.fillWidth: true
placeholderText: "What happened? (optional)"
}
Button {
PrimaryButton {
text: "Abandon"
Material.background: "#e34948"
Material.foreground: "white"
Layout.alignment: Qt.AlignRight
onClicked: { controller.abandon(reasonField.text); reasonField.text = ""; abandonDialog.close() }
}