Files
kadence/desktop/kadence_desktop/qml/TimerPage.qml
T
2026-10-19 11:54:15 +03:00

355 lines
15 KiB
QML

// The timer: the dial, the controls, and what you are working on.
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "."
import "components"
ScrollView {
id: page
contentWidth: availableWidth
clip: true
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 - 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
columns: page.width > 940 ? 2 : 1
columnSpacing: 22
rowSpacing: 22
Panel {
Layout.fillWidth: true
Layout.preferredWidth: 1
padding: 28
TimerDial {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: Math.min(290, page.availableWidth * 0.62)
Layout.preferredHeight: Layout.preferredWidth
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
}
// Switching here takes effect at once — it resizes whatever
// is already running or paused too, not just the next one.
RowLayout {
visible: controller.timerEnabled
&& controller.settings.pomodoro
&& controller.settings.pomodoro.presets
&& controller.settings.pomodoro.presets.length > 0
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 14
spacing: 8
Repeater {
model: controller.settings.pomodoro ? controller.settings.pomodoro.presets : []
delegate: PresetChip {
required property var modelData
required property int index
text: modelData.name
active: controller.activePresetIndex === index
onClicked: controller.applyPreset(index)
}
}
}
RowLayout {
visible: controller.timerEnabled && controller.running
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 14
spacing: 6
GhostButton {
text: "+5 min"
onClicked: controller.extend(5)
}
GhostButton {
text: "Interruption"
onClicked: controller.interrupt()
ToolTip.visible: hovered
ToolTip.text: "Count a distraction without stopping the clock"
}
GhostButton {
text: "Abandon"
onClicked: abandonDialog.open()
}
}
Label {
Layout.alignment: Qt.AlignHCenter
visible: controller.timerEnabled && controller.interruptions > 0
text: controller.interruptions + " interruption(s) so far"
color: Theme.faint
font.pixelSize: 11
font.family: Theme.sansFamily
}
Label {
Layout.alignment: Qt.AlignHCenter
visible: controller.timerEnabled
text: "Space start/pause · Ctrl+Enter finish · Ctrl+E +5 min"
color: Theme.faint
font.pixelSize: 10
font.family: Theme.sansFamily
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.preferredWidth: 1
Layout.alignment: Qt.AlignTop
spacing: 20
ColumnLayout {
Layout.fillWidth: true
spacing: 10
SectionTitle { text: "Today" }
RowLayout {
spacing: 8
Label {
text: controller.completedToday
color: Theme.ink
font.pixelSize: 40
font.family: Theme.displayFamily
font.letterSpacing: -1
}
Label {
text: "of " + controller.goal + " pomodoros"
color: Theme.muted
font.pixelSize: 14
font.family: Theme.sansFamily
Layout.alignment: Qt.AlignBottom
Layout.bottomMargin: 7
}
}
Flow {
Layout.fillWidth: true
spacing: 6
Repeater {
model: Math.max(controller.goal, controller.completedToday)
delegate: Rectangle {
required property int index
width: 11; height: 11; radius: 5.5
color: index < controller.completedToday ? Theme.work : "transparent"
border.width: index < controller.completedToday ? 0 : 1
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
}
}
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 = ["Nothing in particular"]
for (var i = 0; i < page.openTasks.length; i++)
names.push((page.openTasks[i].pinned ? "★ " : "")
+ page.openTasks[i].title
+ (page.openTasks[i].due_date ? " · " + page.openTasks[i].due_date : ""))
return names
}
onActivated: function (index) {
controller.attachTask(index === 0 ? -1 : page.openTasks[index - 1].id)
}
}
RowLayout {
Layout.fillWidth: true
spacing: 14
Label {
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"
color: Theme.muted
font.pixelSize: 11
font.family: Theme.sansFamily
}
Item { Layout.fillWidth: true }
GhostButton { text: "All tasks"; onClicked: window.page = 1 }
}
}
}
}
// ---- the day's numbers -------------------------------------------------
Panel {
padding: 0
RowLayout {
Layout.fillWidth: true
spacing: 0
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 {
id: abandonDialog
anchors.centerIn: Overlay.overlay
width: 380
modal: true
title: "Abandon this interval?"
standardButtons: Dialog.Cancel
ColumnLayout {
anchors.fill: parent
spacing: 12
Label {
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
}
TextField {
id: reasonField
Layout.fillWidth: true
placeholderText: "What happened? (optional)"
}
PrimaryButton {
text: "Abandon"
Layout.alignment: Qt.AlignRight
onClicked: { controller.abandon(reasonField.text); reasonField.text = ""; abandonDialog.close() }
}
}
}
}