forked from Shinonome/dots-hyprland
Polished the UI and added Persistent Config option.
Signed-off-by: Nyx <189459385+nyx-4@users.noreply.github.com> Changelog: Added Config.options.time.pomodoro options in Config.qml Restructered the Pomodoro logic and added Long break Used timestamp instead of naively counting down. Major UI tweaks.
This commit is contained in:
@@ -253,6 +253,12 @@ Singleton {
|
|||||||
// https://doc.qt.io/qt-6/qtime.html#toString
|
// https://doc.qt.io/qt-6/qtime.html#toString
|
||||||
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 int breaktime: 300
|
||||||
|
property int cycle: 4
|
||||||
|
property int focus: 1500
|
||||||
|
property int longbreak: 1200
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property JsonObject windows: JsonObject {
|
property JsonObject windows: JsonObject {
|
||||||
|
|||||||
@@ -166,102 +166,102 @@ Item {
|
|||||||
Item {
|
Item {
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
spacing: 18
|
spacing: 20
|
||||||
|
|
||||||
StyledText {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
text: Pomodoro.timeFormattedPomodoro()
|
|
||||||
font.pixelSize: 50
|
|
||||||
color: Appearance.m3colors.m3onSurface
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
spacing: 40
|
||||||
spacing: 20
|
// The Pomodoro timer circle
|
||||||
|
CircularProgress {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
lineWidth: 7
|
||||||
|
value: {
|
||||||
|
let pomodoroTotalTime = Pomodoro.isPomodoroBreak ? Pomodoro.pomodoroBreakTime : Pomodoro.pomodoroFocusTime
|
||||||
|
return Pomodoro.getPomodoroSecondsLeft / pomodoroTotalTime
|
||||||
|
}
|
||||||
|
size: 125
|
||||||
|
secondaryColor: Appearance.colors.colSecondaryContainer
|
||||||
|
primaryColor: Appearance.m3colors.m3onSecondaryContainer
|
||||||
|
enableAnimation: true
|
||||||
|
|
||||||
DialogButton {
|
ColumnLayout {
|
||||||
buttonText: Pomodoro.isPomodoroRunning ? Translation.tr("Pause") : Translation.tr("Start")
|
anchors.centerIn: parent
|
||||||
Layout.preferredWidth: 90
|
|
||||||
Layout.preferredHeight: 35
|
StyledText {
|
||||||
font.pixelSize: Appearance.font.pixelSize.larger
|
Layout.alignment: Qt.AlignHCenter
|
||||||
onClicked: Pomodoro.togglePomodoro()
|
text: {
|
||||||
background: Rectangle {
|
let minutes = Math.floor(Pomodoro.getPomodoroSecondsLeft / 60).toString().padStart(2, '0')
|
||||||
color: Appearance.m3colors.m3onSecondary
|
let seconds = Math.floor(Pomodoro.getPomodoroSecondsLeft % 60).toString().padStart(2, '0')
|
||||||
radius: Appearance.rounding.normal
|
return `${minutes}:${seconds}`
|
||||||
border.color: Appearance.m3colors.m3outline
|
}
|
||||||
border.width: 1
|
font.pixelSize: Appearance.font.pixelSize.hugeass + 4
|
||||||
|
color: Appearance.m3colors.m3onSurface
|
||||||
|
}
|
||||||
|
StyledText {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
text: Pomodoro.isPomodoroBreak ? Translation.tr("Break") : Translation.tr("Focus")
|
||||||
|
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: Pomodoro.isPomodoroBreak ? Translation.tr("Break time") : Translation.tr("Focus time")
|
spacing: 20
|
||||||
font.pixelSize: Appearance.font.pixelSize.largest
|
|
||||||
color: Appearance.m3colors.m3onSurface
|
|
||||||
}
|
|
||||||
|
|
||||||
DialogButton {
|
RippleButton {
|
||||||
buttonText: Translation.tr("Reset")
|
buttonText: Pomodoro.isPomodoroRunning ? Translation.tr("Pause") : Translation.tr("Start")
|
||||||
Layout.preferredWidth: 90
|
Layout.preferredHeight: 35
|
||||||
Layout.preferredHeight: 35
|
Layout.preferredWidth: 90
|
||||||
font.pixelSize: Appearance.font.pixelSize.larger
|
font.pixelSize: Appearance.font.pixelSize.larger
|
||||||
onClicked: Pomodoro.pomodoroReset()
|
onClicked: Pomodoro.togglePomodoro()
|
||||||
background: Rectangle {
|
colBackground: Appearance.m3colors.m3onSecondary
|
||||||
color: Appearance.m3colors.m3onError
|
colBackgroundHover: Appearance.m3colors.m3onSecondary
|
||||||
radius: Appearance.rounding.normal
|
}
|
||||||
border.color: Appearance.m3colors.m3outline
|
|
||||||
border.width: 1
|
RippleButton {
|
||||||
|
buttonText: Translation.tr("Reset")
|
||||||
|
Layout.preferredHeight: 35
|
||||||
|
Layout.preferredWidth: 90
|
||||||
|
font.pixelSize: Appearance.font.pixelSize.larger
|
||||||
|
onClicked: Pomodoro.pomodoroReset()
|
||||||
|
colBackground: Appearance.m3colors.m3onError
|
||||||
|
colBackgroundHover: Appearance.m3colors.m3onError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The sliders for adjusting duration
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
spacing: 0
|
spacing: 10
|
||||||
|
|
||||||
StyledText {
|
ConfigSpinBox {
|
||||||
text: Translation.tr("Focus Duration: %1 min").arg(Pomodoro.pomodoroWorkTime / 60)
|
text: Translation.tr("Focus Duration: ")
|
||||||
color: Appearance.m3colors.m3onSurface
|
value: Pomodoro.pomodoroFocusTime / 60
|
||||||
|
onValueChanged: {
|
||||||
|
Pomodoro.pomodoroFocusTime = value * 60
|
||||||
|
Config.options.time.pomodoro.focus = value * 60
|
||||||
|
}
|
||||||
|
Layout.alignment: Qt.AlignCenter
|
||||||
}
|
}
|
||||||
Slider {
|
|
||||||
id: workTimeSlider
|
ConfigSpinBox {
|
||||||
Layout.fillWidth: true
|
text: Translation.tr("Break Duration:")
|
||||||
from: 5
|
value: Pomodoro.pomodoroBreakTime / 60
|
||||||
to: 120
|
onValueChanged: {
|
||||||
stepSize: 1
|
Config.options.time.pomodoro.breaktime = value * 60
|
||||||
value: Pomodoro.pomodoroWorkTime / 60
|
Pomodoro.pomodoroBreakTime = value * 60
|
||||||
onValueChanged: Pomodoro.pomodoroWorkTime = value * 60
|
|
||||||
handle: Rectangle {
|
|
||||||
x: workTimeSlider.leftPadding + workTimeSlider.visualPosition * (workTimeSlider.availableWidth - width)
|
|
||||||
y: workTimeSlider.topPadding + (workTimeSlider.availableHeight - height) / 2
|
|
||||||
implicitWidth: 20
|
|
||||||
implicitHeight: 20
|
|
||||||
radius: 10
|
|
||||||
color: Appearance.m3colors.m3onSecondary
|
|
||||||
border.color: Appearance.m3colors.m3outline
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
ConfigSpinBox {
|
||||||
text: Translation.tr("Break Duration: %1 min").arg(Pomodoro.pomodoroBreakTime / 60)
|
text: Translation.tr("Long Break Duration:")
|
||||||
color: Appearance.m3colors.m3onSurface
|
value: Pomodoro.pomodoroLongBreakTime / 60
|
||||||
}
|
onValueChanged:{
|
||||||
Slider {
|
Pomodoro.pomodoroLongBreakTime = value * 60
|
||||||
id: breakTimeSlider
|
Config.options.time.pomodoro.longbreak = value * 60
|
||||||
Layout.fillWidth: true
|
|
||||||
from: 1
|
|
||||||
to: 60
|
|
||||||
stepSize: 1
|
|
||||||
value: Pomodoro.pomodoroBreakTime / 60
|
|
||||||
onValueChanged: Pomodoro.pomodoroBreakTime = value * 60
|
|
||||||
handle: Rectangle {
|
|
||||||
x: breakTimeSlider.leftPadding + breakTimeSlider.visualPosition * (breakTimeSlider.availableWidth - width)
|
|
||||||
y: breakTimeSlider.topPadding + (breakTimeSlider.availableHeight - height) / 2
|
|
||||||
implicitWidth: 20
|
|
||||||
implicitHeight: 20
|
|
||||||
radius: 10
|
|
||||||
color: Appearance.m3colors.m3onSecondary
|
|
||||||
border.color: Appearance.m3colors.m3outline
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -276,7 +276,13 @@ Item {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
text: Pomodoro.timeFormattedStopwatch()
|
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
|
font.pixelSize: 50
|
||||||
color: Appearance.m3colors.m3onSurface
|
color: Appearance.m3colors.m3onSurface
|
||||||
}
|
}
|
||||||
@@ -302,7 +308,7 @@ Item {
|
|||||||
StyledText {
|
StyledText {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
text: Translation.tr("Stopwatch")
|
text: Translation.tr("Stopwatch")
|
||||||
font.pixelSize: Appearance.font.pixelSize.largest
|
font.pixelSize: Appearance.font.pixelSize.large
|
||||||
color: Appearance.m3colors.m3onSurface
|
color: Appearance.m3colors.m3onSurface
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,33 +330,4 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// + FAB
|
|
||||||
StyledRectangularShadow {
|
|
||||||
target: fabButton
|
|
||||||
radius: fabButton.buttonRadius
|
|
||||||
blur: 0.6 * Appearance.sizes.elevationMargin
|
|
||||||
}
|
|
||||||
FloatingActionButton {
|
|
||||||
id: fabButton
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.rightMargin: root.fabMargins
|
|
||||||
anchors.bottomMargin: root.fabMargins
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
if (currentTab === 0) {
|
|
||||||
Pomodoro.togglePomodoro()
|
|
||||||
} else {
|
|
||||||
Pomodoro.toggleStopwatch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
contentItem: MaterialSymbol {
|
|
||||||
text: (currentTab === 0 && Pomodoro.isPomodoroRunning) || (currentTab === 1 && Pomodoro.isStopwatchRunning) ? "pause" : "play_arrow"
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
iconSize: Appearance.font.pixelSize.huge
|
|
||||||
color: Appearance.m3colors.m3onPrimaryContainer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
pragma Singleton
|
pragma Singleton
|
||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import qs
|
||||||
import qs.modules.common
|
import qs.modules.common
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
import Quickshell;
|
||||||
import QtQuick
|
import Quickshell.Io;
|
||||||
|
import QtQuick;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple Pomodoro time manager.
|
* Simple Pomodoro time manager.
|
||||||
@@ -12,58 +14,82 @@ import QtQuick
|
|||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property int pomodoroWorkTime: 25 * 60 // 25 minutes in seconds
|
// TODO: read these values from a config file.
|
||||||
property int pomodoroBreakTime: 5 * 60 // 5 minutes in seconds
|
property int pomodoroFocusTime: Config.options.time.pomodoro.focus
|
||||||
property int pomodoroTime: pomodoroWorkTime
|
property int pomodoroBreakTime: Config.options.time.pomodoro.breaktime
|
||||||
property bool isPomodoroRunning: false
|
property int pomodoroLongBreakTime: Config.options.time.pomodoro.longbreak
|
||||||
|
property int pomodoroLongBreakCycle: Config.options.time.pomodoro.cycle
|
||||||
|
|
||||||
|
property int pomodoroTimeLeft: pomodoroFocusTime
|
||||||
|
property int getPomodoroSecondsLeft: pomodoroFocusTime
|
||||||
|
property int pomodoroTimeStarted: getCurrentTime() // The time pomodoro was last Resumed
|
||||||
property bool isPomodoroBreak: false
|
property bool isPomodoroBreak: false
|
||||||
|
property bool isPomodoroRunning: false
|
||||||
|
property int pomodoroCycle: 1
|
||||||
|
|
||||||
property int stopwatchTime: 0
|
property int stopwatchTime: 0
|
||||||
property bool isStopwatchRunning: false
|
property bool isStopwatchRunning: false
|
||||||
|
|
||||||
|
// Pause and Resume button
|
||||||
function togglePomodoro() {
|
function togglePomodoro() {
|
||||||
isPomodoroRunning = !isPomodoroRunning;
|
isPomodoroRunning = !isPomodoroRunning
|
||||||
}
|
if (isPomodoroRunning) { // Pressed Start button
|
||||||
|
pomodoroTimeStarted = getCurrentTime()
|
||||||
function toggleStopwatch() {
|
} else { // Pressed Pause button
|
||||||
isStopwatchRunning = !isStopwatchRunning;
|
pomodoroTimeLeft -= (getCurrentTime() - pomodoroTimeStarted)
|
||||||
}
|
|
||||||
|
|
||||||
function pomodoroReset() {
|
|
||||||
pomodoroTime = pomodoroWorkTime;
|
|
||||||
isPomodoroRunning = false;
|
|
||||||
isPomodoroBreak = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopwatchReset() {
|
|
||||||
stopwatchTime = 0;
|
|
||||||
isStopwatchRunning = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function tickSecond() {
|
|
||||||
if (pomodoroTime > 0) {
|
|
||||||
pomodoroTime--;
|
|
||||||
} else {
|
|
||||||
isPomodoroBreak = !isPomodoroBreak;
|
|
||||||
pomodoroTime = isPomodoroBreak ? pomodoroBreakTime : pomodoroWorkTime;
|
|
||||||
if (isPomodoroBreak) {
|
|
||||||
Quickshell.execDetached(["bash", "-c", `notify-send "☕ Short Break!" "Relax for ${Math.floor(pomodoroBreakTime / 60)} minutes."`]);
|
|
||||||
} else {
|
|
||||||
Quickshell.execDetached(["bash", "-c", `notify-send "🔴 Pomodoro started!" "Focus for ${Math.floor(pomodoroWorkTime / 60)} minutes."`]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function timeFormattedPomodoro() {
|
// Reset button
|
||||||
let minutes = Math.floor(pomodoroTime / 60);
|
function pomodoroReset() {
|
||||||
let seconds = Math.floor(pomodoroTime % 60);
|
pomodoroTimeLeft = pomodoroFocusTime
|
||||||
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
getPomodoroSecondsLeft = pomodoroFocusTime
|
||||||
|
isPomodoroBreak = false
|
||||||
|
isPomodoroRunning = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function timeFormattedStopwatch() {
|
function tickSecond() {
|
||||||
let totalSeconds = Math.floor(stopwatchTime);
|
if (getCurrentTime() >= pomodoroTimeStarted + pomodoroTimeLeft) {
|
||||||
let hours = Math.floor(totalSeconds / 3600);
|
isPomodoroBreak = !isPomodoroBreak
|
||||||
let minutes = Math.floor((totalSeconds % 3600) / 60);
|
pomodoroTimeStarted += pomodoroTimeLeft
|
||||||
let seconds = Math.floor(totalSeconds % 60);
|
pomodoroTimeLeft = isPomodoroBreak ? pomodoroBreakTime : pomodoroFocusTime
|
||||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
|
||||||
|
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))
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
Quickshell.execDetached([
|
||||||
|
"notify-send",
|
||||||
|
Translation.tr("🔴 Pomodoro started!"),
|
||||||
|
Translation.tr(`Focus for %1 minutes.`).arg(Math.floor(pomodoroFocusTime / 60))
|
||||||
|
])
|
||||||
|
pomodoroCycle += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPomodoroSecondsLeft = (pomodoroTimeStarted + pomodoroTimeLeft) - getCurrentTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentTime() {
|
||||||
|
return Math.floor(Date.now() / 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stopwatch functions
|
||||||
|
function toggleStopwatch() {
|
||||||
|
isStopwatchRunning = !isStopwatchRunning
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopwatchReset() {
|
||||||
|
stopwatchTime = 0
|
||||||
|
isStopwatchRunning = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user