summaryrefslogtreecommitdiff
path: root/src/modules/popdowns
diff options
context:
space:
mode:
author2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-01-25 16:54:43 +1100
committer2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-01-25 16:54:43 +1100
commit0eacbc0ea2b50868d1127bc9a5265d4352632e58 (patch)
treead7bad03e73fea6d40dee358dde0ca2c7830746c /src/modules/popdowns
parentlauncher: change windows mode icon (diff)
downloadcaelestia-shell-0eacbc0ea2b50868d1127bc9a5265d4352632e58.tar.gz
caelestia-shell-0eacbc0ea2b50868d1127bc9a5265d4352632e58.tar.bz2
caelestia-shell-0eacbc0ea2b50868d1127bc9a5265d4352632e58.zip
media: make popdown window
Diffstat (limited to 'src/modules/popdowns')
-rw-r--r--src/modules/popdowns/index.tsx2
-rw-r--r--src/modules/popdowns/media.tsx140
2 files changed, 142 insertions, 0 deletions
diff --git a/src/modules/popdowns/index.tsx b/src/modules/popdowns/index.tsx
index 44668a8..9ee9f77 100644
--- a/src/modules/popdowns/index.tsx
+++ b/src/modules/popdowns/index.tsx
@@ -1,4 +1,5 @@
import BluetoothDevices from "./bluetoothdevices";
+import Media from "./media";
import Networks from "./networks";
import Notifications from "./notifications";
import Updates from "./updates";
@@ -8,6 +9,7 @@ export default () => {
<Updates />;
<BluetoothDevices />;
<Networks />;
+ <Media />;
return null;
};
diff --git a/src/modules/popdowns/media.tsx b/src/modules/popdowns/media.tsx
new file mode 100644
index 0000000..1f73cfa
--- /dev/null
+++ b/src/modules/popdowns/media.tsx
@@ -0,0 +1,140 @@
+import { bind, Variable } from "astal";
+import { Astal, Gtk } from "astal/gtk3";
+import AstalMpris from "gi://AstalMpris";
+import Players from "../../services/players";
+import { isRealPlayer } from "../../utils/mpris";
+import PopupWindow from "../../widgets/popupwindow";
+
+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, "artUrl").as(u => 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={player.canControl}>
+ <icon icon={bind(player, "shuffleStatus").as(shuffleToIcon)} />
+ </button>
+ <button cursor="pointer" onClicked={() => player.previous()} sensitive={player.canGoPrevious}>
+ <icon icon="caelestia-skip-previous-symbolic" />
+ </button>
+ <button cursor="pointer" onClicked={() => player.play_pause()} sensitive={player.canControl}>
+ <icon icon={bind(player, "playbackStatus").as(playbackToIcon)} />
+ </button>
+ <button cursor="pointer" onClicked={() => player.next()} sensitive={player.canGoNext}>
+ <icon icon="caelestia-skip-next-symbolic" />
+ </button>
+ <button cursor="pointer" onClicked={() => player.loop()} sensitive={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>
+ );
+};