diff --git a/.config/quickshell/ii/modules/common/Config.qml b/.config/quickshell/ii/modules/common/Config.qml index 3ee804fda..032fd441f 100644 --- a/.config/quickshell/ii/modules/common/Config.qml +++ b/.config/quickshell/ii/modules/common/Config.qml @@ -254,10 +254,11 @@ Singleton { property string format: "hh:mm" property string dateFormat: "ddd, dd/MM" property JsonObject pomodoro: JsonObject { - property int breaktime: 300 + property int breakTime: 300 property int cycle: 4 property int focus: 1500 - property int longbreak: 1200 + property int longBreak: 1200 + property bool running: false } } diff --git a/.config/quickshell/ii/modules/sidebarRight/pomodoro/PomodoroWidget.qml b/.config/quickshell/ii/modules/sidebarRight/pomodoro/PomodoroWidget.qml index ff39f1b6b..b3c360665 100644 --- a/.config/quickshell/ii/modules/sidebarRight/pomodoro/PomodoroWidget.qml +++ b/.config/quickshell/ii/modules/sidebarRight/pomodoro/PomodoroWidget.qml @@ -2,9 +2,13 @@ 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 @@ -13,10 +17,8 @@ Item { {"name": Translation.tr("Pomodoro"), "icon": "timer_play"}, {"name": Translation.tr("Stopwatch"), "icon": "timer"} ] - property bool showDialog: false - property int dialogMargins: 20 - property int fabSize: 48 - property int fabMargins: 14 + property int lapsListItemPadding: 8 + property int lapsListItemSpacing: 5 // These are keybinds, make sure to change them. @@ -29,7 +31,7 @@ Item { } event.accepted = true } else if (event.key === Qt.Key_Space && !showDialog) { - // Toggle start/pause with Space key + // Toggle start/stop with Space key if (currentTab === 0) { Pomodoro.togglePomodoro() } else { @@ -52,20 +54,18 @@ Item { Timer { id: pomodoroTimer - interval: 1000 - running: Pomodoro.isPomodoroRunning + interval: 200 + running: Config.options.time.pomodoro.running repeat: true onTriggered: Pomodoro.tickSecond() } Timer { id: stopwatchTimer - interval: 1000 + interval: 10 running: Pomodoro.isStopwatchRunning repeat: true - onTriggered: { - Pomodoro.stopwatchTime += 1 - } + onTriggered: Pomodoro.tick10ms() } @@ -174,17 +174,19 @@ Item { CircularProgress { Layout.alignment: Qt.AlignHCenter lineWidth: 7 + gapAngle: Math.PI / 14 value: { let pomodoroTotalTime = Pomodoro.isPomodoroBreak ? Pomodoro.pomodoroBreakTime : Pomodoro.pomodoroFocusTime return Pomodoro.getPomodoroSecondsLeft / pomodoroTotalTime } size: 125 - secondaryColor: Appearance.colors.colSecondaryContainer primaryColor: Appearance.m3colors.m3onSecondaryContainer + secondaryColor: Appearance.colors.colSecondaryContainer enableAnimation: true ColumnLayout { anchors.centerIn: parent + spacing: 0 StyledText { Layout.alignment: Qt.AlignHCenter @@ -208,20 +210,30 @@ Item { // The Start/Stop and Reset buttons ColumnLayout { Layout.alignment: Qt.AlignHCenter - spacing: 20 + spacing: 10 RippleButton { - buttonText: Pomodoro.isPomodoroRunning ? Translation.tr("Pause") : Translation.tr("Start") + contentItem: StyledText { + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + text: Pomodoro.isPomodoroRunning ? Translation.tr("Stop") : Translation.tr("Start") + color: Appearance.colors.colSecondary + } Layout.preferredHeight: 35 Layout.preferredWidth: 90 font.pixelSize: Appearance.font.pixelSize.larger onClicked: Pomodoro.togglePomodoro() - colBackground: Appearance.m3colors.m3onSecondary - colBackgroundHover: Appearance.m3colors.m3onSecondary + colBackground: Appearance.colors.colSecondaryContainer + colBackgroundHover: Appearance.colors.colSecondaryContainer } RippleButton { - buttonText: Translation.tr("Reset") + contentItem: StyledText { + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + text: Translation.tr("Reset") + color: Appearance.colors.colSecondary + } Layout.preferredHeight: 35 Layout.preferredWidth: 90 font.pixelSize: Appearance.font.pixelSize.larger @@ -232,36 +244,89 @@ Item { } } - // The sliders for adjusting duration + // The SpinBoxes for adjusting duration ColumnLayout { - Layout.alignment: Qt.AlignHCenter - spacing: 10 + RowLayout { + Layout.fillWidth: true + spacing: 20 - ConfigSpinBox { - text: Translation.tr("Focus Duration: ") - value: Pomodoro.pomodoroFocusTime / 60 - onValueChanged: { - Pomodoro.pomodoroFocusTime = value * 60 - Config.options.time.pomodoro.focus = value * 60 + StyledText { + id: focusTextBox + Layout.leftMargin: focusSpinBox.implicitWidth / 2 - 7 + text: Translation.tr("Focus") } - Layout.alignment: Qt.AlignCenter - } - - ConfigSpinBox { - text: Translation.tr("Break Duration:") - value: Pomodoro.pomodoroBreakTime / 60 - onValueChanged: { - Config.options.time.pomodoro.breaktime = value * 60 - Pomodoro.pomodoroBreakTime = value * 60 + StyledText { + Layout.leftMargin: breakSpinBox.implicitWidth / 2 + 10 + text: Translation.tr("Break") } } - ConfigSpinBox { - text: Translation.tr("Long Break Duration:") - value: Pomodoro.pomodoroLongBreakTime / 60 - onValueChanged:{ - Pomodoro.pomodoroLongBreakTime = value * 60 - Config.options.time.pomodoro.longbreak = value * 60 + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 0 + + ConfigSpinBox { + id: focusSpinBox + spacing: 0 + Layout.leftMargin: 0 + Layout.rightMargin: 0 + value: Config.options.time.pomodoro.focus / 60 + onValueChanged: { + Config.options.time.pomodoro.focus = value * 60 + } + } + + ConfigSpinBox { + id: breakSpinBox + spacing: 0 + Layout.leftMargin: 0 + Layout.rightMargin: 0 + value: Config.options.time.pomodoro.breakTime / 60 + onValueChanged: { + Config.options.time.pomodoro.breakTime = value * 60 + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 20 + + StyledText { + Layout.leftMargin: focusSpinBox.implicitWidth / 2 - 6 + text: Translation.tr("Cycle") + } + StyledText { + Layout.leftMargin: breakSpinBox.implicitWidth / 2 + text: Translation.tr("Long break") + } + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 0 + + ConfigSpinBox { + id: cycleSpinBox + spacing: 0 + from: 1 + Layout.leftMargin: 0 + Layout.rightMargin: 0 + value: Config.options.time.pomodoro.cycle + onValueChanged: { + Config.options.time.pomodoro.cycle = value + } + } + + ConfigSpinBox { + id: longBreakSpinBox + spacing: 0 + Layout.leftMargin: 0 + Layout.rightMargin: 0 + value: Config.options.time.pomodoro.longBreak / 60 + onValueChanged: { + Config.options.time.pomodoro.longBreak = value * 60 + } } } } @@ -270,59 +335,155 @@ Item { // Stopwatch Tab Item { + Layout.fillWidth: true + ColumnLayout { anchors.horizontalCenter: parent.horizontalCenter - spacing: 18 - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: { - let totalSeconds = Math.floor(Pomodoro.stopwatchTime) - let hours = Math.floor(totalSeconds / 3600).toString().padStart(2, '0') - let minutes = Math.floor((totalSeconds % 3600) / 60).toString().padStart(2, '0') - let seconds = Math.floor(totalSeconds % 60).toString().padStart(2, '0') - return `${hours}:${minutes}:${seconds}` - } - font.pixelSize: 50 - color: Appearance.m3colors.m3onSurface - } + spacing: 20 + Layout.fillWidth: true RowLayout { - Layout.alignment: Qt.AlignHCenter - spacing: 20 + spacing: 40 + // The Stopwatch circle + CircularProgress { + Layout.alignment: Qt.AlignHCenter + lineWidth: 7 + gapAngle: Math.PI / 18 + value: { + return Pomodoro.stopwatchTime % 6000 / 6000 // The seconds in percent + } + size: 125 + primaryColor: Math.floor(Pomodoro.stopwatchTime / 6000) % 2 ? Appearance.colors.colSecondaryContainer : Appearance.m3colors.m3onSecondaryContainer + secondaryColor: Math.floor(Pomodoro.stopwatchTime / 6000) % 2 ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colSecondaryContainer + enableAnimation: false // The animation seems weird after each cycle - DialogButton { - buttonText: Pomodoro.isStopwatchRunning ? Translation.tr("Pause") : Translation.tr("Start") - Layout.preferredWidth: 90 - Layout.preferredHeight: 35 - font.pixelSize: Appearance.font.pixelSize.larger - onClicked: Pomodoro.toggleStopwatch() - background: Rectangle { - color: Appearance.m3colors.m3onSecondary - radius: Appearance.rounding.normal - border.color: Appearance.m3colors.m3outline - border.width: 1 + ColumnLayout { + anchors.centerIn: parent + spacing: 0 + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: { + let totalSeconds = Math.floor(Pomodoro.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}` + } + font.pixelSize: Appearance.font.pixelSize.hugeass + 4 + color: Appearance.m3colors.m3onSurface + } + StyledText { + Layout.alignment: Qt.AlignHCenter + text: { + return (Math.floor(Pomodoro.stopwatchTime) % 100).toString().padStart(2, '0') + } + font.pixelSize: Appearance.font.pixelSize.normal + color: Appearance.m3colors.m3onSurface + } } } - StyledText { + // The Start/Stop and Reset buttons + ColumnLayout { Layout.alignment: Qt.AlignHCenter - text: Translation.tr("Stopwatch") - font.pixelSize: Appearance.font.pixelSize.large - color: Appearance.m3colors.m3onSurface - } + spacing: 10 - DialogButton { - buttonText: Translation.tr("Reset") - Layout.preferredWidth: 90 - Layout.preferredHeight: 35 - font.pixelSize: Appearance.font.pixelSize.larger - onClicked: Pomodoro.stopwatchReset() - background: Rectangle { - color: Appearance.m3colors.m3onError - radius: Appearance.rounding.normal - border.color: Appearance.m3colors.m3outline - border.width: 1 + RippleButton { + contentItem: StyledText { + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + text: Pomodoro.isStopwatchRunning ? Translation.tr("Stop") : Translation.tr("Start") + color: Appearance.colors.colSecondary + } + Layout.preferredHeight: 35 + Layout.preferredWidth: 90 + font.pixelSize: Appearance.font.pixelSize.larger + onClicked: Pomodoro.toggleStopwatch() + colBackground: Appearance.colors.colSecondaryContainer + colBackgroundHover: Appearance.colors.colSecondaryContainer + } + + RippleButton { + contentItem: StyledText { + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + text: Pomodoro.isStopwatchRunning ? Translation.tr("Lap") : Translation.tr("Reset") + color: Appearance.colors.colSecondary + } + Layout.preferredHeight: 35 + Layout.preferredWidth: 90 + font.pixelSize: Appearance.font.pixelSize.larger + onClicked: Pomodoro.stopwatchReset() + colBackground: Appearance.m3colors.m3onError + colBackgroundHover: Appearance.m3colors.m3onError + } + } + } + + StyledListView { + id: lapsList + Layout.fillWidth: true + Layout.preferredHeight: contentHeight + spacing: lapsListItemSpacing + clip: true + model: Pomodoro.stopwatchLaps + + delegate: Rectangle { + width: lapsList.width + implicitHeight: lapsContentText.implicitHeight + lapsListItemPadding + color: Appearance.colors.colLayer2 + radius: Appearance.rounding.small + + StyledText { + id: lapsContentText + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + leftPadding: lapsListItemPadding + rightPadding: lapsListItemPadding + topPadding: lapsListItemPadding / 2 + bottomPadding: lapsListItemPadding / 2 + font.pixelSize: Appearance.font.pixelSize.normal + + text: { + let lapIndex = index + 1 + let lapTime = modelData + // if (index > 0) { + // lapTime = modelData - Pomodoro.stopwatchLaps[index - 1] + // } + let _10ms = (Math.floor(lapTime) % 100).toString().padStart(2, '0') + let totalSeconds = Math.floor(lapTime) / 100 + let minutes = Math.floor(totalSeconds / 60).toString().padStart(2, '0') + let seconds = Math.floor(totalSeconds % 60).toString().padStart(2, '0') + return `${minutes}:${seconds}.${_10ms}` + } + } + + StyledText { + id: lapsDiffText + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + leftPadding: lapsListItemPadding + rightPadding: lapsListItemPadding * 2 + topPadding: lapsListItemPadding / 2 + bottomPadding: lapsListItemPadding / 2 + font.pixelSize: Appearance.font.pixelSize.normal + color: Appearance.colors.colPrimary + + text: { + let lapTime = modelData + if (index != Pomodoro.stopwatchLaps.length - 1) { // except first lap + lapTime = modelData - Pomodoro.stopwatchLaps[index + 1] + let _10ms = (Math.floor(lapTime) % 100).toString().padStart(2, '0') + let totalSeconds = Math.floor(lapTime) / 100 + let minutes = Math.floor(totalSeconds / 60).toString().padStart(2, '0') + let seconds = Math.floor(totalSeconds % 60).toString().padStart(2, '0') + return `+${minutes}:${seconds}.${_10ms}` + } else { + return `` // Nothing for first lap + } + } } } } diff --git a/.config/quickshell/ii/services/Pomodoro.qml b/.config/quickshell/ii/services/Pomodoro.qml index 5244e5382..3387081d5 100644 --- a/.config/quickshell/ii/services/Pomodoro.qml +++ b/.config/quickshell/ii/services/Pomodoro.qml @@ -4,9 +4,9 @@ pragma ComponentBehavior: Bound import qs import qs.modules.common -import Quickshell; -import Quickshell.Io; -import QtQuick; +import Quickshell +import Quickshell.Io +import QtQuick /** * Simple Pomodoro time manager. @@ -14,29 +14,30 @@ import QtQuick; Singleton { id: root - // TODO: read these values from a config file. property int pomodoroFocusTime: Config.options.time.pomodoro.focus - property int pomodoroBreakTime: Config.options.time.pomodoro.breaktime - property int pomodoroLongBreakTime: Config.options.time.pomodoro.longbreak + property int pomodoroBreakTime: Config.options.time.pomodoro.breakTime + property int pomodoroLongBreakTime: Config.options.time.pomodoro.longBreak property int pomodoroLongBreakCycle: Config.options.time.pomodoro.cycle + property bool isPomodoroRunning: Config.options.time.pomodoro.running property int pomodoroTimeLeft: pomodoroFocusTime property int getPomodoroSecondsLeft: pomodoroFocusTime - property int pomodoroTimeStarted: getCurrentTime() // The time pomodoro was last Resumed + property int pomodoroTimeStarted: getCurrentTimeInSeconds() // The time pomodoro was last Resumed property bool isPomodoroBreak: false - property bool isPomodoroRunning: false property int pomodoroCycle: 1 property int stopwatchTime: 0 property bool isStopwatchRunning: false + property int stopwatchStartTime: 0 + property var stopwatchLaps: [] - // Pause and Resume button + // Start and Stop button function togglePomodoro() { - isPomodoroRunning = !isPomodoroRunning + Config.options.time.pomodoro.running = !isPomodoroRunning if (isPomodoroRunning) { // Pressed Start button - pomodoroTimeStarted = getCurrentTime() - } else { // Pressed Pause button - pomodoroTimeLeft -= (getCurrentTime() - pomodoroTimeStarted) + pomodoroTimeStarted = getCurrentTimeInSeconds() + } else { // Pressed Stop button + pomodoroTimeLeft -= (getCurrentTimeInSeconds() - pomodoroTimeStarted) } } @@ -45,51 +46,65 @@ Singleton { pomodoroTimeLeft = pomodoroFocusTime getPomodoroSecondsLeft = pomodoroFocusTime isPomodoroBreak = false - isPomodoroRunning = false + Config.options.time.pomodoro.running = false + pomodoroCycle = 1 } function tickSecond() { - if (getCurrentTime() >= pomodoroTimeStarted + pomodoroTimeLeft) { + if (getCurrentTimeInSeconds() >= pomodoroTimeStarted + pomodoroTimeLeft) { isPomodoroBreak = !isPomodoroBreak pomodoroTimeStarted += pomodoroTimeLeft - pomodoroTimeLeft = isPomodoroBreak ? pomodoroBreakTime : pomodoroFocusTime + pomodoroTimeLeft = isPomodoroBreak ? pomodoroBreakTime : pomodoroFocusTime - if (isPomodoroBreak && pomodoroCycle % pomodoroLongBreakCycle == 0) { // isPomodoroLongBreak - Quickshell.execDetached([ - "notify-send", - Translation.tr("🌿 Long Break!"), - Translation.tr(`Relax for %1 minutes.`).arg(Math.floor(pomodoroLongBreakTime / 60)) - ]) - } else if(isPomodoroBreak){ - Quickshell.execDetached([ - "notify-send", - Translation.tr("☕ Short Break!"), - Translation.tr(`Relax for %1 minutes.`).arg(Math.floor(pomodoroBreakTime / 60)) - ]) + let notificationTitle, notificationMessage + + if (isPomodoroBreak && pomodoroCycle % pomodoroLongBreakCycle === 0) { // isPomodoroLongBreak + notificationMessage = Translation.tr(`Relax for %1 minutes`).arg(Math.floor(pomodoroLongBreakTime / 60)) + } else if (isPomodoroBreak) { + notificationMessage = Translation.tr(`Relax for %1 minutes`).arg(Math.floor(pomodoroBreakTime / 60)) } else { - Quickshell.execDetached([ - "notify-send", - Translation.tr("🔴 Pomodoro started!"), - Translation.tr(`Focus for %1 minutes.`).arg(Math.floor(pomodoroFocusTime / 60)) - ]) + notificationMessage = Translation.tr(`Focus for %1 minutes`).arg(Math.floor(pomodoroFocusTime / 60)) pomodoroCycle += 1 } + + Quickshell.execDetached(["notify-send", "Pomodoro", notificationMessage, "-a", "Shell"]) } - getPomodoroSecondsLeft = (pomodoroTimeStarted + pomodoroTimeLeft) - getCurrentTime() + // A nice abstraction for resume logic by updating the TimeStarted + getPomodoroSecondsLeft = (pomodoroTimeStarted + pomodoroTimeLeft) - getCurrentTimeInSeconds() } - function getCurrentTime() { + function getCurrentTimeInSeconds() { // Pomodoro uses Seconds return Math.floor(Date.now() / 1000) } + function getCurrentTimeIn10ms() { // Stopwatch uses 10ms + return Math.floor(Date.now() / 10) + } + + function tick10ms() { // stopwatch stores time in 10ms + stopwatchTime = getCurrentTimeIn10ms() - stopwatchStartTime + } + // Stopwatch functions function toggleStopwatch() { isStopwatchRunning = !isStopwatchRunning + if (isStopwatchRunning) { + // Resume from paused time by adjusting start time + stopwatchStartTime = getCurrentTimeIn10ms() - stopwatchTime + } } function stopwatchReset() { - stopwatchTime = 0 - isStopwatchRunning = false + if (isStopwatchRunning) { // Clicked on Lap + stopwatchLaps.unshift(stopwatchTime) // Last lap goes first on list + // Reassign to trigger onListChanged, idk copied from Todo.qml + root.stopwatchLaps = stopwatchLaps.slice(0) + } else { // Clicked on Reset + isStopwatchRunning = false + stopwatchTime = 0 + stopwatchStartTime = 0 + stopwatchLaps = [] + } } }