diff options
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter')
39 files changed, 8603 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/BuiltinPlugins/CupsWidget.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/BuiltinPlugins/CupsWidget.qml new file mode 100644 index 0000000..a275397 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/BuiltinPlugins/VpnWidget.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/BuiltinPlugins/VpnWidget.qml new file mode 100644 index 0000000..531045e --- /dev/null +++ b/raveos-hyprland-theme/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-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 + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/ControlCenterPopout.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/ControlCenterPopout.qml new file mode 100644 index 0000000..d8839aa --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/AudioInputDetail.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/AudioInputDetail.qml new file mode 100644 index 0000000..8fc2d46 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/AudioOutputDetail.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/AudioOutputDetail.qml new file mode 100644 index 0000000..7e86ae4 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/BatteryDetail.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/BatteryDetail.qml new file mode 100644 index 0000000..1f0206a --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/BluetoothCodecSelector.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/BluetoothCodecSelector.qml new file mode 100644 index 0000000..0cad08c --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/BluetoothDetail.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/BluetoothDetail.qml new file mode 100644 index 0000000..580ea9f --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/BrightnessDetail.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/BrightnessDetail.qml new file mode 100644 index 0000000..0d42e62 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/DiskUsageDetail.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/DiskUsageDetail.qml new file mode 100644 index 0000000..90fbf9d --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/NetworkDetail.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Details/NetworkDetail.qml new file mode 100644 index 0000000..5f1738a --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Models/WidgetModel.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Models/WidgetModel.qml new file mode 100644 index 0000000..ac616ae --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/AudioSliderRow.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/AudioSliderRow.qml new file mode 100644 index 0000000..81d346d --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/BatteryPill.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/BatteryPill.qml new file mode 100644 index 0000000..c14c308 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/BrightnessSliderRow.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/BrightnessSliderRow.qml new file mode 100644 index 0000000..037c08f --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/ColorPickerPill.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/ColorPickerPill.qml new file mode 100644 index 0000000..a95410b --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/CompactSlider.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/CompactSlider.qml new file mode 100644 index 0000000..eeed4c4 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/CompoundPill.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/CompoundPill.qml new file mode 100644 index 0000000..9165ec4 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/DetailView.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/DetailView.qml new file mode 100644 index 0000000..2d946f3 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/DiskUsagePill.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/DiskUsagePill.qml new file mode 100644 index 0000000..827df4e --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/ErrorPill.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/ErrorPill.qml new file mode 100644 index 0000000..cece467 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/InputAudioSliderRow.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/InputAudioSliderRow.qml new file mode 100644 index 0000000..49ea30e --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/SmallBatteryButton.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/SmallBatteryButton.qml new file mode 100644 index 0000000..02da286 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/SmallDiskUsageButton.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/SmallDiskUsageButton.qml new file mode 100644 index 0000000..30c4e36 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/SmallToggleButton.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/SmallToggleButton.qml new file mode 100644 index 0000000..7d86947 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/ToggleButton.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/ToggleButton.qml new file mode 100644 index 0000000..b8adda8 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/utils/layout.js b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/utils/layout.js new file mode 100644 index 0000000..b8e5651 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/utils/state.js b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/utils/state.js new file mode 100644 index 0000000..a2d59a6 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/utils/widgets.js b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/ControlCenter/utils/widgets.js new file mode 100644 index 0000000..3e11d88 --- /dev/null +++ b/raveos-hyprland-theme/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", []) +} |