156 lines
5.9 KiB
TypeScript
156 lines
5.9 KiB
TypeScript
import { bind } from "astal";
|
|
import { Gtk } from "astal/gtk4";
|
|
import AstalMpris from "gi://AstalMpris";
|
|
import Pango from "gi://Pango?version=1.0";
|
|
const ALIGN = Gtk.Align;
|
|
|
|
const mpris = AstalMpris.get_default();
|
|
mpris.connect("player-added", p => {
|
|
print("Player added:", p);
|
|
});
|
|
|
|
const PlayerModule = () => {
|
|
return (
|
|
<box vertical cssClasses={ [ 'players-box' ] }>
|
|
<label label={"Music Players"} halign={ALIGN.CENTER} cssClasses={[ 'title-2' ]}></label>
|
|
<Gtk.Separator marginTop={3} marginBottom={5}></Gtk.Separator>
|
|
<box cssClasses={["players"]}>
|
|
{bind(mpris, "players").as(players => {
|
|
return players.map(player => {
|
|
return <PlayerItem player={player}></PlayerItem>;
|
|
});
|
|
})}
|
|
</box>
|
|
<label label={"No playback active"} visible={bind(mpris, "players").as( players => players.length === 0 )}></label>
|
|
</box>
|
|
);
|
|
};
|
|
|
|
// TODO: Update widths
|
|
const pbStatus = AstalMpris.PlaybackStatus;
|
|
const PlayerItem = ({ player }: { player: AstalMpris.Player }) => {
|
|
return (
|
|
<box cssClasses={["player"]}>
|
|
<image
|
|
cssClasses={["cover-art"]}
|
|
file={bind(player, "coverArt")}
|
|
hexpand
|
|
vexpand
|
|
></image>
|
|
<box vertical>
|
|
<label
|
|
label={bind(player, "title").as(
|
|
title => title ?? "Unknown title",
|
|
)}
|
|
cssClasses={["title"]}
|
|
halign={ALIGN.START}
|
|
valign={ALIGN.START}
|
|
maxWidthChars={30}
|
|
ellipsize={Pango.EllipsizeMode.END}
|
|
></label>
|
|
<label
|
|
label={bind(player, "artist").as(
|
|
artist => artist ?? "Unknown artist",
|
|
)}
|
|
halign={ALIGN.START}
|
|
valign={ALIGN.START}
|
|
maxWidthChars={30}
|
|
ellipsize={Pango.EllipsizeMode.END}
|
|
></label>
|
|
<slider
|
|
visible={bind(player, "length").as(l => l > 0)}
|
|
value={bind(player, "position")}
|
|
min={0}
|
|
max={bind(player, "length")}
|
|
onChangeValue={v =>
|
|
player.set_position(v.get_value())
|
|
}
|
|
></slider>
|
|
<centerbox
|
|
cssClasses={["actions"]}
|
|
startWidget={
|
|
<label
|
|
label={bind(player, "position").as(v =>
|
|
secondsToFriendlyTime(v),
|
|
)}
|
|
hexpand
|
|
cssClasses={["position"]}
|
|
></label>
|
|
}
|
|
centerWidget={
|
|
<box>
|
|
<button
|
|
visible={bind(player, "canGoPrevious")}
|
|
child={
|
|
<image
|
|
iconName={
|
|
"media-skip-backward-symbolic"
|
|
}
|
|
></image>
|
|
}
|
|
onClicked={() => player.previous()}
|
|
></button>
|
|
<button
|
|
visible={bind(player, "canControl")}
|
|
child={
|
|
<image
|
|
iconName={bind(
|
|
player,
|
|
"playbackStatus",
|
|
).as(status => {
|
|
if (status === pbStatus.PLAYING) {
|
|
return "media-playback-pause-symbolic";
|
|
} else {
|
|
return "media-playback-start-symbolic";
|
|
}
|
|
})}
|
|
></image>
|
|
}
|
|
onClicked={() => player.play_pause()}
|
|
></button>
|
|
<button
|
|
visible={bind(player, "canGoNext")}
|
|
child={
|
|
<image
|
|
iconName={"media-skip-forward-symbolic"}
|
|
></image>
|
|
}
|
|
onClicked={() => player.next()}
|
|
></button>
|
|
</box>
|
|
}
|
|
endWidget={
|
|
<label
|
|
cssClasses={["length"]}
|
|
hexpand
|
|
label={bind(player, "length").as(v =>
|
|
secondsToFriendlyTime(v),
|
|
)}
|
|
></label>
|
|
}
|
|
></centerbox>
|
|
</box>
|
|
</box>
|
|
);
|
|
};
|
|
|
|
const secondsToFriendlyTime = (time: number) => {
|
|
const m = Math.floor(time / 60);
|
|
const minutes = Math.floor(m % 60);
|
|
const hours = Math.floor(m / 60 % 24);
|
|
const seconds = Math.floor(time % 60);
|
|
if (hours > 0) {
|
|
return `${hours}:${expandTime(minutes)}:${expandTime(seconds)}`;
|
|
} else {
|
|
return `${minutes}:${expandTime(seconds)}`;
|
|
}
|
|
};
|
|
|
|
const expandTime = (time: number): string => {
|
|
return time < 10 ? `0${time}` : "" + time;
|
|
};
|
|
|
|
export default {
|
|
PlayerModule,
|
|
};
|