summaryrefslogtreecommitdiff
path: root/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter
diff options
context:
space:
mode:
authornippy <you@example.com>2026-04-18 13:49:56 +0200
committernippy <you@example.com>2026-04-18 13:49:56 +0200
commit5d94c0a7d44a2255b81815a52a7056a94a39842d (patch)
tree759bdea9645c6a62f9e1e4c001f7d81cccd120d2 /raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter
parente79cdf210b267f21a186255ce1a4d50029439d54 (diff)
downloadRaveOS-PKGBUILD-5d94c0a7d44a2255b81815a52a7056a94a39842d.tar.gz
RaveOS-PKGBUILD-5d94c0a7d44a2255b81815a52a7056a94a39842d.zip
update Raveos themes
Diffstat (limited to 'raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter')
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/BuiltinPlugins/CupsWidget.qml321
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/BuiltinPlugins/VpnWidget.qml33
-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
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/ControlCenterPopout.qml272
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/AudioInputDetail.qml363
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/AudioOutputDetail.qml544
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/BatteryDetail.qml267
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/BluetoothCodecSelector.qml332
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/BluetoothDetail.qml743
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/BrightnessDetail.qml498
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/DiskUsageDetail.qml177
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/NetworkDetail.qml893
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Models/WidgetModel.qml283
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/AudioSliderRow.qml103
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/BatteryPill.qml46
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/BrightnessSliderRow.qml194
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/ColorPickerPill.qml29
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/CompactSlider.qml71
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/CompoundPill.qml205
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/DetailView.qml23
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/DiskUsagePill.qml76
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/ErrorPill.qml56
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/InputAudioSliderRow.qml87
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/SmallBatteryButton.qml113
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/SmallDiskUsageButton.qml129
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/SmallToggleButton.qml90
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/ToggleButton.qml134
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/utils/layout.js45
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/utils/state.js17
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/utils/widgets.js90
39 files changed, 8603 insertions, 0 deletions
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/BuiltinPlugins/CupsWidget.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/BuiltinPlugins/CupsWidget.qml
new file mode 100644
index 0000000..a275397
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/BuiltinPlugins/CupsWidget.qml
@@ -0,0 +1,321 @@
+import QtQuick
+import QtQuick.Layouts
+import qs.Common
+import qs.Services
+import qs.Widgets
+import qs.Modules.Plugins
+
+PluginComponent {
+ id: root
+
+ Ref {
+ service: CupsService
+ }
+
+ ccWidgetIcon: CupsService.cupsAvailable && CupsService.getPrintersNum() > 0 ? "print" : "print_disabled"
+ ccWidgetPrimaryText: I18n.tr("Printers")
+ ccWidgetSecondaryText: {
+ if (CupsService.cupsAvailable && CupsService.getPrintersNum() > 0) {
+ return I18n.tr("Printers: ") + CupsService.getPrintersNum() + " - " + I18n.tr("Jobs: ") + CupsService.getTotalJobsNum();
+ } else {
+ if (!CupsService.cupsAvailable) {
+ return I18n.tr("Print Server not available");
+ } else {
+ return I18n.tr("No printer found");
+ }
+ }
+ }
+ ccWidgetIsActive: CupsService.cupsAvailable && CupsService.getTotalJobsNum() > 0
+
+ onCcWidgetToggled: {}
+
+ ccDetailContent: Component {
+ Rectangle {
+ id: detailRoot
+ implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+
+ DankActionButton {
+ anchors.top: parent.top
+ anchors.right: parent.right
+ anchors.topMargin: Theme.spacingS
+ anchors.rightMargin: Theme.spacingS
+ iconName: "settings"
+ buttonSize: 24
+ iconSize: 14
+ iconColor: Theme.surfaceVariantText
+ onClicked: {
+ PopoutService.closeControlCenter();
+ PopoutService.openSettingsWithTab("printers");
+ }
+ }
+
+ Column {
+ visible: !CupsService.cupsAvailable || CupsService.getPrintersNum() == 0
+ anchors.centerIn: parent
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "print_disabled"
+ size: 36
+ color: Theme.surfaceVariantText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: !CupsService.cupsAvailable ? I18n.tr("Print Server not available") : I18n.tr("No printer found")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceVariantText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+
+ Column {
+ id: detailColumn
+ anchors.fill: parent
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingS
+ visible: CupsService.cupsAvailable && CupsService.getPrintersNum() > 0
+ height: visible ? 120 : 0
+
+ RowLayout {
+ spacing: Theme.spacingS
+ width: parent.width
+
+ DankDropdown {
+ id: printerDropdown
+ text: ""
+ Layout.fillWidth: true
+ Layout.maximumWidth: parent.width - 180
+ description: ""
+ currentValue: {
+ CupsService.getSelectedPrinter();
+ }
+ options: CupsService.getPrintersNames()
+ onValueChanged: value => {
+ CupsService.setSelectedPrinter(value);
+ }
+ }
+
+ Column {
+ spacing: Theme.spacingS
+
+ StyledText {
+ text: CupsService.getCurrentPrinterStatePrettyShort()
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ Row {
+ spacing: Theme.spacingM
+
+ Rectangle {
+ height: 24
+ width: 80
+ radius: 14
+ color: printerStatusToggle.containsMouse ? Theme.errorHover : Theme.surfaceLight
+ visible: true
+ opacity: 1.0
+
+ Row {
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ anchors.verticalCenter: parent.verticalCenter
+ name: CupsService.getCurrentPrinterState() === "stopped" ? "play_arrow" : "pause"
+ size: Theme.fontSizeSmall + 4
+ color: Theme.surfaceText
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: CupsService.getCurrentPrinterState() === "stopped" ? I18n.tr("Resume") : I18n.tr("Pause")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+ }
+
+ MouseArea {
+ id: printerStatusToggle
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ enabled: true
+ onClicked: {
+ const selected = CupsService.getSelectedPrinter();
+ if (CupsService.getCurrentPrinterState() === "stopped") {
+ CupsService.resumePrinter(selected);
+ } else {
+ CupsService.pausePrinter(selected);
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ height: 24
+ width: 80
+ radius: 14
+ color: clearJobsToggle.containsMouse ? Theme.errorHover : Theme.surfaceLight
+ visible: true
+ opacity: 1.0
+
+ Row {
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ anchors.verticalCenter: parent.verticalCenter
+ name: "delete_forever"
+ size: Theme.fontSizeSmall + 4
+ color: Theme.surfaceText
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: I18n.tr("Jobs")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+ }
+
+ MouseArea {
+ id: clearJobsToggle
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ enabled: true
+ onClicked: {
+ const selected = CupsService.getSelectedPrinter();
+ CupsService.purgeJobs(selected);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ height: 1
+ width: parent.width
+ color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
+ }
+
+ DankFlickable {
+ width: parent.width
+ height: 160
+ contentHeight: listCol.height
+ clip: true
+
+ Column {
+ id: listCol
+ width: parent.width
+ spacing: Theme.spacingXS
+
+ Item {
+ width: parent.width
+ height: 120
+ visible: CupsService.getCurrentPrinterJobs().length === 0
+
+ Column {
+ anchors.centerIn: parent
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "work"
+ size: 36
+ color: Theme.surfaceVariantText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("The job queue of this printer is empty")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceVariantText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+ }
+
+ Repeater {
+ model: CupsService.getCurrentPrinterJobs()
+
+ delegate: Rectangle {
+ required property var modelData
+
+ width: parent ? parent.width : 300
+ height: 50
+ radius: Theme.cornerRadius
+ color: Theme.surfaceLight
+ border.width: 1
+ border.color: Theme.outlineLight
+ opacity: 1.0
+
+ RowLayout {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingM
+
+ DankIcon {
+ name: "docs"
+ size: Theme.iconSize + 2
+ color: Theme.surfaceText
+ Layout.alignment: Qt.AlignVCenter
+ }
+
+ Column {
+ spacing: 2
+ Layout.alignment: Qt.AlignVCenter
+ Layout.fillWidth: true
+
+ StyledText {
+ text: "[" + modelData.id + "] " + modelData.state + " (" + (modelData.size / 1024) + "kb)"
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ elide: Text.ElideRight
+ wrapMode: Text.NoWrap
+ width: parent.width
+ }
+
+ StyledText {
+ text: {
+ var date = new Date(modelData.timeCreated);
+ return date.toLocaleString(Qt.locale(), Locale.ShortFormat);
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceTextMedium
+ }
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+ }
+
+ DankActionButton {
+ id: cancelJobButton
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ iconName: "delete"
+ buttonSize: 36
+ onClicked: {
+ CupsService.cancelJob(CupsService.getSelectedPrinter(), modelData.id);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/BuiltinPlugins/VpnWidget.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/BuiltinPlugins/VpnWidget.qml
new file mode 100644
index 0000000..531045e
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/BuiltinPlugins/VpnWidget.qml
@@ -0,0 +1,33 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+import qs.Modules.Plugins
+
+PluginComponent {
+ id: root
+
+ Ref {
+ service: DMSNetworkService
+ }
+
+ ccWidgetIcon: DMSNetworkService.isBusy ? "sync" : (DMSNetworkService.connected ? "vpn_lock" : "vpn_key_off")
+ ccWidgetPrimaryText: I18n.tr("VPN")
+ ccWidgetSecondaryText: {
+ if (!DMSNetworkService.connected)
+ return I18n.tr("Disconnected");
+ const names = DMSNetworkService.activeNames || [];
+ if (names.length <= 1)
+ return names[0] || I18n.tr("Connected");
+ return names[0] + " +" + (names.length - 1);
+ }
+ ccWidgetIsActive: DMSNetworkService.connected
+
+ onCcWidgetToggled: DMSNetworkService.toggleVpn()
+
+ ccDetailContent: Component {
+ VpnDetailContent {
+ listHeight: 260
+ }
+ }
+}
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
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/ControlCenterPopout.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/ControlCenterPopout.qml
new file mode 100644
index 0000000..d8839aa
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/ControlCenterPopout.qml
@@ -0,0 +1,272 @@
+import QtQuick
+import Quickshell.Wayland
+import qs.Common
+import qs.Modules.ControlCenter.Details
+import qs.Services
+import qs.Widgets
+import qs.Modules.ControlCenter.Components
+import qs.Modules.ControlCenter.Models
+import "./utils/state.js" as StateUtils
+
+DankPopout {
+ id: root
+
+ layerNamespace: "dms:control-center"
+ fullHeightSurface: true
+
+ property string expandedSection: ""
+ property var triggerScreen: null
+ property bool editMode: false
+ property int expandedWidgetIndex: -1
+ property var expandedWidgetData: null
+ property bool powerMenuOpen: powerMenuModalLoader?.item?.shouldBeVisible ?? false
+
+ signal lockRequested
+
+ function collapseAll() {
+ expandedSection = "";
+ expandedWidgetIndex = -1;
+ expandedWidgetData = null;
+ }
+
+ onEditModeChanged: {
+ if (editMode) {
+ collapseAll();
+ }
+ }
+
+ onVisibleChanged: {
+ if (!visible) {
+ collapseAll();
+ }
+ }
+
+ readonly property color _containerBg: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+
+ function openWithSection(section) {
+ StateUtils.openWithSection(root, section);
+ }
+
+ function toggleSection(section) {
+ StateUtils.toggleSection(root, section);
+ }
+
+ popupWidth: 550
+ popupHeight: {
+ const screenHeight = (triggerScreen?.height ?? 1080);
+ const maxHeight = screenHeight - 100;
+ const contentHeight = contentLoader.item && contentLoader.item.implicitHeight > 0 ? contentLoader.item.implicitHeight + 20 : 400;
+ return Math.min(maxHeight, contentHeight);
+ }
+ triggerWidth: 80
+ positioning: ""
+ screen: triggerScreen
+ shouldBeVisible: false
+
+ property bool credentialsPromptOpen: NetworkService.credentialsRequested
+ property bool wifiPasswordModalOpen: PopoutService.wifiPasswordModal?.visible ?? false
+ property bool polkitModalOpen: PopoutService.polkitAuthModal?.visible ?? false
+ property bool anyModalOpen: credentialsPromptOpen || wifiPasswordModalOpen || polkitModalOpen || powerMenuOpen
+
+ backgroundInteractive: !anyModalOpen
+
+ onCredentialsPromptOpenChanged: {
+ if (credentialsPromptOpen && shouldBeVisible)
+ close();
+ }
+
+ onPolkitModalOpenChanged: {
+ if (polkitModalOpen && shouldBeVisible)
+ close();
+ }
+
+ customKeyboardFocus: {
+ if (!shouldBeVisible)
+ return WlrKeyboardFocus.None;
+ if (anyModalOpen)
+ return WlrKeyboardFocus.None;
+ if (CompositorService.useHyprlandFocusGrab)
+ return WlrKeyboardFocus.OnDemand;
+ return WlrKeyboardFocus.Exclusive;
+ }
+
+ onBackgroundClicked: close()
+
+ onShouldBeVisibleChanged: {
+ if (shouldBeVisible) {
+ collapseAll();
+ Qt.callLater(() => {
+ if (NetworkService.activeService)
+ NetworkService.activeService.autoRefreshEnabled = NetworkService.wifiEnabled;
+ });
+ } else {
+ Qt.callLater(() => {
+ if (NetworkService.activeService) {
+ NetworkService.activeService.autoRefreshEnabled = false;
+ }
+ if (BluetoothService.adapter && BluetoothService.adapter.discovering)
+ BluetoothService.adapter.discovering = false;
+ editMode = false;
+ });
+ }
+ }
+
+ WidgetModel {
+ id: widgetModel
+ }
+
+ content: Component {
+ Rectangle {
+ id: controlContent
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ implicitHeight: mainColumn.implicitHeight + Theme.spacingM
+ property alias bluetoothCodecSelector: bluetoothCodecSelector
+
+ color: "transparent"
+ clip: true
+
+ Rectangle {
+ anchors.fill: parent
+ color: Qt.rgba(0, 0, 0, 0.6)
+ radius: parent.radius
+ visible: root.powerMenuOpen
+ z: 5000
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: 200
+ easing.type: Easing.OutCubic
+ }
+ }
+ }
+
+ Column {
+ id: mainColumn
+ width: parent.width - Theme.spacingL * 2
+ x: Theme.spacingL
+ y: Theme.spacingL
+ spacing: Theme.spacingS
+
+ HeaderPane {
+ id: headerPane
+ width: parent.width
+ editMode: root.editMode
+ onEditModeToggled: root.editMode = !root.editMode
+ onPowerButtonClicked: {
+ if (powerMenuModalLoader) {
+ powerMenuModalLoader.active = true;
+ if (powerMenuModalLoader.item) {
+ const bounds = Qt.rect(root.alignedX, root.alignedY, root.popupWidth, root.popupHeight);
+ powerMenuModalLoader.item.openFromControlCenter(bounds, root.screen);
+ }
+ }
+ }
+ onLockRequested: {
+ root.close();
+ root.lockRequested();
+ }
+ onSettingsButtonClicked: {
+ root.close();
+ }
+ }
+
+ DragDropGrid {
+ id: widgetGrid
+ width: parent.width
+ editMode: root.editMode
+ maxPopoutHeight: {
+ const screenHeight = (root.triggerScreen?.height ?? 1080);
+ return screenHeight - 100 - Theme.spacingL - headerPane.height - Theme.spacingS;
+ }
+ expandedSection: root.expandedSection
+ expandedWidgetIndex: root.expandedWidgetIndex
+ expandedWidgetData: root.expandedWidgetData
+ model: widgetModel
+ bluetoothCodecSelector: bluetoothCodecSelector
+ colorPickerModal: root.colorPickerModal
+ screenName: root.triggerScreen?.name || ""
+ screenModel: root.triggerScreen?.model || ""
+ parentScreen: root.triggerScreen
+ onExpandClicked: (widgetData, globalIndex) => {
+ root.expandedWidgetIndex = globalIndex;
+ root.expandedWidgetData = widgetData;
+ if (widgetData.id === "diskUsage") {
+ root.toggleSection("diskUsage_" + (widgetData.instanceId || "default"));
+ } else if (widgetData.id === "brightnessSlider") {
+ root.toggleSection("brightnessSlider_" + (widgetData.instanceId || "default"));
+ } else {
+ root.toggleSection(widgetData.id);
+ }
+ }
+ onRemoveWidget: index => widgetModel.removeWidget(index)
+ onMoveWidget: (fromIndex, toIndex) => widgetModel.moveWidget(fromIndex, toIndex)
+ onToggleWidgetSize: index => widgetModel.toggleWidgetSize(index)
+ onCollapseRequested: root.collapseAll()
+ }
+
+ EditControls {
+ width: parent.width
+ visible: editMode
+ popoutContent: controlContent
+ availableWidgets: {
+ if (!editMode)
+ return [];
+ const existingIds = (SettingsData.controlCenterWidgets || []).map(w => w.id);
+ const allWidgets = widgetModel.baseWidgetDefinitions.concat(widgetModel.getPluginWidgets());
+ return allWidgets.filter(w => w.allowMultiple || !existingIds.includes(w.id));
+ }
+ onAddWidget: widgetId => widgetModel.addWidget(widgetId)
+ onResetToDefault: () => widgetModel.resetToDefault()
+ onClearAll: () => widgetModel.clearAll()
+ }
+ }
+
+ BluetoothCodecSelector {
+ id: bluetoothCodecSelector
+ anchors.fill: parent
+ z: 10000
+ }
+ }
+ }
+
+ Component {
+ id: networkDetailComponent
+ NetworkDetail {}
+ }
+
+ Component {
+ id: bluetoothDetailComponent
+ BluetoothDetail {
+ id: bluetoothDetail
+ onShowCodecSelector: function (device) {
+ if (contentLoader.item && contentLoader.item.bluetoothCodecSelector) {
+ contentLoader.item.bluetoothCodecSelector.show(device);
+ contentLoader.item.bluetoothCodecSelector.codecSelected.connect(function (deviceAddress, codecName) {
+ bluetoothDetail.updateDeviceCodecDisplay(deviceAddress, codecName);
+ });
+ }
+ }
+ }
+ }
+
+ Component {
+ id: audioOutputDetailComponent
+ AudioOutputDetail {}
+ }
+
+ Component {
+ id: audioInputDetailComponent
+ AudioInputDetail {}
+ }
+
+ Component {
+ id: batteryDetailComponent
+ BatteryDetail {}
+ }
+
+ property var colorPickerModal: null
+ property var powerMenuModalLoader: null
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/AudioInputDetail.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/AudioInputDetail.qml
new file mode 100644
index 0000000..8fc2d46
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/AudioInputDetail.qml
@@ -0,0 +1,363 @@
+import QtQuick
+import Quickshell
+import Quickshell.Services.Pipewire
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property bool hasInputVolumeSliderInCC: {
+ const widgets = SettingsData.controlCenterWidgets || [];
+ return widgets.some(widget => widget.id === "inputVolumeSlider");
+ }
+
+ implicitHeight: headerRow.height + (hasInputVolumeSliderInCC ? 0 : volumeSlider.height) + audioContent.height + Theme.spacingM
+ 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 {
+ id: headerRow
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ anchors.topMargin: Theme.spacingS
+ height: 40
+
+ StyledText {
+ id: headerText
+ text: I18n.tr("Input Devices")
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Item {
+ height: 1
+ width: parent.width - headerText.width - settingsButton.width
+ }
+
+ DankActionButton {
+ id: settingsButton
+ anchors.verticalCenter: parent.verticalCenter
+ iconName: "settings"
+ buttonSize: 28
+ iconSize: 16
+ iconColor: Theme.surfaceVariantText
+ onClicked: {
+ PopoutService.closeControlCenter();
+ PopoutService.openSettingsWithTab("audio");
+ }
+ }
+ }
+
+ Row {
+ id: volumeSlider
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: headerRow.bottom
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ anchors.topMargin: Theme.spacingXS
+ height: 35
+ spacing: 0
+ visible: !hasInputVolumeSliderInCC
+
+ Rectangle {
+ width: Theme.iconSize + Theme.spacingS * 2
+ height: Theme.iconSize + Theme.spacingS * 2
+ anchors.verticalCenter: parent.verticalCenter
+ radius: (Theme.iconSize + Theme.spacingS * 2) / 2
+ color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
+
+ DankRipple {
+ id: iconRipple
+ cornerRadius: parent.radius
+ }
+
+ MouseArea {
+ id: iconArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => iconRipple.trigger(mouse.x, mouse.y)
+ onClicked: {
+ if (AudioService.source && AudioService.source.audio) {
+ AudioService.source.audio.muted = !AudioService.source.audio.muted;
+ }
+ }
+ }
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: {
+ if (!AudioService.source || !AudioService.source.audio)
+ return "mic_off";
+ let muted = AudioService.source.audio.muted;
+ return muted ? "mic_off" : "mic";
+ }
+ size: Theme.iconSize
+ color: AudioService.source && AudioService.source.audio && !AudioService.source.audio.muted && AudioService.source.audio.volume > 0 ? Theme.primary : Theme.surfaceText
+ }
+ }
+
+ DankSlider {
+ readonly property real actualVolumePercent: AudioService.source && AudioService.source.audio ? Math.round(AudioService.source.audio.volume * 100) : 0
+
+ anchors.verticalCenter: parent.verticalCenter
+ width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
+ enabled: AudioService.source && AudioService.source.audio
+ minimum: 0
+ maximum: 100
+ value: AudioService.source && AudioService.source.audio ? Math.min(100, Math.round(AudioService.source.audio.volume * 100)) : 0
+ showValue: true
+ unit: "%"
+ valueOverride: actualVolumePercent
+ thumbOutlineColor: Theme.surfaceVariant
+
+ onSliderValueChanged: function (newValue) {
+ if (AudioService.source && AudioService.source.audio) {
+ AudioService.source.audio.volume = newValue / 100;
+ if (newValue > 0 && AudioService.source.audio.muted) {
+ AudioService.source.audio.muted = false;
+ }
+ }
+ }
+ }
+ }
+
+ DankFlickable {
+ id: audioContent
+ anchors.top: hasInputVolumeSliderInCC ? headerRow.bottom : volumeSlider.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.margins: Theme.spacingM
+ anchors.topMargin: hasInputVolumeSliderInCC ? Theme.spacingM : Theme.spacingS
+ contentHeight: audioColumn.height
+ clip: true
+
+ property int maxPinnedInputs: 3
+
+ function normalizePinList(value) {
+ if (Array.isArray(value))
+ return value.filter(v => v);
+ if (typeof value === "string" && value.length > 0)
+ return [value];
+ return [];
+ }
+
+ function getPinnedInputs() {
+ const pins = SettingsData.audioInputDevicePins || {};
+ return normalizePinList(pins["preferredInput"]);
+ }
+
+ Column {
+ id: audioColumn
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Repeater {
+ model: ScriptModel {
+ values: {
+ const hidden = SessionData.hiddenInputDeviceNames ?? [];
+ const nodes = Pipewire.nodes.values.filter(node => {
+ if (!node.audio || node.isSink || node.isStream)
+ return false;
+ return !hidden.includes(node.name);
+ });
+ const pinnedList = audioContent.getPinnedInputs();
+
+ let sorted = [...nodes];
+ sorted.sort((a, b) => {
+ // Pinned device first
+ const aPinnedIndex = pinnedList.indexOf(a.name);
+ const bPinnedIndex = pinnedList.indexOf(b.name);
+ if (aPinnedIndex !== -1 || bPinnedIndex !== -1) {
+ if (aPinnedIndex === -1)
+ return 1;
+ if (bPinnedIndex === -1)
+ return -1;
+ return aPinnedIndex - bPinnedIndex;
+ }
+ // Then active device
+ if (a === AudioService.source && b !== AudioService.source)
+ return -1;
+ if (b === AudioService.source && a !== AudioService.source)
+ return 1;
+ return 0;
+ });
+ return sorted;
+ }
+ }
+
+ delegate: Rectangle {
+ required property var modelData
+ required property int index
+
+ width: parent.width
+ height: 50
+ radius: Theme.cornerRadius
+ color: deviceMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
+ border.color: modelData === AudioService.source ? Theme.primary : Theme.outlineLight
+ border.width: modelData === AudioService.source ? 2 : 1
+
+ Row {
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.leftMargin: Theme.spacingM
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: {
+ if (modelData.name.includes("bluez"))
+ return "headset";
+ else if (modelData.name.includes("usb"))
+ return "headset";
+ else
+ return "mic";
+ }
+ size: Theme.iconSize - 4
+ color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ width: {
+ const iconWidth = Theme.iconSize;
+ const pinButtonWidth = pinInputRow.width + Theme.spacingS * 4 + Theme.spacingM;
+ return parent.parent.width - iconWidth - parent.spacing - pinButtonWidth - Theme.spacingM * 2;
+ }
+
+ StyledText {
+ text: AudioService.displayName(modelData)
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: modelData === AudioService.source ? Font.Medium : Font.Normal
+ elide: Text.ElideRight
+ width: parent.width
+ wrapMode: Text.NoWrap
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ StyledText {
+ text: modelData === AudioService.source ? I18n.tr("Active") : I18n.tr("Available")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ elide: Text.ElideRight
+ width: parent.width
+ wrapMode: Text.NoWrap
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+ }
+
+ Rectangle {
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ width: pinInputRow.width + Theme.spacingS * 2
+ height: 28
+ radius: height / 2
+ color: {
+ const isThisDevicePinned = audioContent.getPinnedInputs().includes(modelData.name);
+ return isThisDevicePinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05);
+ }
+
+ Row {
+ id: pinInputRow
+ anchors.centerIn: parent
+ spacing: 4
+
+ DankIcon {
+ name: "push_pin"
+ size: 16
+ color: {
+ const isThisDevicePinned = audioContent.getPinnedInputs().includes(modelData.name);
+ return isThisDevicePinned ? Theme.primary : Theme.surfaceText;
+ }
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: {
+ const isThisDevicePinned = audioContent.getPinnedInputs().includes(modelData.name);
+ return isThisDevicePinned ? I18n.tr("Pinned") : I18n.tr("Pin");
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ color: {
+ const isThisDevicePinned = audioContent.getPinnedInputs().includes(modelData.name);
+ return isThisDevicePinned ? Theme.primary : Theme.surfaceText;
+ }
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ DankRipple {
+ id: pinRipple
+ cornerRadius: parent.radius
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
+ onClicked: {
+ const pins = JSON.parse(JSON.stringify(SettingsData.audioInputDevicePins || {}));
+ let pinnedList = audioContent.normalizePinList(pins["preferredInput"]);
+ const pinIndex = pinnedList.indexOf(modelData.name);
+
+ if (pinIndex !== -1) {
+ pinnedList.splice(pinIndex, 1);
+ } else {
+ pinnedList.unshift(modelData.name);
+ if (pinnedList.length > audioContent.maxPinnedInputs)
+ pinnedList = pinnedList.slice(0, audioContent.maxPinnedInputs);
+ }
+
+ if (pinnedList.length > 0)
+ pins["preferredInput"] = pinnedList;
+ else
+ delete pins["preferredInput"];
+
+ SettingsData.set("audioInputDevicePins", pins);
+ }
+ }
+ }
+
+ DankRipple {
+ id: deviceRipple
+ cornerRadius: parent.radius
+ }
+
+ MouseArea {
+ id: deviceMouseArea
+ anchors.fill: parent
+ anchors.rightMargin: pinInputRow.width + Theme.spacingS * 4
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => {
+ let mapped = mapToItem(parent, mouse.x, mouse.y);
+ deviceRipple.trigger(mapped.x, mapped.y);
+ }
+ onClicked: {
+ if (modelData) {
+ Pipewire.preferredDefaultAudioSource = modelData;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/AudioOutputDetail.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/AudioOutputDetail.qml
new file mode 100644
index 0000000..7e86ae4
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/AudioOutputDetail.qml
@@ -0,0 +1,544 @@
+import QtQuick
+import Quickshell
+import Quickshell.Services.Pipewire
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property bool hasVolumeSliderInCC: {
+ const widgets = SettingsData.controlCenterWidgets || [];
+ return widgets.some(widget => widget.id === "volumeSlider");
+ }
+
+ implicitHeight: headerRow.height + (!hasVolumeSliderInCC ? volumeSlider.height : 0) + audioContent.height + Theme.spacingM
+ 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 {
+ id: headerRow
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ anchors.topMargin: Theme.spacingS
+ height: 40
+
+ StyledText {
+ id: headerText
+ text: I18n.tr("Audio Devices")
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Item {
+ height: 1
+ width: parent.width - headerText.width - settingsButton.width
+ }
+
+ DankActionButton {
+ id: settingsButton
+ anchors.verticalCenter: parent.verticalCenter
+ iconName: "settings"
+ buttonSize: 28
+ iconSize: 16
+ iconColor: Theme.surfaceVariantText
+ onClicked: {
+ PopoutService.closeControlCenter();
+ PopoutService.openSettingsWithTab("audio");
+ }
+ }
+ }
+
+ Row {
+ id: volumeSlider
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: headerRow.bottom
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ anchors.topMargin: Theme.spacingXS
+ height: 35
+ spacing: 0
+ visible: !hasVolumeSliderInCC
+
+ Rectangle {
+ width: Theme.iconSize + Theme.spacingS * 2
+ height: Theme.iconSize + Theme.spacingS * 2
+ anchors.verticalCenter: parent.verticalCenter
+ radius: (Theme.iconSize + Theme.spacingS * 2) / 2
+ color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
+
+ DankRipple {
+ id: muteRipple
+ cornerRadius: parent.radius
+ }
+
+ MouseArea {
+ id: iconArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => muteRipple.trigger(mouse.x, mouse.y)
+ onClicked: {
+ if (AudioService.sink && AudioService.sink.audio) {
+ AudioService.sink.audio.muted = !AudioService.sink.audio.muted;
+ }
+ }
+ }
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: {
+ if (!AudioService.sink || !AudioService.sink.audio)
+ return "volume_off";
+ let muted = AudioService.sink.audio.muted;
+ let volume = AudioService.sink.audio.volume;
+ 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";
+ }
+ size: Theme.iconSize
+ color: AudioService.sink && AudioService.sink.audio && !AudioService.sink.audio.muted && AudioService.sink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
+ }
+ }
+
+ DankSlider {
+ readonly property real actualVolumePercent: AudioService.sink && AudioService.sink.audio ? Math.round(AudioService.sink.audio.volume * 100) : 0
+
+ anchors.verticalCenter: parent.verticalCenter
+ width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
+ enabled: AudioService.sink && AudioService.sink.audio
+ minimum: 0
+ maximum: AudioService.sinkMaxVolume
+ value: AudioService.sink && AudioService.sink.audio ? Math.min(AudioService.sinkMaxVolume, Math.round(AudioService.sink.audio.volume * 100)) : 0
+ showValue: true
+ unit: "%"
+ valueOverride: actualVolumePercent
+ thumbOutlineColor: Theme.surfaceVariant
+
+ onSliderValueChanged: function (newValue) {
+ if (AudioService.sink && AudioService.sink.audio) {
+ AudioService.sink.audio.volume = newValue / 100;
+ if (newValue > 0 && AudioService.sink.audio.muted) {
+ AudioService.sink.audio.muted = false;
+ }
+ AudioService.volumeChanged();
+ }
+ }
+ }
+ }
+
+ DankFlickable {
+ id: audioContent
+ anchors.top: volumeSlider.visible ? volumeSlider.bottom : headerRow.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.margins: Theme.spacingM
+ anchors.topMargin: volumeSlider.visible ? Theme.spacingS : Theme.spacingM
+ contentHeight: audioColumn.height
+ clip: true
+
+ property int maxPinnedOutputs: 3
+
+ function normalizePinList(value) {
+ if (Array.isArray(value))
+ return value.filter(v => v);
+ if (typeof value === "string" && value.length > 0)
+ return [value];
+ return [];
+ }
+
+ function getPinnedOutputs() {
+ const pins = SettingsData.audioOutputDevicePins || {};
+ return normalizePinList(pins["preferredOutput"]);
+ }
+
+ Column {
+ id: audioColumn
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Repeater {
+ model: ScriptModel {
+ values: {
+ const hidden = SessionData.hiddenOutputDeviceNames ?? [];
+ const nodes = Pipewire.nodes.values.filter(node => {
+ if (!node.audio || !node.isSink || node.isStream)
+ return false;
+ return !hidden.includes(node.name);
+ });
+ const pinnedList = audioContent.getPinnedOutputs();
+
+ let sorted = [...nodes];
+ sorted.sort((a, b) => {
+ // Pinned device first
+ const aPinnedIndex = pinnedList.indexOf(a.name);
+ const bPinnedIndex = pinnedList.indexOf(b.name);
+ if (aPinnedIndex !== -1 || bPinnedIndex !== -1) {
+ if (aPinnedIndex === -1)
+ return 1;
+ if (bPinnedIndex === -1)
+ return -1;
+ return aPinnedIndex - bPinnedIndex;
+ }
+ // Then active device
+ if (a === AudioService.sink && b !== AudioService.sink)
+ return -1;
+ if (b === AudioService.sink && a !== AudioService.sink)
+ return 1;
+ return 0;
+ });
+ return sorted;
+ }
+ }
+
+ delegate: Rectangle {
+ id: outputDelegate
+ required property var modelData
+ required property int index
+
+ width: parent.width
+ height: 50
+ radius: Theme.cornerRadius
+ color: deviceMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
+ border.color: modelData === AudioService.sink ? Theme.primary : Theme.outlineLight
+ border.width: modelData === AudioService.sink ? 2 : 1
+
+ DankRipple {
+ id: deviceRipple
+ cornerRadius: outputDelegate.radius
+ }
+
+ Row {
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.leftMargin: Theme.spacingM
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: AudioService.sinkIcon(modelData)
+ size: Theme.iconSize - 4
+ color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ width: {
+ const iconWidth = Theme.iconSize;
+ const pinButtonWidth = pinOutputRow.width + Theme.spacingS * 4 + Theme.spacingM;
+ return parent.parent.width - iconWidth - parent.spacing - pinButtonWidth - Theme.spacingM * 2;
+ }
+
+ StyledText {
+ text: AudioService.displayName(modelData)
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
+ elide: Text.ElideRight
+ width: parent.width
+ wrapMode: Text.NoWrap
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ StyledText {
+ text: modelData === AudioService.sink ? I18n.tr("Active") : I18n.tr("Available")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ elide: Text.ElideRight
+ width: parent.width
+ wrapMode: Text.NoWrap
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+ }
+
+ Rectangle {
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ width: pinOutputRow.width + Theme.spacingS * 2
+ height: 28
+ radius: height / 2
+ color: {
+ const isThisDevicePinned = audioContent.getPinnedOutputs().includes(modelData.name);
+ return isThisDevicePinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05);
+ }
+
+ Row {
+ id: pinOutputRow
+ anchors.centerIn: parent
+ spacing: 4
+
+ DankIcon {
+ name: "push_pin"
+ size: 16
+ color: {
+ const isThisDevicePinned = audioContent.getPinnedOutputs().includes(modelData.name);
+ return isThisDevicePinned ? Theme.primary : Theme.surfaceText;
+ }
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: {
+ const isThisDevicePinned = audioContent.getPinnedOutputs().includes(modelData.name);
+ return isThisDevicePinned ? I18n.tr("Pinned") : I18n.tr("Pin");
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ color: {
+ const isThisDevicePinned = audioContent.getPinnedOutputs().includes(modelData.name);
+ return isThisDevicePinned ? Theme.primary : Theme.surfaceText;
+ }
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ DankRipple {
+ id: pinRipple
+ cornerRadius: parent.radius
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
+ onClicked: {
+ const pins = JSON.parse(JSON.stringify(SettingsData.audioOutputDevicePins || {}));
+ let pinnedList = audioContent.normalizePinList(pins["preferredOutput"]);
+ const pinIndex = pinnedList.indexOf(modelData.name);
+
+ if (pinIndex !== -1) {
+ pinnedList.splice(pinIndex, 1);
+ } else {
+ pinnedList.unshift(modelData.name);
+ if (pinnedList.length > audioContent.maxPinnedOutputs)
+ pinnedList = pinnedList.slice(0, audioContent.maxPinnedOutputs);
+ }
+
+ if (pinnedList.length > 0)
+ pins["preferredOutput"] = pinnedList;
+ else
+ delete pins["preferredOutput"];
+
+ SettingsData.set("audioOutputDevicePins", pins);
+ }
+ }
+ }
+
+ MouseArea {
+ id: deviceMouseArea
+ anchors.fill: parent
+ anchors.rightMargin: pinOutputRow.width + Theme.spacingS * 4
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => {
+ let mapped = deviceMouseArea.mapToItem(outputDelegate, mouse.x, mouse.y);
+ deviceRipple.trigger(mapped.x, mapped.y);
+ }
+ onClicked: {
+ if (modelData) {
+ Pipewire.preferredDefaultAudioSink = modelData;
+ }
+ }
+ }
+ }
+ }
+ Row {
+ id: playbackHeaderRow
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ height: 28
+
+ StyledText {
+ id: playbackHeaderText
+ text: I18n.tr("Playback")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ font.weight: Font.Medium
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Repeater {
+ model: ScriptModel {
+ values: {
+ const nodes = Pipewire.nodes.values.filter(node => {
+ return node.audio && node.isSink && node.isStream;
+ });
+ return nodes;
+ }
+ }
+
+ delegate: Rectangle {
+ required property var modelData
+ required property int index
+
+ width: parent.width
+ height: 50
+ radius: Theme.cornerRadius
+ color: Theme.surfaceLight
+ border.color: modelData === AudioService.sink ? Theme.primary : Theme.outlineLight
+ border.width: modelData === AudioService.sink ? 2 : 1
+
+ Row {
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.leftMargin: Theme.spacingM
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "album"
+ size: Theme.iconSize - 4
+ color: !modelData.audio.muted ? Theme.primary : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ width: {
+ const iconWidth = Theme.iconSize;
+ return parent.parent.width - iconWidth - parent.spacing - Theme.spacingM * 2;
+ }
+
+ StyledText {
+ text: {
+ const modelDataMediaName = modelData && modelData.properties ? (modelData.properties["media.name"] || "") : "";
+ const mediaName = AudioService.displayName(modelData) + ": " + modelDataMediaName;
+ const max = 35;
+ return mediaName.length > max ? mediaName.substring(0, max) + "..." : mediaName;
+ }
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
+ elide: Text.ElideRight
+ width: parent.width
+ wrapMode: Text.NoWrap
+ }
+ }
+ }
+ Rectangle {
+ anchors.right: parent.right
+ anchors.rightMargin: 120
+ anchors.verticalCenter: parent.verticalCenter
+ width: appVolumeRow.width
+ height: 28
+ radius: height / 2
+
+ Item {
+ id: appVolumeRow
+ property color sliderTrackColor: "transparent"
+ anchors.centerIn: parent
+
+ height: 40
+ width: parent.width
+
+ Rectangle {
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingM
+ width: Theme.iconSize + Theme.spacingS * 2
+ height: Theme.iconSize + Theme.spacingS * 2
+ anchors.verticalCenter: parent.verticalCenter
+ radius: Theme.cornerRadius
+ color: appIconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.primary, 0)
+
+ DankRipple {
+ id: appMuteRipple
+ cornerRadius: parent.radius
+ }
+
+ MouseArea {
+ id: appIconArea
+ anchors.fill: parent
+ visible: modelData !== null
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => appMuteRipple.trigger(mouse.x, mouse.y)
+ onClicked: {
+ if (modelData) {
+ SessionData.suppressOSDTemporarily();
+ modelData.audio.muted = !modelData.audio.muted;
+ }
+ }
+ }
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: {
+ if (!modelData)
+ return "volume_off";
+
+ let volume = modelData.audio.volume;
+ let muted = modelData.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";
+ }
+ size: Theme.iconSize
+ color: modelData && !modelData.audio.muted && modelData.audio.volume > 0 ? Theme.primary : Theme.surfaceText
+ }
+ }
+
+ DankSlider {
+ readonly property real actualVolumePercent: modelData ? Math.round(modelData.audio.volume * 100) : 0
+
+ anchors.verticalCenter: parent.verticalCenter
+ width: 100
+ enabled: modelData !== null
+ minimum: 0
+ maximum: 100
+ value: modelData ? Math.min(100, Math.round(modelData.audio.volume * 100)) : 0
+ showValue: true
+ unit: "%"
+ valueOverride: actualVolumePercent
+ thumbOutlineColor: Theme.surfaceContainer
+ trackColor: appVolumeRow.sliderTrackColor.a > 0 ? appVolumeRow.sliderTrackColor : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+
+ onSliderValueChanged: function (newValue) {
+ if (modelData) {
+ SessionData.suppressOSDTemporarily();
+ modelData.audio.volume = newValue / 100.0;
+ if (newValue > 0 && modelData.audio.muted) {
+ modelData.audio.muted = false;
+ }
+ AudioService.playVolumeChangeSoundIfEnabled();
+ }
+ }
+ }
+ }
+ PwObjectTracker {
+ objects: [modelData]
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/BatteryDetail.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/BatteryDetail.qml
new file mode 100644
index 0000000..1f0206a
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/BatteryDetail.qml
@@ -0,0 +1,267 @@
+import QtQuick
+import Quickshell.Services.UPower
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
+ 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
+
+ function isActiveProfile(profile) {
+ if (typeof PowerProfiles === "undefined") {
+ return false;
+ }
+ return PowerProfiles.profile === profile;
+ }
+
+ function setProfile(profile) {
+ if (typeof PowerProfiles === "undefined") {
+ ToastService.showError("power-profiles-daemon not available");
+ return;
+ }
+ PowerProfiles.profile = profile;
+ if (PowerProfiles.profile !== profile) {
+ ToastService.showError("Failed to set power profile");
+ }
+ }
+
+ Column {
+ id: contentColumn
+ width: parent.width - Theme.spacingL * 2
+ anchors.left: parent.left
+ anchors.top: parent.top
+ anchors.margins: Theme.spacingL
+ spacing: Theme.spacingL
+
+ Row {
+ id: headerRow
+ width: parent.width
+ spacing: Theme.spacingM
+
+ DankIcon {
+ name: BatteryService.getBatteryIcon()
+ size: Theme.iconSizeLarge
+ color: {
+ if (BatteryService.isLowBattery && !BatteryService.isCharging)
+ return Theme.error;
+ if (BatteryService.isCharging || BatteryService.isPluggedIn)
+ return Theme.primary;
+ return Theme.surfaceText;
+ }
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Column {
+ spacing: Theme.spacingXS
+ anchors.verticalCenter: parent.verticalCenter
+ width: parent.width - Theme.iconSizeLarge - Theme.spacingM
+
+ Row {
+ spacing: Theme.spacingS
+
+ StyledText {
+ text: BatteryService.batteryAvailable ? `${BatteryService.batteryLevel}%` : I18n.tr("Power")
+ font.pixelSize: Theme.fontSizeXLarge
+ color: {
+ if (BatteryService.isLowBattery && !BatteryService.isCharging) {
+ return Theme.error;
+ }
+ if (BatteryService.isCharging) {
+ return Theme.primary;
+ }
+ return Theme.surfaceText;
+ }
+ font.weight: Font.Bold
+ }
+
+ StyledText {
+ text: BatteryService.batteryAvailable ? BatteryService.batteryStatus : I18n.tr("Management")
+ font.pixelSize: Theme.fontSizeLarge
+ color: {
+ if (BatteryService.isLowBattery && !BatteryService.isCharging) {
+ return Theme.error;
+ }
+ if (BatteryService.isCharging) {
+ return Theme.primary;
+ }
+ return Theme.surfaceText;
+ }
+ font.weight: Font.Medium
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ StyledText {
+ text: {
+ if (!BatteryService.batteryAvailable)
+ return I18n.tr("Power profile management available");
+ const time = BatteryService.formatTimeRemaining();
+ if (time !== "Unknown") {
+ return BatteryService.isCharging ? I18n.tr("Time until full: %1").arg(time) : I18n.tr("Time remaining: %1").arg(time);
+ }
+ return "";
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceTextMedium
+ visible: text.length > 0
+ elide: Text.ElideRight
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+ }
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingM
+ visible: BatteryService.batteryAvailable
+
+ StyledRect {
+ width: (parent.width - Theme.spacingM) / 2
+ height: 64
+ radius: Theme.cornerRadius
+ color: Theme.surfaceLight
+ border.color: Theme.outlineLight
+ border.width: 1
+
+ Column {
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Health")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.primary
+ font.weight: Font.Medium
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: BatteryService.batteryHealth
+ font.pixelSize: Theme.fontSizeLarge
+ color: {
+ if (BatteryService.batteryHealth === "N/A") {
+ return Theme.surfaceText;
+ }
+ const healthNum = parseInt(BatteryService.batteryHealth);
+ return healthNum < 80 ? Theme.error : Theme.surfaceText;
+ }
+ font.weight: Font.Bold
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+ }
+
+ StyledRect {
+ width: (parent.width - Theme.spacingM) / 2
+ height: 64
+ radius: Theme.cornerRadius
+ color: Theme.surfaceLight
+ border.color: Theme.outlineLight
+ border.width: 1
+
+ Column {
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Capacity")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.primary
+ font.weight: Font.Medium
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: BatteryService.batteryCapacity > 0 ? `${BatteryService.batteryCapacity.toFixed(1)} Wh` : I18n.tr("Unknown")
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.surfaceText
+ font.weight: Font.Bold
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+ }
+ }
+
+ DankButtonGroup {
+ property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
+ property int currentProfileIndex: {
+ if (typeof PowerProfiles === "undefined")
+ return 1;
+ return profileModel.findIndex(profile => isActiveProfile(profile));
+ }
+
+ model: profileModel.map(profile => Theme.getPowerProfileLabel(profile))
+ currentIndex: currentProfileIndex
+ selectionMode: "single"
+ anchors.horizontalCenter: parent.horizontalCenter
+ onSelectionChanged: (index, selected) => {
+ if (!selected)
+ return;
+ setProfile(profileModel[index]);
+ }
+ }
+
+ StyledRect {
+ width: parent.width
+ height: degradationContent.implicitHeight + Theme.spacingL * 2
+ radius: Theme.cornerRadius
+ color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
+ border.color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3)
+ border.width: 0
+ visible: (typeof PowerProfiles !== "undefined") && PowerProfiles.degradationReason !== PerformanceDegradationReason.None
+
+ Column {
+ id: degradationContent
+ width: parent.width - Theme.spacingL * 2
+ anchors.left: parent.left
+ anchors.top: parent.top
+ anchors.margins: Theme.spacingL
+ spacing: Theme.spacingS
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingM
+
+ DankIcon {
+ name: "warning"
+ size: Theme.iconSize
+ color: Theme.error
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Column {
+ spacing: Theme.spacingXS
+ anchors.verticalCenter: parent.verticalCenter
+ width: parent.width - Theme.iconSize - Theme.spacingM
+
+ StyledText {
+ text: I18n.tr("Power Profile Degradation")
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.error
+ font.weight: Font.Medium
+ }
+
+ StyledText {
+ text: (typeof PowerProfiles !== "undefined") ? PerformanceDegradationReason.toString(PowerProfiles.degradationReason) : ""
+ font.pixelSize: Theme.fontSizeSmall
+ color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.8)
+ wrapMode: Text.WordWrap
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/BluetoothCodecSelector.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/BluetoothCodecSelector.qml
new file mode 100644
index 0000000..0cad08c
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/BluetoothCodecSelector.qml
@@ -0,0 +1,332 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Item {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property var device: null
+ property bool modalVisible: false
+ property var parentItem
+ property var availableCodecs: []
+ property string currentCodec: ""
+ property bool isLoading: false
+
+ readonly property bool deviceValid: device !== null && device.connected && BluetoothService.isAudioDevice(device)
+
+ signal codecSelected(string deviceAddress, string codecName)
+
+ function show(bluetoothDevice) {
+ if (!bluetoothDevice?.connected)
+ return;
+ if (!BluetoothService.isAudioDevice(bluetoothDevice))
+ return;
+ device = bluetoothDevice;
+ isLoading = true;
+ availableCodecs = [];
+ currentCodec = "";
+ visible = true;
+ modalVisible = true;
+ queryCodecs();
+ Qt.callLater(() => {
+ focusScope.forceActiveFocus();
+ });
+ }
+
+ function hide() {
+ modalVisible = false;
+ Qt.callLater(() => {
+ visible = false;
+ device = null;
+ });
+ }
+
+ function queryCodecs() {
+ if (!deviceValid) {
+ hide();
+ return;
+ }
+
+ const capturedDevice = device;
+ const capturedAddress = device.address;
+
+ BluetoothService.getAvailableCodecs(capturedDevice, function (codecs, current) {
+ if (!root.deviceValid || root.device?.address !== capturedAddress)
+ return;
+ availableCodecs = codecs;
+ currentCodec = current;
+ isLoading = false;
+ });
+ }
+
+ function selectCodec(profileName) {
+ if (!deviceValid || isLoading)
+ return;
+
+ const capturedDevice = device;
+ const capturedAddress = device.address;
+
+ const selectedCodec = availableCodecs.find(c => c.profile === profileName);
+ if (!selectedCodec)
+ return;
+
+ BluetoothService.updateDeviceCodec(capturedAddress, selectedCodec.name);
+ codecSelected(capturedAddress, selectedCodec.name);
+
+ isLoading = true;
+ BluetoothService.switchCodec(capturedDevice, profileName, function (success, message) {
+ if (!root.device || root.device.address !== capturedAddress)
+ return;
+
+ isLoading = false;
+ if (success) {
+ ToastService.showToast(message, ToastService.levelInfo);
+ Qt.callLater(root.hide);
+ return;
+ }
+ ToastService.showToast(message, ToastService.levelError);
+ });
+ }
+
+ onDeviceValidChanged: {
+ if (modalVisible && !deviceValid) {
+ hide();
+ }
+ }
+
+ visible: false
+ anchors.fill: parent
+ z: 2000
+
+ MouseArea {
+ id: modalBlocker
+ anchors.fill: parent
+ visible: modalVisible
+ enabled: modalVisible
+ hoverEnabled: true
+ preventStealing: true
+ propagateComposedEvents: false
+
+ onClicked: root.hide()
+ onWheel: wheel => {
+ wheel.accepted = true;
+ }
+ onPositionChanged: mouse => {
+ mouse.accepted = true;
+ }
+ }
+
+ Rectangle {
+ id: modalBackground
+ anchors.fill: parent
+ color: Qt.rgba(0, 0, 0, 0.5)
+ opacity: modalVisible ? 1 : 0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+ }
+
+ FocusScope {
+ id: focusScope
+
+ anchors.fill: parent
+ focus: root.visible
+ enabled: root.visible
+
+ Keys.onEscapePressed: {
+ root.hide();
+ event.accepted = true;
+ }
+ }
+
+ Rectangle {
+ id: modalContent
+ anchors.centerIn: parent
+ width: 320
+ height: contentColumn.implicitHeight + Theme.spacingL * 2
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
+ border.width: 0
+ opacity: modalVisible ? 1 : 0
+ scale: modalVisible ? 1 : 0.9
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ preventStealing: true
+ propagateComposedEvents: false
+ onClicked: mouse => {
+ mouse.accepted = true;
+ }
+ onWheel: wheel => {
+ wheel.accepted = true;
+ }
+ onPositionChanged: mouse => {
+ mouse.accepted = true;
+ }
+ }
+
+ Column {
+ id: contentColumn
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.margins: Theme.spacingL
+ spacing: Theme.spacingM
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingM
+
+ DankIcon {
+ name: device ? BluetoothService.getDeviceIcon(device) : "headset"
+ size: Theme.iconSize + 4
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 2
+
+ StyledText {
+ text: device ? (device.name || device.deviceName) : ""
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+
+ StyledText {
+ text: I18n.tr("Audio Codec Selection")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceTextMedium
+ }
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: 1
+ color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
+ }
+
+ StyledText {
+ text: isLoading ? I18n.tr("Loading codecs...") : I18n.tr("Current: %1").arg(currentCodec)
+ font.pixelSize: Theme.fontSizeSmall
+ color: isLoading ? Theme.primary : Theme.surfaceTextMedium
+ font.weight: Font.Medium
+ }
+
+ Column {
+ width: parent.width
+ spacing: Theme.spacingXS
+ visible: !isLoading
+
+ Repeater {
+ model: availableCodecs
+
+ Rectangle {
+ width: parent.width
+ height: 48
+ radius: Theme.cornerRadius
+ color: {
+ if (modelData.name === currentCodec)
+ return Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency);
+ else if (codecMouseArea.containsMouse)
+ return Theme.surfaceHover;
+ else
+ return "transparent";
+ }
+ border.color: "transparent"
+ border.width: 0
+
+ Row {
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingS
+
+ Rectangle {
+ width: 6
+ height: 6
+ radius: 3
+ color: modelData.qualityColor
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 2
+
+ StyledText {
+ text: modelData.name
+ font.pixelSize: Theme.fontSizeMedium
+ color: modelData.name === currentCodec ? Theme.primary : Theme.surfaceText
+ font.weight: modelData.name === currentCodec ? Font.Medium : Font.Normal
+ }
+
+ StyledText {
+ text: modelData.description
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceTextMedium
+ }
+ }
+ }
+
+ DankIcon {
+ name: "check"
+ size: Theme.iconSize - 4
+ color: Theme.primary
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ visible: modelData.name === currentCodec
+ }
+
+ DankRipple {
+ id: codecRipple
+ cornerRadius: parent.radius
+ }
+
+ MouseArea {
+ id: codecMouseArea
+
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ enabled: modelData.name !== currentCodec && !isLoading
+ onPressed: mouse => codecRipple.trigger(mouse.x, mouse.y)
+ onClicked: {
+ selectCodec(modelData.profile);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+
+ Behavior on scale {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/BluetoothDetail.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/BluetoothDetail.qml
new file mode 100644
index 0000000..580ea9f
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/BluetoothDetail.qml
@@ -0,0 +1,743 @@
+import QtQuick
+import QtQuick.Controls
+import Quickshell
+import Quickshell.Bluetooth
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ implicitHeight: {
+ if (height > 0)
+ return height;
+ if (!BluetoothService.adapter?.enabled)
+ return headerRow.height;
+ return headerRow.height + bluetoothContent.height + Theme.spacingM;
+ }
+ 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
+
+ property var bluetoothCodecModalRef: null
+ property var devicesBeingPaired: new Set()
+
+ signal showCodecSelector(var device)
+
+ function isDeviceBeingPaired(deviceAddress) {
+ return devicesBeingPaired.has(deviceAddress);
+ }
+
+ function handlePairDevice(device) {
+ if (!device)
+ return;
+ const deviceAddr = device.address;
+ const pairingSet = devicesBeingPaired;
+
+ pairingSet.add(deviceAddr);
+ devicesBeingPairedChanged();
+
+ BluetoothService.pairDevice(device, function (response) {
+ pairingSet.delete(deviceAddr);
+ devicesBeingPairedChanged();
+
+ if (response.error) {
+ ToastService.showError(I18n.tr("Pairing failed"), response.error);
+ return;
+ }
+ if (!BluetoothService.enhancedPairingAvailable) {
+ ToastService.showSuccess(I18n.tr("Device paired"));
+ }
+ });
+ }
+
+ function updateDeviceCodecDisplay(deviceAddress, codecName) {
+ for (let i = 0; i < pairedRepeater.count; i++) {
+ const item = pairedRepeater.itemAt(i);
+ if (!item?.modelData)
+ continue;
+ if (item.modelData.address !== deviceAddress)
+ continue;
+ item.currentCodec = codecName;
+ break;
+ }
+ }
+
+ function normalizePinList(value) {
+ if (Array.isArray(value))
+ return value.filter(v => v);
+ if (typeof value === "string" && value.length > 0)
+ return [value];
+ return [];
+ }
+
+ function getPinnedDevices() {
+ const pins = SettingsData.bluetoothDevicePins || {};
+ return normalizePinList(pins["preferredDevice"]);
+ }
+
+ Row {
+ id: headerRow
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ anchors.topMargin: Theme.spacingS
+ height: 40
+
+ StyledText {
+ id: headerText
+ text: I18n.tr("Bluetooth Settings")
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Item {
+ width: Math.max(0, parent.width - headerText.implicitWidth - scanButton.width - Theme.spacingM)
+ height: parent.height
+ }
+
+ Rectangle {
+ id: scanButton
+
+ readonly property bool adapterEnabled: BluetoothService.adapter?.enabled ?? false
+ readonly property bool isDiscovering: BluetoothService.adapter?.discovering ?? false
+
+ width: 100
+ height: 36
+ radius: 18
+ color: scanMouseArea.containsMouse && adapterEnabled ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent"
+ border.color: adapterEnabled ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
+ border.width: 0
+ visible: adapterEnabled
+
+ Row {
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ name: scanButton.isDiscovering ? "stop" : "bluetooth_searching"
+ size: 18
+ color: scanButton.adapterEnabled ? Theme.primary : Theme.surfaceVariantText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: scanButton.isDiscovering ? I18n.tr("Scanning") : I18n.tr("Scan")
+ color: scanButton.adapterEnabled ? Theme.primary : Theme.surfaceVariantText
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ DankRipple {
+ id: scanRipple
+ cornerRadius: scanButton.radius
+ }
+
+ MouseArea {
+ id: scanMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ enabled: scanButton.adapterEnabled
+ cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
+ onPressed: mouse => scanRipple.trigger(mouse.x, mouse.y)
+ onClicked: {
+ if (!BluetoothService.adapter)
+ return;
+ BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering;
+ }
+ }
+ }
+ }
+
+ DankFlickable {
+ id: bluetoothContent
+ anchors.top: headerRow.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.margins: Theme.spacingM
+ anchors.topMargin: Theme.spacingM
+ visible: BluetoothService.adapter?.enabled ?? false
+ contentHeight: bluetoothColumn.height
+ clip: true
+
+ readonly property int maxPinnedDevices: 3
+
+ Column {
+ id: bluetoothColumn
+ width: parent.width
+ spacing: Theme.spacingS
+
+ ScriptModel {
+ id: pairedDevicesModel
+ objectProp: "address"
+ values: {
+ if (!BluetoothService.adapter?.devices)
+ return [];
+
+ const pinnedList = root.getPinnedDevices();
+ const devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))];
+
+ devices.sort((a, b) => {
+ const aPinnedIndex = pinnedList.indexOf(a.address);
+ const bPinnedIndex = pinnedList.indexOf(b.address);
+
+ if (aPinnedIndex !== -1 || bPinnedIndex !== -1) {
+ if (aPinnedIndex === -1)
+ return 1;
+ if (bPinnedIndex === -1)
+ return -1;
+ return aPinnedIndex - bPinnedIndex;
+ }
+
+ if (a.connected !== b.connected)
+ return a.connected ? -1 : 1;
+
+ return (b.signalStrength || 0) - (a.signalStrength || 0);
+ });
+
+ return devices;
+ }
+ }
+
+ Repeater {
+ id: pairedRepeater
+ model: pairedDevicesModel
+
+ delegate: Rectangle {
+ id: pairedDelegate
+ required property var modelData
+ required property int index
+
+ readonly property string currentCodec: BluetoothService.deviceCodecs[modelData.address] || ""
+ readonly property bool isConnecting: modelData.state === BluetoothDeviceState.Connecting
+ readonly property bool isConnected: modelData.connected
+ readonly property bool isPinned: root.getPinnedDevices().includes(modelData.address)
+ readonly property string deviceName: modelData.name || modelData.deviceName || I18n.tr("Unknown Device")
+
+ width: parent.width
+ height: 50
+ radius: Theme.cornerRadius
+
+ Component.onCompleted: {
+ if (!isConnected)
+ return;
+ if (!BluetoothService.isAudioDevice(modelData))
+ return;
+ BluetoothService.refreshDeviceCodec(modelData);
+ }
+
+ color: {
+ if (isConnecting)
+ return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
+ if (deviceMouseArea.containsMouse)
+ return Theme.primaryHoverLight;
+ return Theme.surfaceLight;
+ }
+
+ border.color: {
+ if (isConnecting)
+ return Theme.warning;
+ if (isConnected)
+ return Theme.primary;
+ return Theme.outlineLight;
+ }
+ border.width: (isConnecting || isConnected) ? 2 : 1
+
+ Row {
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.leftMargin: Theme.spacingM
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: BluetoothService.getDeviceIcon(pairedDelegate.modelData)
+ size: Theme.iconSize - 4
+ anchors.verticalCenter: parent.verticalCenter
+ color: {
+ if (pairedDelegate.isConnecting)
+ return Theme.warning;
+ if (pairedDelegate.isConnected)
+ return Theme.primary;
+ return Theme.surfaceText;
+ }
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ width: 200
+
+ StyledText {
+ text: pairedDelegate.deviceName
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: pairedDelegate.isConnected ? Font.Medium : Font.Normal
+ elide: Text.ElideRight
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ Row {
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: {
+ if (pairedDelegate.isConnecting)
+ return I18n.tr("Connecting...");
+ if (!pairedDelegate.isConnected)
+ return I18n.tr("Paired");
+ if (!pairedDelegate.currentCodec)
+ return I18n.tr("Connected");
+ return I18n.tr("Connected") + " • " + pairedDelegate.currentCodec;
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ color: pairedDelegate.isConnecting ? Theme.warning : Theme.surfaceVariantText
+ }
+
+ StyledText {
+ readonly property var btBattery: {
+ const name = pairedDelegate.deviceName;
+ return BatteryService.bluetoothDevices.find(dev => dev.name === name || dev.name.toLowerCase().includes(name.toLowerCase()) || name.toLowerCase().includes(dev.name.toLowerCase()));
+ }
+ text: {
+ if (pairedDelegate.modelData.batteryAvailable && pairedDelegate.modelData.battery > 0)
+ return "• " + Math.round(pairedDelegate.modelData.battery * 100) + "%";
+ if (btBattery)
+ return "• " + btBattery.percentage + "%";
+ return "";
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ visible: text.length > 0
+ }
+
+ StyledText {
+ text: pairedDelegate.modelData.signalStrength > 0 ? "• " + pairedDelegate.modelData.signalStrength + "%" : ""
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ visible: text.length > 0
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ anchors.right: pairedOptionsButton.left
+ anchors.rightMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ width: pinBluetoothRow.width + Theme.spacingS * 2
+ height: 28
+ radius: height / 2
+ color: pairedDelegate.isPinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05)
+
+ Row {
+ id: pinBluetoothRow
+ anchors.centerIn: parent
+ spacing: 4
+
+ DankIcon {
+ name: "push_pin"
+ size: 16
+ color: pairedDelegate.isPinned ? Theme.primary : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: pairedDelegate.isPinned ? I18n.tr("Pinned") : I18n.tr("Pin")
+ font.pixelSize: Theme.fontSizeSmall
+ color: pairedDelegate.isPinned ? Theme.primary : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ const pins = JSON.parse(JSON.stringify(SettingsData.bluetoothDevicePins || {}));
+ let pinnedList = root.normalizePinList(pins["preferredDevice"]);
+ const pinIndex = pinnedList.indexOf(pairedDelegate.modelData.address);
+
+ if (pinIndex !== -1) {
+ pinnedList.splice(pinIndex, 1);
+ } else {
+ pinnedList.unshift(pairedDelegate.modelData.address);
+ if (pinnedList.length > bluetoothContent.maxPinnedDevices)
+ pinnedList = pinnedList.slice(0, bluetoothContent.maxPinnedDevices);
+ }
+
+ if (pinnedList.length > 0) {
+ pins["preferredDevice"] = pinnedList;
+ } else {
+ delete pins["preferredDevice"];
+ }
+
+ SettingsData.set("bluetoothDevicePins", pins);
+ }
+ }
+ }
+
+ DankActionButton {
+ id: pairedOptionsButton
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ iconName: "more_horiz"
+ buttonSize: 28
+ onClicked: {
+ if (bluetoothContextMenu.visible) {
+ bluetoothContextMenu.close();
+ return;
+ }
+ bluetoothContextMenu.currentDevice = pairedDelegate.modelData;
+ bluetoothContextMenu.popup(pairedOptionsButton, -bluetoothContextMenu.width + pairedOptionsButton.width, pairedOptionsButton.height + Theme.spacingXS);
+ }
+ }
+
+ DankRipple {
+ id: deviceRipple
+ cornerRadius: pairedDelegate.radius
+ }
+
+ MouseArea {
+ id: deviceMouseArea
+ anchors.fill: parent
+ anchors.rightMargin: pairedOptionsButton.width + Theme.spacingM + pinBluetoothRow.width + Theme.spacingS * 4
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => {
+ const pos = mapToItem(pairedDelegate, mouse.x, mouse.y);
+ deviceRipple.trigger(pos.x, pos.y);
+ }
+ onClicked: {
+ if (pairedDelegate.isConnected) {
+ pairedDelegate.modelData.disconnect();
+ return;
+ }
+ BluetoothService.connectDeviceWithTrust(pairedDelegate.modelData);
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: 1
+ color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
+ visible: pairedRepeater.count > 0 && availableRepeater.count > 0
+ }
+
+ Item {
+ width: parent.width
+ height: 80
+ visible: (BluetoothService.adapter?.discovering ?? false) && availableRepeater.count === 0
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "sync"
+ size: 24
+ color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.4)
+
+ RotationAnimation on rotation {
+ running: parent.visible
+ loops: Animation.Infinite
+ from: 0
+ to: 360
+ duration: 1500
+ }
+ }
+ }
+
+ ScriptModel {
+ id: availableDevicesModel
+ objectProp: "address"
+ values: {
+ if (!BluetoothService.adapter?.discovering)
+ return [];
+ if (!Bluetooth.devices)
+ return [];
+
+ const filtered = Bluetooth.devices.values.filter(dev => dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0));
+ return BluetoothService.sortDevices(filtered);
+ }
+ }
+
+ Repeater {
+ id: availableRepeater
+ model: availableDevicesModel
+
+ delegate: Rectangle {
+ id: availableDelegate
+ required property var modelData
+ required property int index
+
+ readonly property bool canConnect: BluetoothService.canConnect(modelData)
+ readonly property bool isBusy: BluetoothService.isDeviceBusy(modelData) || root.isDeviceBeingPaired(modelData.address)
+ readonly property bool isInteractive: canConnect && !isBusy
+ readonly property string deviceName: modelData.name || modelData.deviceName || I18n.tr("Unknown Device")
+
+ width: parent.width
+ height: 50
+ radius: Theme.cornerRadius
+ color: availableMouseArea.containsMouse && isInteractive ? Theme.primaryHoverLight : Theme.surfaceLight
+ border.color: Theme.outlineLight
+ border.width: 1
+ opacity: isInteractive ? 1 : 0.6
+
+ Row {
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.leftMargin: Theme.spacingM
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: BluetoothService.getDeviceIcon(availableDelegate.modelData)
+ size: Theme.iconSize - 4
+ color: Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ width: 200
+
+ StyledText {
+ text: availableDelegate.deviceName
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ elide: Text.ElideRight
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ Row {
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: {
+ if (availableDelegate.modelData.pairing || availableDelegate.isBusy)
+ return I18n.tr("Pairing...");
+ if (availableDelegate.modelData.blocked)
+ return I18n.tr("Blocked");
+ return BluetoothService.getSignalStrength(availableDelegate.modelData);
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+
+ StyledText {
+ text: availableDelegate.modelData.signalStrength > 0 ? "• " + availableDelegate.modelData.signalStrength + "%" : ""
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ visible: text.length > 0 && !availableDelegate.modelData.pairing && !availableDelegate.modelData.blocked
+ }
+ }
+ }
+ }
+
+ StyledText {
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ text: {
+ if (availableDelegate.isBusy)
+ return I18n.tr("Pairing...");
+ if (!availableDelegate.canConnect)
+ return I18n.tr("Cannot pair");
+ return I18n.tr("Pair");
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ color: availableDelegate.isInteractive ? Theme.primary : Theme.surfaceVariantText
+ font.weight: Font.Medium
+ }
+
+ DankRipple {
+ id: availableRipple
+ cornerRadius: availableDelegate.radius
+ }
+
+ MouseArea {
+ id: availableMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: availableDelegate.isInteractive ? Qt.PointingHandCursor : Qt.ArrowCursor
+ enabled: availableDelegate.isInteractive
+ onPressed: mouse => availableRipple.trigger(mouse.x, mouse.y)
+ onClicked: root.handlePairDevice(availableDelegate.modelData)
+ }
+ }
+ }
+
+ Item {
+ width: parent.width
+ height: 60
+ visible: !BluetoothService.adapter
+
+ StyledText {
+ anchors.centerIn: parent
+ text: I18n.tr("No Bluetooth adapter found")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceVariantText
+ }
+ }
+ }
+ }
+
+ Menu {
+ id: bluetoothContextMenu
+ width: 150
+ closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
+
+ property var currentDevice: null
+
+ readonly property bool hasDevice: currentDevice !== null
+ readonly property bool deviceConnected: currentDevice?.connected ?? false
+ readonly property bool showCodecOption: hasDevice && deviceConnected && BluetoothService.isAudioDevice(currentDevice)
+
+ background: Rectangle {
+ color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ radius: Theme.cornerRadius
+ border.width: 0
+ border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
+ }
+
+ MenuItem {
+ text: bluetoothContextMenu.deviceConnected ? I18n.tr("Disconnect") : I18n.tr("Connect")
+ height: 32
+
+ contentItem: StyledText {
+ text: parent.text
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ leftPadding: Theme.spacingS
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ background: Rectangle {
+ color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
+ radius: Theme.cornerRadius / 2
+ }
+
+ onTriggered: {
+ if (!bluetoothContextMenu.hasDevice)
+ return;
+ if (bluetoothContextMenu.deviceConnected) {
+ bluetoothContextMenu.currentDevice.disconnect();
+ return;
+ }
+ BluetoothService.connectDeviceWithTrust(bluetoothContextMenu.currentDevice);
+ }
+ }
+
+ MenuItem {
+ text: I18n.tr("Audio Codec")
+ height: bluetoothContextMenu.showCodecOption ? 32 : 0
+ visible: bluetoothContextMenu.showCodecOption
+
+ contentItem: StyledText {
+ text: parent.text
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ leftPadding: Theme.spacingS
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ background: Rectangle {
+ color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
+ radius: Theme.cornerRadius / 2
+ }
+
+ onTriggered: {
+ if (!bluetoothContextMenu.hasDevice)
+ return;
+ if (!bluetoothContextMenu.currentDevice.connected)
+ return;
+ if (!BluetoothService.isAudioDevice(bluetoothContextMenu.currentDevice))
+ return;
+ showCodecSelector(bluetoothContextMenu.currentDevice);
+ }
+ }
+
+ MenuItem {
+ text: bluetoothContextMenu.currentDevice?.trusted ? I18n.tr("Untrust") : I18n.tr("Trust")
+ height: 32
+
+ contentItem: StyledText {
+ text: parent.text
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ leftPadding: Theme.spacingS
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ background: Rectangle {
+ color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
+ radius: Theme.cornerRadius / 2
+ }
+
+ onTriggered: {
+ if (!bluetoothContextMenu.hasDevice)
+ return;
+ bluetoothContextMenu.currentDevice.trusted = !bluetoothContextMenu.currentDevice.trusted;
+ }
+ }
+
+ MenuItem {
+ text: I18n.tr("Forget Device")
+ height: 32
+
+ contentItem: StyledText {
+ text: parent.text
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.error
+ leftPadding: Theme.spacingS
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ background: Rectangle {
+ color: parent.hovered ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : "transparent"
+ radius: Theme.cornerRadius / 2
+ }
+
+ onTriggered: {
+ if (!bluetoothContextMenu.hasDevice)
+ return;
+ if (!BluetoothService.enhancedPairingAvailable) {
+ bluetoothContextMenu.currentDevice.forget();
+ return;
+ }
+
+ const devicePath = BluetoothService.getDevicePath(bluetoothContextMenu.currentDevice);
+ DMSService.bluetoothRemove(devicePath, response => {
+ if (!response.error)
+ return;
+ ToastService.showError(I18n.tr("Failed to remove device"), response.error);
+ });
+ }
+ }
+ }
+
+ Connections {
+ target: DMSService
+
+ function onBluetoothPairingRequest(data) {
+ const modal = PopoutService.bluetoothPairingModal;
+ if (!modal)
+ return;
+ if (modal.token === data.token)
+ return;
+ modal.show(data);
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/BrightnessDetail.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/BrightnessDetail.qml
new file mode 100644
index 0000000..0d42e62
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/BrightnessDetail.qml
@@ -0,0 +1,498 @@
+import QtQuick
+import Quickshell
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property string initialDeviceName: ""
+ property string instanceId: ""
+ property string screenName: ""
+ property string screenModel: ""
+
+ signal deviceNameChanged(string newDeviceName)
+
+ property string currentDeviceName: ""
+
+ function getScreenPinKey() {
+ if (!screenName)
+ return "";
+ const screen = Quickshell.screens.find(s => s.name === screenName);
+ if (screen) {
+ return SettingsData.getScreenDisplayName(screen);
+ }
+ if (SettingsData.displayNameMode === "model" && screenModel && screenModel.length > 0) {
+ return screenModel;
+ }
+ return screenName;
+ }
+
+ function resolveDeviceName() {
+ if (!DisplayService.brightnessAvailable || !DisplayService.devices || DisplayService.devices.length === 0) {
+ return "";
+ }
+
+ const pinKey = getScreenPinKey();
+ if (pinKey.length > 0) {
+ const pins = SettingsData.brightnessDevicePins || {};
+ const pinnedDevice = pins[pinKey];
+ if (pinnedDevice && pinnedDevice.length > 0) {
+ const found = DisplayService.devices.find(dev => dev.name === pinnedDevice);
+ if (found)
+ return found.name;
+ }
+ }
+
+ if (initialDeviceName && initialDeviceName.length > 0) {
+ const found = DisplayService.devices.find(dev => dev.name === initialDeviceName);
+ if (found)
+ return found.name;
+ }
+
+ const currentDeviceNameFromService = DisplayService.currentDevice;
+ if (currentDeviceNameFromService) {
+ const found = DisplayService.devices.find(dev => dev.name === currentDeviceNameFromService);
+ if (found)
+ return found.name;
+ }
+
+ const backlight = DisplayService.devices.find(d => d.class === "backlight");
+ if (backlight)
+ return backlight.name;
+
+ const ddc = DisplayService.devices.find(d => d.class === "ddc");
+ if (ddc)
+ return ddc.name;
+
+ return DisplayService.devices.length > 0 ? DisplayService.devices[0].name : "";
+ }
+
+ Component.onCompleted: {
+ currentDeviceName = resolveDeviceName();
+ }
+
+ property bool isPinnedToScreen: {
+ const pinKey = getScreenPinKey();
+ if (!pinKey || pinKey.length === 0)
+ return false;
+ const pins = SettingsData.brightnessDevicePins || {};
+ return pins[pinKey] === currentDeviceName;
+ }
+
+ function togglePinToScreen() {
+ const pinKey = getScreenPinKey();
+ if (!pinKey || pinKey.length === 0 || !currentDeviceName || currentDeviceName.length === 0)
+ return;
+ const pins = JSON.parse(JSON.stringify(SettingsData.brightnessDevicePins || {}));
+
+ if (isPinnedToScreen) {
+ delete pins[pinKey];
+ } else {
+ pins[pinKey] = currentDeviceName;
+ }
+
+ SettingsData.set("brightnessDevicePins", pins);
+ }
+
+ implicitHeight: {
+ if (height > 0) {
+ return height;
+ }
+ return brightnessContent.height + Theme.spacingM;
+ }
+ 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
+
+ DankFlickable {
+ id: brightnessContent
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.margins: Theme.spacingM
+ anchors.topMargin: Theme.spacingM
+ contentHeight: brightnessColumn.height
+ clip: true
+
+ Column {
+ id: brightnessColumn
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Item {
+ width: parent.width
+ height: 100
+ visible: !DisplayService.brightnessAvailable || !DisplayService.devices || DisplayService.devices.length === 0
+
+ Column {
+ anchors.centerIn: parent
+ spacing: Theme.spacingM
+
+ DankIcon {
+ anchors.horizontalCenter: parent.horizontalCenter
+ name: DisplayService.brightnessAvailable ? "brightness_6" : "error"
+ size: 32
+ color: DisplayService.brightnessAvailable ? Theme.primary : Theme.error
+ }
+
+ StyledText {
+ anchors.horizontalCenter: parent.horizontalCenter
+ text: DisplayService.brightnessAvailable ? I18n.tr("No brightness devices available") : I18n.tr("Brightness control not available")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ horizontalAlignment: Text.AlignHCenter
+ }
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: 40
+ visible: screenName && screenName.length > 0 && DisplayService.devices && DisplayService.devices.length > 1
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
+
+ Item {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingM
+
+ Row {
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingM
+
+ DankIcon {
+ name: "monitor"
+ size: Theme.iconSize
+ color: Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: root.getScreenPinKey() || I18n.tr("Unknown Monitor")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Rectangle {
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ width: pinRow.width + Theme.spacingS * 2
+ height: 28
+ radius: height / 2
+ color: isPinnedToScreen ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05)
+
+ Row {
+ id: pinRow
+ anchors.centerIn: parent
+ spacing: 4
+
+ DankIcon {
+ name: isPinnedToScreen ? "push_pin" : "push_pin"
+ size: 16
+ color: isPinnedToScreen ? Theme.primary : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: isPinnedToScreen ? I18n.tr("Pinned") : I18n.tr("Pin")
+ font.pixelSize: Theme.fontSizeSmall
+ color: isPinnedToScreen ? Theme.primary : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ DankRipple {
+ id: pinRipple
+ cornerRadius: parent.radius
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
+ onClicked: root.togglePinToScreen()
+ }
+ }
+ }
+ }
+
+ Repeater {
+ model: DisplayService.devices || []
+ delegate: Rectangle {
+ required property var modelData
+ required property int index
+
+ property real deviceBrightness: {
+ DisplayService.brightnessVersion;
+ return DisplayService.getDeviceBrightness(modelData.name);
+ }
+
+ width: parent.width
+ height: 100
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
+ border.color: modelData.name === currentDeviceName ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
+ border.width: modelData.name === currentDeviceName ? 2 : 0
+
+ Column {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingS
+
+ Item {
+ width: parent.width
+ height: Math.max(deviceIconColumn.height, deviceInfoColumn.height, exponentControls.height)
+
+ Row {
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingM
+
+ Column {
+ id: deviceIconColumn
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 2
+
+ DankIcon {
+ name: {
+ const deviceClass = modelData.class || "";
+ const deviceName = modelData.name || "";
+
+ if (deviceClass === "backlight" || deviceClass === "ddc") {
+ if (deviceBrightness <= 33)
+ return "brightness_low";
+ if (deviceBrightness <= 66)
+ return "brightness_medium";
+ return "brightness_high";
+ } else if (deviceName.includes("kbd")) {
+ return "keyboard";
+ } else {
+ return "lightbulb";
+ }
+ }
+ size: Theme.iconSize
+ color: modelData.name === currentDeviceName ? Theme.primary : Theme.surfaceText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: Math.round(deviceBrightness) + "%"
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+
+ Column {
+ id: deviceInfoColumn
+ anchors.verticalCenter: parent.verticalCenter
+ width: parent.parent.width - deviceIconColumn.width - exponentControls.width - Theme.spacingM * 3
+
+ StyledText {
+ text: {
+ const name = modelData.name || "";
+ const deviceClass = modelData.class || "";
+ if (deviceClass === "backlight") {
+ return name.replace("_", " ").replace(/\b\w/g, c => c.toUpperCase());
+ }
+ return name;
+ }
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: modelData.name === currentDeviceName ? Font.Medium : Font.Normal
+ elide: Text.ElideRight
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ StyledText {
+ text: modelData.name
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ elide: Text.ElideRight
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ StyledText {
+ text: {
+ const deviceClass = modelData.class || "";
+ if (deviceClass === "backlight")
+ return I18n.tr("Backlight device");
+ if (deviceClass === "ddc")
+ return I18n.tr("DDC/CI monitor");
+ if (deviceClass === "leds")
+ return I18n.tr("LED device");
+ return deviceClass;
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ elide: Text.ElideRight
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+ }
+
+ Row {
+ id: exponentControls
+ width: 140
+ height: 28
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingXS
+ visible: SessionData.getBrightnessExponential(modelData.name)
+ z: 1
+
+ StyledRect {
+ width: 28
+ height: 28
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
+ opacity: SessionData.getBrightnessExponent(modelData.name) > 1.0 ? 1.0 : 0.4
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "remove"
+ size: 14
+ color: Theme.surfaceText
+ }
+
+ StateLayer {
+ stateColor: Theme.primary
+ cornerRadius: parent.radius
+ enabled: SessionData.getBrightnessExponent(modelData.name) > 1.0
+ onClicked: {
+ const current = SessionData.getBrightnessExponent(modelData.name);
+ const newValue = Math.max(1.0, Math.round((current - 0.1) * 10) / 10);
+ SessionData.setBrightnessExponent(modelData.name, newValue);
+ }
+ }
+ }
+
+ StyledRect {
+ width: 50
+ height: 28
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
+ border.width: 0
+
+ StyledText {
+ anchors.centerIn: parent
+ text: SessionData.getBrightnessExponent(modelData.name).toFixed(1)
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.primary
+ }
+ }
+
+ StyledRect {
+ width: 28
+ height: 28
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
+ opacity: SessionData.getBrightnessExponent(modelData.name) < 2.5 ? 1.0 : 0.4
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "add"
+ size: 14
+ color: Theme.surfaceText
+ }
+
+ StateLayer {
+ stateColor: Theme.primary
+ cornerRadius: parent.radius
+ enabled: SessionData.getBrightnessExponent(modelData.name) < 2.5
+ onClicked: {
+ const current = SessionData.getBrightnessExponent(modelData.name);
+ const newValue = Math.min(2.5, Math.round((current + 0.1) * 10) / 10);
+ SessionData.setBrightnessExponent(modelData.name, newValue);
+ }
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: 24
+ radius: height / 2
+ color: SessionData.getBrightnessExponential(modelData.name) ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05)
+
+ Row {
+ anchors.centerIn: parent
+ spacing: 4
+
+ DankIcon {
+ name: "show_chart"
+ size: 14
+ color: SessionData.getBrightnessExponential(modelData.name) ? Theme.primary : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: SessionData.getBrightnessExponential(modelData.name) ? I18n.tr("Exponential") : I18n.tr("Linear")
+ font.pixelSize: Theme.fontSizeSmall
+ color: SessionData.getBrightnessExponential(modelData.name) ? Theme.primary : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ DankRipple {
+ id: expToggleRipple
+ cornerRadius: parent.radius
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => expToggleRipple.trigger(mouse.x, mouse.y)
+ onClicked: {
+ const currentState = SessionData.getBrightnessExponential(modelData.name);
+ SessionData.setBrightnessExponential(modelData.name, !currentState);
+ }
+ }
+ }
+ }
+
+ DankRipple {
+ id: deviceRipple
+ cornerRadius: parent.radius
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ anchors.bottomMargin: 28
+ anchors.rightMargin: SessionData.getBrightnessExponential(modelData.name) ? 145 : 0
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => deviceRipple.trigger(mouse.x, mouse.y)
+ onClicked: {
+ const pinKey = root.getScreenPinKey();
+ if (pinKey.length > 0 && modelData.name !== currentDeviceName) {
+ const pins = JSON.parse(JSON.stringify(SettingsData.brightnessDevicePins || {}));
+ if (pins[pinKey]) {
+ delete pins[pinKey];
+ SettingsData.set("brightnessDevicePins", pins);
+ }
+ }
+ currentDeviceName = modelData.name;
+ deviceNameChanged(modelData.name);
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/DiskUsageDetail.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/DiskUsageDetail.qml
new file mode 100644
index 0000000..90fbf9d
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/DiskUsageDetail.qml
@@ -0,0 +1,177 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property string currentMountPath: "/"
+ property string instanceId: ""
+
+ signal mountPathChanged(string newMountPath)
+
+ implicitHeight: diskContent.height + Theme.spacingM
+ 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
+
+ Component.onCompleted: {
+ DgopService.addRef(["diskmounts"]);
+ }
+
+ Component.onDestruction: {
+ DgopService.removeRef(["diskmounts"]);
+ }
+
+ DankFlickable {
+ id: diskContent
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.margins: Theme.spacingM
+ anchors.topMargin: Theme.spacingM
+ contentHeight: diskColumn.height
+ clip: true
+
+ Column {
+ id: diskColumn
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Item {
+ width: parent.width
+ height: 100
+ visible: !DgopService.dgopAvailable || !DgopService.diskMounts || DgopService.diskMounts.length === 0
+
+ Column {
+ anchors.centerIn: parent
+ spacing: Theme.spacingM
+
+ DankIcon {
+ anchors.horizontalCenter: parent.horizontalCenter
+ name: DgopService.dgopAvailable ? "storage" : "error"
+ size: 32
+ color: DgopService.dgopAvailable ? Theme.primary : Theme.error
+ }
+
+ StyledText {
+ anchors.horizontalCenter: parent.horizontalCenter
+ text: DgopService.dgopAvailable ? I18n.tr("No disk data available") : I18n.tr("dgop not available")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ horizontalAlignment: Text.AlignHCenter
+ }
+ }
+ }
+
+ Repeater {
+ model: DgopService.diskMounts || []
+ delegate: Rectangle {
+ required property var modelData
+ required property int index
+
+ width: parent.width
+ height: 80
+ radius: Theme.cornerRadius
+ color: Theme.surfaceLight
+ border.color: modelData.mount === currentMountPath ? Theme.primary : Theme.outlineLight
+ border.width: modelData.mount === currentMountPath ? 2 : 1
+
+ Row {
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.leftMargin: Theme.spacingM
+ spacing: Theme.spacingM
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 2
+
+ DankIcon {
+ name: "storage"
+ size: Theme.iconSize
+ color: {
+ const percentStr = modelData.percent?.replace("%", "") || "0";
+ const percent = parseFloat(percentStr) || 0;
+ if (percent > 90)
+ return Theme.error;
+ if (percent > 75)
+ return Theme.warning;
+ return modelData.mount === currentMountPath ? Theme.primary : Theme.surfaceText;
+ }
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: {
+ const percentStr = modelData.percent?.replace("%", "") || "0";
+ const percent = parseFloat(percentStr) || 0;
+ return percent.toFixed(0) + "%";
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - 50 - Theme.spacingM
+
+ StyledText {
+ text: modelData.mount === "/" ? I18n.tr("Root Filesystem") : modelData.mount
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: modelData.mount === currentMountPath ? Font.Medium : Font.Normal
+ elide: Text.ElideRight
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ StyledText {
+ text: modelData.mount
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ elide: Text.ElideRight
+ width: parent.width
+ visible: modelData.mount !== "/"
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ StyledText {
+ text: `${modelData.used || "?"} / ${modelData.size || "?"}`
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ elide: Text.ElideRight
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+ }
+
+ DankRipple {
+ id: mountRipple
+ cornerRadius: parent.radius
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => mountRipple.trigger(mouse.x, mouse.y)
+ onClicked: {
+ currentMountPath = modelData.mount;
+ mountPathChanged(modelData.mount);
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/NetworkDetail.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/NetworkDetail.qml
new file mode 100644
index 0000000..5f1738a
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/NetworkDetail.qml
@@ -0,0 +1,893 @@
+import QtQuick
+import QtQuick.Controls
+import Quickshell
+import qs.Common
+import qs.Services
+import qs.Widgets
+import qs.Modals
+
+Rectangle {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ implicitHeight: {
+ if (height > 0)
+ return height;
+ if (NetworkService.wifiToggling)
+ return headerRow.height + wifiToggleContent.height + Theme.spacingM;
+ if (NetworkService.wifiEnabled)
+ return headerRow.height + wifiContent.height + Theme.spacingM;
+ return headerRow.height + wifiOffContent.height + Theme.spacingM;
+ }
+ 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
+
+ Component.onCompleted: {
+ NetworkService.addRef();
+ }
+
+ Component.onDestruction: {
+ NetworkService.removeRef();
+ }
+
+ property bool hasEthernetAvailable: (NetworkService.ethernetDevices?.length ?? 0) > 0
+ property bool hasWifiAvailable: (NetworkService.wifiDevices?.length ?? 0) > 0
+ property bool hasBothConnectionTypes: hasEthernetAvailable && hasWifiAvailable
+ property int maxPinnedNetworks: 3
+
+ function normalizePinList(value) {
+ if (Array.isArray(value))
+ return value.filter(v => v);
+ if (typeof value === "string" && value.length > 0)
+ return [value];
+ return [];
+ }
+
+ function getPinnedNetworks() {
+ const pins = SettingsData.wifiNetworkPins || {};
+ return normalizePinList(pins["preferredWifi"]);
+ }
+
+ property int currentPreferenceIndex: {
+ if (DMSService.apiVersion < 5)
+ return 1;
+ if (NetworkService.backend !== "networkmanager" || DMSService.apiVersion <= 10)
+ return 1;
+ if (!hasEthernetAvailable)
+ return 1;
+ if (!hasWifiAvailable)
+ return 0;
+
+ const pref = NetworkService.userPreference;
+ switch (pref) {
+ case "ethernet":
+ return 0;
+ case "wifi":
+ return 1;
+ default:
+ return NetworkService.networkStatus === "ethernet" ? 0 : 1;
+ }
+ }
+
+ Row {
+ id: headerRow
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ anchors.topMargin: Theme.spacingS
+ height: Math.max(headerLeft.implicitHeight, rightControls.implicitHeight) + Theme.spacingS * 2
+
+ StyledText {
+ id: headerLeft
+ text: I18n.tr("Network")
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Item {
+ height: 1
+ width: parent.width - headerLeft.width - rightControls.width
+ }
+
+ Row {
+ id: rightControls
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingS
+
+ DankDropdown {
+ id: wifiDeviceDropdown
+ anchors.verticalCenter: parent.verticalCenter
+ visible: currentPreferenceIndex === 1 && (NetworkService.wifiDevices?.length ?? 0) > 1
+ compactMode: true
+ dropdownWidth: 120
+ popupWidth: 160
+ alignPopupRight: true
+
+ options: {
+ const devices = NetworkService.wifiDevices;
+ if (!devices || devices.length === 0)
+ return [I18n.tr("Auto")];
+ return [I18n.tr("Auto")].concat(devices.map(d => d.name));
+ }
+
+ currentValue: NetworkService.wifiDeviceOverride || I18n.tr("Auto")
+
+ onValueChanged: value => {
+ const deviceName = value === I18n.tr("Auto") ? "" : value;
+ NetworkService.setWifiDeviceOverride(deviceName);
+ }
+ }
+
+ DankButtonGroup {
+ id: preferenceControls
+ anchors.verticalCenter: parent.verticalCenter
+ visible: hasBothConnectionTypes && NetworkService.backend === "networkmanager" && DMSService.apiVersion > 10
+ buttonHeight: 28
+ textSize: Theme.fontSizeSmall
+
+ model: [I18n.tr("Ethernet"), I18n.tr("WiFi")]
+ currentIndex: currentPreferenceIndex
+ selectionMode: "single"
+ onSelectionChanged: (index, selected) => {
+ if (!selected)
+ return;
+ NetworkService.setNetworkPreference(index === 0 ? "ethernet" : "wifi");
+ }
+ }
+
+ DankActionButton {
+ anchors.verticalCenter: parent.verticalCenter
+ iconName: "settings"
+ buttonSize: 28
+ iconSize: 16
+ iconColor: Theme.surfaceVariantText
+ onClicked: {
+ PopoutService.closeControlCenter();
+ PopoutService.openSettingsWithTab("network");
+ }
+ }
+ }
+ }
+
+ Item {
+ id: wifiToggleContent
+ anchors.top: headerRow.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.margins: Theme.spacingM
+ anchors.topMargin: Theme.spacingM
+ visible: currentPreferenceIndex === 1 && NetworkService.wifiToggling
+ height: visible ? wifiToggleColumn.implicitHeight + Theme.spacingM * 2 : 0
+
+ Column {
+ id: wifiToggleColumn
+ anchors.centerIn: parent
+ spacing: Theme.spacingM
+
+ DankIcon {
+ anchors.horizontalCenter: parent.horizontalCenter
+ name: "sync"
+ size: 32
+ color: Theme.primary
+
+ RotationAnimation on rotation {
+ running: NetworkService.wifiToggling
+ loops: Animation.Infinite
+ from: 0
+ to: 360
+ duration: 1000
+ }
+ }
+
+ StyledText {
+ anchors.horizontalCenter: parent.horizontalCenter
+ text: NetworkService.wifiEnabled ? I18n.tr("Disabling WiFi...") : I18n.tr("Enabling WiFi...")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ horizontalAlignment: Text.AlignHCenter
+ }
+ }
+ }
+
+ Item {
+ id: wifiOffContent
+ anchors.top: headerRow.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.margins: Theme.spacingM
+ anchors.topMargin: Theme.spacingM
+ visible: currentPreferenceIndex === 1 && !NetworkService.wifiEnabled && !NetworkService.wifiToggling
+ height: visible ? wifiOffColumn.implicitHeight + Theme.spacingM * 2 : 0
+
+ Column {
+ id: wifiOffColumn
+ anchors.centerIn: parent
+ spacing: Theme.spacingL
+ width: parent.width
+
+ DankIcon {
+ anchors.horizontalCenter: parent.horizontalCenter
+ name: "wifi_off"
+ size: 48
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
+ }
+
+ StyledText {
+ anchors.horizontalCenter: parent.horizontalCenter
+ text: I18n.tr("WiFi is off")
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ Rectangle {
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: enableWifiLabel.implicitWidth + Theme.spacingL * 2
+ height: enableWifiLabel.implicitHeight + Theme.spacingM * 2
+ radius: height / 2
+ color: enableWifiButton.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
+ border.width: 0
+ border.color: Theme.primary
+
+ StyledText {
+ id: enableWifiLabel
+ anchors.centerIn: parent
+ text: I18n.tr("Enable WiFi")
+ color: Theme.primary
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ }
+
+ MouseArea {
+ id: enableWifiButton
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: NetworkService.toggleWifiRadio()
+ }
+ }
+ }
+ }
+
+ ScriptModel {
+ id: wiredConnectionsModel
+ objectProp: "uuid"
+ values: {
+ const networks = NetworkService.wiredConnections;
+ if (!networks)
+ return [];
+ let sorted = [...networks];
+ sorted.sort((a, b) => {
+ if (a.isActive && !b.isActive)
+ return -1;
+ if (!a.isActive && b.isActive)
+ return 1;
+ return a.id.localeCompare(b.id);
+ });
+ return sorted;
+ }
+ }
+
+ DankFlickable {
+ id: wiredContent
+ anchors.top: headerRow.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.margins: Theme.spacingM
+ anchors.topMargin: Theme.spacingM
+ visible: currentPreferenceIndex === 0 && NetworkService.backend === "networkmanager" && DMSService.apiVersion > 10
+ contentHeight: wiredColumn.height
+ clip: true
+
+ Column {
+ id: wiredColumn
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Repeater {
+ model: wiredConnectionsModel
+
+ delegate: Rectangle {
+ id: wiredDelegate
+ required property var modelData
+ required property int index
+
+ readonly property bool isActive: modelData.isActive
+ readonly property string configName: modelData.id || I18n.tr("Unknown Config")
+
+ width: parent.width
+ height: wiredContentRow.implicitHeight + Theme.spacingM * 2
+ radius: Theme.cornerRadius
+ color: wiredNetworkMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
+ border.color: isActive ? Theme.primary : Theme.outlineLight
+ border.width: isActive ? 2 : 1
+
+ Row {
+ id: wiredContentRow
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.leftMargin: Theme.spacingM
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "lan"
+ size: Theme.iconSize - 4
+ color: wiredDelegate.isActive ? Theme.primary : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ width: 200
+
+ StyledText {
+ text: wiredDelegate.configName
+ font.pixelSize: Theme.fontSizeMedium
+ color: wiredDelegate.isActive ? Theme.primary : Theme.surfaceText
+ font.weight: wiredDelegate.isActive ? Font.Medium : Font.Normal
+ elide: Text.ElideRight
+ width: parent.width
+ }
+ }
+ }
+
+ DankActionButton {
+ id: wiredOptionsButton
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ iconName: "more_horiz"
+ buttonSize: 28
+ onClicked: {
+ if (wiredNetworkContextMenu.visible) {
+ wiredNetworkContextMenu.close();
+ return;
+ }
+ wiredNetworkContextMenu.currentID = modelData.id;
+ wiredNetworkContextMenu.currentUUID = modelData.uuid;
+ wiredNetworkContextMenu.currentConnected = wiredDelegate.isActive;
+ wiredNetworkContextMenu.popup(wiredOptionsButton, -wiredNetworkContextMenu.width + wiredOptionsButton.width, wiredOptionsButton.height + Theme.spacingXS);
+ }
+ }
+
+ DankRipple {
+ id: wiredRipple
+ cornerRadius: parent.radius
+ }
+
+ MouseArea {
+ id: wiredNetworkMouseArea
+ anchors.fill: parent
+ anchors.rightMargin: wiredOptionsButton.width + Theme.spacingS
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => wiredRipple.trigger(mouse.x, mouse.y)
+ onClicked: function (event) {
+ if (modelData.uuid !== NetworkService.ethernetConnectionUuid)
+ NetworkService.connectToSpecificWiredConfig(modelData.uuid);
+ event.accepted = true;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Menu {
+ id: wiredNetworkContextMenu
+ width: 150
+ closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
+
+ property string currentID: ""
+ property string currentUUID: ""
+ property bool currentConnected: false
+
+ background: Rectangle {
+ color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ radius: Theme.cornerRadius
+ border.width: 0
+ border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
+ }
+
+ MenuItem {
+ text: I18n.tr("Activate")
+ height: !wiredNetworkContextMenu.currentConnected ? 32 : 0
+ visible: !wiredNetworkContextMenu.currentConnected
+
+ contentItem: StyledText {
+ text: parent.text
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ leftPadding: Theme.spacingS
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ background: Rectangle {
+ color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
+ radius: Theme.cornerRadius / 2
+ }
+
+ onTriggered: {
+ if (!wiredNetworkContextMenu.currentConnected)
+ NetworkService.connectToSpecificWiredConfig(wiredNetworkContextMenu.currentUUID);
+ }
+ }
+
+ MenuItem {
+ text: I18n.tr("Disconnect")
+ height: wiredNetworkContextMenu.currentConnected ? 32 : 0
+ visible: wiredNetworkContextMenu.currentConnected
+
+ contentItem: StyledText {
+ text: parent.text
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.error
+ leftPadding: Theme.spacingS
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ background: Rectangle {
+ color: parent.hovered ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : "transparent"
+ radius: Theme.cornerRadius / 2
+ }
+
+ onTriggered: {
+ NetworkService.toggleNetworkConnection("ethernet");
+ }
+ }
+
+ MenuItem {
+ text: I18n.tr("Network Info")
+ height: wiredNetworkContextMenu.currentConnected ? 32 : 0
+ visible: wiredNetworkContextMenu.currentConnected
+
+ contentItem: StyledText {
+ text: parent.text
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ leftPadding: Theme.spacingS
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ background: Rectangle {
+ color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
+ radius: Theme.cornerRadius / 2
+ }
+
+ onTriggered: {
+ const networkData = NetworkService.getWiredNetworkInfo(wiredNetworkContextMenu.currentUUID);
+ networkWiredInfoModalLoader.active = true;
+ networkWiredInfoModalLoader.item.showNetworkInfo(wiredNetworkContextMenu.currentID, networkData);
+ }
+ }
+ }
+
+ ScriptModel {
+ id: wifiNetworksModel
+ objectProp: "ssid"
+ values: wifiContent.menuOpen ? wifiContent.frozenNetworks : wifiContent.sortedNetworks
+ }
+
+ Item {
+ id: wifiScanningOverlay
+ anchors.top: headerRow.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.margins: Theme.spacingM
+ anchors.topMargin: Theme.spacingM
+ visible: currentPreferenceIndex === 1 && NetworkService.wifiEnabled && !NetworkService.wifiToggling && NetworkService.wifiInterface && (NetworkService.wifiNetworks?.length ?? 0) < 1 && NetworkService.isScanning
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "refresh"
+ size: 48
+ color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.3)
+
+ RotationAnimation on rotation {
+ running: wifiScanningOverlay.visible
+ loops: Animation.Infinite
+ from: 0
+ to: 360
+ duration: 1000
+ }
+ }
+ }
+
+ DankListView {
+ id: wifiContent
+ anchors.top: headerRow.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.margins: Theme.spacingM
+ anchors.topMargin: Theme.spacingM
+ visible: currentPreferenceIndex === 1 && NetworkService.wifiEnabled && !NetworkService.wifiToggling && !wifiScanningOverlay.visible
+ clip: true
+ spacing: Theme.spacingS
+ model: wifiNetworksModel
+
+ property var frozenNetworks: []
+ property bool menuOpen: false
+ property var sortedNetworks: {
+ const ssid = NetworkService.currentWifiSSID;
+ const networks = NetworkService.wifiNetworks;
+ const pinnedList = root.getPinnedNetworks();
+
+ let sorted = [...networks];
+ sorted.sort((a, b) => {
+ const aPinnedIndex = pinnedList.indexOf(a.ssid);
+ const bPinnedIndex = pinnedList.indexOf(b.ssid);
+ if (aPinnedIndex !== -1 || bPinnedIndex !== -1) {
+ if (aPinnedIndex === -1)
+ return 1;
+ if (bPinnedIndex === -1)
+ return -1;
+ return aPinnedIndex - bPinnedIndex;
+ }
+ if (a.ssid === ssid)
+ return -1;
+ if (b.ssid === ssid)
+ return 1;
+ return b.signal - a.signal;
+ });
+ return sorted;
+ }
+ onSortedNetworksChanged: {
+ if (!menuOpen)
+ frozenNetworks = sortedNetworks;
+ }
+ onMenuOpenChanged: {
+ if (menuOpen)
+ frozenNetworks = sortedNetworks;
+ }
+
+ delegate: Rectangle {
+ id: wifiDelegate
+ required property var modelData
+ required property int index
+
+ readonly property bool isConnected: modelData.ssid === NetworkService.currentWifiSSID
+ readonly property bool isPinned: root.getPinnedNetworks().includes(modelData.ssid)
+ readonly property string networkName: modelData.ssid || I18n.tr("Unknown Network")
+ readonly property int signalStrength: modelData.signal || 0
+
+ width: wifiContent.width
+ height: wifiContentRow.implicitHeight + Theme.spacingM * 2
+ radius: Theme.cornerRadius
+ color: networkMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
+ border.color: wifiDelegate.isConnected ? Theme.primary : Theme.outlineLight
+ border.width: wifiDelegate.isConnected ? 2 : 1
+
+ Row {
+ id: wifiContentRow
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.leftMargin: Theme.spacingM
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: {
+ if (wifiDelegate.signalStrength >= 50)
+ return "wifi";
+ if (wifiDelegate.signalStrength >= 25)
+ return "wifi_2_bar";
+ return "wifi_1_bar";
+ }
+ size: Theme.iconSize - 4
+ color: wifiDelegate.isConnected ? Theme.primary : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ width: 200
+
+ StyledText {
+ text: wifiDelegate.networkName
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: wifiDelegate.isConnected ? Font.Medium : Font.Normal
+ elide: Text.ElideRight
+ width: parent.width
+ }
+
+ Row {
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: wifiDelegate.isConnected ? I18n.tr("Connected") + " \u2022" : (modelData.secured ? I18n.tr("Secured") + " \u2022" : I18n.tr("Open") + " \u2022")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+
+ StyledText {
+ text: modelData.saved ? I18n.tr("Saved") : ""
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.primary
+ visible: text.length > 0
+ }
+
+ StyledText {
+ text: (modelData.saved ? "\u2022 " : "") + wifiDelegate.signalStrength + "%"
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+ }
+ }
+
+ DankActionButton {
+ id: optionsButton
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ iconName: "more_horiz"
+ buttonSize: 28
+ onClicked: {
+ if (networkContextMenu.visible) {
+ networkContextMenu.close();
+ return;
+ }
+ wifiContent.menuOpen = true;
+ networkContextMenu.currentSSID = modelData.ssid;
+ networkContextMenu.currentSecured = modelData.secured;
+ networkContextMenu.currentConnected = wifiDelegate.isConnected;
+ networkContextMenu.currentSaved = modelData.saved;
+ networkContextMenu.currentSignal = modelData.signal;
+ networkContextMenu.currentAutoconnect = modelData.autoconnect || false;
+ networkContextMenu.popup(optionsButton, -networkContextMenu.width + optionsButton.width, optionsButton.height + Theme.spacingXS);
+ }
+ }
+
+ Rectangle {
+ id: pinButton
+ anchors.right: parent.right
+ anchors.rightMargin: optionsButton.width + Theme.spacingM + Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ width: pinWifiRow.width + Theme.spacingS * 2
+ height: pinWifiRow.implicitHeight + Theme.spacingXS * 2
+ radius: height / 2
+ color: wifiDelegate.isPinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05)
+
+ Row {
+ id: pinWifiRow
+ anchors.centerIn: parent
+ spacing: 4
+
+ DankIcon {
+ name: "push_pin"
+ size: 16
+ color: wifiDelegate.isPinned ? Theme.primary : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: wifiDelegate.isPinned ? I18n.tr("Pinned") : I18n.tr("Pin")
+ font.pixelSize: Theme.fontSizeSmall
+ color: wifiDelegate.isPinned ? Theme.primary : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ DankRipple {
+ id: pinRipple
+ cornerRadius: parent.radius
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
+ onClicked: {
+ const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {}));
+ let pinnedList = root.normalizePinList(pins["preferredWifi"]);
+ const pinIndex = pinnedList.indexOf(modelData.ssid);
+
+ if (pinIndex !== -1) {
+ pinnedList.splice(pinIndex, 1);
+ } else {
+ pinnedList.unshift(modelData.ssid);
+ if (pinnedList.length > root.maxPinnedNetworks)
+ pinnedList = pinnedList.slice(0, root.maxPinnedNetworks);
+ }
+
+ if (pinnedList.length > 0)
+ pins["preferredWifi"] = pinnedList;
+ else
+ delete pins["preferredWifi"];
+
+ SettingsData.set("wifiNetworkPins", pins);
+ }
+ }
+ }
+
+ DankActionButton {
+ id: qrCodeButton
+ visible: modelData.secured && modelData.saved
+ anchors.right: parent.right
+ anchors.rightMargin: optionsButton.width + pinWifiRow.width + 3 * Theme.spacingM + Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ iconName: "qr_code"
+ buttonSize: 28
+ onClicked: {
+ PopoutService.showWifiQRCodeModal(modelData.ssid);
+ }
+ }
+
+ DankRipple {
+ id: wifiRipple
+ cornerRadius: parent.radius
+ }
+
+ MouseArea {
+ id: networkMouseArea
+ anchors.fill: parent
+ anchors.rightMargin: optionsButton.width + pinWifiRow.width + (qrCodeButton.visible ? qrCodeButton.width : 0) + Theme.spacingS * 5 + Theme.spacingM
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => wifiRipple.trigger(mouse.x, mouse.y)
+ onClicked: function (event) {
+ if (wifiDelegate.isConnected) {
+ event.accepted = true;
+ return;
+ }
+ if (modelData.secured && !modelData.saved && DMSService.apiVersion < 7) {
+ PopoutService.showWifiPasswordModal(modelData.ssid);
+ } else {
+ NetworkService.connectToWifi(modelData.ssid);
+ }
+ event.accepted = true;
+ }
+ }
+ }
+ }
+
+ Menu {
+ id: networkContextMenu
+ width: 150
+ closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
+
+ property string currentSSID: ""
+ property bool currentSecured: false
+ property bool currentConnected: false
+ property bool currentSaved: false
+ property int currentSignal: 0
+ property bool currentAutoconnect: false
+
+ readonly property bool showSavedOptions: currentSaved || currentConnected
+
+ onClosed: {
+ wifiContent.menuOpen = false;
+ }
+
+ background: Rectangle {
+ color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ radius: Theme.cornerRadius
+ border.width: 0
+ border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
+ }
+
+ MenuItem {
+ text: networkContextMenu.currentConnected ? I18n.tr("Disconnect") : I18n.tr("Connect")
+ height: 32
+
+ contentItem: StyledText {
+ text: parent.text
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ leftPadding: Theme.spacingS
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ background: Rectangle {
+ color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
+ radius: Theme.cornerRadius / 2
+ }
+
+ onTriggered: {
+ if (networkContextMenu.currentConnected) {
+ NetworkService.disconnectWifi();
+ return;
+ }
+ if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved && DMSService.apiVersion < 7) {
+ PopoutService.showWifiPasswordModal(networkContextMenu.currentSSID);
+ return;
+ }
+ NetworkService.connectToWifi(networkContextMenu.currentSSID);
+ }
+ }
+
+ MenuItem {
+ text: I18n.tr("Network Info")
+ height: 32
+
+ contentItem: StyledText {
+ text: parent.text
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ leftPadding: Theme.spacingS
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ background: Rectangle {
+ color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
+ radius: Theme.cornerRadius / 2
+ }
+
+ onTriggered: {
+ const networkData = NetworkService.getNetworkInfo(networkContextMenu.currentSSID);
+ networkInfoModalLoader.active = true;
+ networkInfoModalLoader.item.showNetworkInfo(networkContextMenu.currentSSID, networkData);
+ }
+ }
+
+ MenuItem {
+ text: networkContextMenu.currentAutoconnect ? I18n.tr("Disable Autoconnect") : I18n.tr("Enable Autoconnect")
+ height: networkContextMenu.showSavedOptions && DMSService.apiVersion > 13 ? 32 : 0
+ visible: networkContextMenu.showSavedOptions && DMSService.apiVersion > 13
+
+ contentItem: StyledText {
+ text: parent.text
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ leftPadding: Theme.spacingS
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ background: Rectangle {
+ color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
+ radius: Theme.cornerRadius / 2
+ }
+
+ onTriggered: {
+ NetworkService.setWifiAutoconnect(networkContextMenu.currentSSID, !networkContextMenu.currentAutoconnect);
+ }
+ }
+
+ MenuItem {
+ text: I18n.tr("Forget Network")
+ height: networkContextMenu.showSavedOptions ? 32 : 0
+ visible: networkContextMenu.showSavedOptions
+
+ contentItem: StyledText {
+ text: parent.text
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.error
+ leftPadding: Theme.spacingS
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ background: Rectangle {
+ color: parent.hovered ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : "transparent"
+ radius: Theme.cornerRadius / 2
+ }
+
+ onTriggered: {
+ NetworkService.forgetWifiNetwork(networkContextMenu.currentSSID);
+ }
+ }
+ }
+
+ Loader {
+ id: networkInfoModalLoader
+ active: false
+ sourceComponent: NetworkInfoModal {}
+ }
+
+ Loader {
+ id: networkWiredInfoModalLoader
+ active: false
+ sourceComponent: NetworkWiredInfoModal {}
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Models/WidgetModel.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Models/WidgetModel.qml
new file mode 100644
index 0000000..ac616ae
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Models/WidgetModel.qml
@@ -0,0 +1,283 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Modules.ControlCenter.BuiltinPlugins
+import "../utils/widgets.js" as WidgetUtils
+
+QtObject {
+ id: root
+
+ property var vpnBuiltinInstance: null
+ property var cupsBuiltinInstance: null
+
+ property var vpnLoader: Loader {
+ active: false
+ sourceComponent: Component {
+ VpnWidget {}
+ }
+
+ onItemChanged: {
+ root.vpnBuiltinInstance = item;
+ }
+
+ Connections {
+ target: SettingsData
+ function onControlCenterWidgetsChanged() {
+ const widgets = SettingsData.controlCenterWidgets || [];
+ const hasVpnWidget = widgets.some(w => w.id === "builtin_vpn");
+ if (!hasVpnWidget && vpnLoader.active) {
+ console.log("VpnWidget: No VPN widget in control center, deactivating loader");
+ vpnLoader.active = false;
+ }
+ }
+ }
+ }
+
+ property var cupsLoader: Loader {
+ active: false
+ sourceComponent: Component {
+ CupsWidget {}
+ }
+
+ onItemChanged: {
+ root.cupsBuiltinInstance = item;
+ }
+
+ onActiveChanged: {
+ if (!active) {
+ root.cupsBuiltinInstance = null;
+ }
+ }
+
+ Connections {
+ target: SettingsData
+ function onControlCenterWidgetsChanged() {
+ const widgets = SettingsData.controlCenterWidgets || [];
+ const hasCupsWidget = widgets.some(w => w.id === "builtin_cups");
+ if (!hasCupsWidget && cupsLoader.active) {
+ console.log("CupsWidget: No CUPS widget in control center, deactivating loader");
+ cupsLoader.active = false;
+ }
+ }
+ }
+ }
+
+ readonly property var coreWidgetDefinitions: [
+ {
+ "id": "nightMode",
+ "text": I18n.tr("Night Mode"),
+ "description": I18n.tr("Blue light filter"),
+ "icon": "nightlight",
+ "type": "toggle",
+ "enabled": DisplayService.automationAvailable,
+ "warning": !DisplayService.automationAvailable ? I18n.tr("Requires night mode support") : undefined
+ },
+ {
+ "id": "darkMode",
+ "text": I18n.tr("Dark Mode"),
+ "description": I18n.tr("System theme toggle"),
+ "icon": "contrast",
+ "type": "toggle",
+ "enabled": true
+ },
+ {
+ "id": "doNotDisturb",
+ "text": I18n.tr("Do Not Disturb"),
+ "description": I18n.tr("Block notifications"),
+ "icon": "do_not_disturb_on",
+ "type": "toggle",
+ "enabled": true
+ },
+ {
+ "id": "idleInhibitor",
+ "text": I18n.tr("Keep Awake"),
+ "description": I18n.tr("Prevent screen timeout"),
+ "icon": "motion_sensor_active",
+ "type": "toggle",
+ "enabled": true
+ },
+ {
+ "id": "wifi",
+ "text": I18n.tr("Network"),
+ "description": I18n.tr("Wi-Fi and Ethernet connection"),
+ "icon": "wifi",
+ "type": "connection",
+ "enabled": NetworkService.wifiAvailable,
+ "warning": !NetworkService.wifiAvailable ? I18n.tr("Wi-Fi not available") : undefined
+ },
+ {
+ "id": "bluetooth",
+ "text": I18n.tr("Bluetooth"),
+ "description": I18n.tr("Device connections"),
+ "icon": "bluetooth",
+ "type": "connection",
+ "enabled": BluetoothService.available,
+ "warning": !BluetoothService.available ? I18n.tr("Bluetooth not available") : undefined
+ },
+ {
+ "id": "audioOutput",
+ "text": I18n.tr("Audio Output"),
+ "description": I18n.tr("Speaker settings"),
+ "icon": "volume_up",
+ "type": "connection",
+ "enabled": true
+ },
+ {
+ "id": "audioInput",
+ "text": I18n.tr("Audio Input"),
+ "description": I18n.tr("Microphone settings"),
+ "icon": "mic",
+ "type": "connection",
+ "enabled": true
+ },
+ {
+ "id": "volumeSlider",
+ "text": I18n.tr("Volume Slider"),
+ "description": I18n.tr("Audio volume control"),
+ "icon": "volume_up",
+ "type": "slider",
+ "enabled": true
+ },
+ {
+ "id": "brightnessSlider",
+ "text": I18n.tr("Brightness Slider"),
+ "description": I18n.tr("Display brightness control"),
+ "icon": "brightness_6",
+ "type": "slider",
+ "enabled": DisplayService.brightnessAvailable,
+ "warning": !DisplayService.brightnessAvailable ? I18n.tr("Brightness control not available") : undefined,
+ "allowMultiple": true
+ },
+ {
+ "id": "inputVolumeSlider",
+ "text": I18n.tr("Input Volume Slider"),
+ "description": I18n.tr("Microphone volume control"),
+ "icon": "mic",
+ "type": "slider",
+ "enabled": true
+ },
+ {
+ "id": "battery",
+ "text": I18n.tr("Battery"),
+ "description": I18n.tr("Battery and power management"),
+ "icon": "battery_std",
+ "type": "action",
+ "enabled": true
+ },
+ {
+ "id": "diskUsage",
+ "text": I18n.tr("Disk Usage"),
+ "description": I18n.tr("Filesystem usage monitoring"),
+ "icon": "storage",
+ "type": "action",
+ "enabled": DgopService.dgopAvailable,
+ "warning": !DgopService.dgopAvailable ? I18n.tr("Requires 'dgop' tool") : undefined,
+ "allowMultiple": true
+ },
+ {
+ "id": "colorPicker",
+ "text": I18n.tr("Color Picker"),
+ "description": I18n.tr("Choose colors from palette"),
+ "icon": "palette",
+ "type": "action",
+ "enabled": true
+ },
+ {
+ "id": "builtin_vpn",
+ "text": I18n.tr("VPN"),
+ "description": I18n.tr("VPN connections"),
+ "icon": "vpn_key",
+ "type": "builtin_plugin",
+ "enabled": DMSNetworkService.available,
+ "warning": !DMSNetworkService.available ? I18n.tr("VPN not available") : undefined,
+ "isBuiltinPlugin": true
+ },
+ {
+ "id": "builtin_cups",
+ "text": I18n.tr("Printers"),
+ "description": I18n.tr("Print Server Management"),
+ "icon": "Print",
+ "type": "builtin_plugin",
+ "enabled": CupsService.available,
+ "warning": !CupsService.available ? I18n.tr("CUPS not available") : undefined,
+ "isBuiltinPlugin": true
+ }
+ ]
+
+ function getPluginWidgets() {
+ const plugins = [];
+ const loadedPlugins = PluginService.getLoadedPlugins();
+
+ for (var i = 0; i < loadedPlugins.length; i++) {
+ const plugin = loadedPlugins[i];
+
+ if (plugin.type === "daemon") {
+ continue;
+ }
+
+ const pluginComponent = PluginService.pluginWidgetComponents[plugin.id];
+ if (!pluginComponent)
+ continue;
+
+ let tempInstance;
+ try {
+ tempInstance = pluginComponent.createObject(null);
+ } catch (e) {
+ PluginService.reloadPlugin(plugin.id);
+ continue;
+ }
+ if (!tempInstance)
+ continue;
+
+ const hasCCWidget = tempInstance.ccWidgetIcon && tempInstance.ccWidgetIcon.length > 0;
+ tempInstance.destroy();
+
+ if (!hasCCWidget) {
+ continue;
+ }
+
+ plugins.push({
+ "id": "plugin_" + plugin.id,
+ "pluginId": plugin.id,
+ "text": plugin.name || I18n.tr("Plugin"),
+ "description": plugin.description || "",
+ "icon": plugin.icon || "extension",
+ "type": "plugin",
+ "enabled": true,
+ "isPlugin": true
+ });
+ }
+
+ return plugins;
+ }
+
+ readonly property var baseWidgetDefinitions: coreWidgetDefinitions
+
+ function getWidgetForId(widgetId) {
+ return WidgetUtils.getWidgetForId(baseWidgetDefinitions, widgetId);
+ }
+
+ function addWidget(widgetId) {
+ WidgetUtils.addWidget(widgetId);
+ }
+
+ function removeWidget(index) {
+ WidgetUtils.removeWidget(index);
+ }
+
+ function toggleWidgetSize(index) {
+ WidgetUtils.toggleWidgetSize(index);
+ }
+
+ function moveWidget(fromIndex, toIndex) {
+ WidgetUtils.moveWidget(fromIndex, toIndex);
+ }
+
+ function resetToDefault() {
+ WidgetUtils.resetToDefault();
+ }
+
+ function clearAll() {
+ WidgetUtils.clearAll();
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/AudioSliderRow.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/AudioSliderRow.qml
new file mode 100644
index 0000000..81d346d
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/AudioSliderRow.qml
@@ -0,0 +1,103 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Row {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property var defaultSink: AudioService.sink
+ property color sliderTrackColor: "transparent"
+
+ height: 40
+ spacing: 0
+
+ Rectangle {
+ width: Theme.iconSize + Theme.spacingS * 2
+ height: Theme.iconSize + Theme.spacingS * 2
+ anchors.verticalCenter: parent.verticalCenter
+ radius: (Theme.iconSize + Theme.spacingS * 2) / 2
+ color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.primary, 0)
+
+ DankRipple {
+ id: iconRipple
+ cornerRadius: parent.radius
+ }
+
+ MouseArea {
+ id: iconArea
+ anchors.fill: parent
+ visible: defaultSink !== null
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => iconRipple.trigger(mouse.x, mouse.y)
+ onClicked: {
+ if (defaultSink) {
+ SessionData.suppressOSDTemporarily();
+ defaultSink.audio.muted = !defaultSink.audio.muted;
+ }
+ }
+ }
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: {
+ if (!defaultSink)
+ return "volume_off";
+
+ let volume = defaultSink.audio.volume;
+ let muted = defaultSink.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";
+ }
+ size: Theme.iconSize
+ color: defaultSink && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
+ }
+ }
+
+ DankSlider {
+ id: volumeSlider
+
+ readonly property real actualVolumePercent: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0
+
+ anchors.verticalCenter: parent.verticalCenter
+ width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
+ enabled: defaultSink !== null
+ minimum: 0
+ maximum: AudioService.sinkMaxVolume
+ showValue: true
+ unit: "%"
+ valueOverride: actualVolumePercent
+ thumbOutlineColor: Theme.surfaceContainer
+ trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+
+ onSliderValueChanged: function (newValue) {
+ if (defaultSink) {
+ SessionData.suppressOSDTemporarily();
+ defaultSink.audio.volume = newValue / 100.0;
+ if (newValue > 0 && defaultSink.audio.muted) {
+ defaultSink.audio.muted = false;
+ }
+ AudioService.playVolumeChangeSoundIfEnabled();
+ }
+ }
+ }
+
+ Binding {
+ target: volumeSlider
+ property: "value"
+ value: defaultSink ? Math.min(AudioService.sinkMaxVolume, Math.round(defaultSink.audio.volume * 100)) : 0
+ when: !volumeSlider.isDragging
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/BatteryPill.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/BatteryPill.qml
new file mode 100644
index 0000000..c14c308
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/BatteryPill.qml
@@ -0,0 +1,46 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Modules.ControlCenter.Widgets
+
+CompoundPill {
+ id: root
+
+ iconName: BatteryService.getBatteryIcon()
+
+ isActive: BatteryService.batteryAvailable && (BatteryService.isCharging || BatteryService.isPluggedIn)
+
+ primaryText: {
+ if (!BatteryService.batteryAvailable) {
+ return I18n.tr("No battery");
+ }
+ return I18n.tr("Battery");
+ }
+
+ secondaryText: {
+ if (!BatteryService.batteryAvailable) {
+ return I18n.tr("Not available");
+ }
+ if (BatteryService.isCharging) {
+ return `${BatteryService.batteryLevel}% • ` + I18n.tr("Charging");
+ }
+ if (BatteryService.isPluggedIn) {
+ return `${BatteryService.batteryLevel}% • ` + I18n.tr("Plugged in");
+ }
+ return `${BatteryService.batteryLevel}%`;
+ }
+
+ iconColor: {
+ if (BatteryService.isLowBattery && !BatteryService.isCharging) {
+ return Theme.error;
+ }
+ if (BatteryService.isCharging || BatteryService.isPluggedIn) {
+ return Theme.primary;
+ }
+ return Theme.surfaceText;
+ }
+
+ onToggled: {
+ expandClicked();
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/BrightnessSliderRow.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/BrightnessSliderRow.qml
new file mode 100644
index 0000000..037c08f
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/BrightnessSliderRow.qml
@@ -0,0 +1,194 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Row {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property string deviceName: ""
+ property string instanceId: ""
+ property string screenName: ""
+ property var parentScreen: null
+
+ signal iconClicked
+
+ height: 40
+ spacing: 0
+
+ DankTooltipV2 {
+ id: sharedTooltip
+ }
+
+ property string targetDeviceName: {
+ if (!DisplayService.brightnessAvailable || !DisplayService.devices || DisplayService.devices.length === 0) {
+ return "";
+ }
+
+ if (screenName && screenName.length > 0) {
+ const pins = SettingsData.brightnessDevicePins || {};
+ const pinnedDevice = pins[screenName];
+ if (pinnedDevice && pinnedDevice.length > 0) {
+ const found = DisplayService.devices.find(dev => dev.name === pinnedDevice);
+ if (found) {
+ return found.name;
+ }
+ }
+ }
+
+ if (deviceName && deviceName.length > 0) {
+ const found = DisplayService.devices.find(dev => dev.name === deviceName);
+ if (found) {
+ return found.name;
+ }
+ }
+
+ const currentDeviceName = DisplayService.currentDevice;
+ if (currentDeviceName) {
+ const found = DisplayService.devices.find(dev => dev.name === currentDeviceName);
+ if (found) {
+ return found.name;
+ }
+ }
+
+ const backlight = DisplayService.devices.find(d => d.class === "backlight");
+ if (backlight) {
+ return backlight.name;
+ }
+
+ const ddc = DisplayService.devices.find(d => d.class === "ddc");
+ if (ddc) {
+ return ddc.name;
+ }
+
+ return DisplayService.devices.length > 0 ? DisplayService.devices[0].name : "";
+ }
+
+ property var targetDevice: {
+ if (!targetDeviceName || !DisplayService.devices) {
+ return null;
+ }
+
+ return DisplayService.devices.find(dev => dev.name === targetDeviceName) || null;
+ }
+
+ property real targetBrightness: {
+ DisplayService.brightnessVersion;
+ if (!targetDeviceName) {
+ return 0;
+ }
+
+ return DisplayService.getDeviceBrightness(targetDeviceName);
+ }
+
+ Rectangle {
+ width: Theme.iconSize + Theme.spacingS * 2
+ height: Theme.iconSize + Theme.spacingS * 2
+ anchors.verticalCenter: parent.verticalCenter
+ radius: (Theme.iconSize + Theme.spacingS * 2) / 2
+ color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.primary, 0)
+
+ DankRipple {
+ id: iconRipple
+ cornerRadius: parent.radius
+ }
+
+ MouseArea {
+ id: iconArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: DisplayService.devices && DisplayService.devices.length > 1 ? Qt.PointingHandCursor : Qt.ArrowCursor
+
+ onPressed: mouse => iconRipple.trigger(mouse.x, mouse.y)
+ onClicked: {
+ if (DisplayService.devices && DisplayService.devices.length > 1) {
+ root.iconClicked();
+ }
+ }
+
+ onEntered: {
+ const tooltipText = targetDevice ? "bl device: " + targetDevice.name : "Backlight Control";
+ sharedTooltip.show(tooltipText, iconArea, 0, 0, "bottom");
+ }
+
+ onExited: {
+ sharedTooltip.hide();
+ }
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: {
+ if (!DisplayService.brightnessAvailable || !targetDevice) {
+ return "brightness_low";
+ }
+
+ if (targetDevice.class === "backlight" || targetDevice.class === "ddc") {
+ const brightness = targetBrightness;
+ if (brightness <= 33)
+ return "brightness_low";
+ if (brightness <= 66)
+ return "brightness_medium";
+ return "brightness_high";
+ } else if (targetDevice.name.includes("kbd")) {
+ return "keyboard";
+ } else {
+ return "lightbulb";
+ }
+ }
+ size: Theme.iconSize
+ color: DisplayService.brightnessAvailable && targetDevice && targetBrightness > 0 ? Theme.primary : Theme.surfaceText
+ }
+ }
+ }
+
+ DankSlider {
+ id: brightnessSlider
+
+ anchors.verticalCenter: parent.verticalCenter
+ width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
+ enabled: DisplayService.brightnessAvailable && targetDeviceName.length > 0
+ minimum: {
+ if (!targetDevice)
+ return 1;
+ const isExponential = SessionData.getBrightnessExponential(targetDevice.id);
+ if (isExponential) {
+ return 1;
+ }
+ return (targetDevice.class === "backlight" || targetDevice.class === "ddc") ? 1 : 0;
+ }
+ maximum: {
+ if (!targetDevice)
+ return 100;
+ const isExponential = SessionData.getBrightnessExponential(targetDevice.id);
+ if (isExponential) {
+ return 100;
+ }
+ return targetDevice.displayMax || 100;
+ }
+ showValue: true
+ unit: {
+ if (!targetDevice)
+ return "%";
+ const isExponential = SessionData.getBrightnessExponential(targetDevice.id);
+ if (isExponential) {
+ return "%";
+ }
+ return targetDevice.class === "ddc" ? "" : "%";
+ }
+ onSliderValueChanged: function (newValue) {
+ if (DisplayService.brightnessAvailable && targetDeviceName) {
+ DisplayService.setBrightness(newValue, targetDeviceName, true);
+ }
+ }
+ thumbOutlineColor: Theme.surfaceContainer
+ trackColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+
+ Binding on value {
+ value: root.targetBrightness
+ when: !brightnessSlider.isDragging
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/ColorPickerPill.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/ColorPickerPill.qml
new file mode 100644
index 0000000..a95410b
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/ColorPickerPill.qml
@@ -0,0 +1,29 @@
+import QtQuick
+import qs.Common
+import qs.Modules.ControlCenter.Widgets
+
+CompoundPill {
+ id: root
+
+ property var colorPickerModal: null
+
+ isActive: true
+ iconName: "palette"
+ iconColor: Theme.primary
+ primaryText: I18n.tr("Color Picker")
+ secondaryText: I18n.tr("Choose a color")
+
+ onToggled: {
+ console.log("ColorPickerPill toggled, modal:", colorPickerModal);
+ if (colorPickerModal) {
+ colorPickerModal.show();
+ }
+ }
+
+ onExpandClicked: {
+ console.log("ColorPickerPill expandClicked, modal:", colorPickerModal);
+ if (colorPickerModal) {
+ colorPickerModal.show();
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/CompactSlider.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/CompactSlider.qml
new file mode 100644
index 0000000..eeed4c4
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/CompactSlider.qml
@@ -0,0 +1,71 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property string iconName: ""
+ property color iconColor: Theme.surfaceText
+ property string labelText: ""
+ property real value: 0.0
+ property real maximumValue: 1.0
+ property real minimumValue: 0.0
+ property bool enabled: true
+
+ signal sliderValueChanged(real value)
+
+ width: parent ? parent.width : 200
+ height: 60
+ 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
+ opacity: enabled ? 1.0 : 0.6
+
+ Row {
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.leftMargin: Theme.spacingM
+ anchors.right: sliderContainer.left
+ anchors.rightMargin: Theme.spacingS
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: root.iconName
+ size: Theme.iconSize
+ color: root.iconColor
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: root.labelText
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Rectangle {
+ id: sliderContainer
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.rightMargin: Theme.spacingM
+ width: 120
+ height: parent.height - Theme.spacingS * 2
+
+ DankSlider {
+ anchors.centerIn: parent
+ width: parent.width
+ enabled: root.enabled
+ minimum: Math.round(root.minimumValue * 100)
+ maximum: Math.round(root.maximumValue * 100)
+ value: Math.round(root.value * 100)
+ onSliderValueChanged: root.sliderValueChanged(newValue / 100.0)
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/CompoundPill.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/CompoundPill.qml
new file mode 100644
index 0000000..9165ec4
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/CompoundPill.qml
@@ -0,0 +1,205 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property string iconName: ""
+ property color iconColor: Theme.surfaceText
+ property string primaryText: ""
+ property string secondaryText: ""
+ property bool expanded: false
+ property bool isActive: false
+ property bool showExpandArea: true
+
+ signal toggled
+ signal expandClicked
+ signal wheelEvent(var wheelEvent)
+
+ width: parent ? parent.width : 220
+ height: 60
+ radius: Theme.cornerRadius
+
+ function hoverTint(base) {
+ const factor = 1.2;
+ return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
+ }
+
+ readonly property color _containerBg: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+
+ color: {
+ const baseColor = bodyMouse.containsMouse ? Theme.primaryPressed : _containerBg;
+ return baseColor;
+ }
+ border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.10)
+ border.width: 0
+ antialiasing: true
+
+ readonly property color _labelPrimary: Theme.surfaceText
+ readonly property color _labelSecondary: Theme.surfaceVariantText
+ readonly property color _tileBgActive: Theme.ccTileActiveBg
+ readonly property color _tileBgInactive: {
+ const transparency = Theme.popupTransparency;
+ const surface = Theme.surfaceContainer || Qt.rgba(0.1, 0.1, 0.1, 1);
+ return Qt.rgba(surface.r, surface.g, surface.b, transparency);
+ }
+ readonly property color _tileRingActive: Theme.ccTileRing
+ readonly property color _tileRingInactive: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.18)
+ readonly property color _tileIconActive: Theme.ccTileActiveText
+ readonly property color _tileIconInactive: Theme.ccTileInactiveIcon
+
+ property int _padH: Theme.spacingS
+ property int _tileSize: 48
+ property int _tileRadius: Theme.cornerRadius
+
+ Rectangle {
+ id: rightHoverOverlay
+ anchors.fill: parent
+ radius: root.radius
+ z: 0
+ visible: false
+ color: hoverTint(_containerBg)
+ opacity: 0.08
+ antialiasing: true
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ }
+ }
+ }
+
+ DankRipple {
+ id: bodyRipple
+ cornerRadius: root.radius
+ }
+
+ Row {
+ id: row
+ anchors.fill: parent
+ anchors.leftMargin: _padH
+ anchors.rightMargin: Theme.spacingM
+ spacing: Theme.spacingM
+
+ Rectangle {
+ id: iconTile
+ z: 1
+ width: _tileSize
+ height: _tileSize
+ anchors.verticalCenter: parent.verticalCenter
+ radius: _tileRadius
+ color: isActive ? _tileBgActive : _tileBgInactive
+ border.color: isActive ? _tileRingActive : "transparent"
+ border.width: isActive ? 1 : 0
+ antialiasing: true
+
+ Rectangle {
+ anchors.fill: parent
+ radius: _tileRadius
+ color: hoverTint(iconTile.color)
+ opacity: tileMouse.pressed ? 0.3 : (tileMouse.containsMouse ? 0.2 : 0.0)
+ visible: opacity > 0
+ antialiasing: true
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ }
+ }
+ }
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: iconName
+ size: Theme.iconSize
+ color: isActive ? _tileIconActive : _tileIconInactive
+ }
+
+ DankRipple {
+ id: tileRipple
+ cornerRadius: _tileRadius
+ }
+
+ MouseArea {
+ id: tileMouse
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => tileRipple.trigger(mouse.x, mouse.y)
+ onClicked: root.toggled()
+ }
+ }
+
+ Item {
+ id: body
+ width: row.width - iconTile.width - row.spacing
+ height: row.height
+
+ Column {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 2
+
+ StyledText {
+ width: parent.width
+ text: root.primaryText
+ color: _labelPrimary
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ elide: Text.ElideRight
+ wrapMode: Text.NoWrap
+ horizontalAlignment: Text.AlignLeft
+ }
+ StyledText {
+ width: parent.width
+ text: root.secondaryText
+ color: _labelSecondary
+ font.pixelSize: Theme.fontSizeSmall
+ visible: text.length > 0
+ elide: Text.ElideRight
+ wrapMode: Text.NoWrap
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+
+ MouseArea {
+ id: bodyMouse
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onEntered: {
+ rightHoverOverlay.visible = true;
+ rightHoverOverlay.opacity = 0.08;
+ }
+ onExited: {
+ rightHoverOverlay.opacity = 0.0;
+ rightHoverOverlay.visible = false;
+ }
+ onPressed: mouse => {
+ const pos = mapToItem(root, mouse.x, mouse.y);
+ bodyRipple.trigger(pos.x, pos.y);
+ rightHoverOverlay.opacity = 0.16;
+ }
+ onReleased: rightHoverOverlay.opacity = containsMouse ? 0.08 : 0.0
+ onClicked: root.expandClicked()
+ onWheel: function (ev) {
+ root.wheelEvent(ev);
+ }
+ }
+ }
+ }
+
+ focus: true
+ Keys.onPressed: function (ev) {
+ if (ev.key === Qt.Key_Space || ev.key === Qt.Key_Return) {
+ root.toggled();
+ ev.accepted = true;
+ } else if (ev.key === Qt.Key_Right) {
+ root.expandClicked();
+ ev.accepted = true;
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/DetailView.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/DetailView.qml
new file mode 100644
index 0000000..2d946f3
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/DetailView.qml
@@ -0,0 +1,23 @@
+import QtQuick
+
+Rectangle {
+ id: root
+
+ property string title: ""
+ property Component content: null
+ property bool isVisible: true
+ property int contentHeight: 300
+
+ width: parent ? parent.width : 400
+ implicitHeight: isVisible ? contentHeight : 0
+ height: implicitHeight
+ color: "transparent"
+ clip: true
+
+ Loader {
+ id: contentLoader
+ anchors.fill: parent
+ sourceComponent: root.content
+ asynchronous: true
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/DiskUsagePill.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/DiskUsagePill.qml
new file mode 100644
index 0000000..827df4e
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/DiskUsagePill.qml
@@ -0,0 +1,76 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Modules.ControlCenter.Widgets
+
+CompoundPill {
+ id: root
+
+ property string mountPath: "/"
+ property string instanceId: ""
+
+ iconName: "storage"
+
+ property var selectedMount: {
+ if (!DgopService.diskMounts || DgopService.diskMounts.length === 0) {
+ return null;
+ }
+
+ const targetMount = DgopService.diskMounts.find(mount => mount.mount === mountPath);
+ return targetMount || DgopService.diskMounts.find(mount => mount.mount === "/") || DgopService.diskMounts[0];
+ }
+
+ property real usagePercent: {
+ if (!selectedMount || !selectedMount.percent) {
+ return 0;
+ }
+ const percentStr = selectedMount.percent.replace("%", "");
+ return parseFloat(percentStr) || 0;
+ }
+
+ isActive: DgopService.dgopAvailable && selectedMount !== null
+
+ primaryText: {
+ if (!DgopService.dgopAvailable) {
+ return I18n.tr("Disk Usage");
+ }
+ if (!selectedMount) {
+ return I18n.tr("No disk data");
+ }
+ return selectedMount.mount;
+ }
+
+ secondaryText: {
+ if (!DgopService.dgopAvailable) {
+ return I18n.tr("dgop not available");
+ }
+ if (!selectedMount) {
+ return I18n.tr("No disk data available");
+ }
+ return `${selectedMount.used} / ${selectedMount.size} (${usagePercent.toFixed(0)}%)`;
+ }
+
+ iconColor: {
+ if (!DgopService.dgopAvailable || !selectedMount) {
+ return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5);
+ }
+ if (usagePercent > 90) {
+ return Theme.error;
+ }
+ if (usagePercent > 75) {
+ return Theme.warning;
+ }
+ return Theme.surfaceText;
+ }
+
+ Component.onCompleted: {
+ DgopService.addRef(["diskmounts"]);
+ }
+ Component.onDestruction: {
+ DgopService.removeRef(["diskmounts"]);
+ }
+
+ onToggled: {
+ expandClicked();
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/ErrorPill.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/ErrorPill.qml
new file mode 100644
index 0000000..cece467
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/ErrorPill.qml
@@ -0,0 +1,56 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+StyledRect {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property string primaryMessage: ""
+ property string secondaryMessage: ""
+
+ radius: Theme.cornerRadius
+ color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.1)
+ border.color: Theme.warning
+ border.width: 1
+
+ Row {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ name: "warning"
+ size: 16
+ color: Theme.warning
+ anchors.top: parent.top
+ anchors.topMargin: 2
+ }
+
+ Column {
+ width: parent.width - 16 - parent.spacing
+ spacing: Theme.spacingXS
+
+ StyledText {
+ width: parent.width
+ text: root.primaryMessage
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.warning
+ font.weight: Font.Medium
+ wrapMode: Text.WordWrap
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ StyledText {
+ width: parent.width
+ text: root.secondaryMessage
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.warning
+ visible: text.length > 0
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/InputAudioSliderRow.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/InputAudioSliderRow.qml
new file mode 100644
index 0000000..49ea30e
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/InputAudioSliderRow.qml
@@ -0,0 +1,87 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Row {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property var defaultSource: AudioService.source
+ property color sliderTrackColor: "transparent"
+
+ height: 40
+ spacing: 0
+
+ Rectangle {
+ width: Theme.iconSize + Theme.spacingS * 2
+ height: Theme.iconSize + Theme.spacingS * 2
+ anchors.verticalCenter: parent.verticalCenter
+ radius: (Theme.iconSize + Theme.spacingS * 2) / 2
+ color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.primary, 0)
+
+ DankRipple {
+ id: iconRipple
+ cornerRadius: parent.radius
+ }
+
+ MouseArea {
+ id: iconArea
+ anchors.fill: parent
+ visible: defaultSource !== null
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => iconRipple.trigger(mouse.x, mouse.y)
+ onClicked: {
+ if (defaultSource) {
+ SessionData.suppressOSDTemporarily();
+ defaultSource.audio.muted = !defaultSource.audio.muted;
+ }
+ }
+ }
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: {
+ if (!defaultSource)
+ return "mic_off";
+
+ let volume = defaultSource.audio.volume;
+ let muted = defaultSource.audio.muted;
+
+ if (muted || volume === 0.0)
+ return "mic_off";
+ return "mic";
+ }
+ size: Theme.iconSize
+ color: defaultSource && !defaultSource.audio.muted && defaultSource.audio.volume > 0 ? Theme.primary : Theme.surfaceText
+ }
+ }
+
+ DankSlider {
+ readonly property real actualVolumePercent: defaultSource ? Math.round(defaultSource.audio.volume * 100) : 0
+
+ anchors.verticalCenter: parent.verticalCenter
+ width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
+ enabled: defaultSource !== null
+ minimum: 0
+ maximum: 100
+ value: defaultSource ? Math.min(100, Math.round(defaultSource.audio.volume * 100)) : 0
+ showValue: true
+ unit: "%"
+ valueOverride: actualVolumePercent
+ thumbOutlineColor: Theme.surfaceContainer
+ trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ onSliderValueChanged: function (newValue) {
+ if (defaultSource) {
+ SessionData.suppressOSDTemporarily();
+ defaultSource.audio.volume = newValue / 100.0;
+ if (newValue > 0 && defaultSource.audio.muted) {
+ defaultSource.audio.muted = false;
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/SmallBatteryButton.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/SmallBatteryButton.qml
new file mode 100644
index 0000000..02da286
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/SmallBatteryButton.qml
@@ -0,0 +1,113 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property bool isActive: BatteryService.batteryAvailable && (BatteryService.isCharging || BatteryService.isPluggedIn)
+ property bool enabled: BatteryService.batteryAvailable
+
+ signal clicked
+
+ width: parent ? ((parent.width - parent.spacing * 3) / 4) : 48
+ height: 48
+ radius: {
+ if (Theme.cornerRadius === 0)
+ return 0;
+ return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4;
+ }
+
+ function hoverTint(base) {
+ const factor = 1.2;
+ return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
+ }
+
+ readonly property color _tileBgActive: Theme.ccTileActiveBg
+ readonly property color _tileBgInactive: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ readonly property color _tileRingActive: Theme.ccTileRing
+ readonly property color _tileIconActive: Theme.ccTileActiveText
+ readonly property color _tileIconInactive: Theme.ccTileInactiveIcon
+
+ color: {
+ if (isActive)
+ return _tileBgActive;
+ const baseColor = mouseArea.containsMouse ? Theme.primaryPressed : _tileBgInactive;
+ return baseColor;
+ }
+ border.color: isActive ? _tileRingActive : "transparent"
+ border.width: isActive ? 1 : 0
+ antialiasing: true
+ opacity: enabled ? 1.0 : 0.6
+
+ Rectangle {
+ anchors.fill: parent
+ radius: parent.radius
+ color: hoverTint(root.color)
+ opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0)
+ visible: opacity > 0
+ antialiasing: true
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ }
+ }
+ }
+
+ Row {
+ anchors.centerIn: parent
+ spacing: 4
+
+ DankIcon {
+ name: BatteryService.getBatteryIcon()
+ size: parent.parent.width * 0.25
+ color: {
+ if (BatteryService.isLowBattery && !BatteryService.isCharging) {
+ return Theme.error;
+ }
+ return isActive ? _tileIconActive : _tileIconInactive;
+ }
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: BatteryService.batteryAvailable ? `${BatteryService.batteryLevel}%` : ""
+ font.pixelSize: parent.parent.width * 0.15
+ font.weight: Font.Medium
+ color: {
+ if (BatteryService.isLowBattery && !BatteryService.isCharging) {
+ return Theme.error;
+ }
+ return isActive ? _tileIconActive : _tileIconInactive;
+ }
+ anchors.verticalCenter: parent.verticalCenter
+ visible: BatteryService.batteryAvailable
+ }
+ }
+
+ 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 radius {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/SmallDiskUsageButton.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/SmallDiskUsageButton.qml
new file mode 100644
index 0000000..30c4e36
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/SmallDiskUsageButton.qml
@@ -0,0 +1,129 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property string mountPath: "/"
+ property string instanceId: ""
+
+ property var selectedMount: {
+ if (!DgopService.diskMounts || DgopService.diskMounts.length === 0)
+ return null;
+ const targetMount = DgopService.diskMounts.find(mount => mount.mount === mountPath);
+ return targetMount || DgopService.diskMounts.find(mount => mount.mount === "/") || DgopService.diskMounts[0];
+ }
+
+ property real usagePercent: {
+ if (!selectedMount?.percent)
+ return 0;
+ return parseFloat(selectedMount.percent.replace("%", "")) || 0;
+ }
+
+ property bool enabled: DgopService.dgopAvailable
+
+ signal clicked
+
+ width: parent ? ((parent.width - parent.spacing * 3) / 4) : 48
+ height: 48
+ radius: Theme.cornerRadius + 4
+
+ function hoverTint(base) {
+ const factor = 1.2;
+ return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
+ }
+
+ readonly property color _tileBg: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+
+ color: mouseArea.containsMouse ? Theme.primaryPressed : _tileBg
+ border.color: "transparent"
+ border.width: 0
+ antialiasing: true
+ opacity: enabled ? 1.0 : 0.6
+
+ Rectangle {
+ anchors.fill: parent
+ radius: parent.radius
+ color: hoverTint(root.color)
+ opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0)
+ visible: opacity > 0
+ antialiasing: true
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ }
+ }
+ }
+
+ Row {
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ anchors.verticalCenter: parent.verticalCenter
+ name: "storage"
+ size: Theme.iconSizeSmall
+ color: {
+ if (root.usagePercent > 90)
+ return Theme.error;
+ if (root.usagePercent > 75)
+ return Theme.warning;
+ return Theme.ccTileInactiveIcon;
+ }
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 0
+
+ StyledText {
+ text: root.selectedMount?.mount || root.mountPath
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ elide: Text.ElideMiddle
+ width: Math.min(implicitWidth, root.width - Theme.iconSizeSmall - Theme.spacingM)
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ StyledText {
+ text: `${root.usagePercent.toFixed(0)}%`
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Bold
+ color: {
+ if (root.usagePercent > 90)
+ return Theme.error;
+ if (root.usagePercent > 75)
+ return Theme.warning;
+ return Theme.ccTileInactiveIcon;
+ }
+ }
+ }
+ }
+
+ 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()
+ }
+
+ Component.onCompleted: {
+ DgopService.addRef(["diskmounts"]);
+ }
+ Component.onDestruction: {
+ DgopService.removeRef(["diskmounts"]);
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/SmallToggleButton.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/SmallToggleButton.qml
new file mode 100644
index 0000000..7d86947
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/SmallToggleButton.qml
@@ -0,0 +1,90 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ property string iconName: ""
+ property bool isActive: false
+ property bool enabled: true
+ property real iconRotation: 0
+
+ signal clicked
+ signal iconRotationCompleted
+
+ width: parent ? ((parent.width - parent.spacing * 3) / 4) : 48
+ height: 48
+ radius: {
+ if (Theme.cornerRadius === 0)
+ return 0;
+ return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4;
+ }
+
+ function hoverTint(base) {
+ const factor = 1.2;
+ return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
+ }
+
+ readonly property color _tileBgActive: Theme.ccTileActiveBg
+ readonly property color _tileBgInactive: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ readonly property color _tileRingActive: Theme.ccTileRing
+ readonly property color _tileIconActive: Theme.ccTileActiveText
+ readonly property color _tileIconInactive: Theme.ccTileInactiveIcon
+
+ color: {
+ if (isActive)
+ return _tileBgActive;
+ const baseColor = mouseArea.containsMouse ? Theme.primaryPressed : _tileBgInactive;
+ return baseColor;
+ }
+ border.color: isActive ? _tileRingActive : "transparent"
+ border.width: isActive ? 1 : 0
+ antialiasing: true
+ opacity: enabled ? 1.0 : 0.6
+
+ Rectangle {
+ anchors.fill: parent
+ radius: parent.radius
+ color: hoverTint(root.color)
+ opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0)
+ visible: opacity > 0
+ antialiasing: true
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ }
+ }
+ }
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: iconName
+ size: Theme.iconSize
+ color: isActive ? _tileIconActive : _tileIconInactive
+ rotation: iconRotation
+ onRotationCompleted: root.iconRotationCompleted()
+ }
+
+ 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 radius {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/ToggleButton.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/ToggleButton.qml
new file mode 100644
index 0000000..b8adda8
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/ToggleButton.qml
@@ -0,0 +1,134 @@
+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 bool isActive: false
+ property bool enabled: true
+ property string secondaryText: ""
+ property real iconRotation: 0
+
+ signal clicked
+ signal iconRotationCompleted
+
+ 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: {
+ if (isActive)
+ return _tileBgActive;
+ const baseColor = mouseArea.containsMouse ? Theme.primaryPressed : _tileBgInactive;
+ return baseColor;
+ }
+ border.color: isActive ? _tileRingActive : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
+ border.width: 0
+ opacity: enabled ? 1.0 : 0.6
+
+ function hoverTint(base) {
+ const factor = 1.2;
+ return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
+ }
+
+ readonly property color _containerBg: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+
+ Rectangle {
+ anchors.fill: parent
+ radius: Theme.cornerRadius
+ color: mouseArea.containsMouse ? hoverTint(_containerBg) : Theme.withAlpha(_containerBg, 0)
+ 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
+ rotation: root.iconRotation
+ onRotationCompleted: root.iconRotationCompleted()
+ }
+
+ 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
+
+ StyledText {
+ width: parent.width
+ text: root.text
+ font.pixelSize: Theme.fontSizeMedium
+ color: isActive ? Theme.ccTileActiveText : Theme.surfaceText
+ font.weight: Font.Medium
+ elide: Text.ElideRight
+ wrapMode: Text.NoWrap
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ StyledText {
+ width: parent.width
+ text: root.secondaryText
+ font.pixelSize: Theme.fontSizeSmall
+ 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 radius {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/utils/layout.js b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/utils/layout.js
new file mode 100644
index 0000000..b8e5651
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/utils/layout.js
@@ -0,0 +1,45 @@
+function calculateRowsAndWidgets(controlCenterColumn, expandedSection, expandedWidgetIndex) {
+ var rows = []
+ var currentRow = []
+ var currentWidth = 0
+ var expandedRow = -1
+
+ const widgets = SettingsData.controlCenterWidgets || []
+ const baseWidth = controlCenterColumn.width
+ const spacing = Theme.spacingS
+
+ for (var i = 0; i < widgets.length; i++) {
+ const widget = widgets[i]
+ const widgetWidth = widget.width || 50
+
+ var itemWidth
+ if (widgetWidth <= 25) {
+ itemWidth = (baseWidth - spacing * 3) / 4
+ } else if (widgetWidth <= 50) {
+ itemWidth = (baseWidth - spacing) / 2
+ } else if (widgetWidth <= 75) {
+ itemWidth = (baseWidth - spacing * 2) * 0.75
+ } else {
+ itemWidth = baseWidth
+ }
+
+ if (currentRow.length > 0 && (currentWidth + spacing + itemWidth > baseWidth)) {
+ rows.push([...currentRow])
+ currentRow = [widget]
+ currentWidth = itemWidth
+ } else {
+ currentRow.push(widget)
+ currentWidth += (currentRow.length > 1 ? spacing : 0) + itemWidth
+ }
+
+ if (expandedWidgetIndex === i) {
+ expandedRow = rows.length
+ }
+ }
+
+ if (currentRow.length > 0) {
+ rows.push(currentRow)
+ }
+
+ return { rows: rows, expandedRowIndex: expandedRow }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/utils/state.js b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/utils/state.js
new file mode 100644
index 0000000..a2d59a6
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/utils/state.js
@@ -0,0 +1,17 @@
+function openWithSection(root, section) {
+ if (root.shouldBeVisible) {
+ root.close();
+ } else {
+ root.expandedSection = section;
+ root.open();
+ }
+}
+
+function toggleSection(root, section) {
+ if (root.expandedSection === section) {
+ root.expandedSection = "";
+ root.expandedWidgetIndex = -1;
+ } else {
+ root.expandedSection = section;
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/utils/widgets.js b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/utils/widgets.js
new file mode 100644
index 0000000..3e11d88
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/utils/widgets.js
@@ -0,0 +1,90 @@
+function getWidgetForId(baseWidgetDefinitions, widgetId) {
+ return baseWidgetDefinitions.find(w => w.id === widgetId)
+}
+
+function addWidget(widgetId) {
+ var widgets = SettingsData.controlCenterWidgets.slice()
+ var widget = {
+ "id": widgetId,
+ "enabled": true,
+ "width": 50
+ }
+
+ if (widgetId === "diskUsage") {
+ widget.instanceId = generateUniqueId()
+ widget.mountPath = "/"
+ }
+
+ if (widgetId === "brightnessSlider") {
+ widget.instanceId = generateUniqueId()
+ widget.deviceName = ""
+ }
+
+ widgets.push(widget)
+ SettingsData.set("controlCenterWidgets", widgets)
+}
+
+function generateUniqueId() {
+ return Date.now().toString(36) + Math.random().toString(36).substr(2)
+}
+
+function removeWidget(index) {
+ var widgets = SettingsData.controlCenterWidgets.slice()
+ if (index >= 0 && index < widgets.length) {
+ widgets.splice(index, 1)
+ SettingsData.set("controlCenterWidgets", widgets)
+ }
+}
+
+function toggleWidgetSize(index) {
+ var widgets = SettingsData.controlCenterWidgets.slice()
+ if (index >= 0 && index < widgets.length) {
+ const currentWidth = widgets[index].width || 50
+ const id = widgets[index].id || ""
+
+ if (id === "wifi" || id === "bluetooth" || id === "audioOutput" || id === "audioInput") {
+ widgets[index].width = currentWidth <= 50 ? 100 : 50
+ } else {
+ if (currentWidth <= 25) {
+ widgets[index].width = 50
+ } else if (currentWidth <= 50) {
+ widgets[index].width = 100
+ } else {
+ widgets[index].width = 25
+ }
+ }
+
+ SettingsData.set("controlCenterWidgets", widgets)
+ }
+}
+
+function reorderWidgets(newOrder) {
+ SettingsData.set("controlCenterWidgets", newOrder)
+}
+
+function moveWidget(fromIndex, toIndex) {
+ let widgets = [...(SettingsData.controlCenterWidgets || [])]
+ if (fromIndex >= 0 && fromIndex < widgets.length && toIndex >= 0 && toIndex < widgets.length) {
+ const movedWidget = widgets.splice(fromIndex, 1)[0]
+ widgets.splice(toIndex, 0, movedWidget)
+ SettingsData.set("controlCenterWidgets", widgets)
+ }
+}
+
+function resetToDefault() {
+ const defaultWidgets = [
+ {"id": "volumeSlider", "enabled": true, "width": 50},
+ {"id": "brightnessSlider", "enabled": true, "width": 50},
+ {"id": "wifi", "enabled": true, "width": 50},
+ {"id": "bluetooth", "enabled": true, "width": 50},
+ {"id": "audioOutput", "enabled": true, "width": 50},
+ {"id": "audioInput", "enabled": true, "width": 50},
+ {"id": "nightMode", "enabled": true, "width": 50},
+ {"id": "darkMode", "enabled": true, "width": 50}
+ ]
+ SettingsData.set("controlCenterWidgets", defaultWidgets)
+}
+
+function clearAll() {
+ SettingsData.set("controlCenterWidgets", [])
+}