Something went wrong. Try again.
dope ass quickshell bar
Something went wrong. Try again.
quickshell MediaWidget.qml
9.9 kB · 283 lines
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284import QtQuick 2.15import Quickshell.Ioimport Quickshell.Services.Mpris
Item { id: media property var niriWindowsList: []
// Only show players with track navigation; prefer one that is playing. readonly property var player: { const players = Mpris.players.values let pausedPlayer = null for (const candidate of players) { if (!candidate.canGoPrevious || !candidate.canGoNext) continue if (!candidate.trackTitle || !candidate.trackTitle.trim()) continue if (candidate.isPlaying) return candidate if (!pausedPlayer) pausedPlayer = candidate } return pausedPlayer }
function normalizedAppId(value) { return String(value ?? "").trim().toLowerCase().replace(/\.desktop$/, "") }
readonly property var focusTarget: { const current = media.player if (!current) return null
const dbusPlayer = String(current.dbusName ?? "") .replace(/^org\.mpris\.MediaPlayer2\./, "") const appIds = [current.desktopEntry, dbusPlayer, dbusPlayer.split(".")[0], current.identity] .map(id => media.normalizedAppId(id)) .filter(id => id.length > 0) const trackTitle = String(current.trackTitle ?? "").trim().toLowerCase()
for (const appId of appIds) { const matches = media.niriWindowsList.filter(win => media.normalizedAppId(win.app_id) === appId) if (trackTitle) { const titleMatches = matches.filter(win => String(win.title ?? "").toLowerCase().includes(trackTitle)) if (titleMatches.length === 1) return titleMatches[0] } if (matches.length === 1) return matches[0] } return null }
function focusPlayerWindow() { if (focusTarget) { focusWindowProcess.targetId = focusTarget.id.toString() focusWindowProcess.running = true } else if (player?.canRaise) { player.raise() } }
Process { id: focusWindowProcess property string targetId: "" command: ["niri", "msg", "action", "focus-window", "--id", targetId] }
visible: player !== null implicitWidth: visible ? content.implicitWidth : 0 implicitHeight: visible ? 26 : 0
HoverHandler { id: mediaHover onHoveredChanged: { if (hovered) titleViewport.resetMarquee() else titleViewport.returnToStart() } }
component MediaButton: Item { id: button property string action: "" property bool playing: false property bool available: false signal activated()
implicitWidth: 24 implicitHeight: 26
Rectangle { anchors.fill: parent radius: 5 color: buttonArea.containsMouse && button.available ? "#1c1c1c" : "transparent" }
Canvas { id: icon anchors.centerIn: parent width: 16 height: 16 property color iconColor: button.available ? "white" : "#777" onIconColorChanged: requestPaint() onPaint: { const ctx = getContext("2d") ctx.clearRect(0, 0, width, height) ctx.fillStyle = iconColor
function pill(x, y, w, h) { const r = w / 2 ctx.beginPath() ctx.moveTo(x + r, y) ctx.lineTo(x + w - r, y) ctx.quadraticCurveTo(x + w, y, x + w, y + r) ctx.lineTo(x + w, y + h - r) ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h) ctx.lineTo(x + r, y + h) ctx.quadraticCurveTo(x, y + h, x, y + h - r) ctx.lineTo(x, y + r) ctx.quadraticCurveTo(x, y, x + r, y) ctx.closePath() ctx.fill() }
if (button.action === "previous") { pill(3, 3.5, 2, 9) ctx.beginPath() ctx.moveTo(11.7, 3.6) ctx.quadraticCurveTo(12.5, 3.1, 12.5, 4.2) ctx.lineTo(12.5, 11.8) ctx.quadraticCurveTo(12.5, 12.9, 11.7, 12.4) ctx.lineTo(5.8, 8.6) ctx.quadraticCurveTo(4.9, 8, 5.8, 7.4) ctx.closePath() ctx.fill() } else if (button.action === "next") { pill(11, 3.5, 2, 9) ctx.beginPath() ctx.moveTo(4.3, 3.6) ctx.quadraticCurveTo(3.5, 3.1, 3.5, 4.2) ctx.lineTo(3.5, 11.8) ctx.quadraticCurveTo(3.5, 12.9, 4.3, 12.4) ctx.lineTo(10.2, 8.6) ctx.quadraticCurveTo(11.1, 8, 10.2, 7.4) ctx.closePath() ctx.fill() } else if (button.playing) { pill(4.5, 3.5, 2.7, 9) pill(8.8, 3.5, 2.7, 9) } else { ctx.beginPath() ctx.moveTo(5.7, 3.5) ctx.quadraticCurveTo(5, 3.1, 5, 4.2) ctx.lineTo(5, 11.8) ctx.quadraticCurveTo(5, 12.9, 5.7, 12.5) ctx.lineTo(12.1, 8.6) ctx.quadraticCurveTo(13.1, 8, 12.1, 7.4) ctx.closePath() ctx.fill() } } Connections { target: button function onActionChanged() { icon.requestPaint() } function onPlayingChanged() { icon.requestPaint() } } }
MouseArea { id: buttonArea anchors.fill: parent enabled: button.available hoverEnabled: true onClicked: button.activated() } }
Row { id: content anchors.centerIn: parent spacing: 3
Item { id: titleViewport implicitWidth: Math.min(280, titleText.implicitWidth) implicitHeight: 26 clip: true readonly property int marqueeGap: 24 readonly property real overflow: Math.max(0, titleText.implicitWidth - width)
function resetMarquee() { marquee.stop() resetAnimation.stop() marqueeTrack.x = 0 if (overflow > 0.5 && mediaHover.hovered) marquee.start() }
function returnToStart() { marquee.stop() resetAnimation.start() }
Component.onCompleted: resetMarquee() onOverflowChanged: Qt.callLater(resetMarquee)
Item { id: marqueeTrack width: titleText.implicitWidth * 2 + titleViewport.marqueeGap height: parent.height
Text { id: titleText anchors.verticalCenter: parent.verticalCenter text: { const title = media.player?.trackTitle ?? "" const artist = (media.player?.trackArtist ?? "").trim() return artist ? title + " · " + artist : title } onTextChanged: Qt.callLater(titleViewport.resetMarquee) textFormat: Text.PlainText color: "white" font.family: "Inter" font.pixelSize: 13 font.weight: Font.Bold }
Text { x: titleText.implicitWidth + titleViewport.marqueeGap anchors.verticalCenter: parent.verticalCenter visible: titleViewport.overflow > 0.5 text: titleText.text textFormat: Text.PlainText color: titleText.color font: titleText.font } }
NumberAnimation { id: marquee target: marqueeTrack property: "x" from: 0 to: -(titleText.implicitWidth + titleViewport.marqueeGap) duration: (titleText.implicitWidth + titleViewport.marqueeGap) * 25 easing.type: Easing.Linear loops: Animation.Infinite }
NumberAnimation { id: resetAnimation target: marqueeTrack property: "x" to: 0 duration: 350 easing.type: Easing.OutCubic }
MouseArea { anchors.fill: parent enabled: media.focusTarget !== null || (media.player?.canRaise ?? false) cursorShape: Qt.PointingHandCursor onClicked: media.focusPlayerWindow() } }
Item { implicitWidth: 5; implicitHeight: 1 }
MediaButton { action: "previous" available: media.player?.canGoPrevious ?? false onActivated: { if (media.player) media.player.previous() } }
MediaButton { action: "toggle" playing: media.player?.isPlaying ?? false available: media.player?.canTogglePlaying ?? false onActivated: { if (media.player) media.player.togglePlaying() } }
MediaButton { action: "next" available: media.player?.canGoNext ?? false onActivated: { if (media.player) media.player.next() } } }}