adds overview, removes old stuff
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user