Watch
1
0
Fork
You've already forked souveraine
0

shell: own the right sidebar, move garden into the drawer

Garden was welded onto the notification list in CenterWidgetGroup —
always on, unrelated, stealing space from notifications that said
"Nothing". Moved it into the BottomWidgetGroup drawer as a fifth tab
alongside Calendar, To Do, Pomodoro, and Stopwatch.

CenterWidgetGroup is now just the notification list. Lens events
(note_created, synced) emit through notify-send so they land in the
notification system properly.

Forked calendar, todo, pomodoro, notifications out of ii-base into
our tree — the right sidebar no longer borrows any widget files from
ii. Toggle dialogs (bluetooth, wifi, nightLight) still symlinked as
infrastructure.
This commit is contained in:
Fimeg 2026-08-21 15:42:37 -04:00
commit b8e76c4e9e
22 changed files with 2043 additions and 13 deletions

View file

@ -136,7 +136,22 @@ modules/ii/sidebarRight/SidebarRight.qml souveraine/modules/ii/sidebarRight/Sid
modules/ii/sidebarRight/SidebarRightContent.qml souveraine/modules/ii/sidebarRight/SidebarRightContent.qml
modules/ii/sidebarRight/QuickSliders.qml souveraine/modules/ii/sidebarRight/QuickSliders.qml
modules/ii/sidebarRight/CenterWidgetGroup.qml souveraine/modules/ii/sidebarRight/CenterWidgetGroup.qml
modules/ii/sidebarRight/BottomWidgetGroup.qml souveraine/modules/ii/sidebarRight/BottomWidgetGroup.qml
modules/ii/sidebarRight/lens/LensWidget.qml souveraine/modules/ii/sidebarRight/lens/LensWidget.qml
modules/ii/sidebarRight/calendar/CalendarWidget.qml souveraine/modules/ii/sidebarRight/calendar/CalendarWidget.qml
modules/ii/sidebarRight/calendar/CalendarDayButton.qml souveraine/modules/ii/sidebarRight/calendar/CalendarDayButton.qml
modules/ii/sidebarRight/calendar/CalendarHeaderButton.qml souveraine/modules/ii/sidebarRight/calendar/CalendarHeaderButton.qml
modules/ii/sidebarRight/calendar/calendar_layout.js souveraine/modules/ii/sidebarRight/calendar/calendar_layout.js
modules/ii/sidebarRight/todo/TodoWidget.qml souveraine/modules/ii/sidebarRight/todo/TodoWidget.qml
modules/ii/sidebarRight/todo/TaskList.qml souveraine/modules/ii/sidebarRight/todo/TaskList.qml
modules/ii/sidebarRight/todo/TodoItemActionButton.qml souveraine/modules/ii/sidebarRight/todo/TodoItemActionButton.qml
modules/ii/sidebarRight/pomodoro/PomodoroWidget.qml souveraine/modules/ii/sidebarRight/pomodoro/PomodoroWidget.qml
modules/ii/sidebarRight/pomodoro/StopwatchWidget.qml souveraine/modules/ii/sidebarRight/pomodoro/StopwatchWidget.qml
modules/ii/sidebarRight/pomodoro/PomodoroTimer.qml souveraine/modules/ii/sidebarRight/pomodoro/PomodoroTimer.qml
modules/ii/sidebarRight/pomodoro/PomodoroSettings.qml souveraine/modules/ii/sidebarRight/pomodoro/PomodoroSettings.qml
modules/ii/sidebarRight/pomodoro/Stopwatch.qml souveraine/modules/ii/sidebarRight/pomodoro/Stopwatch.qml
modules/ii/sidebarRight/notifications/NotificationList.qml souveraine/modules/ii/sidebarRight/notifications/NotificationList.qml
modules/ii/sidebarRight/notifications/NotificationStatusButton.qml souveraine/modules/ii/sidebarRight/notifications/NotificationStatusButton.qml
modules/ii/sidebarRight/volumeMixer/VolumeDialogContent.qml souveraine/modules/ii/sidebarRight/volumeMixer/VolumeDialogContent.qml
modules/ii/bar/UtilButtons.qml souveraine/modules/ii/bar/UtilButtons.qml
modules/ii/bar/BarContent.qml souveraine/modules/ii/bar/BarContent.qml
@ -215,6 +230,10 @@ modules/ii/sessionScreen/qmldir souveraine/modules/ii/sessionScreen/qml
modules/ii/sidebarLeft/qmldir souveraine/modules/ii/sidebarLeft/qmldir
modules/ii/sidebarRight/qmldir souveraine/modules/ii/sidebarRight/qmldir
modules/ii/sidebarRight/volumeMixer/qmldir souveraine/modules/ii/sidebarRight/volumeMixer/qmldir
modules/ii/sidebarRight/calendar/qmldir souveraine/modules/ii/sidebarRight/calendar/qmldir
modules/ii/sidebarRight/todo/qmldir souveraine/modules/ii/sidebarRight/todo/qmldir
modules/ii/sidebarRight/pomodoro/qmldir souveraine/modules/ii/sidebarRight/pomodoro/qmldir
modules/ii/sidebarRight/notifications/qmldir souveraine/modules/ii/sidebarRight/notifications/qmldir
modules/settings/qmldir souveraine/modules/settings/qmldir
"

View file

