diff options
| author | Nippy <nippy@rp1.hu> | 2026-05-08 19:50:49 +0200 |
|---|---|---|
| committer | Nippy <nippy@rp1.hu> | 2026-05-08 19:50:49 +0200 |
| commit | 3462bcc46ecf74f576ea4397f16492c16bd75b10 (patch) | |
| tree | c5ab7aef4498647e057bda8d49c11e5f9dda74c9 /raveos-hyprland-theme/theme-data/dms/Modules/ControlCenter/Details | |
| parent | 56616f693b4f263cbf0d47da43b67424efa6022d (diff) | |
| download | RaveOS-PKGBUILD-3462bcc46ecf74f576ea4397f16492c16bd75b10.tar.gz RaveOS-PKGBUILD-3462bcc46ecf74f576ea4397f16492c16bd75b10.zip | |
raveos update
Diffstat (limited to 'raveos-hyprland-theme/theme-data/dms/Modules/ControlCenter/Details')
8 files changed, 3817 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/ControlCenter/Details/AudioInputDetail.qml b/raveos-hyprland-theme/theme-data/dms/Modules/ControlCenter/Details/AudioInputDetail.qml new file mode 100644 index 0000000..8fc2d46 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modules/ControlCenter/Details/AudioOutputDetail.qml b/raveos-hyprland-theme/theme-data/dms/Modules/ControlCenter/Details/AudioOutputDetail.qml new file mode 100644 index 0000000..7e86ae4 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modules/ControlCenter/Details/BatteryDetail.qml b/raveos-hyprland-theme/theme-data/dms/Modules/ControlCenter/Details/BatteryDetail.qml new file mode 100644 index 0000000..1f0206a --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modules/ControlCenter/Details/BluetoothCodecSelector.qml b/raveos-hyprland-theme/theme-data/dms/Modules/ControlCenter/Details/BluetoothCodecSelector.qml new file mode 100644 index 0000000..0cad08c --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modules/ControlCenter/Details/BluetoothDetail.qml b/raveos-hyprland-theme/theme-data/dms/Modules/ControlCenter/Details/BluetoothDetail.qml new file mode 100644 index 0000000..580ea9f --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modules/ControlCenter/Details/BrightnessDetail.qml b/raveos-hyprland-theme/theme-data/dms/Modules/ControlCenter/Details/BrightnessDetail.qml new file mode 100644 index 0000000..0d42e62 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modules/ControlCenter/Details/DiskUsageDetail.qml b/raveos-hyprland-theme/theme-data/dms/Modules/ControlCenter/Details/DiskUsageDetail.qml new file mode 100644 index 0000000..90fbf9d --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modules/ControlCenter/Details/NetworkDetail.qml b/raveos-hyprland-theme/theme-data/dms/Modules/ControlCenter/Details/NetworkDetail.qml new file mode 100644 index 0000000..5f1738a --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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 {} + } +} |