todo widget: add ability to add items

This commit is contained in:
end-4
2025-04-17 16:27:03 +02:00
parent 1bb4bf8372
commit 1d9b543f57
8 changed files with 340 additions and 26 deletions
@@ -120,9 +120,11 @@ Singleton {
property color colLayer2Active: mix(colLayer2, colOnLayer2, 0.80);
property color colLayer3Hover: mix(colLayer3, colOnLayer3, 0.90);
property color colLayer3Active: mix(colLayer3, colOnLayer3, 0.80);
property color colPrimaryHover: mix(m3colors.m3primary, colLayer1Hover, 0.7)
property color colPrimaryActive: mix(m3colors.m3primary, colLayer1Active, 0.4)
property color colSecondaryHover: mix(m3colors.m3secondary, colLayer1Hover, 0.7)
property color colPrimaryHover: mix(m3colors.m3primary, colLayer1Hover, 0.85)
property color colPrimaryActive: mix(m3colors.m3primary, colLayer1Active, 0.7)
property color colPrimaryContainerHover: mix(m3colors.m3primaryContainer, colLayer1Hover, 0.7)
property color colPrimaryContainerActive: mix(m3colors.m3primaryContainer, colLayer1Active, 0.6)
property color colSecondaryHover: mix(m3colors.m3secondary, colLayer1Hover, 0.85)
property color colSecondaryActive: mix(m3colors.m3secondary, colLayer1Active, 0.4)
property color colSecondaryContainerHover: mix(m3colors.m3secondaryContainer, colLayer1Hover, 0.8)
property color colSecondaryContainerActive: mix(m3colors.m3secondaryContainer, colLayer1Active, 0.6)
@@ -144,7 +146,7 @@ Singleton {
property QtObject family: QtObject {
property string main: "Rubik"
property string title: "Rubik"
property string iconMaterial: "Material Symbols Rounded"
property string iconMaterial: "Material Symbols Outlined"
property string iconNerd: "SpaceMono NF"
property string monospace: "JetBrains Mono NF"
property string reading: "Readex Pro"
@@ -189,6 +191,8 @@ Singleton {
property int sidebarWidth: 450
property int hyprlandGapsOut: 5
property int elevationMargin: 7
property int fabShadowRadius: 5
property int fabHoveredShadowRadius: 7
}
}
@@ -0,0 +1,48 @@
import "root:/modules/common"
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
Button {
id: button
property string buttonText
implicitHeight: 30
implicitWidth: buttonTextWidget.implicitWidth + 15 * 2
background: Rectangle {
anchors.fill: parent
radius: Appearance.rounding.full
color: (button.down && button.enabled) ? Appearance.colors.colLayer1Active : ((button.hovered && button.enabled) ? Appearance.colors.colLayer1Hover : Appearance.transparentize(Appearance.m3colors.m3surfaceContainerHigh, 1))
Behavior on color {
ColorAnimation {
duration: Appearance.animation.elementDecel.duration
easing.type: Appearance.animation.elementDecel.type
}
}
}
contentItem: StyledText {
id: buttonTextWidget
anchors.fill: parent
anchors.leftMargin: 15
anchors.rightMargin: 15
text: buttonText
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.normal
color: button.enabled ? Appearance.m3colors.m3primary : Appearance.m3colors.m3outline
Behavior on color {
ColorAnimation {
duration: Appearance.animation.elementDecel.duration
easing.type: Appearance.animation.elementDecel.type
}
}
}
}
@@ -21,21 +21,16 @@ Rectangle {
{"type": "calendar", "name": "Calendar", "icon": "calendar_month", "widget": calendarWidget},
{"type": "todo", "name": "To Do", "icon": "done_outline", "widget": todoWidget}
]
// Calendar
Component {
id: calendarWidget
CalendarWidget {
anchors.centerIn: parent
}
}
// To Do
Component {
id: todoWidget
TodoWidget {
anchors.fill: parent
anchors.margins: 5
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;
}
}
@@ -72,7 +67,7 @@ Rectangle {
height: tabStack.children[0]?.tabLoader?.implicitHeight // TODO: make this less stupid
Layout.alignment: Qt.AlignVCenter
property int realIndex: 0
property int animationDuration: Appearance.animation.elementDecel.duration * 1.5
property int animationDuration: Appearance.animation.elementDecelFast.duration * 1.5
// Switch the tab on halfway of the anim duration
Connections {
@@ -109,9 +104,28 @@ Rectangle {
id: tabLoader
anchors.fill: parent
sourceComponent: modelData.widget
focus: root.selectedTab === tabItem.tabIndex
}
}
}
}
}
// Calendar component
Component {
id: calendarWidget
CalendarWidget {
anchors.centerIn: parent
}
}
// To Do component
Component {
id: todoWidget
TodoWidget {
anchors.fill: parent
anchors.margins: 5
}
}
}
@@ -76,7 +76,7 @@ Scope {
radius: Appearance.rounding.screenRounding - Appearance.sizes.elevationMargin + 1
focus: true
Keys.onPressed: {
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Escape) {
sidebarRoot.visible = false;
event.accepted = true; // Prevent further propagation of the event
@@ -14,9 +14,20 @@ Item {
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: {
onWheel: (event) => {
if (wheel.angleDelta.y > 0) {
monthShift--;
} else if (wheel.angleDelta.y < 0) {
@@ -24,6 +35,7 @@ Item {
}
}
}
ColumnLayout {
id: calendarColumn
anchors.centerIn: parent
@@ -8,10 +8,14 @@ import QtQuick.Layouts
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
Flickable { // Scrolled window
id: flickable
anchors.fill: parent
contentHeight: columnLayout.height
clip: true
@@ -123,6 +127,35 @@ Item {
}
}
}
}
// Bottom padding
Item {
implicitHeight: listBottomPadding
}
}
}
// Placeholder when list is empty
Item {
visible: taskList.length === 0
anchors.fill: parent
ColumnLayout {
anchors.centerIn: parent
spacing: 5
MaterialSymbol {
Layout.alignment: Qt.AlignHCenter
text: emptyPlaceholderIcon
font.pixelSize: 55
color: Appearance.m3colors.m3outline
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: emptyPlaceholderText
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.m3colors.m3outline
horizontalAlignment: Text.AlignHCenter
}
}
}
@@ -4,9 +4,37 @@ import "root:/services"
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
Item {
id: root
property int currentTab: 0
property var tabButtonList: [{"icon": "checklist", "name": "Unfinished"}, {"name": "Done", "icon": "check_circle"}]
property bool showAddDialog: false
property int dialogMargins: 25
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) {
currentTab = Math.min(currentTab + 1, root.tabButtonList.length - 1)
} else if (event.key === Qt.Key_PageUp) {
currentTab = Math.max(currentTab - 1, 0)
}
event.accepted = true;
}
// 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
@@ -14,7 +42,6 @@ Item {
TabBar {
id: tabBar
property var tabButtonList: [{"icon": "checklist", "name": "Unfinished"}, {"name": "Done", "icon": "check_circle"}]
Layout.fillWidth: true
currentIndex: currentTab
onCurrentIndexChanged: currentTab = currentIndex
@@ -23,7 +50,7 @@ Item {
WheelHandler {
onWheel: (event) => {
if (event.angleDelta.y < 0)
currentTab = Math.min(currentTab + 1, tabBar.tabButtonList.length - 1)
currentTab = Math.min(currentTab + 1, root.tabButtonList.length - 1)
else if (event.angleDelta.y > 0)
currentTab = Math.max(currentTab - 1, 0)
}
@@ -32,7 +59,7 @@ Item {
}
Repeater {
model: tabBar.tabButtonList
model: root.tabButtonList
delegate: StyledTabButton {
selected: (index == currentTab)
buttonText: modelData.name
@@ -50,8 +77,8 @@ Item {
height: 3
radius: Appearance.rounding.full
width: tabBar.width / tabBar.tabButtonList.length - indicatorPadding * 2
x: indicatorPadding + tabBar.width / tabBar.tabButtonList.length * currentTab
width: tabBar.width / root.tabButtonList.length - indicatorPadding * 2
x: indicatorPadding + tabBar.width / root.tabButtonList.length * currentTab
z: 2
Behavior on x { SmoothedAnimation {
velocity: Appearance.animation.positionShift.velocity
@@ -71,11 +98,17 @@ Item {
// To Do tab
TaskList {
listBottomPadding: root.fabSize + root.fabMargins * 2
emptyPlaceholderIcon: "check_circle"
emptyPlaceholderText: "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: "Finished tasks will go here"
taskList: Todo.list
.map(function(item, i) { return Object.assign({}, item, {originalIndex: i}); })
.filter(function(item) { return item.done; })
@@ -83,4 +116,164 @@ Item {
}
}
// + FAB
Button {
id: fabButton
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.rightMargin: root.fabMargins
anchors.bottomMargin: root.fabMargins
width: root.fabSize
height: root.fabSize
onClicked: root.showAddDialog = true
background: Rectangle {
id: fabBackground
anchors.fill: parent
radius: Appearance.rounding.small
color: (fabButton.down) ? Appearance.colors.colPrimaryContainerActive : (fabButton.hovered ? Appearance.colors.colPrimaryContainerHover : Appearance.m3colors.m3primaryContainer)
Behavior on color {
ColorAnimation {
duration: Appearance.animation.elementDecel.duration
easing.type: Appearance.animation.elementDecel.type
}
}
}
DropShadow {
id: fabShadow
anchors.fill: fabBackground
source: fabBackground
horizontalOffset: 0
verticalOffset: fabButton.hovered ? 4 : 2
radius: fabButton.hovered ? Appearance.sizes.fabHoveredShadowRadius : Appearance.sizes.fabShadowRadius
samples: fabShadow.radius * 2 + 1
color: Appearance.transparentize(Appearance.m3colors.m3shadow, 0.55)
z: fabBackground.z - 1
Behavior on verticalOffset {
NumberAnimation {
duration: Appearance.animation.elementDecelFast.duration
easing.type: Appearance.animation.elementDecelFast.type
}
}
}
contentItem: MaterialSymbol {
text: "add"
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.huge
color: Appearance.m3colors.m3onPrimaryContainer
}
}
Item {
anchors.fill: parent
visible: root.showAddDialog
z: 1000
onVisibleChanged: {
if (!visible) {
todoInput.text = ""
fabButton.focus = true
}
}
Rectangle { // Scrim
anchors.fill: parent
radius: Appearance.rounding.small
color: "#80000000"
MouseArea {
hoverEnabled: true
anchors.fill: parent
preventStealing: true
propagateComposedEvents: false
}
}
Rectangle { // The dialog
id: dialog
implicitWidth: parent.width - dialogMargins * 2
implicitHeight: dialogColumnLayout.implicitHeight
anchors.centerIn: parent
color: Appearance.m3colors.m3surfaceContainerHigh
radius: Appearance.rounding.normal
function addTask() {
if (todoInput.text.length > 0) {
Todo.addTask(todoInput.text)
todoInput.text = ""
root.showAddDialog = false
root.currentTab = 0 // Show unfinished tasks
}
}
ColumnLayout {
anchors.fill: parent
id: dialogColumnLayout
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: "Add task"
}
TextField {
id: todoInput
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
padding: 10
color: activeFocus ? Appearance.m3colors.m3onSurface : Appearance.m3colors.m3onSurfaceVariant
selectedTextColor: Appearance.m3colors.m3onSurface
placeholderText: "Task description"
focus: root.showAddDialog
onAccepted: dialog.addTask()
background: Rectangle {
anchors.fill: parent
radius: 8
border.width: 2
border.color: todoInput.activeFocus ? Appearance.m3colors.m3primary : Appearance.m3colors.m3outline
color: "transparent"
}
cursorDelegate: Rectangle {
width: 1
color: todoInput.activeFocus ? Appearance.m3colors.m3primary : "transparent"
radius: 1
}
}
RowLayout {
Layout.bottomMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.alignment: Qt.AlignRight
spacing: 5
DialogButton {
buttonText: "Cancel"
onClicked: root.showAddDialog = false
}
DialogButton {
buttonText: "Add"
enabled: todoInput.text.length > 0
onClicked: dialog.addTask()
}
}
}
}
}
}
+10
View File
@@ -13,9 +13,19 @@ Singleton {
function addItem(item) {
list.push(item)
// Reassign to trigger onListChanged
root.list = list.slice(0)
todoFileView.setText(JSON.stringify(root.list))
}
function addTask(desc) {
const item = {
"content": desc,
"done": false,
}
addItem(item)
}
function markDone(index) {
if (index >= 0 && index < list.length) {
list[index].done = true