From 70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 Mon Sep 17 00:00:00 2001 From: AlexanderCurl Date: Sat, 18 Apr 2026 17:46:06 +0100 Subject: Replaced file structures for themes in the correct format --- .../ControlCenter/Components/ActionTile.qml | 132 +++ .../ControlCenter/Components/DetailHost.qml | 250 +++++ .../Components/DragDropDetailHost.qml | 91 ++ .../ControlCenter/Components/DragDropGrid.qml | 1091 ++++++++++++++++++++ .../Components/DragDropWidgetWrapper.qml | 289 ++++++ .../ControlCenter/Components/EditControls.qml | 245 +++++ .../ControlCenter/Components/HeaderPane.qml | 118 +++ .../ControlCenter/Components/PowerButton.qml | 55 + .../ControlCenter/Components/SizeControls.qml | 52 + .../ControlCenter/Components/Typography.qml | 46 + 10 files changed, 2369 insertions(+) create mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/ActionTile.qml create mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DetailHost.qml create mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DragDropDetailHost.qml create mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml create mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DragDropWidgetWrapper.qml create mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/EditControls.qml create mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/HeaderPane.qml create mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/PowerButton.qml create mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/SizeControls.qml create mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/Typography.qml (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components') diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/ActionTile.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/ActionTile.qml new file mode 100644 index 0000000..d85c779 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DetailHost.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DetailHost.qml new file mode 100644 index 0000000..a3b1156 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DragDropDetailHost.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DragDropDetailHost.qml new file mode 100644 index 0000000..4a7ff19 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml new file mode 100644 index 0000000..4cdd979 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DragDropWidgetWrapper.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/DragDropWidgetWrapper.qml new file mode 100644 index 0000000..7cb0e2c --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/EditControls.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/EditControls.qml new file mode 100644 index 0000000..50531aa --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/HeaderPane.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/HeaderPane.qml new file mode 100644 index 0000000..d36c5b6 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/PowerButton.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/PowerButton.qml new file mode 100644 index 0000000..8ed7444 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/SizeControls.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/SizeControls.qml new file mode 100644 index 0000000..aeae4ee --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/Typography.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Components/Typography.qml new file mode 100644 index 0000000..f3e554f --- /dev/null +++ b/raveos-hyprland-theme/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 + } + } +} -- cgit v1.3