From d259a3940e48eed10fa6a44c3d50aa93aa32a942 Mon Sep 17 00:00:00 2001 From: matt1432 Date: Fri, 15 Sep 2023 23:22:16 -0400 Subject: [PATCH] feat(ags): use aylur's config's media player logic --- config/ags/js/media-player/mpris.js | 237 +++++++++++++++++++++++++++ config/ags/js/media-player/player.js | 105 ++++++++++++ config/ags/js/quick-settings/main.js | 32 +++- config/ags/scss/main.scss | 1 + config/ags/scss/widgets/player.scss | 189 +++++++++++++++++++++ config/ags/style.css | 144 ++++++++++++++++ nixos/cfg/packages.nix | 1 + 7 files changed, 706 insertions(+), 3 deletions(-) create mode 100644 config/ags/js/media-player/mpris.js create mode 100644 config/ags/js/media-player/player.js create mode 100644 config/ags/scss/widgets/player.scss diff --git a/config/ags/js/media-player/mpris.js b/config/ags/js/media-player/mpris.js new file mode 100644 index 00000000..bd9008bd --- /dev/null +++ b/config/ags/js/media-player/mpris.js @@ -0,0 +1,237 @@ +const { CACHE_DIR, execAsync, ensureDirectory, lookUpIcon } = ags.Utils; +const { Button, Icon, Label, Box, Stack, Slider } = ags.Widget; +const { GLib } = imports.gi; + +const icons = { + mpris: { + fallback: 'audio-x-generic-symbolic', + shuffle: { + enabled: '󰒟', + disabled: '󰒟', + }, + loop: { + none: '󰓦', + track: '󰓦', + playlist: '󰑐', + }, + playing: '󰏦', + paused: '󰐍', + stopped: '󰐍', + prev: '󰒮', + next: '󰒭', + }, +} + +const MEDIA_CACHE_PATH = CACHE_DIR + '/media'; + +export const CoverArt = (player, props) => Box({ + ...props, + className: 'cover', + connections: [[player, box => { + box.setStyle(`background-image: url("${player.coverPath}")`); + }]], +}); + +export const BlurredCoverArt = (player, props) => Box({ + ...props, + className: 'blurred-cover', + connections: [[player, box => { + const url = player.coverPath; + if (!url) + return; + + const blurredPath = MEDIA_CACHE_PATH + '/blurred'; + const blurred = blurredPath + + url.substring(MEDIA_CACHE_PATH.length); + + if (GLib.file_test(blurred, GLib.FileTest.EXISTS)) { + box.setStyle(`background-image: url("${blurred}")`); + return; + } + + ensureDirectory(blurredPath); + execAsync(['convert', url, '-blur', '0x22', blurred]) + .then(() => box.setStyle(`background-image: url("${blurred}")`)) + .catch(() => { }); + }]], +}); + +export const TitleLabel = (player, props) => Label({ + ...props, + className: 'title', + binds: [['label', player, 'trackTitle']], +}); + +export const ArtistLabel = (player, props) => Label({ + ...props, + className: 'artist', + connections: [[player, label => { + label.label = player.trackArtists.join(', ') || ''; + }]], +}); + +export const PlayerIcon = (player, { symbolic = true, ...props } = {}) => Icon({ + ...props, + className: 'player-icon', + tooltipText: player.indentity || '', + connections: [[player, icon => { + const name = `${player.entry}${symbolic ? '-symbolic' : ''}`; + lookUpIcon(name) + ? icon.icon = name + : icon.icon = icons.mpris.fallback; + }]], +}); + +export const PositionSlider = (player, props) => Slider({ + ...props, + className: 'position-slider', + drawValue: false, + onChange: ({ value }) => { + player.position = player.length * value; + }, + properties: [['update', slider => { + if (slider.dragging) + return; + + slider.visible = player.length > 0; + if (player.length > 0) + slider.value = player.position / player.length; + }]], + connections: [ + [player, s => s._update(s), 'position'], + [1000, s => s._update(s)], + ], +}); + +function lengthStr(length) { + const min = Math.floor(length / 60); + const sec0 = Math.floor(length % 60) < 10 ? '0' : ''; + const sec = Math.floor(length % 60); + return `${min}:${sec0}${sec}`; +} + +export const PositionLabel = player => Label({ + properties: [['update', label => { + player.length > 0 + ? label.label = lengthStr(player.position) + : label.visible = !!player; + }]], + connections: [ + [player, l => l._update(l), 'position'], + [1000, l => l._update(l)], + ], +}); + +export const LengthLabel = player => Label({ + connections: [[player, label => { + player.length > 0 + ? label.label = lengthStr(player.length) + : label.visible = !!player; + }]], +}); + +export const Slash = player => Label({ + label: '/', + connections: [[player, label => { + label.visible = player.length > 0; + }]], +}); + +const PlayerButton = ({ player, items, onClick, prop, canProp, cantValue }) => Button({ + child: Stack({ items }), + onClicked: () => player[onClick](), + connections: [[player, button => { + button.visible = player[canProp] !== cantValue; + button.child.shown = `${player[prop]}`; + }]], +}); + +export const ShuffleButton = player => PlayerButton({ + player, + items: [ + ['true', Label({ + className: 'shuffle enabled', + label: icons.mpris.shuffle.enabled, + })], + ['false', Label({ + className: 'shuffle disabled', + label: icons.mpris.shuffle.disabled, + })], + ], + onClick: 'shuffle', + prop: 'shuffleStatus', + canProp: 'shuffleStatus', + cantValue: null, +}); + +export const LoopButton = player => PlayerButton({ + player, + items: [ + ['None', Label({ + className: 'loop none', + label: icons.mpris.loop.none, + })], + ['Track', Label({ + className: 'loop track', + label: icons.mpris.loop.track, + })], + ['Playlist', Label({ + className: 'loop playlist', + label: icons.mpris.loop.playlist, + })], + ], + onClick: 'loop', + prop: 'loopStatus', + canProp: 'loopStatus', + cantValue: null, +}); + +export const PlayPauseButton = player => PlayerButton({ + player, + items: [ + ['Playing', Label({ + className: 'playing', + label: icons.mpris.playing, + })], + ['Paused', Label({ + className: 'paused', + label: icons.mpris.paused, + })], + ['Stopped', Label({ + className: 'stopped', + label: icons.mpris.stopped, + })], + ], + onClick: 'playPause', + prop: 'playBackStatus', + canProp: 'canPlay', + cantValue: false, +}); + +export const PreviousButton = player => PlayerButton({ + player, + items: [ + ['true', Label({ + className: 'previous', + label: icons.mpris.prev, + })], + ], + onClick: 'previous', + prop: 'canGoPrev', + canProp: 'canGoPrev', + cantValue: false, +}); + +export const NextButton = player => PlayerButton({ + player, + items: [ + ['true', Label({ + className: 'next', + label: icons.mpris.next, + })], + ], + onClick: 'next', + prop: 'canGoNext', + canProp: 'canGoNext', + cantValue: false, +}); diff --git a/config/ags/js/media-player/player.js b/config/ags/js/media-player/player.js new file mode 100644 index 00000000..56669a76 --- /dev/null +++ b/config/ags/js/media-player/player.js @@ -0,0 +1,105 @@ +import * as mpris from './mpris.js'; +const { Mpris } = ags.Service; +const { Box, CenterBox } = ags.Widget; + +const Footer = player => CenterBox({ + className: 'footer-box', + children: [ + Box({ + className: 'position', + children: [ + mpris.PositionLabel(player), + mpris.Slash(player), + mpris.LengthLabel(player), + ], + }), + Box({ + className: 'controls', + children: [ + mpris.ShuffleButton(player), + mpris.PreviousButton(player), + mpris.PlayPauseButton(player), + mpris.NextButton(player), + mpris.LoopButton(player), + ], + }), + mpris.PlayerIcon(player, { + symbolic: false, + hexpand: true, + halign: 'end', + }), + ], +}); + +const TextBox = player => Box({ + children: [ + mpris.CoverArt(player, { + halign: 'end', + hexpand: false, + child: Box({ + className: 'shader', + hexpand: true, + }), + }), + Box({ + hexpand: true, + vertical: true, + className: 'labels', + children: [ + mpris.TitleLabel(player, { + xalign: 0, + justification: 'left', + wrap: true, + }), + mpris.ArtistLabel(player, { + xalign: 0, + justification: 'left', + wrap: true, + }), + ], + }), + ], +}); + +const PlayerBox = player => Box({ + className: `player ${player.name}`, + children: [ + mpris.BlurredCoverArt(player, { + className: 'cover-art-bg', + hexpand: true, + children: [Box({ + className: 'shader', + hexpand: true, + vertical: true, + children: [ + TextBox(player), + mpris.PositionSlider(player), + Footer(player), + ], + })], + }), + ], +}); + +export default () => Box({ + vertical: true, + className: 'media', + properties: [['players', new Map()]], + connections: [ + [Mpris, (box, busName) => { + if (!busName || box._players.has(busName)) + return; + + const player = Mpris.getPlayer(busName); + box._players.set(busName, PlayerBox(player)); + box.children = Array.from(box._players.values()); + }, 'player-added'], + [Mpris, (box, busName) => { + if (!busName || !box._players.has(busName)) + return; + + box._players.delete(busName); + box.children = Array.from(box._players.values()); + }, 'player-closed'], + ], +}); diff --git a/config/ags/js/quick-settings/main.js b/config/ags/js/quick-settings/main.js index a823e6f0..545aa972 100644 --- a/config/ags/js/quick-settings/main.js +++ b/config/ags/js/quick-settings/main.js @@ -1,8 +1,10 @@ -const { Window, CenterBox, Box, Label } = ags.Widget; +const { Window, CenterBox, Box, Label, Revealer, Icon } = ags.Widget; +const { ToggleButton } = imports.gi.Gtk; import { ButtonGrid } from './button-grid.js'; import { SliderBox } from './slider-box.js'; -//import { Player } from +import Player from '../media-player/player.js'; +import { EventBox } from '../misc/cursorbox.js'; export const QuickSettings = Window({ name: 'quick-settings', @@ -31,10 +33,34 @@ export const QuickSettings = Window({ SliderBox, + EventBox({ + child: ags.Widget({ + type: ToggleButton, + connections: [['toggled', button => { + if (button.get_active()) { + button.child.setStyle("-gtk-icon-transform: rotate(0deg);"); + button.get_parent().get_parent().get_parent().children[1].revealChild = true; + } + else { + button.child.setStyle('-gtk-icon-transform: rotate(180deg);'); + button.get_parent().get_parent().get_parent().children[1].revealChild = false; + } + }]], + child: Icon({ + icon: 'folder-download-symbolic', + className: 'arrow', + style: `-gtk-icon-transform: rotate(180deg);`, + }), + }), + }), + ], }), - //Player, + Revealer({ + transition: 'slide_down', + child: Player(), + }) ], }), diff --git a/config/ags/scss/main.scss b/config/ags/scss/main.scss index 552a9cba..2a8c9a4d 100644 --- a/config/ags/scss/main.scss +++ b/config/ags/scss/main.scss @@ -11,3 +11,4 @@ @import "./widgets/notification.scss"; @import "./widgets/date.scss"; @import "./widgets/quick-settings.scss"; +@import "./widgets/player.scss"; diff --git a/config/ags/scss/widgets/player.scss b/config/ags/scss/widgets/player.scss new file mode 100644 index 00000000..da9c7b95 --- /dev/null +++ b/config/ags/scss/widgets/player.scss @@ -0,0 +1,189 @@ +.arrow { + transition: -gtk-icon-transform 0.3s ease-in-out; + margin-bottom: 5px; +} + +.media .player { + all: unset; + border-radius: 9px; + color: #eee; + background-color: rgba(238, 238, 238, 0.06); + border: 1px solid rgba(238, 238, 238, 0.03); + margin-top: 9px; + * { + font-size: 16px; + font-family: "Ubuntu Nerd Font", sans-serif; + } + label { + color: white; + text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.6); + } + .shader { + all: unset; + box-shadow: inset 0 0 3em 1em rgba(23, 23, 23, 0.7); + * { + font-size: 16px; + font-family: "Ubuntu Nerd Font", sans-serif; + } + label { + color: white; + text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.6); + } + } + .cover { + border-radius: 7.2px; + min-height: 100px; + min-width: 100px; + box-shadow: 2px 2px 2px 0 rgba(0, 0, 0, 0.6); + margin: 9px; + margin-bottom: 0; + .shader { + background-color: transparent; + border-radius: 6.2px; + box-shadow: inset 0 0 0 999px rgba(23, 23, 23, 0.2); + } + } + .blurred-cover, .cover { + background-size: cover; + background-position: center; + border-radius: 8px; + } + .labels { + margin-top: 9px; + label { + font-size: 1.1em; + text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.6); + &.title { + font-weight: bold; + } + } + } + .position-slider { + all: unset; + margin: 9px 0; + * { + font-size: 16px; + font-family: "Ubuntu Nerd Font", sans-serif; + } + * { + all: unset; + } + trough { + transition: 200ms; + border-radius: 0; + border: 1px solid rgba(238, 238, 238, 0.03); + background-color: rgba(238, 238, 238, 0.06); + min-height: 0.4em; + min-width: 0.4em; + highlight, progress { + border-radius: 0; + background-image: linear-gradient(white, white); + min-height: 0.4em; + min-width: 0.4em; + } + &:focus { + background-color: rgba(238, 238, 238, 0.154); + box-shadow: inset 0 0 0 1px #51a4e7; + } + } + slider { + box-shadow: none; + background-color: transparent; + border: 1px solid transparent; + transition: 200ms; + border-radius: 0; + min-height: 0.4em; + min-width: 0.4em; + margin: -0.5em; + } + &:hover trough { + background-color: rgba(238, 238, 238, 0.154); + } + &:disabled highlight, &:disabled progress { + background-color: rgba(238, 238, 238, 0.6); + background-image: none; + } + trough { + border: none; + background-color: rgba(255, 255, 255, 0.3); + } + } + .footer-box { + margin: -4.5px 9px 4.5px; + image { + -gtk-icon-shadow: 2px 2px 2px rgba(0, 0, 0, 0.6); + } + } + .controls button { + all: unset; + label { + font-size: 2em; + color: rgba(255, 255, 255, 0.8); + transition: 200ms; + &.shuffle, &.loop { + font-size: 1.4em; + } + } + &:hover label { + color: rgba(255, 255, 255, 0.9); + } + &:active label { + color: white; + } + } + &.spotify button .shuffle.enabled { + color: #43c383; + } + &.spotify button .loop.playlist, &.spotify button .loop.track { + color: #43c383; + } + &.spotify button:active label { + color: #43c383; + } + &.spotify .position-slider:hover trough { + background-color: rgba(67, 195, 131, 0.5); + } + &.spotify .player-icon { + color: #43c383; + } + &.firefox button .shuffle.enabled { + color: #E79E64; + } + &.firefox button .loop.playlist, &.firefox button .loop.track { + color: #E79E64; + } + &.firefox button:active label { + color: #E79E64; + } + &.firefox .position-slider:hover trough { + background-color: rgba(231, 158, 100, 0.5); + } + &.firefox .player-icon { + color: #E79E64; + } + &.mpv button .shuffle.enabled { + color: #9077e7; + } + &.mpv button .loop.playlist, &.mpv button .loop.track { + color: #9077e7; + } + &.mpv button:active label { + color: #9077e7; + } + &.mpv .position-slider:hover trough { + background-color: rgba(144, 119, 231, 0.5); + } + &.mpv .player-icon { + color: #9077e7; + } +} +.media.spotify image { + color: #43c383; +} +.media.firefox image { + color: #E79E64; +} +.media.mpv image { + color: #9077e7; +} + diff --git a/config/ags/style.css b/config/ags/style.css index 20271942..a73cb15e 100644 --- a/config/ags/style.css +++ b/config/ags/style.css @@ -588,3 +588,147 @@ calendar:indeterminate { .slider-box scale slider:hover { background-color: #303240; transition: background-color 0.5s ease-in-out; } + +.arrow { + transition: -gtk-icon-transform 0.3s ease-in-out; + margin-bottom: 5px; } + +.media .player { + all: unset; + border-radius: 9px; + color: #eee; + background-color: rgba(238, 238, 238, 0.06); + border: 1px solid rgba(238, 238, 238, 0.03); + margin-top: 9px; } + .media .player * { + font-size: 16px; + font-family: "Ubuntu Nerd Font", sans-serif; } + .media .player label { + color: white; + text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.6); } + .media .player .shader { + all: unset; + box-shadow: inset 0 0 3em 1em rgba(23, 23, 23, 0.7); } + .media .player .shader * { + font-size: 16px; + font-family: "Ubuntu Nerd Font", sans-serif; } + .media .player .shader label { + color: white; + text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.6); } + .media .player .cover { + border-radius: 7.2px; + min-height: 100px; + min-width: 100px; + box-shadow: 2px 2px 2px 0 rgba(0, 0, 0, 0.6); + margin: 9px; + margin-bottom: 0; } + .media .player .cover .shader { + background-color: transparent; + border-radius: 6.2px; + box-shadow: inset 0 0 0 999px rgba(23, 23, 23, 0.2); } + .media .player .blurred-cover, .media .player .cover { + background-size: cover; + background-position: center; + border-radius: 8px; } + .media .player .labels { + margin-top: 9px; } + .media .player .labels label { + font-size: 1.1em; + text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.6); } + .media .player .labels label.title { + font-weight: bold; } + .media .player .position-slider { + all: unset; + margin: 9px 0; } + .media .player .position-slider * { + font-size: 16px; + font-family: "Ubuntu Nerd Font", sans-serif; } + .media .player .position-slider * { + all: unset; } + .media .player .position-slider trough { + transition: 200ms; + border-radius: 0; + border: 1px solid rgba(238, 238, 238, 0.03); + background-color: rgba(238, 238, 238, 0.06); + min-height: 0.4em; + min-width: 0.4em; } + .media .player .position-slider trough highlight, .media .player .position-slider trough progress { + border-radius: 0; + background-image: linear-gradient(white, white); + min-height: 0.4em; + min-width: 0.4em; } + .media .player .position-slider trough:focus { + background-color: rgba(238, 238, 238, 0.154); + box-shadow: inset 0 0 0 1px #51a4e7; } + .media .player .position-slider slider { + box-shadow: none; + background-color: transparent; + border: 1px solid transparent; + transition: 200ms; + border-radius: 0; + min-height: 0.4em; + min-width: 0.4em; + margin: -0.5em; } + .media .player .position-slider:hover trough { + background-color: rgba(238, 238, 238, 0.154); } + .media .player .position-slider:disabled highlight, .media .player .position-slider:disabled progress { + background-color: rgba(238, 238, 238, 0.6); + background-image: none; } + .media .player .position-slider trough { + border: none; + background-color: rgba(255, 255, 255, 0.3); } + .media .player .footer-box { + margin: -4.5px 9px 4.5px; } + .media .player .footer-box image { + -gtk-icon-shadow: 2px 2px 2px rgba(0, 0, 0, 0.6); } + .media .player .controls button { + all: unset; } + .media .player .controls button label { + font-size: 2em; + color: rgba(255, 255, 255, 0.8); + transition: 200ms; } + .media .player .controls button label.shuffle, .media .player .controls button label.loop { + font-size: 1.4em; } + .media .player .controls button:hover label { + color: rgba(255, 255, 255, 0.9); } + .media .player .controls button:active label { + color: white; } + .media .player.spotify button .shuffle.enabled { + color: #43c383; } + .media .player.spotify button .loop.playlist, .media .player.spotify button .loop.track { + color: #43c383; } + .media .player.spotify button:active label { + color: #43c383; } + .media .player.spotify .position-slider:hover trough { + background-color: rgba(67, 195, 131, 0.5); } + .media .player.spotify .player-icon { + color: #43c383; } + .media .player.firefox button .shuffle.enabled { + color: #E79E64; } + .media .player.firefox button .loop.playlist, .media .player.firefox button .loop.track { + color: #E79E64; } + .media .player.firefox button:active label { + color: #E79E64; } + .media .player.firefox .position-slider:hover trough { + background-color: rgba(231, 158, 100, 0.5); } + .media .player.firefox .player-icon { + color: #E79E64; } + .media .player.mpv button .shuffle.enabled { + color: #9077e7; } + .media .player.mpv button .loop.playlist, .media .player.mpv button .loop.track { + color: #9077e7; } + .media .player.mpv button:active label { + color: #9077e7; } + .media .player.mpv .position-slider:hover trough { + background-color: rgba(144, 119, 231, 0.5); } + .media .player.mpv .player-icon { + color: #9077e7; } + +.media.spotify image { + color: #43c383; } + +.media.firefox image { + color: #E79E64; } + +.media.mpv image { + color: #9077e7; } diff --git a/nixos/cfg/packages.nix b/nixos/cfg/packages.nix index c3bcb492..46ae3eac 100644 --- a/nixos/cfg/packages.nix +++ b/nixos/cfg/packages.nix @@ -41,6 +41,7 @@ libinput.enable = true; }; dbus.enable = true; + gvfs.enable = true; flatpak.enable = true; tlp.enable = true;