nosignal-shell/src/modules/popdowns/media.tsx
2025-03-02 22:53:37 +11:00

152 lines
7.2 KiB
TypeScript

import Players from "@/services/players";
import { isRealPlayer } from "@/utils/mpris";
import PopupWindow from "@/widgets/popupwindow";
import { bind, Variable } from "astal";
import { Astal, Gtk } from "astal/gtk3";
import AstalMpris from "gi://AstalMpris";
const shuffleToIcon = (s: AstalMpris.Shuffle) =>
`caelestia-${s === AstalMpris.Shuffle.ON ? "shuffle" : "consecutive"}-symbolic`;
const playbackToIcon = (s: AstalMpris.PlaybackStatus) =>
`caelestia-${s === AstalMpris.PlaybackStatus.PLAYING ? "pause" : "play"}-symbolic`;
const loopToIcon = (s: AstalMpris.Loop) => {
if (s === AstalMpris.Loop.PLAYLIST) return "caelestia-repeat-symbolic";
if (s === AstalMpris.Loop.TRACK) return "caelestia-repeat-one-symbolic";
return "caelestia-no-repeat-symbolic";
};
const lengthStr = (length: number) =>
`${Math.floor(length / 60)}:${Math.floor(length % 60)
.toString()
.padStart(2, "0")}`;
const Player = ({ player }: { player: AstalMpris.Player }) => {
const background = (
<box className="background" css={bind(player, "coverArt").as(u => `background-image: url("${u}");`)} />
);
return (
<overlay
overlays={[
<box className="overlay" />,
<box
vertical
className="player"
setup={self =>
self.connect("size-allocate", () =>
background.set_size_request(self.get_preferred_width()[1], self.get_preferred_height()[1])
)
}
>
<label className="title" label={bind(player, "title").as(t => t ?? "")} />
<label className="artist" label={bind(player, "artist").as(a => a ?? "")} />
<label className="album" label={bind(player, "album").as(a => a ?? "")} />
<box className="controls" halign={Gtk.Align.CENTER}>
<button
cursor="pointer"
onClicked={() => player.shuffle()}
sensitive={bind(player, "canControl")}
>
<icon icon={bind(player, "shuffleStatus").as(shuffleToIcon)} />
</button>
<button
cursor="pointer"
onClicked={() => player.previous()}
sensitive={bind(player, "canGoPrevious")}
>
<icon icon="caelestia-skip-previous-symbolic" />
</button>
<button
cursor="pointer"
onClicked={() => player.play_pause()}
sensitive={bind(player, "canControl")}
>
<icon icon={bind(player, "playbackStatus").as(playbackToIcon)} />
</button>
<button cursor="pointer" onClicked={() => player.next()} sensitive={bind(player, "canGoNext")}>
<icon icon="caelestia-skip-next-symbolic" />
</button>
<button cursor="pointer" onClicked={() => player.loop()} sensitive={bind(player, "canControl")}>
<icon icon={bind(player, "loopStatus").as(loopToIcon)} />
</button>
</box>
<slider
hexpand
onDragged={self => player.set_position(self.value * player.length)}
setup={self => {
const update = () => {
self.set_tooltip_text(`${lengthStr(player.position)}/${lengthStr(player.length)}`);
self.set_value(player.position / player.length);
};
self.hook(player, "notify::position", update);
self.hook(player, "notify::length", update);
update();
}}
/>
</box>,
]}
>
{background}
</overlay>
);
};
export default () => {
const shown = Variable("none");
return (
<PopupWindow name="media">
<box className="media">
<eventbox
onClick={(_, event) => {
if (event.button === Astal.MouseButton.SECONDARY) {
const current = Players.get_default().list.find(p => p.busName === shown.get());
if (current) Players.get_default().makeCurrent(current);
}
}}
onScroll={(_, event) => {
const players = AstalMpris.get_default().players.filter(isRealPlayer);
const idx = players.findIndex(p => p.busName === shown.get());
if (idx === -1) return;
if (event.delta_y < 0) {
if (idx > 0) shown.set(players[idx - 1].busName);
} else if (idx < players.length - 1) shown.set(players[idx + 1].busName);
}}
>
<stack
expand
transitionType={Gtk.StackTransitionType.SLIDE_LEFT_RIGHT}
transitionDuration={150}
shown={bind(shown)}
setup={self => {
const players = new Map<string, JSX.Element>();
const addPlayer = (player: AstalMpris.Player) => {
if (!isRealPlayer(player) || players.has(player.busName)) return;
const widget = <Player player={player} />;
self.add_named(widget, player.busName);
if (players.size === 0) shown.set(player.busName);
players.set(player.busName, widget);
};
for (const player of Players.get_default().list) addPlayer(player);
self.hook(AstalMpris.get_default(), "player-added", (_, player) => addPlayer(player));
self.hook(AstalMpris.get_default(), "player-closed", (_, player) => {
players.get(player.busName)?.destroy();
players.delete(player.busName);
if (shown.get() === player.busName)
shown.set(AstalMpris.get_default().players.find(isRealPlayer)?.busName ?? "none");
});
}}
>
<box vertical valign={Gtk.Align.CENTER} name="none">
<label className="icon" label="music_note" />
<label label="No media playing" />
</box>
</stack>
</eventbox>
</box>
</PopupWindow>
);
};