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() } } } }