quickshell: tone down vinyl fallback, gate ring on playback, widen stream match

This commit is contained in:
rope 2026-07-30 16:00:29 +01:00
parent 88ab7944f4
commit 2b73b1e470

View file

@ -3853,13 +3853,38 @@ in
// widget. null when no stream matches. // widget. null when no stream matches.
function streamFor(player) { function streamFor(player) {
if (!player) return null; if (!player) return null;
let id = (player.identity || "").toLowerCase(); // Apps are inconsistent about which property carries their
// name Spotify doesn't fill application.name the way
// browsers do so try every property that plausibly
// holds it, against both MPRIS names. Candidates shorter
// than 3 chars are skipped: substring matching on those
// matches almost anything.
let want = [];
let add = s => {
s = (s || "").toLowerCase();
if (s.length >= 3) want.push(s);
};
add(player.identity);
add(player.desktopEntry);
let ns = Pipewire.nodes.values; let ns = Pipewire.nodes.values;
for (let i = 0; i < ns.length; i++) { for (let i = 0; i < ns.length; i++) {
let n = ns[i]; let n = ns[i];
if (!n.isStream || !n.audio) continue; if (!n.isStream || !n.audio) continue;
let an = (n.properties["application.name"] || "").toLowerCase(); let p = n.properties;
if (an && (an === id || an.includes(id) || id.includes(an))) return n; let cands = [
p["application.name"],
p["node.name"],
p["application.process.binary"]
];
for (let c = 0; c < cands.length; c++) {
let an = (cands[c] || "").toLowerCase();
if (an.length < 3) continue;
for (let w = 0; w < want.length; w++) {
if (an === want[w] || an.includes(want[w]) || want[w].includes(an))
return n;
}
}
} }
return null; return null;
} }
@ -4137,8 +4162,14 @@ in
Rectangle { Rectangle {
id: eqBar id: eqBar
required property int index required property int index
// cava hears the whole output mix,
// not this stream, so a paused
// card must flatten its own ring
// otherwise every card dances to
// whatever else is playing.
readonly property real lvl: readonly property real lvl:
index < calPopup.levels.length (mediaCard.modelData.isPlaying
&& index < calPopup.levels.length)
? calPopup.levels[index] / 100 : 0 ? calPopup.levels[index] / 100 : 0
width: 3 width: 3
radius: 1.5 radius: 1.5
@ -4164,67 +4195,36 @@ in
width: vinyl.discSize width: vinyl.discSize
height: vinyl.discSize height: vinyl.discSize
radius: width / 2 radius: width / 2
color: Theme.base02 color: Theme.base01
// Art-less fallback: an actual record // Art-less fallback: a dark record with
// rather than a flat placeholder, so // faint grooves. Deliberately low
// a source with no MediaSession // contrast this is a placeholder, it
// artwork (Jellyfin's web player, a // shouldn't out-shout the actual
// plain <video> tag) still reads as // covers it stands in for. Grooves are
// deliberate. Grooves only turn // eccentric by 1px so they visibly
// visibly because they're eccentric // turn; concentric rings look static
// to the disc concentric rings // while spinning.
// would look static while spinning.
Item { Item {
id: blankLabel id: blankLabel
anchors.fill: parent anchors.fill: parent
visible: albumArt.status !== Image.Ready visible: albumArt.status !== Image.Ready
Repeater { Repeater {
model: 3 model: 4
Rectangle { Rectangle {
required property int index required property int index
anchors.centerIn: parent anchors.centerIn: parent
anchors.horizontalCenterOffset: 1 anchors.horizontalCenterOffset: 1
width: vinyl.discSize - 10 - index * 14 width: vinyl.discSize - 8 - index * 11
height: width height: width
radius: width / 2 radius: width / 2
color: "transparent" color: "transparent"
border.width: 1 border.width: 1
border.color: Theme.base03 border.color: Theme.base03
opacity: 0.5 opacity: 0.18
} }
} }
Rectangle {
anchors.centerIn: parent
width: 40; height: 40
radius: 20
color: Theme.base0C
opacity: 0.25
}
// The site's own favicon, if it
// has one. Kept small and centred
// rather than filling the disc
// most are 16-32px and look awful
// upscaled to 86.
Image {
id: faviconImg
anchors.centerIn: parent
width: 28; height: 28
fillMode: Image.PreserveAspectFit
asynchronous: true
smooth: true
mipmap: true
visible: status === Image.Ready
source: mediaCard.faviconUrl
}
SIcon {
anchors.centerIn: parent
visible: faviconImg.status !== Image.Ready
text: sourceBadge.glyph !== "" ? sourceBadge.glyph : "music"
color: Theme.base05
font.pixelSize: 18
}
} }
Image { Image {
@ -4247,10 +4247,54 @@ in
} }
} }
// Spindle hole outside the disc so // Label content sits outside the rotating
// it stays put while the label turns. // disc so the favicon stays upright
// only the grooves turn. Doubles as the
// record's centre label, so no spindle
// hole is drawn over it.
Item {
anchors.centerIn: parent
width: 38; height: 38
visible: albumArt.status !== Image.Ready
Rectangle {
anchors.fill: parent
radius: width / 2
color: Theme.base02
}
// Circular crop: most favicons are a
// square plate on their own dark
// background, which fights the disc.
ClippingRectangle {
anchors.centerIn: parent
width: 24; height: 24
radius: width / 2
color: "transparent"
Image {
id: faviconImg
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
asynchronous: true
smooth: true
mipmap: true
visible: status === Image.Ready
source: mediaCard.faviconUrl
}
}
SIcon {
anchors.centerIn: parent
visible: faviconImg.status !== Image.Ready
text: sourceBadge.glyph !== "" ? sourceBadge.glyph : "music"
color: Theme.base04
font.pixelSize: 15
}
}
// Spindle hole, only over real artwork
// the blank label already owns the centre.
Rectangle { Rectangle {
anchors.centerIn: parent anchors.centerIn: parent
visible: albumArt.status === Image.Ready
width: 14; height: 14 width: 14; height: 14
radius: 7 radius: 7
color: Theme.cardBg color: Theme.cardBg