mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-12 23:32:50 -04:00
321 lines
6.9 KiB
Markdown
321 lines
6.9 KiB
Markdown
# Advanced Patterns
|
|
|
|
Patterns observed in production DMS plugins that go beyond the basics.
|
|
|
|
## Plugin Variants
|
|
|
|
Create multiple widget instances from a single plugin definition. Each variant has its own configuration.
|
|
|
|
### Manifest
|
|
|
|
No special manifest changes needed - the variant system is built into PluginComponent.
|
|
|
|
### Widget with Variant Support
|
|
|
|
```qml
|
|
PluginComponent {
|
|
property string variantId: ""
|
|
property var variantData: ({})
|
|
|
|
property string displayText: variantData?.text || "Default"
|
|
|
|
horizontalBarPill: Component {
|
|
StyledRect {
|
|
width: label.implicitWidth + Theme.spacingM * 2
|
|
height: parent.widgetThickness
|
|
|
|
StyledText {
|
|
id: label
|
|
anchors.centerIn: parent
|
|
text: root.displayText
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Widget format in bar config: `pluginId:variantId` (e.g., `exampleVariants:variant_1234567890`)
|
|
|
|
### Settings with Variant Management
|
|
|
|
```qml
|
|
PluginSettings {
|
|
pluginId: "exampleVariants"
|
|
|
|
// Variant creation UI
|
|
DankButton {
|
|
text: "Add New Instance"
|
|
onClicked: {
|
|
var id = "variant_" + Date.now()
|
|
root.createVariant(id, { name: "New Instance", text: "Hello" })
|
|
}
|
|
}
|
|
|
|
// Per-variant configuration
|
|
Repeater {
|
|
model: root.variants
|
|
delegate: Column {
|
|
StringSetting {
|
|
settingKey: modelData.id + "_text"
|
|
label: modelData.name || modelData.id
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## JavaScript Utility Files
|
|
|
|
For complex logic, split into `.js` files:
|
|
|
|
### utils.js
|
|
|
|
```javascript
|
|
.pragma library
|
|
|
|
function formatDuration(ms) {
|
|
if (ms < 60000) return "just now"
|
|
if (ms < 3600000) return Math.floor(ms / 60000) + "m ago"
|
|
return Math.floor(ms / 3600000) + "h ago"
|
|
}
|
|
|
|
function parseResponse(json) {
|
|
try {
|
|
return JSON.parse(json)
|
|
} catch (e) {
|
|
return null
|
|
}
|
|
}
|
|
```
|
|
|
|
### Using in QML
|
|
|
|
```qml
|
|
import "utils.js" as Utils
|
|
|
|
Item {
|
|
StyledText {
|
|
text: Utils.formatDuration(Date.now() - timestamp)
|
|
}
|
|
}
|
|
```
|
|
|
|
The `.pragma library` directive makes the JS file a shared singleton - it is loaded once and shared across all QML instances that import it.
|
|
|
|
## qmldir for Singleton Services
|
|
|
|
For plugins with internal singleton services:
|
|
|
|
### qmldir
|
|
|
|
```
|
|
singleton MyService 1.0 MyService.qml
|
|
```
|
|
|
|
### MyService.qml
|
|
|
|
```qml
|
|
pragma Singleton
|
|
import QtQuick
|
|
|
|
QtObject {
|
|
property var cache: ({})
|
|
|
|
function getData(key) {
|
|
return cache[key] || null
|
|
}
|
|
|
|
function setData(key, value) {
|
|
cache[key] = value
|
|
}
|
|
}
|
|
```
|
|
|
|
### Using the singleton
|
|
|
|
```qml
|
|
import "." as Local
|
|
|
|
Item {
|
|
Component.onCompleted: {
|
|
Local.MyService.setData("key", "value")
|
|
}
|
|
}
|
|
```
|
|
|
|
## Inline Component Declarations
|
|
|
|
Reusable sub-components defined inline:
|
|
|
|
```qml
|
|
Item {
|
|
component StatusBadge: Rectangle {
|
|
property string label: ""
|
|
property color badgeColor: Theme.primary
|
|
|
|
width: badgeText.implicitWidth + Theme.spacingM * 2
|
|
height: 24
|
|
radius: 12
|
|
color: badgeColor
|
|
|
|
StyledText {
|
|
id: badgeText
|
|
anchors.centerIn: parent
|
|
text: label
|
|
color: Theme.onPrimary
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
}
|
|
}
|
|
|
|
Row {
|
|
spacing: Theme.spacingS
|
|
StatusBadge { label: "Running"; badgeColor: Theme.success }
|
|
StatusBadge { label: "Stopped"; badgeColor: Theme.error }
|
|
}
|
|
}
|
|
```
|
|
|
|
## Multi-Provider Adapter Pattern
|
|
|
|
For plugins supporting multiple backends (AI providers, API services):
|
|
|
|
### apiAdapters.js
|
|
|
|
```javascript
|
|
.pragma library
|
|
|
|
function createAdapter(provider) {
|
|
switch (provider) {
|
|
case "openai": return {
|
|
url: "https://api.openai.com/v1/chat/completions",
|
|
headers: (key) => ({ "Authorization": "Bearer " + key }),
|
|
formatRequest: (messages) => JSON.stringify({ model: "gpt-4", messages: messages }),
|
|
parseResponse: (text) => JSON.parse(text).choices[0].message.content
|
|
}
|
|
case "anthropic": return {
|
|
url: "https://api.anthropic.com/v1/messages",
|
|
headers: (key) => ({ "x-api-key": key, "anthropic-version": "2023-06-01" }),
|
|
formatRequest: (messages) => JSON.stringify({ model: "claude-sonnet-4-20250514", messages: messages }),
|
|
parseResponse: (text) => JSON.parse(text).content[0].text
|
|
}
|
|
default: return null
|
|
}
|
|
}
|
|
```
|
|
|
|
## IPC Integration
|
|
|
|
For plugins that respond to keyboard shortcuts or external commands:
|
|
|
|
```qml
|
|
PluginComponent {
|
|
Connections {
|
|
target: DMSIpc
|
|
function onCommandReceived(command, args) {
|
|
if (command === "myPlugin.toggle") {
|
|
doToggle()
|
|
} else if (command === "myPlugin.next") {
|
|
goNext()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
External trigger: `dms ipc call myPlugin.toggle`
|
|
|
|
## Networking with Quickshell.Networking
|
|
|
|
For API calls using the built-in networking module:
|
|
|
|
```qml
|
|
import Quickshell.Networking
|
|
|
|
Item {
|
|
NetworkRequest {
|
|
id: request
|
|
url: "https://api.example.com/data"
|
|
method: "GET"
|
|
|
|
onResponseReceived: (response) => {
|
|
const data = JSON.parse(response.body)
|
|
processData(data)
|
|
}
|
|
|
|
onErrorOccurred: (error) => {
|
|
console.error("Network error:", error)
|
|
}
|
|
}
|
|
|
|
function fetchData() {
|
|
request.send()
|
|
}
|
|
}
|
|
```
|
|
|
|
## Toast Notifications
|
|
|
|
Show user feedback:
|
|
|
|
```qml
|
|
import qs.Services
|
|
|
|
// Info toast
|
|
ToastService?.showInfo("Operation completed")
|
|
|
|
// With title
|
|
ToastService?.showInfo("Plugin Name", "Data refreshed successfully")
|
|
```
|
|
|
|
Always use optional chaining since ToastService may not be available in all contexts.
|
|
|
|
## Clipboard Operations
|
|
|
|
```qml
|
|
import Quickshell
|
|
|
|
function copyToClipboard(text) {
|
|
Quickshell.execDetached(["dms", "cl", "copy", text])
|
|
ToastService?.showInfo("Copied to clipboard")
|
|
}
|
|
```
|
|
|
|
Do NOT use `globalThis.clipboard`, `navigator.clipboard`, or any browser API - they do not exist in the QML runtime.
|
|
|
|
## Multi-File Plugin Architecture
|
|
|
|
Large plugins can be split across multiple files:
|
|
|
|
```
|
|
MyPlugin/
|
|
plugin.json
|
|
Main.qml # Main widget component
|
|
Settings.qml # Settings UI
|
|
DetailView.qml # Popout detail view
|
|
utils.js # Utility functions
|
|
apiAdapter.js # API adapter layer
|
|
qmldir # Optional: singleton registrations
|
|
```
|
|
|
|
Import sibling files:
|
|
|
|
```qml
|
|
// In Main.qml
|
|
import "." as Local
|
|
|
|
Item {
|
|
Loader {
|
|
source: "DetailView.qml"
|
|
}
|
|
}
|
|
```
|
|
|
|
## Performance Tips
|
|
|
|
1. Use `Proc.runCommand` with appropriate debounce for external commands
|
|
2. Pre-cache images and thumbnails for image-heavy plugins
|
|
3. Limit concurrent network requests
|
|
4. Use `Timer` with reasonable intervals (don't poll faster than needed)
|
|
5. Lazy-load heavy content (use `Loader` for complex popout content)
|
|
6. Avoid blocking the UI thread with synchronous operations
|