mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 13:32:50 -05:00
feat: Plugin System (#276)
* feat: Plugin System * fix: merge conflicts
This commit is contained in:
committed by
GitHub
parent
ab0759f441
commit
53983933dc
167
CLAUDE.md
167
CLAUDE.md
@@ -63,6 +63,9 @@ quickshell -p shell.qml
|
|||||||
# Or use the shorthand
|
# Or use the shorthand
|
||||||
qs -p .
|
qs -p .
|
||||||
|
|
||||||
|
# Run with verbose output for debugging
|
||||||
|
qs -v -p shell.qml
|
||||||
|
|
||||||
# Code formatting and linting
|
# Code formatting and linting
|
||||||
qmlfmt -t 4 -i 4 -b 250 -w /path/to/file.qml # Format a QML file (requires qmlfmt, do not use qmlformat)
|
qmlfmt -t 4 -i 4 -b 250 -w /path/to/file.qml # Format a QML file (requires qmlfmt, do not use qmlformat)
|
||||||
qmllint **/*.qml # Lint all QML files for syntax errors
|
qmllint **/*.qml # Lint all QML files for syntax errors
|
||||||
@@ -89,6 +92,7 @@ shell.qml # Main entry point (minimal orchestration)
|
|||||||
│ ├── DisplayService.qml
|
│ ├── DisplayService.qml
|
||||||
│ ├── NotificationService.qml
|
│ ├── NotificationService.qml
|
||||||
│ ├── WeatherService.qml
|
│ ├── WeatherService.qml
|
||||||
|
│ ├── PluginService.qml
|
||||||
│ └── [14 more services]
|
│ └── [14 more services]
|
||||||
├── Modules/ # UI components (93 files)
|
├── Modules/ # UI components (93 files)
|
||||||
│ ├── TopBar/ # Panel components (13 files)
|
│ ├── TopBar/ # Panel components (13 files)
|
||||||
@@ -104,15 +108,21 @@ shell.qml # Main entry point (minimal orchestration)
|
|||||||
│ ├── SettingsModal.qml
|
│ ├── SettingsModal.qml
|
||||||
│ ├── ClipboardHistoryModal.qml
|
│ ├── ClipboardHistoryModal.qml
|
||||||
│ ├── ProcessListModal.qml
|
│ ├── ProcessListModal.qml
|
||||||
|
│ ├── PluginSettingsModal.qml
|
||||||
│ └── [7 more modals]
|
│ └── [7 more modals]
|
||||||
└── Widgets/ # Reusable UI controls (19 files)
|
├── Widgets/ # Reusable UI controls (19 files)
|
||||||
├── DankIcon.qml
|
│ ├── DankIcon.qml
|
||||||
├── DankSlider.qml
|
│ ├── DankSlider.qml
|
||||||
├── DankToggle.qml
|
│ ├── DankToggle.qml
|
||||||
├── DankTabBar.qml
|
│ ├── DankTabBar.qml
|
||||||
├── DankGridView.qml
|
│ ├── DankGridView.qml
|
||||||
├── DankListView.qml
|
│ ├── DankListView.qml
|
||||||
└── [13 more widgets]
|
│ └── [13 more widgets]
|
||||||
|
└── plugins/ # External plugins directory ($CONFIGPATH/DankMaterialShell/plugins/)
|
||||||
|
└── PluginName/ # Example Plugin structure
|
||||||
|
├── plugin.json # Plugin manifest
|
||||||
|
├── PluginNameWidget.qml # Widget component
|
||||||
|
└── PluginNameSettings.qml # Settings UI
|
||||||
```
|
```
|
||||||
|
|
||||||
### Component Organization
|
### Component Organization
|
||||||
@@ -163,6 +173,12 @@ shell.qml # Main entry point (minimal orchestration)
|
|||||||
- **DankLocationSearch**: Location picker with search
|
- **DankLocationSearch**: Location picker with search
|
||||||
- **SystemLogo**: Animated system branding component
|
- **SystemLogo**: Animated system branding component
|
||||||
|
|
||||||
|
7. **Plugins/** - External plugin system (`$CONFIGPATH/DankMaterialShell/plugins/`)
|
||||||
|
- **PluginService**: Discovers, loads, and manages plugin lifecycle
|
||||||
|
- **Dynamic Loading**: Plugins loaded at runtime from external directory
|
||||||
|
- **DankBar Integration**: Plugin widgets rendered alongside built-in widgets
|
||||||
|
- **Settings System**: Per-plugin settings with persistence
|
||||||
|
|
||||||
### Key Architectural Patterns
|
### Key Architectural Patterns
|
||||||
|
|
||||||
1. **Singleton Services Pattern**:
|
1. **Singleton Services Pattern**:
|
||||||
@@ -408,10 +424,10 @@ When modifying the shell:
|
|||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property bool featureAvailable: false
|
property bool featureAvailable: false
|
||||||
property type currentValue: defaultValue
|
property type currentValue: defaultValue
|
||||||
|
|
||||||
function performAction(param) {
|
function performAction(param) {
|
||||||
// Implementation
|
// Implementation
|
||||||
}
|
}
|
||||||
@@ -422,7 +438,7 @@ When modifying the shell:
|
|||||||
```qml
|
```qml
|
||||||
// In module files
|
// In module files
|
||||||
property alias serviceValue: NewService.currentValue
|
property alias serviceValue: NewService.currentValue
|
||||||
|
|
||||||
SomeControl {
|
SomeControl {
|
||||||
visible: NewService.featureAvailable
|
visible: NewService.featureAvailable
|
||||||
enabled: NewService.featureAvailable
|
enabled: NewService.featureAvailable
|
||||||
@@ -430,6 +446,134 @@ When modifying the shell:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Creating Plugins
|
||||||
|
|
||||||
|
Plugins are external, dynamically-loaded components that extend DankBar functionality. Plugins are stored in `~/.config/DankMaterialShell/plugins/` and have their settings isolated from core DMS settings.
|
||||||
|
|
||||||
|
1. **Create plugin directory**:
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.config/DankMaterialShell/plugins/YourPlugin
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create manifest** (`plugin.json`):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "yourPlugin",
|
||||||
|
"name": "Your Plugin",
|
||||||
|
"description": "Widget description",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "Your Name",
|
||||||
|
"icon": "extension",
|
||||||
|
"component": "./YourWidget.qml",
|
||||||
|
"settings": "./YourSettings.qml",
|
||||||
|
"permissions": ["settings_read", "settings_write"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Create widget component** (`YourWidget.qml`):
|
||||||
|
```qml
|
||||||
|
import QtQuick
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool compactMode: false
|
||||||
|
property string section: "center"
|
||||||
|
property real widgetHeight: 30
|
||||||
|
property var pluginService: null
|
||||||
|
|
||||||
|
width: content.implicitWidth + 16
|
||||||
|
height: widgetHeight
|
||||||
|
radius: 8
|
||||||
|
color: "#20FFFFFF"
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (pluginService) {
|
||||||
|
var data = pluginService.loadPluginData("yourPlugin", "key", defaultValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Create settings component** (`YourSettings.qml`):
|
||||||
|
```qml
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
|
||||||
|
FocusScope {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var pluginService: null
|
||||||
|
|
||||||
|
implicitHeight: settingsColumn.implicitHeight
|
||||||
|
height: implicitHeight
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: settingsColumn
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 16
|
||||||
|
spacing: 12
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Your Plugin Settings"
|
||||||
|
font.pixelSize: 18
|
||||||
|
font.weight: Font.Bold
|
||||||
|
}
|
||||||
|
|
||||||
|
// Your settings UI here
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSettings(key, value) {
|
||||||
|
if (pluginService) {
|
||||||
|
pluginService.savePluginData("yourPlugin", key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSettings(key, defaultValue) {
|
||||||
|
if (pluginService) {
|
||||||
|
return pluginService.loadPluginData("yourPlugin", key, defaultValue)
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Enable plugin**:
|
||||||
|
- Open Settings → Plugins
|
||||||
|
- Click "Scan for Plugins"
|
||||||
|
- Toggle plugin to enable
|
||||||
|
- Add plugin ID to DankBar widget list
|
||||||
|
|
||||||
|
**Plugin Directory Structure:**
|
||||||
|
```
|
||||||
|
~/.config/DankMaterialShell/
|
||||||
|
├── settings.json # Core DMS settings + plugin settings
|
||||||
|
│ └── pluginSettings: {
|
||||||
|
│ └── yourPlugin: {
|
||||||
|
│ ├── enabled: true,
|
||||||
|
│ └── customData: {...}
|
||||||
|
│ }
|
||||||
|
│ }
|
||||||
|
└── plugins/ # Plugin files directory
|
||||||
|
└── YourPlugin/ # Plugin directory (matches manifest ID)
|
||||||
|
├── plugin.json # Plugin manifest
|
||||||
|
├── YourWidget.qml # Widget component
|
||||||
|
└── YourSettings.qml # Settings UI (optional)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Plugin APIs:**
|
||||||
|
- `pluginService.loadPluginData(pluginId, key, default)` - Load persistent data
|
||||||
|
- `pluginService.savePluginData(pluginId, key, value)` - Save persistent data
|
||||||
|
- `PluginService.enablePlugin(pluginId)` - Load plugin
|
||||||
|
- `PluginService.disablePlugin(pluginId)` - Unload plugin
|
||||||
|
|
||||||
|
**Important Notes:**
|
||||||
|
- Plugin settings are automatically injected by the PluginService via `item.pluginService = PluginService`
|
||||||
|
- Settings are stored in the main settings.json but namespaced under `pluginSettings.{pluginId}`
|
||||||
|
- Plugin directories must match the plugin ID in the manifest
|
||||||
|
- Use the injected `pluginService` property in both widget and settings components
|
||||||
|
|
||||||
### Debugging Common Issues
|
### Debugging Common Issues
|
||||||
|
|
||||||
1. **Import errors**: Check import paths
|
1. **Import errors**: Check import paths
|
||||||
@@ -454,6 +598,7 @@ When modifying the shell:
|
|||||||
- **Function Discovery**: Use grep/search tools to find existing utility functions before implementing new ones
|
- **Function Discovery**: Use grep/search tools to find existing utility functions before implementing new ones
|
||||||
- **Modern QML Patterns**: Leverage new widgets like DankTextField, DankDropdown, CachingImage
|
- **Modern QML Patterns**: Leverage new widgets like DankTextField, DankDropdown, CachingImage
|
||||||
- **Structured Organization**: Follow the established Services/Modules/Widgets/Modals separation
|
- **Structured Organization**: Follow the established Services/Modules/Widgets/Modals separation
|
||||||
|
- **Plugin System**: For user extensions, create plugins instead of modifying core modules - see docs/PLUGINS.md
|
||||||
|
|
||||||
### Common Widget Patterns
|
### Common Widget Patterns
|
||||||
|
|
||||||
|
|||||||
@@ -164,6 +164,8 @@ Singleton {
|
|||||||
|
|
||||||
property bool _loading: false
|
property bool _loading: false
|
||||||
|
|
||||||
|
property var pluginSettings: ({})
|
||||||
|
|
||||||
function getEffectiveTimeFormat() {
|
function getEffectiveTimeFormat() {
|
||||||
if (use24HourClock) {
|
if (use24HourClock) {
|
||||||
return Locale.ShortFormat
|
return Locale.ShortFormat
|
||||||
@@ -350,6 +352,7 @@ Singleton {
|
|||||||
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch"
|
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch"
|
||||||
surfaceBase = settings.surfaceBase !== undefined ? settings.surfaceBase : "s"
|
surfaceBase = settings.surfaceBase !== undefined ? settings.surfaceBase : "s"
|
||||||
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({})
|
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({})
|
||||||
|
pluginSettings = settings.pluginSettings !== undefined ? settings.pluginSettings : ({})
|
||||||
applyStoredTheme()
|
applyStoredTheme()
|
||||||
detectAvailableIconThemes()
|
detectAvailableIconThemes()
|
||||||
detectQtTools()
|
detectQtTools()
|
||||||
@@ -468,7 +471,8 @@ Singleton {
|
|||||||
"notificationTimeoutNormal": notificationTimeoutNormal,
|
"notificationTimeoutNormal": notificationTimeoutNormal,
|
||||||
"notificationTimeoutCritical": notificationTimeoutCritical,
|
"notificationTimeoutCritical": notificationTimeoutCritical,
|
||||||
"notificationPopupPosition": notificationPopupPosition,
|
"notificationPopupPosition": notificationPopupPosition,
|
||||||
"screenPreferences": screenPreferences
|
"screenPreferences": screenPreferences,
|
||||||
|
"pluginSettings": pluginSettings
|
||||||
}, null, 2))
|
}, null, 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1229,6 +1233,33 @@ Singleton {
|
|||||||
return Quickshell.screens.filter(screen => prefs.includes(screen.name))
|
return Quickshell.screens.filter(screen => prefs.includes(screen.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Plugin settings functions
|
||||||
|
function getPluginSetting(pluginId, key, defaultValue) {
|
||||||
|
if (!pluginSettings[pluginId]) {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return pluginSettings[pluginId][key] !== undefined ? pluginSettings[pluginId][key] : defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPluginSetting(pluginId, key, value) {
|
||||||
|
if (!pluginSettings[pluginId]) {
|
||||||
|
pluginSettings[pluginId] = {}
|
||||||
|
}
|
||||||
|
pluginSettings[pluginId][key] = value
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePluginSettings(pluginId) {
|
||||||
|
if (pluginSettings[pluginId]) {
|
||||||
|
delete pluginSettings[pluginId]
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPluginSettingsForPlugin(pluginId) {
|
||||||
|
return pluginSettings[pluginId] || {}
|
||||||
|
}
|
||||||
|
|
||||||
function _shq(s) {
|
function _shq(s) {
|
||||||
return "'" + String(s).replace(/'/g, "'\\''") + "'"
|
return "'" + String(s).replace(/'/g, "'\\''") + "'"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,13 +154,26 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: aboutLoader
|
id: pluginsLoader
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
active: root.currentIndex === 10
|
active: root.currentIndex === 10
|
||||||
visible: active
|
visible: active
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: PluginsTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: aboutLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 11
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
sourceComponent: AboutTab {
|
sourceComponent: AboutTab {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ DankModal {
|
|||||||
|
|
||||||
objectName: "settingsModal"
|
objectName: "settingsModal"
|
||||||
width: 800
|
width: 800
|
||||||
height: 750
|
height: 800
|
||||||
visible: false
|
visible: false
|
||||||
onBackgroundClicked: () => {
|
onBackgroundClicked: () => {
|
||||||
return hide();
|
return hide();
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ Rectangle {
|
|||||||
}, {
|
}, {
|
||||||
"text": "Power",
|
"text": "Power",
|
||||||
"icon": "power_settings_new"
|
"icon": "power_settings_new"
|
||||||
|
}, {
|
||||||
|
"text": "Plugins",
|
||||||
|
"icon": "extension"
|
||||||
}, {
|
}, {
|
||||||
"text": "About",
|
"text": "About",
|
||||||
"icon": "info"
|
"icon": "info"
|
||||||
|
|||||||
@@ -266,7 +266,8 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getWidgetComponent(widgetId) {
|
function getWidgetComponent(widgetId) {
|
||||||
const componentMap = {
|
// Build dynamic component map including plugins
|
||||||
|
let baseMap = {
|
||||||
"launcherButton": "launcherButtonComponent",
|
"launcherButton": "launcherButtonComponent",
|
||||||
"workspaceSwitcher": "workspaceSwitcherComponent",
|
"workspaceSwitcher": "workspaceSwitcherComponent",
|
||||||
"focusedWindow": "focusedWindowComponent",
|
"focusedWindow": "focusedWindowComponent",
|
||||||
@@ -296,8 +297,15 @@ Item {
|
|||||||
"systemUpdate": "systemUpdateComponent"
|
"systemUpdate": "systemUpdateComponent"
|
||||||
}
|
}
|
||||||
|
|
||||||
const componentKey = componentMap[widgetId]
|
// For built-in components, get from components property
|
||||||
return componentKey ? root.components[componentKey] : null
|
const componentKey = baseMap[widgetId]
|
||||||
|
if (componentKey && root.components[componentKey]) {
|
||||||
|
return root.components[componentKey]
|
||||||
|
}
|
||||||
|
|
||||||
|
// For plugin components, get from PluginService
|
||||||
|
let pluginMap = PluginService.getWidgetComponents()
|
||||||
|
return pluginMap[widgetId] || null
|
||||||
}
|
}
|
||||||
|
|
||||||
height: parent.height
|
height: parent.height
|
||||||
@@ -337,6 +345,7 @@ Item {
|
|||||||
id: centerRepeater
|
id: centerRepeater
|
||||||
model: root.widgetsModel
|
model: root.widgetsModel
|
||||||
|
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
property string widgetId: model.widgetId
|
property string widgetId: model.widgetId
|
||||||
property var widgetData: model
|
property var widgetData: model
|
||||||
@@ -364,6 +373,17 @@ Item {
|
|||||||
if (root.axis && "isVertical" in item) {
|
if (root.axis && "isVertical" in item) {
|
||||||
item.isVertical = root.axis.isVertical
|
item.isVertical = root.axis.isVertical
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inject PluginService for plugin widgets
|
||||||
|
if (item.pluginService !== undefined) {
|
||||||
|
console.log("CenterSection: Injecting PluginService into plugin widget:", model.widgetId)
|
||||||
|
item.pluginService = PluginService
|
||||||
|
if (item.loadTimezones) {
|
||||||
|
console.log("CenterSection: Calling loadTimezones for widget:", model.widgetId)
|
||||||
|
item.loadTimezones()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
layoutTimer.restart()
|
layoutTimer.restart()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,4 +399,27 @@ Item {
|
|||||||
layoutTimer.restart()
|
layoutTimer.restart()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listen for plugin changes and refresh components
|
||||||
|
Connections {
|
||||||
|
target: PluginService
|
||||||
|
function onPluginLoaded(pluginId) {
|
||||||
|
// Force refresh of component lookups
|
||||||
|
for (var i = 0; i < centerRepeater.count; i++) {
|
||||||
|
var item = centerRepeater.itemAt(i)
|
||||||
|
if (item && item.widgetId === pluginId) {
|
||||||
|
item.sourceComponent = root.getWidgetComponent(pluginId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onPluginUnloaded(pluginId) {
|
||||||
|
// Force refresh of component lookups
|
||||||
|
for (var i = 0; i < centerRepeater.count; i++) {
|
||||||
|
var item = centerRepeater.itemAt(i)
|
||||||
|
if (item && item.widgetId === pluginId) {
|
||||||
|
item.sourceComponent = root.getWidgetComponent(pluginId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -95,6 +95,24 @@ Item {
|
|||||||
Qt.callLater(() => Qt.callLater(forceWidgetRefresh))
|
Qt.callLater(() => Qt.callLater(forceWidgetRefresh))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: PluginService
|
||||||
|
function onPluginLoaded(pluginId) {
|
||||||
|
console.log("DankBar: Plugin loaded:", pluginId)
|
||||||
|
// Force componentMap to update by triggering property change
|
||||||
|
if (topBarContent) {
|
||||||
|
topBarContent.updateComponentMap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onPluginUnloaded(pluginId) {
|
||||||
|
console.log("DankBar: Plugin unloaded:", pluginId)
|
||||||
|
// Force componentMap to update by triggering property change
|
||||||
|
if (topBarContent) {
|
||||||
|
topBarContent.updateComponentMap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function forceWidgetRefresh() {
|
function forceWidgetRefresh() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,7 +384,13 @@ Item {
|
|||||||
anchors.bottomMargin: !barWindow.isVertical ? SettingsData.dankBarInnerPadding / 2 : Math.max(Theme.spacingXS, SettingsData.dankBarInnerPadding * 0.8)
|
anchors.bottomMargin: !barWindow.isVertical ? SettingsData.dankBarInnerPadding / 2 : Math.max(Theme.spacingXS, SettingsData.dankBarInnerPadding * 0.8)
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
readonly property int availableWidth: width
|
property int componentMapRevision: 0
|
||||||
|
|
||||||
|
function updateComponentMap() {
|
||||||
|
componentMapRevision++
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property int availableWidth: width
|
||||||
readonly property int launcherButtonWidth: 40
|
readonly property int launcherButtonWidth: 40
|
||||||
readonly property int workspaceSwitcherWidth: 120
|
readonly property int workspaceSwitcherWidth: 120
|
||||||
readonly property int focusedAppMaxWidth: 456
|
readonly property int focusedAppMaxWidth: 456
|
||||||
@@ -421,35 +445,44 @@ Item {
|
|||||||
return widgetVisibility[widgetId] ?? true
|
return widgetVisibility[widgetId] ?? true
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property var componentMap: ({
|
readonly property var componentMap: {
|
||||||
"launcherButton": launcherButtonComponent,
|
// This property depends on componentMapRevision to ensure it updates when plugins change
|
||||||
"workspaceSwitcher": workspaceSwitcherComponent,
|
componentMapRevision;
|
||||||
"focusedWindow": focusedWindowComponent,
|
|
||||||
"runningApps": runningAppsComponent,
|
let baseMap = {
|
||||||
"clock": clockComponent,
|
"launcherButton": launcherButtonComponent,
|
||||||
"music": mediaComponent,
|
"workspaceSwitcher": workspaceSwitcherComponent,
|
||||||
"weather": weatherComponent,
|
"focusedWindow": focusedWindowComponent,
|
||||||
"systemTray": systemTrayComponent,
|
"runningApps": runningAppsComponent,
|
||||||
"privacyIndicator": privacyIndicatorComponent,
|
"clock": clockComponent,
|
||||||
"clipboard": clipboardComponent,
|
"music": mediaComponent,
|
||||||
"cpuUsage": cpuUsageComponent,
|
"weather": weatherComponent,
|
||||||
"memUsage": memUsageComponent,
|
"systemTray": systemTrayComponent,
|
||||||
"diskUsage": diskUsageComponent,
|
"privacyIndicator": privacyIndicatorComponent,
|
||||||
"cpuTemp": cpuTempComponent,
|
"clipboard": clipboardComponent,
|
||||||
"gpuTemp": gpuTempComponent,
|
"cpuUsage": cpuUsageComponent,
|
||||||
"notificationButton": notificationButtonComponent,
|
"memUsage": memUsageComponent,
|
||||||
"battery": batteryComponent,
|
"diskUsage": diskUsageComponent,
|
||||||
"controlCenterButton": controlCenterButtonComponent,
|
"cpuTemp": cpuTempComponent,
|
||||||
"idleInhibitor": idleInhibitorComponent,
|
"gpuTemp": gpuTempComponent,
|
||||||
"spacer": spacerComponent,
|
"notificationButton": notificationButtonComponent,
|
||||||
"separator": separatorComponent,
|
"battery": batteryComponent,
|
||||||
"network_speed_monitor": networkComponent,
|
"controlCenterButton": controlCenterButtonComponent,
|
||||||
"keyboard_layout_name": keyboardLayoutNameComponent,
|
"idleInhibitor": idleInhibitorComponent,
|
||||||
"vpn": vpnComponent,
|
"spacer": spacerComponent,
|
||||||
"notepadButton": notepadButtonComponent,
|
"separator": separatorComponent,
|
||||||
"colorPicker": colorPickerComponent,
|
"network_speed_monitor": networkComponent,
|
||||||
"systemUpdate": systemUpdateComponent
|
"keyboard_layout_name": keyboardLayoutNameComponent,
|
||||||
})
|
"vpn": vpnComponent,
|
||||||
|
"notepadButton": notepadButtonComponent,
|
||||||
|
"colorPicker": colorPickerComponent,
|
||||||
|
"systemUpdate": systemUpdateComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge with plugin widgets
|
||||||
|
let pluginMap = PluginService.getWidgetComponents()
|
||||||
|
return Object.assign(baseMap, pluginMap)
|
||||||
|
}
|
||||||
|
|
||||||
function getWidgetComponent(widgetId) {
|
function getWidgetComponent(widgetId) {
|
||||||
return componentMap[widgetId] || null
|
return componentMap[widgetId] || null
|
||||||
@@ -1016,6 +1049,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,10 +33,21 @@ Loader {
|
|||||||
if (axis && "isVertical" in item) {
|
if (axis && "isVertical" in item) {
|
||||||
item.isVertical = axis.isVertical
|
item.isVertical = axis.isVertical
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inject PluginService for plugin widgets
|
||||||
|
if (item.pluginService !== undefined) {
|
||||||
|
console.log("WidgetHost: Injecting PluginService into plugin widget:", widgetId)
|
||||||
|
item.pluginService = PluginService
|
||||||
|
if (item.loadTimezones) {
|
||||||
|
console.log("WidgetHost: Calling loadTimezones for widget:", widgetId)
|
||||||
|
item.loadTimezones()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWidgetComponent(widgetId, components) {
|
function getWidgetComponent(widgetId, components) {
|
||||||
|
// Build component map for built-in widgets
|
||||||
const componentMap = {
|
const componentMap = {
|
||||||
"launcherButton": components.launcherButtonComponent,
|
"launcherButton": components.launcherButtonComponent,
|
||||||
"workspaceSwitcher": components.workspaceSwitcherComponent,
|
"workspaceSwitcher": components.workspaceSwitcherComponent,
|
||||||
@@ -67,7 +78,14 @@ Loader {
|
|||||||
"systemUpdate": components.systemUpdateComponent
|
"systemUpdate": components.systemUpdateComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
return componentMap[widgetId] || null
|
// Check for built-in component first
|
||||||
|
if (componentMap[widgetId]) {
|
||||||
|
return componentMap[widgetId]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for plugin component
|
||||||
|
let pluginMap = PluginService.getWidgetComponents()
|
||||||
|
return pluginMap[widgetId] || null
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWidgetVisible(widgetId, dgopAvailable) {
|
function getWidgetVisible(widgetId, dgopAvailable) {
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import qs.Widgets
|
|||||||
Item {
|
Item {
|
||||||
id: dankBarTab
|
id: dankBarTab
|
||||||
|
|
||||||
property var baseWidgetDefinitions: [{
|
property var baseWidgetDefinitions: {
|
||||||
|
var coreWidgets = [{
|
||||||
"id": "launcherButton",
|
"id": "launcherButton",
|
||||||
"text": "App Launcher",
|
"text": "App Launcher",
|
||||||
"description": "Quick access to application launcher",
|
"description": "Quick access to application launcher",
|
||||||
@@ -177,6 +178,22 @@ Item {
|
|||||||
"icon": "update",
|
"icon": "update",
|
||||||
"enabled": SystemUpdateService.distributionSupported
|
"enabled": SystemUpdateService.distributionSupported
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
// Add plugin widgets dynamically
|
||||||
|
var loadedPlugins = PluginService.getLoadedPlugins()
|
||||||
|
for (var i = 0; i < loadedPlugins.length; i++) {
|
||||||
|
var plugin = loadedPlugins[i]
|
||||||
|
coreWidgets.push({
|
||||||
|
"id": plugin.id,
|
||||||
|
"text": plugin.name,
|
||||||
|
"description": plugin.description || "Plugin widget",
|
||||||
|
"icon": plugin.icon || "extension",
|
||||||
|
"enabled": true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return coreWidgets
|
||||||
|
}
|
||||||
property var defaultLeftWidgets: [{
|
property var defaultLeftWidgets: [{
|
||||||
"id": "launcherButton",
|
"id": "launcherButton",
|
||||||
"enabled": true
|
"enabled": true
|
||||||
|
|||||||
552
Modules/Settings/PluginsTab.qml
Normal file
552
Modules/Settings/PluginsTab.qml
Normal file
@@ -0,0 +1,552 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: pluginsTab
|
||||||
|
|
||||||
|
property string expandedPluginId: ""
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
console.log("PluginsTab: Component completed")
|
||||||
|
console.log("PluginsTab: PluginService available:", typeof PluginService !== "undefined")
|
||||||
|
if (typeof PluginService !== "undefined") {
|
||||||
|
console.log("PluginsTab: Available plugins:", Object.keys(PluginService.availablePlugins).length)
|
||||||
|
console.log("PluginsTab: Plugin directory:", PluginService.pluginDirectory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.topMargin: Theme.spacingL
|
||||||
|
clip: true
|
||||||
|
contentHeight: mainColumn.height
|
||||||
|
contentWidth: width
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: mainColumn
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXL
|
||||||
|
|
||||||
|
// Header Section
|
||||||
|
StyledRect {
|
||||||
|
width: parent.width
|
||||||
|
height: headerColumn.implicitHeight + Theme.spacingL * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: headerColumn
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "extension"
|
||||||
|
size: Theme.iconSizeLarge
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Plugin Management"
|
||||||
|
font.pixelSize: Theme.fontSizeXLarge
|
||||||
|
color: "#FFFFFF"
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Manage and configure plugins for extending DMS functionality"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: "#CCCCCC"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "Scan for Plugins"
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? "#4A90E2" : "#3A3A3A"
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.color: "#666666"
|
||||||
|
border.width: 1
|
||||||
|
}
|
||||||
|
contentItem: Text {
|
||||||
|
text: parent.text
|
||||||
|
color: "#FFFFFF"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
onClicked: {
|
||||||
|
PluginService.scanPlugins()
|
||||||
|
ToastService.showInfo("Scanning for plugins...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "Create Plugin Directory"
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? "#4A90E2" : "#3A3A3A"
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.color: "#666666"
|
||||||
|
border.width: 1
|
||||||
|
}
|
||||||
|
contentItem: Text {
|
||||||
|
text: parent.text
|
||||||
|
color: "#FFFFFF"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
onClicked: {
|
||||||
|
PluginService.createPluginDirectory()
|
||||||
|
ToastService.showInfo("Created plugin directory: " + PluginService.pluginDirectory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugin Directory Info
|
||||||
|
StyledRect {
|
||||||
|
width: parent.width
|
||||||
|
height: directoryColumn.implicitHeight + Theme.spacingL * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: directoryColumn
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Plugin Directory"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: "#FFFFFF"
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: PluginService.pluginDirectory
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: "#CCCCCC"
|
||||||
|
font.family: "monospace"
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Place plugin directories here. Each plugin should have a plugin.json manifest file."
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: "#CCCCCC"
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Available Plugins Section
|
||||||
|
StyledRect {
|
||||||
|
width: parent.width
|
||||||
|
height: Math.max(200, availableColumn.implicitHeight + Theme.spacingL * 2)
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: availableColumn
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Available Plugins"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: "#FFFFFF"
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: Math.max(150, pluginListView.contentHeight)
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: pluginListView
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
model: PluginService.getAvailablePlugins()
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
delegate: StyledRect {
|
||||||
|
id: pluginDelegate
|
||||||
|
width: pluginListView.width
|
||||||
|
height: pluginItemColumn.implicitHeight + Theme.spacingM * 2 + settingsContainer.height
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
|
||||||
|
// Store plugin data in properties to avoid scope issues
|
||||||
|
property var pluginData: modelData
|
||||||
|
property string pluginId: pluginData ? pluginData.id : ""
|
||||||
|
property string pluginName: pluginData ? (pluginData.name || pluginData.id) : ""
|
||||||
|
property string pluginVersion: pluginData ? (pluginData.version || "1.0.0") : ""
|
||||||
|
property string pluginAuthor: pluginData ? (pluginData.author || "Unknown") : ""
|
||||||
|
property string pluginDescription: pluginData ? (pluginData.description || "") : ""
|
||||||
|
property string pluginIcon: pluginData ? (pluginData.icon || "extension") : "extension"
|
||||||
|
property string pluginSettingsPath: pluginData ? (pluginData.settingsPath || "") : ""
|
||||||
|
property var pluginPermissions: pluginData ? (pluginData.permissions || []) : []
|
||||||
|
property bool hasSettings: pluginData && pluginData.settings !== undefined && pluginData.settings !== ""
|
||||||
|
property bool isExpanded: pluginsTab.expandedPluginId === pluginId
|
||||||
|
|
||||||
|
onIsExpandedChanged: {
|
||||||
|
console.log("Plugin", pluginId, "isExpanded changed to:", isExpanded)
|
||||||
|
}
|
||||||
|
|
||||||
|
color: pluginMouseArea.containsMouse ? Theme.surfacePressed : (isExpanded ? Theme.surfaceContainerHigh : Theme.surfaceContainer)
|
||||||
|
border.color: isExpanded ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||||
|
border.width: isExpanded ? 2 : 1
|
||||||
|
|
||||||
|
Behavior on height {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: pluginMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.bottomMargin: pluginDelegate.isExpanded ? settingsContainer.height : 0
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: pluginDelegate.hasSettings ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
onClicked: {
|
||||||
|
console.log("Plugin clicked:", pluginDelegate.pluginId, "hasSettings:", pluginDelegate.hasSettings, "isLoaded:", PluginService.isPluginLoaded(pluginDelegate.pluginId))
|
||||||
|
if (pluginDelegate.hasSettings) {
|
||||||
|
if (pluginsTab.expandedPluginId === pluginDelegate.pluginId) {
|
||||||
|
console.log("Collapsing plugin:", pluginDelegate.pluginId)
|
||||||
|
pluginsTab.expandedPluginId = ""
|
||||||
|
} else {
|
||||||
|
console.log("Expanding plugin:", pluginDelegate.pluginId)
|
||||||
|
pluginsTab.expandedPluginId = pluginDelegate.pluginId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: pluginItemColumn
|
||||||
|
width: parent.width
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: pluginDelegate.pluginIcon
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: PluginService.isPluginLoaded(pluginDelegate.pluginId) ? Theme.primary : "#CCCCCC"
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 2
|
||||||
|
width: parent.width - 250
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: pluginDelegate.pluginName
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: "#FFFFFF"
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: pluginDelegate.hasSettings ? (pluginDelegate.isExpanded ? "expand_less" : "expand_more") : ""
|
||||||
|
size: 16
|
||||||
|
color: pluginDelegate.hasSettings ? Theme.primary : "transparent"
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: pluginDelegate.hasSettings
|
||||||
|
|
||||||
|
Behavior on rotation {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "v" + pluginDelegate.pluginVersion + " by " + pluginDelegate.pluginAuthor
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: "#CCCCCC"
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 10
|
||||||
|
height: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
DankToggle {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
checked: PluginService.isPluginLoaded(pluginDelegate.pluginId)
|
||||||
|
onToggled: (isChecked) => {
|
||||||
|
if (isChecked) {
|
||||||
|
if (PluginService.enablePlugin(pluginDelegate.pluginId)) {
|
||||||
|
ToastService.showInfo("Plugin enabled: " + pluginDelegate.pluginName)
|
||||||
|
} else {
|
||||||
|
ToastService.showError("Failed to enable plugin: " + pluginDelegate.pluginName)
|
||||||
|
checked = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (PluginService.disablePlugin(pluginDelegate.pluginId)) {
|
||||||
|
ToastService.showInfo("Plugin disabled: " + pluginDelegate.pluginName)
|
||||||
|
if (pluginsTab.expandedPluginId === pluginDelegate.pluginId) {
|
||||||
|
pluginsTab.expandedPluginId = ""
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ToastService.showError("Failed to disable plugin: " + pluginDelegate.pluginName)
|
||||||
|
checked = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: pluginDelegate.pluginDescription
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: "#CCCCCC"
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
visible: pluginDelegate.pluginDescription !== ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
visible: pluginDelegate.pluginPermissions && Array.isArray(pluginDelegate.pluginPermissions) && pluginDelegate.pluginPermissions.length > 0
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: pluginDelegate.pluginPermissions
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
height: 20
|
||||||
|
width: permissionText.implicitWidth + Theme.spacingXS * 2
|
||||||
|
radius: 10
|
||||||
|
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||||
|
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: permissionText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: modelData
|
||||||
|
font.pixelSize: Theme.fontSizeSmall - 1
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings container
|
||||||
|
Item {
|
||||||
|
id: settingsContainer
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
height: pluginDelegate.isExpanded && pluginDelegate.hasSettings ? Math.min(500, settingsLoader.item ? settingsLoader.item.implicitHeight + Theme.spacingL * 2 : 200) : 0
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
onHeightChanged: {
|
||||||
|
console.log("Settings container height changed:", height, "for plugin:", pluginDelegate.pluginId)
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on height {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Qt.rgba(Theme.surfaceContainerHighest.r, Theme.surfaceContainerHighest.g, Theme.surfaceContainerHighest.b, 0.5)
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
anchors.topMargin: Theme.spacingXS
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
|
||||||
|
border.width: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
contentHeight: settingsLoader.height
|
||||||
|
contentWidth: width
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: settingsLoader
|
||||||
|
width: parent.width
|
||||||
|
active: pluginDelegate.isExpanded && pluginDelegate.hasSettings && PluginService.isPluginLoaded(pluginDelegate.pluginId)
|
||||||
|
asynchronous: false
|
||||||
|
|
||||||
|
onActiveChanged: {
|
||||||
|
console.log("Settings loader active changed to:", active, "for plugin:", pluginDelegate.pluginId,
|
||||||
|
"isExpanded:", pluginDelegate.isExpanded, "hasSettings:", pluginDelegate.hasSettings,
|
||||||
|
"isLoaded:", PluginService.isPluginLoaded(pluginDelegate.pluginId))
|
||||||
|
}
|
||||||
|
|
||||||
|
source: {
|
||||||
|
if (active && pluginDelegate.pluginSettingsPath) {
|
||||||
|
console.log("Loading plugin settings from:", pluginDelegate.pluginSettingsPath)
|
||||||
|
var path = pluginDelegate.pluginSettingsPath
|
||||||
|
if (!path.startsWith("file://")) {
|
||||||
|
path = "file://" + path
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
onStatusChanged: {
|
||||||
|
console.log("Settings loader status changed:", status, "for plugin:", pluginDelegate.pluginId)
|
||||||
|
if (status === Loader.Error) {
|
||||||
|
console.error("Failed to load plugin settings:", pluginDelegate.pluginSettingsPath)
|
||||||
|
} else if (status === Loader.Ready) {
|
||||||
|
console.log("Settings successfully loaded for plugin:", pluginDelegate.pluginId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoaded: {
|
||||||
|
if (item) {
|
||||||
|
console.log("Plugin settings loaded for:", pluginDelegate.pluginId)
|
||||||
|
|
||||||
|
// Make PluginService available to the loaded component
|
||||||
|
if (typeof PluginService !== "undefined") {
|
||||||
|
console.log("Making PluginService available to plugin settings")
|
||||||
|
console.log("PluginService functions available:",
|
||||||
|
"savePluginData" in PluginService,
|
||||||
|
"loadPluginData" in PluginService)
|
||||||
|
item.pluginService = PluginService
|
||||||
|
console.log("PluginService assignment completed, item.pluginService:", item.pluginService !== null)
|
||||||
|
} else {
|
||||||
|
console.error("PluginService not available in PluginsTab context")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to height changes for dynamic resizing
|
||||||
|
if (item.implicitHeightChanged) {
|
||||||
|
item.implicitHeightChanged.connect(function() {
|
||||||
|
console.log("Plugin settings height changed:", item.implicitHeight)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force load timezones for WorldClock plugin
|
||||||
|
if (item.loadTimezones) {
|
||||||
|
console.log("Calling loadTimezones for WorldClock plugin")
|
||||||
|
item.loadTimezones()
|
||||||
|
}
|
||||||
|
// Generic initialization for any plugin
|
||||||
|
if (item.initializeSettings) {
|
||||||
|
item.initializeSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: !PluginService.isPluginLoaded(pluginDelegate.pluginId) ?
|
||||||
|
"Enable plugin to access settings" :
|
||||||
|
(settingsLoader.status === Loader.Error ?
|
||||||
|
"Failed to load settings" :
|
||||||
|
"No configurable settings")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: "#CCCCCC"
|
||||||
|
visible: pluginDelegate.isExpanded && (!settingsLoader.active || settingsLoader.status === Loader.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "No plugins found.\nPlace plugins in " + PluginService.pluginDirectory
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: "#CCCCCC"
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
visible: pluginListView.model.length === 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connections to update plugin list when plugins are loaded/unloaded
|
||||||
|
Connections {
|
||||||
|
target: PluginService
|
||||||
|
function onPluginLoaded() {
|
||||||
|
pluginListView.model = PluginService.getAvailablePlugins()
|
||||||
|
}
|
||||||
|
function onPluginUnloaded() {
|
||||||
|
pluginListView.model = PluginService.getAvailablePlugins()
|
||||||
|
// Close expanded plugin if it was unloaded
|
||||||
|
if (pluginsTab.expandedPluginId !== "" && !PluginService.isPluginLoaded(pluginsTab.expandedPluginId)) {
|
||||||
|
pluginsTab.expandedPluginId = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
299
Services/PluginService.qml
Normal file
299
Services/PluginService.qml
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtCore
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var availablePlugins: ({})
|
||||||
|
property var loadedPlugins: ({})
|
||||||
|
property var pluginWidgetComponents: ({})
|
||||||
|
property string pluginDirectory: {
|
||||||
|
var configDir = StandardPaths.writableLocation(StandardPaths.ConfigLocation)
|
||||||
|
var configDirStr = configDir.toString()
|
||||||
|
if (configDirStr.startsWith("file://")) {
|
||||||
|
configDirStr = configDirStr.substring(7)
|
||||||
|
}
|
||||||
|
return configDirStr + "/DankMaterialShell/plugins"
|
||||||
|
}
|
||||||
|
|
||||||
|
signal pluginLoaded(string pluginId)
|
||||||
|
signal pluginUnloaded(string pluginId)
|
||||||
|
signal pluginLoadFailed(string pluginId, string error)
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
Qt.callLater(initializePlugins)
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializePlugins() {
|
||||||
|
scanPlugins()
|
||||||
|
}
|
||||||
|
|
||||||
|
property var lsProcess: Process {
|
||||||
|
id: dirScanner
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
var output = text.trim()
|
||||||
|
if (output) {
|
||||||
|
var directories = output.split('\n')
|
||||||
|
for (var i = 0; i < directories.length; i++) {
|
||||||
|
var dir = directories[i].trim()
|
||||||
|
if (dir) {
|
||||||
|
var manifestPath = pluginDirectory + "/" + dir + "/plugin.json"
|
||||||
|
console.log("PluginService: Found plugin directory:", dir, "checking manifest at:", manifestPath)
|
||||||
|
loadPluginManifest(manifestPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("PluginService: No directories found in plugin directory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: function(exitCode) {
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
console.error("PluginService: Failed to scan plugin directory, exit code:", exitCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanPlugins() {
|
||||||
|
lsProcess.command = ["find", pluginDirectory, "-maxdepth", "1", "-type", "d", "-not", "-path", pluginDirectory, "-exec", "basename", "{}", ";"]
|
||||||
|
lsProcess.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
property var manifestReaders: ({})
|
||||||
|
|
||||||
|
function loadPluginManifest(manifestPath) {
|
||||||
|
console.log("PluginService: Loading manifest:", manifestPath)
|
||||||
|
|
||||||
|
// Create a unique key for this manifest reader
|
||||||
|
var readerId = "reader_" + Date.now() + "_" + Math.random()
|
||||||
|
|
||||||
|
var catProcess = Qt.createComponent("data:text/plain,import Quickshell.Io; Process { stdout: StdioCollector { } }")
|
||||||
|
if (catProcess.status === Component.Ready) {
|
||||||
|
var process = catProcess.createObject(root)
|
||||||
|
process.command = ["cat", manifestPath]
|
||||||
|
process.stdout.streamFinished.connect(function() {
|
||||||
|
try {
|
||||||
|
console.log("PluginService: DEBUGGING parsing manifest, text length:", process.stdout.text.length)
|
||||||
|
var manifest = JSON.parse(process.stdout.text.trim())
|
||||||
|
console.log("PluginService: Successfully parsed manifest for plugin:", manifest.id)
|
||||||
|
processManifest(manifest, manifestPath)
|
||||||
|
} catch (e) {
|
||||||
|
console.error("PluginService: Failed to parse manifest", manifestPath, ":", e.message)
|
||||||
|
}
|
||||||
|
process.destroy()
|
||||||
|
delete manifestReaders[readerId]
|
||||||
|
})
|
||||||
|
process.exited.connect(function(exitCode) {
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
console.error("PluginService: Failed to read manifest file:", manifestPath, "exit code:", exitCode)
|
||||||
|
process.destroy()
|
||||||
|
delete manifestReaders[readerId]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
manifestReaders[readerId] = process
|
||||||
|
process.running = true
|
||||||
|
} else {
|
||||||
|
console.error("PluginService: Failed to create manifest reader process")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processManifest(manifest, manifestPath) {
|
||||||
|
registerPlugin(manifest, manifestPath)
|
||||||
|
|
||||||
|
// Auto-load plugin if it's enabled in settings (default to enabled)
|
||||||
|
var enabled = SettingsData.getPluginSetting(manifest.id, "enabled", true)
|
||||||
|
if (enabled) {
|
||||||
|
loadPlugin(manifest.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerPlugin(manifest, manifestPath) {
|
||||||
|
console.log("PluginService: registerPlugin called with", manifest.id)
|
||||||
|
if (!manifest.id || !manifest.name || !manifest.component) {
|
||||||
|
console.error("PluginService: Invalid manifest, missing required fields:", manifestPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var pluginDir = manifestPath.substring(0, manifestPath.lastIndexOf('/'))
|
||||||
|
|
||||||
|
// Clean up relative paths by removing './' prefix
|
||||||
|
var componentFile = manifest.component
|
||||||
|
if (componentFile.startsWith('./')) {
|
||||||
|
componentFile = componentFile.substring(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
var settingsFile = manifest.settings
|
||||||
|
if (settingsFile && settingsFile.startsWith('./')) {
|
||||||
|
settingsFile = settingsFile.substring(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
var pluginInfo = {}
|
||||||
|
for (var key in manifest) {
|
||||||
|
pluginInfo[key] = manifest[key]
|
||||||
|
}
|
||||||
|
pluginInfo.manifestPath = manifestPath
|
||||||
|
pluginInfo.pluginDirectory = pluginDir
|
||||||
|
pluginInfo.componentPath = pluginDir + '/' + componentFile
|
||||||
|
pluginInfo.settingsPath = settingsFile ? pluginDir + '/' + settingsFile : null
|
||||||
|
pluginInfo.loaded = false
|
||||||
|
|
||||||
|
availablePlugins[manifest.id] = pluginInfo
|
||||||
|
console.log("PluginService: Registered plugin:", manifest.id, "-", manifest.name)
|
||||||
|
console.log("PluginService: Component path:", pluginInfo.componentPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPlugin(pluginId) {
|
||||||
|
console.log("PluginService: loadPlugin called for", pluginId)
|
||||||
|
var plugin = availablePlugins[pluginId]
|
||||||
|
if (!plugin) {
|
||||||
|
console.error("PluginService: Plugin not found:", pluginId)
|
||||||
|
pluginLoadFailed(pluginId, "Plugin not found")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin.loaded) {
|
||||||
|
console.log("PluginService: Plugin already loaded:", pluginId)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create the widget component
|
||||||
|
var componentUrl = "file://" + plugin.componentPath
|
||||||
|
console.log("PluginService: Loading component from:", componentUrl)
|
||||||
|
|
||||||
|
var component = Qt.createComponent(componentUrl)
|
||||||
|
if (component.status === Component.Error) {
|
||||||
|
console.error("PluginService: Failed to create component for plugin:", pluginId, "Error:", component.errorString())
|
||||||
|
pluginLoadFailed(pluginId, component.errorString())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginWidgetComponents[pluginId] = component
|
||||||
|
plugin.loaded = true
|
||||||
|
loadedPlugins[pluginId] = plugin
|
||||||
|
|
||||||
|
console.log("PluginService: Successfully loaded plugin:", pluginId)
|
||||||
|
pluginLoaded(pluginId)
|
||||||
|
return true
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("PluginService: Error loading plugin:", pluginId, "Error:", error.message)
|
||||||
|
pluginLoadFailed(pluginId, error.message)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unloadPlugin(pluginId) {
|
||||||
|
var plugin = loadedPlugins[pluginId]
|
||||||
|
if (!plugin) {
|
||||||
|
console.warn("PluginService: Plugin not loaded:", pluginId)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Remove from component map
|
||||||
|
delete pluginWidgetComponents[pluginId]
|
||||||
|
|
||||||
|
// Mark as unloaded
|
||||||
|
plugin.loaded = false
|
||||||
|
delete loadedPlugins[pluginId]
|
||||||
|
|
||||||
|
console.log("PluginService: Successfully unloaded plugin:", pluginId)
|
||||||
|
pluginUnloaded(pluginId)
|
||||||
|
return true
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("PluginService: Error unloading plugin:", pluginId, "Error:", error.message)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWidgetComponents() {
|
||||||
|
return pluginWidgetComponents
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAvailablePlugins() {
|
||||||
|
var result = []
|
||||||
|
for (var key in availablePlugins) {
|
||||||
|
result.push(availablePlugins[key])
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLoadedPlugins() {
|
||||||
|
var result = []
|
||||||
|
for (var key in loadedPlugins) {
|
||||||
|
result.push(loadedPlugins[key])
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPluginLoaded(pluginId) {
|
||||||
|
return loadedPlugins[pluginId] !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function enablePlugin(pluginId) {
|
||||||
|
console.log("PluginService: Enabling plugin:", pluginId)
|
||||||
|
SettingsData.setPluginSetting(pluginId, "enabled", true)
|
||||||
|
return loadPlugin(pluginId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function disablePlugin(pluginId) {
|
||||||
|
console.log("PluginService: Disabling plugin:", pluginId)
|
||||||
|
SettingsData.setPluginSetting(pluginId, "enabled", false)
|
||||||
|
return unloadPlugin(pluginId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function reloadPlugin(pluginId) {
|
||||||
|
if (isPluginLoaded(pluginId)) {
|
||||||
|
unloadPlugin(pluginId)
|
||||||
|
}
|
||||||
|
return loadPlugin(pluginId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function savePluginData(pluginId, key, value) {
|
||||||
|
console.log("PluginService: Saving plugin data:", pluginId, key, JSON.stringify(value))
|
||||||
|
SettingsData.setPluginSetting(pluginId, key, value)
|
||||||
|
console.log("PluginService: Data saved successfully")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPluginData(pluginId, key, defaultValue) {
|
||||||
|
console.log("PluginService: Loading plugin data:", pluginId, key)
|
||||||
|
var value = SettingsData.getPluginSetting(pluginId, key, defaultValue)
|
||||||
|
console.log("PluginService: Loaded key:", key, "value:", JSON.stringify(value))
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPluginDirectory() {
|
||||||
|
console.log("PluginService: Creating plugin directory:", pluginDirectory)
|
||||||
|
var mkdirProcess = Qt.createComponent("data:text/plain,import Quickshell.Io; Process { }")
|
||||||
|
if (mkdirProcess.status === Component.Ready) {
|
||||||
|
var process = mkdirProcess.createObject(root)
|
||||||
|
process.command = ["mkdir", "-p", pluginDirectory]
|
||||||
|
process.exited.connect(function(exitCode) {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
console.log("PluginService: Successfully created plugin directory")
|
||||||
|
} else {
|
||||||
|
console.error("PluginService: Failed to create plugin directory, exit code:", exitCode)
|
||||||
|
}
|
||||||
|
process.destroy()
|
||||||
|
})
|
||||||
|
process.running = true
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
console.error("PluginService: Failed to create mkdir process")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
456
docs/PLUGINS.md
Normal file
456
docs/PLUGINS.md
Normal file
@@ -0,0 +1,456 @@
|
|||||||
|
# Plugin System
|
||||||
|
|
||||||
|
The DMS shell includes an experimental plugin system that allows extending functionality through self-contained, dynamically-loaded QML components.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The plugin system enables developers to create custom widgets that can be displayed in the DankBar alongside built-in widgets. Plugins are discovered, loaded, and managed through the **PluginService**, providing a clean separation between core shell functionality and user extensions.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
1. **PluginService** (`Services/PluginService.qml`)
|
||||||
|
- Singleton service managing plugin lifecycle
|
||||||
|
- Discovers plugins from `$CONFIGPATH/DankMaterialShell/plugins/`
|
||||||
|
- Handles loading, unloading, and state management
|
||||||
|
- Provides data persistence for plugin settings
|
||||||
|
|
||||||
|
2. **PluginsTab** (`Modules/Settings/PluginsTab.qml`)
|
||||||
|
- UI for managing available plugins
|
||||||
|
- Access plugin settings
|
||||||
|
|
||||||
|
3. **PluginsTab Settings** (`Modules/Settings/PluginsTab.qml`)
|
||||||
|
- Accordion-style plugin configuration interface
|
||||||
|
- Dynamically loads plugin settings components inline
|
||||||
|
- Provides consistent settings interface with proper focus handling
|
||||||
|
|
||||||
|
4. **DankBar Integration** (`Modules/DankBar/DankBar.qml`)
|
||||||
|
- Renders plugin widgets in the bar
|
||||||
|
- Merges plugin components with built-in widgets
|
||||||
|
- Supports left, center, and right sections
|
||||||
|
|
||||||
|
## Plugin Structure
|
||||||
|
|
||||||
|
Each plugin must be a directory in `$CONFIGPATH/DankMaterialShell/plugins/` containing:
|
||||||
|
|
||||||
|
```
|
||||||
|
$CONFIGPATH/DankMaterialShell/plugins/YourPlugin/
|
||||||
|
├── plugin.json # Required: Plugin manifest
|
||||||
|
├── YourWidget.qml # Required: Widget component
|
||||||
|
├── YourSettings.qml # Optional: Settings UI
|
||||||
|
├── qmldir # Optional: QML module definition
|
||||||
|
└── *.js # Optional: JavaScript utilities
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plugin Manifest (plugin.json)
|
||||||
|
|
||||||
|
The manifest file defines plugin metadata and configuration:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "yourPlugin",
|
||||||
|
"name": "Your Plugin Name",
|
||||||
|
"description": "Brief description of what your plugin does",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "Your Name",
|
||||||
|
"icon": "material_icon_name",
|
||||||
|
"component": "./YourWidget.qml",
|
||||||
|
"settings": "./YourSettings.qml",
|
||||||
|
"dependencies": {
|
||||||
|
"libraryName": {
|
||||||
|
"url": "https://cdn.example.com/library.js",
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings_schema": {
|
||||||
|
"settingKey": {
|
||||||
|
"type": "string|number|boolean|array|object",
|
||||||
|
"default": "defaultValue"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"permissions": [
|
||||||
|
"settings_read",
|
||||||
|
"settings_write"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Required Fields:**
|
||||||
|
- `id`: Unique plugin identifier (camelCase, no spaces)
|
||||||
|
- `name`: Human-readable plugin name
|
||||||
|
- `component`: Relative path to widget QML file
|
||||||
|
|
||||||
|
**Optional Fields:**
|
||||||
|
- `description`: Short description of plugin functionality
|
||||||
|
- `version`: Semantic version string
|
||||||
|
- `author`: Plugin creator name
|
||||||
|
- `icon`: Material Design icon name
|
||||||
|
- `settings`: Path to settings component
|
||||||
|
- `dependencies`: External JS libraries
|
||||||
|
- `settings_schema`: Configuration schema
|
||||||
|
- `permissions`: Required capabilities
|
||||||
|
|
||||||
|
### Widget Component
|
||||||
|
|
||||||
|
The main widget component is displayed in the DankBar. It receives several properties from the shell:
|
||||||
|
|
||||||
|
```qml
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Standard properties provided by DankBar
|
||||||
|
property bool compactMode: false
|
||||||
|
property string section: "center" // "left", "center", or "right"
|
||||||
|
property var popupTarget: null
|
||||||
|
property var parentScreen: null
|
||||||
|
property real barHeight: 48
|
||||||
|
property real widgetHeight: 30
|
||||||
|
|
||||||
|
// Widget dimensions
|
||||||
|
width: content.implicitWidth + horizontalPadding * 2
|
||||||
|
height: widgetHeight
|
||||||
|
|
||||||
|
// PluginService is injected by PluginsTab when loading settings
|
||||||
|
property var pluginService
|
||||||
|
|
||||||
|
// Access plugin data
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (pluginService) {
|
||||||
|
var savedData = pluginService.loadPluginData("yourPlugin", "dataKey", defaultValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save plugin data
|
||||||
|
function saveData(key, value) {
|
||||||
|
if (pluginService) {
|
||||||
|
pluginService.savePluginData("yourPlugin", key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important Properties:**
|
||||||
|
- `compactMode`: Whether the bar is in compact display mode
|
||||||
|
- `section`: Which bar section the widget is in
|
||||||
|
- `barHeight`: Height of the entire bar
|
||||||
|
- `widgetHeight`: Recommended widget height
|
||||||
|
- `parentScreen`: Reference to the screen object
|
||||||
|
|
||||||
|
### Settings Component
|
||||||
|
|
||||||
|
Optional settings UI loaded inline in the PluginsTab accordion interface:
|
||||||
|
|
||||||
|
```qml
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// PluginService is injected by PluginsTab
|
||||||
|
property var pluginService
|
||||||
|
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: settingInput
|
||||||
|
width: parent.width
|
||||||
|
label: "Setting Label"
|
||||||
|
text: pluginService ? pluginService.loadPluginData("yourPlugin", "settingKey", "default") : ""
|
||||||
|
onTextChanged: {
|
||||||
|
if (pluginService) {
|
||||||
|
pluginService.savePluginData("yourPlugin", "settingKey", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankToggle {
|
||||||
|
checked: pluginService ? pluginService.loadPluginData("yourPlugin", "enabled", true) : false
|
||||||
|
onToggled: {
|
||||||
|
if (pluginService) {
|
||||||
|
pluginService.savePluginData("yourPlugin", "enabled", checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## PluginService API
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
```qml
|
||||||
|
PluginService.pluginDirectory: string
|
||||||
|
// Path to plugins directory ($CONFIGPATH/DankMaterialShell/plugins)
|
||||||
|
|
||||||
|
PluginService.availablePlugins: object
|
||||||
|
// Map of all discovered plugins {pluginId: pluginInfo}
|
||||||
|
|
||||||
|
PluginService.loadedPlugins: object
|
||||||
|
// Map of currently loaded plugins {pluginId: pluginInfo}
|
||||||
|
|
||||||
|
PluginService.pluginWidgetComponents: object
|
||||||
|
// Map of loaded widget components {pluginId: Component}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Functions
|
||||||
|
|
||||||
|
```qml
|
||||||
|
// Plugin Management
|
||||||
|
PluginService.loadPlugin(pluginId: string): bool
|
||||||
|
PluginService.unloadPlugin(pluginId: string): bool
|
||||||
|
PluginService.reloadPlugin(pluginId: string): bool
|
||||||
|
PluginService.enablePlugin(pluginId: string): bool
|
||||||
|
PluginService.disablePlugin(pluginId: string): bool
|
||||||
|
|
||||||
|
// Plugin Discovery
|
||||||
|
PluginService.scanPlugins(): void
|
||||||
|
PluginService.getAvailablePlugins(): array
|
||||||
|
PluginService.getLoadedPlugins(): array
|
||||||
|
PluginService.isPluginLoaded(pluginId: string): bool
|
||||||
|
PluginService.getWidgetComponents(): object
|
||||||
|
|
||||||
|
// Data Persistence
|
||||||
|
PluginService.savePluginData(pluginId: string, key: string, value: any): bool
|
||||||
|
PluginService.loadPluginData(pluginId: string, key: string, defaultValue: any): any
|
||||||
|
```
|
||||||
|
|
||||||
|
### Signals
|
||||||
|
|
||||||
|
```qml
|
||||||
|
PluginService.pluginLoaded(pluginId: string)
|
||||||
|
PluginService.pluginUnloaded(pluginId: string)
|
||||||
|
PluginService.pluginLoadFailed(pluginId: string, error: string)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating a Plugin
|
||||||
|
|
||||||
|
### Step 1: Create Plugin Directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p $CONFIGPATH/DankMaterialShell/plugins/MyPlugin
|
||||||
|
cd $CONFIGPATH/DankMaterialShell/plugins/MyPlugin
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Create Manifest
|
||||||
|
|
||||||
|
Create `plugin.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "myPlugin",
|
||||||
|
"name": "My Plugin",
|
||||||
|
"description": "A sample plugin",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "Your Name",
|
||||||
|
"icon": "extension",
|
||||||
|
"component": "./MyWidget.qml",
|
||||||
|
"settings": "./MySettings.qml",
|
||||||
|
"permissions": ["settings_read", "settings_write"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Create Widget Component
|
||||||
|
|
||||||
|
Create `MyWidget.qml`:
|
||||||
|
|
||||||
|
```qml
|
||||||
|
import QtQuick
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool compactMode: false
|
||||||
|
property string section: "center"
|
||||||
|
property real widgetHeight: 30
|
||||||
|
property string displayText: "Hello World"
|
||||||
|
|
||||||
|
width: textItem.implicitWidth + 16
|
||||||
|
height: widgetHeight
|
||||||
|
radius: 8
|
||||||
|
color: "#20FFFFFF"
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
displayText = PluginService.loadPluginData("myPlugin", "text", "Hello World")
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: textItem
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: root.displayText
|
||||||
|
color: "#FFFFFF"
|
||||||
|
font.pixelSize: 13
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: console.log("Plugin clicked!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Create Settings Component (Optional)
|
||||||
|
|
||||||
|
Create `MySettings.qml`:
|
||||||
|
|
||||||
|
```qml
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Column {
|
||||||
|
// PluginService is injected by PluginsTab
|
||||||
|
property var pluginService
|
||||||
|
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Plugin Settings"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
width: parent.width
|
||||||
|
label: "Display Text"
|
||||||
|
text: pluginService ? pluginService.loadPluginData("myPlugin", "text", "Hello World") : ""
|
||||||
|
onTextChanged: {
|
||||||
|
if (pluginService) {
|
||||||
|
pluginService.savePluginData("myPlugin", "text", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Enable Plugin
|
||||||
|
|
||||||
|
1. Run the shell: `qs -p $CONFIGPATH/quickshell/dms/shell.qml`
|
||||||
|
2. Open Settings (Ctrl+,)
|
||||||
|
3. Navigate to Plugins tab
|
||||||
|
4. Click "Scan for Plugins"
|
||||||
|
5. Enable your plugin with the toggle switch
|
||||||
|
6. Add the plugin to your DankBar configuration
|
||||||
|
|
||||||
|
## Adding Plugin to DankBar
|
||||||
|
|
||||||
|
After enabling a plugin, add it to the bar:
|
||||||
|
|
||||||
|
1. Open Settings → Appearance → DankBar Layout
|
||||||
|
2. Add a new widget entry with your plugin ID
|
||||||
|
3. Choose section (left, center, right)
|
||||||
|
4. Save and reload
|
||||||
|
|
||||||
|
Or edit `$CONFIGPATH/quickshell/dms/config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dankBarLeftWidgets": [
|
||||||
|
{"widgetId": "myPlugin", "enabled": true}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Use Existing Widgets**: Leverage `qs.Widgets` components (DankIcon, DankToggle, etc.) for consistency
|
||||||
|
2. **Follow Theme**: Use `Theme` singleton for colors, spacing, and fonts
|
||||||
|
3. **Data Persistence**: Use PluginService data APIs instead of manual file operations
|
||||||
|
4. **Error Handling**: Gracefully handle missing dependencies and invalid data
|
||||||
|
5. **Performance**: Keep widgets lightweight, avoid expensive operations
|
||||||
|
6. **Responsive Design**: Adapt to `compactMode` and different screen sizes
|
||||||
|
7. **Clean Code**: Follow QML code conventions from CLAUDE.md
|
||||||
|
8. **Documentation**: Include README.md explaining plugin usage
|
||||||
|
9. **Versioning**: Use semantic versioning for updates
|
||||||
|
10. **Dependencies**: Document external library requirements
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
|
||||||
|
### Console Logging
|
||||||
|
|
||||||
|
View plugin logs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
qs -v -p $CONFIGPATH/quickshell/dms/shell.qml
|
||||||
|
```
|
||||||
|
|
||||||
|
Look for lines prefixed with:
|
||||||
|
- `PluginService:` - Service operations
|
||||||
|
- `PluginsTab:` - UI interactions
|
||||||
|
- `PluginsTab:` - Settings loading and accordion interface
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Plugin Not Detected**
|
||||||
|
- Check plugin.json syntax (use `jq` or JSON validator)
|
||||||
|
- Verify directory is in `$CONFIGPATH/DankMaterialShell/plugins/`
|
||||||
|
- Click "Scan for Plugins" in Settings
|
||||||
|
|
||||||
|
2. **Widget Not Displaying**
|
||||||
|
- Ensure plugin is enabled in Settings
|
||||||
|
- Add plugin ID to DankBar widget list
|
||||||
|
- Check widget width/height properties
|
||||||
|
|
||||||
|
3. **Settings Not Loading**
|
||||||
|
- Verify `settings` path in plugin.json
|
||||||
|
- Check settings component for errors
|
||||||
|
- Ensure plugin is enabled and loaded
|
||||||
|
- Review PluginsTab console output for injection issues
|
||||||
|
|
||||||
|
4. **Data Not Persisting**
|
||||||
|
- Confirm pluginService.savePluginData() calls (with injection)
|
||||||
|
- Check `$CONFIGPATH/DankMaterialShell/settings.json` for pluginSettings data
|
||||||
|
- Verify plugin has settings permissions
|
||||||
|
- Ensure PluginService was properly injected into settings component
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
Plugins run with full QML runtime access. Only install plugins from trusted sources.
|
||||||
|
|
||||||
|
**Permissions System:**
|
||||||
|
- `settings_read`: Read plugin configuration
|
||||||
|
- `settings_write`: Write plugin configuration
|
||||||
|
- `process`: Execute system commands
|
||||||
|
- `network`: Network access
|
||||||
|
|
||||||
|
Future versions may enforce permission restrictions.
|
||||||
|
|
||||||
|
## API Stability
|
||||||
|
|
||||||
|
The plugin API is currently **experimental**. Breaking changes may occur in minor version updates. Pin to specific DMS versions for production use.
|
||||||
|
|
||||||
|
**Roadmap:**
|
||||||
|
- Plugin marketplace/repository
|
||||||
|
- Sandboxed plugin execution
|
||||||
|
- Enhanced permission system
|
||||||
|
- Plugin update notifications
|
||||||
|
- Inter-plugin communication
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- **Example Plugin**: https://github.com/rochacbruno/WorldClock
|
||||||
|
- **PluginService**: `Services/PluginService.qml`
|
||||||
|
- **Settings UI**: `Modules/Settings/PluginsTab.qml`
|
||||||
|
- **DankBar Integration**: `Modules/DankBar/DankBar.qml`
|
||||||
|
- **Theme Reference**: `Common/Theme.qml`
|
||||||
|
- **Widget Library**: `Widgets/`
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Share your plugins with the community:
|
||||||
|
|
||||||
|
1. Create a public repository with your plugin
|
||||||
|
2. Include comprehensive README.md
|
||||||
|
4. Add example screenshots
|
||||||
|
5. Document dependencies and permissions
|
||||||
|
|
||||||
|
For plugin system improvements, submit issues or PRs to the main DMS repository.
|
||||||
@@ -37,6 +37,8 @@ ShellRoot {
|
|||||||
DisplayService.nightModeEnabled
|
DisplayService.nightModeEnabled
|
||||||
// Initialize WallpaperCyclingService
|
// Initialize WallpaperCyclingService
|
||||||
WallpaperCyclingService.cyclingActive
|
WallpaperCyclingService.cyclingActive
|
||||||
|
// Initialize PluginService by accessing its properties
|
||||||
|
PluginService.pluginDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
WallpaperBackground {}
|
WallpaperBackground {}
|
||||||
|
|||||||
Reference in New Issue
Block a user