mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-08 12:13:31 -04:00
8a4be4936a
* fix(Mpris): exclude idle players from active-player selection Add MprisController.isIdle() (player Stopped with empty title and artist). _resolveActivePlayer excludes idle players from every selection path, and re-resolves when the active player itself goes idle. Existing triggers (availablePlayers change, isPlaying becoming true) do not fire for a player merely stopping. The fallback now gates on !isIdle(p) instead of p.canPlay. canPlay describes whether Play() would succeed; !isIdle describes whether there is anything to surface. canControl unchanged. When no eligible player remains, activePlayer becomes null and consumers that gate on it unload (the bar media widget via WidgetHost; the dash via the companion fix). * fix(DankDash): show no-player state when active player resolves to null showNoPlayerNow gated on _noneAvailable (player count === 0) or activePlayer being idle. Neither covers activePlayer being null while players remain registered, which is now possible (an always-on player sitting stopped with empty metadata). Key showNoPlayerNow on !activePlayer; drop the unreachable _trulyIdle. * add(MprisController): add track artist change handling to active player resolution ---------
912 lines
35 KiB
QML
912 lines
35 KiB
QML
import QtQuick
|
|
import QtQuick.Effects
|
|
import QtQuick.Layouts
|
|
import Quickshell.Services.Mpris
|
|
import qs.Common
|
|
import qs.Services
|
|
import qs.Widgets
|
|
|
|
Item {
|
|
id: root
|
|
|
|
LayoutMirroring.enabled: I18n.isRtl
|
|
LayoutMirroring.childrenInherit: true
|
|
|
|
property MprisPlayer activePlayer: MprisController.activePlayer
|
|
readonly property real stableLength: MprisController.activePlayerStableLength
|
|
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 dropdownButtonExited
|
|
signal dropdownButtonEntered
|
|
|
|
property bool volumeExpanded: false
|
|
property bool devicesExpanded: false
|
|
property bool playersExpanded: false
|
|
|
|
function resetDropdownStates() {
|
|
volumeExpanded = false;
|
|
devicesExpanded = false;
|
|
playersExpanded = false;
|
|
}
|
|
|
|
|
|
|
|
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 showNoPlayerNow: (!_switchHold) && (_noneAvailable || !activePlayer)
|
|
|
|
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();
|
|
}
|
|
|
|
function maybeFinishSwitch() {
|
|
if (activePlayer && activePlayer.trackTitle !== "") {
|
|
isSwitching = false;
|
|
_switchHold = false;
|
|
}
|
|
}
|
|
|
|
readonly property real ratio: {
|
|
if (!activePlayer || stableLength <= 0) {
|
|
return 0;
|
|
}
|
|
const pos = (activePlayer.position || 0) % Math.max(1, stableLength);
|
|
const calculatedRatio = pos / stableLength;
|
|
return Math.max(0, Math.min(1, calculatedRatio));
|
|
}
|
|
|
|
implicitWidth: SettingsData.showWeekNumber ? 736 : 700
|
|
implicitHeight: playerContent.height + playerContent.anchors.topMargin * 2
|
|
|
|
Connections {
|
|
target: activePlayer
|
|
ignoreUnknownSignals: true
|
|
function onTrackTitleChanged() {
|
|
_switchHoldTimer.restart();
|
|
maybeFinishSwitch();
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
function triggerVolumeDropdown() {
|
|
if (!volumeAvailable)
|
|
return;
|
|
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);
|
|
}
|
|
|
|
function toggleMute() {
|
|
if (!volumeAvailable)
|
|
return;
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleKeyEvent(event) {
|
|
if (!activePlayer)
|
|
return false;
|
|
|
|
// 1. Number keys 0-9 to seek to 0%-90%
|
|
if (event.key >= Qt.Key_0 && event.key <= Qt.Key_9) {
|
|
if (activePlayer.canSeek && stableLength > 0) {
|
|
const ratio = (event.key - Qt.Key_0) * 0.1;
|
|
const targetPosition = ratio * stableLength;
|
|
activePlayer.position = Math.max(0.1, Math.min(targetPosition, stableLength * 0.99));
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// 2. Left / Right arrows to seek backward / forward 5s
|
|
if (event.key === Qt.Key_Left) {
|
|
if (activePlayer.canSeek) {
|
|
activePlayer.position = Math.max(0.1, activePlayer.position - 5);
|
|
return true;
|
|
}
|
|
}
|
|
if (event.key === Qt.Key_Right) {
|
|
if (activePlayer.canSeek && stableLength > 0) {
|
|
activePlayer.position = Math.max(0.1, Math.min(stableLength - 1, activePlayer.position + 5));
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// 3. Up / Down arrows to adjust volume
|
|
if (event.key === Qt.Key_Up) {
|
|
adjustVolume(5);
|
|
triggerVolumeDropdown();
|
|
dropdownButtonExited();
|
|
return true;
|
|
}
|
|
if (event.key === Qt.Key_Down) {
|
|
adjustVolume(-5);
|
|
triggerVolumeDropdown();
|
|
dropdownButtonExited();
|
|
return true;
|
|
}
|
|
|
|
// 4. Spacebar to play/pause
|
|
if (event.key === Qt.Key_Space) {
|
|
if (activePlayer.canTogglePlaying) {
|
|
activePlayer.togglePlaying();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// 5. M key to toggle mute
|
|
if (event.key === Qt.Key_M) {
|
|
toggleMute();
|
|
triggerVolumeDropdown();
|
|
dropdownButtonExited();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
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.resolvedArtUrl !== ""
|
|
|
|
Image {
|
|
id: bgImage
|
|
anchors.centerIn: parent
|
|
width: Math.max(parent.width, parent.height) * 1.1
|
|
height: width
|
|
source: TrackArtService.resolvedArtUrl
|
|
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?.trackArtist || 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 = stableLength ? rawPos % Math.max(1, stableLength) : 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 || stableLength <= 0)
|
|
return "--:--";
|
|
const dur = stableLength;
|
|
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: Theme.outlineStrong
|
|
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) {
|
|
if (allPlayers && allPlayers.length > 1) {
|
|
let currentIndex = -1;
|
|
for (let i = 0; i < allPlayers.length; i++) {
|
|
if (allPlayers[i] === activePlayer) {
|
|
currentIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
const nextIndex = (currentIndex + 1) % allPlayers.length;
|
|
MprisController.setActivePlayer(allPlayers[nextIndex]);
|
|
}
|
|
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: {
|
|
dropdownButtonEntered();
|
|
if (playersExpanded)
|
|
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);
|
|
}
|
|
onExited: {
|
|
if (playersExpanded)
|
|
dropdownButtonExited();
|
|
}
|
|
}
|
|
}
|
|
|
|
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: volumeAvailable ? Theme.outlineStrong : Theme.outlineMedium
|
|
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: {
|
|
dropdownButtonEntered();
|
|
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)
|
|
dropdownButtonExited();
|
|
}
|
|
onClicked: {
|
|
toggleMute();
|
|
}
|
|
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: Theme.outlineStrong
|
|
border.width: 1
|
|
z: 100
|
|
|
|
DankIcon {
|
|
anchors.centerIn: parent
|
|
name: "speaker"
|
|
size: 18
|
|
color: Theme.surfaceText
|
|
}
|
|
|
|
MouseArea {
|
|
id: audioDevicesArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
if (devicesExpanded) {
|
|
const sinks = AudioService.getAvailableSinks();
|
|
if (sinks && sinks.length > 1) {
|
|
let currentIndex = -1;
|
|
for (let i = 0; i < sinks.length; i++) {
|
|
if (sinks[i]?.name === AudioService.sink?.name) {
|
|
currentIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
const nextIndex = (currentIndex + 1) % sinks.length;
|
|
AudioService.setSink(sinks[nextIndex]);
|
|
}
|
|
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: {
|
|
dropdownButtonEntered();
|
|
if (devicesExpanded)
|
|
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);
|
|
}
|
|
onExited: {
|
|
if (devicesExpanded)
|
|
dropdownButtonExited();
|
|
}
|
|
}
|
|
}
|
|
}
|