qs: add notecard

This commit is contained in:
2026-03-06 15:09:25 +01:00
parent ca514ac2fa
commit f9facdd61b
8 changed files with 499 additions and 35 deletions
@@ -4,10 +4,10 @@ import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Bluetooth import Quickshell.Bluetooth
import Quickshell.Wayland import Quickshell.Wayland
import qs.Constants
import qs.Services
import qs.Components import qs.Components
import qs.Constants
import qs.Modules.Sidebar.Misc import qs.Modules.Sidebar.Misc
import qs.Services
ColumnLayout { ColumnLayout {
spacing: Style.marginM spacing: Style.marginM
@@ -119,7 +119,6 @@ ColumnLayout {
ColumnLayout { ColumnLayout {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
spacing: Style.marginL spacing: Style.marginL
visible: { visible: {
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
return false; return false;
@@ -138,7 +137,7 @@ ColumnLayout {
} }
UText { UText {
text: "Pairing Mode" text: "Scanning for devices..."
pointSize: Style.fontSizeM pointSize: Style.fontSizeM
color: Colors.mOnSurfaceVariant color: Colors.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
@@ -8,7 +8,7 @@ import qs.Services
UBox { UBox {
id: root 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 implicitHeight: contentLoader.implicitHeight + toggleGroup.implicitHeight + Style.marginXS * 2 + Style.marginS * 2
@@ -21,13 +21,14 @@ UBox {
Layout.fillWidth: true Layout.fillWidth: true
Rectangle { Rectangle {
// border.color: Colors.mOutline
id: toggleGroup id: toggleGroup
Layout.preferredWidth: Style.baseWidgetSize * 2.8 Layout.preferredWidth: Style.baseWidgetSize * 2.8
Layout.preferredHeight: Style.baseWidgetSize Layout.preferredHeight: Style.baseWidgetSize
radius: Math.min(Style.radiusS, height / 2) radius: Math.min(Style.radiusS, height / 2)
color: Colors.mSurface color: Colors.mSurface
// border.color: Colors.mOutline
Row { Row {
anchors.fill: parent anchors.fill: parent
@@ -41,18 +42,6 @@ UBox {
radius: Math.min(Style.radiusS, height / 2) radius: Math.min(Style.radiusS, height / 2)
color: root.currentPanel === "bluetooth" ? Colors.mPrimary : "transparent" color: root.currentPanel === "bluetooth" ? Colors.mPrimary : "transparent"
Behavior on width {
NumberAnimation {
duration: 250
easing.type: Easing.OutCubic
}
}
Behavior on color {
ColorAnimation {
duration: 200
}
}
UIcon { UIcon {
anchors.centerIn: parent anchors.centerIn: parent
iconName: "bluetooth" iconName: "bluetooth"
@@ -63,14 +52,32 @@ UBox {
ColorAnimation { ColorAnimation {
duration: 200 duration: 200
} }
} }
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
onClicked: root.currentPanel = "bluetooth" onClicked: ShellState.leftSiderbarTab = "bluetooth"
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
} }
Behavior on width {
NumberAnimation {
duration: 250
easing.type: Easing.OutCubic
}
}
Behavior on color {
ColorAnimation {
duration: 200
}
}
} }
Rectangle { Rectangle {
@@ -81,18 +88,6 @@ UBox {
radius: Math.min(Style.radiusS, height / 2) radius: Math.min(Style.radiusS, height / 2)
color: root.currentPanel === "wifi" ? Colors.mPrimary : "transparent" color: root.currentPanel === "wifi" ? Colors.mPrimary : "transparent"
Behavior on width {
NumberAnimation {
duration: 250
easing.type: Easing.OutCubic
}
}
Behavior on color {
ColorAnimation {
duration: 200
}
}
UIcon { UIcon {
anchors.centerIn: parent anchors.centerIn: parent
iconName: "wifi" iconName: "wifi"
@@ -103,16 +98,36 @@ UBox {
ColorAnimation { ColorAnimation {
duration: 200 duration: 200
} }
} }
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
onClicked: root.currentPanel = "wifi" onClicked: ShellState.leftSiderbarTab = "wifi"
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
} }
Behavior on width {
NumberAnimation {
duration: 250
easing.type: Easing.OutCubic
} }
} }
Behavior on color {
ColorAnimation {
duration: 200
}
}
}
}
} }
Item { Item {
@@ -147,7 +162,9 @@ UBox {
} }
colorFg: Colors.mGreen colorFg: Colors.mGreen
} }
} }
} }
Component { Component {
@@ -171,8 +188,11 @@ UBox {
onClicked: NetworkService.scan() onClicked: NetworkService.scan()
colorFg: Colors.mGreen colorFg: Colors.mGreen
} }
} }
} }
} }
} }
@@ -186,7 +206,6 @@ UBox {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
sourceComponent: currentPanel === "bluetooth" ? bluetoothComponent : wifiComponent sourceComponent: currentPanel === "bluetooth" ? bluetoothComponent : wifiComponent
Component { Component {
@@ -196,6 +215,7 @@ UBox {
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginS anchors.margins: Style.marginS
} }
} }
Component { Component {
@@ -205,7 +225,11 @@ UBox {
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginS anchors.margins: Style.marginS
} }
} }
} }
} }
} }
@@ -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)
}
}
}
}
}
}
}
}
}
@@ -491,7 +491,7 @@ Rectangle {
readonly property real swipeDismissThreshold: Math.max(110, width * 0.3) readonly property real swipeDismissThreshold: Math.max(110, width * 0.3)
readonly property int removeAnimationDuration: Style.animationNormal readonly property int removeAnimationDuration: Style.animationNormal
readonly property int notificationTextFormat: notificationDelegate.isExpanded ? Text.MarkdownText : Text.StyledText 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 buttonClusterWidth: notificationDelegate.actionButtonSize * 2 + Style.marginXS
readonly property real iconSize: 40 readonly property real iconSize: 40
// Parse actions safely // Parse actions safely
@@ -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"
}
}
}
}
@@ -69,7 +69,7 @@ Variants {
Layout.fillWidth: true Layout.fillWidth: true
} }
NotificationHistoryCard { NotificationNoteToggleCard {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
} }
@@ -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 {
}
}
@@ -13,6 +13,8 @@ Singleton {
property alias notificationsState: adapter.notificationsState property alias notificationsState: adapter.notificationsState
property alias lyricsState: adapter.lyricsState property alias lyricsState: adapter.lyricsState
property alias sunsetState: adapter.sunsetState property alias sunsetState: adapter.sunsetState
property alias leftSiderbarTab: adapter.leftSiderbarTab
property alias rightSiderbarTab: adapter.rightSiderbarTab
function save() { function save() {
saveTimer.restart(); saveTimer.restart();
@@ -21,6 +23,8 @@ Singleton {
onNotificationsStateChanged: save() onNotificationsStateChanged: save()
onLyricsStateChanged: save() onLyricsStateChanged: save()
onSunsetStateChanged: save() onSunsetStateChanged: save()
onLeftSiderbarTabChanged: save()
onRightSiderbarTabChanged: save()
Component.onCompleted: { Component.onCompleted: {
stateFileView.path = stateFile; stateFileView.path = stateFile;
} }
@@ -51,6 +55,8 @@ Singleton {
property var sunsetState: ({ property var sunsetState: ({
"enabled": true "enabled": true
}) })
property string leftSiderbarTab: "bluetooth"
property string rightSiderbarTab: "notes"
} }
} }