summaryrefslogtreecommitdiff
path: root/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components')
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/ActionTile.qml132
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DetailHost.qml250
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DragDropDetailHost.qml91
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml1091
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DragDropWidgetWrapper.qml289
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/EditControls.qml245
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/HeaderPane.qml118
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/PowerButton.qml55
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/SizeControls.qml52
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/Typography.qml46
10 files changed, 2369 insertions, 0 deletions
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/ActionTile.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/ActionTile.qml
new file mode 100644
index 0000000..d85c779
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/ActionTile.qml
@@ -0,0 +1,132 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property string iconName: ""
+ property string text: ""
+ property string secondaryText: ""
+ property bool isActive: false
+ property bool enabled: true
+ property int widgetIndex: 0
+ property var widgetData: null
+ property bool editMode: false
+
+ signal clicked
+
+ width: parent ? parent.width : 200
+ height: 60
+ radius: {
+ if (Theme.cornerRadius === 0)
+ return 0;
+ return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4;
+ }
+
+ readonly property color _tileBgActive: Theme.ccTileActiveBg
+ readonly property color _tileBgInactive: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ readonly property color _tileRingActive: Theme.ccTileRing
+
+ color: isActive ? _tileBgActive : _tileBgInactive
+ border.color: isActive ? _tileRingActive : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
+ border.width: isActive ? 1 : 1
+ opacity: enabled ? 1.0 : 0.6
+
+ function hoverTint(base) {
+ const factor = 1.2;
+ return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ radius: Theme.cornerRadius
+ color: mouseArea.containsMouse ? hoverTint(root.color) : "transparent"
+ opacity: mouseArea.containsMouse ? 0.08 : 0.0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ }
+ }
+ }
+
+ Row {
+ anchors.fill: parent
+ anchors.leftMargin: Theme.spacingL + 2
+ anchors.rightMargin: Theme.spacingM
+ spacing: Theme.spacingM
+
+ DankIcon {
+ name: root.iconName
+ size: Theme.iconSize
+ color: isActive ? Theme.ccTileActiveText : Theme.ccTileInactiveIcon
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Item {
+ width: parent.width - Theme.iconSize - parent.spacing
+ height: parent.height
+
+ Column {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 2
+
+ Typography {
+ width: parent.width
+ text: root.text
+ style: Typography.Style.Body
+ color: isActive ? Theme.ccTileActiveText : Theme.surfaceText
+ elide: Text.ElideRight
+ wrapMode: Text.NoWrap
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ Typography {
+ width: parent.width
+ text: root.secondaryText
+ style: Typography.Style.Caption
+ color: isActive ? Theme.ccTileActiveText : Theme.surfaceVariantText
+ visible: text.length > 0
+ elide: Text.ElideRight
+ wrapMode: Text.NoWrap
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+ }
+ }
+
+ DankRipple {
+ id: ripple
+ cornerRadius: root.radius
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ enabled: root.enabled
+ onPressed: mouse => ripple.trigger(mouse.x, mouse.y)
+ onClicked: root.clicked()
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Behavior on radius {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DetailHost.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DetailHost.qml
new file mode 100644
index 0000000..a3b1156
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DetailHost.qml
@@ -0,0 +1,250 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Modules.ControlCenter.Details
+
+Item {
+ id: root
+
+ property string expandedSection: ""
+ property var expandedWidgetData: null
+ property var bluetoothCodecSelector: null
+ property string screenName: ""
+ property string screenModel: ""
+
+ property var pluginDetailInstance: null
+ property var widgetModel: null
+ property var collapseCallback: null
+ property real maxAvailableHeight: 9999
+
+ function getDetailHeight(section) {
+ switch (true) {
+ case section === "wifi":
+ case section === "bluetooth":
+ case section === "builtin_vpn":
+ return Math.min(350, maxAvailableHeight);
+ case section.startsWith("brightnessSlider_"):
+ return Math.min(400, maxAvailableHeight);
+ case section.startsWith("plugin_"):
+ if (pluginDetailInstance?.ccDetailHeight)
+ return Math.min(pluginDetailInstance.ccDetailHeight, maxAvailableHeight);
+ return Math.min(250, maxAvailableHeight);
+ default:
+ return Math.min(250, maxAvailableHeight);
+ }
+ }
+
+ Loader {
+ id: pluginDetailLoader
+ width: parent.width
+ height: parent.height - Theme.spacingS
+ y: Theme.spacingS
+ active: false
+ sourceComponent: null
+ }
+
+ Loader {
+ id: coreDetailLoader
+ width: parent.width
+ height: parent.height - Theme.spacingS
+ y: Theme.spacingS
+ active: false
+ sourceComponent: null
+ }
+
+ Connections {
+ target: coreDetailLoader.item
+ enabled: root.expandedSection.startsWith("brightnessSlider_")
+ ignoreUnknownSignals: true
+
+ function onDeviceNameChanged(newDeviceName) {
+ if (root.expandedWidgetData && root.expandedWidgetData.id === "brightnessSlider") {
+ const widgets = SettingsData.controlCenterWidgets || [];
+ const newWidgets = widgets.map(w => {
+ if (w.id === "brightnessSlider" && w.instanceId === root.expandedWidgetData.instanceId) {
+ const updatedWidget = Object.assign({}, w);
+ updatedWidget.deviceName = newDeviceName;
+ return updatedWidget;
+ }
+ return w;
+ });
+ SettingsData.set("controlCenterWidgets", newWidgets);
+ if (root.collapseCallback) {
+ root.collapseCallback();
+ }
+ }
+ }
+ }
+
+ Connections {
+ target: coreDetailLoader.item
+ enabled: root.expandedSection.startsWith("diskUsage_")
+ ignoreUnknownSignals: true
+
+ function onMountPathChanged(newMountPath) {
+ if (root.expandedWidgetData && root.expandedWidgetData.id === "diskUsage") {
+ const widgets = SettingsData.controlCenterWidgets || [];
+ const newWidgets = widgets.map(w => {
+ if (w.id === "diskUsage" && w.instanceId === root.expandedWidgetData.instanceId) {
+ const updatedWidget = Object.assign({}, w);
+ updatedWidget.mountPath = newMountPath;
+ return updatedWidget;
+ }
+ return w;
+ });
+ SettingsData.set("controlCenterWidgets", newWidgets);
+ if (root.collapseCallback) {
+ root.collapseCallback();
+ }
+ }
+ }
+ }
+
+ onExpandedSectionChanged: {
+ if (pluginDetailInstance) {
+ pluginDetailInstance.destroy();
+ pluginDetailInstance = null;
+ }
+ pluginDetailLoader.active = false;
+ coreDetailLoader.active = false;
+
+ if (!root.expandedSection) {
+ return;
+ }
+
+ if (root.expandedSection.startsWith("builtin_")) {
+ const builtinId = root.expandedSection;
+ let builtinInstance = null;
+
+ if (builtinId === "builtin_vpn") {
+ if (widgetModel?.vpnLoader) {
+ widgetModel.vpnLoader.active = true;
+ }
+ builtinInstance = widgetModel.vpnBuiltinInstance;
+ }
+ if (builtinId === "builtin_cups") {
+ if (widgetModel?.cupsLoader) {
+ widgetModel.cupsLoader.active = true;
+ }
+ builtinInstance = widgetModel.cupsBuiltinInstance;
+ }
+
+ if (!builtinInstance || !builtinInstance.ccDetailContent) {
+ return;
+ }
+
+ pluginDetailLoader.sourceComponent = builtinInstance.ccDetailContent;
+ pluginDetailLoader.active = parent.height > 0;
+ return;
+ }
+
+ if (root.expandedSection.startsWith("plugin_")) {
+ const pluginId = root.expandedSection.replace("plugin_", "");
+ const pluginComponent = PluginService.pluginWidgetComponents[pluginId];
+ if (!pluginComponent) {
+ return;
+ }
+
+ pluginDetailInstance = pluginComponent.createObject(null);
+ if (!pluginDetailInstance || !pluginDetailInstance.ccDetailContent) {
+ if (pluginDetailInstance) {
+ pluginDetailInstance.destroy();
+ pluginDetailInstance = null;
+ }
+ return;
+ }
+
+ pluginDetailLoader.sourceComponent = pluginDetailInstance.ccDetailContent;
+ pluginDetailLoader.active = parent.height > 0;
+ return;
+ }
+
+ if (root.expandedSection.startsWith("diskUsage_")) {
+ coreDetailLoader.sourceComponent = diskUsageDetailComponent;
+ coreDetailLoader.active = parent.height > 0;
+ return;
+ }
+
+ if (root.expandedSection.startsWith("brightnessSlider_")) {
+ coreDetailLoader.sourceComponent = brightnessDetailComponent;
+ coreDetailLoader.active = parent.height > 0;
+ return;
+ }
+
+ switch (root.expandedSection) {
+ case "network":
+ case "wifi":
+ coreDetailLoader.sourceComponent = networkDetailComponent;
+ break;
+ case "bluetooth":
+ coreDetailLoader.sourceComponent = bluetoothDetailComponent;
+ break;
+ case "audioOutput":
+ coreDetailLoader.sourceComponent = audioOutputDetailComponent;
+ break;
+ case "audioInput":
+ coreDetailLoader.sourceComponent = audioInputDetailComponent;
+ break;
+ case "battery":
+ coreDetailLoader.sourceComponent = batteryDetailComponent;
+ break;
+ default:
+ return;
+ }
+
+ coreDetailLoader.active = parent.height > 0;
+ }
+
+ Component {
+ id: networkDetailComponent
+ NetworkDetail {}
+ }
+
+ Component {
+ id: bluetoothDetailComponent
+ BluetoothDetail {
+ id: bluetoothDetail
+ onShowCodecSelector: function (device) {
+ if (root.bluetoothCodecSelector) {
+ root.bluetoothCodecSelector.show(device);
+ root.bluetoothCodecSelector.codecSelected.connect(function (deviceAddress, codecName) {
+ bluetoothDetail.updateDeviceCodecDisplay(deviceAddress, codecName);
+ });
+ }
+ }
+ }
+ }
+
+ Component {
+ id: audioOutputDetailComponent
+ AudioOutputDetail {}
+ }
+
+ Component {
+ id: audioInputDetailComponent
+ AudioInputDetail {}
+ }
+
+ Component {
+ id: batteryDetailComponent
+ BatteryDetail {}
+ }
+
+ Component {
+ id: diskUsageDetailComponent
+ DiskUsageDetail {
+ currentMountPath: root.expandedWidgetData?.mountPath || "/"
+ instanceId: root.expandedWidgetData?.instanceId || ""
+ }
+ }
+
+ Component {
+ id: brightnessDetailComponent
+ BrightnessDetail {
+ initialDeviceName: root.expandedWidgetData?.deviceName || ""
+ instanceId: root.expandedWidgetData?.instanceId || ""
+ screenName: root.screenName
+ screenModel: root.screenModel
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DragDropDetailHost.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DragDropDetailHost.qml
new file mode 100644
index 0000000..4a7ff19
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DragDropDetailHost.qml
@@ -0,0 +1,91 @@
+import QtQuick
+import qs.Common
+import qs.Modules.ControlCenter.Details
+
+Item {
+ id: root
+
+ property string expandedSection: ""
+ property var expandedWidgetData: null
+
+ height: active ? 250 : 0
+ visible: active
+
+ readonly property bool active: expandedSection !== ""
+
+ Loader {
+ anchors.fill: parent
+ anchors.topMargin: Theme.spacingS
+ sourceComponent: {
+ if (!root.active)
+ return null;
+
+ if (expandedSection.startsWith("diskUsage_")) {
+ return diskUsageDetailComponent;
+ }
+
+ switch (expandedSection) {
+ case "wifi":
+ return networkDetailComponent;
+ case "bluetooth":
+ return bluetoothDetailComponent;
+ case "audioOutput":
+ return audioOutputDetailComponent;
+ case "audioInput":
+ return audioInputDetailComponent;
+ case "battery":
+ return batteryDetailComponent;
+ default:
+ return null;
+ }
+ }
+ }
+
+ Component {
+ id: networkDetailComponent
+ NetworkDetail {}
+ }
+
+ Component {
+ id: bluetoothDetailComponent
+ BluetoothDetail {}
+ }
+
+ Component {
+ id: audioOutputDetailComponent
+ AudioOutputDetail {}
+ }
+
+ Component {
+ id: audioInputDetailComponent
+ AudioInputDetail {}
+ }
+
+ Component {
+ id: batteryDetailComponent
+ BatteryDetail {}
+ }
+
+ Component {
+ id: diskUsageDetailComponent
+ DiskUsageDetail {
+ currentMountPath: root.expandedWidgetData?.mountPath || "/"
+ instanceId: root.expandedWidgetData?.instanceId || ""
+
+ onMountPathChanged: newMountPath => {
+ if (root.expandedWidgetData && root.expandedWidgetData.id === "diskUsage") {
+ const widgets = SettingsData.controlCenterWidgets || [];
+ const newWidgets = widgets.map(w => {
+ if (w.id === "diskUsage" && w.instanceId === root.expandedWidgetData.instanceId) {
+ const updatedWidget = Object.assign({}, w);
+ updatedWidget.mountPath = newMountPath;
+ return updatedWidget;
+ }
+ return w;
+ });
+ SettingsData.set("controlCenterWidgets", newWidgets);
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml
new file mode 100644
index 0000000..4cdd979
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml
@@ -0,0 +1,1091 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Modules.ControlCenter.Widgets
+import qs.Modules.ControlCenter.Components
+import "../utils/layout.js" as LayoutUtils
+
+Column {
+ id: root
+
+ property bool editMode: false
+ property string expandedSection: ""
+ property int expandedWidgetIndex: -1
+ property var model: null
+ property var expandedWidgetData: null
+ property var bluetoothCodecSelector: null
+ property bool darkModeTransitionPending: false
+ property string screenName: ""
+ property string screenModel: ""
+ property var parentScreen: null
+
+ signal expandClicked(var widgetData, int globalIndex)
+ signal removeWidget(int index)
+ signal moveWidget(int fromIndex, int toIndex)
+ signal toggleWidgetSize(int index)
+ signal collapseRequested
+
+ function requestCollapse() {
+ collapseRequested();
+ }
+
+ spacing: editMode ? Theme.spacingL : Theme.spacingS
+
+ property real maxPopoutHeight: 9999
+ property var currentRowWidgets: []
+ property real currentRowWidth: 0
+ property int expandedRowIndex: -1
+ property var colorPickerModal: null
+
+ readonly property real _maxDetailHeight: {
+ const rows = layoutResult.rows;
+ let totalRowHeight = 0;
+ for (let i = 0; i < rows.length; i++) {
+ const sliderOnly = rows[i].every(w => {
+ const id = w.id || "";
+ return id === "volumeSlider" || id === "brightnessSlider" || id === "inputVolumeSlider";
+ });
+ totalRowHeight += sliderOnly ? 36 : 60;
+ }
+ const rowSpacing = Math.max(0, rows.length - 1) * spacing;
+ return Math.max(100, maxPopoutHeight - totalRowHeight - rowSpacing);
+ }
+
+ function calculateRowsAndWidgets() {
+ return LayoutUtils.calculateRowsAndWidgets(root, expandedSection, expandedWidgetIndex);
+ }
+
+ property var layoutResult: {
+ const dummy = [expandedSection, expandedWidgetIndex, model?.controlCenterWidgets];
+ return calculateRowsAndWidgets();
+ }
+
+ onLayoutResultChanged: {
+ expandedRowIndex = layoutResult.expandedRowIndex;
+ }
+
+ function moveToTop(item) {
+ const children = root.children;
+ for (var i = 0; i < children.length; i++) {
+ if (children[i] === item)
+ continue;
+ if (children[i].z)
+ children[i].z = Math.min(children[i].z, 999);
+ }
+ item.z = 1000;
+ }
+
+ Repeater {
+ model: root.layoutResult.rows
+
+ Column {
+ width: root.width
+ spacing: 0
+ property int rowIndex: index
+ property var rowWidgets: modelData
+ property bool isSliderOnlyRow: {
+ const widgets = rowWidgets || [];
+ if (widgets.length === 0)
+ return false;
+ return widgets.every(w => w.id === "volumeSlider" || w.id === "brightnessSlider" || w.id === "inputVolumeSlider");
+ }
+ topPadding: isSliderOnlyRow ? (root.editMode ? 4 : -6) : 0
+ bottomPadding: isSliderOnlyRow ? (root.editMode ? 4 : -6) : 0
+
+ Flow {
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Repeater {
+ model: rowWidgets || []
+
+ DragDropWidgetWrapper {
+ widgetData: modelData
+ property int globalWidgetIndex: {
+ const widgets = SettingsData.controlCenterWidgets || [];
+ for (var i = 0; i < widgets.length; i++) {
+ if (widgets[i].id === modelData.id) {
+ if (modelData.id === "diskUsage" || modelData.id === "brightnessSlider") {
+ if (widgets[i].instanceId === modelData.instanceId) {
+ return i;
+ }
+ } else {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+ property int widgetWidth: modelData.width || 50
+ width: {
+ const baseWidth = root.width;
+ const spacing = Theme.spacingS;
+ if (widgetWidth <= 25) {
+ return (baseWidth - spacing * 3) / 4;
+ } else if (widgetWidth <= 50) {
+ return (baseWidth - spacing) / 2;
+ } else if (widgetWidth <= 75) {
+ return (baseWidth - spacing * 2) * 0.75;
+ } else {
+ return baseWidth;
+ }
+ }
+ height: isSliderOnlyRow ? 48 : 60
+
+ editMode: root.editMode
+ widgetIndex: globalWidgetIndex
+ gridCellWidth: width
+ gridCellHeight: height
+ gridColumns: 4
+ gridLayout: root
+ isSlider: {
+ const id = modelData.id || "";
+ return id === "volumeSlider" || id === "brightnessSlider" || id === "inputVolumeSlider";
+ }
+
+ widgetComponent: {
+ const id = modelData.id || "";
+ if (id.startsWith("builtin_")) {
+ return builtinPluginWidgetComponent;
+ } else if (id.startsWith("plugin_")) {
+ return pluginWidgetComponent;
+ } else if (id === "wifi" || id === "bluetooth" || id === "audioOutput" || id === "audioInput") {
+ return compoundPillComponent;
+ } else if (id === "volumeSlider") {
+ return audioSliderComponent;
+ } else if (id === "brightnessSlider") {
+ return brightnessSliderComponent;
+ } else if (id === "inputVolumeSlider") {
+ return inputAudioSliderComponent;
+ } else if (id === "battery") {
+ return widgetWidth <= 25 ? smallBatteryComponent : batteryPillComponent;
+ } else if (id === "diskUsage") {
+ return widgetWidth <= 25 ? smallDiskUsageComponent : diskUsagePillComponent;
+ } else if (id === "colorPicker") {
+ return colorPickerPillComponent;
+ } else {
+ return widgetWidth <= 25 ? smallToggleComponent : toggleButtonComponent;
+ }
+ }
+
+ onWidgetMoved: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
+ onRemoveWidget: index => root.removeWidget(index)
+ onToggleWidgetSize: index => root.toggleWidgetSize(index)
+ }
+ }
+ }
+
+ DetailHost {
+ id: detailHost
+ width: parent.width
+ maxAvailableHeight: root._maxDetailHeight
+ height: active ? (getDetailHeight(root.expandedSection) + Theme.spacingS) : 0
+ property bool active: {
+ if (root.expandedSection === "")
+ return false;
+
+ if (root.expandedSection.startsWith("diskUsage_") && root.expandedWidgetData) {
+ const expandedInstanceId = root.expandedWidgetData.instanceId;
+ return rowWidgets.some(w => w.id === "diskUsage" && w.instanceId === expandedInstanceId);
+ }
+
+ if (root.expandedSection.startsWith("brightnessSlider_") && root.expandedWidgetData) {
+ const expandedInstanceId = root.expandedWidgetData.instanceId;
+ return rowWidgets.some(w => w.id === "brightnessSlider" && w.instanceId === expandedInstanceId);
+ }
+
+ return rowIndex === root.expandedRowIndex;
+ }
+ visible: active
+ expandedSection: root.expandedSection
+ expandedWidgetData: root.expandedWidgetData
+ bluetoothCodecSelector: root.bluetoothCodecSelector
+ widgetModel: root.model
+ collapseCallback: root.requestCollapse
+ screenName: root.screenName
+ screenModel: root.screenModel
+ }
+ }
+ }
+
+ Component {
+ id: errorPillComponent
+ ErrorPill {
+ property var widgetData: parent.widgetData || {}
+ width: parent.width
+ height: 60
+ primaryMessage: {
+ if (!DMSService.dmsAvailable) {
+ return I18n.tr("DMS_SOCKET not available");
+ }
+ return I18n.tr("NM not supported");
+ }
+ secondaryMessage: I18n.tr("update dms for NM integration.")
+ }
+ }
+
+ Component {
+ id: compoundPillComponent
+ CompoundPill {
+ property var widgetData: parent.widgetData || {}
+ property int widgetIndex: parent.widgetIndex || 0
+ property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
+ width: parent.width
+ height: 60
+ iconName: {
+ switch (widgetData.id || "") {
+ case "wifi":
+ {
+ if (NetworkService.wifiToggling)
+ return "sync";
+
+ const status = NetworkService.networkStatus;
+ if (status === "ethernet")
+ return "settings_ethernet";
+ if (status === "vpn")
+ return NetworkService.ethernetConnected ? "settings_ethernet" : NetworkService.wifiSignalIcon;
+ if (status === "wifi")
+ return NetworkService.wifiSignalIcon;
+ if (NetworkService.wifiEnabled)
+ return "wifi_off";
+ return "wifi_off";
+ }
+ case "bluetooth":
+ {
+ if (!BluetoothService.available)
+ return "bluetooth_disabled";
+ if (!BluetoothService.adapter || !BluetoothService.adapter.enabled)
+ return "bluetooth_disabled";
+ return "bluetooth";
+ }
+ case "audioOutput":
+ {
+ if (!AudioService.sink)
+ return "volume_off";
+ let volume = AudioService.sink.audio.volume;
+ let muted = AudioService.sink.audio.muted;
+ if (muted)
+ return "volume_off";
+ if (volume === 0.0)
+ return "volume_mute";
+ if (volume <= 0.33)
+ return "volume_down";
+ if (volume <= 0.66)
+ return "volume_up";
+ return "volume_up";
+ }
+ case "audioInput":
+ {
+ if (!AudioService.source)
+ return "mic_off";
+ let muted = AudioService.source.audio.muted;
+ return muted ? "mic_off" : "mic";
+ }
+ default:
+ return widgetDef?.icon || "help";
+ }
+ }
+ primaryText: {
+ switch (widgetData.id || "") {
+ case "wifi":
+ {
+ if (NetworkService.wifiToggling)
+ return NetworkService.wifiEnabled ? I18n.tr("Disabling WiFi...", "network status") : I18n.tr("Enabling WiFi...", "network status");
+
+ const status = NetworkService.networkStatus;
+ if (status === "ethernet")
+ return I18n.tr("Ethernet", "network status");
+ if (status === "vpn") {
+ if (NetworkService.ethernetConnected)
+ return I18n.tr("Ethernet", "network status");
+ if (NetworkService.wifiConnected && NetworkService.currentWifiSSID)
+ return NetworkService.currentWifiSSID;
+ }
+ if (status === "wifi" && NetworkService.currentWifiSSID)
+ return NetworkService.currentWifiSSID;
+ if (NetworkService.wifiEnabled)
+ return I18n.tr("Not connected", "network status");
+ return I18n.tr("WiFi off", "network status");
+ }
+ case "bluetooth":
+ {
+ if (!BluetoothService.available)
+ return I18n.tr("Bluetooth", "bluetooth status");
+ if (!BluetoothService.adapter)
+ return I18n.tr("No adapter", "bluetooth status");
+ if (!BluetoothService.adapter.enabled)
+ return I18n.tr("Disabled", "bluetooth status");
+ return I18n.tr("Enabled", "bluetooth status");
+ }
+ case "audioOutput":
+ return AudioService.sink?.description || I18n.tr("No output device", "audio status");
+ case "audioInput":
+ return AudioService.source?.description || I18n.tr("No input device", "audio status");
+ default:
+ return widgetDef?.text || I18n.tr("Unknown", "widget status");
+ }
+ }
+ secondaryText: {
+ switch (widgetData.id || "") {
+ case "wifi":
+ {
+ if (NetworkService.wifiToggling)
+ return I18n.tr("Please wait...", "network status");
+
+ const status = NetworkService.networkStatus;
+ if (status === "ethernet")
+ return I18n.tr("Connected", "network status");
+ if (status === "vpn") {
+ if (NetworkService.ethernetConnected)
+ return I18n.tr("Connected", "network status");
+ if (NetworkService.wifiConnected)
+ return NetworkService.wifiSignalStrength > 0 ? NetworkService.wifiSignalStrength + "%" : I18n.tr("Connected", "network status");
+ }
+ if (status === "wifi")
+ return NetworkService.wifiSignalStrength > 0 ? NetworkService.wifiSignalStrength + "%" : I18n.tr("Connected", "network status");
+ if (NetworkService.wifiEnabled)
+ return I18n.tr("Select network", "network status");
+ return "";
+ }
+ case "bluetooth":
+ {
+ if (!BluetoothService.available)
+ return I18n.tr("No adapters", "bluetooth status");
+ if (!BluetoothService.adapter || !BluetoothService.adapter.enabled)
+ return I18n.tr("Off", "bluetooth status");
+ const primaryDevice = (() => {
+ if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
+ return null;
+ let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))];
+ for (let device of devices) {
+ if (device && device.connected)
+ return device;
+ }
+ return null;
+ })();
+ if (primaryDevice)
+ return primaryDevice.name || primaryDevice.alias || primaryDevice.deviceName || I18n.tr("Connected Device", "bluetooth status");
+ return I18n.tr("No devices", "bluetooth status");
+ }
+ case "audioOutput":
+ {
+ if (!AudioService.sink)
+ return I18n.tr("Select device", "audio status");
+ if (AudioService.sink.audio.muted)
+ return I18n.tr("Muted", "audio status");
+ const volume = AudioService.sink.audio.volume;
+ if (typeof volume !== "number" || isNaN(volume))
+ return "0%";
+ return Math.round(volume * 100) + "%";
+ }
+ case "audioInput":
+ {
+ if (!AudioService.source)
+ return I18n.tr("Select device", "audio status");
+ if (AudioService.source.audio.muted)
+ return I18n.tr("Muted", "audio status");
+ const volume = AudioService.source.audio.volume;
+ if (typeof volume !== "number" || isNaN(volume))
+ return "0%";
+ return Math.round(volume * 100) + "%";
+ }
+ default:
+ return widgetDef?.description || "";
+ }
+ }
+ isActive: {
+ switch (widgetData.id || "") {
+ case "wifi":
+ {
+ if (NetworkService.wifiToggling)
+ return false;
+
+ const status = NetworkService.networkStatus;
+ if (status === "ethernet")
+ return true;
+ if (status === "vpn")
+ return NetworkService.ethernetConnected || NetworkService.wifiConnected;
+ if (status === "wifi")
+ return true;
+ return NetworkService.wifiEnabled;
+ }
+ case "bluetooth":
+ return !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled);
+ case "audioOutput":
+ return !!(AudioService.sink && !AudioService.sink.audio.muted);
+ case "audioInput":
+ return !!(AudioService.source && !AudioService.source.audio.muted);
+ default:
+ return false;
+ }
+ }
+ enabled: widgetDef?.enabled ?? true
+ onToggled: {
+ if (root.editMode)
+ return;
+ switch (widgetData.id || "") {
+ case "wifi":
+ {
+ if (NetworkService.networkStatus !== "ethernet" && !NetworkService.wifiToggling) {
+ NetworkService.toggleWifiRadio();
+ }
+ break;
+ }
+ case "bluetooth":
+ {
+ if (BluetoothService.available && BluetoothService.adapter) {
+ BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled;
+ }
+ break;
+ }
+ case "audioOutput":
+ {
+ if (AudioService.sink && AudioService.sink.audio) {
+ AudioService.sink.audio.muted = !AudioService.sink.audio.muted;
+ }
+ break;
+ }
+ case "audioInput":
+ {
+ if (AudioService.source && AudioService.source.audio) {
+ AudioService.source.audio.muted = !AudioService.source.audio.muted;
+ }
+ break;
+ }
+ }
+ }
+ onExpandClicked: {
+ if (root.editMode)
+ return;
+ root.expandClicked(widgetData, widgetIndex);
+ }
+ onWheelEvent: function (wheelEvent) {
+ if (root.editMode)
+ return;
+ const id = widgetData.id || "";
+ if (id === "audioOutput") {
+ if (!AudioService.sink || !AudioService.sink.audio)
+ return;
+ let delta = wheelEvent.angleDelta.y;
+ let maxVol = AudioService.sinkMaxVolume;
+ let currentVolume = AudioService.sink.audio.volume * 100;
+ let newVolume;
+ if (delta > 0)
+ newVolume = Math.min(maxVol, currentVolume + 5);
+ else
+ newVolume = Math.max(0, currentVolume - 5);
+ AudioService.sink.audio.muted = false;
+ AudioService.sink.audio.volume = newVolume / 100;
+ wheelEvent.accepted = true;
+ } else if (id === "audioInput") {
+ if (!AudioService.source || !AudioService.source.audio)
+ return;
+ let delta = wheelEvent.angleDelta.y;
+ let currentVolume = AudioService.source.audio.volume * 100;
+ let newVolume;
+ if (delta > 0)
+ newVolume = Math.min(100, currentVolume + 5);
+ else
+ newVolume = Math.max(0, currentVolume - 5);
+ AudioService.source.audio.muted = false;
+ AudioService.source.audio.volume = newVolume / 100;
+ wheelEvent.accepted = true;
+ }
+ }
+ }
+ }
+
+ Component {
+ id: audioSliderComponent
+ Item {
+ property var widgetData: parent.widgetData || {}
+ property int widgetIndex: parent.widgetIndex || 0
+ width: parent.width
+ height: 16
+
+ AudioSliderRow {
+ anchors.centerIn: parent
+ width: parent.width
+ height: 14
+ property color sliderTrackColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ }
+ }
+ }
+
+ Component {
+ id: brightnessSliderComponent
+ Item {
+ property var widgetData: parent.widgetData || {}
+ property int widgetIndex: parent.widgetIndex || 0
+ width: parent.width
+ height: 16
+
+ BrightnessSliderRow {
+ id: brightnessSliderRow
+ anchors.centerIn: parent
+ width: parent.width
+ height: 14
+ deviceName: widgetData.deviceName || ""
+ instanceId: widgetData.instanceId || ""
+ screenName: root.screenName
+ parentScreen: root.parentScreen
+ property color sliderTrackColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+
+ onIconClicked: {
+ if (!root.editMode && DisplayService.devices && DisplayService.devices.length > 1) {
+ root.expandClicked(widgetData, widgetIndex);
+ }
+ }
+ }
+ }
+ }
+
+ Component {
+ id: inputAudioSliderComponent
+ Item {
+ property var widgetData: parent.widgetData || {}
+ property int widgetIndex: parent.widgetIndex || 0
+ width: parent.width
+ height: 16
+
+ InputAudioSliderRow {
+ anchors.centerIn: parent
+ width: parent.width
+ height: 14
+ property color sliderTrackColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ }
+ }
+ }
+
+ Component {
+ id: batteryPillComponent
+ BatteryPill {
+ property var widgetData: parent.widgetData || {}
+ property int widgetIndex: parent.widgetIndex || 0
+ width: parent.width
+ height: 60
+
+ onExpandClicked: {
+ if (!root.editMode) {
+ root.expandClicked(widgetData, widgetIndex);
+ }
+ }
+ }
+ }
+
+ Component {
+ id: smallBatteryComponent
+ SmallBatteryButton {
+ property var widgetData: parent.widgetData || {}
+ property int widgetIndex: parent.widgetIndex || 0
+ width: parent.width
+ height: 48
+
+ onClicked: {
+ if (!root.editMode) {
+ root.expandClicked(widgetData, widgetIndex);
+ }
+ }
+ }
+ }
+
+ Component {
+ id: toggleButtonComponent
+ ToggleButton {
+ property var widgetData: parent.widgetData || {}
+ property int widgetIndex: parent.widgetIndex || 0
+ width: parent.width
+ height: 60
+
+ iconName: {
+ switch (widgetData.id || "") {
+ case "nightMode":
+ return DisplayService.nightModeEnabled ? "nightlight" : "dark_mode";
+ case "darkMode":
+ return "contrast";
+ case "doNotDisturb":
+ return SessionData.doNotDisturb ? "do_not_disturb_on" : "do_not_disturb_off";
+ case "idleInhibitor":
+ return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle";
+ default:
+ return "help";
+ }
+ }
+
+ text: {
+ switch (widgetData.id || "") {
+ case "nightMode":
+ return I18n.tr("Night Mode");
+ case "darkMode":
+ return I18n.tr("Dark Mode");
+ case "doNotDisturb":
+ return I18n.tr("Do Not Disturb");
+ case "idleInhibitor":
+ return SessionService.idleInhibited ? I18n.tr("Keeping Awake") : I18n.tr("Keep Awake");
+ default:
+ return I18n.tr("Unknown", "widget status");
+ }
+ }
+
+ iconRotation: {
+ if (widgetData.id !== "darkMode")
+ return 0;
+ if (darkModeTransitionPending) {
+ return SessionData.isLightMode ? 180 : 0;
+ }
+ return SessionData.isLightMode ? 180 : 0;
+ }
+
+ isActive: {
+ switch (widgetData.id || "") {
+ case "nightMode":
+ return DisplayService.nightModeEnabled || false;
+ case "darkMode":
+ return !SessionData.isLightMode;
+ case "doNotDisturb":
+ return SessionData.doNotDisturb || false;
+ case "idleInhibitor":
+ return SessionService.idleInhibited || false;
+ default:
+ return false;
+ }
+ }
+
+ enabled: !root.editMode
+
+ onClicked: {
+ if (root.editMode)
+ return;
+ switch (widgetData.id || "") {
+ case "nightMode":
+ {
+ if (DisplayService.automationAvailable)
+ DisplayService.toggleNightMode();
+ break;
+ }
+ case "darkMode":
+ {
+ const newMode = !SessionData.isLightMode;
+ Theme.screenTransition();
+ Theme.setLightMode(newMode);
+ break;
+ }
+ case "doNotDisturb":
+ {
+ SessionData.setDoNotDisturb(!SessionData.doNotDisturb);
+ break;
+ }
+ case "idleInhibitor":
+ {
+ SessionService.toggleIdleInhibit();
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ Component {
+ id: smallToggleComponent
+ SmallToggleButton {
+ property var widgetData: parent.widgetData || {}
+ property int widgetIndex: parent.widgetIndex || 0
+ width: parent.width
+ height: 48
+
+ iconName: {
+ switch (widgetData.id || "") {
+ case "nightMode":
+ return DisplayService.nightModeEnabled ? "nightlight" : "dark_mode";
+ case "darkMode":
+ return "contrast";
+ case "doNotDisturb":
+ return SessionData.doNotDisturb ? "do_not_disturb_on" : "do_not_disturb_off";
+ case "idleInhibitor":
+ return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle";
+ default:
+ return "help";
+ }
+ }
+
+ iconRotation: {
+ if (widgetData.id !== "darkMode")
+ return 0;
+ if (darkModeTransitionPending) {
+ return SessionData.isLightMode ? 180 : 0;
+ }
+ return SessionData.isLightMode ? 180 : 0;
+ }
+
+ isActive: {
+ switch (widgetData.id || "") {
+ case "nightMode":
+ return DisplayService.nightModeEnabled || false;
+ case "darkMode":
+ return !SessionData.isLightMode;
+ case "doNotDisturb":
+ return SessionData.doNotDisturb || false;
+ case "idleInhibitor":
+ return SessionService.idleInhibited || false;
+ default:
+ return false;
+ }
+ }
+
+ enabled: !root.editMode
+
+ onClicked: {
+ if (root.editMode)
+ return;
+ switch (widgetData.id || "") {
+ case "nightMode":
+ {
+ if (DisplayService.automationAvailable)
+ DisplayService.toggleNightMode();
+ break;
+ }
+ case "darkMode":
+ {
+ const newMode = !SessionData.isLightMode;
+ Theme.screenTransition();
+ Theme.setLightMode(newMode);
+ break;
+ }
+ case "doNotDisturb":
+ {
+ SessionData.setDoNotDisturb(!SessionData.doNotDisturb);
+ break;
+ }
+ case "idleInhibitor":
+ {
+ SessionService.toggleIdleInhibit();
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ Component {
+ id: diskUsagePillComponent
+ DiskUsagePill {
+ property var widgetData: parent.widgetData || {}
+ property int widgetIndex: parent.widgetIndex || 0
+ width: parent.width
+ height: 60
+
+ mountPath: widgetData.mountPath || "/"
+ instanceId: widgetData.instanceId || ""
+
+ onExpandClicked: {
+ if (!root.editMode) {
+ root.expandClicked(widgetData, widgetIndex);
+ }
+ }
+ }
+ }
+
+ Component {
+ id: smallDiskUsageComponent
+ SmallDiskUsageButton {
+ property var widgetData: parent.widgetData || {}
+ property int widgetIndex: parent.widgetIndex || 0
+ width: parent.width
+ height: 48
+
+ mountPath: widgetData.mountPath || "/"
+ instanceId: widgetData.instanceId || ""
+
+ onClicked: {
+ if (!root.editMode) {
+ root.expandClicked(widgetData, widgetIndex);
+ }
+ }
+ }
+ }
+
+ Component {
+ id: colorPickerPillComponent
+ ColorPickerPill {
+ property var widgetData: parent.widgetData || {}
+ property int widgetIndex: parent.widgetIndex || 0
+ width: parent.width
+ height: 60
+
+ colorPickerModal: root.colorPickerModal
+ }
+ }
+
+ Component {
+ id: builtinPluginWidgetComponent
+ Loader {
+ property var widgetData: parent.widgetData || {}
+ property int widgetIndex: parent.widgetIndex || 0
+ property int widgetWidth: widgetData.width || 50
+ width: parent.width
+ height: 60
+
+ property var builtinInstance: null
+
+ Component.onCompleted: {
+ const id = widgetData.id || "";
+ if (id === "builtin_vpn") {
+ if (root.model?.vpnLoader) {
+ root.model.vpnLoader.active = true;
+ }
+ builtinInstance = Qt.binding(() => root.model?.vpnBuiltinInstance);
+ }
+ if (id === "builtin_cups") {
+ if (root.model?.cupsLoader) {
+ root.model.cupsLoader.active = true;
+ }
+ builtinInstance = Qt.binding(() => root.model?.cupsBuiltinInstance);
+ }
+ }
+
+ sourceComponent: {
+ if (!builtinInstance)
+ return null;
+
+ const hasDetail = builtinInstance.ccDetailContent !== null;
+
+ if (widgetWidth <= 25) {
+ return builtinSmallToggleComponent;
+ } else if (hasDetail) {
+ return builtinCompoundPillComponent;
+ } else {
+ return builtinToggleComponent;
+ }
+ }
+ }
+ }
+
+ Component {
+ id: builtinCompoundPillComponent
+ CompoundPill {
+ property var widgetData: parent.widgetData || {}
+ property int widgetIndex: parent.widgetIndex || 0
+ property var builtinInstance: parent.builtinInstance
+
+ iconName: builtinInstance?.ccWidgetIcon || "extension"
+ primaryText: builtinInstance?.ccWidgetPrimaryText || "Built-in"
+ secondaryText: builtinInstance?.ccWidgetSecondaryText || ""
+ isActive: builtinInstance?.ccWidgetIsActive || false
+
+ onToggled: {
+ if (root.editMode)
+ return;
+ if (builtinInstance) {
+ builtinInstance.ccWidgetToggled();
+ }
+ }
+
+ onExpandClicked: {
+ if (root.editMode)
+ return;
+ if (builtinInstance) {
+ builtinInstance.ccWidgetExpanded();
+ }
+ root.expandClicked(widgetData, widgetIndex);
+ }
+ }
+ }
+
+ Component {
+ id: builtinToggleComponent
+ ToggleButton {
+ property var widgetData: parent.widgetData || {}
+ property int widgetIndex: parent.widgetIndex || 0
+ property var builtinInstance: parent.builtinInstance
+
+ iconName: builtinInstance?.ccWidgetIcon || "extension"
+ text: builtinInstance?.ccWidgetPrimaryText || "Built-in"
+ isActive: builtinInstance?.ccWidgetIsActive || false
+ enabled: !root.editMode
+
+ onClicked: {
+ if (root.editMode)
+ return;
+ if (builtinInstance) {
+ builtinInstance.ccWidgetToggled();
+ }
+ }
+ }
+ }
+
+ Component {
+ id: builtinSmallToggleComponent
+ SmallToggleButton {
+ property var widgetData: parent.widgetData || {}
+ property int widgetIndex: parent.widgetIndex || 0
+ property var builtinInstance: parent.builtinInstance
+
+ iconName: builtinInstance?.ccWidgetIcon || "extension"
+ isActive: builtinInstance?.ccWidgetIsActive || false
+ enabled: !root.editMode
+
+ onClicked: {
+ if (root.editMode)
+ return;
+ if (builtinInstance) {
+ builtinInstance.ccWidgetToggled();
+ }
+ }
+ }
+ }
+
+ Component {
+ id: pluginWidgetComponent
+ Loader {
+ property var widgetData: parent.widgetData || {}
+ property int widgetIndex: parent.widgetIndex || 0
+ property int widgetWidth: widgetData.width || 50
+ width: parent.width
+ height: 60
+
+ property var pluginInstance: null
+ property string pluginId: widgetData.id?.replace("plugin_", "") || ""
+
+ sourceComponent: {
+ if (!pluginInstance)
+ return null;
+
+ const hasDetail = pluginInstance.ccDetailContent !== null;
+
+ if (widgetWidth <= 25) {
+ return pluginSmallToggleComponent;
+ } else if (hasDetail) {
+ return pluginCompoundPillComponent;
+ } else {
+ return pluginToggleComponent;
+ }
+ }
+
+ function tryCreatePluginInstance() {
+ const pluginComponent = PluginService.pluginWidgetComponents[pluginId];
+ if (!pluginComponent)
+ return false;
+ try {
+ const instance = pluginComponent.createObject(null, {
+ "pluginId": pluginId,
+ "pluginService": PluginService,
+ "visible": false,
+ "width": 0,
+ "height": 0
+ });
+ if (instance) {
+ pluginInstance = instance;
+ return true;
+ }
+ } catch (e) {
+ console.warn("DragDropGrid: stale plugin component for", pluginId, "- reloading");
+ PluginService.reloadPlugin(pluginId);
+ }
+ return false;
+ }
+
+ Component.onCompleted: {
+ Qt.callLater(() => tryCreatePluginInstance());
+ }
+
+ Connections {
+ target: PluginService
+ function onPluginDataChanged(changedPluginId) {
+ if (changedPluginId === pluginId && pluginInstance) {
+ pluginInstance.loadPluginData();
+ }
+ }
+ function onPluginLoaded(loadedPluginId) {
+ if (loadedPluginId !== pluginId || pluginInstance)
+ return;
+ Qt.callLater(() => tryCreatePluginInstance());
+ }
+ }
+
+ Component.onDestruction: {
+ if (pluginInstance) {
+ pluginInstance.destroy();
+ }
+ }
+ }
+ }
+
+ Component {
+ id: pluginCompoundPillComponent
+ CompoundPill {
+ property var widgetData: parent.widgetData || {}
+ property int widgetIndex: parent.widgetIndex || 0
+ property var pluginInstance: parent.pluginInstance
+
+ iconName: pluginInstance?.ccWidgetIcon || "extension"
+ primaryText: pluginInstance?.ccWidgetPrimaryText || "Plugin"
+ secondaryText: pluginInstance?.ccWidgetSecondaryText || ""
+ isActive: pluginInstance?.ccWidgetIsActive || false
+
+ onToggled: {
+ if (root.editMode)
+ return;
+ if (pluginInstance) {
+ pluginInstance.ccWidgetToggled();
+ }
+ }
+
+ onExpandClicked: {
+ if (root.editMode)
+ return;
+ if (pluginInstance) {
+ pluginInstance.ccWidgetExpanded();
+ }
+ root.expandClicked(widgetData, widgetIndex);
+ }
+ }
+ }
+
+ Component {
+ id: pluginToggleComponent
+ ToggleButton {
+ property var widgetData: parent.widgetData || {}
+ property int widgetIndex: parent.widgetIndex || 0
+ property var pluginInstance: parent.pluginInstance
+ property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
+
+ iconName: pluginInstance?.ccWidgetIcon || widgetDef?.icon || "extension"
+ text: pluginInstance?.ccWidgetPrimaryText || widgetDef?.text || "Plugin"
+ secondaryText: pluginInstance?.ccWidgetSecondaryText || ""
+ isActive: pluginInstance?.ccWidgetIsActive || false
+ enabled: !root.editMode
+
+ onClicked: {
+ if (root.editMode)
+ return;
+ if (pluginInstance) {
+ pluginInstance.ccWidgetToggled();
+ }
+ }
+ }
+ }
+
+ Component {
+ id: pluginSmallToggleComponent
+ SmallToggleButton {
+ property var widgetData: parent.widgetData || {}
+ property int widgetIndex: parent.widgetIndex || 0
+ property var pluginInstance: parent.pluginInstance
+ property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
+
+ iconName: pluginInstance?.ccWidgetIcon || widgetDef?.icon || "extension"
+ isActive: pluginInstance?.ccWidgetIsActive || false
+ enabled: !root.editMode
+
+ onClicked: {
+ if (root.editMode)
+ return;
+ if (pluginInstance && pluginInstance.ccDetailContent) {
+ root.expandClicked(widgetData, widgetIndex);
+ } else if (pluginInstance) {
+ pluginInstance.ccWidgetToggled();
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DragDropWidgetWrapper.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DragDropWidgetWrapper.qml
new file mode 100644
index 0000000..7cb0e2c
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DragDropWidgetWrapper.qml
@@ -0,0 +1,289 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Item {
+ id: root
+
+ property bool editMode: false
+ property var widgetData: null
+ property int widgetIndex: -1
+ property bool isSlider: false
+ property Component widgetComponent: null
+ property real gridCellWidth: 100
+ property real gridCellHeight: 60
+ property int gridColumns: 4
+ property var gridLayout: null
+
+ z: dragArea.drag.active ? 10000 : 1
+
+ signal widgetMoved(int fromIndex, int toIndex)
+ signal removeWidget(int index)
+ signal toggleWidgetSize(int index)
+
+ width: {
+ const widgetWidth = widgetData?.width || 50
+ if (widgetWidth <= 25) return gridCellWidth
+ else if (widgetWidth <= 50) return gridCellWidth * 2
+ else if (widgetWidth <= 75) return gridCellWidth * 3
+ else return gridCellWidth * 4
+ }
+ height: isSlider ? 16 : gridCellHeight
+
+ Rectangle {
+ id: dragIndicator
+ anchors.fill: parent
+ color: "transparent"
+ border.color: Theme.primary
+ border.width: dragArea.drag.active ? 2 : 0
+ radius: Theme.cornerRadius
+ opacity: dragArea.drag.active ? 0.8 : 1.0
+ z: dragArea.drag.active ? 10000 : 1
+
+ Behavior on border.width {
+ NumberAnimation { duration: 150 }
+ }
+ Behavior on opacity {
+ NumberAnimation { duration: 150 }
+ }
+ }
+
+ Loader {
+ id: widgetLoader
+ anchors.fill: parent
+ sourceComponent: widgetComponent
+ property var widgetData: root.widgetData
+ property int widgetIndex: root.widgetIndex
+ property int globalWidgetIndex: root.widgetIndex
+ property int widgetWidth: root.widgetData?.width || 50
+
+
+ MouseArea {
+ id: editModeBlocker
+ anchors.fill: parent
+ enabled: root.editMode
+ acceptedButtons: Qt.AllButtons
+ onPressed: function(mouse) { mouse.accepted = true }
+ onWheel: function(wheel) { wheel.accepted = true }
+ z: 100
+ }
+ }
+
+ MouseArea {
+ id: dragArea
+ anchors.fill: parent
+ enabled: editMode
+ cursorShape: editMode ? Qt.OpenHandCursor : Qt.PointingHandCursor
+ drag.target: editMode ? root : null
+ drag.axis: Drag.XAndYAxis
+ drag.smoothed: true
+
+ onPressed: function(mouse) {
+ if (editMode) {
+ cursorShape = Qt.ClosedHandCursor
+ if (root.gridLayout && root.gridLayout.moveToTop) {
+ root.gridLayout.moveToTop(root)
+ }
+ }
+ }
+
+ onReleased: function(mouse) {
+ if (editMode) {
+ cursorShape = Qt.OpenHandCursor
+ root.snapToGrid()
+ }
+ }
+ }
+
+ Drag.active: dragArea.drag.active
+ Drag.hotSpot.x: width / 2
+ Drag.hotSpot.y: height / 2
+
+ function swapIndices(i, j) {
+ if (i === j) return;
+ const arr = SettingsData.controlCenterWidgets;
+ if (!arr || i < 0 || j < 0 || i >= arr.length || j >= arr.length) return;
+
+ const copy = arr.slice();
+ const tmp = copy[i];
+ copy[i] = copy[j];
+ copy[j] = tmp;
+
+ SettingsData.set("controlCenterWidgets", copy);
+ }
+
+ function snapToGrid() {
+ if (!editMode || !gridLayout) return
+
+ const globalPos = root.mapToItem(gridLayout, 0, 0)
+ const cellWidth = gridLayout.width / gridColumns
+ const cellHeight = gridCellHeight + Theme.spacingS
+
+ const centerX = globalPos.x + (root.width / 2)
+ const centerY = globalPos.y + (root.height / 2)
+
+ let targetCol = Math.max(0, Math.floor(centerX / cellWidth))
+ let targetRow = Math.max(0, Math.floor(centerY / cellHeight))
+
+ targetCol = Math.min(targetCol, gridColumns - 1)
+
+ const newIndex = findBestInsertionIndex(targetRow, targetCol)
+
+ if (newIndex !== widgetIndex && newIndex >= 0 && newIndex < (SettingsData.controlCenterWidgets?.length || 0)) {
+ swapIndices(widgetIndex, newIndex)
+ }
+ }
+
+ function findBestInsertionIndex(targetRow, targetCol) {
+ const widgets = SettingsData.controlCenterWidgets || [];
+ const n = widgets.length;
+ if (!n || widgetIndex < 0 || widgetIndex >= n) return -1;
+
+ function spanFor(width) {
+ const w = width ?? 50;
+ if (w <= 25) return 1;
+ if (w <= 50) return 2;
+ if (w <= 75) return 3;
+ return 4;
+ }
+
+ const cols = gridColumns || 4;
+
+ let row = 0, col = 0;
+ let draggedOrigKey = null;
+
+ const pos = [];
+
+ for (let i = 0; i < n; i++) {
+ const span = Math.min(spanFor(widgets[i].width), cols);
+
+ if (col + span > cols) {
+ row++;
+ col = 0;
+ }
+
+ const startCol = col;
+ const centerKey = row * cols + (startCol + (span - 1) / 2);
+
+ if (i === widgetIndex) {
+ draggedOrigKey = centerKey;
+ } else {
+ pos.push({ index: i, row, startCol, span, centerKey });
+ }
+
+ col += span;
+ if (col >= cols) {
+ row++;
+ col = 0;
+ }
+ }
+
+ if (pos.length === 0) return -1;
+
+ const centerColCoord = targetCol + 0.5;
+ const targetKey = targetRow * cols + centerColCoord;
+
+ for (let k = 0; k < pos.length; k++) {
+ const p = pos[k];
+ if (p.row === targetRow && centerColCoord >= p.startCol && centerColCoord < (p.startCol + p.span)) {
+ return p.index;
+ }
+ }
+
+ let lo = 0, hi = pos.length - 1;
+ if (targetKey <= pos[0].centerKey) return pos[0].index;
+ if (targetKey >= pos[hi].centerKey) return pos[hi].index;
+
+ while (lo <= hi) {
+ const mid = (lo + hi) >> 1;
+ const mk = pos[mid].centerKey;
+ if (targetKey < mk) hi = mid - 1;
+ else if (targetKey > mk) lo = mid + 1;
+ else return pos[mid].index;
+ }
+ const movingUp = (draggedOrigKey != null) ? (targetKey < draggedOrigKey) : false;
+ return (movingUp ? pos[lo].index : pos[hi].index);
+ }
+
+ Rectangle {
+ width: 16
+ height: 16
+ radius: 8
+ color: Theme.error
+ anchors.top: parent.top
+ anchors.right: parent.right
+ anchors.margins: -4
+ visible: editMode
+ z: 10
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "close"
+ size: 12
+ color: Theme.primaryText
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: removeWidget(widgetIndex)
+ }
+ }
+
+ SizeControls {
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.margins: -6
+ visible: editMode
+ z: 10
+ currentSize: root.widgetData?.width || 50
+ isSlider: root.isSlider
+ widgetIndex: root.widgetIndex
+ onSizeChanged: (newSize) => {
+ var widgets = SettingsData.controlCenterWidgets.slice()
+ if (widgetIndex >= 0 && widgetIndex < widgets.length) {
+ widgets[widgetIndex].width = newSize
+ SettingsData.set("controlCenterWidgets", widgets)
+ }
+ }
+ }
+
+ Rectangle {
+ id: dragHandle
+ width: 16
+ height: 12
+ radius: 2
+ color: Theme.primary
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.margins: 4
+ visible: editMode
+ z: 15
+ opacity: dragArea.drag.active ? 1.0 : 0.7
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "drag_indicator"
+ size: 10
+ color: Theme.primaryText
+ }
+
+ Behavior on opacity {
+ NumberAnimation { duration: 150 }
+ }
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: editMode ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
+ radius: Theme.cornerRadius
+ border.color: "transparent"
+ border.width: 0
+ z: -1
+
+ Behavior on color {
+ ColorAnimation { duration: Theme.shortDuration }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/EditControls.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/EditControls.qml
new file mode 100644
index 0000000..50531aa
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/EditControls.qml
@@ -0,0 +1,245 @@
+import QtQuick
+import QtQuick.Controls
+import qs.Common
+import qs.Widgets
+
+Row {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property var availableWidgets: []
+ property Item popoutContent: null
+
+ signal addWidget(string widgetId)
+ signal resetToDefault
+ signal clearAll
+
+ height: 48
+ spacing: Theme.spacingS
+
+ onAddWidget: addWidgetPopup.close()
+
+ Popup {
+ id: addWidgetPopup
+ parent: popoutContent
+ x: parent ? Math.round((parent.width - width) / 2) : 0
+ y: parent ? Math.round((parent.height - height) / 2) : 0
+ width: 400
+ height: 300
+ modal: false
+ focus: true
+ closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
+
+ background: Rectangle {
+ color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ border.color: Theme.primarySelected
+ border.width: 0
+ radius: Theme.cornerRadius
+ }
+
+ contentItem: Item {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingL
+
+ Row {
+ id: headerRow
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ spacing: Theme.spacingM
+
+ DankIcon {
+ name: "add_circle"
+ size: Theme.iconSize
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Typography {
+ text: I18n.tr("Add Widget")
+ style: Typography.Style.Subtitle
+ color: Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ DankListView {
+ anchors.top: headerRow.bottom
+ anchors.topMargin: Theme.spacingM
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ spacing: Theme.spacingS
+ clip: true
+ model: root.availableWidgets
+
+ delegate: Rectangle {
+ width: 400 - Theme.spacingL * 2
+ height: 50
+ radius: Theme.cornerRadius
+ color: widgetMouseArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
+ border.width: 0
+
+ Row {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingM
+
+ DankIcon {
+ name: modelData.icon
+ size: Theme.iconSize
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 2
+ width: 400 - Theme.spacingL * 2 - Theme.iconSize - Theme.spacingM * 3 - Theme.iconSize
+
+ Typography {
+ text: modelData.text
+ style: Typography.Style.Body
+ color: Theme.surfaceText
+ elide: Text.ElideRight
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ Typography {
+ text: modelData.description
+ style: Typography.Style.Caption
+ color: Theme.outline
+ elide: Text.ElideRight
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+
+ DankIcon {
+ name: "add"
+ size: Theme.iconSize - 4
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ MouseArea {
+ id: widgetMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ root.addWidget(modelData.id);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ width: (parent.width - Theme.spacingS * 2) / 3
+ height: 48
+ radius: Theme.cornerRadius
+ color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
+ border.color: Theme.primary
+ border.width: 0
+
+ Row {
+ anchors.centerIn: parent
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "add"
+ size: Theme.iconSize - 2
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Typography {
+ text: I18n.tr("Add Widget")
+ style: Typography.Style.Button
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: addWidgetPopup.open()
+ }
+ }
+
+ Rectangle {
+ width: (parent.width - Theme.spacingS * 2) / 3
+ height: 48
+ radius: Theme.cornerRadius
+ color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12)
+ border.color: Theme.warning
+ border.width: 0
+
+ Row {
+ anchors.centerIn: parent
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "settings_backup_restore"
+ size: Theme.iconSize - 2
+ color: Theme.warning
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Typography {
+ text: I18n.tr("Defaults")
+ style: Typography.Style.Button
+ color: Theme.warning
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.resetToDefault()
+ }
+ }
+
+ Rectangle {
+ width: (parent.width - Theme.spacingS * 2) / 3
+ height: 48
+ radius: Theme.cornerRadius
+ color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
+ border.color: Theme.error
+ border.width: 0
+
+ Row {
+ anchors.centerIn: parent
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "clear_all"
+ size: Theme.iconSize - 2
+ color: Theme.error
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Typography {
+ text: I18n.tr("Reset")
+ style: Typography.Style.Button
+ color: Theme.error
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.clearAll()
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/HeaderPane.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/HeaderPane.qml
new file mode 100644
index 0000000..d36c5b6
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/HeaderPane.qml
@@ -0,0 +1,118 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property bool editMode: false
+
+ signal powerButtonClicked
+ signal lockRequested
+ signal editModeToggled
+ signal settingsButtonClicked
+
+ Component.onCompleted: DgopService.addRef("system")
+ Component.onDestruction: DgopService.removeRef("system")
+
+ implicitHeight: 70
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
+ border.width: 0
+
+ Row {
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.leftMargin: Theme.spacingL
+ anchors.rightMargin: Theme.spacingL
+ spacing: Theme.spacingM
+
+ DankCircularImage {
+ id: avatarContainer
+
+ width: 60
+ height: 60
+ imageSource: {
+ if (PortalService.profileImage === "")
+ return "";
+
+ if (PortalService.profileImage.startsWith("/"))
+ return "file://" + PortalService.profileImage;
+
+ return PortalService.profileImage;
+ }
+ fallbackIcon: "person"
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 2
+
+ Typography {
+ text: UserInfoService.fullName || UserInfoService.username || I18n.tr("User")
+ style: Typography.Style.Subtitle
+ color: Theme.surfaceText
+ }
+
+ Typography {
+ text: DgopService.uptime || I18n.tr("Unknown")
+ style: Typography.Style.Caption
+ color: Theme.surfaceVariantText
+ }
+ }
+ }
+
+ Row {
+ id: actionButtonsRow
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.rightMargin: Theme.spacingXS
+ spacing: Theme.spacingXS
+
+ DankActionButton {
+ buttonSize: 36
+ iconName: "lock"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ backgroundColor: "transparent"
+ onClicked: {
+ root.lockRequested();
+ }
+ }
+
+ DankActionButton {
+ buttonSize: 36
+ iconName: "power_settings_new"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ backgroundColor: "transparent"
+ onClicked: root.powerButtonClicked()
+ }
+
+ DankActionButton {
+ buttonSize: 36
+ iconName: "settings"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ backgroundColor: "transparent"
+ onClicked: {
+ root.settingsButtonClicked();
+ PopoutService.focusOrToggleSettings();
+ }
+ }
+
+ DankActionButton {
+ buttonSize: 36
+ iconName: editMode ? "done" : "edit"
+ iconSize: Theme.iconSize - 4
+ iconColor: editMode ? Theme.primary : Theme.surfaceText
+ backgroundColor: "transparent"
+ onClicked: root.editModeToggled()
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/PowerButton.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/PowerButton.qml
new file mode 100644
index 0000000..8ed7444
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/PowerButton.qml
@@ -0,0 +1,55 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property string iconName: ""
+ property string text: ""
+
+ signal pressed
+
+ height: 34
+ radius: Theme.cornerRadius
+ color: mouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
+
+ Row {
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ name: root.iconName
+ size: Theme.fontSizeSmall
+ color: mouseArea.containsMouse ? Theme.primary : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Typography {
+ text: root.text
+ style: Typography.Style.Button
+ color: mouseArea.containsMouse ? Theme.primary : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ DankRipple {
+ id: ripple
+ cornerRadius: root.radius
+ }
+
+ MouseArea {
+ id: mouseArea
+
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => {
+ ripple.trigger(mouse.x, mouse.y);
+ root.pressed();
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/SizeControls.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/SizeControls.qml
new file mode 100644
index 0000000..aeae4ee
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/SizeControls.qml
@@ -0,0 +1,52 @@
+import QtQuick
+import QtQuick.Controls
+import qs.Common
+import qs.Widgets
+
+Row {
+ id: root
+
+ property int currentSize: 50
+ property bool isSlider: false
+ property int widgetIndex: -1
+
+ signal sizeChanged(int newSize)
+
+ readonly property var availableSizes: isSlider ? [50, 75, 100] : [25, 50, 75, 100]
+
+ spacing: 2
+
+ Repeater {
+ model: root.availableSizes
+
+ Rectangle {
+ width: 16
+ height: 16
+ radius: 3
+ color: modelData === root.currentSize ? Theme.primary : Theme.surfaceContainer
+ border.color: modelData === root.currentSize ? Theme.primary : Theme.outline
+ border.width: 1
+
+ StyledText {
+ anchors.centerIn: parent
+ text: modelData.toString()
+ font.pixelSize: 8
+ font.weight: Font.Medium
+ color: modelData === root.currentSize ? Theme.primaryText : Theme.surfaceText
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ root.currentSize = modelData
+ root.sizeChanged(modelData)
+ }
+ }
+
+ Behavior on color {
+ ColorAnimation { duration: Theme.shortDuration }
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/Typography.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/Typography.qml
new file mode 100644
index 0000000..f3e554f
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/Typography.qml
@@ -0,0 +1,46 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+StyledText {
+ id: root
+
+ enum Style {
+ Title,
+ Subtitle,
+ Body,
+ Caption,
+ Button
+ }
+
+ property int style: Typography.Style.Body
+
+ font.pixelSize: {
+ switch (style) {
+ case Typography.Style.Title: return Theme.fontSizeXLarge
+ case Typography.Style.Subtitle: return Theme.fontSizeLarge
+ case Typography.Style.Body: return Theme.fontSizeMedium
+ case Typography.Style.Caption: return Theme.fontSizeSmall
+ case Typography.Style.Button: return Theme.fontSizeSmall
+ default: return Theme.fontSizeMedium
+ }
+ }
+
+ font.weight: {
+ switch (style) {
+ case Typography.Style.Title: return Font.Bold
+ case Typography.Style.Subtitle: return Font.Medium
+ case Typography.Style.Body: return Font.Normal
+ case Typography.Style.Caption: return Font.Normal
+ case Typography.Style.Button: return Font.Medium
+ default: return Font.Normal
+ }
+ }
+
+ color: {
+ switch (style) {
+ case Typography.Style.Caption: return Theme.surfaceVariantText
+ default: return Theme.surfaceText
+ }
+ }
+}