144 lines
4.5 KiB
QML
144 lines
4.5 KiB
QML
import QtQuick
|
|
import Quickshell.Services.Mpris
|
|
import qs.Common
|
|
import qs.Services
|
|
import qs.Widgets
|
|
|
|
Item {
|
|
id: widget
|
|
|
|
readonly property var activePlayer: MprisController.activePlayer
|
|
readonly property bool isPlaying: activePlayer !== null && activePlayer !== undefined && activePlayer.playbackState === MprisPlaybackState.Playing
|
|
// Follow whatever is actually coming out of this machine - games do not
|
|
// publish MPRIS, and a remote MPRIS player produces no local audio at all.
|
|
readonly property bool live: visible
|
|
readonly property bool hasSignal: {
|
|
const values = CavaService.values;
|
|
if (!values || values.length < 6)
|
|
return false;
|
|
|
|
for (let i = 0; i < values.length; i++) {
|
|
if (values[i] > 0)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Cava reports six bands; mirroring them reads as a classic visualiser.
|
|
// CavaService.values is a list<int>, so follow its change signal rather than
|
|
// binding to it (this is how the shell's own visualiser reads it).
|
|
property var bands: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
|
|
|
function refreshBands() {
|
|
const values = CavaService.values;
|
|
if (!values || values.length < 6) {
|
|
widget.bands = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
|
return;
|
|
}
|
|
|
|
const left = [values[5], values[4], values[3], values[2], values[1], values[0]];
|
|
widget.bands = left.concat([values[0], values[1], values[2], values[3], values[4], values[5]]);
|
|
}
|
|
|
|
Connections {
|
|
target: CavaService
|
|
enabled: widget.live
|
|
|
|
function onValuesChanged() {
|
|
widget.refreshBands();
|
|
}
|
|
}
|
|
|
|
onLiveChanged: {
|
|
if (!widget.live)
|
|
widget.bands = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
|
else
|
|
widget.refreshBands();
|
|
}
|
|
|
|
// Holding a Ref keeps the cava process alive only while this widget is up.
|
|
Loader {
|
|
active: widget.live
|
|
|
|
sourceComponent: Component {
|
|
Ref {
|
|
service: CavaService
|
|
}
|
|
}
|
|
}
|
|
|
|
Row {
|
|
id: barRow
|
|
|
|
anchors.top: parent.top
|
|
anchors.left: parent.left
|
|
width: parent.width
|
|
height: parent.height - trackLabel.height - Theme.spacingS
|
|
spacing: Math.max(2, width * 0.012)
|
|
|
|
readonly property real barWidth: (width - spacing * 11) / 12
|
|
|
|
Repeater {
|
|
model: widget.bands
|
|
|
|
// A positioner manages x, so the growing bar lives inside a
|
|
// full-height cell rather than anchoring to the Row itself.
|
|
delegate: Item {
|
|
required property var modelData
|
|
|
|
readonly property real level: {
|
|
if (!widget.live)
|
|
return 0;
|
|
|
|
// Square-root curve, as the shell's own visualiser uses -
|
|
// linear levels look flat for anything but loud passages.
|
|
const raw = Math.max(0, Math.min(100, modelData || 0));
|
|
return Math.sqrt(raw * 0.01);
|
|
}
|
|
|
|
width: barRow.barWidth
|
|
height: barRow.height
|
|
|
|
Rectangle {
|
|
anchors.bottom: parent.bottom
|
|
width: parent.width
|
|
height: Math.max(3, parent.height * parent.level)
|
|
radius: width / 2
|
|
color: Theme.primary
|
|
opacity: 0.55 + parent.level * 0.45
|
|
|
|
Behavior on height {
|
|
NumberAnimation {
|
|
duration: 90
|
|
easing.type: Easing.OutQuad
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
StyledText {
|
|
id: trackLabel
|
|
|
|
anchors.bottom: parent.bottom
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
horizontalAlignment: Text.AlignHCenter
|
|
text: {
|
|
if (!CavaService.cavaAvailable)
|
|
return "cava is not installed";
|
|
if (widget.isPlaying) {
|
|
const title = widget.activePlayer.trackTitle || "";
|
|
const artist = widget.activePlayer.trackArtist || "";
|
|
return artist !== "" ? title + " — " + artist : title;
|
|
}
|
|
|
|
return widget.hasSignal ? "Playing" : "No audio playing";
|
|
}
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceVariantText
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|