parent
7d64a1fe25
commit
2e15e10fd5
24 changed files with 1196 additions and 1026 deletions
modules/ags/config
|
@ -7,6 +7,7 @@
|
|||
@use '../widgets/clipboard';
|
||||
@use '../widgets/date';
|
||||
@use '../widgets/icon-browser';
|
||||
@use '../widgets/media-player';
|
||||
@use '../widgets/misc';
|
||||
@use '../widgets/network';
|
||||
@use '../widgets/notifs';
|
||||
|
|
124
modules/ags/config/widgets/media-player/_index.scss
Normal file
124
modules/ags/config/widgets/media-player/_index.scss
Normal file
|
@ -0,0 +1,124 @@
|
|||
@use '../../style/colors';
|
||||
|
||||
.media-player {
|
||||
margin-top: 9px;
|
||||
min-width: 100px;
|
||||
min-height: 100px;
|
||||
|
||||
* {
|
||||
all: unset;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
transition: -gtk-icon-transform 0.3s ease-in-out;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.player {
|
||||
padding: 10px;
|
||||
min-width: 400px;
|
||||
min-height: 200px;
|
||||
border-radius: 30px;
|
||||
border-top: 2px solid colors.$accent_color;
|
||||
border-bottom: 2px solid colors.$accent_color;
|
||||
transition: background 250ms;
|
||||
|
||||
.top {
|
||||
font-size: 23px;
|
||||
}
|
||||
|
||||
.metadata {
|
||||
.title {
|
||||
font-weight: 500;
|
||||
transition: text 250ms;
|
||||
}
|
||||
|
||||
.artist {
|
||||
font-weight: 400;
|
||||
font-size: 15px;
|
||||
transition: text 250ms;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.pausebutton {
|
||||
transition: background-color ease .2s, color ease .2s;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.playing {
|
||||
transition: background-color ease .2s, color ease .2s;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.stopped,
|
||||
.paused {
|
||||
transition: background-color ease .2s, color ease .2s;
|
||||
border-radius: 26px;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
button label {
|
||||
min-width: 35px;
|
||||
}
|
||||
}
|
||||
|
||||
.player-icon, .position-indicator {
|
||||
min-width: 18px;
|
||||
margin: 7px;
|
||||
}
|
||||
|
||||
.position-indicator {
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
box-shadow: 0 0 5px 0 rgba(255, 255, 255, 0.3);
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.previous,
|
||||
.next,
|
||||
.shuffle,
|
||||
.loop {
|
||||
border-radius: 100%;
|
||||
transition: color 200ms;
|
||||
|
||||
&:hover {
|
||||
border-radius: 100%;
|
||||
background-color: rgba(127, 132, 156, 0.4);
|
||||
transition: color 200ms;
|
||||
}
|
||||
}
|
||||
|
||||
.loop {
|
||||
label {
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.position-slider {
|
||||
min-height: 20px;
|
||||
|
||||
highlight {
|
||||
margin: 0;
|
||||
border-radius: 2em;
|
||||
}
|
||||
|
||||
trough {
|
||||
margin: 0 8px;
|
||||
border-radius: 2em;
|
||||
}
|
||||
|
||||
slider {
|
||||
margin: -8px;
|
||||
min-height: 20px;
|
||||
border-radius: 10px;
|
||||
transition: background-color 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
slider:hover {
|
||||
transition: background-color 0.5s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
214
modules/ags/config/widgets/media-player/gesture.tsx
Normal file
214
modules/ags/config/widgets/media-player/gesture.tsx
Normal file
|
@ -0,0 +1,214 @@
|
|||
import { timeout } from 'astal';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import { property, register } from 'astal/gobject';
|
||||
import { CenterBox, CenterBoxProps, EventBox, Overlay, OverlayProps } from 'astal/gtk3/widget';
|
||||
|
||||
import Mpris from 'gi://AstalMpris';
|
||||
|
||||
const MAX_OFFSET = 200;
|
||||
const OFFSCREEN = 500;
|
||||
const ANIM_DURATION = 500;
|
||||
const TRANSITION = `transition: margin ${ANIM_DURATION}ms ease,
|
||||
opacity ${ANIM_DURATION}ms ease;`;
|
||||
|
||||
/* Types */
|
||||
export interface Gesture {
|
||||
attribute?: object
|
||||
setup?: (self: PlayerGesture) => void
|
||||
props?: OverlayProps
|
||||
}
|
||||
|
||||
|
||||
@register()
|
||||
export class PlayerBox extends CenterBox {
|
||||
@property(String)
|
||||
declare bgStyle: string;
|
||||
|
||||
@property(Object)
|
||||
declare player: Mpris.Player;
|
||||
|
||||
constructor(props: Omit<CenterBoxProps, 'setup'> & {
|
||||
bgStyle?: string
|
||||
player?: Mpris.Player
|
||||
setup?: (self: PlayerBox) => void
|
||||
}) {
|
||||
super(props as CenterBoxProps);
|
||||
}
|
||||
}
|
||||
|
||||
@register()
|
||||
export class PlayerGesture extends Overlay {
|
||||
private _widget: EventBox;
|
||||
private _gesture: Gtk.GestureDrag;
|
||||
|
||||
players = new Map();
|
||||
setup = false;
|
||||
dragging = false;
|
||||
|
||||
set overlays(value) {
|
||||
super.overlays = value;
|
||||
}
|
||||
|
||||
get overlays() {
|
||||
return super.overlays.filter((overlay) => overlay !== this.child);
|
||||
}
|
||||
|
||||
includesWidget(playerW: PlayerBox) {
|
||||
return this.overlays.find((w) => w === playerW);
|
||||
}
|
||||
|
||||
showTopOnly() {
|
||||
this.overlays.forEach((over) => {
|
||||
over.visible = over === this.overlays.at(-1);
|
||||
});
|
||||
}
|
||||
|
||||
moveToTop(player: PlayerBox) {
|
||||
player.visible = true;
|
||||
this.reorder_overlay(player, -1);
|
||||
timeout(ANIM_DURATION, () => {
|
||||
this.showTopOnly();
|
||||
});
|
||||
}
|
||||
|
||||
dragUpdate(realGesture: Gtk.GestureDrag) {
|
||||
if (realGesture) {
|
||||
this.overlays.forEach((over) => {
|
||||
over.visible = true;
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.showTopOnly();
|
||||
}
|
||||
|
||||
// Don't allow gesture when only one player
|
||||
if (this.overlays.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dragging = true;
|
||||
let offset = this._gesture.get_offset()[1];
|
||||
const playerBox = this.overlays.at(-1) as PlayerBox;
|
||||
|
||||
if (!offset) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Slide right
|
||||
if (offset >= 0) {
|
||||
playerBox.css = `
|
||||
margin-left: ${offset}px;
|
||||
margin-right: -${offset}px;
|
||||
${playerBox.bgStyle}
|
||||
`;
|
||||
}
|
||||
|
||||
// Slide left
|
||||
else {
|
||||
offset = Math.abs(offset);
|
||||
playerBox.css = `
|
||||
margin-left: -${offset}px;
|
||||
margin-right: ${offset}px;
|
||||
${playerBox.bgStyle}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
dragEnd() {
|
||||
// Don't allow gesture when only one player
|
||||
if (this.overlays.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dragging = false;
|
||||
const offset = this._gesture.get_offset()[1];
|
||||
|
||||
const playerBox = this.overlays.at(-1) as PlayerBox;
|
||||
|
||||
// If crosses threshold after letting go, slide away
|
||||
if (offset && Math.abs(offset) > MAX_OFFSET) {
|
||||
// Disable inputs during animation
|
||||
this._widget.sensitive = false;
|
||||
|
||||
// Slide away right
|
||||
if (offset >= 0) {
|
||||
playerBox.css = `
|
||||
${TRANSITION}
|
||||
margin-left: ${OFFSCREEN}px;
|
||||
margin-right: -${OFFSCREEN}px;
|
||||
opacity: 0.7; ${playerBox.bgStyle}
|
||||
`;
|
||||
}
|
||||
|
||||
// Slide away left
|
||||
else {
|
||||
playerBox.css = `
|
||||
${TRANSITION}
|
||||
margin-left: -${OFFSCREEN}px;
|
||||
margin-right: ${OFFSCREEN}px;
|
||||
opacity: 0.7; ${playerBox.bgStyle}
|
||||
`;
|
||||
}
|
||||
|
||||
timeout(ANIM_DURATION, () => {
|
||||
// Put the player in the back after anim
|
||||
this.reorder_overlay(playerBox, 0);
|
||||
// Recenter player
|
||||
playerBox.css = playerBox.bgStyle;
|
||||
|
||||
this._widget.sensitive = true;
|
||||
|
||||
this.showTopOnly();
|
||||
});
|
||||
}
|
||||
else {
|
||||
// Recenter with transition for animation
|
||||
playerBox.css = `${TRANSITION} ${playerBox.bgStyle}`;
|
||||
|
||||
timeout(ANIM_DURATION, () => {
|
||||
this.showTopOnly();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
constructor({
|
||||
setup = () => { /**/ },
|
||||
widget,
|
||||
...props
|
||||
}: Omit<OverlayProps, 'setup'> & {
|
||||
widget: EventBox
|
||||
setup: (self: PlayerGesture) => void
|
||||
}) {
|
||||
super(props);
|
||||
setup(this);
|
||||
|
||||
this._widget = widget;
|
||||
this._gesture = Gtk.GestureDrag.new(this);
|
||||
|
||||
this.hook(this._gesture, 'drag-update', (_, realGesture) => this.dragUpdate(realGesture));
|
||||
this.hook(this._gesture, 'drag-end', () => this.dragEnd());
|
||||
}
|
||||
}
|
||||
|
||||
export default ({
|
||||
setup = () => { /**/ },
|
||||
...props
|
||||
}: Gesture) => {
|
||||
const widget = new EventBox();
|
||||
|
||||
// Have empty PlayerBox to define the size of the widget
|
||||
const emptyPlayer = new PlayerBox({
|
||||
className: 'player',
|
||||
});
|
||||
|
||||
const content = new PlayerGesture({
|
||||
...props,
|
||||
setup,
|
||||
widget,
|
||||
child: emptyPlayer,
|
||||
});
|
||||
|
||||
widget.add(content);
|
||||
|
||||
return widget;
|
||||
};
|
502
modules/ags/config/widgets/media-player/mpris.tsx
Normal file
502
modules/ags/config/widgets/media-player/mpris.tsx
Normal file
|
@ -0,0 +1,502 @@
|
|||
import { bind, execAsync, idle, type Variable } from 'astal';
|
||||
import { Gdk, Gtk } from 'astal/gtk3';
|
||||
import { Box, Button, CenterBoxProps, Icon, Label, Slider, Stack } from 'astal/gtk3/widget';
|
||||
import { kebabify } from 'astal/binding';
|
||||
|
||||
import Mpris from 'gi://AstalMpris';
|
||||
|
||||
import Separator from '../misc/separator';
|
||||
|
||||
import { PlayerBox, PlayerGesture } from './gesture';
|
||||
|
||||
const ICON_SIZE = 32;
|
||||
|
||||
const icons = {
|
||||
mpris: {
|
||||
fallback: 'audio-x-generic-symbolic',
|
||||
shuffle: {
|
||||
enabled: '',
|
||||
disabled: '',
|
||||
},
|
||||
loop: {
|
||||
none: '',
|
||||
track: '',
|
||||
playlist: '',
|
||||
},
|
||||
playing: 'media-playback-pause-symbolic',
|
||||
paused: 'media-playback-start-symbolic',
|
||||
stopped: 'media-playback-stop-symbolic',
|
||||
prev: '',
|
||||
next: '',
|
||||
},
|
||||
};
|
||||
|
||||
/* Types */
|
||||
export interface Colors {
|
||||
imageAccent: string
|
||||
buttonAccent: string
|
||||
buttonText: string
|
||||
hoverAccent: string
|
||||
}
|
||||
|
||||
|
||||
export const CoverArt = (
|
||||
player: Mpris.Player,
|
||||
colors: Variable<Colors>,
|
||||
props: CenterBoxProps,
|
||||
) => new PlayerBox({
|
||||
...props,
|
||||
vertical: true,
|
||||
|
||||
bgStyle: '',
|
||||
player,
|
||||
|
||||
setup: (self: PlayerBox) => {
|
||||
const setCover = () => {
|
||||
execAsync(['bash', '-c', `[[ -f "${player.coverArt}" ]] &&
|
||||
coloryou "${player.coverArt}" | grep -v Warning`])
|
||||
.then((out) => {
|
||||
if (!player.coverArt) {
|
||||
return;
|
||||
}
|
||||
|
||||
colors.set(JSON.parse(out));
|
||||
|
||||
self.bgStyle = `
|
||||
background: radial-gradient(circle,
|
||||
rgba(0, 0, 0, 0.4) 30%,
|
||||
${colors.get().imageAccent}),
|
||||
url("${player.coverArt}");
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
`;
|
||||
|
||||
if (!(self.get_parent() as PlayerGesture).dragging) {
|
||||
self.css = self.bgStyle;
|
||||
}
|
||||
}).catch(() => {
|
||||
colors.set({
|
||||
imageAccent: '#6b4fa2',
|
||||
buttonAccent: '#ecdcff',
|
||||
buttonText: '#25005a',
|
||||
hoverAccent: '#d4baff',
|
||||
});
|
||||
|
||||
self.bgStyle = `
|
||||
background: radial-gradient(circle,
|
||||
rgba(0, 0, 0, 0.4) 30%,
|
||||
${(colors as Variable<Colors>).get().imageAccent}),
|
||||
rgb(0, 0, 0);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
`;
|
||||
self.css = self.bgStyle;
|
||||
});
|
||||
};
|
||||
|
||||
player.connect('notify::cover-art', () => setCover());
|
||||
idle(() => setCover());
|
||||
},
|
||||
});
|
||||
|
||||
export const TitleLabel = (player: Mpris.Player) => new Label({
|
||||
xalign: 0,
|
||||
max_width_chars: 40,
|
||||
truncate: true,
|
||||
justify: Gtk.Justification.LEFT,
|
||||
className: 'title',
|
||||
label: bind(player, 'title'),
|
||||
});
|
||||
|
||||
export const ArtistLabel = (player: Mpris.Player) => new Label({
|
||||
xalign: 0,
|
||||
max_width_chars: 40,
|
||||
truncate: true,
|
||||
justify: Gtk.Justification.LEFT,
|
||||
className: 'artist',
|
||||
label: bind(player, 'artist'),
|
||||
});
|
||||
|
||||
|
||||
export const PlayerIcon = (player: Mpris.Player, overlay: PlayerGesture) => {
|
||||
const playerIcon = (
|
||||
p: Mpris.Player,
|
||||
widget?: PlayerGesture,
|
||||
playerBox?: PlayerBox,
|
||||
) => new Button({
|
||||
tooltip_text: p.identity || '',
|
||||
|
||||
onButtonReleaseEvent: () => {
|
||||
if (widget && playerBox) {
|
||||
widget.moveToTop(playerBox);
|
||||
}
|
||||
},
|
||||
|
||||
child: new Icon({
|
||||
css: `font-size: ${ICON_SIZE}px;`,
|
||||
className: widget ? 'position-indicator' : 'player-icon',
|
||||
|
||||
setup: (self) => {
|
||||
idle(() => {
|
||||
if (p.entry === null) { return; }
|
||||
|
||||
self.icon = Icon.lookup_icon(p.entry) ?
|
||||
p.entry :
|
||||
icons.mpris.fallback;
|
||||
});
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return new Box({
|
||||
setup(self) {
|
||||
const update = () => {
|
||||
const grandPa = self.get_parent()?.get_parent();
|
||||
|
||||
if (!grandPa) {
|
||||
return;
|
||||
}
|
||||
|
||||
const thisIndex = overlay.overlays.indexOf(grandPa);
|
||||
|
||||
self.children = (overlay.overlays as PlayerBox[])
|
||||
.map((playerBox, i) => {
|
||||
self.children.push(Separator({ size: 2 }));
|
||||
|
||||
return i === thisIndex ?
|
||||
playerIcon(player) :
|
||||
playerIcon(playerBox.player, overlay, playerBox);
|
||||
})
|
||||
.reverse();
|
||||
};
|
||||
|
||||
self.hook(Mpris.get_default(), 'player-added', update);
|
||||
self.hook(Mpris.get_default(), 'player-closed', update);
|
||||
|
||||
idle(() => update());
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const display = Gdk.Display.get_default();
|
||||
|
||||
export const PositionSlider = (
|
||||
player: Mpris.Player,
|
||||
colors: Variable<Colors>,
|
||||
) => new Slider({
|
||||
className: 'position-slider',
|
||||
valign: Gtk.Align.CENTER,
|
||||
hexpand: true,
|
||||
draw_value: false,
|
||||
|
||||
onDragged: ({ value }) => {
|
||||
player.position = player.length * value;
|
||||
},
|
||||
|
||||
onButtonPressEvent: (self) => {
|
||||
if (!display) {
|
||||
return;
|
||||
}
|
||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
'grabbing',
|
||||
));
|
||||
},
|
||||
|
||||
onButtonReleaseEvent: (self) => {
|
||||
if (!display) {
|
||||
return;
|
||||
}
|
||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
'pointer',
|
||||
));
|
||||
},
|
||||
|
||||
onEnterNotifyEvent: (self) => {
|
||||
if (!display) {
|
||||
return;
|
||||
}
|
||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
'pointer',
|
||||
));
|
||||
},
|
||||
|
||||
onLeaveNotifyEvent: (self) => {
|
||||
self.window.set_cursor(null);
|
||||
},
|
||||
|
||||
setup: (self) => {
|
||||
const update = () => {
|
||||
if (!self.dragging) {
|
||||
self.visible = player.length > 0;
|
||||
if (player.length > 0) {
|
||||
self.value = player.position / player.length;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (player) {
|
||||
update();
|
||||
}
|
||||
else {
|
||||
interval.destroy();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
self.hook(colors, () => {
|
||||
if (colors.get()) {
|
||||
const c = colors.get();
|
||||
|
||||
self.css = `
|
||||
highlight { background-color: ${c.buttonAccent}; }
|
||||
slider { background-color: ${c.buttonAccent}; }
|
||||
slider:hover { background-color: ${c.hoverAccent}; }
|
||||
trough { background-color: ${c.buttonText}; }
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
player.connect('notify::position', () => update());
|
||||
},
|
||||
});
|
||||
|
||||
const PlayerButton = ({
|
||||
player,
|
||||
colors,
|
||||
children = [],
|
||||
onClick,
|
||||
prop,
|
||||
}: {
|
||||
player: Mpris.Player
|
||||
colors: Variable<Colors>
|
||||
children: Label[] | Icon[]
|
||||
onClick: keyof Mpris.Player
|
||||
prop: string
|
||||
}) => {
|
||||
let hovered = false;
|
||||
const stack = new Stack({ children });
|
||||
|
||||
return new Button({
|
||||
cursor: 'pointer',
|
||||
|
||||
child: stack,
|
||||
|
||||
// @ts-expect-error FIXME
|
||||
onButtonReleaseEvent: () => player[onClick](),
|
||||
|
||||
onHover: () => {
|
||||
hovered = true;
|
||||
|
||||
if (prop === 'playbackStatus' && colors.get()) {
|
||||
const c = colors.get();
|
||||
|
||||
children.forEach((ch) => {
|
||||
ch.css = `
|
||||
background-color: ${c.hoverAccent};
|
||||
color: ${c.buttonText};
|
||||
min-height: 40px;
|
||||
min-width: 36px;
|
||||
margin-bottom: 1px;
|
||||
margin-right: 1px;
|
||||
`;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onHoverLost: () => {
|
||||
hovered = false;
|
||||
if (prop === 'playbackStatus' && colors.get()) {
|
||||
const c = colors.get();
|
||||
|
||||
children.forEach((ch) => {
|
||||
ch.css = `
|
||||
background-color: ${c.buttonAccent};
|
||||
color: ${c.buttonText};
|
||||
min-height: 42px;
|
||||
min-width: 38px;
|
||||
`;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setup: (self) => {
|
||||
// @ts-expect-error FIXME
|
||||
if (player[prop] && player[prop] !== false) {
|
||||
// @ts-expect-error FIXME
|
||||
stack.shown = String(player[prop]);
|
||||
}
|
||||
|
||||
player.connect(`notify::${kebabify(prop)}`, () => {
|
||||
// @ts-expect-error FIXME
|
||||
if (player[prop] !== 0) {
|
||||
// @ts-expect-error FIXME
|
||||
stack.shown = String(player[prop]);
|
||||
}
|
||||
});
|
||||
|
||||
self.hook(colors, () => {
|
||||
if (!Mpris.get_default().get_players().find((p) => player === p)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (colors.get()) {
|
||||
const c = colors.get();
|
||||
|
||||
if (prop === 'playbackStatus') {
|
||||
if (hovered) {
|
||||
children.forEach((ch) => {
|
||||
ch.css = `
|
||||
background-color: ${c.hoverAccent};
|
||||
color: ${c.buttonText};
|
||||
min-height: 40px;
|
||||
min-width: 36px;
|
||||
margin-bottom: 1px;
|
||||
margin-right: 1px;
|
||||
`;
|
||||
});
|
||||
}
|
||||
else {
|
||||
children.forEach((ch) => {
|
||||
ch.css = `
|
||||
background-color: ${c.buttonAccent};
|
||||
color: ${c.buttonText};
|
||||
min-height: 42px;
|
||||
min-width: 38px;
|
||||
`;
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
self.css = `
|
||||
* { color: ${c.buttonAccent}; }
|
||||
*:hover { color: ${c.hoverAccent}; }
|
||||
`;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const ShuffleButton = (
|
||||
player: Mpris.Player,
|
||||
colors: Variable<Colors>,
|
||||
) => PlayerButton({
|
||||
player,
|
||||
colors,
|
||||
children: [
|
||||
(new Label({
|
||||
name: Mpris.Shuffle.ON.toString(),
|
||||
className: 'shuffle enabled',
|
||||
label: icons.mpris.shuffle.enabled,
|
||||
})),
|
||||
(new Label({
|
||||
name: Mpris.Shuffle.OFF.toString(),
|
||||
className: 'shuffle disabled',
|
||||
label: icons.mpris.shuffle.disabled,
|
||||
})),
|
||||
],
|
||||
onClick: 'shuffle',
|
||||
prop: 'shuffleStatus',
|
||||
});
|
||||
|
||||
export const LoopButton = (
|
||||
player: Mpris.Player,
|
||||
colors: Variable<Colors>,
|
||||
) => PlayerButton({
|
||||
player,
|
||||
colors,
|
||||
children: [
|
||||
(new Label({
|
||||
name: Mpris.Loop.NONE.toString(),
|
||||
className: 'loop none',
|
||||
label: icons.mpris.loop.none,
|
||||
})),
|
||||
(new Label({
|
||||
name: Mpris.Loop.TRACK.toString(),
|
||||
className: 'loop track',
|
||||
label: icons.mpris.loop.track,
|
||||
})),
|
||||
(new Label({
|
||||
name: Mpris.Loop.PLAYLIST.toString(),
|
||||
className: 'loop playlist',
|
||||
label: icons.mpris.loop.playlist,
|
||||
})),
|
||||
],
|
||||
onClick: 'loop',
|
||||
prop: 'loopStatus',
|
||||
});
|
||||
|
||||
export const PlayPauseButton = (
|
||||
player: Mpris.Player,
|
||||
colors: Variable<Colors>,
|
||||
) => PlayerButton({
|
||||
player,
|
||||
colors,
|
||||
children: [
|
||||
(new Icon({
|
||||
name: Mpris.PlaybackStatus.PLAYING.toString(),
|
||||
className: 'pausebutton playing',
|
||||
icon: icons.mpris.playing,
|
||||
})),
|
||||
(new Icon({
|
||||
name: Mpris.PlaybackStatus.PAUSED.toString(),
|
||||
className: 'pausebutton paused',
|
||||
icon: icons.mpris.paused,
|
||||
})),
|
||||
(new Icon({
|
||||
name: Mpris.PlaybackStatus.STOPPED.toString(),
|
||||
className: 'pausebutton stopped paused',
|
||||
icon: icons.mpris.stopped,
|
||||
})),
|
||||
],
|
||||
onClick: 'play_pause',
|
||||
prop: 'playbackStatus',
|
||||
});
|
||||
|
||||
export const PreviousButton = (
|
||||
player: Mpris.Player,
|
||||
colors: Variable<Colors>,
|
||||
) => PlayerButton({
|
||||
player,
|
||||
colors,
|
||||
children: [
|
||||
(new Label({
|
||||
name: 'true',
|
||||
className: 'previous',
|
||||
label: icons.mpris.prev,
|
||||
})),
|
||||
(new Label({
|
||||
name: 'false',
|
||||
className: 'previous',
|
||||
label: icons.mpris.prev,
|
||||
})),
|
||||
],
|
||||
onClick: 'previous',
|
||||
prop: 'canGoPrev',
|
||||
});
|
||||
|
||||
export const NextButton = (
|
||||
player: Mpris.Player,
|
||||
colors: Variable<Colors>,
|
||||
) => PlayerButton({
|
||||
player,
|
||||
colors,
|
||||
children: [
|
||||
(new Label({
|
||||
name: 'true',
|
||||
className: 'next',
|
||||
label: icons.mpris.next,
|
||||
})),
|
||||
(new Label({
|
||||
name: 'false',
|
||||
className: 'next',
|
||||
label: icons.mpris.next,
|
||||
})),
|
||||
],
|
||||
onClick: 'next',
|
||||
prop: 'canGoNext',
|
||||
});
|
183
modules/ags/config/widgets/media-player/player.tsx
Normal file
183
modules/ags/config/widgets/media-player/player.tsx
Normal file
|
@ -0,0 +1,183 @@
|
|||
import { Variable } from 'astal';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import { Box, CenterBox } from 'astal/gtk3/widget';
|
||||
|
||||
import Mpris from 'gi://AstalMpris';
|
||||
|
||||
import Separator from '../misc/separator';
|
||||
|
||||
import * as mpris from './mpris';
|
||||
|
||||
import PlayerGesture, {
|
||||
PlayerGesture as PlayerGestureClass,
|
||||
PlayerBox as PlayerBoxClass,
|
||||
} from './gesture';
|
||||
|
||||
const FAVE_PLAYER = 'org.mpris.MediaPlayer2.spotify';
|
||||
const SPACING = 8;
|
||||
|
||||
|
||||
const Top = (
|
||||
player: Mpris.Player,
|
||||
overlay: PlayerGestureClass,
|
||||
) => new Box({
|
||||
className: 'top',
|
||||
halign: Gtk.Align.START,
|
||||
valign: Gtk.Align.START,
|
||||
|
||||
children: [
|
||||
mpris.PlayerIcon(player, overlay),
|
||||
],
|
||||
});
|
||||
|
||||
const Center = (
|
||||
player: Mpris.Player,
|
||||
colors: Variable<mpris.Colors>,
|
||||
) => new Box({
|
||||
className: 'center',
|
||||
|
||||
children: [
|
||||
(new CenterBox({
|
||||
vertical: true,
|
||||
|
||||
start_widget: new Box({
|
||||
className: 'metadata',
|
||||
vertical: true,
|
||||
halign: Gtk.Align.START,
|
||||
valign: Gtk.Align.CENTER,
|
||||
hexpand: true,
|
||||
|
||||
children: [
|
||||
mpris.TitleLabel(player),
|
||||
mpris.ArtistLabel(player),
|
||||
],
|
||||
}),
|
||||
})),
|
||||
|
||||
(new CenterBox({
|
||||
vertical: true,
|
||||
|
||||
center_widget: mpris.PlayPauseButton(player, colors),
|
||||
})),
|
||||
|
||||
],
|
||||
});
|
||||
|
||||
const Bottom = (
|
||||
player: Mpris.Player,
|
||||
colors: Variable<mpris.Colors>,
|
||||
) => new Box({
|
||||
className: 'bottom',
|
||||
|
||||
children: [
|
||||
mpris.PreviousButton(player, colors),
|
||||
Separator({ size: SPACING }),
|
||||
|
||||
mpris.PositionSlider(player, colors),
|
||||
Separator({ size: SPACING }),
|
||||
|
||||
mpris.NextButton(player, colors),
|
||||
Separator({ size: SPACING }),
|
||||
|
||||
mpris.ShuffleButton(player, colors),
|
||||
Separator({ size: SPACING }),
|
||||
|
||||
mpris.LoopButton(player, colors),
|
||||
],
|
||||
});
|
||||
|
||||
const PlayerBox = (
|
||||
player: Mpris.Player,
|
||||
colors: Variable<mpris.Colors>,
|
||||
overlay: PlayerGestureClass,
|
||||
) => {
|
||||
const widget = mpris.CoverArt(player, colors, {
|
||||
className: `player ${player.identity}`,
|
||||
hexpand: true,
|
||||
|
||||
start_widget: Top(player, overlay),
|
||||
center_widget: Center(player, colors),
|
||||
end_widget: Bottom(player, colors),
|
||||
});
|
||||
|
||||
widget.visible = false;
|
||||
|
||||
return widget;
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const content = PlayerGesture({
|
||||
setup: (self) => {
|
||||
const addPlayer = (player: Mpris.Player) => {
|
||||
if (!player || self.players.has(player.bus_name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the one on top so we can move it up later
|
||||
const previousFirst = self.overlays.at(-1) as PlayerBoxClass;
|
||||
|
||||
// Make the new player
|
||||
const colorsVar = Variable({
|
||||
imageAccent: '#6b4fa2',
|
||||
buttonAccent: '#ecdcff',
|
||||
buttonText: '#25005a',
|
||||
hoverAccent: '#d4baff',
|
||||
});
|
||||
|
||||
self.players.set(
|
||||
player.bus_name,
|
||||
PlayerBox(player, colorsVar, self),
|
||||
);
|
||||
self.add_overlay(self.players.get(player.bus_name));
|
||||
|
||||
// Select favorite player at startup
|
||||
if (!self.setup && self.players.has(FAVE_PLAYER)) {
|
||||
self.moveToTop(self.players.get(FAVE_PLAYER));
|
||||
self.setup = true;
|
||||
}
|
||||
|
||||
// Move previousFirst on top again
|
||||
else {
|
||||
self.moveToTop(previousFirst);
|
||||
}
|
||||
};
|
||||
|
||||
const removePlayer = (player: Mpris.Player) => {
|
||||
if (!player || !self.players.has(player.bus_name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toDelete = self.players.get(player.bus_name);
|
||||
|
||||
// Get the one on top so we can move it up later
|
||||
const previousFirst = self.overlays.at(-1) as PlayerBoxClass;
|
||||
|
||||
// Move previousFirst on top again
|
||||
if (previousFirst !== toDelete) {
|
||||
self.moveToTop(previousFirst);
|
||||
}
|
||||
else {
|
||||
self.moveToTop(self.players.has(FAVE_PLAYER) ?
|
||||
self.players.get(FAVE_PLAYER) :
|
||||
self.overlays[0]);
|
||||
}
|
||||
|
||||
// Remake overlays without deleted one
|
||||
self.remove(toDelete);
|
||||
self.players.delete(player.bus_name);
|
||||
};
|
||||
|
||||
const mprisDefault = Mpris.get_default();
|
||||
|
||||
self.hook(mprisDefault, 'player-added', (_, player) => addPlayer(player));
|
||||
self.hook(mprisDefault, 'player-closed', (_, player) => removePlayer(player));
|
||||
|
||||
mprisDefault.players.forEach(addPlayer);
|
||||
},
|
||||
});
|
||||
|
||||
return new Box({
|
||||
className: 'media-player',
|
||||
child: content,
|
||||
});
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue