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/PomodoroWidget.qml b/.config/quickshell/ii/modules/sidebarRight/pomodoro/PomodoroWidget.qml new file mode 100644 index 000000000..b39be28ce --- /dev/null +++ b/.config/quickshell/ii/modules/sidebarRight/pomodoro/PomodoroWidget.qml @@ -0,0 +1,356 @@ +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": "timer_play"}, + {"name": Translation.tr("Stopwatch"), "icon": "timer"} + ] + property bool showDialog: false + property int dialogMargins: 20 + property int fabSize: 48 + property int fabMargins: 14 + + + // These are keybinds, make sure to change them. + Keys.onPressed: (event) => { + if ((event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp) && event.modifiers === Qt.NoModifier) { + 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 && !showDialog) { + // Toggle start/pause with Space key + if (currentTab === 0) { + Pomodoro.togglePomodoro() + } else { + Pomodoro.toggleStopwatch() + } + event.accepted = true + } else if (event.key === Qt.Key_R && !showDialog) { + // Reset with R key + if (currentTab === 0) { + Pomodoro.pomodoroReset() + } else { + Pomodoro.stopwatchReset() + } + event.accepted = true + } else if (event.key === Qt.Key_Escape && showDialog) { + showDialog = false + event.accepted = true + } + } + + Timer { + id: pomodoroTimer + interval: 1000 + running: Pomodoro.isPomodoroRunning + repeat: true + onTriggered: Pomodoro.tickSecond() + } + + Timer { + id: stopwatchTimer + interval: 1000 + running: Pomodoro.isStopwatchRunning + repeat: true + onTriggered: { + Pomodoro.stopwatchTime += 1 + } + } + + + 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 + } + + // Pomodoro Timer Tab + Item { + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + spacing: 18 + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: Pomodoro.timeFormattedPomodoro() + font.pixelSize: 50 + color: Appearance.m3colors.m3onSurface + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 20 + + DialogButton { + buttonText: Pomodoro.isPomodoroRunning ? Translation.tr("Pause") : Translation.tr("Start") + Layout.preferredWidth: 90 + Layout.preferredHeight: 35 + font.pixelSize: Appearance.font.pixelSize.larger + onClicked: Pomodoro.togglePomodoro() + background: Rectangle { + color: Appearance.m3colors.m3onSecondary + radius: Appearance.rounding.normal + border.color: Appearance.m3colors.m3outline + border.width: 1 + } + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: Pomodoro.isPomodoroBreak ? Translation.tr("Break time") : Translation.tr("Focus time") + font.pixelSize: Appearance.font.pixelSize.largest + color: Appearance.m3colors.m3onSurface + } + + DialogButton { + buttonText: Translation.tr("Reset") + Layout.preferredWidth: 90 + Layout.preferredHeight: 35 + font.pixelSize: Appearance.font.pixelSize.larger + onClicked: Pomodoro.pomodoroReset() + background: Rectangle { + color: Appearance.m3colors.m3onError + radius: Appearance.rounding.normal + border.color: Appearance.m3colors.m3outline + border.width: 1 + } + } + } + + ColumnLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 0 + + StyledText { + text: Translation.tr("Focus Duration: %1 min").arg(Pomodoro.pomodoroWorkTime / 60) + color: Appearance.m3colors.m3onSurface + } + Slider { + id: workTimeSlider + Layout.fillWidth: true + from: 5 + to: 120 + stepSize: 1 + value: Pomodoro.pomodoroWorkTime / 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 { + text: Translation.tr("Break Duration: %1 min").arg(Pomodoro.pomodoroBreakTime / 60) + color: Appearance.m3colors.m3onSurface + } + Slider { + id: breakTimeSlider + 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 + } + } + } + } + } + + // Stopwatch Tab + Item { + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + spacing: 18 + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: Pomodoro.timeFormattedStopwatch() + font.pixelSize: 50 + color: Appearance.m3colors.m3onSurface + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 20 + + 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 + } + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: Translation.tr("Stopwatch") + font.pixelSize: Appearance.font.pixelSize.largest + color: Appearance.m3colors.m3onSurface + } + + 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 + } + } + } + } + } + } + } + + // + 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 + } + } +} \ No newline at end of file diff --git a/.config/quickshell/ii/services/Pomodoro.qml b/.config/quickshell/ii/services/Pomodoro.qml new file mode 100644 index 000000000..e29ef25ab --- /dev/null +++ b/.config/quickshell/ii/services/Pomodoro.qml @@ -0,0 +1,69 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import qs.modules.common +import Quickshell +import Quickshell.Io +import QtQuick + +/** + * Simple Pomodoro time manager. + */ +Singleton { + id: root + + property int pomodoroWorkTime: 25 * 60 // 25 minutes in seconds + property int pomodoroBreakTime: 5 * 60 // 5 minutes in seconds + property int pomodoroTime: pomodoroWorkTime + property bool isPomodoroRunning: false + property bool isPomodoroBreak: false + property int stopwatchTime: 0 + property bool isStopwatchRunning: false + + function togglePomodoro() { + isPomodoroRunning = !isPomodoroRunning; + } + + function toggleStopwatch() { + isStopwatchRunning = !isStopwatchRunning; + } + + 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() { + let minutes = Math.floor(pomodoroTime / 60); + let seconds = Math.floor(pomodoroTime % 60); + return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + } + + function timeFormattedStopwatch() { + let totalSeconds = Math.floor(stopwatchTime); + let hours = Math.floor(totalSeconds / 3600); + let minutes = Math.floor((totalSeconds % 3600) / 60); + let seconds = Math.floor(totalSeconds % 60); + return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + } +}