Watch
1
0
Fork
You've already forked souveraine
0
souveraine/surfaces/quickshell/ii-base/modules/ii/mediaControls/PlayerControl.qml
Fimeg 8f42fc953d publish: the public projection begins here
This is a projection, not a development branch. The tree above was constructed
from the internal source named below under a manifest that decides which paths
may leave, then scanned as a whole tree rather than as a series of patches, and
only then published.

Public history starts here because the history before it was not admissible,
and neither was the tree. What used to stand in this repository included a
rescue copy of another machine, a directory of phone handoffs, deployment
wired to one house, and a submodule pointing at a forge no stranger can reach.
None of that was ever the product. It stays in the private forge, which is
allowed to hold the whole working organism, and this is what was deliberately
sent out instead.

Three mechanisms produced this tree, in decreasing order of trust. A top-level
path the manifest does not name never arrives at all, which is the one that
catches directories nobody has thought of yet. Named internal files inside
admitted roots are dropped. A short, reviewed table replaces deployment
defaults that a public build must not carry -- an endpoint aimed at one LAN, a
VPN profile belonging to one phone, packaging built from one checkout path.

Everything after this commit is an ordinary publication with the same three
trailers, so a force push stops being routine and starts meaning that
something deliberate happened. The trailers bind the projection to its source
without pretending the public SHA is the private one: same lineage, different
tree, and the record says so.

Source-Sha: 8f27b1e76a8fef560a336aba18e6990713ff1047
Policy-Sha: 6b261d2f3e6e1fb19874846ba4bb1dfe15565d25b8618c1c1afba0419c101d27
Tree-Digest: 18ec3563c5e5ef9a414993a9f6734b251ff9ed3cd56eebdd6cac01e45c6e3067
2026-09-04 15:55:48 -04:00

318 lines
No EOL
13 KiB
QML

