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:
parent
7e6633a290
commit
b8e76c4e9e
22 changed files with 2043 additions and 13 deletions
|
|
@ -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
|
||||
"
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
CalendarDayButton 1.0 CalendarDayButton.qml
|
||||
CalendarHeaderButton 1.0 CalendarHeaderButton.qml
|
||||
CalendarWidget 1.0 CalendarWidget.qml
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
NotificationList 1.0 NotificationList.qml
|
||||
NotificationStatusButton 1.0 NotificationStatusButton.qml
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
141
surfaces/quickshell/modules/ii/sidebarRight/todo/TaskList.qml
Normal file
141
surfaces/quickshell/modules/ii/sidebarRight/todo/TaskList.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
219
surfaces/quickshell/modules/ii/sidebarRight/todo/TodoWidget.qml
Normal file
219
surfaces/quickshell/modules/ii/sidebarRight/todo/TodoWidget.qml
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
surfaces/quickshell/modules/ii/sidebarRight/todo/qmldir
Normal file
3
surfaces/quickshell/modules/ii/sidebarRight/todo/qmldir
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
TaskList 1.0 TaskList.qml
|
||||
TodoItemActionButton 1.0 TodoItemActionButton.qml
|
||||
TodoWidget 1.0 TodoWidget.qml
|
||||
|
|
@ -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 ?? [];
|
||||
|
|
|
|||
Loading…
Reference in a new issue