summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankDash/MediaPlayerTab.qml
diff options
context:
space:
mode:
authorAlexanderCurl <alexc@alexc.hu>2026-04-18 17:46:06 +0100
committerAlexanderCurl <alexc@alexc.hu>2026-04-18 17:46:06 +0100
commit70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 (patch)
treeab1123d4067c1b086dd6faa7ee4ea643236b565a /raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankDash/MediaPlayerTab.qml
parent5d94c0a7d44a2255b81815a52a7056a94a39842d (diff)
downloadRaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.tar.gz
RaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.zip
Replaced file structures for themes in the correct format
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankDash/MediaPlayerTab.qml')
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankDash/MediaPlayerTab.qml785
1 files changed, 785 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankDash/MediaPlayerTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankDash/MediaPlayerTab.qml
new file mode 100644
index 0000000..c6d7bfc
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankDash/MediaPlayerTab.qml
@@ -0,0 +1,785 @@
+import QtQuick
+import QtQuick.Effects
+import QtQuick.Layouts
+import Quickshell.Services.Mpris
+import Quickshell.Io
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Item {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ 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
+ property string section: ""
+ property int barPosition: SettingsData.Position.Top
+
+ 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: {
+ if (barPosition === SettingsData.Position.Right)
+ return true;
+ if (barPosition === SettingsData.Position.Left)
+ return false;
+ return section === "right";
+ }
+ 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)
+
+ property bool isSwitching: false
+
+ // Derived "no players" state: always correct, no timers.
+ readonly property int _playerCount: allPlayers ? allPlayers.length : 0
+ readonly property bool _noneAvailable: _playerCount === 0
+ readonly property bool _trulyIdle: activePlayer && activePlayer.playbackState === MprisPlaybackState.Stopped && !activePlayer.trackTitle && !activePlayer.trackArtist
+ readonly property bool showNoPlayerNow: (!_switchHold) && (_noneAvailable || _trulyIdle)
+
+ property bool _switchHold: false
+ Timer {
+ id: _switchHoldTimer
+ interval: 650
+ repeat: false
+ onTriggered: _switchHold = false
+ }
+
+ onActivePlayerChanged: {
+ if (!activePlayer) {
+ isSwitching = false;
+ _switchHold = false;
+ return;
+ }
+ isSwitching = true;
+ _switchHold = true;
+ _switchHoldTimer.restart();
+ TrackArtService.loadArtwork(activePlayer.trackArtUrl);
+ }
+
+ function maybeFinishSwitch() {
+ if (activePlayer && activePlayer.trackTitle !== "") {
+ isSwitching = false;
+ _switchHold = false;
+ }
+ }
+
+ readonly property real ratio: {
+ if (!activePlayer || !activePlayer.length || activePlayer.length <= 0) {
+ return 0;
+ }
+ const pos = (activePlayer.position || 0) % Math.max(1, activePlayer.length);
+ const calculatedRatio = pos / activePlayer.length;
+ return Math.max(0, Math.min(1, calculatedRatio));
+ }
+
+ implicitWidth: SettingsData.showWeekNumber ? 736 : 700
+ implicitHeight: playerContent.height + playerContent.anchors.topMargin * 2
+
+ Connections {
+ target: activePlayer
+ function onTrackTitleChanged() {
+ _switchHoldTimer.restart();
+ maybeFinishSwitch();
+ }
+ function onTrackArtUrlChanged() {
+ TrackArtService.loadArtwork(activePlayer.trackArtUrl);
+ }
+ }
+
+ Connections {
+ target: MprisController
+ function onAvailablePlayersChanged() {
+ const count = (MprisController.availablePlayers?.length || 0);
+ if (count === 0) {
+ isSwitching = false;
+ _switchHold = false;
+ } else {
+ _switchHold = true;
+ _switchHoldTimer.restart();
+ }
+ }
+ }
+
+ function getAudioDeviceIcon(device) {
+ if (!device || !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";
+ if (name.includes("analog") || name.includes("built-in"))
+ return "speaker";
+
+ return "speaker";
+ }
+
+ function getVolumeIcon() {
+ if (!volumeAvailable)
+ return "volume_off";
+
+ const volume = currentVolume;
+
+ if (usePlayerVolume) {
+ if (volume === 0.0)
+ return "music_off";
+ return "music_note";
+ }
+
+ if (volume === 0.0)
+ return "volume_off";
+ if (volume <= 0.33)
+ return "volume_down";
+ if (volume <= 0.66)
+ return "volume_up";
+ return "volume_up";
+ }
+
+ function adjustVolume(step) {
+ if (!volumeAvailable)
+ return;
+ const maxVol = usePlayerVolume ? 100 : AudioService.sinkMaxVolume;
+ const current = Math.round(currentVolume * 100);
+ const newVolume = Math.min(maxVol, Math.max(0, current + step));
+
+ SessionData.suppressOSDTemporarily();
+ if (usePlayerVolume) {
+ activePlayer.volume = newVolume / 100;
+ } else if (AudioService.sink?.audio) {
+ AudioService.sink.audio.volume = newVolume / 100;
+ }
+ }
+
+ property bool isSeeking: false
+
+ Timer {
+ interval: 1000
+ running: activePlayer?.playbackState === MprisPlaybackState.Playing && !isSeeking
+ repeat: true
+ onTriggered: activePlayer?.positionChanged()
+ }
+
+ Item {
+ id: bgContainer
+ anchors.fill: parent
+ visible: TrackArtService._bgArtSource !== ""
+
+ Image {
+ id: bgImage
+ anchors.centerIn: parent
+ width: Math.max(parent.width, parent.height) * 1.1
+ height: width
+ source: TrackArtService._bgArtSource
+ fillMode: Image.PreserveAspectCrop
+ asynchronous: true
+ cache: true
+ visible: false
+ onStatusChanged: {
+ if (status === Image.Ready)
+ maybeFinishSwitch();
+ }
+ }
+
+ Item {
+ id: blurredBg
+ anchors.fill: parent
+ visible: false
+
+ MultiEffect {
+ anchors.centerIn: parent
+ width: bgImage.width
+ height: bgImage.height
+ source: bgImage
+ blurEnabled: true
+ blurMax: 64
+ blur: 0.8
+ saturation: -0.2
+ brightness: -0.25
+ }
+ }
+
+ Rectangle {
+ id: bgMask
+ anchors.fill: parent
+ radius: Theme.cornerRadius
+ visible: false
+ layer.enabled: true
+ }
+
+ MultiEffect {
+ anchors.fill: parent
+ source: blurredBg
+ maskEnabled: true
+ maskSource: bgMask
+ maskThresholdMin: 0.5
+ maskSpreadAtMin: 1.0
+ opacity: 0.7
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ radius: Theme.cornerRadius
+ color: Theme.surface
+ opacity: 0.3
+ }
+ }
+
+ Column {
+ anchors.centerIn: parent
+ spacing: Theme.spacingM
+ visible: showNoPlayerNow
+
+ DankIcon {
+ name: "music_note"
+ size: Theme.iconSize * 3
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("No Active Players")
+ font.pixelSize: Theme.fontSizeLarge
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+
+ Item {
+ anchors.fill: parent
+ clip: false
+ visible: !_noneAvailable && (!showNoPlayerNow)
+ ColumnLayout {
+ id: playerContent
+ width: 484
+ height: 370
+ spacing: Theme.spacingXS
+ anchors.top: parent.top
+ anchors.topMargin: 20
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ Item {
+ width: parent.width
+ height: 200
+ clip: false
+
+ DankAlbumArt {
+ width: Math.min(parent.width * 0.8, parent.height * 0.9)
+ height: width
+ anchors.centerIn: parent
+ activePlayer: root.activePlayer
+ }
+ }
+
+ // Song Info and Controls Section
+ Item {
+ width: parent.width
+ Layout.fillHeight: true
+
+ Column {
+ id: songInfo
+ width: parent.width
+ spacing: Theme.spacingXS
+ anchors.top: parent.top
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ StyledText {
+ text: activePlayer?.trackTitle || I18n.tr("Unknown Track")
+ font.pixelSize: Theme.fontSizeLarge
+ font.weight: Font.Bold
+ color: Theme.surfaceText
+ width: parent.width
+ horizontalAlignment: Text.AlignHCenter
+ elide: Text.ElideRight
+ wrapMode: Text.WordWrap
+ maximumLineCount: 2
+ }
+
+ StyledText {
+ text: activePlayer?.trackTitle || I18n.tr("Unknown Artist")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
+ width: parent.width
+ horizontalAlignment: Text.AlignHCenter
+ elide: Text.ElideRight
+ wrapMode: Text.WordWrap
+ maximumLineCount: 1
+ }
+
+ StyledText {
+ text: activePlayer?.trackAlbum || ""
+ font.pixelSize: Theme.fontSizeSmall
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
+ width: parent.width
+ horizontalAlignment: Text.AlignHCenter
+ elide: Text.ElideRight
+ wrapMode: Text.WordWrap
+ maximumLineCount: 1
+ visible: text.length > 0
+ }
+ }
+
+ Item {
+ id: seekbarContainer
+ width: parent.width
+ anchors.top: songInfo.bottom
+ anchors.bottom: playbackControls.top
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ Column {
+ width: parent.width
+ spacing: 2
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.verticalCenterOffset: parent.height * 0.2
+
+ DankSeekbar {
+ width: parent.width * 0.8
+ height: 20
+ anchors.horizontalCenter: parent.horizontalCenter
+ activePlayer: root.activePlayer
+ isSeeking: root.isSeeking
+ onIsSeekingChanged: root.isSeeking = isSeeking
+ }
+
+ Item {
+ width: parent.width * 0.8
+ height: 16
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ StyledText {
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ text: {
+ if (!activePlayer)
+ return "0:00";
+ const rawPos = Math.max(0, activePlayer.position || 0);
+ const pos = activePlayer.length ? rawPos % Math.max(1, activePlayer.length) : rawPos;
+ const minutes = Math.floor(pos / 60);
+ const seconds = Math.floor(pos % 60);
+ const timeStr = minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
+ return timeStr;
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+
+ StyledText {
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ text: {
+ if (!activePlayer || !activePlayer.length)
+ return "0:00";
+ const dur = Math.max(0, activePlayer.length || 0);
+ const minutes = Math.floor(dur / 60);
+ const seconds = Math.floor(dur % 60);
+ return minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+ }
+ }
+
+ Item {
+ id: playbackControls
+ width: parent.width
+ height: 50
+ anchors.bottom: parent.bottom
+
+ Row {
+ anchors.centerIn: parent
+ spacing: Theme.spacingM
+ height: parent.height
+
+ Item {
+ width: 50
+ height: 50
+ anchors.verticalCenter: parent.verticalCenter
+ visible: activePlayer && activePlayer.shuffleSupported
+
+ Rectangle {
+ width: 40
+ height: 40
+ radius: 20
+ anchors.centerIn: parent
+ color: shuffleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "shuffle"
+ size: 20
+ color: activePlayer && activePlayer.shuffle ? Theme.primary : Theme.surfaceText
+ }
+
+ MouseArea {
+ id: shuffleArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (activePlayer && activePlayer.canControl && activePlayer.shuffleSupported) {
+ activePlayer.shuffle = !activePlayer.shuffle;
+ }
+ }
+ }
+ }
+ }
+
+ Item {
+ width: 50
+ height: 50
+ anchors.verticalCenter: parent.verticalCenter
+
+ Rectangle {
+ width: 40
+ height: 40
+ radius: 20
+ anchors.centerIn: parent
+ color: prevBtnArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent"
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "skip_previous"
+ size: 24
+ color: Theme.surfaceText
+ }
+
+ MouseArea {
+ id: prevBtnArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: MprisController.previousOrRewind()
+ }
+ }
+ }
+
+ Item {
+ width: 50
+ height: 50
+ anchors.verticalCenter: parent.verticalCenter
+
+ Rectangle {
+ width: 50
+ height: 50
+ radius: 25
+ anchors.centerIn: parent
+ color: Theme.primary
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
+ size: 28
+ color: Theme.background
+ weight: 500
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: activePlayer && activePlayer.togglePlaying()
+ }
+
+ ElevationShadow {
+ anchors.fill: parent
+ z: -1
+ level: Theme.elevationLevel1
+ fallbackOffset: 1
+ targetRadius: parent.radius
+ targetColor: parent.color
+ shadowOpacity: Theme.elevationLevel1 && Theme.elevationLevel1.alpha !== undefined ? Theme.elevationLevel1.alpha : 0.2
+ shadowEnabled: Theme.elevationEnabled
+ }
+ }
+ }
+
+ Item {
+ width: 50
+ height: 50
+ anchors.verticalCenter: parent.verticalCenter
+
+ Rectangle {
+ width: 40
+ height: 40
+ radius: 20
+ anchors.centerIn: parent
+ color: nextBtnArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent"
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "skip_next"
+ size: 24
+ color: Theme.surfaceText
+ }
+
+ MouseArea {
+ id: nextBtnArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: activePlayer && activePlayer.next()
+ }
+ }
+ }
+
+ Item {
+ width: 50
+ height: 50
+ anchors.verticalCenter: parent.verticalCenter
+ visible: activePlayer && activePlayer.loopSupported
+
+ Rectangle {
+ width: 40
+ height: 40
+ radius: 20
+ anchors.centerIn: parent
+ color: repeatArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: {
+ if (!activePlayer)
+ return "repeat";
+ switch (activePlayer.loopState) {
+ case MprisLoopState.Track:
+ return "repeat_one";
+ case MprisLoopState.Playlist:
+ return "repeat";
+ default:
+ return "repeat";
+ }
+ }
+ size: 20
+ color: activePlayer && activePlayer.loopState !== MprisLoopState.None ? Theme.primary : Theme.surfaceText
+ }
+
+ MouseArea {
+ id: repeatArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (activePlayer && activePlayer.canControl && activePlayer.loopSupported) {
+ switch (activePlayer.loopState) {
+ case MprisLoopState.None:
+ activePlayer.loopState = MprisLoopState.Playlist;
+ break;
+ case MprisLoopState.Playlist:
+ activePlayer.loopState = MprisLoopState.Track;
+ break;
+ case MprisLoopState.Track:
+ activePlayer.loopState = MprisLoopState.None;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ id: playerSelectorButton
+ width: 40
+ height: 40
+ radius: 20
+ x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
+ y: 185
+ 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
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "assistant_device"
+ size: 18
+ color: Theme.surfaceText
+ }
+
+ MouseArea {
+ id: playerSelectorArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ 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(I18n.tr("Media Players"), playerSelectorButton, 0, 0, isRightEdge ? "right" : "left")
+ onExited: sharedTooltip.hide()
+ }
+ }
+
+ Rectangle {
+ id: volumeButton
+ width: 40
+ height: 40
+ radius: 20
+ x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
+ y: 130
+ 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 real previousVolume: 0.0
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: getVolumeIcon()
+ size: 18
+ color: volumeAvailable && currentVolume > 0 ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, volumeAvailable ? 1.0 : 0.5)
+ }
+
+ MouseArea {
+ id: volumeButtonArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onEntered: {
+ 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: {
+ if (volumeExpanded)
+ volumeButtonExited();
+ }
+ onClicked: {
+ SessionData.suppressOSDTemporarily();
+ if (currentVolume > 0) {
+ volumeButton.previousVolume = currentVolume;
+ if (usePlayerVolume) {
+ activePlayer.volume = 0;
+ } else if (AudioService.sink?.audio) {
+ AudioService.sink.audio.volume = 0;
+ }
+ } else {
+ const restoreVolume = volumeButton.previousVolume > 0 ? volumeButton.previousVolume : 0.5;
+ if (usePlayerVolume) {
+ activePlayer.volume = restoreVolume;
+ } else if (AudioService.sink?.audio) {
+ AudioService.sink.audio.volume = restoreVolume;
+ }
+ }
+ }
+ onWheel: wheelEvent => {
+ SessionData.suppressOSDTemporarily();
+ const delta = wheelEvent.angleDelta.y;
+ const current = (currentVolume * 100) || 0;
+ const maxVol = usePlayerVolume ? 100 : AudioService.sinkMaxVolume;
+ const newVolume = delta > 0 ? Math.min(maxVol, current + 5) : Math.max(0, current - 5);
+
+ if (usePlayerVolume) {
+ activePlayer.volume = newVolume / 100;
+ } else if (AudioService.sink?.audio) {
+ AudioService.sink.audio.volume = newVolume / 100;
+ }
+ wheelEvent.accepted = true;
+ }
+ }
+ }
+
+ Rectangle {
+ id: audioDevicesButton
+ width: 40
+ height: 40
+ radius: 20
+ x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
+ y: 240
+ 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
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: devicesExpanded ? "expand_less" : "speaker"
+ size: 18
+ color: Theme.surfaceText
+ }
+
+ MouseArea {
+ id: audioDevicesArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ 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(I18n.tr("Output Device"), audioDevicesButton, 0, 0, isRightEdge ? "right" : "left")
+ onExited: sharedTooltip.hide()
+ }
+ }
+}