pragma ComponentBehavior: Bound
import qs.modules.common
import qs.modules.common.models
import qs.modules.common.widgets
import qs.services
import qs.modules.common.functions
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Services.Mpris
Item { // Player instance
id: root
required property MprisPlayer player
property color artDominantColor: ColorUtils.mix((colorQuantizer?.colors[0] ?? Appearance.colors.colPrimary), Appearance.colors.colPrimaryContainer, 0.8) || Appearance.m3colors.m3secondaryContainer
property list<real> visualizerPoints: []
property real maxVisualizerValue: 1000 // Max value in the data points
property int visualizerSmoothing: 2 // Number of points to average for smoothing
property real radius
// Cover art is downloaded centrally by MprisController (per-player Process
// that runs regardless of panel state), and tracked there as the
// highest-quality variant seen for each (player, track) pair. We just
// read that cache. This survives both:
// - panel close/reopen (we weren't here to receive emissions; the
// central downloader was)
// - players that emit multiple sizes per track (Firefox: 544x544 then
// 60x60 thumbnail; Chromium: several near-identical tmp files) —
// bestArtByPlayer keeps the largest.
// Cache the last non-empty trackTitle/trackArtist. Some players
// (pear-desktop / youtube-music) clear metadata briefly during a track
// change before emitting the new track's values; binding the UI
// directly to player.trackTitle would make the title flash to
// "Untitled" mid-skip and would also invalidate the trackKey, dropping
// the cover image.
property string _stableTitle: ""
property string _stableArtist: ""
Connections {
target: root.player
function onTrackTitleChanged() {
if (root.player?.trackTitle) root._stableTitle = root.player.trackTitle;
}
function onTrackArtistChanged() {
if (root.player?.trackArtist) root._stableArtist = root.player.trackArtist;
}
}
Component.onCompleted: {
if (root.player?.trackTitle) _stableTitle = root.player.trackTitle;
if (root.player?.trackArtist) _stableArtist = root.player.trackArtist;
}
// title|artist only; see MprisController._trackKeyOf for why album is omitted.
readonly property string _trackKey: `${_stableTitle}|${_stableArtist}`
readonly property string displayedArtFilePath: {
const entry = MprisController.getBestArt(player, _trackKey);
return entry ? Qt.resolvedUrl(entry.artFilePath) : "";
}
component TrackChangeButton: RippleButton {
implicitWidth: 24
implicitHeight: 24
property var iconName
colBackground: ColorUtils.transparentize(blendedColors.colSecondaryContainer, 1)
colBackgroundHover: blendedColors.colSecondaryContainerHover
colRipple: blendedColors.colSecondaryContainerActive
contentItem: MaterialSymbol {
iconSize: Appearance.font.pixelSize.huge
fill: 1
horizontalAlignment: Text.AlignHCenter
color: blendedColors.colOnSecondaryContainer
text: iconName
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
}
}
Timer { // Force update for revision
running: root.player?.playbackState == MprisPlaybackState.Playing
interval: Config.options.resources.updateInterval
repeat: true
onTriggered: {
root.player.positionChanged()
}
}
ColorQuantizer {
id: colorQuantizer
source: root.displayedArtFilePath
depth: 0 // 2^0 = 1 color
rescaleSize: 1 // Rescale to 1x1 pixel for faster processing
}
property QtObject blendedColors: AdaptedMaterialScheme {
color: artDominantColor
}
StyledRectangularShadow {
target: background
}
Rectangle { // Background
id: background
anchors.fill: parent
anchors.margins: Appearance.sizes.elevationMargin
color: ColorUtils.applyAlpha(blendedColors.colLayer0, 1)
radius: root.radius
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: background.width
height: background.height
radius: background.radius
}
}
StyledImage {
id: blurredArt
anchors.fill: parent
source: root.displayedArtFilePath
fillMode: Image.PreserveAspectCrop
cache: false
antialiasing: true
asynchronous: true
layer.enabled: true
layer.effect: StyledBlurEffect {
source: blurredArt
}
Rectangle {
anchors.fill: parent
color: ColorUtils.transparentize(blendedColors.colLayer0, 0.3)
radius: root.radius
}
}
WaveVisualizer {
id: visualizerCanvas
anchors.fill: parent
live: root.player?.isPlaying
points: root.visualizerPoints
maxVisualizerValue: root.maxVisualizerValue
smoothing: root.visualizerSmoothing
color: blendedColors.colPrimary
}
RowLayout {
anchors.fill: parent
anchors.margins: 13
spacing: 15
Rectangle { // Art background
id: artBackground
Layout.fillHeight: true
implicitWidth: height
radius: Appearance.rounding.verysmall
color: ColorUtils.transparentize(blendedColors.colLayer1, 0.5)
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: artBackground.width
height: artBackground.height
radius: artBackground.radius
}
}
StyledImage { // Art image
id: mediaArt
property int size: parent.height
anchors.fill: parent
source: root.displayedArtFilePath
fillMode: Image.PreserveAspectCrop
cache: false
antialiasing: true
width: size
height: size
}
}
ColumnLayout { // Info & controls
Layout.fillHeight: true
spacing: 2
StyledText {
id: trackTitle
Layout.fillWidth: true
font.pixelSize: Appearance.font.pixelSize.large
color: blendedColors.colOnLayer0
elide: Text.ElideRight
text: StringUtils.cleanMusicTitle(root._stableTitle) || "Untitled"
animateChange: true
animationDistanceX: 6
animationDistanceY: 0
}
StyledText {
id: trackArtist
Layout.fillWidth: true
font.pixelSize: Appearance.font.pixelSize.smaller
color: blendedColors.colSubtext
elide: Text.ElideRight
text: root._stableArtist
animateChange: true
animationDistanceX: 6
animationDistanceY: 0
}
Item { Layout.fillHeight: true }
Item {
Layout.fillWidth: true
implicitHeight: trackTime.implicitHeight + sliderRow.implicitHeight
StyledText {
id: trackTime
anchors.bottom: sliderRow.top
anchors.bottomMargin: 5
anchors.left: parent.left
font.pixelSize: Appearance.font.pixelSize.small
color: blendedColors.colSubtext
elide: Text.ElideRight
text: `${StringUtils.friendlyTimeForSeconds(root.player?.position)} / ${StringUtils.friendlyTimeForSeconds(root.player?.length)}`
}
RowLayout {
id: sliderRow
anchors {
bottom: parent.bottom
left: parent.left
right: parent.right
}
TrackChangeButton {
iconName: "skip_previous"
downAction: () => root.player?.previous()
}
Item {
id: progressBarContainer
Layout.fillWidth: true
implicitHeight: Math.max(sliderLoader.implicitHeight, progressBarLoader.implicitHeight)
Loader {
id: sliderLoader
anchors.fill: parent
active: root.player?.canSeek ?? false
sourceComponent: StyledSlider {
configuration: StyledSlider.Configuration.Wavy
highlightColor: blendedColors.colPrimary
trackColor: blendedColors.colSecondaryContainer
handleColor: blendedColors.colPrimary
value: root.player?.position / root.player?.length
onMoved: {
root.player.position = value * root.player.length;
}
}
}
Loader {
id: progressBarLoader
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
right: parent.right
}
active: !(root.player?.canSeek ?? false)
sourceComponent: StyledProgressBar {
wavy: root.player?.isPlaying
highlightColor: blendedColors.colPrimary
trackColor: blendedColors.colSecondaryContainer
value: root.player?.position / root.player?.length
}
}
}
TrackChangeButton {
iconName: "skip_next"
downAction: () => root.player?.next()
}
}
RippleButton {
id: playPauseButton
anchors.right: parent.right
anchors.bottom: sliderRow.top
anchors.bottomMargin: 5
property real size: 44
implicitWidth: size
implicitHeight: size
downAction: () => root.player.togglePlaying();
buttonRadius: root.player?.isPlaying ? Appearance?.rounding.normal : size / 2
colBackground: root.player?.isPlaying ? blendedColors.colPrimary : blendedColors.colSecondaryContainer
colBackgroundHover: root.player?.isPlaying ? blendedColors.colPrimaryHover : blendedColors.colSecondaryContainerHover
colRipple: root.player?.isPlaying ? blendedColors.colPrimaryActive : blendedColors.colSecondaryContainerActive
contentItem: MaterialSymbol {
iconSize: Appearance.font.pixelSize.huge
fill: 1
horizontalAlignment: Text.AlignHCenter
color: root.player?.isPlaying ? blendedColors.colOnPrimary : blendedColors.colOnSecondaryContainer
text: root.player?.isPlaying ? "pause" : "play_arrow"
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
}
}
}
}
}
}
}