285 lines
11 KiB
QML
285 lines
11 KiB
QML
// The timer page: the dial, the controls, and what you are working on.
|
|
import QtQuick
|
|
import QtQuick.Controls.Material
|
|
import QtQuick.Layouts
|
|
|
|
import "components"
|
|
|
|
ScrollView {
|
|
id: page
|
|
contentWidth: availableWidth
|
|
clip: true
|
|
|
|
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
|
|
}
|
|
|
|
ColumnLayout {
|
|
width: page.availableWidth
|
|
spacing: 16
|
|
anchors.margins: 20
|
|
|
|
GridLayout {
|
|
Layout.fillWidth: true
|
|
Layout.margins: 20
|
|
columns: page.width > 900 ? 2 : 1
|
|
columnSpacing: 16
|
|
rowSpacing: 16
|
|
|
|
// ---- dial + controls --------------------------------------------
|
|
Card {
|
|
Layout.fillWidth: true
|
|
Layout.preferredWidth: 1
|
|
|
|
TimerDial {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
Layout.preferredWidth: Math.min(300, page.availableWidth * 0.7)
|
|
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)
|
|
paused: controller.status === "paused"
|
|
}
|
|
|
|
Flow {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
Layout.topMargin: 8
|
|
spacing: 8
|
|
|
|
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"
|
|
onClicked: controller.extend(5)
|
|
ToolTip.visible: hovered
|
|
ToolTip.text: "Add five minutes"
|
|
}
|
|
Button {
|
|
visible: controller.running
|
|
text: "🔔"
|
|
onClicked: controller.interrupt()
|
|
ToolTip.visible: hovered
|
|
ToolTip.text: "Count an interruption without stopping"
|
|
}
|
|
Button {
|
|
visible: controller.running
|
|
text: "⏹"
|
|
flat: true
|
|
Material.foreground: "#e34948"
|
|
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"
|
|
font.pixelSize: 11
|
|
opacity: 0.6
|
|
}
|
|
Label {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
text: "Space start/pause · Ctrl+Enter finish · Ctrl+E +5 min"
|
|
font.pixelSize: 10
|
|
opacity: 0.45
|
|
}
|
|
}
|
|
|
|
// ---- today + task picker ----------------------------------------
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
Layout.preferredWidth: 1
|
|
Layout.alignment: Qt.AlignTop
|
|
spacing: 16
|
|
|
|
Card {
|
|
title: "Today"
|
|
subtitle: "Cycle " + controller.cyclePosition + " of " + controller.cycleLength
|
|
+ " before a long break"
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 8
|
|
Label {
|
|
text: controller.completedToday
|
|
font.pixelSize: 34
|
|
font.weight: Font.Light
|
|
}
|
|
Label {
|
|
text: "/ " + controller.goal + " pomodoros"
|
|
font.pixelSize: 16
|
|
opacity: 0.7
|
|
Layout.alignment: Qt.AlignBottom
|
|
Layout.bottomMargin: 6
|
|
}
|
|
Item { Layout.fillWidth: true }
|
|
}
|
|
|
|
ProgressBar {
|
|
Layout.fillWidth: true
|
|
from: 0
|
|
to: Math.max(controller.goal, 1)
|
|
value: Math.min(controller.completedToday, controller.goal)
|
|
}
|
|
|
|
Flow {
|
|
Layout.fillWidth: true
|
|
spacing: 6
|
|
Repeater {
|
|
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"
|
|
border.width: index < controller.completedToday ? 0 : 1
|
|
border.color: Material.hintTextColor
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Card {
|
|
title: "What are you working on?"
|
|
|
|
ComboBox {
|
|
id: taskPicker
|
|
Layout.fillWidth: true
|
|
model: {
|
|
var names = ["No task"]
|
|
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)
|
|
}
|
|
}
|
|
|
|
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
|
|
Label {
|
|
text: "🔴 " + (controller.taskCounts.overdue || 0) + " overdue"
|
|
font.pixelSize: 12
|
|
}
|
|
Label {
|
|
text: "🟡 " + (controller.taskCounts.today || 0) + " due today"
|
|
font.pixelSize: 12
|
|
}
|
|
Item { Layout.fillWidth: true }
|
|
Button { text: "Tasks"; flat: true; 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
|
|
|
|
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"
|
|
}
|
|
}
|
|
}
|
|
|
|
Dialog {
|
|
id: abandonDialog
|
|
anchors.centerIn: Overlay.overlay
|
|
width: 380
|
|
modal: true
|
|
title: "Abandon this interval?"
|
|
standardButtons: Dialog.Cancel
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
spacing: 10
|
|
Label {
|
|
text: controller.clockText + " left. The interval is recorded as abandoned."
|
|
wrapMode: Text.WordWrap
|
|
Layout.fillWidth: true
|
|
}
|
|
TextField {
|
|
id: reasonField
|
|
Layout.fillWidth: true
|
|
placeholderText: "What happened? (optional)"
|
|
}
|
|
Button {
|
|
text: "Abandon"
|
|
Material.background: "#e34948"
|
|
Material.foreground: "white"
|
|
Layout.alignment: Qt.AlignRight
|
|
onClicked: { controller.abandon(reasonField.text); reasonField.text = ""; abandonDialog.close() }
|
|
}
|
|
}
|
|
}
|
|
}
|