adds overview, removes old stuff

This commit is contained in:
2026-08-19 20:47:29 +02:00
parent 6a9ae98c26
commit 67d50fb504
75 changed files with 3558 additions and 1691 deletions
@@ -0,0 +1,248 @@
// MediaCavaWidget.qml - cava visualiser + media info + volume in one pill
import QtQuick
import QtQuick.Layouts
import Quickshell.Io
import Quickshell.Services.Mpris
import ".."
Rectangle {
id: root
// ── Cava ──────────────────────────────────────────────────
property var bars: Array(12).fill(0)
property bool silence: bars.every(v => v === 0)
property bool allBlank: bars.every(v => v < 28.5)
property bool silentMode: false
readonly property var blocks: [" ","▁","▂","▃","▄","▅","▆","▇","█"]
// ── Audio (wpctl - avoids PipeWire binding issues) ────────
property real vol: 0.0
property bool muted: false
property string volIcon: {
if (muted || vol === 0) return " "
if (vol < 0.34) return " "
if (vol < 0.67) return " "
return " "
}
// ── Media ─────────────────────────────────────────────────
property MprisPlayer activePlayer: {
var players = Mpris.players.values
for (var i = 0; i < players.length; i++)
if (players[i].identity.toLowerCase() === "spotify") return players[i]
return players.length > 0 ? players[0] : null
}
property string trackText: {
if (!activePlayer) return ""
var p = activePlayer
var info = ""
if (p.trackArtists && p.trackTitle)
info = Array.from(p.trackArtists).join(", ") + " - " + p.trackTitle
else if (p.trackTitle)
info = p.trackTitle
if (info.length > 45) info = info.substring(0, 45) + "..."
if (p.playbackState !== MprisPlaybackState.Playing && info)
info = " " + info
return info + " "
}
// ── Silence delay ─────────────────────────────────────────
onAllBlankChanged: {
if (allBlank) silenceTimer.start()
else { silenceTimer.stop(); silentMode = false }
}
Timer {
id: silenceTimer
interval: Theme.cavaDissapearTime
repeat: false
onTriggered: root.silentMode = true
}
// ── Hover → media popup ───────────────────────────────────
property bool hovered: mouseArea.containsMouse
onHoveredChanged: {
if (hovered) GlobalStates.popups.open("media", mapToItem(null, width / 2, 0).x)
}
// ── Appearance ────────────────────────────────────────────
implicitHeight: Theme.barHeight
implicitWidth: contentRow.implicitWidth + Theme.pillPadH * 2
radius: Theme.radius
clip: true
color: root.hovered ? Theme.pillHover : Theme.pill
Behavior on color { ColorAnimation { duration: 150 } }
// Volume fill - slightly more opaque than background, fills left→right by volume %
Rectangle {
anchors { left: parent.left; top: parent.top; bottom: parent.bottom }
width: parent.width * Math.min(root.vol, 1.0)
radius: Theme.radius
color: root.hovered ? Theme.setColorAlpha(Theme.pill, 0.3) : Theme.setColorAlpha(Theme.pill, 0.9)
Behavior on width { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
}
// ── Content ───────────────────────────────────────────────
RowLayout {
id: contentRow
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: Theme.pillPadH
spacing: 4
// Cava bars -d slides right and collapses after silence delay
Item {
id: cavaContainer
clip: true
implicitHeight: Theme.barHeight
implicitWidth: root.silentMode ? 0 : cavaRow.implicitWidth
Behavior on implicitWidth {
NumberAnimation { duration: 350; easing.type: Easing.InOutCubic }
}
FontMetrics {
id: barMetrics
font.family: Theme.fontMono
font.pixelSize: Theme.fontSize + 1
}
Row {
id: cavaRow
anchors.verticalCenter: parent.verticalCenter
spacing: 1
x: root.silentMode ? 16 : 0
Behavior on x {
NumberAnimation { duration: 350; easing.type: Easing.InOutCubic }
}
Repeater {
model: root.bars.length
Text {
required property int index
width: barMetrics.advanceWidth("█")
horizontalAlignment: Text.AlignHCenter
text: root.silence
? " "
: root.blocks[Math.min(Math.floor(root.bars[index] / 28.5), 8)]
font { family: Theme.fontMono; pixelSize: Theme.fontSize + 1 }
color: Theme.pillText
}
}
}
}
// Track info
Text {
visible: root.trackText !== ""
text: root.trackText
font { family: Theme.fontSans; pixelSize: Theme.fontSize }
color: Theme.pillText
elide: Text.ElideRight
maximumLineCount: 1
}
// Volume icon
Text {
text: root.volIcon
font { family: Theme.fontMono; pixelSize: Theme.fontSize }
color: "#fab387"
}
// Volume percentage
Text {
text: root.muted ? "muted" : Math.round(root.vol * 100) + "%"
font { family: Theme.fontSans; pixelSize: Theme.fontSize }
color: Theme.pillText
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (m) => {
if (m.button === Qt.RightButton) {
Exec.run(["wpctl", "set-mute", "@DEFAULT_AUDIO_SINK@", "toggle"])
root.muted = !root.muted
}
}
onWheel: (w) => {
var step = w.angleDelta.y > 0 ? "4%+" : "4%-"
Exec.run(["wpctl", "set-volume", "@DEFAULT_AUDIO_SINK@", step])
// Optimistic update - poll syncs it within 1s
root.vol = Math.max(0, Math.min(1.5, root.vol + (w.angleDelta.y > 0 ? 0.04 : -0.04)))
}
}
// ── Volume poll (wpctl) ───────────────────────────────────
Process {
id: volProc
running: false
command: ["wpctl", "get-volume", "@DEFAULT_AUDIO_SINK@"]
stdout: SplitParser {
onRead: (line) => {
// "Volume: 0.40" or "Volume: 0.40 [MUTED]"
var m = /Volume:\s+([\d.]+)(.*)/.exec(line)
if (m) {
root.vol = parseFloat(m[1])
root.muted = m[2].includes("MUTED")
}
}
}
onExited: volProc.running = false
}
Timer {
interval: 1000
running: true
repeat: true
triggeredOnStart: true
onTriggered: volProc.running = true
}
// ── Cava process ──────────────────────────────────────────
Component.onCompleted: writeCfg.running = true
Process {
id: writeCfg
running: false
command: ["bash", "-c",
"mkdir -p /tmp/qs-cava && cat > /tmp/qs-cava/cava.ini <<'CFG'\n" +
"[general]\n" +
"framerate = 30\n" +
"bars = 12\n" +
"[input]\n" +
"method = pipewire\n" +
"source = auto\n" +
"[smoothing]\n" +
"noise_reduction = 77\n" +
"monstercat = 1\n" +
"[output]\n" +
"method = raw\n" +
"raw_target = /dev/stdout\n" +
"data_format = ascii\n" +
"ascii_max_range = 255\n" +
"bar_delimiter = 59\n" +
"CFG\n"
]
onExited: cavaProc.running = true
}
Process {
id: cavaProc
running: false
command: ["cava", "-p", "/tmp/qs-cava/cava.ini"]
stdout: SplitParser {
onRead: (line) => {
const parts = line.trim().replace(/;$/, "").split(";")
if (parts.length >= root.bars.length)
root.bars = parts.slice(0, root.bars.length).map(v => parseInt(v) || 0)
}
}
onExited: cavaProc.running = true
}
}