1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-30 00:12:50 -05:00

media: fix player button control popup things

This commit is contained in:
bbedward
2025-11-24 20:51:05 -05:00
parent fa98a27c90
commit 5288d042ca
4 changed files with 761 additions and 737 deletions

View File

@@ -1,11 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Mpris
import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Services
import qs.Widgets import qs.Widgets
import qs.Modules.DankDash import qs.Modules.DankDash
@@ -27,42 +23,130 @@ DankPopout {
property bool __focusArmed: false property bool __focusArmed: false
property bool __contentReady: 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() { function __tryFocusOnce() {
if (!__focusArmed) if (!__focusArmed)
return return;
const win = root.window const win = root.window;
if (!win || !win.visible) if (!win || !win.visible)
return return;
if (!contentLoader.item) if (!contentLoader.item)
return return;
if (win.requestActivate) if (win.requestActivate)
win.requestActivate() win.requestActivate();
contentLoader.item.forceActiveFocus(Qt.TabFocusReason) contentLoader.item.forceActiveFocus(Qt.TabFocusReason);
if (contentLoader.item.activeFocus) if (contentLoader.item.activeFocus)
__focusArmed = false __focusArmed = false;
} }
onDashVisibleChanged: { onDashVisibleChanged: {
if (dashVisible) { if (dashVisible) {
__focusArmed = true __focusArmed = true;
__contentReady = !!contentLoader.item __contentReady = !!contentLoader.item;
open() open();
__tryFocusOnce() __tryFocusOnce();
} else { } else {
__focusArmed = false __focusArmed = false;
__contentReady = false __contentReady = false;
close() __hideDropdowns();
close();
} }
} }
Connections { Connections {
target: contentLoader target: contentLoader
function onLoaded() { function onLoaded() {
__contentReady = true __contentReady = true;
if (__focusArmed) if (__focusArmed)
__tryFocusOnce() __tryFocusOnce();
} }
} }
@@ -71,12 +155,12 @@ DankPopout {
enabled: !!root.window enabled: !!root.window
function onVisibleChanged() { function onVisibleChanged() {
if (__focusArmed) if (__focusArmed)
__tryFocusOnce() __tryFocusOnce();
} }
} }
onBackgroundClicked: { onBackgroundClicked: {
dashVisible = false dashVisible = false;
} }
content: Component { content: Component {
@@ -90,7 +174,7 @@ DankPopout {
Component.onCompleted: { Component.onCompleted: {
if (root.shouldBeVisible) { if (root.shouldBeVisible) {
mainContainer.forceActiveFocus() mainContainer.forceActiveFocus();
} }
} }
@@ -99,54 +183,54 @@ DankPopout {
function onShouldBeVisibleChanged() { function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) { if (root.shouldBeVisible) {
Qt.callLater(function () { Qt.callLater(function () {
mainContainer.forceActiveFocus() mainContainer.forceActiveFocus();
}) });
} }
} }
} }
Keys.onPressed: function (event) { Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
root.dashVisible = false root.dashVisible = false;
event.accepted = true event.accepted = true;
return return;
} }
if (event.key === Qt.Key_Tab && !(event.modifiers & Qt.ShiftModifier)) { 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) { while (nextIndex < tabBar.model.length && tabBar.model[nextIndex] && tabBar.model[nextIndex].isAction) {
nextIndex++ nextIndex++;
} }
if (nextIndex >= tabBar.model.length) { if (nextIndex >= tabBar.model.length) {
nextIndex = 0 nextIndex = 0;
} }
root.currentTabIndex = nextIndex root.currentTabIndex = nextIndex;
event.accepted = true event.accepted = true;
return return;
} }
if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))) { 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) { while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) {
prevIndex-- prevIndex--;
} }
if (prevIndex < 0) { if (prevIndex < 0) {
prevIndex = tabBar.model.length - 1 prevIndex = tabBar.model.length - 1;
while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) { while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) {
prevIndex-- prevIndex--;
} }
} }
if (prevIndex >= 0) { if (prevIndex >= 0) {
root.currentTabIndex = prevIndex root.currentTabIndex = prevIndex;
} }
event.accepted = true event.accepted = true;
return return;
} }
if (root.currentTabIndex === 2 && wallpaperTab.handleKeyEvent) { if (root.currentTabIndex === 2 && wallpaperTab.handleKeyEvent) {
if (wallpaperTab.handleKeyEvent(event)) { if (wallpaperTab.handleKeyEvent(event)) {
event.accepted = true event.accepted = true;
return return;
} }
} }
} }
@@ -171,50 +255,54 @@ DankPopout {
focus: false focus: false
activeFocusOnTab: false activeFocusOnTab: false
nextFocusTarget: { nextFocusTarget: {
const item = pages.currentItem const item = pages.currentItem;
if (!item) if (!item)
return null return null;
if (item.focusTarget) if (item.focusTarget)
return item.focusTarget return item.focusTarget;
return item return item;
} }
model: { model: {
let tabs = [{ let tabs = [
"icon": "dashboard", {
"text": I18n.tr("Overview") "icon": "dashboard",
}, { "text": I18n.tr("Overview")
"icon": "music_note", },
"text": I18n.tr("Media") {
}, { "icon": "music_note",
"icon": "wallpaper", "text": I18n.tr("Media")
"text": I18n.tr("Wallpapers") },
}] {
"icon": "wallpaper",
"text": I18n.tr("Wallpapers")
}
];
if (SettingsData.weatherEnabled) { if (SettingsData.weatherEnabled) {
tabs.push({ tabs.push({
"icon": "wb_sunny", "icon": "wb_sunny",
"text": I18n.tr("Weather") "text": I18n.tr("Weather")
}) });
} }
tabs.push({ tabs.push({
"icon": "settings", "icon": "settings",
"text": I18n.tr("Settings"), "text": I18n.tr("Settings"),
"isAction": true "isAction": true
}) });
return tabs return tabs;
} }
onTabClicked: function (index) { onTabClicked: function (index) {
root.currentTabIndex = index root.currentTabIndex = index;
} }
onActionTriggered: function (index) { onActionTriggered: function (index) {
let settingsIndex = SettingsData.weatherEnabled ? 4 : 3 let settingsIndex = SettingsData.weatherEnabled ? 4 : 3;
if (index === settingsIndex) { if (index === settingsIndex) {
dashVisible = false dashVisible = false;
settingsModal.show() settingsModal.show();
} }
} }
} }
@@ -229,14 +317,14 @@ DankPopout {
width: parent.width width: parent.width
implicitHeight: { implicitHeight: {
if (currentIndex === 0) if (currentIndex === 0)
return overviewTab.implicitHeight return overviewTab.implicitHeight;
if (currentIndex === 1) if (currentIndex === 1)
return mediaTab.implicitHeight return mediaTab.implicitHeight;
if (currentIndex === 2) if (currentIndex === 2)
return wallpaperTab.implicitHeight return wallpaperTab.implicitHeight;
if (SettingsData.weatherEnabled && currentIndex === 3) if (SettingsData.weatherEnabled && currentIndex === 3)
return weatherTab.implicitHeight return weatherTab.implicitHeight;
return overviewTab.implicitHeight return overviewTab.implicitHeight;
} }
currentIndex: root.currentTabIndex currentIndex: root.currentTabIndex
@@ -244,24 +332,42 @@ DankPopout {
id: overviewTab id: overviewTab
onCloseDash: { onCloseDash: {
root.dashVisible = false root.dashVisible = false;
} }
onSwitchToWeatherTab: { onSwitchToWeatherTab: {
if (SettingsData.weatherEnabled) { if (SettingsData.weatherEnabled) {
tabBar.currentIndex = 3 tabBar.currentIndex = 3;
tabBar.tabClicked(3) tabBar.tabClicked(3);
} }
} }
onSwitchToMediaTab: { onSwitchToMediaTab: {
tabBar.currentIndex = 1 tabBar.currentIndex = 1;
tabBar.tabClicked(1) tabBar.tabClicked(1);
} }
} }
MediaPlayerTab { MediaPlayerTab {
id: mediaTab 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 { WallpaperTab {

View File

@@ -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()
}
}

View File

@@ -1,9 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Controls
import QtQuick.Effects import QtQuick.Effects
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import Quickshell.Services.Pipewire
import Quickshell.Io import Quickshell.Io
import Quickshell import Quickshell
import qs.Common import qs.Common
@@ -15,14 +13,42 @@ Item {
property MprisPlayer activePlayer: MprisController.activePlayer property MprisPlayer activePlayer: MprisController.activePlayer
property var allPlayers: MprisController.availablePlayers 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 { DankTooltipV2 {
id: sharedTooltip id: sharedTooltip
} }
readonly property bool isRightEdge: (SettingsData.barConfigs[0]?.position ?? SettingsData.Position.Top) === SettingsData.Position.Right 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 __isChromeBrowser: {
readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported 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) readonly property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0)
// Palette that stays stable across track switches until new colors are ready // Palette that stays stable across track switches until new colors are ready
@@ -334,383 +360,6 @@ Item {
anchors.fill: parent anchors.fill: parent
clip: false clip: false
visible: !_noneAvailable && (!showNoPlayerNow) 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 { ColumnLayout {
x: 72 x: 72
y: 20 y: 20
@@ -1051,14 +700,12 @@ Item {
radius: 20 radius: 20
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
y: 185 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.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
border.width: 1 border.width: 1
z: 100 z: 100
visible: (allPlayers?.length || 0) >= 1 visible: (allPlayers?.length || 0) >= 1
property bool playersExpanded: false
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: "assistant_device" name: "assistant_device"
@@ -1072,14 +719,20 @@ Item {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
parent.playersExpanded = !parent.playersExpanded; if (playersExpanded) {
} hideDropdowns();
onEntered: { return;
sharedTooltip.show("Media Players", playerSelectorButton, 0, 0, isRightEdge ? "right" : "left"); }
} hideDropdowns();
onExited: { playersExpanded = true;
sharedTooltip.hide(); 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 radius: 20
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
y: 130 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.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, volumeAvailable ? 0.3 : 0.15)
border.width: 1 border.width: 1
z: 101 z: 101
enabled: volumeAvailable enabled: volumeAvailable
property bool volumeExpanded: false
property real previousVolume: 0.0 property real previousVolume: 0.0
Timer {
id: volumeHideTimer
interval: 500
onTriggered: volumeButton.volumeExpanded = false
}
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: getVolumeIcon() name: getVolumeIcon()
@@ -1118,11 +764,19 @@ Item {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onEntered: { onEntered: {
volumeButton.volumeExpanded = true; if (volumeExpanded)
volumeHideTimer.stop(); 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: { onExited: {
volumeHideTimer.restart(); if (volumeExpanded)
volumeButtonExited();
} }
onClicked: { onClicked: {
if (currentVolume > 0) { if (currentVolume > 0) {
@@ -1142,22 +796,15 @@ Item {
} }
} }
onWheel: wheelEvent => { onWheel: wheelEvent => {
let delta = wheelEvent.angleDelta.y; const delta = wheelEvent.angleDelta.y;
let current = (currentVolume * 100) || 0; const current = (currentVolume * 100) || 0;
let newVolume; const newVolume = delta > 0 ? Math.min(100, current + 5) : Math.max(0, current - 5);
if (delta > 0) {
newVolume = Math.min(100, current + 5);
} else {
newVolume = Math.max(0, current - 5);
}
if (usePlayerVolume) { if (usePlayerVolume) {
activePlayer.volume = newVolume / 100; activePlayer.volume = newVolume / 100;
} else if (AudioService.sink?.audio) { } else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = newVolume / 100; AudioService.sink.audio.volume = newVolume / 100;
} }
volumeButton.volumeExpanded = true;
wheelEvent.accepted = true; wheelEvent.accepted = true;
} }
} }
@@ -1170,16 +817,14 @@ Item {
radius: 20 radius: 20
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
y: 240 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.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
border.width: 1 border.width: 1
z: 100 z: 100
property bool devicesExpanded: false
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: parent.devicesExpanded ? "expand_less" : "speaker" name: devicesExpanded ? "expand_less" : "speaker"
size: 18 size: 18
color: Theme.surfaceText color: Theme.surfaceText
} }
@@ -1190,247 +835,20 @@ Item {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { 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: { onEntered: sharedTooltip.show("Output Device", audioDevicesButton, 0, 0, isRightEdge ? "right" : "left")
sharedTooltip.show("Output Device", audioDevicesButton, 0, 0, isRightEdge ? "right" : "left"); onExited: sharedTooltip.hide()
}
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
} }
} }
} }

View File

@@ -12,6 +12,8 @@ Item {
property string layerNamespace: "dms:popout" property string layerNamespace: "dms:popout"
property alias content: contentLoader.sourceComponent property alias content: contentLoader.sourceComponent
property alias contentLoader: contentLoader property alias contentLoader: contentLoader
property Component overlayContent: null
property alias overlayLoader: overlayLoader
property real popupWidth: 400 property real popupWidth: 400
property real popupHeight: 300 property real popupHeight: 300
property real triggerX: 0 property real triggerX: 0
@@ -243,6 +245,13 @@ Item {
backgroundClicked(); backgroundClicked();
} }
} }
Loader {
id: overlayLoader
anchors.fill: parent
active: root.overlayContent !== null && backgroundWindow.visible
sourceComponent: root.overlayContent
}
} }
PanelWindow { PanelWindow {