mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-03 20:32:07 -04:00
plugins: add plugin state helpers
This commit is contained in:
@@ -147,6 +147,24 @@ Item {
|
||||
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) {
|
||||
var current = item?.parent;
|
||||
while (current) {
|
||||
|
||||
174
quickshell/PLUGINS/QuickNotesExample/QuickNotesLauncher.qml
Normal file
174
quickshell/PLUGINS/QuickNotesExample/QuickNotesLauncher.qml
Normal 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);
|
||||
}
|
||||
}
|
||||
263
quickshell/PLUGINS/QuickNotesExample/QuickNotesSettings.qml
Normal file
263
quickshell/PLUGINS/QuickNotesExample/QuickNotesSettings.qml
Normal 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;
|
||||
}
|
||||
}
|
||||
17
quickshell/PLUGINS/QuickNotesExample/plugin.json
Normal file
17
quickshell/PLUGINS/QuickNotesExample/plugin.json
Normal 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"
|
||||
]
|
||||
}
|
||||
@@ -33,10 +33,17 @@ Singleton {
|
||||
property var pluginInstances: ({})
|
||||
property var globalVars: ({})
|
||||
|
||||
property var _stateCache: ({})
|
||||
property var _stateLoaded: ({})
|
||||
property var _stateWriters: ({})
|
||||
property var _stateDirtyPlugins: ({})
|
||||
property bool _stateDirCreated: false
|
||||
|
||||
signal pluginLoaded(string pluginId)
|
||||
signal pluginUnloaded(string pluginId)
|
||||
signal pluginLoadFailed(string pluginId, string error)
|
||||
signal pluginDataChanged(string pluginId)
|
||||
signal pluginStateChanged(string pluginId)
|
||||
signal pluginListUpdated
|
||||
signal globalVarChanged(string pluginId, string varName)
|
||||
signal requestLauncherUpdate(string pluginId)
|
||||
@@ -48,6 +55,13 @@ Singleton {
|
||||
onTriggered: resyncAll()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: _stateWriteTimer
|
||||
interval: 150
|
||||
repeat: false
|
||||
onTriggered: root._flushDirtyStates()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
userWatcher.folder = Paths.toFileUrl(root.pluginDirectory);
|
||||
systemWatcher.folder = Paths.toFileUrl(root.systemPluginDirectory);
|
||||
@@ -374,6 +388,7 @@ Singleton {
|
||||
delete newLoaded[pluginId];
|
||||
loadedPlugins = newLoaded;
|
||||
|
||||
_cleanupPluginStateWriter(pluginId);
|
||||
pluginUnloaded(pluginId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -603,6 +618,111 @@ Singleton {
|
||||
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() {
|
||||
const userUrl = Paths.toFileUrl(root.pluginDirectory);
|
||||
const systemUrl = Paths.toFileUrl(root.systemPluginDirectory);
|
||||
|
||||
Reference in New Issue
Block a user