diff --git a/simple-notes/BarWidget.qml b/simple-notes/BarWidget.qml new file mode 100644 index 0000000..2dcb396 --- /dev/null +++ b/simple-notes/BarWidget.qml @@ -0,0 +1,74 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Widgets + +Rectangle { + id: root + + property var pluginApi: null + property ShellScreen screen + property string widgetId: "" + property string section: "" + + // Standard capsule dimensions + implicitWidth: barIsVertical ? Style.capsuleHeight : contentRow.implicitWidth + Style.marginM * 2 + implicitHeight: Style.capsuleHeight + + readonly property string barPosition: Settings.data.bar.position || "top" + readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" + + // Settings + readonly property bool showCount: pluginApi?.pluginSettings?.showCountInBar ?? true + + function getIntValue(value, defaultValue) { + return (typeof value === 'number') ? Math.floor(value) : defaultValue; + } + + readonly property int noteCount: getIntValue(pluginApi?.pluginSettings?.count, 0) + + color: Style.capsuleColor + radius: Style.radiusL + + RowLayout { + id: contentRow + anchors.centerIn: parent + spacing: Style.marginS + + NIcon { + icon: "paperclip" + applyUiScale: false + color: mouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface + } + + NText { + visible: !barIsVertical && root.showCount + text: root.noteCount.toString() + color: mouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface + font.pointSize: Style.fontSizeS + font.weight: Font.Medium + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onEntered: { + root.color = Color.mHover; + } + + onExited: { + root.color = Style.capsuleColor; + } + + onClicked: { + if (pluginApi) { + pluginApi.openPanel(root.screen); + } + } + } +} diff --git a/simple-notes/Main.qml b/simple-notes/Main.qml new file mode 100644 index 0000000..69a0afd --- /dev/null +++ b/simple-notes/Main.qml @@ -0,0 +1,25 @@ +import QtQuick +import Quickshell.Io +import qs.Services.UI + +Item { + property var pluginApi: null + + Component.onCompleted: { + if (pluginApi) { + // Initialize settings if they don't exist + if (!pluginApi.pluginSettings.notes) { + pluginApi.pluginSettings.notes = []; + pluginApi.saveSettings(); + } + if (pluginApi.pluginSettings.showCountInBar === undefined) { + pluginApi.pluginSettings.showCountInBar = true; + pluginApi.saveSettings(); + } + if (pluginApi.pluginSettings.count === undefined) { + pluginApi.pluginSettings.count = 0; + pluginApi.saveSettings(); + } + } + } +} diff --git a/simple-notes/Panel.qml b/simple-notes/Panel.qml new file mode 100644 index 0000000..5a9a4e4 --- /dev/null +++ b/simple-notes/Panel.qml @@ -0,0 +1,357 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.UI +import qs.Widgets + +Item { + id: root + + property var pluginApi: null + readonly property var geometryPlaceholder: panelContainer + property real contentPreferredWidth: 700 * Style.uiScaleRatio + property real contentPreferredHeight: 500 * Style.uiScaleRatio + readonly property bool allowAttach: true + anchors.fill: parent + + // State + property string viewMode: "list" // "list" or "edit" + property var currentNote: null // Object { id, title, content, modifiedAt } + property ListModel notesModel: ListModel {} + + Component.onCompleted: { + if (pluginApi) { + loadNotes(); + } + } + + // Reload notes when api changes or external updates happen + onPluginApiChanged: { + if (pluginApi) loadNotes(); + } + + function loadNotes() { + notesModel.clear(); + var notes = pluginApi?.pluginSettings?.notes || []; + // Sort by modification date descending + notes.sort((a, b) => new Date(b.modifiedAt) - new Date(a.modifiedAt)); + + for (var i = 0; i < notes.length; i++) { + notesModel.append(notes[i]); + } + } + + function createNote() { + root.currentNote = { + id: null, + title: "", + content: "", + modifiedAt: new Date().toISOString() + }; + root.viewMode = "edit"; + } + + function editNote(noteId) { + var notes = pluginApi?.pluginSettings?.notes || []; + var note = notes.find(n => n.id === noteId); + if (note) { + // Clone to avoid direct mutation + root.currentNote = { + id: note.id, + title: note.title, + content: note.content, + modifiedAt: note.modifiedAt + }; + root.viewMode = "edit"; + } + } + + function saveCurrentNote(title, content) { + if (!pluginApi) return; + + var notes = pluginApi.pluginSettings.notes || []; + var now = new Date().toISOString(); + + if (root.currentNote.id === null) { + // New note + var newNote = { + id: Date.now().toString(), + title: title || "Untitled Note", + content: content, + modifiedAt: now + }; + notes.push(newNote); + } else { + // Update existing + var idx = notes.findIndex(n => n.id === root.currentNote.id); + if (idx >= 0) { + notes[idx].title = title || "Untitled Note"; + notes[idx].content = content; + notes[idx].modifiedAt = now; + } + } + + pluginApi.pluginSettings.notes = notes; + pluginApi.pluginSettings.count = notes.length; + pluginApi.saveSettings(); + loadNotes(); + root.viewMode = "list"; + root.currentNote = null; + } + + function deleteNote(noteId) { + if (!pluginApi) return; + + var notes = pluginApi.pluginSettings.notes || []; + var idx = notes.findIndex(n => n.id === noteId); + if (idx >= 0) { + notes.splice(idx, 1); + pluginApi.pluginSettings.notes = notes; + pluginApi.pluginSettings.count = notes.length; + pluginApi.saveSettings(); + loadNotes(); + } + } + + function deleteCurrentNote() { + if (root.currentNote && root.currentNote.id) { + deleteNote(root.currentNote.id); + } + root.viewMode = "list"; + root.currentNote = null; + } + + Rectangle { + id: panelContainer + anchors.fill: parent + color: Color.transparent + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginL + + // Header + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: Color.mSurfaceVariant + radius: Style.radiusL + + // LIST VIEW + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginM + visible: root.viewMode === "list" + spacing: Style.marginM + + RowLayout { + spacing: Style.marginM + + NIcon { + icon: "sticky-note" + pointSize: Style.fontSizeL + } + + NText { + text: pluginApi?.tr("panel.header.title") || "Notes" + font.pointSize: Style.fontSizeL + font.weight: Font.Medium + color: Color.mOnSurface + } + Item { Layout.fillWidth: true } + NButton { + text: pluginApi?.tr("panel.header.add_button") || "New Note" + icon: "plus" + onClicked: createNote() + } + } + + // Empty State + Item { + Layout.fillWidth: true + Layout.fillHeight: true + visible: notesModel.count === 0 + + NText { + text: pluginApi?.tr("panel.list.empty_message") || "No notes yet" + color: Color.mOnSurfaceVariant + anchors.centerIn: parent + font.pointSize: Style.fontSizeM + } + } + + // List + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + visible: notesModel.count > 0 + clip: true + + ListView { + id: notesList + model: root.notesModel + spacing: Style.marginS + boundsBehavior: Flickable.StopAtBounds + flickableDirection: Flickable.VerticalFlick + + delegate: Rectangle { + width: ListView.view.width + height: cardContent.implicitHeight + Style.marginM * 2 + color: Color.mSurface + radius: Style.radiusS + + // Hover effect + property bool hovered: false + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: parent.hovered = true + onExited: parent.hovered = false + onClicked: editNote(model.id) + } + + RowLayout { + id: cardContent + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Style.marginM + spacing: Style.marginM + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginS + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + NText { + text: model.title + font.weight: Font.Medium + font.pointSize: Style.fontSizeM + color: parent.parent.parent.hovered ? Color.mPrimary : Color.mOnSurface + elide: Text.ElideRight + Layout.fillWidth: true + } + + NText { + text: new Date(model.modifiedAt).toLocaleDateString() + font.pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + } + } + + // Preview content (one line) + NText { + text: model.content.replace(/\n/g, " ") + font.pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + elide: Text.ElideRight + Layout.fillWidth: true + maximumLineCount: 1 + } + } + + // Delete button on the right + NIconButton { + id: deleteButton + icon: "circle-x" + tooltipText: pluginApi?.tr("panel.editor.delete_button") || "Delete" + color: Color.mError + implicitWidth: Style.baseWidgetSize * 0.8 + implicitHeight: Style.baseWidgetSize * 0.8 + radius: Style.radiusM + opacity: 0.7 + + onEntered: opacity = 1.0 + onExited: opacity = 0.7 + onClicked: deleteNote(model.id) + } + } + } + } + } + } + + // EDIT VIEW + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginM + visible: root.viewMode === "edit" + spacing: Style.marginM + + // Toolbar + RowLayout { + spacing: Style.marginS + + NIconButton { + icon: "arrow-left" + onClicked: { + root.viewMode = "list"; + root.currentNote = null; + } + } + + NText { + text: root.currentNote && root.currentNote.id ? "Edit Note" : "New Note" + font.pointSize: Style.fontSizeL + font.weight: Font.Medium + color: Color.mOnSurface + } + + Item { Layout.fillWidth: true } + + NButton { + text: pluginApi?.tr("panel.editor.delete_button") || "Delete" + visible: root.currentNote && root.currentNote.id !== null + color: Color.mError + onClicked: deleteCurrentNote() + } + + NButton { + text: pluginApi?.tr("panel.editor.save_button") || "Save" + onClicked: saveCurrentNote(titleInput.text, contentInput.text) + } + } + + // Editor Fields + NTextInput { + id: titleInput + Layout.fillWidth: true + placeholderText: pluginApi?.tr("panel.editor.title_placeholder") || "Title" + text: root.currentNote ? root.currentNote.title : "" + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: Color.mSurface + radius: Style.radiusM + + ScrollView { + anchors.fill: parent + anchors.margins: Style.marginS + + TextArea { + id: contentInput + width: parent.width + placeholderText: pluginApi?.tr("panel.editor.content_placeholder") || "Content" + placeholderTextColor: Color.mOnSurfaceVariant + text: root.currentNote ? root.currentNote.content : "" + wrapMode: TextEdit.Wrap + color: Color.mOnSurface + font.pointSize: Style.fontSizeM + background: null // Remove default background + selectByMouse: true + } + } + } + } + } + } + } +} diff --git a/simple-notes/README.md b/simple-notes/README.md new file mode 100644 index 0000000..84bcd35 --- /dev/null +++ b/simple-notes/README.md @@ -0,0 +1,24 @@ +# Simple Notes Plugin + +A simple note-taking plugin for [Noctalia Shell](https://github.com/noctalia-dev/noctalia-shell). + +## Features +- **Quick Notes**: Create, edit, and delete notes quickly from your desktop panel. +- **Persistence**: Notes are automatically saved. +- **Bar Widget**: Shows an icon and the current note count in your status bar. + +## Installation + +1. Clone or download this repository into your Noctalia plugins directory. +2. Restart Noctalia Shell. +3. Enable "Simple Notes" in the Noctalia settings if not enabled by default. +4. Add the widget to your bar or desktop. + +## Usage + +- **Bar Widget**: Click the note icon to open the panel. +- **Panel**: + - Click "New Note" to start writing. + - Click an existing note to edit it. + - Use the "Save" button to persist changes. + - Use the "Delete" button to remove a note. diff --git a/simple-notes/Settings.qml b/simple-notes/Settings.qml new file mode 100644 index 0000000..efb53f6 --- /dev/null +++ b/simple-notes/Settings.qml @@ -0,0 +1,30 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + property var pluginApi: null + + // Local state + property bool showCount: pluginApi?.pluginSettings?.showCountInBar ?? true + + spacing: Style.marginM + + NToggle { + label: pluginApi?.tr("settings.show_count.label") || "Show Note Count" + description: pluginApi?.tr("settings.show_count.description") || "Show the number of notes in the bar widget" + checked: root.showCount + onToggled: (checked) => { + root.showCount = checked; + } + } + + function saveSettings() { + if (pluginApi) { + pluginApi.pluginSettings.showCountInBar = root.showCount; + pluginApi.saveSettings(); + } + } +} diff --git a/simple-notes/i18n/en.json b/simple-notes/i18n/en.json new file mode 100644 index 0000000..9cfe109 --- /dev/null +++ b/simple-notes/i18n/en.json @@ -0,0 +1,32 @@ +{ + "panel": { + "header": { + "title": "Notes", + "add_button": "New Note" + }, + "list": { + "empty_message": "No notes yet. Create one!", + "last_modified": "Last modified: " + }, + "editor": { + "title_placeholder": "Note Title", + "content_placeholder": "Write your thoughts here...", + "save_button": "Save", + "cancel_button": "Cancel", + "delete_button": "Delete" + }, + "confirm": { + "delete_title": "Delete Note?", + "delete_message": "Are you sure you want to delete this note?" + } + }, + "bar_widget": { + "tooltip": "Simple Notes" + }, + "settings": { + "show_count": { + "label": "Show Note Count", + "description": "Show the number of notes in the bar widget" + } + } +} diff --git a/simple-notes/manifest.json b/simple-notes/manifest.json new file mode 100644 index 0000000..d2b58e2 --- /dev/null +++ b/simple-notes/manifest.json @@ -0,0 +1,25 @@ +{ + "id": "simple-notes", + "name": "Simple Notes", + "version": "1.0.0", + "minNoctaliaVersion": "3.7.1", + "author": "fusuyfusuy", + "license": "MIT", + "repository": "", + "description": "A simple note taking plugin for Noctalia Shell.", + "entryPoints": { + "main": "Main.qml", + "barWidget": "BarWidget.qml", + "panel": "Panel.qml", + "settings": "Settings.qml" + }, + "dependencies": { + "plugins": [] + }, + "metadata": { + "defaultSettings": { + "notes": [], + "showCountInBar": true + } + } +} \ No newline at end of file