355 lines
15 KiB
QML
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() }
|
|
}
|
|
}
|
|
}
|
|
}
|