@ -0,0 +1,294 @@
pragma ComponentBehavior: Bound
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import qs.modules.ii.sidebarRight.calendar
import qs.modules.ii.sidebarRight.todo
import qs.modules.ii.sidebarRight.pomodoro
import QtQuick
import QtQuick.Layouts
Rectangle {
id: root
radius: Appearance.rounding.normal
color: Appearance.colors.colLayer1
clip: true
implicitHeight: collapsed ? collapsedBottomWidgetGroupRow.implicitHeight : 430
property int selectedTab: Persistent.states.sidebar.bottomGroup.tab
property int previousIndex: -1
property bool collapsed: Persistent.states.sidebar.bottomGroup.collapsed
readonly property int collapsedHeight: Math.max(
Appearance.font.pixelSize.larger + 24,
Appearance.font.pixelSize.large + 24
)
property var tabs: [
{
"type": "calendar",
"name": Translation.tr("Calendar"),
"icon": "calendar_month",
"widget": "calendar/CalendarWidget.qml"
},
{
"type": "todo",
"name": Translation.tr("To Do"),
"icon": "done_outline",
"widget": "todo/TodoWidget.qml"
},
{
"type": "pomodoro",
"name": Translation.tr("Pomodoro"),
"icon": "search_activity",
"widget": "pomodoro/PomodoroWidget.qml"
},
{
"type": "stopwatch",
"name": Translation.tr("Stopwatch"),
"icon": "timer",
"widget": "pomodoro/StopwatchWidget.qml"
},
{
"type": "garden",
"name": Translation.tr("Garden"),
"icon": "hub",
"widget": "lens/LensWidget.qml"
},
]
Behavior on implicitHeight {
NumberAnimation {
duration: Appearance.animation.elementMove.duration
easing.type: Appearance.animation.elementMove.type
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
}
}
function setCollapsed(state) {
Persistent.states.sidebar.bottomGroup.collapsed = state;
if (collapsed) {
bottomWidgetGroupRow.opacity = 0;
} else {
collapsedBottomWidgetGroupRow.opacity = 0;
}
collapseCleanFadeTimer.start();
}
Connections {
target: Persistent.states.sidebar.bottomGroup
function onTabChanged() {
root.selectedTab = Persistent.states.sidebar.bottomGroup.tab;
}
}
Timer {
id: collapseCleanFadeTimer
interval: Appearance.animation.elementMove.duration / 2
repeat: false
onTriggered: {
if (collapsed)
collapsedBottomWidgetGroupRow.opacity = 1;
else
bottomWidgetGroupRow.opacity = 1;
}
}
Keys.onPressed: event => {
if ((event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp) && event.modifiers === Qt.ControlModifier) {
if (event.key === Qt.Key_PageDown) {
root.selectedTab = Math.min(root.selectedTab + 1, root.tabs.length - 1);
} else if (event.key === Qt.Key_PageUp) {
root.selectedTab = Math.max(root.selectedTab - 1, 0);
}
event.accepted = true;
}
}
RowLayout {
id: collapsedBottomWidgetGroupRow
opacity: collapsed ? 1 : 0
visible: opacity > 0
Behavior on opacity {
NumberAnimation {
id: collapsedBottomWidgetGroupRowFade
duration: Appearance.animation.elementMove.duration / 2
easing.type: Appearance.animation.elementMove.type
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
}
}
spacing: 15
CalendarHeaderButton {
Layout.margins: 10
Layout.rightMargin: 0
forceCircle: true
downAction: () => {
root.setCollapsed(false);
}
contentItem: MaterialSymbol {
text: "keyboard_arrow_up"
iconSize: Appearance.font.pixelSize.larger
horizontalAlignment: Text.AlignHCenter
color: Appearance.colors.colOnLayer1
}
}
StyledText {
property int remainingTasks: Todo.list.filter(task => !task.done).length
Layout.margins: 10
Layout.leftMargin: 0
text: Translation.tr("%1 • %2 tasks").arg(DateTime.collapsedCalendarFormat).arg(remainingTasks)
font.pixelSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer1
}
}
RowLayout {
id: bottomWidgetGroupRow
opacity: collapsed ? 0 : 1
visible: opacity > 0
Behavior on opacity {
NumberAnimation {
id: bottomWidgetGroupRowFade
duration: Appearance.animation.elementMove.duration / 2
easing.type: Appearance.animation.elementMove.type
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
}
}
anchors.fill: parent
spacing: 20
Item {
Layout.fillHeight: true
Layout.fillWidth: false
Layout.leftMargin: 10
Layout.topMargin: 10
implicitWidth: tabBar.implicitWidth
NavigationRailTabArray {
id: tabBar
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 5
currentIndex: root.selectedTab
expanded: false
Repeater {
model: root.tabs
NavigationRailButton {
required property int index
required property var modelData
showToggledHighlight: false
toggled: root.selectedTab == index
buttonText: modelData.name
buttonIcon: modelData.icon
onPressed: {
root.selectedTab = index;
Persistent.states.sidebar.bottomGroup.tab = index;
}
}
}
}
CalendarHeaderButton {
anchors.left: parent.left
anchors.top: parent.top
forceCircle: true
downAction: () => {
root.setCollapsed(true);
}
contentItem: MaterialSymbol {
text: "keyboard_arrow_down"
iconSize: Appearance.font.pixelSize.larger
horizontalAlignment: Text.AlignHCenter
color: Appearance.colors.colOnLayer1
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Loader {
id: tabStack
anchors.fill: parent
anchors.bottomMargin: -anchors.topMargin
Component.onCompleted: {
tabStack.source = root.tabs[root.selectedTab].widget;
}
Connections {
target: root
function onSelectedTabChanged() {
if (root.selectedTab > root.previousIndex)
tabSwitchBehavior.animation.down = true;
else if (root.selectedTab < root.previousIndex)
tabSwitchBehavior.animation.down = false;
tabStack.source = root.tabs[root.selectedTab].widget;
}
}
Behavior on source {
id: tabSwitchBehavior
animation: TabSwitchAnim {
id: upAnim
down: true
}
}
}
}
}
component TabSwitchAnim: SequentialAnimation {
id: switchAnim
property bool down: false
ParallelAnimation {
PropertyAnimation {
target: tabStack
properties: "opacity"
to: 0
duration: Appearance.animation.elementMoveFast.duration
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
PropertyAnimation {
target: tabStack.anchors
properties: "topMargin"
to: 10 * (switchAnim.down ? -1 : 1)
duration: Appearance.animation.elementMoveFast.duration
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
}
PropertyAction {
target: tabStack
property: "source"
value: root.tabs[root.selectedTab].widget
}
ParallelAnimation {
PropertyAnimation {
target: tabStack.anchors
properties: "topMargin"
from: 10 * -(switchAnim.down ? -1 : 1)
to: 0
duration: Appearance.animation.elementMoveFast.duration
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.animation.elementMoveEnter.bezierCurve
}
PropertyAnimation {
target: tabStack
properties: "opacity"
to: 1
duration: Appearance.animation.elementMoveFast.duration
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.animation.elementMoveEnter.bezierCurve
}
}
ScriptAction {
script: {
root.previousIndex = root.selectedTab;
}
}
}
}

View file

@ -2,7 +2,6 @@ import qs.modules.common
import qs.modules.common.widgets
import qs.services
import qs.modules.ii.sidebarRight.notifications
import qs.modules.ii.sidebarRight.lens
import QtQuick
import QtQuick.Layouts
@ -12,19 +11,8 @@ Rectangle {
radius: Appearance.rounding.normal
color: Appearance.colors.colLayer1
ColumnLayout {
NotificationList {
anchors.fill: parent
anchors.margins: 5
spacing: 5
LensWidget {
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
}
NotificationList {
Layout.fillWidth: true
Layout.fillHeight: true
}
}
}

View file

@ -0,0 +1,34 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
RippleButton {
id: button
property string day
property int isToday
property bool bold
Layout.fillWidth: false
Layout.fillHeight: false
implicitWidth: 38;
implicitHeight: 38;
toggled: (isToday == 1)
buttonRadius: Appearance.rounding.small
contentItem: StyledText {
anchors.fill: parent
text: day
horizontalAlignment: Text.AlignHCenter
font.weight: bold ? Font.DemiBold : Font.Normal
color: (isToday == 1) ? Appearance.m3colors.m3onPrimary :
(isToday == 0) ? Appearance.colors.colOnLayer1 :
Appearance.colors.colOutlineVariant
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
}
}

View file

@ -0,0 +1,36 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
RippleButton {
id: button
property string buttonText: ""
property string tooltipText: ""
property bool forceCircle: false
implicitHeight: 30
implicitWidth: forceCircle ? implicitHeight : (contentItem.implicitWidth + 10 * 2)
Behavior on implicitWidth {
SmoothedAnimation {
velocity: Appearance.animation.elementMove.velocity
}
}
background.anchors.fill: button
buttonRadius: Appearance.rounding.full
colBackground: Appearance.colors.colLayer2
colBackgroundHover: Appearance.colors.colLayer2Hover
colRipple: Appearance.colors.colLayer2Active
contentItem: StyledText {
text: buttonText
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colOnLayer1
}
StyledToolTip {
text: tooltipText
extraVisibleCondition: tooltipText.length > 0
}
}

View file

@ -0,0 +1,122 @@
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import "calendar_layout.js" as CalendarLayout
import QtQuick
import QtQuick.Layouts
Item {
// Layout.topMargin: 10
anchors.topMargin: 10
property int monthShift: 0
property var viewingDate: CalendarLayout.getDateInXMonthsTime(monthShift)
property var calendarLayout: CalendarLayout.getCalendarLayout(viewingDate, monthShift === 0)
width: calendarColumn.width
implicitHeight: calendarColumn.height + 10 * 2
Keys.onPressed: (event) => {
if ((event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp)
&& event.modifiers === Qt.NoModifier) {
if (event.key === Qt.Key_PageDown) {
monthShift++;
} else if (event.key === Qt.Key_PageUp) {
monthShift--;
}
event.accepted = true;
}
}
MouseArea {
anchors.fill: parent
onWheel: (event) => {
if (event.angleDelta.y > 0) {
monthShift--;
} else if (event.angleDelta.y < 0) {
monthShift++;
}
}
}
ColumnLayout {
id: calendarColumn
anchors.centerIn: parent
spacing: 5
// Calendar header
RowLayout {
Layout.fillWidth: true
spacing: 5
CalendarHeaderButton {
clip: true
buttonText: `${monthShift != 0 ? "• " : ""}${viewingDate.toLocaleDateString(Qt.locale(), "MMMM yyyy")}`
tooltipText: (monthShift === 0) ? "" : Translation.tr("Jump to current month")
downAction: () => {
monthShift = 0;
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: false
}
CalendarHeaderButton {
forceCircle: true
downAction: () => {
monthShift--;
}
contentItem: MaterialSymbol {
text: "chevron_left"
iconSize: Appearance.font.pixelSize.larger
horizontalAlignment: Text.AlignHCenter
color: Appearance.colors.colOnLayer1
}
}
CalendarHeaderButton {
forceCircle: true
downAction: () => {
monthShift++;
}
contentItem: MaterialSymbol {
text: "chevron_right"
iconSize: Appearance.font.pixelSize.larger
horizontalAlignment: Text.AlignHCenter
color: Appearance.colors.colOnLayer1
}
}
}
// Week days row
RowLayout {
id: weekDaysRow
Layout.alignment: Qt.AlignHCenter
Layout.fillHeight: false
spacing: 5
Repeater {
model: CalendarLayout.weekDays
delegate: CalendarDayButton {
day: Translation.tr(modelData.day)
isToday: modelData.today
bold: true
enabled: false
}
}
}
// Real week rows
Repeater {
id: calendarRows
// model: calendarLayout
model: 6
delegate: RowLayout {
Layout.alignment: Qt.AlignHCenter
Layout.fillHeight: false
spacing: 5
Repeater {
model: Array(7).fill(modelData)
delegate: CalendarDayButton {
day: calendarLayout[modelData][index].day
isToday: calendarLayout[modelData][index].today
}
}
}
}
}
}

View file

@ -0,0 +1,113 @@
const weekDays = [ // MONDAY IS THE FIRST DAY OF THE WEEK :HESRIGHTYOUKNOW:
{ day: 'Mo', today: 0 },
{ day: 'Tu', today: 0 },
{ day: 'We', today: 0 },
{ day: 'Th', today: 0 },
{ day: 'Fr', today: 0 },
{ day: 'Sa', today: 0 },
{ day: 'Su', today: 0 },
]
function checkLeapYear(year) {
return (
year % 400 == 0 ||
(year % 4 == 0 && year % 100 != 0));
}
function getMonthDays(month, year) {
const leapYear = checkLeapYear(year);
if ((month <= 7 && month % 2 == 1) || (month >= 8 && month % 2 == 0)) return 31;
if (month == 2 && leapYear) return 29;
if (month == 2 && !leapYear) return 28;
return 30;
}
function getNextMonthDays(month, year) {
const leapYear = checkLeapYear(year);
if (month == 1 && leapYear) return 29;
if (month == 1 && !leapYear) return 28;
if (month == 12) return 31;
if ((month <= 7 && month % 2 == 1) || (month >= 8 && month % 2 == 0)) return 30;
return 31;
}
function getPrevMonthDays(month, year) {
const leapYear = checkLeapYear(year);
if (month == 3 && leapYear) return 29;
if (month == 3 && !leapYear) return 28;
if (month == 1) return 31;
if ((month <= 7 && month % 2 == 1) || (month >= 8 && month % 2 == 0)) return 30;
return 31;
}
function getDateInXMonthsTime(x) {
var currentDate = new Date(); // Get the current date
if (x == 0) return currentDate; // If x is 0, return the current date
var targetMonth = currentDate.getMonth() + x; // Calculate the target month
var targetYear = currentDate.getFullYear(); // Get the current year
// Adjust the year and month if necessary
targetYear += Math.floor(targetMonth / 12);
targetMonth = (targetMonth % 12 + 12) % 12;
// Create a new date object with the target year and month
var targetDate = new Date(targetYear, targetMonth, 1);
// Set the day to the last day of the month to get the desired date
// targetDate.setDate(0);
return targetDate;
}
function getCalendarLayout(dateObject, highlight) {
if (!dateObject) dateObject = new Date();
const weekday = (dateObject.getDay() + 6) % 7; // MONDAY IS THE FIRST DAY OF THE WEEK
const day = dateObject.getDate();
const month = dateObject.getMonth() + 1;
const year = dateObject.getFullYear();
const weekdayOfMonthFirst = (weekday + 35 - (day - 1)) % 7;
const daysInMonth = getMonthDays(month, year);
const daysInNextMonth = getNextMonthDays(month, year);
const daysInPrevMonth = getPrevMonthDays(month, year);
// Fill
var monthDiff = (weekdayOfMonthFirst == 0 ? 0 : -1);
var toFill, dim;
if (weekdayOfMonthFirst == 0) {
toFill = 1;
dim = daysInMonth;
}
else {
toFill = (daysInPrevMonth - (weekdayOfMonthFirst - 1));
dim = daysInPrevMonth;
}
var calendar = [...Array(6)].map(() => Array(7));
var i = 0, j = 0;
while (i < 6 && j < 7) {
calendar[i][j] = {
"day": toFill,
"today": ((toFill == day && monthDiff == 0 && highlight) ? 1 : (
monthDiff == 0 ? 0 : -1
))
};
// Increment
toFill++;
if (toFill > dim) { // Next month?
monthDiff++;
if (monthDiff == 0)
dim = daysInMonth;
else if (monthDiff == 1)
dim = daysInNextMonth;
toFill = 1;
}
// Next tile
j++;
if (j == 7) {
j = 0;
i++;
}
}
return calendar;
}

View file

@ -0,0 +1,3 @@
CalendarDayButton 1.0 CalendarDayButton.qml
CalendarHeaderButton 1.0 CalendarHeaderButton.qml
CalendarWidget 1.0 CalendarWidget.qml

View file

@ -0,0 +1,71 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Item {
id: root
NotificationListView { // Scrollable window
id: listview
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: statusRow.top
anchors.bottomMargin: 5
clip: true
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: listview.width
height: listview.height
radius: Appearance.rounding.normal
}
}
popup: false
}
// Placeholder when list is empty
PagePlaceholder {
shown: Notifications.list.length === 0
icon: "notifications_active"
description: Translation.tr("Nothing")
shape: MaterialShape.Shape.Ghostish
descriptionHorizontalAlignment: Text.AlignHCenter
}
ButtonGroup {
id: statusRow
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
NotificationStatusButton {
Layout.fillWidth: false
buttonIcon: "notifications_paused"
toggled: Notifications.silent
onClicked: () => {
Notifications.silent = !Notifications.silent;
}
}
NotificationStatusButton {
enabled: false
Layout.fillWidth: true
buttonText: Translation.tr("%1 notifications").arg(Notifications.list.length)
}
NotificationStatusButton {
Layout.fillWidth: false
buttonIcon: "delete_sweep"
onClicked: () => {
Notifications.discardAllNotifications()
}
}
}
}

View file

@ -0,0 +1,46 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
GroupButton {
id: button
property string buttonIcon: ""
property string buttonText: ""
baseHeight: 36
baseWidth: content.implicitWidth + 46
clickedWidth: baseWidth + 6
buttonRadius: baseHeight / 2
buttonRadiusPressed: Appearance.rounding.small
colBackground: Appearance.colors.colLayer2
colBackgroundHover: Appearance.colors.colLayer2Hover
colBackgroundActive: Appearance.colors.colLayer2Active
property color colText: toggled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer1
contentItem: Item {
id: content
anchors.fill: parent
implicitWidth: contentRowLayout.implicitWidth
implicitHeight: contentRowLayout.implicitHeight
RowLayout {
id: contentRowLayout
anchors.centerIn: parent
spacing: 5
MaterialSymbol {
visible: buttonIcon !== ""
text: buttonIcon
iconSize: Appearance.font.pixelSize.huge
color: button.colText
}
StyledText {
visible: buttonText !== ""
text: buttonText
font.pixelSize: Appearance.font.pixelSize.small
color: button.colText
}
}
}
}

View file

@ -0,0 +1,2 @@
NotificationList 1.0 NotificationList.qml
NotificationStatusButton 1.0 NotificationStatusButton.qml

View file

@ -0,0 +1,368 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
StyledFlickable {
id: root
clip: true
contentWidth: width
contentHeight: settingsColumn.implicitHeight
function setMinutes(optionName, minutes) {
Config.options.time.pomodoro[optionName] = Math.max(1, minutes) * 60;
if (!TimerService.pomodoroRunning) {
TimerService.resetPomodoro();
}
}
function minutes(optionName) {
return Math.round(Config.options.time.pomodoro[optionName] / 60);
}
function applyPreset(focus, shortBreak, longBreak, cycles) {
Config.options.time.pomodoro.focus = focus * 60;
Config.options.time.pomodoro.breakTime = shortBreak * 60;
Config.options.time.pomodoro.longBreak = longBreak * 60;
Config.options.time.pomodoro.cyclesBeforeLongBreak = cycles;
if (!TimerService.pomodoroRunning) {
TimerService.resetPomodoro();
}
}
function setCycles(cycles) {
Config.options.time.pomodoro.cyclesBeforeLongBreak = cycles;
if (!TimerService.pomodoroRunning) {
TimerService.resetPomodoro();
}
}
ColumnLayout {
id: settingsColumn
width: root.width
spacing: 10
Rectangle {
Layout.fillWidth: true
Layout.leftMargin: 4
Layout.rightMargin: 12
implicitHeight: 108
radius: Appearance.rounding.normal
color: Appearance.colors.colLayer2
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 8
RowLayout {
Layout.fillWidth: true
spacing: 10
Rectangle {
Layout.alignment: Qt.AlignVCenter
implicitWidth: 42
implicitHeight: 42
radius: Appearance.rounding.full
color: Appearance.colors.colSecondaryContainer
MaterialSymbol {
anchors.centerIn: parent
text: "search_activity"
iconSize: Appearance.font.pixelSize.hugeass
color: Appearance.colors.colOnSecondaryContainer
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 1
StyledText {
text: Translation.tr("Focus profile")
font.pixelSize: Appearance.font.pixelSize.normal
font.weight: Font.Medium
color: Appearance.colors.colOnLayer2
}
StyledText {
text: `${root.minutes("focus")} / ${root.minutes("breakTime")} / ${root.minutes("longBreak")} min ${Config.options.time.pomodoro.cyclesBeforeLongBreak} cycles`
font.pixelSize: Appearance.font.pixelSize.smaller
color: Appearance.colors.colSubtext
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: 8
ProfileChip {
iconName: "search_activity"
label: `${root.minutes("focus")}m`
color: Appearance.colors.colSecondaryContainer
textColor: Appearance.colors.colOnSecondaryContainer
}
ProfileChip {
iconName: "coffee"
label: `${root.minutes("breakTime")}m`
color: Appearance.colors.colTertiaryContainer
textColor: Appearance.colors.colOnTertiaryContainer
}
ProfileChip {
iconName: "spa"
label: `${root.minutes("longBreak")}m`
color: Appearance.colors.colLayer1
textColor: Appearance.colors.colOnLayer1
}
Item { Layout.fillWidth: true }
RowLayout {
spacing: 3
Repeater {
model: Config.options.time.pomodoro.cyclesBeforeLongBreak
Rectangle {
implicitWidth: 7
implicitHeight: 7
radius: Appearance.rounding.full
color: Appearance.colors.colOnLayer2
opacity: 0.45
}
}
}
}
}
}
ContentSection {
icon: "timer"
title: Translation.tr("Pomodoro")
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: 8
Layout.rightMargin: 8
spacing: 6
uniformCellSizes: true
PresetButton {
label: "25/5"
onClicked: root.applyPreset(25, 5, 15, 4)
}
PresetButton {
label: "50/10"
onClicked: root.applyPreset(50, 10, 25, 4)
}
PresetButton {
label: "15/5"
onClicked: root.applyPreset(15, 5, 15, 4)
}
}
PomodoroSpinRow {
iconName: "search_activity"
label: Translation.tr("Focus")
suffix: Translation.tr("min")
from: 1
to: 180
stepSize: 5
value: root.minutes("focus")
onValueModified: value => root.setMinutes("focus", value)
}
PomodoroSpinRow {
iconName: "coffee"
label: Translation.tr("Break")
suffix: Translation.tr("min")
from: 1
to: 60
stepSize: 1
value: root.minutes("breakTime")
onValueModified: value => root.setMinutes("breakTime", value)
}
PomodoroSpinRow {
iconName: "spa"
label: Translation.tr("Long break")
suffix: Translation.tr("min")
from: 1
to: 120
stepSize: 5
value: root.minutes("longBreak")
onValueModified: value => root.setMinutes("longBreak", value)
}
PomodoroSpinRow {
iconName: "repeat"
label: Translation.tr("Cycles")
suffix: ""
from: 1
to: 12
stepSize: 1
value: Config.options.time.pomodoro.cyclesBeforeLongBreak
onValueModified: value => root.setCycles(value)
}
}
ContentSection {
icon: "notifications"
title: Translation.tr("Alerts")
ConfigRow {
uniform: true
ConfigSwitch {
buttonIcon: "notifications"
text: Translation.tr("Notifications")
checked: Config.options.time.pomodoro.notifications
onCheckedChanged: Config.options.time.pomodoro.notifications = checked
}
ConfigSwitch {
buttonIcon: "notification_sound"
text: Translation.tr("Sound")
checked: Config.options.sounds.pomodoro
onCheckedChanged: Config.options.sounds.pomodoro = checked
}
}
RippleButton {
Layout.fillWidth: true
implicitHeight: 40
buttonRadius: Appearance.rounding.full
colBackground: Appearance.colors.colLayer2
colBackgroundHover: Appearance.colors.colLayer2Hover
colRipple: Appearance.colors.colLayer2Active
onClicked: Audio.playSystemSound("alarm-clock-elapsed")
contentItem: RowLayout {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
spacing: 8
MaterialSymbol {
text: "play_circle"
iconSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colOnLayer2
}
StyledText {
Layout.fillWidth: true
text: Translation.tr("Test sound")
color: Appearance.colors.colOnLayer2
}
}
}
}
Item {
Layout.fillWidth: true
implicitHeight: 4
}
}
component PresetButton: RippleButton {
id: preset
property string label
Layout.fillWidth: true
implicitHeight: 34
buttonRadius: Appearance.rounding.full
colBackground: Appearance.colors.colLayer2
colBackgroundHover: Appearance.colors.colLayer2Hover
colRipple: Appearance.colors.colLayer2Active
contentItem: StyledText {
anchors.centerIn: parent
text: preset.label
horizontalAlignment: Text.AlignHCenter
font.family: Appearance.font.family.monospace
font.weight: Font.DemiBold
color: Appearance.colors.colOnLayer2
}
}
component ProfileChip: Rectangle {
property string iconName
property string label
property color textColor
Layout.fillWidth: true
implicitHeight: 28
radius: Appearance.rounding.full
RowLayout {
anchors.centerIn: parent
spacing: 4
MaterialSymbol {
text: parent.parent.iconName
iconSize: 14
color: parent.parent.textColor
}
StyledText {
text: parent.parent.label
font.family: Appearance.font.family.monospace
font.pixelSize: Appearance.font.pixelSize.smaller
font.weight: Font.DemiBold
color: parent.parent.textColor
}
}
}
component PomodoroSpinRow: RowLayout {
id: row
property string iconName
property string label
property string suffix
property alias value: spinBox.value
property alias from: spinBox.from
property alias to: spinBox.to
property alias stepSize: spinBox.stepSize
signal valueModified(int value)
Layout.fillWidth: true
Layout.leftMargin: 8
Layout.rightMargin: 8
spacing: 10
MaterialSymbol {
text: row.iconName
iconSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colOnSecondaryContainer
}
StyledText {
Layout.fillWidth: true
text: row.label
color: Appearance.colors.colOnSecondaryContainer
}
StyledSpinBox {
id: spinBox
Layout.preferredWidth: 96
stepSize: 1
onValueModified: row.valueModified(value)
}
StyledText {
Layout.preferredWidth: 24
text: row.suffix
color: Appearance.colors.colSubtext
}
}
}

View file

@ -0,0 +1,187 @@
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
readonly property color stateColor: TimerService.pomodoroBreak ? Appearance.colors.colTertiaryContainer : Appearance.colors.colSecondaryContainer
readonly property color stateTextColor: TimerService.pomodoroBreak ? Appearance.colors.colOnTertiaryContainer : Appearance.colors.colOnSecondaryContainer
readonly property string stateLabel: TimerService.pomodoroLongBreak ? Translation.tr("Long break") : TimerService.pomodoroBreak ? Translation.tr("Break") : Translation.tr("Focus")
readonly property int cyclePosition: TimerService.pomodoroCycle + 1
ColumnLayout {
id: contentColumn
anchors.fill: parent
spacing: 10
// The Pomodoro timer circle
CircularProgress {
Layout.alignment: Qt.AlignHCenter
lineWidth: 10
value: {
return TimerService.pomodoroSecondsLeft / TimerService.pomodoroLapDuration;
}
implicitSize: 200
colPrimary: root.stateTextColor
colSecondary: root.stateColor
enableAnimation: true
ColumnLayout {
anchors.centerIn: parent
spacing: 2
StyledText {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: 150
horizontalAlignment: Text.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.family: Appearance.font.family.monospace
font.pixelSize: 40
font.weight: Font.DemiBold
color: Appearance.m3colors.m3onSurface
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: root.stateLabel
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colSubtext
}
}
Rectangle {
radius: Appearance.rounding.full
color: root.stateColor
anchors {
right: parent.right
bottom: parent.bottom
}
implicitWidth: 58
implicitHeight: 36
RowLayout {
anchors.centerIn: parent
spacing: 3
MaterialSymbol {
text: "repeat"
iconSize: 14
color: root.stateTextColor
}
StyledText {
id: cycleText
font.family: Appearance.font.family.monospace
font.weight: Font.DemiBold
color: root.stateTextColor
text: `${root.cyclePosition}/${TimerService.cyclesBeforeLongBreak}`
}
}
}
}
Rectangle {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: 210
Layout.preferredHeight: 30
radius: Appearance.rounding.full
color: Appearance.colors.colLayer2
RowLayout {
anchors.centerIn: parent
spacing: 6
MaterialSymbol {
text: TimerService.pomodoroBreak ? "coffee" : "search_activity"
iconSize: Appearance.font.pixelSize.larger
color: root.stateTextColor
}
StyledText {
text: root.stateLabel
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnLayer2
}
}
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 5
Repeater {
model: TimerService.cyclesBeforeLongBreak
Rectangle {
required property int index
implicitWidth: index === TimerService.pomodoroCycle ? 18 : 8
implicitHeight: 8
radius: Appearance.rounding.full
color: index <= TimerService.pomodoroCycle ? root.stateTextColor : Appearance.colors.colLayer2
Behavior on implicitWidth {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
}
}
}
// The Start/Stop and Reset buttons
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 10
RippleButton {
buttonRadius: Appearance.rounding.full
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: 38
implicitWidth: 96
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 {
buttonRadius: Appearance.rounding.full
implicitHeight: 38
implicitWidth: 96
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
}
}
}
}
}

View file

@ -0,0 +1,68 @@
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Item {
id: root
property var tabButtonList: [
{"name": Translation.tr("Timer"), "icon": "search_activity"},
{"name": Translation.tr("Customize"), "icon": "tune"}
]
// Pomodoro keybinds
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) {
tabBar.incrementCurrentIndex();
} else if (event.key === Qt.Key_PageUp) {
tabBar.decrementCurrentIndex();
}
event.accepted = true
} else if (event.key === Qt.Key_Space || event.key === Qt.Key_S) { // Pause/resume with Space or S
if (tabBar.currentIndex === 0) {
TimerService.togglePomodoro()
}
event.accepted = true
} else if (event.key === Qt.Key_R) { // Reset with R
if (tabBar.currentIndex === 0) {
TimerService.resetPomodoro()
}
event.accepted = true
}
}
ColumnLayout {
anchors.fill: parent
spacing: 0
SecondaryTabBar {
id: tabBar
currentIndex: swipeView.currentIndex
Repeater {
model: root.tabButtonList
delegate: SecondaryTabButton {
buttonText: modelData.name
buttonIcon: modelData.icon
}
}
}
SwipeView {
id: swipeView
Layout.topMargin: 10
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 10
clip: true
currentIndex: tabBar.currentIndex
// Tabs
PomodoroTimer {}
PomodoroSettings {}
}
}
}

View file

@ -0,0 +1,207 @@
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
}
}
}
}
}

