From f9facdd61b546b208ed0e19c00a4b5c8c2febb11 Mon Sep 17 00:00:00 2001 From: Uyanide Date: Fri, 6 Mar 2026 15:09:25 +0100 Subject: [PATCH] qs: add notecard --- .../Modules/Sidebar/Modules/BluetoothCard.qml | 7 +- .../Sidebar/Modules/ConnectionCard.qml | 82 +++++---- .../Modules/Sidebar/Modules/NoteCard.qml | 160 ++++++++++++++++++ .../Modules/NotificationHistoryCard.qml | 2 +- .../Modules/NotificationNoteToggleCard.qml | 144 ++++++++++++++++ .../quickshell/Modules/Sidebar/Sidebars.qml | 2 +- .../quickshell/Services/NotesService.qml | 131 ++++++++++++++ .../quickshell/Services/ShellState.qml | 6 + 8 files changed, 499 insertions(+), 35 deletions(-) create mode 100644 config/quickshell/.config/quickshell/Modules/Sidebar/Modules/NoteCard.qml create mode 100644 config/quickshell/.config/quickshell/Modules/Sidebar/Modules/NotificationNoteToggleCard.qml create mode 100644 config/quickshell/.config/quickshell/Services/NotesService.qml diff --git a/config/quickshell/.config/quickshell/Modules/Sidebar/Modules/BluetoothCard.qml b/config/quickshell/.config/quickshell/Modules/Sidebar/Modules/BluetoothCard.qml index aee94a8..e2b4c99 100644 --- a/config/quickshell/.config/quickshell/Modules/Sidebar/Modules/BluetoothCard.qml +++ b/config/quickshell/.config/quickshell/Modules/Sidebar/Modules/BluetoothCard.qml @@ -4,10 +4,10 @@ import QtQuick.Layouts import Quickshell import Quickshell.Bluetooth import Quickshell.Wayland -import qs.Constants -import qs.Services import qs.Components +import qs.Constants import qs.Modules.Sidebar.Misc +import qs.Services ColumnLayout { spacing: Style.marginM @@ -119,7 +119,6 @@ ColumnLayout { ColumnLayout { Layout.alignment: Qt.AlignHCenter spacing: Style.marginL - visible: { if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) return false; @@ -138,7 +137,7 @@ ColumnLayout { } UText { - text: "Pairing Mode" + text: "Scanning for devices..." pointSize: Style.fontSizeM color: Colors.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter diff --git a/config/quickshell/.config/quickshell/Modules/Sidebar/Modules/ConnectionCard.qml b/config/quickshell/.config/quickshell/Modules/Sidebar/Modules/ConnectionCard.qml index b899aa9..1d4786a 100644 --- a/config/quickshell/.config/quickshell/Modules/Sidebar/Modules/ConnectionCard.qml +++ b/config/quickshell/.config/quickshell/Modules/Sidebar/Modules/ConnectionCard.qml @@ -8,7 +8,7 @@ import qs.Services UBox { id: root - property string currentPanel: "bluetooth" // "bluetooth", "wifi" + property string currentPanel: ShellState.leftSiderbarTab // "bluetooth", "wifi" implicitHeight: contentLoader.implicitHeight + toggleGroup.implicitHeight + Style.marginXS * 2 + Style.marginS * 2 @@ -21,13 +21,14 @@ UBox { Layout.fillWidth: true Rectangle { + // border.color: Colors.mOutline + id: toggleGroup Layout.preferredWidth: Style.baseWidgetSize * 2.8 Layout.preferredHeight: Style.baseWidgetSize radius: Math.min(Style.radiusS, height / 2) color: Colors.mSurface - // border.color: Colors.mOutline Row { anchors.fill: parent @@ -41,18 +42,6 @@ UBox { radius: Math.min(Style.radiusS, height / 2) color: root.currentPanel === "bluetooth" ? Colors.mPrimary : "transparent" - Behavior on width { - NumberAnimation { - duration: 250 - easing.type: Easing.OutCubic - } - } - Behavior on color { - ColorAnimation { - duration: 200 - } - } - UIcon { anchors.centerIn: parent iconName: "bluetooth" @@ -63,14 +52,32 @@ UBox { ColorAnimation { duration: 200 } + } + } MouseArea { anchors.fill: parent - onClicked: root.currentPanel = "bluetooth" + onClicked: ShellState.leftSiderbarTab = "bluetooth" cursorShape: Qt.PointingHandCursor } + + Behavior on width { + NumberAnimation { + duration: 250 + easing.type: Easing.OutCubic + } + + } + + Behavior on color { + ColorAnimation { + duration: 200 + } + + } + } Rectangle { @@ -81,18 +88,6 @@ UBox { radius: Math.min(Style.radiusS, height / 2) color: root.currentPanel === "wifi" ? Colors.mPrimary : "transparent" - Behavior on width { - NumberAnimation { - duration: 250 - easing.type: Easing.OutCubic - } - } - Behavior on color { - ColorAnimation { - duration: 200 - } - } - UIcon { anchors.centerIn: parent iconName: "wifi" @@ -103,16 +98,36 @@ UBox { ColorAnimation { duration: 200 } + } + } MouseArea { anchors.fill: parent - onClicked: root.currentPanel = "wifi" + onClicked: ShellState.leftSiderbarTab = "wifi" cursorShape: Qt.PointingHandCursor } + + Behavior on width { + NumberAnimation { + duration: 250 + easing.type: Easing.OutCubic + } + + } + + Behavior on color { + ColorAnimation { + duration: 200 + } + + } + } + } + } Item { @@ -147,7 +162,9 @@ UBox { } colorFg: Colors.mGreen } + } + } Component { @@ -171,8 +188,11 @@ UBox { onClicked: NetworkService.scan() colorFg: Colors.mGreen } + } + } + } } @@ -186,7 +206,6 @@ UBox { Layout.fillWidth: true Layout.fillHeight: true - sourceComponent: currentPanel === "bluetooth" ? bluetoothComponent : wifiComponent Component { @@ -196,6 +215,7 @@ UBox { anchors.fill: parent anchors.margins: Style.marginS } + } Component { @@ -205,7 +225,11 @@ UBox { anchors.fill: parent anchors.margins: Style.marginS } + } + } + } + } diff --git a/config/quickshell/.config/quickshell/Modules/Sidebar/Modules/NoteCard.qml b/config/quickshell/.config/quickshell/Modules/Sidebar/Modules/NoteCard.qml new file mode 100644 index 0000000..630966f --- /dev/null +++ b/config/quickshell/.config/quickshell/Modules/Sidebar/Modules/NoteCard.qml @@ -0,0 +1,160 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import qs.Components +import qs.Constants +import qs.Services +import qs.Utils + +Rectangle { + id: root + + property real calculatedHeight: headerBox.implicitHeight + scrollView.implicitHeight + Style.marginL * 2 + Style.marginM + property real contentPreferredHeight: Math.min(root.height, Math.ceil(calculatedHeight)) + property real layoutWidth: Math.max(1, root.width - Style.marginL * 2) + + color: "transparent" + + ColumnLayout { + id: mainColumn + + anchors.fill: parent + spacing: Style.marginM + + // Header + UBox { + id: headerBox + + Layout.fillWidth: true + implicitHeight: header.implicitHeight + Style.marginM * 2 + + ColumnLayout { + id: header + + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + RowLayout { + Layout.fillWidth: true + + UIcon { + iconName: "notes" + iconSize: Style.fontSizeXXL + color: Colors.mPrimary + } + + UText { + text: "Notes" + " (" + NotesService.notesModel.count + ")" + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Colors.mOnSurface + Layout.fillWidth: true + } + + UIconButton { + iconName: "plus" + baseSize: Style.baseWidgetSize * 0.8 + colorFg: Colors.mGreen + onClicked: NotesService.createNote() + } + + } + + } + + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + NScrollView { + id: scrollView + + anchors.fill: parent + implicitHeight: notesColumn.implicitHeight + + ColumnLayout { + id: notesColumn + + width: scrollView.width + spacing: Style.marginM + + Repeater { + model: NotesService.notesModel + + delegate: UBox { + property color accentColor: Colors.cavaList[model.colorIdx % Colors.cavaList.length] + + width: notesColumn.width + implicitHeight: noteLayout.implicitHeight + Style.marginM * 2 + + Rectangle { + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: Style.marginS + color: parent.accentColor + radius: Style.radiusS + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton + onClicked: NotesService.openNote(model.noteId) + } + + FileView { + id: fileView + + path: NotesService.notesDir + "/" + model.noteId + ".txt" + watchChanges: true + onFileChanged: reload() + } + + RowLayout { + id: noteLayout + + anchors.fill: parent + anchors.margins: Style.marginM + anchors.leftMargin: Style.marginM + Style.marginS + + UText { + Layout.fillWidth: true + text: { + var t = fileView.text(); + if (!t) + return "(empty note)"; + + return t.trim().split('\n').slice(0, 5).join('\n'); + } + wrapMode: Text.Wrap + elide: Text.ElideRight + maximumLineCount: 5 + } + + UIconButton { + iconName: "trash" + baseSize: Style.baseWidgetSize * 0.8 + colorFg: Colors.mError + onClicked: NotesService.deleteNote(model.noteId) + } + + } + + } + + } + + } + + } + + } + + } + +} diff --git a/config/quickshell/.config/quickshell/Modules/Sidebar/Modules/NotificationHistoryCard.qml b/config/quickshell/.config/quickshell/Modules/Sidebar/Modules/NotificationHistoryCard.qml index 0a55f2d..37fc46b 100644 --- a/config/quickshell/.config/quickshell/Modules/Sidebar/Modules/NotificationHistoryCard.qml +++ b/config/quickshell/.config/quickshell/Modules/Sidebar/Modules/NotificationHistoryCard.qml @@ -491,7 +491,7 @@ Rectangle { readonly property real swipeDismissThreshold: Math.max(110, width * 0.3) readonly property int removeAnimationDuration: Style.animationNormal readonly property int notificationTextFormat: notificationDelegate.isExpanded ? Text.MarkdownText : Text.StyledText - readonly property real actionButtonSize: Style.baseWidgetSize * 0.7 + readonly property real actionButtonSize: Style.baseWidgetSize * 0.8 readonly property real buttonClusterWidth: notificationDelegate.actionButtonSize * 2 + Style.marginXS readonly property real iconSize: 40 // Parse actions safely diff --git a/config/quickshell/.config/quickshell/Modules/Sidebar/Modules/NotificationNoteToggleCard.qml b/config/quickshell/.config/quickshell/Modules/Sidebar/Modules/NotificationNoteToggleCard.qml new file mode 100644 index 0000000..0ec5a78 --- /dev/null +++ b/config/quickshell/.config/quickshell/Modules/Sidebar/Modules/NotificationNoteToggleCard.qml @@ -0,0 +1,144 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Components +import qs.Constants +import qs.Services + +Item { + id: root + + property string currentPanel: ShellState.rightSiderbarTab // "notifications", "notes" + + ColumnLayout { + anchors.fill: parent + spacing: Style.marginM + + Rectangle { + id: toggleGroup + + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: Style.baseWidgetSize * 2.8 + Layout.preferredHeight: Style.baseWidgetSize + radius: Math.min(Style.radiusS, height / 2) + color: Colors.mSurface + + Row { + anchors.fill: parent + spacing: Style.marginS / 2 + + Rectangle { + id: btnNotifications + + width: root.currentPanel === "notifications" ? (parent.width - parent.spacing) * 0.65 : (parent.width - parent.spacing) * 0.35 + height: parent.height + radius: Math.min(Style.radiusS, height / 2) + color: root.currentPanel === "notifications" ? Colors.mPrimary : "transparent" + + UIcon { + anchors.centerIn: parent + iconName: "bell" + iconSize: Style.fontSizeL + color: root.currentPanel === "notifications" ? Colors.mOnPrimary : Colors.mOnSurface + + Behavior on color { + ColorAnimation { + duration: 200 + } + + } + + } + + MouseArea { + anchors.fill: parent + onClicked: ShellState.rightSiderbarTab = "notifications" + cursorShape: Qt.PointingHandCursor + } + + Behavior on width { + NumberAnimation { + duration: 250 + easing.type: Easing.OutCubic + } + + } + + Behavior on color { + ColorAnimation { + duration: 200 + } + + } + + } + + Rectangle { + id: btnNotes + + width: root.currentPanel === "notes" ? (parent.width - parent.spacing) * 0.65 : (parent.width - parent.spacing) * 0.35 + height: parent.height + radius: Math.min(Style.radiusS, height / 2) + color: root.currentPanel === "notes" ? Colors.mPrimary : "transparent" + + UIcon { + anchors.centerIn: parent + iconName: "notes" + iconSize: Style.fontSizeL + color: root.currentPanel === "notes" ? Colors.mOnPrimary : Colors.mOnSurface + + Behavior on color { + ColorAnimation { + duration: 200 + } + + } + + } + + MouseArea { + anchors.fill: parent + onClicked: ShellState.rightSiderbarTab = "notes" + cursorShape: Qt.PointingHandCursor + } + + Behavior on width { + NumberAnimation { + duration: 250 + easing.type: Easing.OutCubic + } + + } + + Behavior on color { + ColorAnimation { + duration: 200 + } + + } + + } + + } + + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + NotificationHistoryCard { + anchors.fill: parent + visible: root.currentPanel === "notifications" + } + + NoteCard { + anchors.fill: parent + visible: root.currentPanel === "notes" + } + + } + + } + +} diff --git a/config/quickshell/.config/quickshell/Modules/Sidebar/Sidebars.qml b/config/quickshell/.config/quickshell/Modules/Sidebar/Sidebars.qml index 3e0127e..be6f6a1 100644 --- a/config/quickshell/.config/quickshell/Modules/Sidebar/Sidebars.qml +++ b/config/quickshell/.config/quickshell/Modules/Sidebar/Sidebars.qml @@ -69,7 +69,7 @@ Variants { Layout.fillWidth: true } - NotificationHistoryCard { + NotificationNoteToggleCard { Layout.fillWidth: true Layout.fillHeight: true } diff --git a/config/quickshell/.config/quickshell/Services/NotesService.qml b/config/quickshell/.config/quickshell/Services/NotesService.qml new file mode 100644 index 0000000..3c52353 --- /dev/null +++ b/config/quickshell/.config/quickshell/Services/NotesService.qml @@ -0,0 +1,131 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Constants +import qs.Utils +pragma Singleton + +Singleton { + id: root + + property string notesDir: Paths.cacheDir + "/notes" + property var notes: [] + property ListModel notesModel + + function loadNotes() { + listProcess.running = true; + } + + function createNote() { + var id = new Date().getTime().toString(); + var filePath = notesDir + "/" + id + ".txt"; + // Random color index from 0 to 7 + var colorIdx = Math.floor(Math.random() * 8); + createProcess.command = ["sh", "-c", "mkdir -p " + notesDir + " && echo 'New Note' > " + filePath + " && echo " + colorIdx + " > " + filePath + ".color"]; + createProcess.running = true; + } + + function deleteNote(id) { + var filePath = notesDir + "/" + id + ".txt"; + var colorPath = notesDir + "/" + id + ".txt.color"; + deleteProcess.command = ["rm", "-f", filePath, colorPath]; + deleteProcess.running = true; + } + + function openNote(id) { + var filePath = notesDir + "/" + id + ".txt"; + openProcess.command = ["gnome-text-editor", filePath]; + openProcess.running = true; + } + + Component.onCompleted: { + loadNotes(); + } + + Process { + id: openProcess + } + + Process { + id: createProcess + + onExited: root.loadNotes() + } + + Process { + id: deleteProcess + + onExited: root.loadNotes() + } + + Process { + id: listProcess + + command: ["sh", "-c", "mkdir -p " + notesDir + " && ls -1 " + notesDir + " | grep '\\.txt$' || true"] + + stdout: StdioCollector { + id: listCollector + + onStreamFinished: { + var files = listCollector.text.split('\n'); + notesModel.clear(); + for (var i = 0; i < files.length; i++) { + if (files[i] === "") + continue; + + var id = files[i].replace(".txt", ""); + var contentFile = notesDir + "/" + files[i]; + var colorFile = notesDir + "/" + files[i] + ".color"; + // create an intermediate reader process + readProcessComponent.createObject(root, { + "noteId": id, + "contentFile": contentFile, + "colorFile": colorFile + }).run(); + } + } + } + + } + + Component { + id: readProcessComponent + + Process { + id: p + + property string noteId + property string contentFile + property string colorFile + + function run() { + running = true; + } + + command: ["sh", "-c", "cat " + colorFile + " 2>/dev/null || echo 0; head -n 5 " + contentFile] + + stdout: StdioCollector { + id: readCollector + + onStreamFinished: { + var lines = readCollector.text.split('\n'); + var colorIdx = parseInt(lines[0] || "0"); + lines.shift(); + var contentLines = lines.join('\n').trim(); + notesModel.append({ + "noteId": p.noteId, + "title": contentLines, + "colorIdx": colorIdx + }); + p.destroy(); + } + } + + } + + } + + notesModel: ListModel { + } + +} diff --git a/config/quickshell/.config/quickshell/Services/ShellState.qml b/config/quickshell/.config/quickshell/Services/ShellState.qml index 344265d..c267993 100644 --- a/config/quickshell/.config/quickshell/Services/ShellState.qml +++ b/config/quickshell/.config/quickshell/Services/ShellState.qml @@ -13,6 +13,8 @@ Singleton { property alias notificationsState: adapter.notificationsState property alias lyricsState: adapter.lyricsState property alias sunsetState: adapter.sunsetState + property alias leftSiderbarTab: adapter.leftSiderbarTab + property alias rightSiderbarTab: adapter.rightSiderbarTab function save() { saveTimer.restart(); @@ -21,6 +23,8 @@ Singleton { onNotificationsStateChanged: save() onLyricsStateChanged: save() onSunsetStateChanged: save() + onLeftSiderbarTabChanged: save() + onRightSiderbarTabChanged: save() Component.onCompleted: { stateFileView.path = stateFile; } @@ -51,6 +55,8 @@ Singleton { property var sunsetState: ({ "enabled": true }) + property string leftSiderbarTab: "bluetooth" + property string rightSiderbarTab: "notes" } }