diff options
| author | 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> | 2025-01-25 16:54:43 +1100 |
|---|---|---|
| committer | 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> | 2025-01-25 16:54:43 +1100 |
| commit | 0eacbc0ea2b50868d1127bc9a5265d4352632e58 (patch) | |
| tree | ad7bad03e73fea6d40dee358dde0ca2c7830746c /src/modules/popdowns | |
| parent | launcher: change windows mode icon (diff) | |
| download | caelestia-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.tsx | 2 | ||||
| -rw-r--r-- | src/modules/popdowns/media.tsx | 140 |
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> + ); +}; |