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
|
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"
|
${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 {
|
in {
|
||||||
"quickshell/qmldir" = {
|
"quickshell/qmldir" = {
|
||||||
onChange = qsRestart;
|
onChange = qsRestart;
|
||||||
|
|
@ -478,6 +499,8 @@ in
|
||||||
// Recoloured to the theme accent at draw time (ColorOverlay),
|
// Recoloured to the theme accent at draw time (ColorOverlay),
|
||||||
// so the stock blue/grey snowflake artwork is fine here.
|
// 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 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;
|
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 {
|
Row {
|
||||||
id: calRow
|
id: calRow
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
@ -3992,23 +4043,120 @@ in
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: 12
|
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
|
width: 128; height: 128
|
||||||
radius: Theme.radiusSmall
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
color: Theme.base02
|
|
||||||
SIcon {
|
readonly property real discSize: 86
|
||||||
anchors.centerIn: parent
|
readonly property real ringR: 47
|
||||||
visible: albumArt.status !== Image.Ready
|
readonly property bool spinning: mediaCard.modelData.isPlaying
|
||||||
text: "music"
|
|
||||||
color: Theme.base04
|
// Spectrum ring. Each bar sits with
|
||||||
font.pixelSize: 48
|
// 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
|
ClippingRectangle {
|
||||||
anchors.fill: parent
|
id: disc
|
||||||
fillMode: Image.PreserveAspectCrop
|
anchors.centerIn: parent
|
||||||
source: mediaCard.modelData.trackArtUrl
|
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