From 8236bcb196e9f78fb43e436128fd25377d92c128 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Fri, 12 Jul 2024 11:35:17 +0700 Subject: [PATCH] Add mpris in bar --- modules/wm/ags/layouts/bar.js | 9 ++ modules/wm/ags/misc/circular-progress.js | 166 +++++++++++++++++++++++ modules/wm/ags/modules/mpris.js | 90 ++++++++++-- modules/wm/ags/style.css | 4 + 4 files changed, 258 insertions(+), 11 deletions(-) create mode 100644 modules/wm/ags/misc/circular-progress.js diff --git a/modules/wm/ags/layouts/bar.js b/modules/wm/ags/layouts/bar.js index e63df4a..4b44d0b 100644 --- a/modules/wm/ags/layouts/bar.js +++ b/modules/wm/ags/layouts/bar.js @@ -5,6 +5,7 @@ import * as network from "../modules/network.js"; import * as bluetooth from "../modules/bluetooth.js"; import * as battery from "../modules/battery.js"; import * as notifications from "../modules/notifications.js"; +import * as mpris from "../modules/mpris.js"; /** *@param {number} monitor @@ -44,6 +45,14 @@ export const Bar = (monitor) => endWidget: Widget.Box({ hpack: "end", children: [ + Widget.Box({ + className: "module", + css: "margin-right: 48px", + visible: mpris.activePlayer.bind().as((x) => !!x), + children: mpris.activePlayer + .bind() + .as((player) => (player ? [mpris.LinePlayer({ player })] : [])), + }), Widget.Button({ onClicked: () => App.toggleWindow("quicksettings"), className: "module quicksettings", diff --git a/modules/wm/ags/misc/circular-progress.js b/modules/wm/ags/misc/circular-progress.js new file mode 100644 index 0000000..89f1ff0 --- /dev/null +++ b/modules/wm/ags/misc/circular-progress.js @@ -0,0 +1,166 @@ +const { Gtk } = imports.gi; +const Lang = imports.lang; +import * as Utils from "resource:///com/github/Aylur/ags/utils.js"; + +// -- Styling -- +// min-height for diameter +// min-width for trough stroke +// padding for space between trough and progress +// margin for space between widget and parent +// background-color for trough color +// color for progress color +// -- Usage -- +// font size for progress value (0-100px) (hacky i know, but i want animations) +export const AnimatedCircProg = ({ + initFrom = 0, + initTo = 0, + initAnimTime = 2900, + initAnimPoints = 1, + extraSetup = () => {}, + ...rest +}) => + Widget.DrawingArea({ + ...rest, + css: `${ + initFrom !== initTo + ? `font-size: ${initFrom}px; transition: ${initAnimTime}ms linear;` + : "" + }`, + setup: (area) => { + const styleContext = area.get_style_context(); + const width = styleContext.get_property( + "min-height", + Gtk.StateFlags.NORMAL, + ); + const height = styleContext.get_property( + "min-height", + Gtk.StateFlags.NORMAL, + ); + const padding = styleContext.get_padding(Gtk.StateFlags.NORMAL).left; + const marginLeft = styleContext.get_margin(Gtk.StateFlags.NORMAL).left; + const marginRight = styleContext.get_margin(Gtk.StateFlags.NORMAL).right; + const marginTop = styleContext.get_margin(Gtk.StateFlags.NORMAL).top; + const marginBottom = styleContext.get_margin( + Gtk.StateFlags.NORMAL, + ).bottom; + area.set_size_request( + width + marginLeft + marginRight, + height + marginTop + marginBottom, + ); + area.connect( + "draw", + Lang.bind(area, (area, cr) => { + const styleContext = area.get_style_context(); + const width = styleContext.get_property( + "min-height", + Gtk.StateFlags.NORMAL, + ); + const height = styleContext.get_property( + "min-height", + Gtk.StateFlags.NORMAL, + ); + const padding = styleContext.get_padding(Gtk.StateFlags.NORMAL).left; + const marginLeft = styleContext.get_margin( + Gtk.StateFlags.NORMAL, + ).left; + const marginRight = styleContext.get_margin( + Gtk.StateFlags.NORMAL, + ).right; + const marginTop = styleContext.get_margin(Gtk.StateFlags.NORMAL).top; + const marginBottom = styleContext.get_margin( + Gtk.StateFlags.NORMAL, + ).bottom; + area.set_size_request( + width + marginLeft + marginRight, + height + marginTop + marginBottom, + ); + + const progressValue = + styleContext.get_property("font-size", Gtk.StateFlags.NORMAL) / + 100.0; + + const bg_stroke = styleContext.get_property( + "min-width", + Gtk.StateFlags.NORMAL, + ); + const fg_stroke = bg_stroke - padding; + const radius = + Math.min(width, height) / 2.0 - + Math.max(bg_stroke, fg_stroke) / 2.0; + const center_x = width / 2.0 + marginLeft; + const center_y = height / 2.0 + marginTop; + const start_angle = -Math.PI / 2.0; + const end_angle = start_angle + 2 * Math.PI * progressValue; + const start_x = center_x + Math.cos(start_angle) * radius; + const start_y = center_y + Math.sin(start_angle) * radius; + const end_x = center_x + Math.cos(end_angle) * radius; + const end_y = center_y + Math.sin(end_angle) * radius; + + // Draw background + const background_color = styleContext.get_property( + "background-color", + Gtk.StateFlags.NORMAL, + ); + cr.setSourceRGBA( + background_color.red, + background_color.green, + background_color.blue, + background_color.alpha, + ); + cr.arc(center_x, center_y, radius, 0, 2 * Math.PI); + cr.setLineWidth(bg_stroke); + cr.stroke(); + + if (progressValue == 0) return; + + // Draw progress + const color = styleContext.get_property( + "color", + Gtk.StateFlags.NORMAL, + ); + cr.setSourceRGBA(color.red, color.green, color.blue, color.alpha); + cr.arc(center_x, center_y, radius, start_angle, end_angle); + cr.setLineWidth(fg_stroke); + cr.stroke(); + + // Draw rounded ends for progress arcs + cr.setLineWidth(0); + cr.arc(start_x, start_y, fg_stroke / 2, 0, 0 - 0.01); + cr.fill(); + cr.arc(end_x, end_y, fg_stroke / 2, 0, 0 - 0.01); + cr.fill(); + }), + ); + + // Init animation + if (initFrom != initTo) { + area.css = `font-size: ${initFrom}px; transition: ${initAnimTime}ms linear;`; + Utils.timeout( + 20, + () => { + area.css = `font-size: ${initTo}px;`; + }, + area, + ); + const transitionDistance = initTo - initFrom; + const oneStep = initAnimTime / initAnimPoints; + area.css = ` + font-size: ${initFrom}px; + transition: ${oneStep}ms linear; + `; + for (let i = 0; i < initAnimPoints; i++) { + Utils.timeout(Math.max(10, i * oneStep), () => { + if (!area) return; + area.css = `${ + initFrom != initTo + ? "font-size: " + + (initFrom + (transitionDistance / initAnimPoints) * (i + 1)) + + "px;" + : "" + }`; + }); + } + } else area.css = "font-size: 0px;"; + extraSetup(area); + }, + }); diff --git a/modules/wm/ags/modules/mpris.js b/modules/wm/ags/modules/mpris.js index 59fe35e..209329d 100644 --- a/modules/wm/ags/modules/mpris.js +++ b/modules/wm/ags/modules/mpris.js @@ -1,6 +1,8 @@ +import { AnimatedCircProg } from "../misc/circular-progress.js"; + const mpris = await Service.import("mpris"); -/** @param {{player: import("../types/service/mpris").MprisPlayer} & import("../types/widgets/icon").IconProps} props */ +/** @param {{player: import("types/service/mpris").MprisPlayer} & import("../types/widgets/icon").IconProps} props */ const PlayerIcon = ({ player, ...props }) => Widget.Icon({ size: 24, @@ -18,7 +20,7 @@ const PlayerIcon = ({ player, ...props }) => ...props, }); -/** @param {{player: import("../types/service/mpris").MprisPlayer} & import("../types/widgets/label").LabelProps} props */ +/** @param {{player: import("types/service/mpris").MprisPlayer} & import("../types/widgets/label").LabelProps} props */ const TitleLabel = ({ player, ...props }) => Widget.Label({ wrap: true, @@ -28,7 +30,7 @@ const TitleLabel = ({ player, ...props }) => ...props, }); -/** @param {{player: import("../types/service/mpris").MprisPlayer} & import("../types/widgets/label").LabelProps} props */ +/** @param {{player: import("types/service/mpris").MprisPlayer} & import("../types/widgets/label").LabelProps} props */ const ArtistLabel = ({ player, ...props }) => Widget.Label({ wrap: true, @@ -38,8 +40,11 @@ const ArtistLabel = ({ player, ...props }) => ...props, }); -/** @param {{player: import("../types/service/mpris").MprisPlayer} & import("../types/widgets/button").ButtonProps} props */ -export const PlayPause = ({ player, ...props }) => +/** @param {{ + * player: import("types/service/mpris").MprisPlayer, + * iconProps?: import("types/widgets/icon").IconProps, + * } & import("types/widgets/button").ButtonProps} props */ +export const PlayPause = ({ player, iconProps = {}, ...props }) => Widget.Button({ child: Widget.Icon({ icon: player.bind("play_back_status").as( @@ -50,13 +55,14 @@ export const PlayPause = ({ player, ...props }) => Stopped: "media-playback-start-symbolic", })[x], ), + ...iconProps, }), onClicked: () => player.playPause(), visible: player.bind("can_play"), ...props, }); -/** @param {{player: import("../types/service/mpris").MprisPlayer} & import("../types/widgets/button").ButtonProps} props */ +/** @param {{player: import("types/service/mpris").MprisPlayer} & import("../types/widgets/button").ButtonProps} props */ const PreviousButton = ({ player, ...props }) => Widget.Button({ child: Widget.Icon({ icon: "media-skip-backward-symbolic" }), @@ -65,7 +71,7 @@ const PreviousButton = ({ player, ...props }) => ...props, }); -/** @param {{player: import("../types/service/mpris").MprisPlayer} & import("../types/widgets/button").ButtonProps} props */ +/** @param {{player: import("types/service/mpris").MprisPlayer} & import("../types/widgets/button").ButtonProps} props */ const NextButton = ({ player, ...props }) => Widget.Button({ child: Widget.Icon({ icon: "media-skip-forward-symbolic" }), @@ -74,7 +80,7 @@ const NextButton = ({ player, ...props }) => ...props, }); -/** @param {{player: import("../types/service/mpris").MprisPlayer} & import("../types/widgets/slider").SliderProps} props */ +/** @param {{player: import("types/service/mpris").MprisPlayer} & import("../types/widgets/slider").SliderProps} props */ const PositionSlider = ({ player, ...props }) => Widget.Slider({ className: "mpris-position-slider", @@ -95,10 +101,72 @@ const PositionSlider = ({ player, ...props }) => ...props, }); +/** @param {{player: import("types/service/mpris").MprisPlayer} & import("../types/widgets/box").BoxProps} props */ +const PositionCircle = ({ player, child, ...props }) => + Widget.Box({ + child: Widget.Overlay({ + child: AnimatedCircProg({ + css: ` + min-width: 0.136rem; + min-height: 1.636rem; + padding: 0rem; + `, + vpack: "center", + hpack: "center", + className: "accent-rev", + extraSetup: (self) => { + function update() { + const value = player.position / player.length; + console.log( + value, + player.position, + player.length, + player.name, + player.track_title, + ); + self.css = ` + font-size: ${Math.max(value * 100, 0)}px; + min-width: 0.136rem; + min-height: 1.636rem; + padding: 0rem; + `; + } + self.hook(player, update); + self.hook(player, update, "position"); + self.poll(1000, update); + }, + }), + overlays: [child], + }), + ...props, + }); + +/** @param {{player: import("types/service/mpris").MprisPlayer} & import("../types/widgets/box").BoxProps} props */ +export const LinePlayer = ({ player, ...props }) => + Widget.Box({ + children: [ + PositionCircle({ + player, + child: PlayPause({ player, iconProps: { size: 10 } }), + css: "margin-right: 12px;", + }), + Widget.Label({ + label: Utils.merge( + [player.bind("track_title"), player.bind("track_artists")], + (title, artists) => `${title} - ${artists.join(", ")}`, + ), + maxWidthChars: 30, + wrap: true, + truncate: "end", + hpack: "start", + }), + ], + ...props, + }); + export const activePlayer = Variable(mpris.players[0]); mpris.connect("player-added", (_, bus) => { mpris.getPlayer(bus)?.connect("changed", (player) => { - console.log("Player changed", bus, player); if (player?.play_back_status !== "Stopped") { activePlayer.value = player || mpris.players[0]; } else { @@ -107,7 +175,7 @@ mpris.connect("player-added", (_, bus) => { }); }); -/** @param {{player?: import("../types/service/mpris").MprisPlayer | null} & import("../types/widgets/box").BoxProps} props */ +/** @param {{player?: import("types/service/mpris").MprisPlayer | null} & import("../types/widgets/box").BoxProps} props */ export const MprisPlayer = ({ player, ...props }) => { if (!player) return Widget.Box({ visible: false }); const colors = getMaterialColors(player); @@ -203,7 +271,7 @@ const ret = Variable({ background: "#222222", onBackground: "#ffffff", }); -/** @param {import("../types/service/mpris").MprisPlayer} player */ +/** @param {import("types/service/mpris").MprisPlayer} player */ export const getMaterialColors = (player) => { // TODO: Move that to a hook to allow graceful disconnections player.connect("changed", (player) => { diff --git a/modules/wm/ags/style.css b/modules/wm/ags/style.css index 2eb60c8..74921f9 100644 --- a/modules/wm/ags/style.css +++ b/modules/wm/ags/style.css @@ -24,6 +24,10 @@ button { background-color: #94e2d5; color: #1e1e2e; } +.accent-rev { + color: #94e2d5; + background-color: #1e1e2e; +} .secondary { background-color: #cba6f7; color: #1e1e2e;