1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-13 09:12:08 -04:00

plugins: add plugin state helpers

This commit is contained in:
bbedward
2026-02-12 14:04:56 -05:00
parent ba5bf0cabc
commit 0e9b21d359
5 changed files with 592 additions and 0 deletions

View File

@@ -147,6 +147,24 @@ Item {
return defaultValue; return defaultValue;
} }
function saveState(key, value) {
if (!pluginService)
return;
if (pluginService.savePluginState)
pluginService.savePluginState(pluginId, key, value);
}
function loadState(key, defaultValue) {
if (pluginService && pluginService.loadPluginState)
return pluginService.loadPluginState(pluginId, key, defaultValue);
return defaultValue;
}
function clearState() {
if (pluginService && pluginService.clearPluginState)
pluginService.clearPluginState(pluginId);
}
function findFlickable(item) { function findFlickable(item) {
var current = item?.parent; var current = item?.parent;
while (current) { while (current) {

View File

@@ -0,0 +1,174 @@
import QtQuick
import Quickshell
import qs.Services
Item {
id: root
property var pluginService: null
property string trigger: "n"
signal itemsChanged
property var notes: []
property int maxNotes: 50
Component.onCompleted: {
if (!pluginService)
return;
trigger = pluginService.loadPluginData("quickNotesExample", "trigger", "n");
maxNotes = pluginService.loadPluginData("quickNotesExample", "maxNotes", 50);
// Load notes from plugin STATE (persistent across sessions, separate file)
notes = pluginService.loadPluginState("quickNotesExample", "notes", []);
}
function getItems(query) {
const items = [];
if (query && query.trim().length > 0) {
const text = query.trim();
items.push({
name: "Save note: " + text,
icon: "material:note_add",
comment: "Save as a new note",
action: "add:" + text,
categories: ["Quick Notes"]
});
items.push({
name: "Copy: " + text,
icon: "material:content_copy",
comment: "Copy text to clipboard",
action: "copy:" + text,
categories: ["Quick Notes"]
});
}
const filteredNotes = query ? notes.filter(n => n.text.toLowerCase().includes(query.toLowerCase())) : notes;
for (let i = 0; i < Math.min(20, filteredNotes.length); i++) {
const note = filteredNotes[i];
const age = _formatAge(note.timestamp);
items.push({
name: note.text,
icon: "material:sticky_note_2",
comment: age + " — select to copy, hold for options",
action: "copy:" + note.text,
categories: ["Quick Notes"]
});
}
if (notes.length > 0 && !query) {
items.push({
name: "Clear all notes (" + notes.length + ")",
icon: "material:delete_sweep",
comment: "Remove all saved notes",
action: "clear:",
categories: ["Quick Notes"]
});
}
return items;
}
function executeItem(item) {
if (!item?.action)
return;
const colonIdx = item.action.indexOf(":");
const actionType = item.action.substring(0, colonIdx);
const actionData = item.action.substring(colonIdx + 1);
switch (actionType) {
case "add":
addNote(actionData);
break;
case "copy":
copyToClipboard(actionData);
break;
case "remove":
removeNote(actionData);
break;
case "clear":
clearAllNotes();
break;
default:
showToast("Unknown action: " + actionType);
}
}
function addNote(text) {
if (!text)
return;
const existing = notes.findIndex(n => n.text === text);
if (existing !== -1)
notes.splice(existing, 1);
notes.unshift({
text: text,
timestamp: Date.now()
});
if (notes.length > maxNotes)
notes = notes.slice(0, maxNotes);
_saveNotes();
showToast("Note saved");
}
function removeNote(text) {
notes = notes.filter(n => n.text !== text);
_saveNotes();
showToast("Note removed");
}
function clearAllNotes() {
notes = [];
pluginService.clearPluginState("quickNotesExample");
showToast("All notes cleared");
itemsChanged();
}
function _saveNotes() {
if (!pluginService)
return;
// Save to plugin STATE — writes to quickNotesExample_state.json
// This is separate from plugin SETTINGS (plugin_settings.json)
pluginService.savePluginState("quickNotesExample", "notes", notes);
itemsChanged();
}
function copyToClipboard(text) {
Quickshell.execDetached(["dms", "cl", "copy", text]);
showToast("Copied to clipboard");
}
function showToast(message) {
if (typeof ToastService !== "undefined")
ToastService.showInfo("Quick Notes", message);
}
function _formatAge(timestamp) {
if (!timestamp)
return "";
const seconds = Math.floor((Date.now() - timestamp) / 1000);
if (seconds < 60)
return "just now";
const minutes = Math.floor(seconds / 60);
if (minutes < 60)
return minutes + "m ago";
const hours = Math.floor(minutes / 60);
if (hours < 24)
return hours + "h ago";
const days = Math.floor(hours / 24);
return days + "d ago";
}
onTriggerChanged: {
if (!pluginService)
return;
pluginService.savePluginData("quickNotesExample", "trigger", trigger);
}
}

View File

@@ -0,0 +1,263 @@
import QtQuick
import qs.Widgets
FocusScope {
id: root
property var pluginService: null
implicitHeight: settingsColumn.implicitHeight
height: implicitHeight
Column {
id: settingsColumn
anchors.fill: parent
anchors.margins: 16
spacing: 16
Text {
text: "Quick Notes Settings"
font.pixelSize: 18
font.weight: Font.Bold
color: "#FFFFFF"
}
Text {
text: "Demonstrates the plugin state API — notes are stored in a separate state file (quickNotesExample_state.json) rather than plugin_settings.json."
font.pixelSize: 14
color: "#CCFFFFFF"
wrapMode: Text.WordWrap
width: parent.width - 32
}
Rectangle {
width: parent.width - 32
height: 1
color: "#30FFFFFF"
}
Column {
spacing: 12
width: parent.width - 32
Text {
text: "Trigger Configuration"
font.pixelSize: 16
font.weight: Font.Medium
color: "#FFFFFF"
}
Row {
spacing: 12
anchors.left: parent.left
anchors.right: parent.right
Text {
text: "Trigger:"
font.pixelSize: 14
color: "#FFFFFF"
anchors.verticalCenter: parent.verticalCenter
}
DankTextField {
id: triggerField
width: 100
height: 40
text: loadSettings("trigger", "n")
placeholderText: "n"
backgroundColor: "#30FFFFFF"
textColor: "#FFFFFF"
onTextEdited: {
saveSettings("trigger", text.trim() || "n");
}
}
}
}
Rectangle {
width: parent.width - 32
height: 1
color: "#30FFFFFF"
}
Column {
spacing: 12
width: parent.width - 32
Text {
text: "Storage"
font.pixelSize: 16
font.weight: Font.Medium
color: "#FFFFFF"
}
Row {
spacing: 12
Text {
text: "Max notes:"
font.pixelSize: 14
color: "#FFFFFF"
anchors.verticalCenter: parent.verticalCenter
}
DankTextField {
id: maxNotesField
width: 80
height: 40
text: loadSettings("maxNotes", 50).toString()
placeholderText: "50"
backgroundColor: "#30FFFFFF"
textColor: "#FFFFFF"
onTextEdited: {
const val = parseInt(text);
if (!isNaN(val) && val > 0)
saveSettings("maxNotes", val);
}
}
}
Text {
text: {
const count = loadState("notes", []).length;
return "Currently storing " + count + " note(s)";
}
font.pixelSize: 12
color: "#AAFFFFFF"
}
Rectangle {
width: clearRow.implicitWidth + 24
height: clearRow.implicitHeight + 16
radius: 8
color: clearMouseArea.containsMouse ? "#40FF5252" : "#30FF5252"
Row {
id: clearRow
anchors.centerIn: parent
spacing: 8
Text {
text: "🗑"
font.pixelSize: 14
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Clear all notes"
font.pixelSize: 14
color: "#FF5252"
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: clearMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (pluginService) {
pluginService.clearPluginState("quickNotesExample");
}
}
}
}
}
Rectangle {
width: parent.width - 32
height: 1
color: "#30FFFFFF"
}
Column {
spacing: 8
width: parent.width - 32
Text {
text: "API Usage (for plugin developers):"
font.pixelSize: 14
font.weight: Font.Medium
color: "#FFFFFF"
}
Column {
spacing: 4
leftPadding: 16
bottomPadding: 24
Text {
text: "• pluginService.savePluginState(id, key, value)"
font.pixelSize: 12
color: "#CCFFFFFF"
font.family: "monospace"
}
Text {
text: " Writes to ~/.local/state/.../id_state.json"
font.pixelSize: 11
color: "#AAFFFFFF"
}
Text {
text: "• pluginService.loadPluginState(id, key, default)"
font.pixelSize: 12
color: "#CCFFFFFF"
font.family: "monospace"
}
Text {
text: " Reads from the per-plugin state file"
font.pixelSize: 11
color: "#AAFFFFFF"
}
Text {
text: "• pluginService.clearPluginState(id)"
font.pixelSize: 12
color: "#CCFFFFFF"
font.family: "monospace"
}
Text {
text: " Clears all state for a plugin"
font.pixelSize: 11
color: "#AAFFFFFF"
}
Text {
text: "• pluginService.removePluginStateKey(id, key)"
font.pixelSize: 12
color: "#CCFFFFFF"
font.family: "monospace"
}
Text {
text: " Removes a single key from plugin state"
font.pixelSize: 11
color: "#AAFFFFFF"
}
}
}
}
function saveSettings(key, value) {
if (pluginService)
pluginService.savePluginData("quickNotesExample", key, value);
}
function loadSettings(key, defaultValue) {
if (pluginService)
return pluginService.loadPluginData("quickNotesExample", key, defaultValue);
return defaultValue;
}
function loadState(key, defaultValue) {
if (pluginService)
return pluginService.loadPluginState("quickNotesExample", key, defaultValue);
return defaultValue;
}
}

View File

@@ -0,0 +1,17 @@
{
"id": "quickNotesExample",
"name": "Quick Notes",
"description": "Example launcher plugin demonstrating the plugin state API for persistent data like history and notes",
"version": "1.0.0",
"author": "DankMaterialShell",
"icon": "sticky_note_2",
"type": "launcher",
"capabilities": ["clipboard"],
"component": "./QuickNotesLauncher.qml",
"settings": "./QuickNotesSettings.qml",
"trigger": "n",
"permissions": [
"settings_read",
"settings_write"
]
}

View File

@@ -33,10 +33,17 @@ Singleton {
property var pluginInstances: ({}) property var pluginInstances: ({})
property var globalVars: ({}) property var globalVars: ({})
property var _stateCache: ({})
property var _stateLoaded: ({})
property var _stateWriters: ({})
property var _stateDirtyPlugins: ({})
property bool _stateDirCreated: false
signal pluginLoaded(string pluginId) signal pluginLoaded(string pluginId)
signal pluginUnloaded(string pluginId) signal pluginUnloaded(string pluginId)
signal pluginLoadFailed(string pluginId, string error) signal pluginLoadFailed(string pluginId, string error)
signal pluginDataChanged(string pluginId) signal pluginDataChanged(string pluginId)
signal pluginStateChanged(string pluginId)
signal pluginListUpdated signal pluginListUpdated
signal globalVarChanged(string pluginId, string varName) signal globalVarChanged(string pluginId, string varName)
signal requestLauncherUpdate(string pluginId) signal requestLauncherUpdate(string pluginId)
@@ -48,6 +55,13 @@ Singleton {
onTriggered: resyncAll() onTriggered: resyncAll()
} }
Timer {
id: _stateWriteTimer
interval: 150
repeat: false
onTriggered: root._flushDirtyStates()
}
Component.onCompleted: { Component.onCompleted: {
userWatcher.folder = Paths.toFileUrl(root.pluginDirectory); userWatcher.folder = Paths.toFileUrl(root.pluginDirectory);
systemWatcher.folder = Paths.toFileUrl(root.systemPluginDirectory); systemWatcher.folder = Paths.toFileUrl(root.systemPluginDirectory);
@@ -374,6 +388,7 @@ Singleton {
delete newLoaded[pluginId]; delete newLoaded[pluginId];
loadedPlugins = newLoaded; loadedPlugins = newLoaded;
_cleanupPluginStateWriter(pluginId);
pluginUnloaded(pluginId); pluginUnloaded(pluginId);
return true; return true;
} catch (error) { } catch (error) {
@@ -603,6 +618,111 @@ Singleton {
SettingsData.savePluginSettings(); SettingsData.savePluginSettings();
} }
function getPluginStatePath(pluginId) {
return Paths.strip(Paths.state) + "/plugins/" + pluginId + "_state.json";
}
function loadPluginState(pluginId, key, defaultValue) {
if (!_stateLoaded[pluginId])
_loadStateFromDisk(pluginId);
const state = _stateCache[pluginId];
if (!state)
return defaultValue;
return state[key] !== undefined ? state[key] : defaultValue;
}
function savePluginState(pluginId, key, value) {
if (!_stateLoaded[pluginId])
_loadStateFromDisk(pluginId);
if (!_stateCache[pluginId])
_stateCache[pluginId] = {};
_stateCache[pluginId][key] = value;
_stateDirtyPlugins[pluginId] = true;
_stateWriteTimer.restart();
pluginStateChanged(pluginId);
}
function clearPluginState(pluginId) {
_stateCache[pluginId] = {};
_stateLoaded[pluginId] = true;
_flushStateToDisk(pluginId);
pluginStateChanged(pluginId);
}
function removePluginStateKey(pluginId, key) {
if (!_stateCache[pluginId])
return;
delete _stateCache[pluginId][key];
_stateDirtyPlugins[pluginId] = true;
_stateWriteTimer.restart();
pluginStateChanged(pluginId);
}
function _ensureStateDir() {
if (_stateDirCreated)
return;
_stateDirCreated = true;
Paths.mkdir(Paths.state + "/plugins");
}
function _loadStateFromDisk(pluginId) {
_stateLoaded[pluginId] = true;
_ensureStateDir();
const path = getPluginStatePath(pluginId);
const escapedPath = path.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
try {
const qml = 'import QtQuick; import Quickshell.Io; FileView { path: "' + escapedPath + '"; blockLoading: true; blockWrites: true; atomicWrites: true }';
const fv = Qt.createQmlObject(qml, root, "sf_" + pluginId);
const raw = fv.text();
if (raw && raw.trim()) {
_stateCache[pluginId] = JSON.parse(raw);
} else {
_stateCache[pluginId] = {};
}
_stateWriters[pluginId] = fv;
} catch (e) {
_stateCache[pluginId] = {};
}
}
function _flushStateToDisk(pluginId) {
_ensureStateDir();
const content = JSON.stringify(_stateCache[pluginId] || {}, null, 2);
if (_stateWriters[pluginId]) {
_stateWriters[pluginId].setText(content);
return;
}
const path = getPluginStatePath(pluginId);
const escapedPath = path.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
try {
const qml = 'import QtQuick; import Quickshell.Io; FileView { path: "' + escapedPath + '"; blockWrites: true; atomicWrites: true }';
const fv = Qt.createQmlObject(qml, root, "sw_" + pluginId);
_stateWriters[pluginId] = fv;
fv.loaded.connect(function () {
fv.setText(content);
});
fv.loadFailed.connect(function () {
fv.setText(content);
});
} catch (e) {
console.warn("PluginService: Failed to write state for", pluginId, e.message);
}
}
function _flushDirtyStates() {
const dirty = _stateDirtyPlugins;
_stateDirtyPlugins = {};
for (const pluginId in dirty)
_flushStateToDisk(pluginId);
}
function _cleanupPluginStateWriter(pluginId) {
if (!_stateWriters[pluginId])
return;
_stateWriters[pluginId].destroy();
delete _stateWriters[pluginId];
}
function scanPlugins() { function scanPlugins() {
const userUrl = Paths.toFileUrl(root.pluginDirectory); const userUrl = Paths.toFileUrl(root.pluginDirectory);
const systemUrl = Paths.toFileUrl(root.systemPluginDirectory); const systemUrl = Paths.toFileUrl(root.systemPluginDirectory);