import QtQuick import Quickshell import Quickshell.Io import Quickshell.Services.Mpris import "." as M import "content" as C M.BarSection { id: root spacing: M.Theme.moduleSpacing opacity: M.Modules.mpris.enable && player !== null ? 1 : 0 visible: opacity > 0 tooltip: "" property int _playerIdx: 0 readonly property var _players: (Mpris.players.values ?? []).filter(p => p.trackTitle || p.playbackState === MprisPlaybackState.Playing || p.playbackState === MprisPlaybackState.Paused) readonly property MprisPlayer player: _players[_playerIdx] ?? _players[0] ?? null readonly property bool playing: player?.playbackState === MprisPlaybackState.Playing // Reset index if current player disappears on_PlayersChanged: if (_playerIdx >= _players.length) _playerIdx = 0 property string _cachedArt: "" property string _artTrack: "" // Cache art URL at root level so it's captured even when panel is hidden readonly property string _artUrl: player?.trackArtUrl ?? "" readonly property string _currentTrack: player?.trackTitle ?? "" on_ArtUrlChanged: if (_artUrl) _cachedArt = _artUrl on_CurrentTrackChanged: if (_currentTrack !== _artTrack) { _artTrack = _currentTrack; _cachedArt = _artUrl || ""; } // Preload art while panel is hidden Image { visible: false source: root._cachedArt asynchronous: true } // Cava visualizer - 16 bars, raw output mode property var _cavaBars: Array(16).fill(0) property bool _cavaActive: false on_ShowPanelChanged: { if (_showPanel) { _cavaKillTimer.stop(); _cavaActive = true; } else { _cavaKillTimer.restart(); } } Timer { id: _cavaKillTimer interval: 30000 onTriggered: root._cavaActive = false } Process { id: cavaProc running: root.playing && root._cavaActive command: ["sh", "-c", "cfg=$(mktemp /tmp/nova-cava-XXXXXX.conf);" + "cat > \"$cfg\" << 'CAVAEOF'\n" + "[general]\nbars=16\nframerate=30\n[output]\nmethod=raw\nraw_target=/dev/stdout\ndata_format=ascii\nascii_max_range=100\n" + "CAVAEOF\n" + "trap 'rm -f \"$cfg\"' EXIT;" + "exec cava -p \"$cfg\""] stdout: SplitParser { splitMarker: "\n" onRead: line => { const vals = line.split(";").filter(s => s).map(Number); if (vals.length >= 16) root._cavaBars = vals.map(v => v / 100); } } } required property var bar property bool _pinned: false readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered readonly property bool _showPanel: _anyHover || _pinned on_AnyHoverChanged: { if (_anyHover) _unpinTimer.stop(); else if (_pinned) _unpinTimer.start(); } Timer { id: _unpinTimer interval: 500 onTriggered: root._pinned = false } M.BarIcon { icon: root.playing ? "\uF04B" : (root.player?.playbackState === MprisPlaybackState.Paused ? "\uDB80\uDFE4" : "\uDB81\uDCDB") anchors.verticalCenter: parent.verticalCenter MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: root._pinned = !root._pinned } } M.BarLabel { label: root.player?.trackTitle || root.player?.identity || "" elide: Text.ElideRight width: Math.min(implicitWidth, 200) anchors.verticalCenter: parent.verticalCenter MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: root._pinned = !root._pinned } } M.HoverPanel { id: hoverPanel showPanel: root._showPanel screen: QsWindow.window?.screen ?? null anchorItem: root accentColor: root.accentColor panelNamespace: "nova-mpris" panelTitle: "Now Playing" contentWidth: 280 C.MprisContent { width: hoverPanel.contentWidth player: root.player players: root._players playing: root.playing accentColor: root.accentColor cachedArt: root._cachedArt cavaBars: root._cavaBars playerIdx: root._playerIdx onPlayerSwitched: idx => { root._playerIdx = idx; hoverPanel.keepOpen(400); } } } }