diff --git a/.config/quickshell/ii/modules/common/Appearance.qml b/.config/quickshell/ii/modules/common/Appearance.qml index cfc9e1cae..2c00e48e5 100644 --- a/.config/quickshell/ii/modules/common/Appearance.qml +++ b/.config/quickshell/ii/modules/common/Appearance.qml @@ -146,6 +146,14 @@ Singleton { property color colScrim: ColorUtils.transparentize(m3colors.m3scrim, 0.5) property color colShadow: ColorUtils.transparentize(m3colors.m3shadow, 0.7) property color colOutlineVariant: m3colors.m3outlineVariant + property color colError: m3colors.m3error + property color colErrorHover: ColorUtils.mix(m3colors.m3error, colLayer1Hover, 0.85) + property color colErrorActive: ColorUtils.mix(m3colors.m3error, colLayer1Active, 0.7) + property color colOnError: m3colors.m3onError + property color colErrorContainer: m3colors.m3errorContainer + property color colErrorContainerHover: ColorUtils.mix(m3colors.m3errorContainer, m3colors.m3onErrorContainer, 0.90) + property color colErrorContainerActive: ColorUtils.mix(m3colors.m3errorContainer, m3colors.m3onErrorContainer, 0.70) + property color colOnErrorContainer: m3colors.m3onErrorContainer } rounding: QtObject { diff --git a/.config/quickshell/ii/modules/common/Config.qml b/.config/quickshell/ii/modules/common/Config.qml index d3a9d2f0f..8337be313 100644 --- a/.config/quickshell/ii/modules/common/Config.qml +++ b/.config/quickshell/ii/modules/common/Config.qml @@ -275,6 +275,13 @@ Singleton { // https://doc.qt.io/qt-6/qtime.html#toString property string format: "hh:mm" property string dateFormat: "ddd, dd/MM" + property JsonObject pomodoro: JsonObject { + property string alertSound: "" + property int breakTime: 300 + property int cyclesBeforeLongBreak: 4 + property int focus: 1500 + property int longBreak: 900 + } } property JsonObject windows: JsonObject { diff --git a/.config/quickshell/ii/modules/common/Persistent.qml b/.config/quickshell/ii/modules/common/Persistent.qml index abd062d73..29d41e18d 100644 --- a/.config/quickshell/ii/modules/common/Persistent.qml +++ b/.config/quickshell/ii/modules/common/Persistent.qml @@ -11,18 +11,35 @@ Singleton { property string fileName: "states.json" property string filePath: `${root.fileDir}/${root.fileName}` + Timer { + id: fileReloadTimer + interval: 100 + repeat: false + onTriggered: { + persistentStatesFileView.reload() + } + } + + Timer { + id: fileWriteTimer + interval: 100 + repeat: false + onTriggered: { + persistentStatesFileView.writeAdapter() + } + } + FileView { + id: persistentStatesFileView path: root.filePath watchChanges: true - onFileChanged: reload() - onAdapterUpdated: { - writeAdapter() - } + onFileChanged: fileReloadTimer.restart() + onAdapterUpdated: fileWriteTimer.restart() onLoadFailed: error => { console.log("Failed to load persistent states file:", error); if (error == FileViewError.FileNotFound) { - writeAdapter(); + fileWriteTimer.restart(); } } @@ -44,6 +61,20 @@ Singleton { property bool allowNsfw: false property string provider: "yandere" } + + property JsonObject timer: JsonObject { + property JsonObject pomodoro: JsonObject { + property bool running: false + property int start: 0 + property bool isBreak: false + property int cycle: 0 + } + property JsonObject stopwatch: JsonObject { + property bool running: false + property int start: 0 + property list laps: [] + } + } } } } diff --git a/.config/quickshell/ii/modules/common/widgets/RippleButton.qml b/.config/quickshell/ii/modules/common/widgets/RippleButton.qml index 955d0bb69..759bc043c 100644 --- a/.config/quickshell/ii/modules/common/widgets/RippleButton.qml +++ b/.config/quickshell/ii/modules/common/widgets/RippleButton.qml @@ -29,6 +29,7 @@ Button { property color colRipple: Appearance?.colors.colLayer1Active ?? "#D6CEE2" property color colRippleToggled: Appearance?.colors.colPrimaryActive ?? "#D6CEE2" + opacity: root.enabled ? 1 : 0.4 property color buttonColor: root.enabled ? (root.toggled ? (root.hovered ? colBackgroundToggledHover : colBackgroundToggled) : diff --git a/.config/quickshell/ii/modules/sidebarRight/BottomWidgetGroup.qml b/.config/quickshell/ii/modules/sidebarRight/BottomWidgetGroup.qml index 384011634..cd5385c75 100644 --- a/.config/quickshell/ii/modules/sidebarRight/BottomWidgetGroup.qml +++ b/.config/quickshell/ii/modules/sidebarRight/BottomWidgetGroup.qml @@ -4,6 +4,7 @@ import qs import qs.services import "./calendar" import "./todo" +import "./pomodoro" import QtQuick import QtQuick.Layouts @@ -17,7 +18,8 @@ Rectangle { property bool collapsed: Persistent.states.sidebar.bottomGroup.collapsed property var tabs: [ {"type": "calendar", "name": Translation.tr("Calendar"), "icon": "calendar_month", "widget": calendarWidget}, - {"type": "todo", "name": Translation.tr("To Do"), "icon": "done_outline", "widget": todoWidget} + {"type": "todo", "name": Translation.tr("To Do"), "icon": "done_outline", "widget": todoWidget}, + {"type": "timer", "name": Translation.tr("Timer"), "icon": "schedule", "widget": pomodoroWidget}, ] Behavior on implicitHeight { @@ -238,4 +240,13 @@ Rectangle { anchors.margins: 5 } } + + // Pomodoro component + Component { + id: pomodoroWidget + PomodoroWidget { + anchors.fill: parent + anchors.margins: 5 + } + } } \ No newline at end of file diff --git a/.config/quickshell/ii/modules/sidebarRight/pomodoro/PomodoroTimer.qml b/.config/quickshell/ii/modules/sidebarRight/pomodoro/PomodoroTimer.qml new file mode 100644 index 000000000..439c06ee4 --- /dev/null +++ b/.config/quickshell/ii/modules/sidebarRight/pomodoro/PomodoroTimer.qml @@ -0,0 +1,115 @@ +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import Qt5Compat.GraphicalEffects +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell + +Item { + id: root + + implicitHeight: contentColumn.implicitHeight + implicitWidth: contentColumn.implicitWidth + + ColumnLayout { + id: contentColumn + anchors.fill: parent + spacing: 0 + + // The Pomodoro timer circle + CircularProgress { + Layout.alignment: Qt.AlignHCenter + lineWidth: 8 + value: { + return TimerService.pomodoroSecondsLeft / TimerService.pomodoroLapDuration; + } + size: 200 + enableAnimation: true + + ColumnLayout { + anchors.centerIn: parent + spacing: 0 + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: { + let minutes = Math.floor(TimerService.pomodoroSecondsLeft / 60).toString().padStart(2, '0'); + let seconds = Math.floor(TimerService.pomodoroSecondsLeft % 60).toString().padStart(2, '0'); + return `${minutes}:${seconds}`; + } + font.pixelSize: 40 + color: Appearance.m3colors.m3onSurface + } + StyledText { + Layout.alignment: Qt.AlignHCenter + text: TimerService.pomodoroLongBreak ? Translation.tr("Long break") : TimerService.pomodoroBreak ? Translation.tr("Break") : Translation.tr("Focus") + font.pixelSize: Appearance.font.pixelSize.normal + color: Appearance.colors.colSubtext + } + } + + Rectangle { + radius: Appearance.rounding.full + color: Appearance.colors.colLayer2 + + anchors { + right: parent.right + bottom: parent.bottom + } + implicitWidth: 36 + implicitHeight: implicitWidth + + StyledText { + id: cycleText + anchors.centerIn: parent + color: Appearance.colors.colOnLayer2 + text: TimerService.pomodoroCycle + 1 + } + } + } + + // The Start/Stop and Reset buttons + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 10 + + RippleButton { + contentItem: StyledText { + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + text: TimerService.pomodoroRunning ? Translation.tr("Pause") : (TimerService.pomodoroSecondsLeft === TimerService.focusTime) ? Translation.tr("Start") : Translation.tr("Resume") + color: TimerService.pomodoroRunning ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnPrimary + } + implicitHeight: 35 + implicitWidth: 90 + font.pixelSize: Appearance.font.pixelSize.larger + onClicked: TimerService.togglePomodoro() + colBackground: TimerService.pomodoroRunning ? Appearance.colors.colSecondaryContainer : Appearance.colors.colPrimary + colBackgroundHover: TimerService.pomodoroRunning ? Appearance.colors.colSecondaryContainer : Appearance.colors.colPrimary + } + + RippleButton { + implicitHeight: 35 + implicitWidth: 90 + + onClicked: TimerService.resetPomodoro() + enabled: (TimerService.pomodoroSecondsLeft < TimerService.pomodoroLapDuration) || TimerService.pomodoroCycle > 0 || TimerService.pomodoroBreak + + font.pixelSize: Appearance.font.pixelSize.larger + colBackground: Appearance.colors.colErrorContainer + colBackgroundHover: Appearance.colors.colErrorContainerHover + colRipple: Appearance.colors.colErrorContainerActive + + contentItem: StyledText { + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + text: Translation.tr("Reset") + color: Appearance.colors.colOnErrorContainer + } + } + } + } +} diff --git a/.config/quickshell/ii/modules/sidebarRight/pomodoro/PomodoroWidget.qml b/.config/quickshell/ii/modules/sidebarRight/pomodoro/PomodoroWidget.qml new file mode 100644 index 000000000..729d45809 --- /dev/null +++ b/.config/quickshell/ii/modules/sidebarRight/pomodoro/PomodoroWidget.qml @@ -0,0 +1,144 @@ +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Item { + id: root + property int currentTab: 0 + property var tabButtonList: [ + {"name": Translation.tr("Pomodoro"), "icon": "search_activity"}, + {"name": Translation.tr("Stopwatch"), "icon": "timer"} + ] + + // These are keybinds for stopwatch and pomodoro + Keys.onPressed: (event) => { + if ((event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp) && event.modifiers === Qt.NoModifier) { // Switch tabs + if (event.key === Qt.Key_PageDown) { + currentTab = Math.min(currentTab + 1, root.tabButtonList.length - 1) + } else if (event.key === Qt.Key_PageUp) { + currentTab = Math.max(currentTab - 1, 0) + } + event.accepted = true + } else if (event.key === Qt.Key_Space || event.key === Qt.Key_S) { // Pause/resume with Space or S + if (currentTab === 0) { + TimerService.togglePomodoro() + } else { + TimerService.toggleStopwatch() + } + event.accepted = true + } else if (event.key === Qt.Key_R) { // Reset with R + if (currentTab === 0) { + TimerService.resetPomodoro() + } else { + TimerService.stopwatchReset() + } + event.accepted = true + } else if (event.key === Qt.Key_L) { // Record lap with L + TimerService.stopwatchRecordLap() + event.accepted = true + } + } + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + TabBar { + id: tabBar + Layout.fillWidth: true + currentIndex: currentTab + onCurrentIndexChanged: currentTab = currentIndex + + background: Item { + WheelHandler { + onWheel: (event) => { + if (event.angleDelta.y < 0) + tabBar.currentIndex = Math.min(tabBar.currentIndex + 1, root.tabButtonList.length - 1) + else if (event.angleDelta.y > 0) + tabBar.currentIndex = Math.max(tabBar.currentIndex - 1, 0) + } + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + } + } + + Repeater { + model: root.tabButtonList + delegate: SecondaryTabButton { + selected: (index == currentTab) + buttonText: modelData.name + buttonIcon: modelData.icon + } + } + } + + Item { // Tab indicator + id: tabIndicator + Layout.fillWidth: true + height: 3 + property bool enableIndicatorAnimation: false + Connections { + target: root + function onCurrentTabChanged() { + tabIndicator.enableIndicatorAnimation = true + } + } + + Rectangle { + id: indicator + property int tabCount: root.tabButtonList.length + property real fullTabSize: root.width / tabCount; + property real targetWidth: tabBar.contentItem.children[0].children[tabBar.currentIndex].tabContentWidth + + implicitWidth: targetWidth + anchors { + top: parent.top + bottom: parent.bottom + } + + x: tabBar.currentIndex * fullTabSize + (fullTabSize - targetWidth) / 2 + + color: Appearance.colors.colPrimary + radius: Appearance.rounding.full + + Behavior on x { + enabled: tabIndicator.enableIndicatorAnimation + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + + Behavior on implicitWidth { + enabled: tabIndicator.enableIndicatorAnimation + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + } + } + + Rectangle { // Tabbar bottom border + id: tabBarBottomBorder + Layout.fillWidth: true + height: 1 + color: Appearance.colors.colOutlineVariant + } + + SwipeView { + id: swipeView + Layout.topMargin: 10 + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 10 + clip: true + currentIndex: currentTab + onCurrentIndexChanged: { + tabIndicator.enableIndicatorAnimation = true + currentTab = currentIndex + } + + // Tabs + PomodoroTimer {} + Stopwatch {} + } + } +} diff --git a/.config/quickshell/ii/modules/sidebarRight/pomodoro/Stopwatch.qml b/.config/quickshell/ii/modules/sidebarRight/pomodoro/Stopwatch.qml new file mode 100644 index 000000000..ffc706568 --- /dev/null +++ b/.config/quickshell/ii/modules/sidebarRight/pomodoro/Stopwatch.qml @@ -0,0 +1,208 @@ +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import Qt5Compat.GraphicalEffects +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell + +Item { + id: stopwatchTab + Layout.fillWidth: true + Layout.fillHeight: true + + Item { + anchors { + fill: parent + topMargin: 8 + leftMargin: 16 + rightMargin: 16 + } + + RowLayout { // Elapsed + id: elapsedIndicator + + anchors { + top: undefined + verticalCenter: parent.verticalCenter + left: controlButtons.left + leftMargin: 6 + } + + states: State { + name: "hasLaps" + when: TimerService.stopwatchLaps.length > 0 + AnchorChanges { + target: elapsedIndicator + anchors.top: parent.top + anchors.verticalCenter: undefined + anchors.left: controlButtons.left + } + } + + transitions: Transition { + AnchorAnimation { + duration: Appearance.animation.elementMoveFast.duration + easing.type: Appearance.animation.elementMoveFast.type + easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve + } + } + + spacing: 0 + StyledText { + // Layout.preferredWidth: elapsedIndicator.width * 0.6 // Prevent shakiness + font.pixelSize: 40 + color: Appearance.m3colors.m3onSurface + text: { + let totalSeconds = Math.floor(TimerService.stopwatchTime) / 100 + let minutes = Math.floor(totalSeconds / 60).toString().padStart(2, '0') + let seconds = Math.floor(totalSeconds % 60).toString().padStart(2, '0') + return `${minutes}:${seconds}` + } + } + StyledText { + Layout.fillWidth: true + font.pixelSize: 40 + color: Appearance.colors.colSubtext + text: { + return `:${(Math.floor(TimerService.stopwatchTime) % 100).toString().padStart(2, '0')}` + } + } + } + + // Laps + StyledListView { + id: lapsList + anchors { + top: elapsedIndicator.bottom + bottom: controlButtons.top + left: parent.left + right: parent.right + topMargin: 16 + bottomMargin: 16 + } + spacing: 4 + clip: true + popin: true + + model: ScriptModel { + values: TimerService.stopwatchLaps.map((v, i, arr) => arr[arr.length - 1 - i]) + } + + delegate: Rectangle { + id: lapItem + required property int index + required property var modelData + property var horizontalPadding: 10 + property var verticalPadding: 6 + width: lapsList.width + implicitHeight: lapRow.implicitHeight + verticalPadding * 2 + implicitWidth: lapRow.implicitWidth + horizontalPadding * 2 + color: Appearance.colors.colLayer2 + radius: Appearance.rounding.small + + RowLayout { + id: lapRow + anchors { + fill: parent + leftMargin: lapItem.horizontalPadding + rightMargin: lapItem.horizontalPadding + topMargin: lapItem.verticalPadding + bottomMargin: lapItem.verticalPadding + } + + StyledText { + font.pixelSize: Appearance.font.pixelSize.small + color: Appearance.colors.colSubtext + text: `${TimerService.stopwatchLaps.length - lapItem.index}.` + } + + StyledText { + font.pixelSize: Appearance.font.pixelSize.small + text: { + const lapTime = lapItem.modelData + const _10ms = (Math.floor(lapTime) % 100).toString().padStart(2, '0') + const totalSeconds = Math.floor(lapTime) / 100 + const minutes = Math.floor(totalSeconds / 60).toString().padStart(2, '0') + const seconds = Math.floor(totalSeconds % 60).toString().padStart(2, '0') + return `${minutes}:${seconds}.${_10ms}` + } + } + + Item { Layout.fillWidth: true } + + StyledText { + font.pixelSize: Appearance.font.pixelSize.smaller + color: Appearance.colors.colPrimary + text: { + const originalIndex = TimerService.stopwatchLaps.length - lapItem.index - 1 + const lastTime = originalIndex > 0 ? TimerService.stopwatchLaps[originalIndex - 1] : 0 + const lapTime = lapItem.modelData - lastTime + const _10ms = (Math.floor(lapTime) % 100).toString().padStart(2, '0') + const totalSeconds = Math.floor(lapTime) / 100 + const minutes = Math.floor(totalSeconds / 60).toString().padStart(2, '0') + const seconds = Math.floor(totalSeconds % 60).toString().padStart(2, '0') + return `+${minutes == "00" ? "" : minutes + ":"}${seconds}.${_10ms}` + } + } + } + } + } + + RowLayout { + id: controlButtons + anchors { + horizontalCenter: parent.horizontalCenter + bottom: parent.bottom + bottomMargin: 6 + } + spacing: 4 + + RippleButton { + Layout.preferredHeight: 35 + Layout.preferredWidth: 90 + font.pixelSize: Appearance.font.pixelSize.larger + + onClicked: { + TimerService.toggleStopwatch() + } + + colBackground: TimerService.stopwatchRunning ? Appearance.colors.colSecondaryContainer : Appearance.colors.colPrimary + colBackgroundHover: TimerService.stopwatchRunning ? Appearance.colors.colSecondaryContainerHover : Appearance.colors.colPrimaryHover + colRipple: TimerService.stopwatchRunning ? Appearance.colors.colSecondaryContainerActive : Appearance.colors.colPrimaryActive + + contentItem: StyledText { + horizontalAlignment: Text.AlignHCenter + color: TimerService.stopwatchRunning ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnPrimary + text: TimerService.stopwatchRunning ? Translation.tr("Pause") : TimerService.stopwatchTime === 0 ? Translation.tr("Start") : Translation.tr("Resume") + } + } + + RippleButton { + implicitHeight: 35 + implicitWidth: 90 + font.pixelSize: Appearance.font.pixelSize.larger + + onClicked: { + if (TimerService.stopwatchRunning) + TimerService.stopwatchRecordLap() + else + TimerService.stopwatchReset() + } + enabled: TimerService.stopwatchTime > 0 || Persistent.states.timer.stopwatch.laps.length > 0 + + colBackground: TimerService.stopwatchRunning ? Appearance.colors.colLayer2 : Appearance.colors.colErrorContainer + colBackgroundHover: TimerService.stopwatchRunning ? Appearance.colors.colLayer2Hover : Appearance.colors.colErrorContainerHover + colRipple: TimerService.stopwatchRunning ? Appearance.colors.colLayer2Active : Appearance.colors.colErrorContainerActive + + contentItem: StyledText { + horizontalAlignment: Text.AlignHCenter + text: TimerService.stopwatchRunning ? Translation.tr("Lap") : Translation.tr("Reset") + color: TimerService.stopwatchRunning ? Appearance.colors.colOnLayer2 : Appearance.colors.colOnErrorContainer + } + } + } + } +} \ No newline at end of file diff --git a/.config/quickshell/ii/services/TimerService.qml b/.config/quickshell/ii/services/TimerService.qml new file mode 100644 index 000000000..e6c8906bc --- /dev/null +++ b/.config/quickshell/ii/services/TimerService.qml @@ -0,0 +1,142 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import qs +import qs.modules.common + +import Quickshell +import Quickshell.Io +import QtQuick + +/** + * Simple Pomodoro time manager. + */ +Singleton { + id: root + + property int focusTime: Config.options.time.pomodoro.focus + property int breakTime: Config.options.time.pomodoro.breakTime + property int longBreakTime: Config.options.time.pomodoro.longBreak + property int cyclesBeforeLongBreak: Config.options.time.pomodoro.cyclesBeforeLongBreak + property string alertSound: Config.options.time.pomodoro.alertSound + + property bool pomodoroRunning: Persistent.states.timer.pomodoro.running + property bool pomodoroBreak: Persistent.states.timer.pomodoro.isBreak + property bool pomodoroLongBreak: Persistent.states.timer.pomodoro.isBreak && (pomodoroCycle + 1 == cyclesBeforeLongBreak); + property int pomodoroLapDuration: pomodoroLongBreak ? longBreakTime : pomodoroBreak ? breakTime : focusTime // This is a binding that's to be kept + property int pomodoroSecondsLeft: pomodoroLapDuration // Reasonable init value, to be changed + property int pomodoroCycle: Persistent.states.timer.pomodoro.cycle + + property bool stopwatchRunning: Persistent.states.timer.stopwatch.running + property int stopwatchTime: 0 + property int stopwatchStart: Persistent.states.timer.stopwatch.start + property var stopwatchLaps: Persistent.states.timer.stopwatch.laps + + // General + Component.onCompleted: { + if (!stopwatchRunning) + stopwatchReset(); + } + + function getCurrentTimeInSeconds() { // Pomodoro uses Seconds + return Math.floor(Date.now() / 1000); + } + + function getCurrentTimeIn10ms() { // Stopwatch uses 10ms + return Math.floor(Date.now() / 10); + } + + // Pomodoro + function refreshPomodoro() { + // Work <-> break ? + if (getCurrentTimeInSeconds() >= Persistent.states.timer.pomodoro.start + pomodoroLapDuration) { + // Reset counts + Persistent.states.timer.pomodoro.isBreak = !Persistent.states.timer.pomodoro.isBreak; + Persistent.states.timer.pomodoro.start = getCurrentTimeInSeconds(); + + // Send notification + let notificationMessage; + if (Persistent.states.timer.pomodoro.isBreak && (pomodoroCycle + 1 == cyclesBeforeLongBreak)) { + notificationMessage = Translation.tr(`🌿 Long break: %1 minutes`).arg(Math.floor(longBreakTime / 60)); + } else if (Persistent.states.timer.pomodoro.isBreak) { + notificationMessage = Translation.tr(`☕ Break: %1 minutes`).arg(Math.floor(breakTime / 60)); + } else { + notificationMessage = Translation.tr(`🔴 Focus: %1 minutes`).arg(Math.floor(focusTime / 60)); + } + + Quickshell.execDetached(["notify-send", "Pomodoro", notificationMessage, "-a", "Shell"]); + if (alertSound) + Quickshell.execDetached(["ffplay", "-nodisp", "-autoexit", alertSound]); + + if (!pomodoroBreak) { + Persistent.states.timer.pomodoro.cycle = (Persistent.states.timer.pomodoro.cycle + 1) % root.cyclesBeforeLongBreak; + } + } + + pomodoroSecondsLeft = pomodoroLapDuration - (getCurrentTimeInSeconds() - Persistent.states.timer.pomodoro.start); + } + + Timer { + id: pomodoroTimer + interval: 200 + running: root.pomodoroRunning + repeat: true + onTriggered: refreshPomodoro() + } + + function togglePomodoro() { + Persistent.states.timer.pomodoro.running = !pomodoroRunning; + if (Persistent.states.timer.pomodoro.running) { + // Start/Resume + Persistent.states.timer.pomodoro.start = getCurrentTimeInSeconds() + pomodoroSecondsLeft - pomodoroLapDuration; + } + } + + function resetPomodoro() { + Persistent.states.timer.pomodoro.running = false; + Persistent.states.timer.pomodoro.isBreak = false; + Persistent.states.timer.pomodoro.start = getCurrentTimeInSeconds(); + Persistent.states.timer.pomodoro.cycle = 0; + refreshPomodoro(); + } + + // Stopwatch + function refreshStopwatch() { // Stopwatch stores time in 10ms + stopwatchTime = getCurrentTimeIn10ms() - stopwatchStart; + } + + Timer { + id: stopwatchTimer + interval: 10 + running: root.stopwatchRunning + repeat: true + onTriggered: refreshStopwatch() + } + + function toggleStopwatch() { + if (root.stopwatchRunning) + stopwatchPause(); + else + stopwatchResume(); + } + + function stopwatchPause() { + Persistent.states.timer.stopwatch.running = false; + } + + function stopwatchResume() { + if (stopwatchTime === 0) Persistent.states.timer.stopwatch.laps = []; + Persistent.states.timer.stopwatch.running = true; + Persistent.states.timer.stopwatch.start = getCurrentTimeIn10ms() - stopwatchTime; + } + + function stopwatchReset() { + stopwatchTime = 0; + Persistent.states.timer.stopwatch.laps = []; + Persistent.states.timer.stopwatch.running = false; + } + + function stopwatchRecordLap() { + Persistent.states.timer.stopwatch.laps.push(stopwatchTime); + } +}