- Idle is handled in the shell instead of hypridle: the screen dims slowly at 4 minutes, locks at 5, powers down at 6, and suspends at 15 on battery. Idle inhibitors hold every stage off; Super+Shift+I does the same by hand and puts a cup on the rail. - The shell draws the wallpaper (backdrop/), so hyprpaper is gone. The picture is drawn a little larger than the screen and drifts with the workspace a beat after the windows, and dims a shade while windows are open. The path lives in quickshell/wallpaper. - The lock screen and power menu say "Take your time." instead of the NieR slogan. - hyprpaper is no longer launched twice, the media module keeps the player that played last instead of an empty browser tab, and grouped windows show thin untitled tabs with their place in the group (2/3) in the bar title. - qt6ct uses Darkly; the package list gains adw-gtk-theme and darkly-bin.
122 lines
4.9 KiB
QML
122 lines
4.9 KiB
QML
pragma Singleton
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Services.Mpris
|
|
|
|
// MPRIS. Prefers whichever player is actually playing, falling back to the last
|
|
// one that was, so the bar does not flip between Spotify and a stray browser tab.
|
|
Singleton {
|
|
id: root
|
|
|
|
property string preferredId: ""
|
|
// The player that was playing most recently, so pausing it keeps it in
|
|
// the bar instead of handing the slot to whichever player is listed first.
|
|
property string lastPlayingId: ""
|
|
|
|
readonly property var players: Mpris.players?.values ?? []
|
|
readonly property bool hasPlayer: active !== null
|
|
|
|
readonly property MprisPlayer active: {
|
|
if (players.length === 0) return null;
|
|
|
|
// An explicit pick wins, while it still exists.
|
|
if (root.preferredId !== "") {
|
|
for (const p of players) if (p.dbusName === root.preferredId) return p;
|
|
}
|
|
for (const p of players) if (p.isPlaying) return p;
|
|
if (root.lastPlayingId !== "") {
|
|
for (const p of players) if (p.dbusName === root.lastPlayingId) return p;
|
|
}
|
|
// Browsers register a player for every tab with a media element, most
|
|
// of them with no track at all. Prefer one that has something to show.
|
|
for (const p of players) if (p.trackTitle) return p;
|
|
return players[0];
|
|
}
|
|
|
|
// Remember whichever player was playing last. Bound through the active
|
|
// player, so it updates whenever playback starts anywhere.
|
|
readonly property string playingId: {
|
|
for (const p of players) if (p.isPlaying) return p.dbusName;
|
|
return "";
|
|
}
|
|
onPlayingIdChanged: if (root.playingId !== "") root.lastPlayingId = root.playingId
|
|
|
|
readonly property string title: active?.trackTitle || ""
|
|
readonly property string artist: active?.trackArtist || ""
|
|
readonly property string album: active?.trackAlbum || ""
|
|
readonly property string artUrl: active?.trackArtUrl || ""
|
|
readonly property string identity: active?.identity || ""
|
|
readonly property bool playing: active?.isPlaying ?? false
|
|
|
|
readonly property bool canGoNext: active?.canGoNext ?? false
|
|
readonly property bool canGoPrevious: active?.canGoPrevious ?? false
|
|
readonly property bool positionSupported: active?.positionSupported ?? false
|
|
readonly property bool lengthSupported: active?.lengthSupported ?? false
|
|
readonly property bool hasTimeline: positionSupported && lengthSupported && length > 0
|
|
readonly property bool canSeek: (active?.canSeek ?? false) && hasTimeline
|
|
|
|
readonly property real length: lengthSupported ? (active?.length ?? 0) : 0
|
|
readonly property real position: positionSupported ? (active?.position ?? 0) : 0
|
|
readonly property real progress: hasTimeline ? Math.min(1, position / length) : 0
|
|
|
|
readonly property string label: {
|
|
if (!hasPlayer) return "Nothing playing";
|
|
if (title === "") return identity || "Unknown track";
|
|
if (artist === "") return title;
|
|
return artist + " — " + title;
|
|
}
|
|
|
|
signal trackChanged()
|
|
|
|
onTitleChanged: trackChanged()
|
|
|
|
// MPRIS position is pull-only; tick it so the seek bar moves.
|
|
Timer {
|
|
interval: 1000
|
|
running: root.playing && root.positionSupported
|
|
repeat: true
|
|
onTriggered: if (root.active) root.active.positionChanged()
|
|
}
|
|
|
|
function playPause() { if (active?.canTogglePlaying) active.togglePlaying(); }
|
|
function next() { if (canGoNext) active.next(); }
|
|
function previous() { if (canGoPrevious) active.previous(); }
|
|
function seekTo(fraction: real) {
|
|
if (!canSeek || length <= 0) return;
|
|
active.position = Math.max(0, Math.min(length, fraction * length));
|
|
}
|
|
function raise() { if (active?.canRaise) active.raise(); }
|
|
|
|
function select(player: MprisPlayer) {
|
|
root.preferredId = player ? player.dbusName : "";
|
|
}
|
|
|
|
// Nerd Font glyph for the source app, so the bar reads at a glance.
|
|
readonly property string sourceIcon: {
|
|
const id = (identity || "").toLowerCase();
|
|
if (id.includes("spotify")) return "";
|
|
if (id.includes("firefox")) return "";
|
|
if (id.includes("chrom") || id.includes("brave")) return "";
|
|
if (id.includes("mpv")) return "";
|
|
if (id.includes("vlc")) return "";
|
|
if (id.includes("jellyfin")) return "";
|
|
if (id.includes("youtube")) return "";
|
|
return "";
|
|
}
|
|
|
|
// Quickshell exposes MPRIS position/length in seconds (with millisecond
|
|
// precision), not the protocol's raw microseconds.
|
|
function timeString(seconds: real): string {
|
|
if (!(seconds > 0)) return "0:00";
|
|
const total = Math.floor(seconds);
|
|
const m = Math.floor(total / 60);
|
|
const s = total % 60;
|
|
if (m >= 60) {
|
|
const h = Math.floor(m / 60);
|
|
return h + ":" + String(m % 60).padStart(2, "0") + ":" + String(s).padStart(2, "0");
|
|
}
|
|
return m + ":" + String(s).padStart(2, "0");
|
|
}
|
|
}
|