diff --git a/ags/media/Media.js b/ags/media/Media.js new file mode 100644 index 0000000..337656d --- /dev/null +++ b/ags/media/Media.js @@ -0,0 +1,153 @@ +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/ags/media/README.md b/ags/media/README.md new file mode 100644 index 0000000..0db707c --- /dev/null +++ b/ags/media/README.md @@ -0,0 +1,18 @@ +# Media Widget + +setup + +```bash +mkdir -p ~/.config/ags +git clone https://github.com/Aylur/ags.git /tmp/ags +cp -r /tmp/ags/example/media-widget/* ~/.config/ags + +# optionally setup types +ags --init -c ~/.config/ags/config.js +``` + +running + +```bash +ags -c ~/.config/ags/config.js & +``` diff --git a/ags/media/config.js b/ags/media/config.js new file mode 100644 index 0000000..5f77f05 --- /dev/null +++ b/ags/media/config.js @@ -0,0 +1,13 @@ +import { Media } from "./Media.js" + +const win = Widget.Window({ + name: "mpris", + css: "border-radius: 16px;", + anchor: ["top", "right"], + child: Media(), +}) + +App.config({ + style: "./style.css", + windows: [win], +}) diff --git a/ags/media/style.css b/ags/media/style.css new file mode 100644 index 0000000..8488ac8 --- /dev/null +++ b/ags/media/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; +} diff --git a/ags/media/toggle_media b/ags/media/toggle_media new file mode 100755 index 0000000..b4bbcae --- /dev/null +++ b/ags/media/toggle_media @@ -0,0 +1,9 @@ +#!/bin/sh + +if [ ! -f ~/.ags_media ]; then + ags --bus-name media -c ~/dotfiles/ags/media/config.js & + echo $! > ~/.ags_media +else + ags --bus-name media -c ~/dotfiles/ags/media/config.js --quit + rm ~/.ags_media +fi diff --git a/ags/notification-popups/README.md b/ags/notification-popups/README.md new file mode 100644 index 0000000..ea184d6 --- /dev/null +++ b/ags/notification-popups/README.md @@ -0,0 +1,18 @@ +# Notification Popups + +setup + +```bash +mkdir -p ~/.config/ags +git clone https://github.com/Aylur/ags.git /tmp/ags +cp -r /tmp/ags/example/notification-popups/* ~/.config/ags + +# optionally setup types +ags --init -c ~/.config/ags/config.js +``` + +running + +```bash +ags -c ~/.config/ags/config.js & +``` \ No newline at end of file diff --git a/ags/notification-popups/config.js b/ags/notification-popups/config.js new file mode 100644 index 0000000..47eeb47 --- /dev/null +++ b/ags/notification-popups/config.js @@ -0,0 +1,18 @@ +import { NotificationPopups } from "./notificationPopups.js" + +Utils.timeout(100, () => Utils.notify({ + summary: "Notification Popup Example", + iconName: "info-symbolic", + body: "Lorem ipsum dolor sit amet, qui minim labore adipisicing " + + "minim sint cillum sint consectetur cupidatat.", + actions: { + "Cool": () => print("pressed Cool"), + }, +})) + +App.config({ + style: App.configDir + "/style.css", + windows: [ + NotificationPopups(), + ], +}) \ No newline at end of file diff --git a/ags/notification-popups/notificationPopups.js b/ags/notification-popups/notificationPopups.js new file mode 100644 index 0000000..4c63d74 --- /dev/null +++ b/ags/notification-popups/notificationPopups.js @@ -0,0 +1,130 @@ +const notifications = await Service.import("notifications") + +/** @param {import('resource:///com/github/Aylur/ags/service/notifications.js').Notification} n */ +function NotificationIcon({ app_entry, app_icon, image }) { + if (image) { + return Widget.Box({ + css: `background-image: url("${image}");` + + "background-size: contain;" + + "background-repeat: no-repeat;" + + "background-position: center;", + }) + } + + let icon = "dialog-information-symbolic" + if (Utils.lookUpIcon(app_icon)) + icon = app_icon + + if (app_entry && Utils.lookUpIcon(app_entry)) + icon = app_entry + + return Widget.Box({ + child: Widget.Icon(icon), + }) +} + +/** @param {import('resource:///com/github/Aylur/ags/service/notifications.js').Notification} n */ +function Notification(n) { + const icon = Widget.Box({ + vpack: "start", + class_name: "icon", + child: NotificationIcon(n), + }) + + const title = Widget.Label({ + class_name: "title", + xalign: 0, + justification: "left", + hexpand: true, + max_width_chars: 24, + truncate: "end", + wrap: true, + label: n.summary, + use_markup: true, + }) + + const body = Widget.Label({ + class_name: "body", + hexpand: true, + use_markup: true, + xalign: 0, + justification: "left", + label: n.body, + wrap: true, + }) + + const actions = Widget.Box({ + class_name: "actions", + children: n.actions.map(({ id, label }) => Widget.Button({ + class_name: "action-button", + on_clicked: () => { + n.invoke(id) + n.dismiss() + }, + hexpand: true, + child: Widget.Label(label), + })), + }) + + return Widget.EventBox( + { + attribute: { id: n.id }, + on_primary_click: n.dismiss, + }, + Widget.Box( + { + class_name: `notification ${n.urgency}`, + vertical: true, + }, + Widget.Box([ + icon, + Widget.Box( + { vertical: true }, + title, + body, + ), + ]), + actions, + ), + ) +} + +export function NotificationPopups(monitor = 0) { + const list = Widget.Box({ + vertical: true, + children: notifications.popups.map(Notification), + }) + + function onNotified(_, /** @type {number} */ id) { + const n = notifications.getNotification(id) + if (n) + list.children = [Notification(n), ...list.children] + } + + function onDismissed(_, /** @type {number} */ id) { + list.children.find(n => n.attribute.id === id)?.destroy() + } + + list.hook(notifications, onNotified, "notified") + .hook(notifications, onDismissed, "dismissed") + + return Widget.Window({ + monitor, + name: `notifications${monitor}`, + class_name: "notification-popups", + anchor: ["top", "right"], + child: Widget.Box({ + css: "min-width: 2px; min-height: 2px;", + class_name: "notifications", + vertical: true, + child: list, + + /** this is a simple one liner that could be used instead of + hooking into the 'notified' and 'dismissed' signals. + but its not very optimized becuase it will recreate + the whole list everytime a notification is added or dismissed */ + // children: notifications.bind('popups') + // .as(popups => popups.map(Notification)) + }), + }) +} \ No newline at end of file diff --git a/ags/notification-popups/start_notification_service b/ags/notification-popups/start_notification_service new file mode 100755 index 0000000..9f83402 --- /dev/null +++ b/ags/notification-popups/start_notification_service @@ -0,0 +1,9 @@ +#!/bin/sh + +if [ ! -f ~/.ags_notification_service ]; then + ags --bus-name notification_service -c ~/dotfiles/ags/notification-popups/config.js & + echo $! > ~/.ags_notification_service +else + ags --bus-name notification_service -c ~/dotfiles/ags/notification-popups/config.js --quit + rm ~/.ags_notification_service +fi diff --git a/ags/notification-popups/style.css b/ags/notification-popups/style.css new file mode 100644 index 0000000..7f1ff0b --- /dev/null +++ b/ags/notification-popups/style.css @@ -0,0 +1,57 @@ +window.notification-popups box.notifications { + padding: .5em; +} + +.icon { + min-width: 68px; + min-height: 68px; + margin-right: 1em; +} + +.icon image { + font-size: 58px; + /* to center the icon */ + margin: 5px; + color: @theme_fg_color; +} + +.icon box { + min-width: 68px; + min-height: 68px; + border-radius: 7px; +} + +.notification { + min-width: 350px; + border-radius: 11px; + padding: 1em; + margin: .5em; + border: 1px solid @wm_borders_edge; + background-color: @theme_bg_color; +} + +.notification.critical { + border: 1px solid lightcoral; +} + +.title { + color: @theme_fg_color; + font-size: 1.4em; +} + +.body { + color: @theme_unfocused_fg_color; +} + +.actions .action-button { + margin: 0 .4em; + margin-top: .8em; +} + +.actions .action-button:first-child { + margin-left: 0; +} + +.actions .action-button:last-child { + margin-right: 0; +} \ No newline at end of file diff --git a/hypr/keybinds.conf b/hypr/keybinds.conf index a2d90ac..268f009 100644 --- a/hypr/keybinds.conf +++ b/hypr/keybinds.conf @@ -18,7 +18,7 @@ bind = $mainMod SHIFT, O, exec, $onscreenkeyboard bind = $mainMod, S, exec, spotify bind = $mainMod, Y, exec, $calculator bind = $mainMod, E, exec, $emojipicker -bind = $mainMod, M, exec, ~/dotfiles/eww/toggle_music +bind = $mainMod, M, exec, ~/dotfiles/ags/media/toggle_media #--------------------------------------------------- #-------------- windows -------------------------- diff --git a/hypr/startup.conf b/hypr/startup.conf index 63c2ed5..973a673 100644 --- a/hypr/startup.conf +++ b/hypr/startup.conf @@ -23,7 +23,8 @@ exec-once = systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESK exec-once = dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP #------------------------------------------------------------------------------------------------ -exec-once = mako +exec-once = ~/dotfiles/ags/notification-popups/start_notification_service +# exec-once = mako exec-once = nm-applet --indicator exec-once = greenclip daemon exec-once = uxplay diff --git a/waybar/config b/waybar/config index 81a6319..a9cb094 100644 --- a/waybar/config +++ b/waybar/config @@ -229,7 +229,7 @@ "playing": "", "paused": "" }, - "on-click-right": "~/dotfiles/eww/toggle_music", + "on-click-right": "~/dotfiles/ags/media/toggle_media", // "ignored-players": ["firefox"] } }