forked from Shinonome/dots-hyprland
@@ -146,6 +146,14 @@ Singleton {
|
|||||||
property color colScrim: ColorUtils.transparentize(m3colors.m3scrim, 0.5)
|
property color colScrim: ColorUtils.transparentize(m3colors.m3scrim, 0.5)
|
||||||
property color colShadow: ColorUtils.transparentize(m3colors.m3shadow, 0.7)
|
property color colShadow: ColorUtils.transparentize(m3colors.m3shadow, 0.7)
|
||||||
property color colOutlineVariant: m3colors.m3outlineVariant
|
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 {
|
rounding: QtObject {
|
||||||
|
|||||||
@@ -275,6 +275,13 @@ 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 string alertSound: ""
|
||||||
|
property int breakTime: 300
|
||||||
|
property int cyclesBeforeLongBreak: 4
|
||||||
|
property int focus: 1500
|
||||||
|
property int longBreak: 900
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property JsonObject windows: JsonObject {
|
property JsonObject windows: JsonObject {
|
||||||
|
|||||||
@@ -11,18 +11,35 @@ Singleton {
|
|||||||
property string fileName: "states.json"
|
property string fileName: "states.json"
|
||||||
property string filePath: `${root.fileDir}/${root.fileName}`
|
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 {
|
FileView {
|
||||||
|
id: persistentStatesFileView
|
||||||
path: root.filePath
|
path: root.filePath
|
||||||
|
|
||||||
watchChanges: true
|
watchChanges: true
|
||||||
onFileChanged: reload()
|
onFileChanged: fileReloadTimer.restart()
|
||||||
onAdapterUpdated: {
|
onAdapterUpdated: fileWriteTimer.restart()
|
||||||
writeAdapter()
|
|
||||||
}
|
|
||||||
onLoadFailed: error => {
|
onLoadFailed: error => {
|
||||||
console.log("Failed to load persistent states file:", error);
|
console.log("Failed to load persistent states file:", error);
|
||||||
if (error == FileViewError.FileNotFound) {
|
if (error == FileViewError.FileNotFound) {
|
||||||
writeAdapter();
|
fileWriteTimer.restart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +61,20 @@ Singleton {
|
|||||||
property bool allowNsfw: false
|
property bool allowNsfw: false
|
||||||
property string provider: "yandere"
|
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<var> laps: []
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ Button {
|
|||||||
property color colRipple: Appearance?.colors.colLayer1Active ?? "#D6CEE2"
|
property color colRipple: Appearance?.colors.colLayer1Active ?? "#D6CEE2"
|
||||||
property color colRippleToggled: Appearance?.colors.colPrimaryActive ?? "#D6CEE2"
|
property color colRippleToggled: Appearance?.colors.colPrimaryActive ?? "#D6CEE2"
|
||||||
|
|
||||||
|
opacity: root.enabled ? 1 : 0.4
|
||||||
property color buttonColor: root.enabled ? (root.toggled ?
|
property color buttonColor: root.enabled ? (root.toggled ?
|
||||||
(root.hovered ? colBackgroundToggledHover :
|
(root.hovered ? colBackgroundToggledHover :
|
||||||
colBackgroundToggled) :
|
colBackgroundToggled) :
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import qs
|
|||||||
import qs.services
|
import qs.services
|
||||||
import "./calendar"
|
import "./calendar"
|
||||||
import "./todo"
|
import "./todo"
|
||||||
|
import "./pomodoro"
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
|
||||||
@@ -17,7 +18,8 @@ Rectangle {
|
|||||||
property bool collapsed: Persistent.states.sidebar.bottomGroup.collapsed
|
property bool collapsed: Persistent.states.sidebar.bottomGroup.collapsed
|
||||||
property var tabs: [
|
property var tabs: [
|
||||||
{"type": "calendar", "name": Translation.tr("Calendar"), "icon": "calendar_month", "widget": calendarWidget},
|
{"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 {
|
Behavior on implicitHeight {
|
||||||
@@ -238,4 +240,13 @@ Rectangle {
|
|||||||
anchors.margins: 5
|
anchors.margins: 5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pomodoro component
|
||||||
|
Component {
|
||||||
|
id: pomodoroWidget
|
||||||
|
PomodoroWidget {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 5
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 `:<sub>${(Math.floor(TimerService.stopwatchTime) % 100).toString().padStart(2, '0')}</sub>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user