quickshell: spinning vinyl media art with cava spectrum ring
This commit is contained in:
parent
7e8275a654
commit
8dbdb42841
1 changed files with 162 additions and 14 deletions
|
|
@ -181,6 +181,27 @@ in
|
|||
esac
|
||||
${pkgs.curl}/bin/curl -sf --max-time 10 "https://api.open-meteo.com/v1/forecast?latitude=$lat&longitude=$lon&daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset&timezone=auto&forecast_days=7"
|
||||
'';
|
||||
|
||||
# cava in raw mode: one newline-terminated frame per tick, bars as
|
||||
# semicolon-separated 0-100 ints — trivially parsed by SplitParser.
|
||||
# Drives the media card's spectrum ring. Only spawned while the
|
||||
# calendar popup is open, so nothing captures audio at idle.
|
||||
cavaConfig = pkgs.writeText "cava-quickshell.conf" ''
|
||||
[general]
|
||||
framerate = 30
|
||||
bars = 24
|
||||
autosens = 1
|
||||
|
||||
[input]
|
||||
method = pipewire
|
||||
source = auto
|
||||
|
||||
[output]
|
||||
method = raw
|
||||
data_format = ascii
|
||||
ascii_max_range = 100
|
||||
channels = mono
|
||||
'';
|
||||
in {
|
||||
"quickshell/qmldir" = {
|
||||
onChange = qsRestart;
|
||||
|
|
@ -478,6 +499,8 @@ in
|
|||
// Recoloured to the theme accent at draw time (ColorOverlay),
|
||||
// so the stock blue/grey snowflake artwork is fine here.
|
||||
readonly property string nixLogo: "${pkgs.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg"
|
||||
readonly property string cava: "${pkgs.cava}/bin/cava"
|
||||
readonly property string cavaConfig: "${cavaConfig}"
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
|
@ -3796,6 +3819,34 @@ in
|
|||
return null;
|
||||
}
|
||||
|
||||
// Live spectrum for the media cards' rings. cava runs only
|
||||
// while this popup is open AND something is playing, so it
|
||||
// isn't capturing audio in the background. levels is a
|
||||
// barCount-long array of 0-100 ints, one frame per tick.
|
||||
readonly property int barCount: 24
|
||||
property var levels: []
|
||||
|
||||
Process {
|
||||
id: cavaProc
|
||||
running: calPopup.open && calPopup.players.length > 0
|
||||
command: [Commands.cava, "-p", Commands.cavaConfig]
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
let parts = data.split(";");
|
||||
let out = [];
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (parts[i] === "") continue;
|
||||
out.push(parseInt(parts[i]) || 0);
|
||||
}
|
||||
if (out.length > 0) calPopup.levels = out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bars decay to flat when cava stops, so a paused card
|
||||
// doesn't keep the last frame frozen around the disc.
|
||||
onOpenChanged: if (!open) levels = []
|
||||
|
||||
Row {
|
||||
id: calRow
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
|
@ -3992,23 +4043,120 @@ in
|
|||
width: parent.width
|
||||
spacing: 12
|
||||
|
||||
ClippingRectangle {
|
||||
// Vinyl: cover art is the disc label,
|
||||
// spinning while playback runs, ringed
|
||||
// by cava's spectrum. Kept at 128 so
|
||||
// the row's text column is unaffected.
|
||||
Item {
|
||||
id: vinyl
|
||||
width: 128; height: 128
|
||||
radius: Theme.radiusSmall
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Theme.base02
|
||||
SIcon {
|
||||
anchors.centerIn: parent
|
||||
visible: albumArt.status !== Image.Ready
|
||||
text: "music"
|
||||
color: Theme.base04
|
||||
font.pixelSize: 48
|
||||
|
||||
readonly property real discSize: 86
|
||||
readonly property real ringR: 47
|
||||
readonly property bool spinning: mediaCard.modelData.isPlaying
|
||||
|
||||
// Spectrum ring. Each bar sits with
|
||||
// its base on the ring and grows
|
||||
// outward, then is rotated about the
|
||||
// disc centre into its slot.
|
||||
Repeater {
|
||||
model: calPopup.barCount
|
||||
Rectangle {
|
||||
id: eqBar
|
||||
required property int index
|
||||
readonly property real lvl:
|
||||
index < calPopup.levels.length
|
||||
? calPopup.levels[index] / 100 : 0
|
||||
width: 3
|
||||
radius: 1.5
|
||||
height: 3 + lvl * 13
|
||||
color: Theme.base0C
|
||||
opacity: 0.35 + lvl * 0.65
|
||||
x: vinyl.width / 2 - width / 2
|
||||
y: vinyl.height / 2 - vinyl.ringR - height
|
||||
transform: Rotation {
|
||||
origin.x: eqBar.width / 2
|
||||
origin.y: vinyl.ringR + eqBar.height
|
||||
angle: eqBar.index * (360 / calPopup.barCount)
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation { duration: 90; easing.type: Easing.OutQuad }
|
||||
}
|
||||
}
|
||||
}
|
||||
Image {
|
||||
id: albumArt
|
||||
anchors.fill: parent
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
source: mediaCard.modelData.trackArtUrl
|
||||
|
||||
ClippingRectangle {
|
||||
id: disc
|
||||
anchors.centerIn: parent
|
||||
width: vinyl.discSize
|
||||
height: vinyl.discSize
|
||||
radius: width / 2
|
||||
color: Theme.base02
|
||||
|
||||
SIcon {
|
||||
anchors.centerIn: parent
|
||||
visible: albumArt.status !== Image.Ready
|
||||
text: "music"
|
||||
color: Theme.base04
|
||||
font.pixelSize: 30
|
||||
}
|
||||
Image {
|
||||
id: albumArt
|
||||
anchors.fill: parent
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
source: mediaCard.modelData.trackArtUrl
|
||||
}
|
||||
|
||||
// paused (not stopped) so resuming
|
||||
// picks up at the current angle
|
||||
// instead of snapping back to 0.
|
||||
NumberAnimation on rotation {
|
||||
from: 0; to: 360
|
||||
duration: 8000
|
||||
loops: Animation.Infinite
|
||||
running: true
|
||||
paused: !vinyl.spinning
|
||||
}
|
||||
}
|
||||
|
||||
// Spindle hole — outside the disc so
|
||||
// it stays put while the label turns.
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 14; height: 14
|
||||
radius: 7
|
||||
color: Theme.cardBg
|
||||
border.width: 1
|
||||
border.color: Theme.base03
|
||||
}
|
||||
|
||||
// Source badge. MPRIS identity is the
|
||||
// browser, not the site, so YouTube is
|
||||
// inferred from the art host instead.
|
||||
Rectangle {
|
||||
id: sourceBadge
|
||||
readonly property string glyph: {
|
||||
let ident = (mediaCard.modelData.identity || "").toLowerCase();
|
||||
let art = (mediaCard.modelData.trackArtUrl || "").toLowerCase();
|
||||
if (art.indexOf("ytimg") >= 0 || ident.indexOf("youtube") >= 0) return "youtube";
|
||||
if (ident.indexOf("mpv") >= 0 || ident.indexOf("vlc") >= 0) return "video";
|
||||
return "";
|
||||
}
|
||||
visible: sourceBadge.glyph !== ""
|
||||
width: 22; height: 22
|
||||
radius: 11
|
||||
color: Theme.cardBg
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: 6
|
||||
SIcon {
|
||||
anchors.centerIn: parent
|
||||
text: sourceBadge.glyph
|
||||
color: Theme.base05
|
||||
font.pixelSize: 14
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue