diff --git a/quickshell/Modules/DankDash/DankDashPopout.qml b/quickshell/Modules/DankDash/DankDashPopout.qml index 97073a94..43ca08f8 100644 --- a/quickshell/Modules/DankDash/DankDashPopout.qml +++ b/quickshell/Modules/DankDash/DankDashPopout.qml @@ -1,11 +1,7 @@ import QtQuick -import QtQuick.Controls -import QtQuick.Effects import QtQuick.Layouts -import Quickshell -import Quickshell.Services.Mpris -import Quickshell.Wayland import qs.Common +import qs.Services import qs.Widgets import qs.Modules.DankDash @@ -27,42 +23,130 @@ DankPopout { property bool __focusArmed: false property bool __contentReady: false + property var __mediaTabRef: null + + property int __dropdownType: 0 + property point __dropdownAnchor: Qt.point(0, 0) + property bool __dropdownRightEdge: false + property var __dropdownPlayer: null + property var __dropdownPlayers: [] + + function __showVolumeDropdown(pos, rightEdge, player, players) { + __dropdownAnchor = pos; + __dropdownRightEdge = rightEdge; + __dropdownPlayer = player; + __dropdownPlayers = players; + __dropdownType = 1; + } + + function __showAudioDevicesDropdown(pos, rightEdge) { + __dropdownAnchor = pos; + __dropdownRightEdge = rightEdge; + __dropdownType = 2; + } + + function __showPlayersDropdown(pos, rightEdge, player, players) { + __dropdownAnchor = pos; + __dropdownRightEdge = rightEdge; + __dropdownPlayer = player; + __dropdownPlayers = players; + __dropdownType = 3; + } + + function __hideDropdowns() { + __volumeCloseTimer.stop(); + __dropdownType = 0; + __mediaTabRef?.resetDropdownStates(); + } + + function __startCloseTimer() { + __volumeCloseTimer.restart(); + } + + function __stopCloseTimer() { + __volumeCloseTimer.stop(); + } + + Timer { + id: __volumeCloseTimer + interval: 400 + onTriggered: { + if (__dropdownType === 1) { + __hideDropdowns(); + } + } + } + + overlayContent: Component { + MediaDropdownOverlay { + dropdownType: root.__dropdownType + anchorPos: root.__dropdownAnchor + isRightEdge: root.__dropdownRightEdge + activePlayer: root.__dropdownPlayer + allPlayers: root.__dropdownPlayers + onCloseRequested: root.__hideDropdowns() + onPanelEntered: root.__stopCloseTimer() + onPanelExited: root.__startCloseTimer() + onVolumeChanged: volume => { + const player = root.__dropdownPlayer; + const isChrome = player?.identity?.toLowerCase().includes("chrome") || player?.identity?.toLowerCase().includes("chromium"); + const usePlayerVolume = player && player.volumeSupported && !isChrome; + if (usePlayerVolume) { + player.volume = volume; + } else if (AudioService.sink?.audio) { + AudioService.sink.audio.volume = volume; + } + } + onPlayerSelected: player => { + const currentPlayer = MprisController.activePlayer; + if (currentPlayer && currentPlayer !== player && currentPlayer.canPause) { + currentPlayer.pause(); + } + MprisController.activePlayer = player; + root.__hideDropdowns(); + } + onDeviceSelected: device => { + root.__hideDropdowns(); + } + } + } + function __tryFocusOnce() { if (!__focusArmed) - return - const win = root.window + return; + const win = root.window; if (!win || !win.visible) - return + return; if (!contentLoader.item) - return - + return; if (win.requestActivate) - win.requestActivate() - contentLoader.item.forceActiveFocus(Qt.TabFocusReason) + win.requestActivate(); + contentLoader.item.forceActiveFocus(Qt.TabFocusReason); if (contentLoader.item.activeFocus) - __focusArmed = false + __focusArmed = false; } onDashVisibleChanged: { if (dashVisible) { - __focusArmed = true - __contentReady = !!contentLoader.item - open() - __tryFocusOnce() + __focusArmed = true; + __contentReady = !!contentLoader.item; + open(); + __tryFocusOnce(); } else { - __focusArmed = false - __contentReady = false - close() + __focusArmed = false; + __contentReady = false; + __hideDropdowns(); + close(); } } Connections { target: contentLoader function onLoaded() { - __contentReady = true + __contentReady = true; if (__focusArmed) - __tryFocusOnce() + __tryFocusOnce(); } } @@ -71,12 +155,12 @@ DankPopout { enabled: !!root.window function onVisibleChanged() { if (__focusArmed) - __tryFocusOnce() + __tryFocusOnce(); } } onBackgroundClicked: { - dashVisible = false + dashVisible = false; } content: Component { @@ -90,7 +174,7 @@ DankPopout { Component.onCompleted: { if (root.shouldBeVisible) { - mainContainer.forceActiveFocus() + mainContainer.forceActiveFocus(); } } @@ -99,54 +183,54 @@ DankPopout { function onShouldBeVisibleChanged() { if (root.shouldBeVisible) { Qt.callLater(function () { - mainContainer.forceActiveFocus() - }) + mainContainer.forceActiveFocus(); + }); } } } Keys.onPressed: function (event) { if (event.key === Qt.Key_Escape) { - root.dashVisible = false - event.accepted = true - return + root.dashVisible = false; + event.accepted = true; + return; } if (event.key === Qt.Key_Tab && !(event.modifiers & Qt.ShiftModifier)) { - let nextIndex = root.currentTabIndex + 1 + let nextIndex = root.currentTabIndex + 1; while (nextIndex < tabBar.model.length && tabBar.model[nextIndex] && tabBar.model[nextIndex].isAction) { - nextIndex++ + nextIndex++; } if (nextIndex >= tabBar.model.length) { - nextIndex = 0 + nextIndex = 0; } - root.currentTabIndex = nextIndex - event.accepted = true - return + root.currentTabIndex = nextIndex; + event.accepted = true; + return; } if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))) { - let prevIndex = root.currentTabIndex - 1 + let prevIndex = root.currentTabIndex - 1; while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) { - prevIndex-- + prevIndex--; } if (prevIndex < 0) { - prevIndex = tabBar.model.length - 1 + prevIndex = tabBar.model.length - 1; while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) { - prevIndex-- + prevIndex--; } } if (prevIndex >= 0) { - root.currentTabIndex = prevIndex + root.currentTabIndex = prevIndex; } - event.accepted = true - return + event.accepted = true; + return; } if (root.currentTabIndex === 2 && wallpaperTab.handleKeyEvent) { if (wallpaperTab.handleKeyEvent(event)) { - event.accepted = true - return + event.accepted = true; + return; } } } @@ -171,50 +255,54 @@ DankPopout { focus: false activeFocusOnTab: false nextFocusTarget: { - const item = pages.currentItem + const item = pages.currentItem; if (!item) - return null + return null; if (item.focusTarget) - return item.focusTarget - return item + return item.focusTarget; + return item; } model: { - let tabs = [{ - "icon": "dashboard", - "text": I18n.tr("Overview") - }, { - "icon": "music_note", - "text": I18n.tr("Media") - }, { - "icon": "wallpaper", - "text": I18n.tr("Wallpapers") - }] + let tabs = [ + { + "icon": "dashboard", + "text": I18n.tr("Overview") + }, + { + "icon": "music_note", + "text": I18n.tr("Media") + }, + { + "icon": "wallpaper", + "text": I18n.tr("Wallpapers") + } + ]; if (SettingsData.weatherEnabled) { tabs.push({ - "icon": "wb_sunny", - "text": I18n.tr("Weather") - }) + "icon": "wb_sunny", + "text": I18n.tr("Weather") + }); } tabs.push({ - "icon": "settings", - "text": I18n.tr("Settings"), - "isAction": true - }) - return tabs + "icon": "settings", + "text": I18n.tr("Settings"), + "isAction": true + }); + return tabs; } onTabClicked: function (index) { - root.currentTabIndex = index + root.currentTabIndex = index; } onActionTriggered: function (index) { - let settingsIndex = SettingsData.weatherEnabled ? 4 : 3 + let settingsIndex = SettingsData.weatherEnabled ? 4 : 3; if (index === settingsIndex) { - dashVisible = false - settingsModal.show() + dashVisible = false; + settingsModal.show(); } } } @@ -229,14 +317,14 @@ DankPopout { width: parent.width implicitHeight: { if (currentIndex === 0) - return overviewTab.implicitHeight + return overviewTab.implicitHeight; if (currentIndex === 1) - return mediaTab.implicitHeight + return mediaTab.implicitHeight; if (currentIndex === 2) - return wallpaperTab.implicitHeight + return wallpaperTab.implicitHeight; if (SettingsData.weatherEnabled && currentIndex === 3) - return weatherTab.implicitHeight - return overviewTab.implicitHeight + return weatherTab.implicitHeight; + return overviewTab.implicitHeight; } currentIndex: root.currentTabIndex @@ -244,24 +332,42 @@ DankPopout { id: overviewTab onCloseDash: { - root.dashVisible = false + root.dashVisible = false; } onSwitchToWeatherTab: { if (SettingsData.weatherEnabled) { - tabBar.currentIndex = 3 - tabBar.tabClicked(3) + tabBar.currentIndex = 3; + tabBar.tabClicked(3); } } onSwitchToMediaTab: { - tabBar.currentIndex = 1 - tabBar.tabClicked(1) + tabBar.currentIndex = 1; + tabBar.tabClicked(1); } } MediaPlayerTab { id: mediaTab + targetScreen: root.screen + popoutX: root.alignedX + popoutY: root.alignedY + popoutWidth: root.alignedWidth + popoutHeight: root.alignedHeight + contentOffsetY: Theme.spacingM + 48 + Theme.spacingS + Theme.spacingXS + Component.onCompleted: root.__mediaTabRef = this + onShowVolumeDropdown: (pos, screen, rightEdge, player, players) => { + root.__showVolumeDropdown(pos, rightEdge, player, players); + } + onShowAudioDevicesDropdown: (pos, screen, rightEdge) => { + root.__showAudioDevicesDropdown(pos, rightEdge); + } + onShowPlayersDropdown: (pos, screen, rightEdge, player, players) => { + root.__showPlayersDropdown(pos, rightEdge, player, players); + } + onHideDropdowns: root.__hideDropdowns() + onVolumeButtonExited: root.__startCloseTimer() } WallpaperTab { diff --git a/quickshell/Modules/DankDash/MediaDropdownOverlay.qml b/quickshell/Modules/DankDash/MediaDropdownOverlay.qml new file mode 100644 index 00000000..1339b15e --- /dev/null +++ b/quickshell/Modules/DankDash/MediaDropdownOverlay.qml @@ -0,0 +1,491 @@ +import QtQuick +import QtQuick.Effects +import Quickshell.Services.Pipewire +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + property int dropdownType: 0 + property var activePlayer: null + property var allPlayers: [] + property point anchorPos: Qt.point(0, 0) + property bool isRightEdge: false + + property bool __isChromeBrowser: { + if (!activePlayer?.identity) + return false; + const id = activePlayer.identity.toLowerCase(); + return id.includes("chrome") || id.includes("chromium"); + } + property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser + property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0) + property bool volumeAvailable: (activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio) + property var availableDevices: Pipewire.nodes.values.filter(node => node.audio && node.isSink && !node.isStream) + + signal closeRequested + signal deviceSelected(var device) + signal playerSelected(var player) + signal volumeChanged(real volume) + signal panelEntered + signal panelExited + + property int __volumeHoverCount: 0 + + function volumeAreaEntered() { + __volumeHoverCount++; + panelEntered(); + } + + function volumeAreaExited() { + __volumeHoverCount--; + Qt.callLater(() => { + if (__volumeHoverCount <= 0) + panelExited(); + }); + } + + Rectangle { + id: volumePanel + visible: dropdownType === 1 && volumeAvailable + width: 60 + height: 180 + x: isRightEdge ? anchorPos.x : anchorPos.x - width + y: anchorPos.y - height / 2 + radius: Theme.cornerRadius * 2 + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) + border.width: 1 + + opacity: dropdownType === 1 ? 1 : 0 + scale: dropdownType === 1 ? 1 : 0.96 + transformOrigin: isRightEdge ? Item.Left : Item.Right + + Behavior on opacity { + NumberAnimation { + duration: Theme.expressiveDurations.expressiveDefaultSpatial + easing.type: Easing.BezierSpline + easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.expressiveDurations.expressiveDefaultSpatial + easing.type: Easing.BezierSpline + easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial + } + } + + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true + shadowHorizontalOffset: 0 + shadowVerticalOffset: 8 + shadowBlur: 1.0 + shadowColor: Qt.rgba(0, 0, 0, 0.4) + shadowOpacity: 0.7 + } + + MouseArea { + anchors.fill: parent + anchors.margins: -12 + hoverEnabled: true + onEntered: volumeAreaEntered() + onExited: volumeAreaExited() + } + + Item { + anchors.fill: parent + anchors.margins: Theme.spacingS + + Item { + id: volumeSlider + width: parent.width * 0.5 + height: parent.height - Theme.spacingXL * 2 + anchors.top: parent.top + anchors.topMargin: Theme.spacingS + anchors.horizontalCenter: parent.horizontalCenter + + Rectangle { + width: parent.width + height: parent.height + anchors.centerIn: parent + color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + radius: Theme.cornerRadius + } + + Rectangle { + width: parent.width + height: volumeAvailable ? (Math.min(1.0, currentVolume) * parent.height) : 0 + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + color: Theme.primary + bottomLeftRadius: Theme.cornerRadius + bottomRightRadius: Theme.cornerRadius + } + + Rectangle { + width: parent.width + 8 + height: 8 + radius: Theme.cornerRadius + y: { + const ratio = volumeAvailable ? Math.min(1.0, currentVolume) : 0; + const travel = parent.height - height; + return Math.max(0, Math.min(travel, travel * (1 - ratio))); + } + anchors.horizontalCenter: parent.horizontalCenter + color: Theme.primary + border.width: 3 + border.color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1.0) + } + + MouseArea { + anchors.fill: parent + anchors.margins: -12 + enabled: volumeAvailable + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + preventStealing: true + + onEntered: volumeAreaEntered() + onExited: volumeAreaExited() + onPressed: mouse => updateVolume(mouse) + onPositionChanged: mouse => { + if (pressed) + updateVolume(mouse); + } + onClicked: mouse => updateVolume(mouse) + + function updateVolume(mouse) { + if (!volumeAvailable) + return; + const ratio = 1.0 - (mouse.y / height); + const volume = Math.max(0, Math.min(1, ratio)); + root.volumeChanged(volume); + } + } + } + + StyledText { + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottomMargin: Theme.spacingL + text: volumeAvailable ? Math.round(currentVolume * 100) + "%" : "0%" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + } + } + + Rectangle { + id: audioDevicesPanel + visible: dropdownType === 2 + width: 280 + height: Math.max(200, Math.min(280, availableDevices.length * 50 + 100)) + x: isRightEdge ? anchorPos.x : anchorPos.x - width + y: anchorPos.y - height / 2 + radius: Theme.cornerRadius * 2 + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6) + border.width: 2 + + opacity: dropdownType === 2 ? 1 : 0 + scale: dropdownType === 2 ? 1 : 0.96 + transformOrigin: isRightEdge ? Item.Left : Item.Right + + Behavior on opacity { + NumberAnimation { + duration: Theme.expressiveDurations.expressiveDefaultSpatial + easing.type: Easing.BezierSpline + easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.expressiveDurations.expressiveDefaultSpatial + easing.type: Easing.BezierSpline + easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial + } + } + + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true + shadowHorizontalOffset: 0 + shadowVerticalOffset: 8 + shadowBlur: 1.0 + shadowColor: Qt.rgba(0, 0, 0, 0.4) + shadowOpacity: 0.7 + } + + Column { + anchors.fill: parent + anchors.margins: Theme.spacingM + + StyledText { + text: I18n.tr("Audio Output Devices (") + availableDevices.length + ")" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignHCenter + bottomPadding: Theme.spacingM + } + + DankFlickable { + width: parent.width + height: parent.height - 40 + contentHeight: deviceColumn.height + clip: true + + Column { + id: deviceColumn + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: availableDevices + delegate: Rectangle { + required property var modelData + required property int index + + width: parent.width + height: 48 + radius: Theme.cornerRadius + color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: modelData === AudioService.sink ? 2 : 1 + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + width: parent.width - Theme.spacingM * 2 + + DankIcon { + name: getAudioDeviceIcon(modelData) + size: 20 + color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + + function getAudioDeviceIcon(device) { + if (!device?.name) + return "speaker"; + const name = device.name.toLowerCase(); + if (name.includes("bluez") || name.includes("bluetooth")) + return "headset"; + if (name.includes("hdmi")) + return "tv"; + if (name.includes("usb")) + return "headset"; + return "speaker"; + } + } + + Column { + anchors.verticalCenter: parent.verticalCenter + width: parent.width - 20 - 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 + wrapMode: Text.NoWrap + width: parent.width + } + + StyledText { + text: modelData === AudioService.sink ? "Active" : "Available" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + elide: Text.ElideRight + width: parent.width + } + } + } + + MouseArea { + id: deviceMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (modelData) { + Pipewire.preferredDefaultAudioSink = modelData; + root.deviceSelected(modelData); + } + } + } + } + } + } + } + } + } + + Rectangle { + id: playersPanel + visible: dropdownType === 3 + width: 240 + height: Math.max(180, Math.min(240, (allPlayers?.length || 0) * 50 + 80)) + x: isRightEdge ? anchorPos.x : anchorPos.x - width + y: anchorPos.y - height / 2 + radius: Theme.cornerRadius * 2 + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6) + border.width: 2 + + opacity: dropdownType === 3 ? 1 : 0 + scale: dropdownType === 3 ? 1 : 0.96 + transformOrigin: isRightEdge ? Item.Left : Item.Right + + Behavior on opacity { + NumberAnimation { + duration: Theme.expressiveDurations.expressiveDefaultSpatial + easing.type: Easing.BezierSpline + easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.expressiveDurations.expressiveDefaultSpatial + easing.type: Easing.BezierSpline + easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial + } + } + + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true + shadowHorizontalOffset: 0 + shadowVerticalOffset: 8 + shadowBlur: 1.0 + shadowColor: Qt.rgba(0, 0, 0, 0.4) + shadowOpacity: 0.7 + } + + Column { + anchors.fill: parent + anchors.margins: Theme.spacingM + + StyledText { + text: I18n.tr("Media Players (") + (allPlayers?.length || 0) + ")" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignHCenter + bottomPadding: Theme.spacingM + } + + DankFlickable { + width: parent.width + height: parent.height - 40 + contentHeight: playerColumn.height + clip: true + + Column { + id: playerColumn + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: allPlayers || [] + delegate: Rectangle { + required property var modelData + required property int index + + width: parent.width + height: 48 + radius: Theme.cornerRadius + color: playerMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + border.color: modelData === activePlayer ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: modelData === activePlayer ? 2 : 1 + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + width: parent.width - Theme.spacingM * 2 + + DankIcon { + name: "music_note" + size: 20 + color: modelData === activePlayer ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + width: parent.width - 20 - Theme.spacingM * 2 + + StyledText { + text: { + if (!modelData) + return "Unknown Player"; + const identity = modelData.identity || "Unknown Player"; + const trackTitle = modelData.trackTitle || ""; + return trackTitle.length > 0 ? identity + " - " + trackTitle : identity; + } + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: modelData === activePlayer ? Font.Medium : Font.Normal + elide: Text.ElideRight + wrapMode: Text.NoWrap + width: parent.width + } + + StyledText { + text: { + if (!modelData) + return ""; + const artist = modelData.trackArtist || ""; + const isActive = modelData === activePlayer; + if (artist.length > 0) + return artist + (isActive ? " (Active)" : ""); + return isActive ? "Active" : "Available"; + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + elide: Text.ElideRight + wrapMode: Text.NoWrap + width: parent.width + } + } + } + + MouseArea { + id: playerMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (modelData?.identity) { + root.playerSelected(modelData); + } + } + } + } + } + } + } + } + } + + MouseArea { + anchors.fill: parent + z: -1 + enabled: dropdownType !== 0 + onClicked: closeRequested() + } +} diff --git a/quickshell/Modules/DankDash/MediaPlayerTab.qml b/quickshell/Modules/DankDash/MediaPlayerTab.qml index 1d8da039..1e2cb545 100644 --- a/quickshell/Modules/DankDash/MediaPlayerTab.qml +++ b/quickshell/Modules/DankDash/MediaPlayerTab.qml @@ -1,9 +1,7 @@ import QtQuick -import QtQuick.Controls import QtQuick.Effects import QtQuick.Layouts import Quickshell.Services.Mpris -import Quickshell.Services.Pipewire import Quickshell.Io import Quickshell import qs.Common @@ -15,14 +13,42 @@ Item { property MprisPlayer activePlayer: MprisController.activePlayer property var allPlayers: MprisController.availablePlayers + property var targetScreen: null + property real popoutX: 0 + property real popoutY: 0 + property real popoutWidth: 0 + property real popoutHeight: 0 + property real contentOffsetY: 0 + + signal showVolumeDropdown(point pos, var screen, bool rightEdge, var player, var players) + signal showAudioDevicesDropdown(point pos, var screen, bool rightEdge) + signal showPlayersDropdown(point pos, var screen, bool rightEdge, var player, var players) + signal hideDropdowns + signal volumeButtonExited + + property bool volumeExpanded: false + property bool devicesExpanded: false + property bool playersExpanded: false + + function resetDropdownStates() { + volumeExpanded = false; + devicesExpanded = false; + playersExpanded = false; + } DankTooltipV2 { id: sharedTooltip } readonly property bool isRightEdge: (SettingsData.barConfigs[0]?.position ?? SettingsData.Position.Top) === SettingsData.Position.Right - readonly property bool volumeAvailable: (activePlayer && activePlayer.volumeSupported) || (AudioService.sink && AudioService.sink.audio) - readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported + readonly property bool __isChromeBrowser: { + if (!activePlayer?.identity) + return false; + const id = activePlayer.identity.toLowerCase(); + return id.includes("chrome") || id.includes("chromium"); + } + readonly property bool volumeAvailable: (activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio) + readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser readonly property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0) // Palette that stays stable across track switches until new colors are ready @@ -334,383 +360,6 @@ Item { anchors.fill: parent clip: false visible: !_noneAvailable && (!showNoPlayerNow) - - MouseArea { - anchors.fill: parent - enabled: audioDevicesButton.devicesExpanded || volumeButton.volumeExpanded || playerSelectorButton.playersExpanded - onClicked: function (mouse) { - const clickOutside = item => { - return mouse.x < item.x || mouse.x > item.x + item.width || mouse.y < item.y || mouse.y > item.y + item.height; - }; - - if (playerSelectorButton.playersExpanded && clickOutside(playerSelectorDropdown)) { - playerSelectorButton.playersExpanded = false; - } - if (audioDevicesButton.devicesExpanded && clickOutside(audioDevicesDropdown)) { - audioDevicesButton.devicesExpanded = false; - } - if (volumeButton.volumeExpanded && clickOutside(volumeSliderPanel) && clickOutside(volumeButton)) { - volumeButton.volumeExpanded = false; - } - } - } - - Popup { - id: audioDevicesDropdown - width: 280 - height: audioDevicesButton.devicesExpanded ? Math.max(200, Math.min(280, audioDevicesDropdown.availableDevices.length * 50 + 100)) : 0 - x: isRightEdge ? audioDevicesButton.x + audioDevicesButton.width + Theme.spacingS : audioDevicesButton.x - width - Theme.spacingS - y: audioDevicesButton.y - 50 - visible: audioDevicesButton.devicesExpanded - closePolicy: Popup.NoAutoClose - modal: false - dim: false - padding: 0 - - property var availableDevices: Pipewire.nodes.values.filter(node => { - return node.audio && node.isSink && !node.isStream; - }) - - background: Rectangle { - color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98) - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6) - border.width: 2 - radius: Theme.cornerRadius * 2 - - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - shadowHorizontalOffset: 0 - shadowVerticalOffset: 8 - shadowBlur: 1.0 - shadowColor: Qt.rgba(0, 0, 0, 0.4) - shadowOpacity: 0.7 - } - } - - Behavior on height { - NumberAnimation { - duration: Anims.durShort - easing.type: Easing.BezierSpline - easing.bezierCurve: Anims.emphasizedDecel - } - } - - enter: Transition { - NumberAnimation { - property: "opacity" - from: 0 - to: 1 - duration: Anims.durShort - easing.type: Easing.BezierSpline - easing.bezierCurve: Anims.standard - } - } - - exit: Transition { - NumberAnimation { - property: "opacity" - from: 1 - to: 0 - duration: Anims.durShort - easing.type: Easing.BezierSpline - easing.bezierCurve: Anims.standard - } - } - - Column { - anchors.fill: parent - anchors.margins: Theme.spacingM - - StyledText { - text: I18n.tr("Audio Output Devices (") + audioDevicesDropdown.availableDevices.length + ")" - font.pixelSize: Theme.fontSizeMedium - font.weight: Font.Medium - color: Theme.surfaceText - width: parent.width - horizontalAlignment: Text.AlignHCenter - bottomPadding: Theme.spacingM - } - - DankFlickable { - width: parent.width - height: parent.height - 40 - contentHeight: deviceColumn.height - clip: true - - Column { - id: deviceColumn - width: parent.width - spacing: Theme.spacingS - - Repeater { - model: audioDevicesDropdown.availableDevices - delegate: Rectangle { - required property var modelData - required property int index - - width: parent.width - height: 48 - radius: Theme.cornerRadius - color: deviceMouseAreaLeft.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) - border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) - border.width: modelData === AudioService.sink ? 2 : 1 - - Row { - anchors.left: parent.left - anchors.leftMargin: Theme.spacingM - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.spacingM - width: parent.width - Theme.spacingM * 2 - - DankIcon { - name: getAudioDeviceIcon(modelData) - size: 20 - color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - Column { - anchors.verticalCenter: parent.verticalCenter - width: parent.width - 20 - 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 - } - - StyledText { - text: modelData === AudioService.sink ? "Active" : "Available" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - elide: Text.ElideRight - width: parent.width - wrapMode: Text.NoWrap - } - } - } - - MouseArea { - id: deviceMouseAreaLeft - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (modelData) { - Pipewire.preferredDefaultAudioSink = modelData; - } - audioDevicesButton.devicesExpanded = false; - } - } - - Behavior on border.color { - ColorAnimation { - duration: Anims.durShort - } - } - } - } - } - } - } - } - - Popup { - id: playerSelectorDropdown - width: 240 - height: playerSelectorButton.playersExpanded ? Math.max(180, Math.min(240, (root.allPlayers?.length || 0) * 50 + 80)) : 0 - x: isRightEdge ? playerSelectorButton.x + playerSelectorButton.width + Theme.spacingS : playerSelectorButton.x - width - Theme.spacingS - y: playerSelectorButton.y - 50 - visible: playerSelectorButton.playersExpanded - closePolicy: Popup.NoAutoClose - modal: false - dim: false - padding: 0 - - background: Rectangle { - color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98) - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6) - border.width: 2 - radius: Theme.cornerRadius * 2 - - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - shadowHorizontalOffset: 0 - shadowVerticalOffset: 8 - shadowBlur: 1.0 - shadowColor: Qt.rgba(0, 0, 0, 0.4) - shadowOpacity: 0.7 - } - } - - Behavior on height { - NumberAnimation { - duration: Anims.durShort - easing.type: Easing.BezierSpline - easing.bezierCurve: Anims.emphasizedDecel - } - } - - enter: Transition { - NumberAnimation { - property: "opacity" - from: 0 - to: 1 - duration: Anims.durShort - easing.type: Easing.BezierSpline - easing.bezierCurve: Anims.standard - } - } - - exit: Transition { - NumberAnimation { - property: "opacity" - from: 1 - to: 0 - duration: Anims.durShort - easing.type: Easing.BezierSpline - easing.bezierCurve: Anims.standard - } - } - - Column { - anchors.fill: parent - anchors.margins: Theme.spacingM - - StyledText { - text: I18n.tr("Media Players (") + (allPlayers?.length || 0) + ")" - font.pixelSize: Theme.fontSizeMedium - font.weight: Font.Medium - color: Theme.surfaceText - width: parent.width - horizontalAlignment: Text.AlignHCenter - bottomPadding: Theme.spacingM - } - - DankFlickable { - width: parent.width - height: parent.height - 40 - contentHeight: playerColumn.height - clip: true - - Column { - id: playerColumn - width: parent.width - spacing: Theme.spacingS - - Repeater { - model: allPlayers || [] - delegate: Rectangle { - required property var modelData - required property int index - - width: parent.width - height: 48 - radius: Theme.cornerRadius - color: playerMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) - border.color: modelData === activePlayer ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) - border.width: modelData === activePlayer ? 2 : 1 - - Row { - anchors.left: parent.left - anchors.leftMargin: Theme.spacingM - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.spacingM - width: parent.width - Theme.spacingM * 2 - - DankIcon { - name: "music_note" - size: 20 - color: modelData === activePlayer ? Theme.primary : Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - Column { - anchors.verticalCenter: parent.verticalCenter - width: parent.width - 20 - Theme.spacingM * 2 - - StyledText { - text: { - if (!modelData) - return "Unknown Player"; - - const identity = modelData.identity || "Unknown Player"; - const trackTitle = modelData.trackTitle || ""; - - if (trackTitle.length > 0) { - return identity + " - " + trackTitle; - } - - return identity; - } - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - font.weight: modelData === activePlayer ? Font.Medium : Font.Normal - elide: Text.ElideRight - width: parent.width - wrapMode: Text.NoWrap - } - - StyledText { - text: { - if (!modelData) - return ""; - - const artist = modelData.trackArtist || ""; - const isActive = modelData === activePlayer; - - if (artist.length > 0) { - return artist + (isActive ? " (Active)" : ""); - } - - return isActive ? "Active" : "Available"; - } - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - elide: Text.ElideRight - width: parent.width - wrapMode: Text.NoWrap - } - } - } - - MouseArea { - id: playerMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (modelData && modelData.identity) { - if (activePlayer && activePlayer !== modelData && activePlayer.canPause) { - activePlayer.pause(); - } - - MprisController.activePlayer = modelData; - } - playerSelectorButton.playersExpanded = false; - } - } - - Behavior on border.color { - ColorAnimation { - duration: Anims.durShort - easing.type: Easing.BezierSpline - easing.bezierCurve: Anims.standard - } - } - } - } - } - } - } - } - // Center Column: Main Media Content ColumnLayout { x: 72 y: 20 @@ -1051,14 +700,12 @@ Item { radius: 20 x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM y: 185 - color: playerSelectorArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent" + color: playerSelectorArea.containsMouse || playersExpanded ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent" border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) border.width: 1 z: 100 visible: (allPlayers?.length || 0) >= 1 - property bool playersExpanded: false - DankIcon { anchors.centerIn: parent name: "assistant_device" @@ -1072,14 +719,20 @@ Item { hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - parent.playersExpanded = !parent.playersExpanded; - } - onEntered: { - sharedTooltip.show("Media Players", playerSelectorButton, 0, 0, isRightEdge ? "right" : "left"); - } - onExited: { - sharedTooltip.hide(); + if (playersExpanded) { + hideDropdowns(); + return; + } + hideDropdowns(); + playersExpanded = true; + const buttonsOnRight = !isRightEdge; + const btnY = playerSelectorButton.y + playerSelectorButton.height / 2; + const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX; + const screenY = popoutY + contentOffsetY + btnY; + showPlayersDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers); } + onEntered: sharedTooltip.show("Media Players", playerSelectorButton, 0, 0, isRightEdge ? "right" : "left") + onExited: sharedTooltip.hide() } } @@ -1090,21 +743,14 @@ Item { radius: 20 x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM y: 130 - color: volumeButtonArea.containsMouse && volumeAvailable ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent" + color: volumeButtonArea.containsMouse && volumeAvailable || volumeExpanded ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent" border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, volumeAvailable ? 0.3 : 0.15) border.width: 1 z: 101 enabled: volumeAvailable - property bool volumeExpanded: false property real previousVolume: 0.0 - Timer { - id: volumeHideTimer - interval: 500 - onTriggered: volumeButton.volumeExpanded = false - } - DankIcon { anchors.centerIn: parent name: getVolumeIcon() @@ -1118,11 +764,19 @@ Item { hoverEnabled: true cursorShape: Qt.PointingHandCursor onEntered: { - volumeButton.volumeExpanded = true; - volumeHideTimer.stop(); + if (volumeExpanded) + return; + hideDropdowns(); + volumeExpanded = true; + const buttonsOnRight = !isRightEdge; + const btnY = volumeButton.y + volumeButton.height / 2; + const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX; + const screenY = popoutY + contentOffsetY + btnY; + showVolumeDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers); } onExited: { - volumeHideTimer.restart(); + if (volumeExpanded) + volumeButtonExited(); } onClicked: { if (currentVolume > 0) { @@ -1142,22 +796,15 @@ Item { } } onWheel: wheelEvent => { - let delta = wheelEvent.angleDelta.y; - let current = (currentVolume * 100) || 0; - let newVolume; - - if (delta > 0) { - newVolume = Math.min(100, current + 5); - } else { - newVolume = Math.max(0, current - 5); - } + const delta = wheelEvent.angleDelta.y; + const current = (currentVolume * 100) || 0; + const newVolume = delta > 0 ? Math.min(100, current + 5) : Math.max(0, current - 5); if (usePlayerVolume) { activePlayer.volume = newVolume / 100; } else if (AudioService.sink?.audio) { AudioService.sink.audio.volume = newVolume / 100; } - volumeButton.volumeExpanded = true; wheelEvent.accepted = true; } } @@ -1170,16 +817,14 @@ Item { radius: 20 x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM y: 240 - color: audioDevicesArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent" + color: audioDevicesArea.containsMouse || devicesExpanded ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent" border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) border.width: 1 z: 100 - property bool devicesExpanded: false - DankIcon { anchors.centerIn: parent - name: parent.devicesExpanded ? "expand_less" : "speaker" + name: devicesExpanded ? "expand_less" : "speaker" size: 18 color: Theme.surfaceText } @@ -1190,247 +835,20 @@ Item { hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - parent.devicesExpanded = !parent.devicesExpanded; + if (devicesExpanded) { + hideDropdowns(); + return; + } + hideDropdowns(); + devicesExpanded = true; + const buttonsOnRight = !isRightEdge; + const btnY = audioDevicesButton.y + audioDevicesButton.height / 2; + const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX; + const screenY = popoutY + contentOffsetY + btnY; + showAudioDevicesDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight); } - onEntered: { - sharedTooltip.show("Output Device", audioDevicesButton, 0, 0, isRightEdge ? "right" : "left"); - } - onExited: { - sharedTooltip.hide(); - } - } - } - } - - Popup { - id: volumeSliderPanel - width: 60 - height: 180 - x: isRightEdge ? volumeButton.x + volumeButton.width + Theme.spacingS : volumeButton.x - width - Theme.spacingS - y: volumeButton.y - 50 - visible: volumeButton.volumeExpanded && volumeAvailable - closePolicy: Popup.NoAutoClose - modal: false - dim: false - padding: 0 - - background: Rectangle { - radius: Theme.cornerRadius * 2 - color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) - border.width: 1 - - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - shadowHorizontalOffset: 0 - shadowVerticalOffset: 8 - shadowBlur: 1.0 - shadowColor: Qt.rgba(0, 0, 0, 0.4) - shadowOpacity: 0.7 - } - } - - enter: Transition { - NumberAnimation { - property: "opacity" - from: 0 - to: 1 - duration: Anims.durShort - easing.type: Easing.BezierSpline - easing.bezierCurve: Anims.standard - } - } - - exit: Transition { - NumberAnimation { - property: "opacity" - from: 1 - to: 0 - duration: Anims.durShort - easing.type: Easing.BezierSpline - easing.bezierCurve: Anims.standard - } - } - - Item { - anchors.fill: parent - anchors.margins: Theme.spacingS - - Item { - id: volumeSlider - width: parent.width * 0.5 - height: parent.height - Theme.spacingXL * 2 - anchors.top: parent.top - anchors.topMargin: Theme.spacingS - anchors.horizontalCenter: parent.horizontalCenter - - property bool dragging: false - property bool containsMouse: volumeSliderArea.containsMouse - property bool active: volumeSliderArea.containsMouse || volumeSliderArea.pressed || dragging - - Rectangle { - id: sliderTrack - width: parent.width - height: parent.height - anchors.centerIn: parent - color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) - radius: Theme.cornerRadius - } - - Rectangle { - id: sliderFill - width: parent.width - height: volumeAvailable ? (Math.min(1.0, currentVolume) * parent.height) : 0 - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - color: Theme.primary - bottomLeftRadius: Theme.cornerRadius - bottomRightRadius: Theme.cornerRadius - topLeftRadius: 0 - topRightRadius: 0 - } - - Rectangle { - id: sliderHandle - width: parent.width + 8 - height: 8 - radius: Theme.cornerRadius - y: { - const ratio = volumeAvailable ? Math.min(1.0, currentVolume) : 0; - const travel = parent.height - height; - return Math.max(0, Math.min(travel, travel * (1 - ratio))); - } - anchors.horizontalCenter: parent.horizontalCenter - color: Theme.primary - border.width: 3 - border.color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1.0) - - Rectangle { - anchors.fill: parent - radius: Theme.cornerRadius - color: Theme.onPrimary - opacity: volumeSliderArea.pressed ? 0.16 : (volumeSliderArea.containsMouse ? 0.08 : 0) - visible: opacity > 0 - } - - Rectangle { - id: ripple - anchors.centerIn: parent - width: 0 - height: 0 - radius: width / 2 - color: Theme.onPrimary - opacity: 0 - - function start() { - opacity = 0.16; - width = 0; - height = 0; - rippleAnimation.start(); - } - - SequentialAnimation { - id: rippleAnimation - NumberAnimation { - target: ripple - properties: "width,height" - to: 28 - duration: 180 - } - NumberAnimation { - target: ripple - property: "opacity" - to: 0 - duration: 150 - } - } - } - - TapHandler { - acceptedButtons: Qt.LeftButton - onPressedChanged: { - if (pressed) { - ripple.start(); - } - } - } - - scale: volumeSlider.active ? 1.05 : 1.0 - - Behavior on scale { - NumberAnimation { - duration: Anims.durShort - easing.type: Easing.BezierSpline - easing.bezierCurve: Anims.standard - } - } - } - - MouseArea { - id: volumeSliderArea - anchors.fill: parent - anchors.margins: -12 - enabled: volumeAvailable - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - preventStealing: true - - onEntered: { - volumeHideTimer.stop(); - } - - onExited: { - volumeHideTimer.restart(); - } - - onPressed: function (mouse) { - parent.dragging = true; - updateVolume(mouse); - } - - onReleased: { - parent.dragging = false; - } - - onPositionChanged: function (mouse) { - if (pressed) { - updateVolume(mouse); - } - } - - onClicked: function (mouse) { - updateVolume(mouse); - } - - onWheel: wheelEvent => { - const step = 1; - adjustVolume(wheelEvent.angleDelta.y > 0 ? step : -step); - wheelEvent.accepted = true; - } - - function updateVolume(mouse) { - if (volumeAvailable) { - const ratio = 1.0 - (mouse.y / height); - const volume = Math.max(0, Math.min(1, ratio)); - if (usePlayerVolume) { - activePlayer.volume = volume; - } else if (AudioService.sink?.audio) { - AudioService.sink.audio.volume = volume; - } - } - } - } - } - - StyledText { - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottomMargin: Theme.spacingL - text: volumeAvailable ? Math.round(currentVolume * 100) + "%" : "0%" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - font.weight: Font.Medium + onEntered: sharedTooltip.show("Output Device", audioDevicesButton, 0, 0, isRightEdge ? "right" : "left") + onExited: sharedTooltip.hide() } } } diff --git a/quickshell/Widgets/DankPopout.qml b/quickshell/Widgets/DankPopout.qml index a7ba1be7..64df7226 100644 --- a/quickshell/Widgets/DankPopout.qml +++ b/quickshell/Widgets/DankPopout.qml @@ -12,6 +12,8 @@ Item { property string layerNamespace: "dms:popout" property alias content: contentLoader.sourceComponent property alias contentLoader: contentLoader + property Component overlayContent: null + property alias overlayLoader: overlayLoader property real popupWidth: 400 property real popupHeight: 300 property real triggerX: 0 @@ -243,6 +245,13 @@ Item { backgroundClicked(); } } + + Loader { + id: overlayLoader + anchors.fill: parent + active: root.overlayContent !== null && backgroundWindow.visible + sourceComponent: root.overlayContent + } } PanelWindow {