mirror of
https://github.com/zoriya/noctalia-shell.git
synced 2026-08-06 06:16:29 +00:00
Detailed Explanation: Addresses an issue where the media player would automatically switch,overriding user's manual selections when the Control Center was open. The core problem was that the automatic player detection logic was too aggressive and didn't respect explicit user choices. Previous attempts to pause auto-switching based on UI visibility were unreliable due to incorrect event handling for the custom NPanel component.
444 lines
12 KiB
QML
444 lines
12 KiB
QML
import QtQuick
|
|
import QtQuick.Controls
|
|
import QtQuick.Layouts
|
|
import QtQuick.Effects
|
|
import Quickshell
|
|
import qs.Modules.Audio
|
|
import qs.Commons
|
|
import qs.Services
|
|
import qs.Widgets
|
|
|
|
NBox {
|
|
id: root
|
|
|
|
// Wrapper - rounded rect clipper
|
|
Item {
|
|
anchors.fill: parent
|
|
layer.enabled: true
|
|
layer.effect: MultiEffect {
|
|
maskEnabled: true
|
|
maskThresholdMin: 0.5
|
|
maskSpreadAtMin: 0.0
|
|
maskSource: ShaderEffectSource {
|
|
sourceItem: Rectangle {
|
|
width: root.width
|
|
height: root.height
|
|
radius: Style.radiusM
|
|
color: "white"
|
|
}
|
|
}
|
|
}
|
|
|
|
// Background image that covers everything
|
|
Image {
|
|
readonly property int dim: Math.round(256 * Style.uiScaleRatio)
|
|
id: bgImage
|
|
anchors.fill: parent
|
|
source: MediaService.trackArtUrl || WallpaperService.getWallpaper(Screen.name)
|
|
sourceSize: Qt.size(dim, dim)
|
|
fillMode: Image.PreserveAspectCrop
|
|
|
|
layer.enabled: true
|
|
layer.effect: MultiEffect {
|
|
blurEnabled: true
|
|
blur: 0.25
|
|
blurMax: 16
|
|
}
|
|
}
|
|
|
|
// Dark overlay for readability
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: Color.mSurfaceVariant
|
|
opacity: 0.85
|
|
radius: Style.radiusM
|
|
}
|
|
|
|
// Border
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: Color.transparent
|
|
border.color: Color.mOutline
|
|
border.width: 1
|
|
radius: Style.radiusM
|
|
}
|
|
//}
|
|
|
|
// Background visualizer on top of the artwork
|
|
Loader {
|
|
anchors.fill: parent
|
|
active: Settings.data.audio.visualizerType !== "" && Settings.data.audio.visualizerType !== "none"
|
|
|
|
sourceComponent: {
|
|
switch (Settings.data.audio.visualizerType) {
|
|
case "linear":
|
|
return linearComponent
|
|
case "mirrored":
|
|
return mirroredComponent
|
|
case "wave":
|
|
return waveComponent
|
|
default:
|
|
return null
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: linearComponent
|
|
LinearSpectrum {
|
|
anchors.fill: parent
|
|
values: CavaService.values
|
|
fillColor: Color.mPrimary
|
|
opacity: 0.5
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: mirroredComponent
|
|
MirroredSpectrum {
|
|
anchors.fill: parent
|
|
values: CavaService.values
|
|
fillColor: Color.mPrimary
|
|
opacity: 0.5
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: waveComponent
|
|
WaveSpectrum {
|
|
anchors.fill: parent
|
|
values: CavaService.values
|
|
fillColor: Color.mPrimary
|
|
opacity: 0.5
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Player selector
|
|
Rectangle {
|
|
id: playerSelectorButton
|
|
anchors.top: parent.top
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.topMargin: Style.marginXS
|
|
anchors.leftMargin: Style.marginM
|
|
anchors.rightMargin: Style.marginM
|
|
height: Style.barHeight
|
|
visible: MediaService.getAvailablePlayers().length > 1
|
|
radius: Style.radiusM
|
|
color: Color.transparent
|
|
|
|
property var currentPlayer: MediaService.getAvailablePlayers()[MediaService.selectedPlayerIndex]
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
spacing: Style.marginS
|
|
|
|
NIcon {
|
|
icon: "caret-down"
|
|
pointSize: Style.fontSizeXXL
|
|
color: Color.mOnSurfaceVariant
|
|
}
|
|
|
|
NText {
|
|
text: playerSelectorButton.currentPlayer ? playerSelectorButton.currentPlayer.identity : ""
|
|
pointSize: Style.fontSizeXS
|
|
color: Color.mOnSurfaceVariant
|
|
Layout.fillWidth: true
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: playerSelectorMouseArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
|
|
onClicked: {
|
|
var menuItems = []
|
|
var players = MediaService.getAvailablePlayers()
|
|
for (var i = 0; i < players.length; i++) {
|
|
menuItems.push({
|
|
"label": players[i].identity,
|
|
"action": i.toString(),
|
|
"icon": "disc",
|
|
"enabled": true,
|
|
"visible": true
|
|
})
|
|
}
|
|
playerContextMenu.model = menuItems
|
|
playerContextMenu.openAtItem(playerSelectorButton, playerSelectorButton.width - playerContextMenu.width, playerSelectorButton.height)
|
|
}
|
|
}
|
|
|
|
NContextMenu {
|
|
id: playerContextMenu
|
|
parent: root
|
|
width: 200
|
|
|
|
onTriggered: function (action) {
|
|
var index = parseInt(action)
|
|
if (!isNaN(index)) {
|
|
MediaService.switchToPlayer(index)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: Style.marginM
|
|
|
|
// No media player detected
|
|
ColumnLayout {
|
|
id: fallback
|
|
|
|
visible: !main.visible
|
|
spacing: Style.marginS
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
}
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
|
|
ColumnLayout {
|
|
anchors.centerIn: parent
|
|
spacing: Style.marginL
|
|
|
|
Item {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
Layout.preferredWidth: Style.fontSizeXXXL * 4
|
|
Layout.preferredHeight: Style.fontSizeXXXL * 4
|
|
|
|
// Pulsating audio circles (background)
|
|
Repeater {
|
|
model: 3
|
|
Rectangle {
|
|
anchors.centerIn: parent
|
|
width: parent.width * (1.0 + index * 0.2)
|
|
height: width
|
|
radius: width / 2
|
|
color: "transparent"
|
|
border.color: Color.mOnSurfaceVariant
|
|
border.width: 2
|
|
opacity: 0
|
|
|
|
SequentialAnimation on opacity {
|
|
running: true
|
|
loops: Animation.Infinite
|
|
PauseAnimation {
|
|
duration: index * 600
|
|
}
|
|
NumberAnimation {
|
|
from: 1.0
|
|
to: 0
|
|
duration: 2000
|
|
easing.type: Easing.OutQuad
|
|
}
|
|
}
|
|
|
|
SequentialAnimation on scale {
|
|
running: true
|
|
loops: Animation.Infinite
|
|
PauseAnimation {
|
|
duration: index * 600
|
|
}
|
|
NumberAnimation {
|
|
from: 0.5
|
|
to: 1.2
|
|
duration: 2000
|
|
easing.type: Easing.OutQuad
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Spinning disc
|
|
NIcon {
|
|
anchors.centerIn: parent
|
|
icon: "disc"
|
|
pointSize: Style.fontSizeXXXL * 3
|
|
color: Color.mOnSurfaceVariant
|
|
|
|
RotationAnimator on rotation {
|
|
from: 0
|
|
to: 360
|
|
duration: 8000
|
|
loops: Animation.Infinite
|
|
running: true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Descriptive text
|
|
ColumnLayout {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
spacing: Style.marginXS
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
}
|
|
}
|
|
|
|
// MediaPlayer Main Content
|
|
ColumnLayout {
|
|
id: main
|
|
|
|
visible: MediaService.currentPlayer && MediaService.canPlay
|
|
spacing: Style.marginS
|
|
|
|
// Spacer to push content down
|
|
Item {
|
|
Layout.preferredHeight: Style.marginM
|
|
}
|
|
|
|
// Metadata at the bottom left
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
Layout.alignment: Qt.AlignLeft
|
|
spacing: Style.marginXS
|
|
|
|
NText {
|
|
visible: MediaService.trackTitle !== ""
|
|
text: MediaService.trackTitle
|
|
pointSize: Style.fontSizeL
|
|
font.weight: Style.fontWeightBold
|
|
elide: Text.ElideRight
|
|
wrapMode: Text.Wrap
|
|
maximumLineCount: 2
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
NText {
|
|
visible: MediaService.trackArtist !== ""
|
|
text: MediaService.trackArtist
|
|
color: Color.mPrimary
|
|
pointSize: Style.fontSizeS
|
|
elide: Text.ElideRight
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
NText {
|
|
visible: MediaService.trackAlbum !== ""
|
|
text: MediaService.trackAlbum
|
|
color: Color.mOnSurfaceVariant
|
|
pointSize: Style.fontSizeM
|
|
elide: Text.ElideRight
|
|
Layout.fillWidth: true
|
|
}
|
|
}
|
|
|
|
// Progress slider
|
|
Item {
|
|
id: progressWrapper
|
|
visible: (MediaService.currentPlayer && MediaService.trackLength > 0)
|
|
Layout.fillWidth: true
|
|
height: Style.baseWidgetSize * 0.5
|
|
|
|
property real localSeekRatio: -1
|
|
property real lastSentSeekRatio: -1
|
|
property real seekEpsilon: 0.01
|
|
property real progressRatio: {
|
|
if (!MediaService.currentPlayer || MediaService.trackLength <= 0)
|
|
return 0
|
|
const r = MediaService.currentPosition / MediaService.trackLength
|
|
if (isNaN(r) || !isFinite(r))
|
|
return 0
|
|
return Math.max(0, Math.min(1, r))
|
|
}
|
|
property real effectiveRatio: (MediaService.isSeeking && localSeekRatio >= 0) ? Math.max(0, Math.min(1, localSeekRatio)) : progressRatio
|
|
|
|
Timer {
|
|
id: seekDebounce
|
|
interval: 75
|
|
repeat: false
|
|
onTriggered: {
|
|
if (MediaService.isSeeking && progressWrapper.localSeekRatio >= 0) {
|
|
const next = Math.max(0, Math.min(1, progressWrapper.localSeekRatio))
|
|
if (progressWrapper.lastSentSeekRatio < 0 || Math.abs(next - progressWrapper.lastSentSeekRatio) >= progressWrapper.seekEpsilon) {
|
|
MediaService.seekByRatio(next)
|
|
progressWrapper.lastSentSeekRatio = next
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
NSlider {
|
|
id: progressSlider
|
|
anchors.fill: parent
|
|
from: 0
|
|
to: 1
|
|
stepSize: 0
|
|
snapAlways: false
|
|
enabled: MediaService.trackLength > 0 && MediaService.canSeek
|
|
heightRatio: 0.6
|
|
|
|
onMoved: {
|
|
progressWrapper.localSeekRatio = value
|
|
seekDebounce.restart()
|
|
}
|
|
onPressedChanged: {
|
|
if (pressed) {
|
|
MediaService.isSeeking = true
|
|
progressWrapper.localSeekRatio = value
|
|
MediaService.seekByRatio(value)
|
|
progressWrapper.lastSentSeekRatio = value
|
|
} else {
|
|
seekDebounce.stop()
|
|
MediaService.seekByRatio(value)
|
|
MediaService.isSeeking = false
|
|
progressWrapper.localSeekRatio = -1
|
|
progressWrapper.lastSentSeekRatio = -1
|
|
}
|
|
}
|
|
}
|
|
|
|
Binding {
|
|
target: progressSlider
|
|
property: "value"
|
|
value: progressWrapper.progressRatio
|
|
when: !MediaService.isSeeking
|
|
}
|
|
}
|
|
|
|
// Spacer to push media controls down
|
|
Item {
|
|
Layout.preferredHeight: Style.marginL
|
|
}
|
|
|
|
// Media controls
|
|
RowLayout {
|
|
spacing: Style.marginS
|
|
Layout.fillWidth: true
|
|
Layout.alignment: Qt.AlignHCenter
|
|
|
|
NIconButton {
|
|
icon: "media-prev"
|
|
visible: MediaService.canGoPrevious
|
|
onClicked: MediaService.canGoPrevious ? MediaService.previous() : {}
|
|
}
|
|
|
|
NIconButton {
|
|
icon: MediaService.isPlaying ? "media-pause" : "media-play"
|
|
visible: (MediaService.canPlay || MediaService.canPause)
|
|
onClicked: (MediaService.canPlay || MediaService.canPause) ? MediaService.playPause() : {}
|
|
}
|
|
|
|
NIconButton {
|
|
icon: "media-next"
|
|
visible: MediaService.canGoNext
|
|
onClicked: MediaService.canGoNext ? MediaService.next() : {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|