Add media player card and bar center track button
This commit is contained in:
parent
d501ea4688
commit
28c17a0e44
|
|
@ -285,6 +285,76 @@ $module-bg: transparentize($base02, 0.4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.media-section {
|
||||||
|
margin-top: 10px;
|
||||||
|
border-top: 1px solid $base02;
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player {
|
||||||
|
.cover {
|
||||||
|
min-width: 48px;
|
||||||
|
min-height: 48px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: transparentize($base02, 0.4);
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-artist {
|
||||||
|
font-size: 0.85em;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.position {
|
||||||
|
trough {
|
||||||
|
background: $base03;
|
||||||
|
border-radius: 999px;
|
||||||
|
min-height: 10px;
|
||||||
|
|
||||||
|
highlight {
|
||||||
|
background: $base0D;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
slider {
|
||||||
|
min-width: 12px;
|
||||||
|
min-height: 12px;
|
||||||
|
background: $base05;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-ctl {
|
||||||
|
padding: 4px 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
icon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: transparentize($base05, 0.88);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar .media {
|
||||||
|
icon {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-title {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.wifi-section {
|
.wifi-section {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
border-top: 1px solid $base02;
|
border-top: 1px solid $base02;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { App, Astal, Gdk, Gtk } from "astal/gtk3"
|
||||||
import Workspaces from "./Workspaces"
|
import Workspaces from "./Workspaces"
|
||||||
import Logo from "./Logo"
|
import Logo from "./Logo"
|
||||||
import Clock from "./Clock"
|
import Clock from "./Clock"
|
||||||
|
import { MediaButton } from "./Media"
|
||||||
import QuickSettings from "./QuickSettings"
|
import QuickSettings from "./QuickSettings"
|
||||||
|
|
||||||
const RADIUS = 12
|
const RADIUS = 12
|
||||||
|
|
@ -85,7 +86,9 @@ export default function Bar(gdkmonitor: Gdk.Monitor) {
|
||||||
className="bar-center"
|
className="bar-center"
|
||||||
halign={Gtk.Align.CENTER}
|
halign={Gtk.Align.CENTER}
|
||||||
valign={Gtk.Align.CENTER}
|
valign={Gtk.Align.CENTER}
|
||||||
/>
|
>
|
||||||
|
<MediaButton />
|
||||||
|
</box>
|
||||||
<box
|
<box
|
||||||
className="bar-right"
|
className="bar-right"
|
||||||
halign={Gtk.Align.END}
|
halign={Gtk.Align.END}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,189 @@
|
||||||
|
import Gtk from "gi://Gtk?version=3.0"
|
||||||
|
import Pango from "gi://Pango"
|
||||||
|
import AstalMpris from "gi://AstalMpris"
|
||||||
|
import { Variable, bind } from "astal"
|
||||||
|
|
||||||
|
const mpris = AstalMpris.get_default()
|
||||||
|
|
||||||
|
const PREV = "media-skip-backward-symbolic"
|
||||||
|
const PLAY = "media-playback-start-symbolic"
|
||||||
|
const PAUSE = "media-playback-pause-symbolic"
|
||||||
|
const NEXT = "media-skip-forward-symbolic"
|
||||||
|
const NO_ART = "audio-x-generic-symbolic"
|
||||||
|
|
||||||
|
function fmtTime(seconds: number) {
|
||||||
|
if (!seconds || seconds < 0) return "0:00"
|
||||||
|
const min = Math.floor(seconds / 60)
|
||||||
|
const sec = Math.floor(seconds % 60)
|
||||||
|
return `${min}:${sec < 10 ? "0" : ""}${sec}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function playerIcon(entry: string) {
|
||||||
|
const name = entry ? `${entry.toLowerCase()}-symbolic` : ""
|
||||||
|
const theme = Gtk.IconTheme.get_default()!
|
||||||
|
return name !== "" && theme.has_icon(name) ? name : NO_ART
|
||||||
|
}
|
||||||
|
|
||||||
|
function PlayerCard(player: AstalMpris.Player) {
|
||||||
|
// position only changes on the bus when seeking; poll for smooth progress
|
||||||
|
const position = Variable(-1).poll(1000, () =>
|
||||||
|
player.available ? player.position : -1,
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box className="player" spacing={8}>
|
||||||
|
<box
|
||||||
|
className="cover"
|
||||||
|
css={bind(player, "cover_art").as((c) =>
|
||||||
|
c !== "" ? `background-image: url('${encodeURI(`file://${c}`)}')` : "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<icon
|
||||||
|
pixelSize={24}
|
||||||
|
icon={NO_ART}
|
||||||
|
visible={bind(player, "cover_art").as((c) => c === "")}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
<box vertical spacing={4}>
|
||||||
|
<box spacing={6}>
|
||||||
|
<label
|
||||||
|
className="player-title"
|
||||||
|
hexpand
|
||||||
|
halign={Gtk.Align.START}
|
||||||
|
label={bind(player, "title").as((t) => t || "")}
|
||||||
|
maxWidthChars={20}
|
||||||
|
ellipsize={Pango.EllipsizeMode.END}
|
||||||
|
/>
|
||||||
|
<icon
|
||||||
|
className="player-icon"
|
||||||
|
icon={bind(player, "entry").as(playerIcon)}
|
||||||
|
tooltipText={bind(player, "identity")}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
<label
|
||||||
|
className="player-artist"
|
||||||
|
halign={Gtk.Align.START}
|
||||||
|
label={bind(player, "artist").as((a) => a || "")}
|
||||||
|
maxWidthChars={24}
|
||||||
|
ellipsize={Pango.EllipsizeMode.END}
|
||||||
|
visible={bind(player, "artist").as((a) => (a || "") !== "")}
|
||||||
|
/>
|
||||||
|
<slider
|
||||||
|
className="position"
|
||||||
|
value={position().as((p) =>
|
||||||
|
player.length > 0
|
||||||
|
? Math.min(1, Math.max(0, p / player.length))
|
||||||
|
: 0,
|
||||||
|
)}
|
||||||
|
onDragged={(self) => {
|
||||||
|
if (player.length > 0) player.position = self.value * player.length
|
||||||
|
}}
|
||||||
|
visible={bind(player, "length").as((l) => l > 0)}
|
||||||
|
/>
|
||||||
|
<box className="player-footer" visible={bind(player, "length").as((l) => l > 0)}>
|
||||||
|
<label
|
||||||
|
className="player-position"
|
||||||
|
label={position().as((p) => fmtTime(p))}
|
||||||
|
/>
|
||||||
|
<box hexpand />
|
||||||
|
<button
|
||||||
|
className="player-ctl"
|
||||||
|
onClicked={() => player.previous()}
|
||||||
|
visible={bind(player, "can_go_previous")}
|
||||||
|
>
|
||||||
|
<icon icon={PREV} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="player-ctl"
|
||||||
|
onClicked={() => player.play_pause()}
|
||||||
|
visible={bind(player, "can_play")}
|
||||||
|
>
|
||||||
|
<icon
|
||||||
|
icon={bind(player, "playback_status").as((s) =>
|
||||||
|
s === AstalMpris.PlaybackStatus.PLAYING ? PAUSE : PLAY,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="player-ctl"
|
||||||
|
onClicked={() => player.next()}
|
||||||
|
visible={bind(player, "can_go_next")}
|
||||||
|
>
|
||||||
|
<icon icon={NEXT} />
|
||||||
|
</button>
|
||||||
|
<box hexpand />
|
||||||
|
<label
|
||||||
|
className="player-length"
|
||||||
|
label={bind(player, "length").as((l) => fmtTime(l))}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MediaSection() {
|
||||||
|
const players = bind(mpris, "players")
|
||||||
|
return (
|
||||||
|
<box className="media-section" vertical visible={players.as((ps) => ps.length > 0)}>
|
||||||
|
{players.as((ps) => ps.map((p) => PlayerCard(p)))}
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the players property only notifies on connect/disconnect, so the bar
|
||||||
|
// button tracks the first player directly to follow track changes
|
||||||
|
let player: AstalMpris.Player | null = null
|
||||||
|
let hookIds: Array<number> = []
|
||||||
|
|
||||||
|
const hasPlayer = Variable(false)
|
||||||
|
const title = Variable("")
|
||||||
|
const icon = Variable(NO_ART)
|
||||||
|
|
||||||
|
function sync() {
|
||||||
|
const current = mpris.players[0] ?? null
|
||||||
|
if (current !== player) {
|
||||||
|
hookIds.forEach((id) => player?.disconnect(id))
|
||||||
|
hookIds = []
|
||||||
|
player = current
|
||||||
|
if (player) {
|
||||||
|
hookIds = [
|
||||||
|
player.connect("notify::title", sync),
|
||||||
|
player.connect("notify::entry", sync),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hasPlayer.set(player !== null)
|
||||||
|
title.set(player?.title || "")
|
||||||
|
icon.set(player ? playerIcon(player.entry) : "")
|
||||||
|
}
|
||||||
|
|
||||||
|
mpris.connect("notify::players", sync)
|
||||||
|
sync()
|
||||||
|
|
||||||
|
export function MediaButton() {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="media center"
|
||||||
|
visible={bind(hasPlayer)}
|
||||||
|
onClicked={() => {
|
||||||
|
if (player && player.can_play) player.play_pause()
|
||||||
|
}}
|
||||||
|
onScroll={(_, e) => {
|
||||||
|
if (!player) return
|
||||||
|
if (e.delta_y < 0) player.next()
|
||||||
|
else player.previous()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<box spacing={6}>
|
||||||
|
<icon className="media-icon" icon={bind(icon)} />
|
||||||
|
<label
|
||||||
|
className="media-title"
|
||||||
|
label={bind(title)}
|
||||||
|
maxWidthChars={30}
|
||||||
|
ellipsize={Pango.EllipsizeMode.END}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import Dropdown from "./Dropdown"
|
||||||
import { wifiIcon, WifiSection } from "./Wifi"
|
import { wifiIcon, WifiSection } from "./Wifi"
|
||||||
import { bluetoothIndicator, BluetoothSection } from "./Bluetooth"
|
import { bluetoothIndicator, BluetoothSection } from "./Bluetooth"
|
||||||
import { BrightnessRow } from "./Brightness"
|
import { BrightnessRow } from "./Brightness"
|
||||||
|
import { MediaSection } from "./Media"
|
||||||
import { batteryIcon, batteryPercent, BatterySection } from "./Battery"
|
import { batteryIcon, batteryPercent, BatterySection } from "./Battery"
|
||||||
import { NotificationCard } from "./Notifications"
|
import { NotificationCard } from "./Notifications"
|
||||||
|
|
||||||
|
|
@ -145,6 +146,7 @@ export default function QuickSettings(gdkmonitor: Gdk.Monitor) {
|
||||||
<BrightnessRow />
|
<BrightnessRow />
|
||||||
<WifiSection open={open} />
|
<WifiSection open={open} />
|
||||||
<BluetoothSection open={open} />
|
<BluetoothSection open={open} />
|
||||||
|
<MediaSection />
|
||||||
<box className="notif-section" vertical spacing={6}>
|
<box className="notif-section" vertical spacing={6}>
|
||||||
<box className="notif-header-row" spacing={6}>
|
<box className="notif-header-row" spacing={6}>
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ let
|
||||||
wireplumber
|
wireplumber
|
||||||
notifd
|
notifd
|
||||||
bluetooth
|
bluetooth
|
||||||
|
mpris
|
||||||
]
|
]
|
||||||
++ [ pkgs.networkmanager ];
|
++ [ pkgs.networkmanager ];
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue