feat: timer widget with countdown, stopwatch and alarm
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: widget
|
||||
|
||||
property var service: null
|
||||
|
||||
property int tabIndex: 0
|
||||
|
||||
readonly property bool onTimerTab: tabIndex === 0
|
||||
readonly property var presets: [1, 3, 5, 10, 15, 25, 45, 60]
|
||||
|
||||
component ActionButton: Rectangle {
|
||||
id: button
|
||||
|
||||
property string icon: ""
|
||||
property string label: ""
|
||||
property bool accent: false
|
||||
property bool enabled: true
|
||||
|
||||
signal triggered
|
||||
|
||||
width: buttonRow.width + Theme.spacingM * 2
|
||||
height: 34
|
||||
radius: Theme.cornerRadius
|
||||
color: accent ? Theme.primary : (buttonArea.containsMouse && enabled ? Theme.surfaceHover : Theme.withAlpha(Theme.surfaceLight, Theme.popupTransparency))
|
||||
border.color: accent ? Theme.primary : Theme.outlineLight
|
||||
border.width: Theme.layerOutlineWidth
|
||||
opacity: enabled ? 1 : 0.45
|
||||
|
||||
Row {
|
||||
id: buttonRow
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: button.icon
|
||||
size: Theme.iconSize - 8
|
||||
color: button.accent ? Theme.onPrimary : Theme.surfaceText
|
||||
visible: button.icon !== ""
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: button.label
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: button.accent ? Font.Medium : Font.Normal
|
||||
color: button.accent ? Theme.onPrimary : Theme.surfaceText
|
||||
visible: button.label !== ""
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: buttonArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: button.enabled
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: button.triggered()
|
||||
}
|
||||
}
|
||||
|
||||
DankTabBar {
|
||||
id: tabs
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
tabHeight: 34
|
||||
showIcons: false
|
||||
currentIndex: widget.tabIndex
|
||||
model: [
|
||||
{
|
||||
"text": "Timer"
|
||||
},
|
||||
{
|
||||
"text": "Stopwatch"
|
||||
}
|
||||
]
|
||||
onTabClicked: index => widget.tabIndex = index
|
||||
}
|
||||
|
||||
// ── Timer ────────────────────────────────────────────────────────────────
|
||||
Item {
|
||||
anchors.top: tabs.bottom
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
visible: widget.onTimerTab
|
||||
|
||||
StyledText {
|
||||
id: remaining
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: widget.service ? widget.service.formatDuration(widget.service.remainingMs) : "00:00"
|
||||
font.pixelSize: 40
|
||||
font.weight: Font.Medium
|
||||
color: (widget.service && widget.service.timerFinished) ? Theme.error : Theme.surfaceText
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
running: widget.service !== null && widget.service.timerFinished
|
||||
loops: Animation.Infinite
|
||||
|
||||
NumberAnimation {
|
||||
to: 0.35
|
||||
duration: 500
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
to: 1
|
||||
duration: 500
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: progress
|
||||
|
||||
anchors.top: remaining.bottom
|
||||
anchors.topMargin: Theme.spacingXS
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 4
|
||||
radius: 2
|
||||
color: Theme.withAlpha(Theme.outline, 0.4)
|
||||
|
||||
Rectangle {
|
||||
width: parent.width * (widget.service ? widget.service.timerProgress : 0)
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: (widget.service && widget.service.timerFinished) ? Theme.error : Theme.primary
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: 180
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: labelField
|
||||
|
||||
anchors.top: progress.bottom
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 34
|
||||
placeholderText: "What is it for? (shown when it ends)"
|
||||
text: widget.service ? widget.service.timerLabel : ""
|
||||
onTextChanged: {
|
||||
if (widget.service)
|
||||
widget.service.timerLabel = text;
|
||||
}
|
||||
}
|
||||
|
||||
Flow {
|
||||
id: presetFlow
|
||||
|
||||
anchors.top: labelField.bottom
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: widget.presets
|
||||
|
||||
delegate: ActionButton {
|
||||
required property var modelData
|
||||
|
||||
label: modelData + "m"
|
||||
onTriggered: {
|
||||
if (!widget.service)
|
||||
return;
|
||||
|
||||
widget.service.resetTimer();
|
||||
widget.service.startTimer(modelData * 60 * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
ActionButton {
|
||||
icon: "remove"
|
||||
label: "1m"
|
||||
enabled: widget.service !== null
|
||||
onTriggered: widget.service.addTime(-60 * 1000)
|
||||
}
|
||||
|
||||
ActionButton {
|
||||
icon: (widget.service && widget.service.timerRunning) ? "pause" : "play_arrow"
|
||||
label: (widget.service && widget.service.timerRunning) ? "Pause" : "Start"
|
||||
accent: true
|
||||
enabled: widget.service !== null
|
||||
onTriggered: widget.service.toggleTimer()
|
||||
}
|
||||
|
||||
ActionButton {
|
||||
icon: "add"
|
||||
label: "1m"
|
||||
enabled: widget.service !== null
|
||||
onTriggered: widget.service.addTime(60 * 1000)
|
||||
}
|
||||
|
||||
ActionButton {
|
||||
icon: "restart_alt"
|
||||
label: (widget.service && widget.service.timerFinished) ? "Dismiss" : "Reset"
|
||||
enabled: widget.service !== null
|
||||
onTriggered: {
|
||||
if (widget.service.timerFinished)
|
||||
widget.service.dismissFinished();
|
||||
else
|
||||
widget.service.resetTimer();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Stopwatch ────────────────────────────────────────────────────────────
|
||||
Item {
|
||||
anchors.top: tabs.bottom
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
visible: !widget.onTimerTab
|
||||
|
||||
StyledText {
|
||||
id: elapsed
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: widget.service ? widget.service.formatStopwatch(widget.service.stopwatchElapsedMs) : "00:00.00"
|
||||
font.pixelSize: 40
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
id: lapList
|
||||
|
||||
anchors.top: elapsed.bottom
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: stopwatchControls.top
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
clip: true
|
||||
contentHeight: lapColumn.height
|
||||
contentWidth: width
|
||||
|
||||
Column {
|
||||
id: lapColumn
|
||||
|
||||
width: lapList.width
|
||||
spacing: 2
|
||||
|
||||
Repeater {
|
||||
model: widget.service ? widget.service.laps : []
|
||||
|
||||
delegate: Item {
|
||||
required property var modelData
|
||||
|
||||
width: lapColumn.width
|
||||
height: 22
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Lap " + modelData.index
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.right: totalText.left
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "+" + widget.service.formatStopwatch(modelData.split)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: totalText
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: widget.service.formatStopwatch(modelData.total)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: lapList
|
||||
text: "No laps yet"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
visible: !widget.service || widget.service.laps.length === 0
|
||||
}
|
||||
|
||||
Row {
|
||||
id: stopwatchControls
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
ActionButton {
|
||||
icon: (widget.service && widget.service.stopwatchRunning) ? "pause" : "play_arrow"
|
||||
label: (widget.service && widget.service.stopwatchRunning) ? "Pause" : "Start"
|
||||
accent: true
|
||||
enabled: widget.service !== null
|
||||
onTriggered: widget.service.toggleStopwatch()
|
||||
}
|
||||
|
||||
ActionButton {
|
||||
icon: "flag"
|
||||
label: "Lap"
|
||||
enabled: widget.service !== null && widget.service.stopwatchElapsedMs > 0
|
||||
onTriggered: widget.service.lap()
|
||||
}
|
||||
|
||||
ActionButton {
|
||||
icon: "restart_alt"
|
||||
label: "Reset"
|
||||
enabled: widget.service !== null && widget.service.stopwatchElapsedMs > 0
|
||||
onTriggered: widget.service.resetStopwatch()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user