Files
colorshell/ags/widget/bar/Media.tsx
T
retrozinndev ba5694271d 💥 fix(media, big-media): only show button to copy media link if it's provided by the player
makes use of a connection to notify::metadata to get when the link is updated, instead of binding directly to AstalMpris.Player::metadata and getting the 'xesam:url' property, since JS does not support the hashmap thing
2025-07-29 14:18:58 -03:00

169 lines
7.1 KiB
TypeScript

import { Accessor, createBinding, createConnection, createState, onCleanup, With } from "ags";
import { Gtk } from "ags/gtk4";
import { Separator } from "../Separator";
import { Windows } from "../../windows";
import { Clipboard } from "../../scripts/clipboard";
import GObject from "ags/gobject";
import AstalMpris from "gi://AstalMpris";
import Pango from "gi://Pango?version=1.0";
import { decoder, getPlayerIconFromBusName, variableToBoolean } from "../../scripts/utils";
export const dummyPlayer = AstalMpris.Player.new("colorshellDummy");
export let [player, setPlayer] = createState(dummyPlayer);
export const Media = () => {
const connections: Map<GObject.Object, Array<number>|number> = new Map();
if(AstalMpris.get_default().players[0])
setPlayer(AstalMpris.get_default().players[0]);
onCleanup(() => connections.forEach((id, obj) =>
Array.isArray(id) ?
id.forEach(id => obj.disconnect(id))
: obj.disconnect(id)
));
connections.set(AstalMpris.get_default(), [
AstalMpris.get_default().connect("player-added", (_, player) =>
player.available && setPlayer(player)),
AstalMpris.get_default().connect("player-closed", (_, closedPlayer) => {
const players = AstalMpris.get_default().players.filter(pl => pl?.available &&
pl.busName !== closedPlayer.busName);
if(players.length > 0) {
setPlayer(players[0]);
return;
}
setPlayer(dummyPlayer);
})
]);
return <Gtk.Box class={"media"} visible={player((pl) => pl.available)}
$={(self) => {
const gestureClick = Gtk.GestureClick.new(),
controllerMotion = Gtk.EventControllerMotion.new(),
controllerScroll = Gtk.EventControllerScroll.new(
Gtk.EventControllerScrollFlags.VERTICAL);
self.add_controller(gestureClick);
self.add_controller(controllerMotion);
self.add_controller(controllerScroll);
connections.set(gestureClick, gestureClick.connect("released", () =>
Windows.getDefault().toggle("center-window")));
connections.set(controllerScroll,
controllerScroll.connect("scroll", (_, _dx, dy) => {
if(AstalMpris.get_default().players.length === 1 &&
player.get()?.busName === AstalMpris.get_default().players[0].busName)
return true;
const players = AstalMpris.get_default().players;
for(let i = 0; i < players.length; i++) {
const pl = players[i];
if(pl.busName !== player.get().busName)
continue;
if(dy > 0 && players[i-1]) {
setPlayer(players[i-1]);
break;
}
if(dy < 0 && players[i+1]) {
setPlayer(players[i+1]);
break;
}
}
return true;
})
);
connections.set(controllerMotion, [
controllerMotion.connect("enter", () => {
const revealer = self.get_last_child() as Gtk.Revealer;
revealer.set_reveal_child(true);
}),
controllerMotion.connect("leave", () => {
const revealer = self.get_last_child() as Gtk.Revealer;
revealer.set_reveal_child(false);
})
]);
connections.set(self, self.connect("destroy", () =>
connections.forEach((ids, obj) => Array.isArray(ids) ?
ids.forEach(id => obj.disconnect(id))
: obj.disconnect(ids))
));
}}>
<Gtk.Box spacing={4} visible={player(pl => pl.available)}>
<With value={player(pl => pl.available)}>
{(available: boolean) => available && <Gtk.Box>
<Gtk.Image class={"player-icon"} iconName={
createBinding(player.get(), "busName").as(getPlayerIconFromBusName)}
/>
<Gtk.Label class={"title"} label={createBinding(player.get(), "title").as(title =>
title ?? "No Title")} maxWidthChars={20} ellipsize={Pango.EllipsizeMode.END}
/>
<Separator orientation={Gtk.Orientation.HORIZONTAL} size={1} margin={5}
alpha={.3} spacing={6} />
<Gtk.Label class={"artist"} label={createBinding(player.get(), "artist").as(artist =>
artist ?? "No Artist")} maxWidthChars={18} ellipsize={Pango.EllipsizeMode.END}
/>
</Gtk.Box>}
</With>
</Gtk.Box>
<Gtk.Revealer transitionType={Gtk.RevealerTransitionType.SLIDE_RIGHT} transitionDuration={260}
revealChild={false}>
<With value={player(pl => pl.available)}>
{(available: boolean) => available && <Gtk.Box class={"media-controls button-row"}>
<Gtk.Button class={"link"} iconName={"edit-paste-symbolic"}
visible={variableToBoolean(getMediaUrl(player.get()))}
tooltipText={"Copy link to Clipboard"} onClicked={() => {
const url = getMediaUrl(player.get()).get();
url && Clipboard.getDefault().copyAsync(url);
}}
/>
<Gtk.Button class={"previous"} iconName={"media-skip-backward-symbolic"}
tooltipText={"Previous"} onClicked={() =>
player.get().canGoPrevious && player.get().previous()}
/>
<Gtk.Button class={"play-pause"} iconName={createBinding(player.get(), "playbackStatus").as(status =>
status === AstalMpris.PlaybackStatus.PAUSED ?
"media-playback-start-symbolic"
: "media-playback-pause-symbolic")}
tooltipText={
createBinding(player.get(), "playbackStatus").as(status =>
status === AstalMpris.PlaybackStatus.PAUSED ? "Play" : "Pause")
} onClicked={() => player.get().play_pause()}
/>
<Gtk.Button class={"next"} iconName={"media-skip-forward-symbolic"}
tooltipText={"Next"} onClicked={() => player.get().canGoNext &&
player.get().next()}
/>
</Gtk.Box>}
</With>
</Gtk.Revealer>
</Gtk.Box>
}
export function getMediaUrl(player: AstalMpris.Player): Accessor<string|undefined> {
return createConnection(player.get_meta("xesam:url"),
[player, "notify::metadata", () => player.get_meta("xesam:url")]
).as(url => {
const byteString = url?.get_data_as_bytes();
return byteString ?
decoder.decode(byteString.toArray())
: undefined;
})
}