right sidebar: don't load all bottom group tabs

This commit is contained in:
end-4
2025-11-25 10:44:29 +01:00
parent 9053927480
commit 08739043f6
3 changed files with 137 additions and 201 deletions
@@ -16,9 +16,9 @@ Rectangle {
property int selectedTab: Persistent.states.sidebar.bottomGroup.tab property int selectedTab: Persistent.states.sidebar.bottomGroup.tab
property bool collapsed: Persistent.states.sidebar.bottomGroup.collapsed property bool collapsed: Persistent.states.sidebar.bottomGroup.collapsed
property var tabs: [ property var tabs: [
{"type": "calendar", "name": Translation.tr("Calendar"), "icon": "calendar_month", "widget": calendarWidget}, {"type": "calendar", "name": Translation.tr("Calendar"), "icon": "calendar_month", "widget": "calendar/CalendarWidget.qml"},
{"type": "todo", "name": Translation.tr("To Do"), "icon": "done_outline", "widget": todoWidget}, {"type": "todo", "name": Translation.tr("To Do"), "icon": "done_outline", "widget": "todo/TodoWidget.qml"},
{"type": "timer", "name": Translation.tr("Timer"), "icon": "schedule", "widget": pomodoroWidget}, {"type": "timer", "name": Translation.tr("Timer"), "icon": "schedule", "widget": "pomodoro/PomodoroWidget.qml"},
] ]
Behavior on implicitHeight { Behavior on implicitHeight {
@@ -170,83 +170,56 @@ Rectangle {
} }
// Content area // Content area
StackLayout { Loader {
id: tabStack id: tabStack
Layout.fillWidth: true Layout.fillWidth: true
// Take the highest one, because the TODO list has no implicit height. This way the heigth of the calendar is used when it's initially loaded with the TODO list Layout.fillHeight: true
height: Math.max(...tabStack.children.map(child => child.tabLoader?.implicitHeight || 0)) // TODO: make this less stupid source: root.tabs[root.selectedTab].widget
Layout.alignment: Qt.AlignVCenter
property int realIndex: root.selectedTab
property int animationDuration: Appearance.animation.elementMoveFast.duration * 1.5
currentIndex: root.selectedTab
// Switch the tab on halfway of the anim duration Behavior on source {
Connections { id: switchBehavior
target: root
function onSelectedTabChanged() {
delayedStackSwitch.start()
tabStack.realIndex = root.selectedTab
}
}
Timer {
id: delayedStackSwitch
interval: tabStack.animationDuration / 2
repeat: false
onTriggered: {
tabStack.currentIndex = root.selectedTab
}
}
Repeater { SequentialAnimation {
model: tabs id: switchAnim
Item { // TODO: make behavior on y also act for the item that's switched to ParallelAnimation {
id: tabItem PropertyAnimation {
property int tabIndex: index target: tabStack.item
property string tabType: modelData.type properties: "opacity"
property int animDistance: 5 to: 0
property var tabLoader: tabLoader duration: Appearance.animation.elementMoveFast.duration
// Opacity: show up only when being animated to easing.type: Easing.BezierSpline
opacity: (tabStack.currentIndex === tabItem.tabIndex && tabStack.realIndex === tabItem.tabIndex) ? 1 : 0 easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
// Y: starts animating when user selects a different tab }
y: (tabStack.realIndex === tabItem.tabIndex) ? 0 : (tabStack.realIndex < tabItem.tabIndex) ? animDistance : -animDistance PropertyAnimation {
Behavior on opacity { NumberAnimation { duration: tabStack.animationDuration / 2; easing.type: Easing.OutCubic } } target: tabStack.item
Behavior on y { NumberAnimation { duration: tabStack.animationDuration; easing.type: Easing.OutExpo } } properties: "y"
Loader { from: 0
id: tabLoader to: 20
anchors.fill: parent duration: Appearance.animation.elementMoveFast.duration
sourceComponent: modelData.widget easing.type: Easing.InExpo
focus: root.selectedTab === tabItem.tabIndex }
}
PropertyAction {} // The source change happens here
ParallelAnimation {
PropertyAnimation {
target: tabStack.item
properties: "opacity"
to: 1
duration: Appearance.animation.elementMoveFast.duration
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
PropertyAnimation {
target: tabStack.item
properties: "y"
from: 20
to: 0
duration: Appearance.animation.elementMoveFast.duration
easing.type: Easing.OutExpo
}
} }
} }
} }
} }
} }
// Calendar component
Component {
id: calendarWidget
CalendarWidget {
anchors.fill: parent
anchors.margins: 5
}
}
// To Do component
Component {
id: todoWidget
TodoWidget {
anchors.fill: parent
anchors.margins: 5
}
}
// Pomodoro component
Component {
id: pomodoroWidget
PomodoroWidget {
anchors.fill: parent
anchors.margins: 5
}
}
} }
@@ -9,147 +9,108 @@ import Quickshell
Item { Item {
id: root id: root
required property var taskList; required property var taskList
property string emptyPlaceholderIcon property string emptyPlaceholderIcon
property string emptyPlaceholderText property string emptyPlaceholderText
property int todoListItemSpacing: 5 property int todoListItemSpacing: 5
property int todoListItemPadding: 8 property int todoListItemPadding: 8
property int listBottomPadding: 80 property int listBottomPadding: 80
StyledFlickable { StyledListView {
id: flickable id: listView
anchors.fill: parent anchors.fill: parent
contentHeight: columnLayout.height spacing: root.todoListItemSpacing
animateAppearance: false
clip: true model: ScriptModel {
layer.enabled: true values: root.taskList
layer.effect: OpacityMask {
maskSource: Rectangle {
width: flickable.width
height: flickable.height
radius: Appearance.rounding.small
}
} }
delegate: Item {
id: todoItem
required property var modelData
property bool pendingDoneToggle: false
property bool pendingDelete: false
property bool enableHeightAnimation: false
ColumnLayout { implicitHeight: todoItemRectangle.implicitHeight
id: columnLayout width: ListView.view.width
width: parent.width clip: true
spacing: 0
Repeater { Behavior on implicitHeight {
model: ScriptModel { enabled: enableHeightAnimation
values: taskList NumberAnimation {
duration: Appearance.animation.elementMoveFast.duration
easing.type: Appearance.animation.elementMoveFast.type
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
} }
delegate: Item {
id: todoItem
property bool pendingDoneToggle: false
property bool pendingDelete: false
property bool enableHeightAnimation: false
Layout.fillWidth: true
implicitHeight: todoItemRectangle.implicitHeight + todoListItemSpacing
height: implicitHeight
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
}
}
function startAction() {
enableHeightAnimation = true
todoItem.implicitHeight = 0
actionTimer.start()
}
Timer {
id: actionTimer
interval: Appearance.animation.elementMoveFast.duration
repeat: false
onTriggered: {
if (todoItem.pendingDelete) {
Todo.deleteItem(modelData.originalIndex)
} else if (todoItem.pendingDoneToggle) {
if (!modelData.done) Todo.markDone(modelData.originalIndex)
else Todo.markUnfinished(modelData.originalIndex)
}
}
}
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 {
Layout.fillWidth: true // Needed for wrapping
Layout.leftMargin: 10
Layout.rightMargin: 10
Layout.topMargin: todoListItemPadding
id: todoContentText
text: modelData.content
wrapMode: Text.Wrap
}
RowLayout {
Layout.leftMargin: 10
Layout.rightMargin: 10
Layout.bottomMargin: todoListItemPadding
Item {
Layout.fillWidth: true
}
TodoItemActionButton {
Layout.fillWidth: false
onClicked: {
todoItem.pendingDoneToggle = true
todoItem.startAction()
}
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: modelData.done ? "remove_done" : "check"
iconSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colOnLayer1
}
}
TodoItemActionButton {
Layout.fillWidth: false
onClicked: {
todoItem.pendingDelete = true
todoItem.startAction()
}
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: "delete_forever"
iconSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colOnLayer1
}
}
}
}
}
}
} }
// Bottom padding
Item { Rectangle {
implicitHeight: listBottomPadding 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 Item {
// Placeholder when list is empty
visible: opacity > 0 visible: opacity > 0
opacity: taskList.length === 0 ? 1 : 0 opacity: taskList.length === 0 ? 1 : 0
anchors.fill: parent anchors.fill: parent
@@ -9,6 +9,8 @@ import qs.modules.common
import qs.modules.common.functions import qs.modules.common.functions
import qs.modules.waffle.looks import qs.modules.waffle.looks
// TODO: The overlaps are crazy, but the positioning approach works.
// This could work well if we do it week by week instead of month by month.
BodyRectangle { BodyRectangle {
id: root id: root