View file

@ -0,0 +1,63 @@
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
Item {
id: root
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Space || event.key === Qt.Key_S) {
TimerService.toggleStopwatch();
event.accepted = true;
} else if (event.key === Qt.Key_R) {
TimerService.stopwatchReset();
event.accepted = true;
} else if (event.key === Qt.Key_L) {
TimerService.stopwatchRecordLap();
event.accepted = true;
}
}
ColumnLayout {
anchors.fill: parent
spacing: 8
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: 4
Layout.rightMargin: 14
spacing: 8
MaterialSymbol {
text: "timer"
iconSize: Appearance.font.pixelSize.hugeass
color: Appearance.colors.colOnSecondaryContainer
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
StyledText {
text: Translation.tr("Stopwatch")
font.pixelSize: Appearance.font.pixelSize.larger
font.weight: Font.Medium
color: Appearance.colors.colOnLayer1
}
StyledText {
text: TimerService.stopwatchRunning ? Translation.tr("Running") : Translation.tr("Ready")
font.pixelSize: Appearance.font.pixelSize.smaller
color: Appearance.colors.colSubtext
}
}
}
Stopwatch {
Layout.fillWidth: true
Layout.fillHeight: true
}
}
}

View file

@ -0,0 +1,5 @@
PomodoroSettings 1.0 PomodoroSettings.qml
PomodoroTimer 1.0 PomodoroTimer.qml
PomodoroWidget 1.0 PomodoroWidget.qml
Stopwatch 1.0 Stopwatch.qml
StopwatchWidget 1.0 StopwatchWidget.qml

