From e0ef3cba342cf4857aae942b94684af6afd5f6dd Mon Sep 17 00:00:00 2001 From: Lilith Date: Tue, 11 Jun 2024 22:04:10 +0200 Subject: [PATCH] ags --- home/ags/default.nix | 2 +- home/ags/media-widget/Media.js | 154 ++++++++++++++++++++++++++++++++ home/ags/media-widget/config.js | 12 +++ home/ags/media-widget/style.css | 50 +++++++++++ 4 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 home/ags/media-widget/Media.js create mode 100644 home/ags/media-widget/config.js create mode 100644 home/ags/media-widget/style.css diff --git a/home/ags/default.nix b/home/ags/default.nix index 5ca0b1d1..33dd0d06 100644 --- a/home/ags/default.nix +++ b/home/ags/default.nix @@ -3,6 +3,6 @@ programs.ags = { enable = true; - configDir = ./widgets/applauncher; + configDir = ./widgets/media-widget; }; } diff --git a/home/ags/media-widget/Media.js b/home/ags/media-widget/Media.js new file mode 100644 index 00000000..4f12cf45 --- /dev/null +++ b/home/ags/media-widget/Media.js @@ -0,0 +1,154 @@ + +const mpris = await Service.import("mpris") +const players = mpris.bind("players") + +const FALLBACK_ICON = "audio-x-generic-symbolic" +const PLAY_ICON = "media-playback-start-symbolic" +const PAUSE_ICON = "media-playback-pause-symbolic" +const PREV_ICON = "media-skip-backward-symbolic" +const NEXT_ICON = "media-skip-forward-symbolic" + +/** @param {number} length */ +function lengthStr(length) { + const min = Math.floor(length / 60) + const sec = Math.floor(length % 60) + const sec0 = sec < 10 ? "0" : "" + return `${min}:${sec0}${sec}` +} + +/** @param {import('types/service/mpris').MprisPlayer} player */ +function Player(player) { + const img = Widget.Box({ + class_name: "img", + vpack: "start", + css: player.bind("cover_path").transform(p => ` + background-image: url('${p}'); + `), + }) + + const title = Widget.Label({ + class_name: "title", + wrap: true, + hpack: "start", + label: player.bind("track_title"), + }) + + const artist = Widget.Label({ + class_name: "artist", + wrap: true, + hpack: "start", + label: player.bind("track_artists").transform(a => a.join(", ")), + }) + + const positionSlider = Widget.Slider({ + class_name: "position", + draw_value: false, + on_change: ({ value }) => player.position = value * player.length, + visible: player.bind("length").as(l => l > 0), + setup: self => { + function update() { + const value = player.position / player.length + self.value = value > 0 ? value : 0 + } + self.hook(player, update) + self.hook(player, update, "position") + self.poll(1000, update) + }, + }) + + const positionLabel = Widget.Label({ + class_name: "position", + hpack: "start", + setup: self => { + const update = (_, time) => { + self.label = lengthStr(time || player.position) + self.visible = player.length > 0 + } + + self.hook(player, update, "position") + self.poll(1000, update) + }, + }) + + const lengthLabel = Widget.Label({ + class_name: "length", + hpack: "end", + visible: player.bind("length").transform(l => l > 0), + label: player.bind("length").transform(lengthStr), + }) + + const icon = Widget.Icon({ + class_name: "icon", + hexpand: true, + hpack: "end", + vpack: "start", + tooltip_text: player.identity || "", + icon: player.bind("entry").transform(entry => { + const name = `${entry}-symbolic` + return Utils.lookUpIcon(name) ? name : FALLBACK_ICON + }), + }) + + const playPause = Widget.Button({ + class_name: "play-pause", + on_clicked: () => player.playPause(), + visible: player.bind("can_play"), + child: Widget.Icon({ + icon: player.bind("play_back_status").transform(s => { + switch (s) { + case "Playing": return PAUSE_ICON + case "Paused": + case "Stopped": return PLAY_ICON + } + }), + }), + }) + + const prev = Widget.Button({ + on_clicked: () => player.previous(), + visible: player.bind("can_go_prev"), + child: Widget.Icon(PREV_ICON), + }) + + const next = Widget.Button({ + on_clicked: () => player.next(), + visible: player.bind("can_go_next"), + child: Widget.Icon(NEXT_ICON), + }) + + return Widget.Box( + { class_name: "player" }, + img, + Widget.Box( + { + vertical: true, + hexpand: true, + }, + Widget.Box([ + title, + icon, + ]), + artist, + Widget.Box({ vexpand: true }), + positionSlider, + Widget.CenterBox({ + start_widget: positionLabel, + center_widget: Widget.Box([ + prev, + playPause, + next, + ]), + end_widget: lengthLabel, + }), + ), + ) +} + +export function Media() { + return Widget.Box({ + vertical: true, + css: "min-height: 2px; min-width: 2px;", // small hack to make it visible + visible: players.as(p => p.length > 0), + children: players.as(p => p.map(Player)), + }) +} diff --git a/home/ags/media-widget/config.js b/home/ags/media-widget/config.js new file mode 100644 index 00000000..d8f5c86f --- /dev/null +++ b/home/ags/media-widget/config.js @@ -0,0 +1,12 @@ +import { Media } from "./Media.js" + +const win = Widget.Window({ + name: "mpris", + anchor: ["top", "right"], + child: Media(), +}) + +App.config({ + style: "./style.css", + windows: [win], +}) diff --git a/home/ags/media-widget/style.css b/home/ags/media-widget/style.css new file mode 100644 index 00000000..8100594a --- /dev/null +++ b/home/ags/media-widget/style.css @@ -0,0 +1,50 @@ + +.player { + padding: 10px; + min-width: 350px; +} + +.player .img { + min-width: 100px; + min-height: 100px; + background-size: cover; + background-position: center; + border-radius: 13px; + margin-right: 1em; +} + +.player .title { + font-size: 1.2em; +} + +.player .artist { + font-size: 1.1em; + color: @insensitive_fg_color; +} + +.player scale.position { + padding: 0; + margin-bottom: .3em; +} + +.player scale.position trough { + min-height: 8px; +} + +.player scale.position highlight { + background-color: @theme_fg_color; +} + +.player scale.position slider { + all: unset; +} + +.player button { + min-height: 1em; + min-width: 1em; + padding: .3em; +} + +.player button.play-pause { + margin: 0 .3em; +}