- 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.
121 lines
3.9 KiB
QML
121 lines
3.9 KiB
QML
// The dial.
|
|
//
|
|
// A hairline ring with one tick per minute inside it: the ticks light up as the
|
|
// minutes are spent, so the dial reads as "how much is left" at a glance and as
|
|
// "how long was this?" on a second look. The time is the hero, set in the
|
|
// display serif.
|
|
import QtQuick
|
|
import QtQuick.Controls
|
|
|
|
import ".."
|
|
|
|
Item {
|
|
id: dial
|
|
property real progress: 0 // 0..1 elapsed
|
|
property string clockText: "00:00"
|
|
property string label: ""
|
|
property string sublabel: ""
|
|
property color accent: Theme.work
|
|
property bool paused: false
|
|
property bool idle: false
|
|
property int minutes: 25
|
|
|
|
implicitWidth: 280
|
|
implicitHeight: 280
|
|
|
|
onProgressChanged: canvas.requestPaint()
|
|
onAccentChanged: canvas.requestPaint()
|
|
onPausedChanged: canvas.requestPaint()
|
|
onIdleChanged: canvas.requestPaint()
|
|
|
|
Canvas {
|
|
id: canvas
|
|
anchors.fill: parent
|
|
antialiasing: true
|
|
|
|
onPaint: {
|
|
var ctx = getContext("2d")
|
|
ctx.reset()
|
|
var c = Math.min(width, height) / 2
|
|
var radius = c - 26
|
|
|
|
// The ring.
|
|
ctx.lineWidth = 1.5
|
|
ctx.strokeStyle = Theme.rule
|
|
ctx.beginPath()
|
|
ctx.arc(c, c, radius, 0, Math.PI * 2)
|
|
ctx.stroke()
|
|
|
|
// Minute ticks, inside the ring.
|
|
var count = dial.minutes <= 60 ? Math.max(1, dial.minutes) : Math.round(dial.minutes / 5)
|
|
var spent = Math.floor(count * Math.max(0, Math.min(1, dial.progress)))
|
|
for (var i = 0; i < count; i++) {
|
|
var angle = (i / count) * Math.PI * 2 - Math.PI / 2
|
|
var major = i % 5 === 0
|
|
var outer = radius - 7
|
|
var inner = outer - (major ? 9 : 5)
|
|
ctx.beginPath()
|
|
ctx.lineWidth = major ? 1.4 : 1
|
|
ctx.strokeStyle = (i < spent && !dial.idle) ? dial.accent : Theme.rule
|
|
ctx.moveTo(c + inner * Math.cos(angle), c + inner * Math.sin(angle))
|
|
ctx.lineTo(c + outer * Math.cos(angle), c + outer * Math.sin(angle))
|
|
ctx.stroke()
|
|
}
|
|
|
|
// The remaining arc.
|
|
if (!dial.idle) {
|
|
var remaining = 1 - Math.max(0, Math.min(1, dial.progress))
|
|
if (remaining > 0) {
|
|
ctx.beginPath()
|
|
ctx.lineWidth = 2.5
|
|
ctx.lineCap = "round"
|
|
ctx.globalAlpha = dial.paused ? 0.4 : 1
|
|
ctx.strokeStyle = dial.accent
|
|
ctx.arc(c, c, radius, -Math.PI / 2, -Math.PI / 2 + remaining * Math.PI * 2)
|
|
ctx.stroke()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
anchors.centerIn: parent
|
|
spacing: 6
|
|
width: parent.width * 0.7
|
|
|
|
Label {
|
|
width: parent.width
|
|
text: dial.label.toUpperCase()
|
|
color: dial.idle ? Theme.muted : dial.accent
|
|
font.pixelSize: 10
|
|
font.letterSpacing: 1.4
|
|
font.weight: Font.DemiBold
|
|
font.family: Theme.sansFamily
|
|
horizontalAlignment: Text.AlignHCenter
|
|
}
|
|
Label {
|
|
width: parent.width
|
|
text: dial.clockText
|
|
color: Theme.ink
|
|
opacity: dial.paused ? 0.55 : 1
|
|
font.pixelSize: Math.round(dial.width * 0.21)
|
|
font.family: Theme.displayFamily
|
|
font.letterSpacing: -1.5
|
|
horizontalAlignment: Text.AlignHCenter
|
|
Behavior on opacity { NumberAnimation { duration: 180 } }
|
|
}
|
|
Label {
|
|
width: parent.width
|
|
visible: dial.sublabel !== ""
|
|
text: dial.sublabel
|
|
color: Theme.muted
|
|
font.pixelSize: 12
|
|
font.family: Theme.sansFamily
|
|
horizontalAlignment: Text.AlignHCenter
|
|
elide: Text.ElideRight
|
|
maximumLineCount: 2
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
}
|
|
}
|