View file

@ -0,0 +1,141 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
Item {
id: root
required property var taskList
property string emptyPlaceholderIcon
property string emptyPlaceholderText
property int todoListItemSpacing: 5
property int todoListItemPadding: 8
property int listBottomPadding: 80
StyledListView {
id: listView
anchors.fill: parent
spacing: root.todoListItemSpacing
animateAppearance: false
model: ScriptModel {
values: root.taskList
}
delegate: Item {
id: todoItem
required property var modelData
property bool pendingDoneToggle: false
property bool pendingDelete: false
property bool enableHeightAnimation: false
implicitHeight: todoItemRectangle.implicitHeight
width: ListView.view.width
clip: true
Behavior on implicitHeight {
enabled: enableHeightAnimation
NumberAnimation {
duration: Appearance.animation.elementMoveFast.duration
easing.type: Appearance.animation.elementMoveFast.type
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
}
Rectangle {
id: todoItemRectangle
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
implicitHeight: todoContentRowLayout.implicitHeight
color: Appearance.colors.colLayer2
radius: Appearance.rounding.small
ColumnLayout {
id: todoContentRowLayout
anchors.left: parent.left
anchors.right: parent.right
StyledText {
id: todoContentText
Layout.fillWidth: true // Needed for wrapping
Layout.leftMargin: 10
Layout.rightMargin: 10
Layout.topMargin: todoListItemPadding
text: todoItem.modelData.content
wrapMode: Text.Wrap
}
RowLayout {
Layout.leftMargin: 10
Layout.rightMargin: 10
Layout.bottomMargin: todoListItemPadding
Item {
Layout.fillWidth: true
}
TodoItemActionButton {
Layout.fillWidth: false
onClicked: {
if (!todoItem.modelData.done)
Todo.markDone(todoItem.modelData.originalIndex);
else
Todo.markUnfinished(todoItem.modelData.originalIndex);
}
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: todoItem.modelData.done ? "remove_done" : "check"
iconSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colOnLayer1
}
}
TodoItemActionButton {
Layout.fillWidth: false
onClicked: {
Todo.deleteItem(todoItem.modelData.originalIndex);
}
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: "delete_forever"
iconSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colOnLayer1
}
}
}
}
}
}
}
Item {
// Placeholder when list is empty
visible: opacity > 0
opacity: taskList.length === 0 ? 1 : 0
anchors.fill: parent
Behavior on opacity {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
ColumnLayout {
anchors.centerIn: parent
spacing: 5
MaterialSymbol {
Layout.alignment: Qt.AlignHCenter
iconSize: 55
color: Appearance.m3colors.m3outline
text: emptyPlaceholderIcon
}
StyledText {
Layout.alignment: Qt.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.m3colors.m3outline
horizontalAlignment: Text.AlignHCenter
text: emptyPlaceholderText
}
}
}
}

