fedora
This commit is contained in:
@@ -0,0 +1,576 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.MainScreen
|
||||
import qs.Services.Media
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
import qs.Widgets.AudioSpectrum
|
||||
|
||||
SmartPanel {
|
||||
id: root
|
||||
|
||||
preferredWidth: Math.round((root.isSideBySide ? 480 : 360) * Style.uiScaleRatio)
|
||||
// Fallback only; SmartPanel uses panelContent.contentPreferredHeight when set.
|
||||
preferredHeight: Math.round((root.compactMode ? 240 : 400) * Style.uiScaleRatio)
|
||||
|
||||
property var mediaMiniSettings: {
|
||||
const widget = BarService.lookupWidget("MediaMini", screen?.name);
|
||||
return widget ? widget.widgetSettings : null;
|
||||
}
|
||||
|
||||
function refreshMediaMiniSettings() {
|
||||
const widget = BarService.lookupWidget("MediaMini", screen?.name);
|
||||
root.mediaMiniSettings = widget ? widget.widgetSettings : null;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: BarService
|
||||
function onActiveWidgetsChanged() {
|
||||
root.refreshMediaMiniSettings();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Settings
|
||||
function onSettingsSaved() {
|
||||
root.refreshMediaMiniSettings();
|
||||
}
|
||||
}
|
||||
|
||||
readonly property string visualizerType: (mediaMiniSettings && mediaMiniSettings.visualizerType !== undefined) ? mediaMiniSettings.visualizerType : "linear"
|
||||
readonly property bool showArtistFirst: !!(mediaMiniSettings && mediaMiniSettings.showArtistFirst !== undefined ? mediaMiniSettings.showArtistFirst : true)
|
||||
readonly property bool showAlbumArt: !!(mediaMiniSettings && mediaMiniSettings.panelShowAlbumArt !== undefined ? mediaMiniSettings.panelShowAlbumArt : true)
|
||||
readonly property bool showVisualizer: !!(mediaMiniSettings && mediaMiniSettings.showVisualizer !== undefined ? mediaMiniSettings.showVisualizer : true)
|
||||
readonly property bool compactMode: !!(mediaMiniSettings && mediaMiniSettings.compactMode !== undefined ? mediaMiniSettings.compactMode : false)
|
||||
readonly property string scrollingMode: (mediaMiniSettings && mediaMiniSettings.scrollingMode !== undefined) ? mediaMiniSettings.scrollingMode : "hover"
|
||||
|
||||
readonly property bool isSideBySide: root.compactMode && root.showAlbumArt
|
||||
|
||||
readonly property bool needsSpectrum: root.showVisualizer && root.visualizerType !== "" && root.visualizerType !== "none" && root.isPanelOpen
|
||||
|
||||
onNeedsSpectrumChanged: {
|
||||
if (root.needsSpectrum) {
|
||||
SpectrumService.registerComponent("mediaplayerpanel");
|
||||
} else {
|
||||
SpectrumService.unregisterComponent("mediaplayerpanel");
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.needsSpectrum) {
|
||||
SpectrumService.registerComponent("mediaplayerpanel");
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
SpectrumService.unregisterComponent("mediaplayerpanel");
|
||||
}
|
||||
|
||||
panelContent: Item {
|
||||
id: playerContent
|
||||
anchors.fill: parent
|
||||
|
||||
// implicitHeight + mainLayout vertical anchors.margins (each Style.marginL).
|
||||
readonly property real contentPreferredHeight: {
|
||||
const m = mainLayout.implicitHeight + 2 * Style.marginL;
|
||||
const scale = Style.uiScaleRatio;
|
||||
if (root.compactMode)
|
||||
return Math.max(m, 240 * scale);
|
||||
if (!root.showAlbumArt)
|
||||
return Math.max(m, 300 * scale);
|
||||
return Math.max(m, 260 * scale);
|
||||
}
|
||||
|
||||
property Component visualizerSource: {
|
||||
switch (root.visualizerType) {
|
||||
case "linear":
|
||||
return linearComponent;
|
||||
case "mirrored":
|
||||
return mirroredComponent;
|
||||
case "wave":
|
||||
return waveComponent;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: mainLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: root.compactMode ? Style.marginL : Style.marginM
|
||||
z: 1
|
||||
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: headerRow.implicitHeight + Style.margin2M
|
||||
|
||||
RowLayout {
|
||||
id: headerRow
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
NIcon {
|
||||
icon: "music"
|
||||
pointSize: Style.fontSizeL
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("common.media-player")
|
||||
font.weight: Style.fontWeightBold
|
||||
pointSize: Style.fontSizeL
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
radius: Style.radiusS
|
||||
color: playerSelectorMouse.containsMouse ? Color.mPrimary : "transparent"
|
||||
implicitWidth: playerRow.implicitWidth + Style.marginM
|
||||
implicitHeight: Style.baseWidgetSize * 0.8
|
||||
visible: MediaService.getAvailablePlayers().length > 1
|
||||
|
||||
RowLayout {
|
||||
id: playerRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginXS
|
||||
|
||||
NText {
|
||||
text: MediaService.currentPlayer ? MediaService.currentPlayer.identity : "Select Player"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: playerSelectorMouse.containsMouse ? Color.mOnPrimary : Color.mOnSurfaceVariant
|
||||
}
|
||||
NIcon {
|
||||
icon: "chevron-down"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: playerSelectorMouse.containsMouse ? Color.mOnPrimary : Color.mOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: playerSelectorMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: playerContextMenu.open()
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: playerContextMenu
|
||||
x: 0
|
||||
y: parent.height
|
||||
width: 160
|
||||
padding: Style.marginS
|
||||
|
||||
background: Rectangle {
|
||||
color: Color.mSurfaceVariant
|
||||
border.color: Color.mOutline
|
||||
border.width: Style.borderS
|
||||
radius: Style.iRadiusM
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
Repeater {
|
||||
model: MediaService.getAvailablePlayers()
|
||||
delegate: Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 30
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: itemMouse.containsMouse ? Color.mPrimary : "transparent"
|
||||
radius: Style.iRadiusS
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS
|
||||
spacing: Style.marginS
|
||||
|
||||
NIcon {
|
||||
visible: MediaService.currentPlayer && MediaService.currentPlayer.identity === modelData.identity
|
||||
icon: "check"
|
||||
color: itemMouse.containsMouse ? Color.mOnPrimary : Color.mPrimary
|
||||
pointSize: Style.fontSizeS
|
||||
}
|
||||
|
||||
NText {
|
||||
text: modelData.identity
|
||||
pointSize: Style.fontSizeS
|
||||
color: itemMouse.containsMouse ? Color.mOnPrimary : Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: itemMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
MediaService.currentPlayer = modelData;
|
||||
playerContextMenu.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: I18n.tr("common.close")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: mediaContentGrid.implicitHeight + Style.marginM + Style.marginM
|
||||
|
||||
// Visualizer background for content area
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
z: 0
|
||||
active: !!(root.needsSpectrum && !root.showAlbumArt)
|
||||
sourceComponent: visualizerSource
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
id: mediaContentGrid
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.leftMargin: root.compactMode ? Style.marginL : Style.marginM
|
||||
anchors.rightMargin: root.compactMode ? Style.marginL : Style.marginM
|
||||
anchors.topMargin: Style.marginM
|
||||
anchors.bottomMargin: Style.marginM
|
||||
columns: root.isSideBySide ? 2 : 1
|
||||
columnSpacing: Style.marginL
|
||||
rowSpacing: root.compactMode ? Style.marginL : Style.marginM
|
||||
|
||||
// Album Art (Vertical in normal, Horizontal in compact)
|
||||
Item {
|
||||
id: albumArtItem
|
||||
readonly property real compactArtSize: Math.round(110 * Style.uiScaleRatio)
|
||||
readonly property bool artSizeKnown: artSizeProbe.status === Image.Ready && artSizeProbe.sourceSize.width > 0 && artSizeProbe.sourceSize.height > 0
|
||||
readonly property real artAspectRatio: artSizeKnown ? artSizeProbe.sourceSize.width / artSizeProbe.sourceSize.height : 1
|
||||
// Non-compact: height from width÷aspect so grid implicit height does not depend on panel height (no layout loop).
|
||||
readonly property real artBoxW: root.compactMode ? compactArtSize : Math.max(parent.width, 1)
|
||||
readonly property real artBoxH: root.compactMode ? compactArtSize : (artBoxW / Math.max(artAspectRatio, 0.001))
|
||||
readonly property real fitArtW: artBoxW / artBoxH > artAspectRatio ? artBoxH * artAspectRatio : artBoxW
|
||||
readonly property real fitArtH: artBoxW / artBoxH > artAspectRatio ? artBoxH : artBoxW / artAspectRatio
|
||||
|
||||
Layout.preferredWidth: fitArtW
|
||||
Layout.preferredHeight: fitArtH
|
||||
Layout.minimumWidth: fitArtW
|
||||
Layout.maximumWidth: fitArtW
|
||||
Layout.minimumHeight: fitArtH
|
||||
Layout.maximumHeight: fitArtH
|
||||
Layout.fillWidth: false
|
||||
Layout.fillHeight: false
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: root.showAlbumArt
|
||||
|
||||
Image {
|
||||
id: artSizeProbe
|
||||
visible: false
|
||||
asynchronous: true
|
||||
source: MediaService.trackArtUrl
|
||||
}
|
||||
|
||||
NImageRounded {
|
||||
anchors.fill: parent
|
||||
radius: root.compactMode ? Style.radiusM : Style.radiusL
|
||||
imagePath: MediaService.trackArtUrl
|
||||
imageFillMode: Image.PreserveAspectCrop
|
||||
fallbackIcon: "disc"
|
||||
fallbackIconSize: root.compactMode ? Style.fontSizeXXXL * 3 : Style.fontSizeXXXL * 6
|
||||
borderWidth: 0
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS
|
||||
z: 2
|
||||
active: !!(root.needsSpectrum && root.showAlbumArt)
|
||||
sourceComponent: visualizerSource
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: controlsLayout
|
||||
Layout.preferredWidth: root.compactMode ? -1 : albumArtItem.width
|
||||
Layout.fillWidth: root.compactMode
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillHeight: root.compactMode
|
||||
spacing: root.compactMode ? Style.marginXS : Style.marginS
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
NScrollText {
|
||||
Layout.fillWidth: true
|
||||
maxWidth: parent.width
|
||||
text: {
|
||||
if (root.showArtistFirst) {
|
||||
return MediaService.trackArtist || (MediaService.trackAlbum || "Unknown Artist");
|
||||
} else {
|
||||
return MediaService.trackTitle || "No Media";
|
||||
}
|
||||
}
|
||||
|
||||
scrollMode: {
|
||||
if (root.scrollingMode === "always")
|
||||
return NScrollText.ScrollMode.Always;
|
||||
if (root.scrollingMode === "hover")
|
||||
return NScrollText.ScrollMode.Hover;
|
||||
return NScrollText.ScrollMode.Never;
|
||||
}
|
||||
fadeExtent: 0.01
|
||||
fadeCornerRadius: Style.radiusM
|
||||
|
||||
delegate: NText {
|
||||
pointSize: root.compactMode ? Style.fontSizeL : Style.fontSizeXL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
horizontalAlignment: root.isSideBySide ? Text.AlignLeft : Text.AlignHCenter
|
||||
elide: Text.ElideNone
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
|
||||
NScrollText {
|
||||
Layout.fillWidth: true
|
||||
maxWidth: parent.width
|
||||
text: {
|
||||
if (root.showArtistFirst) {
|
||||
return MediaService.trackTitle || "No Media";
|
||||
} else {
|
||||
return MediaService.trackArtist || (MediaService.trackAlbum || "Unknown Artist");
|
||||
}
|
||||
}
|
||||
|
||||
scrollMode: {
|
||||
if (root.scrollingMode === "always")
|
||||
return NScrollText.ScrollMode.Always;
|
||||
if (root.scrollingMode === "hover")
|
||||
return NScrollText.ScrollMode.Hover;
|
||||
return NScrollText.ScrollMode.Never;
|
||||
}
|
||||
fadeExtent: 0.01
|
||||
fadeCornerRadius: Style.radiusM
|
||||
|
||||
delegate: NText {
|
||||
pointSize: root.compactMode ? Style.fontSizeS : Style.fontSizeM
|
||||
color: Color.mOnSurfaceVariant
|
||||
horizontalAlignment: root.isSideBySide ? Text.AlignLeft : Text.AlignHCenter
|
||||
elide: Text.ElideNone
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: progressWrapper
|
||||
visible: (MediaService.currentPlayer && MediaService.trackLength > 0)
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: progressColumn.implicitHeight
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: progressColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: 2
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: root.compactMode ? (Style.baseWidgetSize * 0.4) : (Style.baseWidgetSize * 0.5)
|
||||
|
||||
NSlider {
|
||||
id: progressSlider
|
||||
anchors.fill: parent
|
||||
from: 0
|
||||
to: 1
|
||||
stepSize: 0
|
||||
snapAlways: false
|
||||
enabled: MediaService.trackLength > 0 && MediaService.canSeek
|
||||
heightRatio: 0.4
|
||||
|
||||
value: (!MediaService.isSeeking) ? progressWrapper.progressRatio : (progressWrapper.localSeekRatio >= 0 ? progressWrapper.localSeekRatio : 0)
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
NText {
|
||||
text: MediaService.positionString || "0:00"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
visible: progressWrapper.visible
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: 0
|
||||
}
|
||||
|
||||
NText {
|
||||
text: MediaService.lengthString || "0:00"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
horizontalAlignment: Text.AlignRight
|
||||
visible: progressWrapper.visible
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: root.isSideBySide ? Style.marginM : Style.marginS
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: root.isSideBySide ? Style.marginL : Style.marginXL
|
||||
|
||||
NIconButton {
|
||||
icon: "media-prev"
|
||||
baseSize: root.compactMode ? (Style.baseWidgetSize * 0.9) : (Style.baseWidgetSize * 1.2)
|
||||
onClicked: MediaService.previous()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
implicitWidth: root.compactMode ? (Style.baseWidgetSize * 1.3) : (Style.baseWidgetSize * 1.8)
|
||||
implicitHeight: root.compactMode ? (Style.baseWidgetSize * 1.3) : (Style.baseWidgetSize * 1.8)
|
||||
radius: root.compactMode ? Style.iRadiusM : Style.iRadiusL
|
||||
color: Color.mPrimary
|
||||
|
||||
NIcon {
|
||||
anchors.centerIn: parent
|
||||
icon: MediaService.isPlaying ? "media-pause" : "media-play"
|
||||
pointSize: root.compactMode ? Style.fontSizeL : Style.fontSizeXXL
|
||||
color: Color.mOnPrimary
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
onEntered: parent.color = Color.mPrimary
|
||||
onClicked: MediaService.playPause()
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "media-next"
|
||||
baseSize: root.compactMode ? (Style.baseWidgetSize * 0.9) : (Style.baseWidgetSize * 1.2)
|
||||
onClicked: MediaService.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Visualizer Components
|
||||
Component {
|
||||
id: linearComponent
|
||||
NLinearSpectrum {
|
||||
width: parent.width - Style.marginS
|
||||
height: 20
|
||||
values: SpectrumService.values
|
||||
fillColor: Color.mPrimary
|
||||
opacity: 0.4
|
||||
barPosition: Settings.getBarPositionForScreen(root.screen?.name)
|
||||
mirrored: Settings.data.audio.spectrumMirrored
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: mirroredComponent
|
||||
NMirroredSpectrum {
|
||||
width: parent.width - Style.marginS
|
||||
height: parent.height - Style.marginS
|
||||
values: SpectrumService.values
|
||||
fillColor: Color.mPrimary
|
||||
opacity: 0.4
|
||||
mirrored: Settings.data.audio.spectrumMirrored
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: waveComponent
|
||||
NWaveSpectrum {
|
||||
width: parent.width - Style.marginS
|
||||
height: parent.height - Style.marginS
|
||||
values: SpectrumService.values
|
||||
fillColor: Color.mPrimary
|
||||
opacity: 0.4
|
||||
mirrored: Settings.data.audio.spectrumMirrored
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user