Reworked on Pomodoro's UI and added Stopwatch

Signed-off-by: Nyx <189459385+nyx-4@users.noreply.github.com>
This commit is contained in:
Nyx
2025-08-05 16:06:22 +05:00
parent e4b761917a
commit bfb7ccffb5
3 changed files with 300 additions and 123 deletions
@@ -254,10 +254,11 @@ Singleton {
property string format: "hh:mm" property string format: "hh:mm"
property string dateFormat: "ddd, dd/MM" property string dateFormat: "ddd, dd/MM"
property JsonObject pomodoro: JsonObject { property JsonObject pomodoro: JsonObject {
property int breaktime: 300 property int breakTime: 300
property int cycle: 4 property int cycle: 4
property int focus: 1500 property int focus: 1500
property int longbreak: 1200 property int longBreak: 1200
property bool running: false
} }
} }
@@ -2,9 +2,13 @@ import qs
import qs.services import qs.services
import qs.modules.common import qs.modules.common
import qs.modules.common.widgets import qs.modules.common.widgets
import Qt5Compat.GraphicalEffects
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell
Item { Item {
id: root id: root
@@ -13,10 +17,8 @@ Item {
{"name": Translation.tr("Pomodoro"), "icon": "timer_play"}, {"name": Translation.tr("Pomodoro"), "icon": "timer_play"},
{"name": Translation.tr("Stopwatch"), "icon": "timer"} {"name": Translation.tr("Stopwatch"), "icon": "timer"}
] ]
property bool showDialog: false property int lapsListItemPadding: 8
property int dialogMargins: 20 property int lapsListItemSpacing: 5
property int fabSize: 48
property int fabMargins: 14
// These are keybinds, make sure to change them. // These are keybinds, make sure to change them.
@@ -29,7 +31,7 @@ Item {
} }
event.accepted = true event.accepted = true
} else if (event.key === Qt.Key_Space && !showDialog) { } else if (event.key === Qt.Key_Space && !showDialog) {
// Toggle start/pause with Space key // Toggle start/stop with Space key
if (currentTab === 0) { if (currentTab === 0) {
Pomodoro.togglePomodoro() Pomodoro.togglePomodoro()
} else { } else {
@@ -52,20 +54,18 @@ Item {
Timer { Timer {
id: pomodoroTimer id: pomodoroTimer
interval: 1000 interval: 200
running: Pomodoro.isPomodoroRunning running: Config.options.time.pomodoro.running
repeat: true repeat: true
onTriggered: Pomodoro.tickSecond() onTriggered: Pomodoro.tickSecond()
} }
Timer { Timer {
id: stopwatchTimer id: stopwatchTimer
interval: 1000 interval: 10
running: Pomodoro.isStopwatchRunning running: Pomodoro.isStopwatchRunning
repeat: true repeat: true
onTriggered: { onTriggered: Pomodoro.tick10ms()
Pomodoro.stopwatchTime += 1
}
} }
@@ -174,17 +174,19 @@ Item {
CircularProgress { CircularProgress {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
lineWidth: 7 lineWidth: 7
gapAngle: Math.PI / 14
value: { value: {
let pomodoroTotalTime = Pomodoro.isPomodoroBreak ? Pomodoro.pomodoroBreakTime : Pomodoro.pomodoroFocusTime let pomodoroTotalTime = Pomodoro.isPomodoroBreak ? Pomodoro.pomodoroBreakTime : Pomodoro.pomodoroFocusTime
return Pomodoro.getPomodoroSecondsLeft / pomodoroTotalTime return Pomodoro.getPomodoroSecondsLeft / pomodoroTotalTime
} }
size: 125 size: 125
secondaryColor: Appearance.colors.colSecondaryContainer
primaryColor: Appearance.m3colors.m3onSecondaryContainer primaryColor: Appearance.m3colors.m3onSecondaryContainer
secondaryColor: Appearance.colors.colSecondaryContainer
enableAnimation: true enableAnimation: true
ColumnLayout { ColumnLayout {
anchors.centerIn: parent anchors.centerIn: parent
spacing: 0
StyledText { StyledText {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
@@ -208,20 +210,30 @@ Item {
// The Start/Stop and Reset buttons // The Start/Stop and Reset buttons
ColumnLayout { ColumnLayout {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
spacing: 20 spacing: 10
RippleButton { 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.preferredHeight: 35
Layout.preferredWidth: 90 Layout.preferredWidth: 90
font.pixelSize: Appearance.font.pixelSize.larger font.pixelSize: Appearance.font.pixelSize.larger
onClicked: Pomodoro.togglePomodoro() onClicked: Pomodoro.togglePomodoro()
colBackground: Appearance.m3colors.m3onSecondary colBackground: Appearance.colors.colSecondaryContainer
colBackgroundHover: Appearance.m3colors.m3onSecondary colBackgroundHover: Appearance.colors.colSecondaryContainer
} }
RippleButton { 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.preferredHeight: 35
Layout.preferredWidth: 90 Layout.preferredWidth: 90
font.pixelSize: Appearance.font.pixelSize.larger font.pixelSize: Appearance.font.pixelSize.larger
@@ -232,36 +244,89 @@ Item {
} }
} }
// The sliders for adjusting duration // The SpinBoxes for adjusting duration
ColumnLayout { ColumnLayout {
Layout.alignment: Qt.AlignHCenter RowLayout {
spacing: 10 Layout.fillWidth: true
spacing: 20
ConfigSpinBox { StyledText {
text: Translation.tr("Focus Duration: ") id: focusTextBox
value: Pomodoro.pomodoroFocusTime / 60 Layout.leftMargin: focusSpinBox.implicitWidth / 2 - 7
onValueChanged: { text: Translation.tr("Focus")
Pomodoro.pomodoroFocusTime = value * 60
Config.options.time.pomodoro.focus = value * 60
} }
Layout.alignment: Qt.AlignCenter StyledText {
} Layout.leftMargin: breakSpinBox.implicitWidth / 2 + 10
text: Translation.tr("Break")
ConfigSpinBox {
text: Translation.tr("Break Duration:")
value: Pomodoro.pomodoroBreakTime / 60
onValueChanged: {
Config.options.time.pomodoro.breaktime = value * 60
Pomodoro.pomodoroBreakTime = value * 60
} }
} }
ConfigSpinBox { RowLayout {
text: Translation.tr("Long Break Duration:") Layout.alignment: Qt.AlignHCenter
value: Pomodoro.pomodoroLongBreakTime / 60 spacing: 0
onValueChanged:{
Pomodoro.pomodoroLongBreakTime = value * 60 ConfigSpinBox {
Config.options.time.pomodoro.longbreak = value * 60 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 // Stopwatch Tab
Item { Item {
Layout.fillWidth: true
ColumnLayout { ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
spacing: 18 spacing: 20
Layout.fillWidth: true
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
}
RowLayout { RowLayout {
Layout.alignment: Qt.AlignHCenter spacing: 40
spacing: 20 // 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 { ColumnLayout {
buttonText: Pomodoro.isStopwatchRunning ? Translation.tr("Pause") : Translation.tr("Start") anchors.centerIn: parent
Layout.preferredWidth: 90 spacing: 0
Layout.preferredHeight: 35
font.pixelSize: Appearance.font.pixelSize.larger StyledText {
onClicked: Pomodoro.toggleStopwatch() Layout.alignment: Qt.AlignHCenter
background: Rectangle { text: {
color: Appearance.m3colors.m3onSecondary let totalSeconds = Math.floor(Pomodoro.stopwatchTime) / 100
radius: Appearance.rounding.normal let minutes = Math.floor(totalSeconds / 60).toString().padStart(2, '0')
border.color: Appearance.m3colors.m3outline let seconds = Math.floor(totalSeconds % 60).toString().padStart(2, '0')
border.width: 1 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 Layout.alignment: Qt.AlignHCenter
text: Translation.tr("Stopwatch") spacing: 10
font.pixelSize: Appearance.font.pixelSize.large
color: Appearance.m3colors.m3onSurface
}
DialogButton { RippleButton {
buttonText: Translation.tr("Reset") contentItem: StyledText {
Layout.preferredWidth: 90 anchors.centerIn: parent
Layout.preferredHeight: 35 horizontalAlignment: Text.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.larger text: Pomodoro.isStopwatchRunning ? Translation.tr("Stop") : Translation.tr("Start")
onClicked: Pomodoro.stopwatchReset() color: Appearance.colors.colSecondary
background: Rectangle { }
color: Appearance.m3colors.m3onError Layout.preferredHeight: 35
radius: Appearance.rounding.normal Layout.preferredWidth: 90
border.color: Appearance.m3colors.m3outline font.pixelSize: Appearance.font.pixelSize.larger
border.width: 1 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
}
}
} }
} }
} }
+52 -37
View File
@@ -4,9 +4,9 @@ pragma ComponentBehavior: Bound
import qs import qs
import qs.modules.common import qs.modules.common
import Quickshell; import Quickshell
import Quickshell.Io; import Quickshell.Io
import QtQuick; import QtQuick
/** /**
* Simple Pomodoro time manager. * Simple Pomodoro time manager.
@@ -14,29 +14,30 @@ import QtQuick;
Singleton { Singleton {
id: root id: root
// TODO: read these values from a config file.
property int pomodoroFocusTime: Config.options.time.pomodoro.focus property int pomodoroFocusTime: Config.options.time.pomodoro.focus
property int pomodoroBreakTime: Config.options.time.pomodoro.breaktime property int pomodoroBreakTime: Config.options.time.pomodoro.breakTime
property int pomodoroLongBreakTime: Config.options.time.pomodoro.longbreak property int pomodoroLongBreakTime: Config.options.time.pomodoro.longBreak
property int pomodoroLongBreakCycle: Config.options.time.pomodoro.cycle property int pomodoroLongBreakCycle: Config.options.time.pomodoro.cycle
property bool isPomodoroRunning: Config.options.time.pomodoro.running
property int pomodoroTimeLeft: pomodoroFocusTime property int pomodoroTimeLeft: pomodoroFocusTime
property int getPomodoroSecondsLeft: 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 isPomodoroBreak: false
property bool isPomodoroRunning: false
property int pomodoroCycle: 1 property int pomodoroCycle: 1
property int stopwatchTime: 0 property int stopwatchTime: 0
property bool isStopwatchRunning: false property bool isStopwatchRunning: false
property int stopwatchStartTime: 0
property var stopwatchLaps: []
// Pause and Resume button // Start and Stop button
function togglePomodoro() { function togglePomodoro() {
isPomodoroRunning = !isPomodoroRunning Config.options.time.pomodoro.running = !isPomodoroRunning
if (isPomodoroRunning) { // Pressed Start button if (isPomodoroRunning) { // Pressed Start button
pomodoroTimeStarted = getCurrentTime() pomodoroTimeStarted = getCurrentTimeInSeconds()
} else { // Pressed Pause button } else { // Pressed Stop button
pomodoroTimeLeft -= (getCurrentTime() - pomodoroTimeStarted) pomodoroTimeLeft -= (getCurrentTimeInSeconds() - pomodoroTimeStarted)
} }
} }
@@ -45,51 +46,65 @@ Singleton {
pomodoroTimeLeft = pomodoroFocusTime pomodoroTimeLeft = pomodoroFocusTime
getPomodoroSecondsLeft = pomodoroFocusTime getPomodoroSecondsLeft = pomodoroFocusTime
isPomodoroBreak = false isPomodoroBreak = false
isPomodoroRunning = false Config.options.time.pomodoro.running = false
pomodoroCycle = 1
} }
function tickSecond() { function tickSecond() {
if (getCurrentTime() >= pomodoroTimeStarted + pomodoroTimeLeft) { if (getCurrentTimeInSeconds() >= pomodoroTimeStarted + pomodoroTimeLeft) {
isPomodoroBreak = !isPomodoroBreak isPomodoroBreak = !isPomodoroBreak
pomodoroTimeStarted += pomodoroTimeLeft pomodoroTimeStarted += pomodoroTimeLeft
pomodoroTimeLeft = isPomodoroBreak ? pomodoroBreakTime : pomodoroFocusTime pomodoroTimeLeft = isPomodoroBreak ? pomodoroBreakTime : pomodoroFocusTime
if (isPomodoroBreak && pomodoroCycle % pomodoroLongBreakCycle == 0) { // isPomodoroLongBreak let notificationTitle, notificationMessage
Quickshell.execDetached([
"notify-send", if (isPomodoroBreak && pomodoroCycle % pomodoroLongBreakCycle === 0) { // isPomodoroLongBreak
Translation.tr("🌿 Long Break!"), notificationMessage = Translation.tr(`Relax for %1 minutes`).arg(Math.floor(pomodoroLongBreakTime / 60))
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 if(isPomodoroBreak){
Quickshell.execDetached([
"notify-send",
Translation.tr("☕ Short Break!"),
Translation.tr(`Relax for %1 minutes.`).arg(Math.floor(pomodoroBreakTime / 60))
])
} else { } else {
Quickshell.execDetached([ notificationMessage = Translation.tr(`Focus for %1 minutes`).arg(Math.floor(pomodoroFocusTime / 60))
"notify-send",
Translation.tr("🔴 Pomodoro started!"),
Translation.tr(`Focus for %1 minutes.`).arg(Math.floor(pomodoroFocusTime / 60))
])
pomodoroCycle += 1 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) 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 // Stopwatch functions
function toggleStopwatch() { function toggleStopwatch() {
isStopwatchRunning = !isStopwatchRunning isStopwatchRunning = !isStopwatchRunning
if (isStopwatchRunning) {
// Resume from paused time by adjusting start time
stopwatchStartTime = getCurrentTimeIn10ms() - stopwatchTime
}
} }
function stopwatchReset() { function stopwatchReset() {
stopwatchTime = 0 if (isStopwatchRunning) { // Clicked on Lap
isStopwatchRunning = false 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 = []
}
} }
} }