From 19f823b93ee9b1de22e2a3acae5dfbe70c0a1e7a Mon Sep 17 00:00:00 2001 From: Aylur Date: Sun, 7 Jan 2024 20:50:27 +0100 Subject: [PATCH] add mpris widget example --- example/media-widget/Media.js | 157 +++++++++++++++++++++++++++++++++ example/media-widget/config.js | 13 +++ example/media-widget/style.css | 49 ++++++++++ 3 files changed, 219 insertions(+) create mode 100644 example/media-widget/Media.js create mode 100644 example/media-widget/config.js create mode 100644 example/media-widget/style.css diff --git a/example/media-widget/Media.js b/example/media-widget/Media.js new file mode 100644 index 00000000..79f7d3ce --- /dev/null +++ b/example/media-widget/Media.js @@ -0,0 +1,157 @@ +import Mpris from 'resource:///com/github/Aylur/ags/service/mpris.js' +import Widget from 'resource:///com/github/Aylur/ags/widget.js'; +import Utils from 'resource:///com/github/Aylur/ags/utils.js' + +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 */ +const 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, + setup: self => { + const update = () => { + self.visible = player.length > 0; + self.value = player.position / player.length; + }; + 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', + children: [ + img, + Widget.Box({ + vertical: true, + hexpand: true, + children: [ + Widget.Box({ + children: [ + title, + icon, + ], + }), + artist, + Widget.Box({ vexpand: true }), + positionSlider, + Widget.CenterBox({ + start_widget: positionLabel, + center_widget: Widget.Box({ + children: [ + prev, + playPause, + next, + ], + }), + end_widget: lengthLabel, + }), + ], + }), + ], + }); +} + +export default () => Widget.Box({ + vertical: true, + css: 'padding: 1px', // small hack to make sure it is visible + visible: Mpris.bind('players').transform(p => p.length > 0), + children: Mpris.bind('players').transform(p => p.map(Player)), +}); diff --git a/example/media-widget/config.js b/example/media-widget/config.js new file mode 100644 index 00000000..769ffae2 --- /dev/null +++ b/example/media-widget/config.js @@ -0,0 +1,13 @@ +import App from 'resource:///com/github/Aylur/ags/app.js'; +import Widget from 'resource:///com/github/Aylur/ags/widget.js'; +import Media from './Media.js'; + +const win = Widget.Window({ + name: 'mpris', + child: Media(), +}); + +export default { + style: App.configDir + '/style.css', + windows: [win], +}; diff --git a/example/media-widget/style.css b/example/media-widget/style.css new file mode 100644 index 00000000..8488ac86 --- /dev/null +++ b/example/media-widget/style.css @@ -0,0 +1,49 @@ +.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; +}