mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-05 21:15:38 -05:00
Abstract away plugin dev a little more
This commit is contained in:
@@ -22,6 +22,7 @@ import qs.Modules.ProcessList
|
||||
import qs.Modules.Settings
|
||||
import qs.Modules.DankBar
|
||||
import qs.Modules.DankBar.Popouts
|
||||
import qs.Modules.Plugins
|
||||
import qs.Services
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@ Item {
|
||||
property var components: null
|
||||
property bool noBackground: false
|
||||
required property var axis
|
||||
property string section: "center"
|
||||
property var parentScreen: null
|
||||
property real widgetThickness: 30
|
||||
property real barThickness: 48
|
||||
|
||||
readonly property bool isVertical: axis?.isVertical ?? false
|
||||
readonly property real spacing: noBackground ? 2 : Theme.spacingXS
|
||||
@@ -371,7 +375,24 @@ Item {
|
||||
item.axis = root.axis
|
||||
}
|
||||
if (root.axis && "isVertical" in item) {
|
||||
item.isVertical = root.axis.isVertical
|
||||
try {
|
||||
item.isVertical = root.axis.isVertical
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
// Inject properties for plugin widgets
|
||||
if ("section" in item) {
|
||||
item.section = root.section
|
||||
}
|
||||
if ("parentScreen" in item) {
|
||||
item.parentScreen = root.parentScreen
|
||||
}
|
||||
if ("widgetThickness" in item) {
|
||||
item.widgetThickness = root.widgetThickness
|
||||
}
|
||||
if ("barThickness" in item) {
|
||||
item.barThickness = root.barThickness
|
||||
}
|
||||
|
||||
// Inject PluginService for plugin widgets
|
||||
|
||||
@@ -537,6 +537,9 @@ Item {
|
||||
widgetsModel: SettingsData.dankBarLeftWidgetsModel
|
||||
components: topBarContent.allComponents
|
||||
noBackground: SettingsData.dankBarNoBackground
|
||||
parentScreen: barWindow.screen
|
||||
widgetThickness: barWindow.widgetThickness
|
||||
barThickness: barWindow.effectiveBarThickness
|
||||
}
|
||||
|
||||
RightSection {
|
||||
@@ -549,6 +552,9 @@ Item {
|
||||
widgetsModel: SettingsData.dankBarRightWidgetsModel
|
||||
components: topBarContent.allComponents
|
||||
noBackground: SettingsData.dankBarNoBackground
|
||||
parentScreen: barWindow.screen
|
||||
widgetThickness: barWindow.widgetThickness
|
||||
barThickness: barWindow.effectiveBarThickness
|
||||
}
|
||||
|
||||
CenterSection {
|
||||
@@ -561,6 +567,9 @@ Item {
|
||||
widgetsModel: SettingsData.dankBarCenterWidgetsModel
|
||||
components: topBarContent.allComponents
|
||||
noBackground: SettingsData.dankBarNoBackground
|
||||
parentScreen: barWindow.screen
|
||||
widgetThickness: barWindow.widgetThickness
|
||||
barThickness: barWindow.effectiveBarThickness
|
||||
}
|
||||
}
|
||||
|
||||
@@ -580,6 +589,9 @@ Item {
|
||||
widgetsModel: SettingsData.dankBarLeftWidgetsModel
|
||||
components: topBarContent.allComponents
|
||||
noBackground: SettingsData.dankBarNoBackground
|
||||
parentScreen: barWindow.screen
|
||||
widgetThickness: barWindow.widgetThickness
|
||||
barThickness: barWindow.effectiveBarThickness
|
||||
}
|
||||
|
||||
CenterSection {
|
||||
@@ -593,6 +605,9 @@ Item {
|
||||
widgetsModel: SettingsData.dankBarCenterWidgetsModel
|
||||
components: topBarContent.allComponents
|
||||
noBackground: SettingsData.dankBarNoBackground
|
||||
parentScreen: barWindow.screen
|
||||
widgetThickness: barWindow.widgetThickness
|
||||
barThickness: barWindow.effectiveBarThickness
|
||||
}
|
||||
|
||||
RightSection {
|
||||
|
||||
@@ -8,6 +8,9 @@ Item {
|
||||
property var components: null
|
||||
property bool noBackground: false
|
||||
required property var axis
|
||||
property var parentScreen: null
|
||||
property real widgetThickness: 30
|
||||
property real barThickness: 48
|
||||
|
||||
readonly property bool isVertical: axis?.isVertical ?? false
|
||||
|
||||
@@ -38,6 +41,10 @@ Item {
|
||||
components: root.components
|
||||
isInColumn: false
|
||||
axis: root.axis
|
||||
section: "left"
|
||||
parentScreen: root.parentScreen
|
||||
widgetThickness: root.widgetThickness
|
||||
barThickness: root.barThickness
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,6 +70,10 @@ Item {
|
||||
components: root.components
|
||||
isInColumn: true
|
||||
axis: root.axis
|
||||
section: "left"
|
||||
parentScreen: root.parentScreen
|
||||
widgetThickness: root.widgetThickness
|
||||
barThickness: root.barThickness
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ Item {
|
||||
property var components: null
|
||||
property bool noBackground: false
|
||||
required property var axis
|
||||
property var parentScreen: null
|
||||
property real widgetThickness: 30
|
||||
property real barThickness: 48
|
||||
|
||||
readonly property bool isVertical: axis?.isVertical ?? false
|
||||
|
||||
@@ -40,6 +43,10 @@ Item {
|
||||
components: root.components
|
||||
isInColumn: false
|
||||
axis: root.axis
|
||||
section: "right"
|
||||
parentScreen: root.parentScreen
|
||||
widgetThickness: root.widgetThickness
|
||||
barThickness: root.barThickness
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,6 +72,10 @@ Item {
|
||||
components: root.components
|
||||
isInColumn: true
|
||||
axis: root.axis
|
||||
section: "right"
|
||||
parentScreen: root.parentScreen
|
||||
widgetThickness: root.widgetThickness
|
||||
barThickness: root.barThickness
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ Loader {
|
||||
property var components: null
|
||||
property bool isInColumn: false
|
||||
property var axis: null
|
||||
property string section: "center"
|
||||
property var parentScreen: null
|
||||
property real widgetThickness: 30
|
||||
property real barThickness: 48
|
||||
|
||||
asynchronous: false
|
||||
|
||||
@@ -21,20 +25,59 @@ Loader {
|
||||
|
||||
signal contentItemReady(var item)
|
||||
|
||||
Binding {
|
||||
target: root.item
|
||||
when: root.item && "parentScreen" in root.item
|
||||
property: "parentScreen"
|
||||
value: root.parentScreen
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: root.item
|
||||
when: root.item && "section" in root.item
|
||||
property: "section"
|
||||
value: root.section
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: root.item
|
||||
when: root.item && "widgetThickness" in root.item
|
||||
property: "widgetThickness"
|
||||
value: root.widgetThickness
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: root.item
|
||||
when: root.item && "barThickness" in root.item
|
||||
property: "barThickness"
|
||||
value: root.barThickness
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: root.item
|
||||
when: root.item && "axis" in root.item
|
||||
property: "axis"
|
||||
value: root.axis
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (item) {
|
||||
contentItemReady(item)
|
||||
if (widgetId === "spacer") {
|
||||
item.spacerSize = Qt.binding(() => spacerSize)
|
||||
}
|
||||
if (axis && "axis" in item) {
|
||||
item.axis = axis
|
||||
}
|
||||
if (axis && "isVertical" in item) {
|
||||
item.isVertical = axis.isVertical
|
||||
try {
|
||||
item.isVertical = axis.isVertical
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
// Inject PluginService for plugin widgets
|
||||
if (item.pluginService !== undefined) {
|
||||
console.log("WidgetHost: Injecting PluginService into plugin widget:", widgetId)
|
||||
item.pluginService = PluginService
|
||||
|
||||
53
Modules/Plugins/BaseHorizontalPill.qml
Normal file
53
Modules/Plugins/BaseHorizontalPill.qml
Normal file
@@ -0,0 +1,53 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property var axis: null
|
||||
property string section: "center"
|
||||
property var popoutTarget: null
|
||||
property var parentScreen: null
|
||||
property real widgetThickness: 30
|
||||
property real barThickness: 48
|
||||
property alias content: contentLoader.sourceComponent
|
||||
|
||||
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30))
|
||||
|
||||
signal clicked()
|
||||
|
||||
width: contentLoader.item ? (contentLoader.item.implicitWidth + horizontalPadding * 2) : 0
|
||||
height: widgetThickness
|
||||
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
|
||||
color: {
|
||||
if (SettingsData.dankBarNoBackground) {
|
||||
return "transparent"
|
||||
}
|
||||
|
||||
const baseColor = mouseArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor
|
||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency)
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: {
|
||||
if (popoutTarget && popoutTarget.setTriggerPosition) {
|
||||
const globalPos = mapToGlobal(0, 0)
|
||||
const currentScreen = parentScreen || Screen
|
||||
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, width)
|
||||
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
|
||||
}
|
||||
root.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
53
Modules/Plugins/BaseVerticalPill.qml
Normal file
53
Modules/Plugins/BaseVerticalPill.qml
Normal file
@@ -0,0 +1,53 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property var axis: null
|
||||
property string section: "center"
|
||||
property var popoutTarget: null
|
||||
property var parentScreen: null
|
||||
property real widgetThickness: 30
|
||||
property real barThickness: 48
|
||||
property alias content: contentLoader.sourceComponent
|
||||
|
||||
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30))
|
||||
|
||||
signal clicked()
|
||||
|
||||
width: widgetThickness
|
||||
height: contentLoader.item ? (contentLoader.item.implicitHeight + horizontalPadding * 2) : 0
|
||||
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
|
||||
color: {
|
||||
if (SettingsData.dankBarNoBackground) {
|
||||
return "transparent"
|
||||
}
|
||||
|
||||
const baseColor = mouseArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor
|
||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency)
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: {
|
||||
if (popoutTarget && popoutTarget.setTriggerPosition) {
|
||||
const globalPos = mapToGlobal(0, 0)
|
||||
const currentScreen = parentScreen || Screen
|
||||
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, height)
|
||||
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
|
||||
}
|
||||
root.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
131
Modules/Plugins/ListSetting.qml
Normal file
131
Modules/Plugins/ListSetting.qml
Normal file
@@ -0,0 +1,131 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
required property string settingKey
|
||||
required property string label
|
||||
property string description: ""
|
||||
property var items: []
|
||||
property Component delegate: null
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Component.onCompleted: {
|
||||
const settings = findSettings()
|
||||
if (settings) {
|
||||
items = settings.loadValue(settingKey, [])
|
||||
}
|
||||
}
|
||||
|
||||
onItemsChanged: {
|
||||
const settings = findSettings()
|
||||
if (settings) {
|
||||
settings.saveValue(settingKey, items)
|
||||
}
|
||||
}
|
||||
|
||||
function findSettings() {
|
||||
let item = parent
|
||||
while (item) {
|
||||
if (item.saveValue !== undefined && item.loadValue !== undefined) {
|
||||
return item
|
||||
}
|
||||
item = item.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function addItem(item) {
|
||||
items = items.concat([item])
|
||||
}
|
||||
|
||||
function removeItem(index) {
|
||||
const newItems = items.slice()
|
||||
newItems.splice(index, 1)
|
||||
items = newItems
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.label
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.description
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
visible: root.description !== ""
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: root.items
|
||||
delegate: root.delegate ? root.delegate : defaultDelegate
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "No items added yet"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
visible: root.items.length === 0
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: defaultDelegate
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.width: 0
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: modelData
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 60
|
||||
height: 28
|
||||
color: removeArea.containsMouse ? Theme.errorHover : Theme.error
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: "Remove"
|
||||
color: Theme.errorText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: removeArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.removeItem(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
230
Modules/Plugins/ListSettingWithInput.qml
Normal file
230
Modules/Plugins/ListSettingWithInput.qml
Normal file
@@ -0,0 +1,230 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
required property string settingKey
|
||||
required property string label
|
||||
property string description: ""
|
||||
property var fields: []
|
||||
property var items: []
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Component.onCompleted: {
|
||||
const settings = findSettings()
|
||||
if (settings) {
|
||||
items = settings.loadValue(settingKey, [])
|
||||
}
|
||||
}
|
||||
|
||||
onItemsChanged: {
|
||||
const settings = findSettings()
|
||||
if (settings) {
|
||||
settings.saveValue(settingKey, items)
|
||||
}
|
||||
}
|
||||
|
||||
function findSettings() {
|
||||
let item = parent
|
||||
while (item) {
|
||||
if (item.saveValue !== undefined && item.loadValue !== undefined) {
|
||||
return item
|
||||
}
|
||||
item = item.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function addItem(item) {
|
||||
items = items.concat([item])
|
||||
}
|
||||
|
||||
function removeItem(index) {
|
||||
const newItems = items.slice()
|
||||
newItems.splice(index, 1)
|
||||
items = newItems
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.label
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.description
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
visible: root.description !== ""
|
||||
}
|
||||
|
||||
Flow {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: root.fields
|
||||
|
||||
StyledText {
|
||||
text: modelData.label
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
width: modelData.width || 200
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Flow {
|
||||
id: inputRow
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
property var inputFields: []
|
||||
|
||||
Repeater {
|
||||
id: inputRepeater
|
||||
model: root.fields
|
||||
|
||||
DankTextField {
|
||||
width: modelData.width || 200
|
||||
placeholderText: modelData.placeholder || ""
|
||||
|
||||
Component.onCompleted: {
|
||||
inputRow.inputFields.push(this)
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
addButton.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankButton {
|
||||
id: addButton
|
||||
width: 50
|
||||
height: 36
|
||||
text: "Add"
|
||||
|
||||
onClicked: {
|
||||
let newItem = {}
|
||||
let hasValue = false
|
||||
|
||||
for (let i = 0; i < root.fields.length; i++) {
|
||||
const field = root.fields[i]
|
||||
const input = inputRow.inputFields[i]
|
||||
const value = input.text.trim()
|
||||
|
||||
if (value !== "") {
|
||||
hasValue = true
|
||||
}
|
||||
|
||||
if (field.required && value === "") {
|
||||
return
|
||||
}
|
||||
|
||||
newItem[field.id] = value || (field.default || "")
|
||||
}
|
||||
|
||||
if (hasValue) {
|
||||
root.addItem(newItem)
|
||||
for (let i = 0; i < inputRow.inputFields.length; i++) {
|
||||
inputRow.inputFields[i].text = ""
|
||||
}
|
||||
if (inputRow.inputFields.length > 0) {
|
||||
inputRow.inputFields[0].forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Current Items"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
visible: root.items.length > 0
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: root.items
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.width: 0
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Repeater {
|
||||
model: root.fields
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
const value = root.items[index][modelData.id]
|
||||
return value || ""
|
||||
}
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
width: modelData.width || 200
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 60
|
||||
height: 28
|
||||
color: removeArea.containsMouse ? Theme.errorHover : Theme.error
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: "Remove"
|
||||
color: Theme.errorText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: removeArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.removeItem(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "No items added yet"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
visible: root.items.length === 0
|
||||
}
|
||||
}
|
||||
}
|
||||
69
Modules/Plugins/PluginComponent.qml
Normal file
69
Modules/Plugins/PluginComponent.qml
Normal file
@@ -0,0 +1,69 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var axis: null
|
||||
property string section: "center"
|
||||
property var parentScreen: null
|
||||
property real widgetThickness: 30
|
||||
property real barThickness: 48
|
||||
|
||||
property Component horizontalBarPill: null
|
||||
property Component verticalBarPill: null
|
||||
property Component popoutContent: null
|
||||
property real popoutWidth: 400
|
||||
property real popoutHeight: 400
|
||||
|
||||
readonly property bool isVertical: axis?.isVertical ?? false
|
||||
readonly property bool hasHorizontalPill: horizontalBarPill !== null
|
||||
readonly property bool hasVerticalPill: verticalBarPill !== null
|
||||
readonly property bool hasPopout: popoutContent !== null
|
||||
|
||||
width: isVertical ? (hasVerticalPill ? verticalPill.width : 0) : (hasHorizontalPill ? horizontalPill.width : 0)
|
||||
height: isVertical ? (hasVerticalPill ? verticalPill.height : 0) : (hasHorizontalPill ? horizontalPill.height : 0)
|
||||
|
||||
BaseHorizontalPill {
|
||||
id: horizontalPill
|
||||
visible: !isVertical && hasHorizontalPill
|
||||
axis: root.axis
|
||||
section: root.section
|
||||
popoutTarget: hasPopout ? pluginPopout : null
|
||||
parentScreen: root.parentScreen
|
||||
widgetThickness: root.widgetThickness
|
||||
barThickness: root.barThickness
|
||||
content: root.horizontalBarPill
|
||||
onClicked: {
|
||||
if (hasPopout) {
|
||||
pluginPopout.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BaseVerticalPill {
|
||||
id: verticalPill
|
||||
visible: isVertical && hasVerticalPill
|
||||
axis: root.axis
|
||||
section: root.section
|
||||
popoutTarget: hasPopout ? pluginPopout : null
|
||||
parentScreen: root.parentScreen
|
||||
widgetThickness: root.widgetThickness
|
||||
barThickness: root.barThickness
|
||||
content: root.verticalBarPill
|
||||
onClicked: {
|
||||
if (hasPopout) {
|
||||
pluginPopout.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PluginPopout {
|
||||
id: pluginPopout
|
||||
contentWidth: root.popoutWidth
|
||||
contentHeight: root.popoutHeight
|
||||
pluginContent: root.popoutContent
|
||||
}
|
||||
}
|
||||
116
Modules/Plugins/PluginPopout.qml
Normal file
116
Modules/Plugins/PluginPopout.qml
Normal file
@@ -0,0 +1,116 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
DankPopout {
|
||||
id: root
|
||||
|
||||
property var triggerScreen: null
|
||||
property Component pluginContent: null
|
||||
property real contentWidth: 400
|
||||
property real contentHeight: 400
|
||||
|
||||
function setTriggerPosition(x, y, width, section, screen) {
|
||||
triggerX = x
|
||||
triggerY = y
|
||||
triggerWidth = width
|
||||
triggerSection = section
|
||||
triggerScreen = screen
|
||||
}
|
||||
|
||||
popupWidth: contentWidth
|
||||
popupHeight: popoutContent.item ? popoutContent.item.implicitHeight : contentHeight
|
||||
screen: triggerScreen
|
||||
shouldBeVisible: false
|
||||
visible: shouldBeVisible
|
||||
|
||||
content: Component {
|
||||
Rectangle {
|
||||
id: popoutContainer
|
||||
|
||||
implicitHeight: popoutColumn.implicitHeight + Theme.spacingL * 2
|
||||
color: Theme.popupBackground()
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 0
|
||||
antialiasing: true
|
||||
smooth: true
|
||||
focus: true
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.shouldBeVisible) {
|
||||
forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.close()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onShouldBeVisibleChanged() {
|
||||
if (root.shouldBeVisible) {
|
||||
Qt.callLater(() => {
|
||||
popoutContainer.forceActiveFocus()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: popoutColumn
|
||||
width: parent.width - Theme.spacingL * 2
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 32
|
||||
visible: closeButton.visible
|
||||
|
||||
Item {
|
||||
width: parent.width - 32
|
||||
height: 32
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: closeButton
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: closeArea.containsMouse ? Theme.errorHover : "transparent"
|
||||
visible: true
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "close"
|
||||
size: Theme.iconSize - 4
|
||||
color: closeArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: {
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: popoutContent
|
||||
width: parent.width
|
||||
sourceComponent: root.pluginContent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Modules/Plugins/PluginSettings.qml
Normal file
33
Modules/Plugins/PluginSettings.qml
Normal file
@@ -0,0 +1,33 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string pluginId
|
||||
property var pluginService: null
|
||||
default property alias content: settingsColumn.children
|
||||
|
||||
implicitHeight: settingsColumn.implicitHeight
|
||||
height: implicitHeight
|
||||
|
||||
function saveValue(key, value) {
|
||||
if (pluginService && pluginService.savePluginData) {
|
||||
pluginService.savePluginData(pluginId, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
function loadValue(key, defaultValue) {
|
||||
if (pluginService && pluginService.loadPluginData) {
|
||||
return pluginService.loadPluginData(pluginId, key, defaultValue)
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
Column {
|
||||
id: settingsColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
}
|
||||
}
|
||||
113
Modules/Plugins/SelectionSetting.qml
Normal file
113
Modules/Plugins/SelectionSetting.qml
Normal file
@@ -0,0 +1,113 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
required property string settingKey
|
||||
required property string label
|
||||
property string description: ""
|
||||
required property var options
|
||||
property string defaultValue: ""
|
||||
property string value: defaultValue
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
readonly property var optionLabels: {
|
||||
const labels = []
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
labels.push(options[i].label || options[i])
|
||||
}
|
||||
return labels
|
||||
}
|
||||
|
||||
readonly property var valueToLabel: {
|
||||
const map = {}
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
const opt = options[i]
|
||||
if (typeof opt === 'object') {
|
||||
map[opt.value] = opt.label
|
||||
} else {
|
||||
map[opt] = opt
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
readonly property var labelToValue: {
|
||||
const map = {}
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
const opt = options[i]
|
||||
if (typeof opt === 'object') {
|
||||
map[opt.label] = opt.value
|
||||
} else {
|
||||
map[opt] = opt
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
const settings = findSettings()
|
||||
if (settings) {
|
||||
value = settings.loadValue(settingKey, defaultValue)
|
||||
}
|
||||
}
|
||||
|
||||
onValueChanged: {
|
||||
const settings = findSettings()
|
||||
if (settings) {
|
||||
settings.saveValue(settingKey, value)
|
||||
}
|
||||
}
|
||||
|
||||
function findSettings() {
|
||||
let item = parent
|
||||
while (item) {
|
||||
if (item.saveValue !== undefined && item.loadValue !== undefined) {
|
||||
return item
|
||||
}
|
||||
item = item.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Column {
|
||||
width: parent.width * 0.4
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: root.label
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.description
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
visible: root.description !== ""
|
||||
}
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
width: parent.width * 0.6 - Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
currentValue: root.valueToLabel[root.value] || root.value
|
||||
options: root.optionLabels
|
||||
onValueChanged: newValue => {
|
||||
root.value = root.labelToValue[newValue] || newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
65
Modules/Plugins/StringSetting.qml
Normal file
65
Modules/Plugins/StringSetting.qml
Normal file
@@ -0,0 +1,65 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
required property string settingKey
|
||||
required property string label
|
||||
property string description: ""
|
||||
property string placeholder: ""
|
||||
property string defaultValue: ""
|
||||
property string value: defaultValue
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Component.onCompleted: {
|
||||
const settings = findSettings()
|
||||
if (settings) {
|
||||
value = settings.loadValue(settingKey, defaultValue)
|
||||
textField.text = value
|
||||
}
|
||||
}
|
||||
|
||||
function findSettings() {
|
||||
let item = parent
|
||||
while (item) {
|
||||
if (item.saveValue !== undefined && item.loadValue !== undefined) {
|
||||
return item
|
||||
}
|
||||
item = item.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.label
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.description
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
visible: root.description !== ""
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: textField
|
||||
width: parent.width
|
||||
placeholderText: root.placeholder
|
||||
onEditingFinished: {
|
||||
root.value = text
|
||||
const settings = findSettings()
|
||||
if (settings) {
|
||||
settings.saveValue(settingKey, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
72
Modules/Plugins/ToggleSetting.qml
Normal file
72
Modules/Plugins/ToggleSetting.qml
Normal file
@@ -0,0 +1,72 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Row {
|
||||
id: root
|
||||
|
||||
required property string settingKey
|
||||
required property string label
|
||||
property string description: ""
|
||||
property bool defaultValue: false
|
||||
property bool value: defaultValue
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Component.onCompleted: {
|
||||
const settings = findSettings()
|
||||
if (settings) {
|
||||
value = settings.loadValue(settingKey, defaultValue)
|
||||
}
|
||||
}
|
||||
|
||||
onValueChanged: {
|
||||
const settings = findSettings()
|
||||
if (settings) {
|
||||
settings.saveValue(settingKey, value)
|
||||
}
|
||||
}
|
||||
|
||||
function findSettings() {
|
||||
let item = parent
|
||||
while (item) {
|
||||
if (item.saveValue !== undefined && item.loadValue !== undefined) {
|
||||
return item
|
||||
}
|
||||
item = item.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - toggle.width - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: root.label
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.description
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
visible: root.description !== ""
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: toggle
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: root.value
|
||||
onToggled: isChecked => {
|
||||
root.value = isChecked
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,21 +160,17 @@ Item {
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Item {
|
||||
Column {
|
||||
width: parent.width
|
||||
height: Math.max(150, pluginListView.contentHeight)
|
||||
spacing: Theme.spacingM
|
||||
|
||||
ListView {
|
||||
id: pluginListView
|
||||
|
||||
anchors.fill: parent
|
||||
Repeater {
|
||||
id: pluginRepeater
|
||||
model: PluginService.getAvailablePlugins()
|
||||
spacing: Theme.spacingM
|
||||
clip: true
|
||||
|
||||
delegate: StyledRect {
|
||||
StyledRect {
|
||||
id: pluginDelegate
|
||||
width: pluginListView.width
|
||||
width: parent.width
|
||||
height: pluginItemColumn.implicitHeight + Theme.spacingM * 2 + settingsContainer.height
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
@@ -194,23 +190,9 @@ Item {
|
||||
console.log("Plugin", pluginId, "isExpanded changed to:", isExpanded)
|
||||
}
|
||||
|
||||
color: pluginMouseArea.containsMouse ? Theme.surfacePressed : (isExpanded ? Theme.surfaceContainerHighest : Theme.surfaceContainer)
|
||||
color: pluginMouseArea.containsMouse ? Theme.surfacePressed : (isExpanded ? Theme.surfaceContainerHighest : Theme.surfaceContainerHigh)
|
||||
border.width: 0
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: pluginMouseArea
|
||||
anchors.fill: parent
|
||||
@@ -238,7 +220,7 @@ Item {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
@@ -252,9 +234,9 @@ Item {
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM - pluginToggle.width - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
width: parent.width - 250
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
@@ -262,7 +244,7 @@ Item {
|
||||
|
||||
StyledText {
|
||||
text: pluginDelegate.pluginName
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -274,13 +256,6 @@ Item {
|
||||
color: pluginDelegate.hasSettings ? Theme.primary : "transparent"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: pluginDelegate.hasSettings
|
||||
|
||||
Behavior on rotation {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,16 +264,11 @@ Item {
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 10
|
||||
height: 1
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: pluginToggle
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: PluginService.isPluginLoaded(pluginDelegate.pluginId)
|
||||
onToggled: (isChecked) => {
|
||||
@@ -367,20 +337,9 @@ Item {
|
||||
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
|
||||
height: pluginDelegate.isExpanded && pluginDelegate.hasSettings ? (settingsLoader.item ? settingsLoader.item.implicitHeight + Theme.spacingL * 2 : 0) : 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: Theme.surfaceContainerHighest
|
||||
@@ -389,78 +348,61 @@ Item {
|
||||
border.width: 0
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
Loader {
|
||||
id: settingsLoader
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
contentHeight: settingsLoader.height
|
||||
contentWidth: width
|
||||
clip: true
|
||||
active: pluginDelegate.isExpanded && pluginDelegate.hasSettings && PluginService.isPluginLoaded(pluginDelegate.pluginId)
|
||||
asynchronous: false
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
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
|
||||
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 ""
|
||||
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)
|
||||
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)
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
if (item.loadTimezones) {
|
||||
console.log("Calling loadTimezones for WorldClock plugin")
|
||||
item.loadTimezones()
|
||||
}
|
||||
if (item.initializeSettings) {
|
||||
item.initializeSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -482,12 +424,12 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
text: "No plugins found.\nPlace plugins in " + PluginService.pluginDirectory
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
visible: pluginListView.model.length === 0
|
||||
visible: pluginRepeater.model.length === 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -498,10 +440,10 @@ Item {
|
||||
Connections {
|
||||
target: PluginService
|
||||
function onPluginLoaded() {
|
||||
pluginListView.model = PluginService.getAvailablePlugins()
|
||||
pluginRepeater.model = PluginService.getAvailablePlugins()
|
||||
}
|
||||
function onPluginUnloaded() {
|
||||
pluginListView.model = PluginService.getAvailablePlugins()
|
||||
pluginRepeater.model = PluginService.getAvailablePlugins()
|
||||
if (pluginsTab.expandedPluginId !== "" && !PluginService.isPluginLoaded(pluginsTab.expandedPluginId)) {
|
||||
pluginsTab.expandedPluginId = ""
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ Singleton {
|
||||
}
|
||||
return configDirStr + "/DankMaterialShell/plugins"
|
||||
}
|
||||
property string systemPluginDirectory: "/etc/xdg/quickshell/dms-plugins"
|
||||
property var pluginDirectories: [pluginDirectory, systemPluginDirectory]
|
||||
|
||||
signal pluginLoaded(string pluginId)
|
||||
signal pluginUnloaded(string pluginId)
|
||||
@@ -35,37 +37,54 @@ Singleton {
|
||||
scanPlugins()
|
||||
}
|
||||
|
||||
property int currentScanIndex: 0
|
||||
property var scanResults: []
|
||||
|
||||
property var lsProcess: Process {
|
||||
id: dirScanner
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var output = text.trim()
|
||||
var currentDir = pluginDirectories[currentScanIndex]
|
||||
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"
|
||||
var manifestPath = currentDir + "/" + 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")
|
||||
console.log("PluginService: No directories found in:", currentDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: function(exitCode) {
|
||||
if (exitCode !== 0) {
|
||||
console.error("PluginService: Failed to scan plugin directory, exit code:", exitCode)
|
||||
console.log("PluginService: Directory scan failed for:", pluginDirectories[currentScanIndex], "exit code:", exitCode)
|
||||
}
|
||||
currentScanIndex++
|
||||
if (currentScanIndex < pluginDirectories.length) {
|
||||
scanNextDirectory()
|
||||
} else {
|
||||
currentScanIndex = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scanPlugins() {
|
||||
lsProcess.command = ["find", pluginDirectory, "-maxdepth", "1", "-type", "d", "-not", "-path", pluginDirectory, "-exec", "basename", "{}", ";"]
|
||||
currentScanIndex = 0
|
||||
scanNextDirectory()
|
||||
}
|
||||
|
||||
function scanNextDirectory() {
|
||||
var dir = pluginDirectories[currentScanIndex]
|
||||
console.log("PluginService: Scanning directory:", dir)
|
||||
lsProcess.command = ["find", "-L", dir, "-maxdepth", "1", "-type", "d", "-not", "-path", dir, "-exec", "basename", "{}", ";"]
|
||||
lsProcess.running = true
|
||||
}
|
||||
|
||||
|
||||
417
docs/PLUGINS.md
417
docs/PLUGINS.md
@@ -93,94 +93,301 @@ The manifest file defines plugin metadata and configuration:
|
||||
|
||||
### Widget Component
|
||||
|
||||
The main widget component is displayed in the DankBar. It receives several properties from the shell:
|
||||
The main widget component uses the **PluginComponent** wrapper which provides automatic property injection and bar integration:
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
PluginComponent {
|
||||
// Define horizontal bar pill (optional)
|
||||
horizontalBarPill: Component {
|
||||
StyledRect {
|
||||
width: content.implicitWidth + Theme.spacingM * 2
|
||||
height: parent.widgetThickness
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
// 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)
|
||||
StyledText {
|
||||
id: content
|
||||
anchors.centerIn: parent
|
||||
text: "Hello World"
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save plugin data
|
||||
function saveData(key, value) {
|
||||
if (pluginService) {
|
||||
pluginService.savePluginData("yourPlugin", key, value)
|
||||
// Define vertical bar pill (optional)
|
||||
verticalBarPill: Component {
|
||||
// Same as horizontal but optimized for vertical layout
|
||||
}
|
||||
|
||||
// Define popout content (optional)
|
||||
popoutContent: Component {
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
padding: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: "Popout Content"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Popout dimensions (required if popoutContent is set)
|
||||
popoutWidth: 400
|
||||
popoutHeight: 300
|
||||
}
|
||||
```
|
||||
|
||||
**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
|
||||
**PluginComponent Properties (automatically injected):**
|
||||
- `axis`: Bar axis information (horizontal/vertical)
|
||||
- `section`: Bar section ("left", "center", "right")
|
||||
- `parentScreen`: Screen reference for multi-monitor support
|
||||
- `widgetThickness`: Recommended widget size perpendicular to bar
|
||||
- `barThickness`: Bar thickness parallel to edge
|
||||
|
||||
**Component Options:**
|
||||
- `horizontalBarPill`: Component shown in horizontal bars
|
||||
- `verticalBarPill`: Component shown in vertical bars
|
||||
- `popoutContent`: Optional popout window content
|
||||
- `popoutWidth`: Popout window width
|
||||
- `popoutHeight`: Popout window height
|
||||
|
||||
The PluginComponent automatically handles:
|
||||
- Bar orientation detection
|
||||
- Click handlers for popouts
|
||||
- Proper positioning and anchoring
|
||||
- Theme integration
|
||||
|
||||
### Settings Component
|
||||
|
||||
Optional settings UI loaded inline in the PluginsTab accordion interface:
|
||||
Optional settings UI loaded inline in the PluginsTab accordion interface. Use the simplified settings API with auto-storage components:
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
Column {
|
||||
PluginSettings {
|
||||
id: root
|
||||
pluginId: "yourPlugin"
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
StringSetting {
|
||||
settingKey: "apiKey"
|
||||
label: "API Key"
|
||||
description: "Your API key for accessing the service"
|
||||
placeholder: "Enter API key..."
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
checked: pluginService ? pluginService.loadPluginData("yourPlugin", "enabled", true) : false
|
||||
onToggled: {
|
||||
if (pluginService) {
|
||||
pluginService.savePluginData("yourPlugin", "enabled", checked)
|
||||
ToggleSetting {
|
||||
settingKey: "notifications"
|
||||
label: "Enable Notifications"
|
||||
description: "Show desktop notifications for updates"
|
||||
defaultValue: true
|
||||
}
|
||||
|
||||
SelectionSetting {
|
||||
settingKey: "updateInterval"
|
||||
label: "Update Interval"
|
||||
description: "How often to refresh data"
|
||||
options: [
|
||||
{label: "1 minute", value: "60"},
|
||||
{label: "5 minutes", value: "300"},
|
||||
{label: "15 minutes", value: "900"}
|
||||
]
|
||||
defaultValue: "300"
|
||||
}
|
||||
|
||||
ListSetting {
|
||||
id: itemList
|
||||
settingKey: "items"
|
||||
label: "Saved Items"
|
||||
description: "List of configured items"
|
||||
delegate: Component {
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: modelData.name
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 60
|
||||
height: 28
|
||||
color: removeArea.containsMouse ? Theme.errorHover : Theme.error
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: "Remove"
|
||||
color: Theme.errorText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: removeArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: itemList.removeItem(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Available Setting Components:**
|
||||
|
||||
All settings automatically save on change and load on component creation. No manual `pluginService.savePluginData()` calls needed!
|
||||
|
||||
1. **PluginSettings** - Root wrapper for all plugin settings
|
||||
- `pluginId`: Your plugin ID (required)
|
||||
- Auto-handles storage and provides saveValue/loadValue to children
|
||||
- Place all other setting components inside this wrapper
|
||||
|
||||
2. **StringSetting** - Text input field
|
||||
- `settingKey`: Storage key (required)
|
||||
- `label`: Display label (required)
|
||||
- `description`: Help text (optional)
|
||||
- `placeholder`: Input placeholder (optional)
|
||||
- `defaultValue`: Default value (optional)
|
||||
- Layout: Vertical stack (label, description, input field)
|
||||
|
||||
3. **ToggleSetting** - Boolean toggle switch
|
||||
- `settingKey`: Storage key (required)
|
||||
- `label`: Display label (required)
|
||||
- `description`: Help text (optional)
|
||||
- `defaultValue`: Default boolean (optional)
|
||||
- Layout: Horizontal (label/description left, toggle right)
|
||||
|
||||
4. **SelectionSetting** - Dropdown menu
|
||||
- `settingKey`: Storage key (required)
|
||||
- `label`: Display label (required)
|
||||
- `description`: Help text (optional)
|
||||
- `options`: Array of `{label, value}` objects or simple strings (required)
|
||||
- `defaultValue`: Default value (optional)
|
||||
- Layout: Horizontal (label/description left, dropdown right)
|
||||
- Stores the `value` field, displays the `label` field
|
||||
|
||||
5. **ListSetting** - Manage list of items (manual add/remove)
|
||||
- `settingKey`: Storage key (required)
|
||||
- `label`: Display label (required)
|
||||
- `description`: Help text (optional)
|
||||
- `delegate`: Custom item delegate Component (optional)
|
||||
- `addItem(item)`: Add item to list
|
||||
- `removeItem(index)`: Remove item from list
|
||||
- Use when you need custom UI for adding items
|
||||
|
||||
6. **ListSettingWithInput** - Complete list management with built-in form
|
||||
- `settingKey`: Storage key (required)
|
||||
- `label`: Display label (required)
|
||||
- `description`: Help text (optional)
|
||||
- `fields`: Array of field definitions (required)
|
||||
- `id`: Field ID in saved object (required)
|
||||
- `label`: Column header text (required)
|
||||
- `placeholder`: Input placeholder (optional)
|
||||
- `width`: Column width in pixels (optional, default 200)
|
||||
- `required`: Must have value to add (optional, default false)
|
||||
- `default`: Default value if empty (optional)
|
||||
- Automatically generates:
|
||||
- Column headers from field labels
|
||||
- Input fields with placeholders
|
||||
- Add button with validation
|
||||
- List display showing all field values
|
||||
- Remove buttons for each item
|
||||
- Best for collecting structured data (servers, locations, etc.)
|
||||
|
||||
**Complete Settings Example:**
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginSettings {
|
||||
pluginId: "myPlugin"
|
||||
|
||||
// Section header (optional)
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: "General Settings"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
// Text input
|
||||
StringSetting {
|
||||
settingKey: "apiKey"
|
||||
label: "API Key"
|
||||
description: "Your service API key"
|
||||
placeholder: "sk-..."
|
||||
defaultValue: ""
|
||||
}
|
||||
|
||||
// Toggle switches
|
||||
ToggleSetting {
|
||||
settingKey: "enabled"
|
||||
label: "Enable Feature"
|
||||
description: "Turn this feature on or off"
|
||||
defaultValue: true
|
||||
}
|
||||
|
||||
// Dropdown selection
|
||||
SelectionSetting {
|
||||
settingKey: "theme"
|
||||
label: "Theme"
|
||||
description: "Choose your preferred theme"
|
||||
options: [
|
||||
{label: "Dark", value: "dark"},
|
||||
{label: "Light", value: "light"},
|
||||
{label: "Auto", value: "auto"}
|
||||
]
|
||||
defaultValue: "dark"
|
||||
}
|
||||
|
||||
// Structured list with multi-field input
|
||||
ListSettingWithInput {
|
||||
settingKey: "locations"
|
||||
label: "Locations"
|
||||
description: "Track multiple locations"
|
||||
fields: [
|
||||
{id: "name", label: "Name", placeholder: "Home", width: 150, required: true},
|
||||
{id: "timezone", label: "Timezone", placeholder: "America/New_York", width: 200, required: true}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Benefits:**
|
||||
- Zero boilerplate - just define your settings
|
||||
- Automatic persistence to `settings.json`
|
||||
- Clean, consistent UI across all plugins
|
||||
- No manual `pluginService` calls needed
|
||||
- Proper layout and spacing handled automatically
|
||||
|
||||
## PluginService API
|
||||
|
||||
### Properties
|
||||
@@ -262,72 +469,84 @@ Create `MyWidget.qml`:
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Services
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
PluginComponent {
|
||||
horizontalBarPill: Component {
|
||||
StyledRect {
|
||||
width: textItem.implicitWidth + Theme.spacingM * 2
|
||||
height: parent.widgetThickness
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
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")
|
||||
StyledText {
|
||||
id: textItem
|
||||
anchors.centerIn: parent
|
||||
text: "Hello World"
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: textItem
|
||||
anchors.centerIn: parent
|
||||
text: root.displayText
|
||||
color: "#FFFFFF"
|
||||
font.pixelSize: 13
|
||||
}
|
||||
verticalBarPill: Component {
|
||||
StyledRect {
|
||||
width: parent.widgetThickness
|
||||
height: textItem.implicitWidth + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: console.log("Plugin clicked!")
|
||||
StyledText {
|
||||
id: textItem
|
||||
anchors.centerIn: parent
|
||||
text: "Hello"
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
rotation: 90
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Use `PluginComponent` wrapper for automatic property injection and bar integration. Define separate components for horizontal and vertical orientations.
|
||||
|
||||
### Step 4: Create Settings Component (Optional)
|
||||
|
||||
Create `MySettings.qml`:
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
Column {
|
||||
// PluginService is injected by PluginsTab
|
||||
property var pluginService
|
||||
|
||||
spacing: Theme.spacingM
|
||||
PluginSettings {
|
||||
pluginId: "myPlugin"
|
||||
|
||||
StyledText {
|
||||
text: "Plugin Settings"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
width: parent.width
|
||||
text: "Configure your plugin settings"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
width: parent.width
|
||||
StringSetting {
|
||||
settingKey: "text"
|
||||
label: "Display Text"
|
||||
text: pluginService ? pluginService.loadPluginData("myPlugin", "text", "Hello World") : ""
|
||||
onTextChanged: {
|
||||
if (pluginService) {
|
||||
pluginService.savePluginData("myPlugin", "text", text)
|
||||
}
|
||||
}
|
||||
description: "Text shown in the bar widget"
|
||||
placeholder: "Hello World"
|
||||
defaultValue: "Hello World"
|
||||
}
|
||||
|
||||
ToggleSetting {
|
||||
settingKey: "showIcon"
|
||||
label: "Show Icon"
|
||||
description: "Display an icon next to the text"
|
||||
defaultValue: true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user