View file

@ -0,0 +1,32 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
RippleButton {
id: button
property string buttonText: ""
property string tooltipText: ""
implicitHeight: 30
implicitWidth: implicitHeight
Behavior on implicitWidth {
SmoothedAnimation {
velocity: Appearance.animation.elementMove.velocity
}
}
buttonRadius: Appearance.rounding.small
contentItem: StyledText {
text: buttonText
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colOnLayer1
}
StyledToolTip {
text: tooltipText
extraVisibleCondition: tooltipText.length > 0
}
}

View file

@ -0,0 +1,219 @@
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Item {
id: root
property var tabButtonList: [{"icon": "checklist", "name": Translation.tr("Unfinished")}, {"name": Translation.tr("Done"), "icon": "check_circle"}]
property bool showAddDialog: false
property int dialogMargins: 20
property int fabSize: 48
property int fabMargins: 14
Keys.onPressed: (event) => {
if ((event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp) && event.modifiers === Qt.NoModifier) {
if (event.key === Qt.Key_PageDown) {
tabBar.incrementCurrentIndex();
} else if (event.key === Qt.Key_PageUp) {
tabBar.decrementCurrentIndex();
}
event.accepted = true;
}
// Open add dialog on "N" (any modifiers)
else if (event.key === Qt.Key_N) {
root.showAddDialog = true
event.accepted = true;
}
// Close dialog on Esc if open
else if (event.key === Qt.Key_Escape && root.showAddDialog) {
root.showAddDialog = false
event.accepted = true;
}
}
ColumnLayout {
anchors.fill: parent
spacing: 0
SecondaryTabBar {
id: tabBar
currentIndex: swipeView.currentIndex
Repeater {
model: root.tabButtonList
delegate: SecondaryTabButton {
buttonText: modelData.name
buttonIcon: modelData.icon
}
}
}
SwipeView {
id: swipeView
Layout.topMargin: 10
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 10
clip: true
currentIndex: tabBar.currentIndex
// To Do tab
TaskList {
listBottomPadding: root.fabSize + root.fabMargins * 2
emptyPlaceholderIcon: "check_circle"
emptyPlaceholderText: Translation.tr("Nothing here!")
taskList: Todo.list
.map(function(item, i) { return Object.assign({}, item, {originalIndex: i}); })
.filter(function(item) { return !item.done; })
}
TaskList {
listBottomPadding: root.fabSize + root.fabMargins * 2
emptyPlaceholderIcon: "checklist"
emptyPlaceholderText: Translation.tr("Finished tasks will go here")
taskList: Todo.list
.map(function(item, i) { return Object.assign({}, item, {originalIndex: i}); })
.filter(function(item) { return item.done; })
}
}
}
// + 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: root.showAddDialog = true
iconText: "add"
}
Item {
anchors.fill: parent
z: 9999
visible: opacity > 0
opacity: root.showAddDialog ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Appearance.animation.elementMoveFast.duration
easing.type: Appearance.animation.elementMoveFast.type
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
}
onVisibleChanged: {
if (!visible) {
todoInput.text = ""
fabButton.focus = true
}
}
Rectangle { // Scrim
anchors.fill: parent
radius: Appearance.rounding.small
color: Appearance.colors.colScrim
MouseArea {
hoverEnabled: true
anchors.fill: parent
preventStealing: true
propagateComposedEvents: false
}
}
Rectangle { // The dialog
id: dialog
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: root.dialogMargins
implicitHeight: dialogColumnLayout.implicitHeight
color: Appearance.m3colors.m3surfaceContainerHigh
radius: Appearance.rounding.normal
function addTask() {
if (todoInput.text.length > 0) {
Todo.addTask(todoInput.text)
todoInput.text = ""
root.showAddDialog = false
tabBar.setCurrentIndex(0) // Show unfinished tasks
}
}
ColumnLayout {
id: dialogColumnLayout
anchors.fill: parent
spacing: 16
StyledText {
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.alignment: Qt.AlignLeft
color: Appearance.m3colors.m3onSurface
font.pixelSize: Appearance.font.pixelSize.larger
text: Translation.tr("Add task")
}
TextField {
id: todoInput
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
padding: 10
color: activeFocus ? Appearance.m3colors.m3onSurface : Appearance.m3colors.m3onSurfaceVariant
renderType: Text.NativeRendering
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
selectionColor: Appearance.colors.colSecondaryContainer
placeholderText: Translation.tr("Task description")
placeholderTextColor: Appearance.m3colors.m3outline
focus: root.showAddDialog
onAccepted: dialog.addTask()
background: Rectangle {
anchors.fill: parent
radius: Appearance.rounding.verysmall
border.width: 2
border.color: todoInput.activeFocus ? Appearance.colors.colPrimary : Appearance.m3colors.m3outline
color: "transparent"
}
cursorDelegate: Rectangle {
width: 1
color: todoInput.activeFocus ? Appearance.colors.colPrimary : "transparent"
radius: 1
}
}
RowLayout {
Layout.bottomMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.alignment: Qt.AlignRight
spacing: 5
DialogButton {
buttonText: Translation.tr("Cancel")
onClicked: root.showAddDialog = false
}
DialogButton {
buttonText: Translation.tr("Add")
enabled: todoInput.text.length > 0
onClicked: dialog.addTask()
}
}
}
}
}
}

View file

@ -0,0 +1,3 @@
TaskList 1.0 TaskList.qml
TodoItemActionButton 1.0 TodoItemActionButton.qml
TodoWidget 1.0 TodoWidget.qml

View file

@ -126,6 +126,15 @@ Singleton {
} catch (e) {
return;
}
if (msg.event === "note_created") {
const rel = msg.rel ?? "";
const title = rel.replace(/\.md$/, "").split("/").pop();
Quickshell.execDetached(["notify-send", "-a", "Garden", "Note planted", title]);
}
else if (msg.event === "synced") {
Quickshell.execDetached(["notify-send", "-a", "Garden", "Garden synced"]);
}
if (msg.event === "state" && msg.state) {
const st = msg.state.status ?? {};
root.notes = msg.state.notes ?? [];