feat(ags): finish migrating v1 to v2
All checks were successful
Discord / discord commits (push) Has been skipped
All checks were successful
Discord / discord commits (push) Has been skipped
This commit is contained in:
parent
7d64a1fe25
commit
2e15e10fd5
24 changed files with 1196 additions and 1026 deletions
|
@ -7,6 +7,7 @@
|
||||||
@use '../widgets/clipboard';
|
@use '../widgets/clipboard';
|
||||||
@use '../widgets/date';
|
@use '../widgets/date';
|
||||||
@use '../widgets/icon-browser';
|
@use '../widgets/icon-browser';
|
||||||
|
@use '../widgets/media-player';
|
||||||
@use '../widgets/misc';
|
@use '../widgets/misc';
|
||||||
@use '../widgets/network';
|
@use '../widgets/network';
|
||||||
@use '../widgets/notifs';
|
@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,
|
||||||
|
});
|
||||||
|
};
|
|
@ -115,6 +115,10 @@ in {
|
||||||
playerctl
|
playerctl
|
||||||
wayfreeze
|
wayfreeze
|
||||||
;
|
;
|
||||||
|
inherit
|
||||||
|
(self.packages.${pkgs.system})
|
||||||
|
coloryou
|
||||||
|
;
|
||||||
})
|
})
|
||||||
++ (optionals cfgDesktop.isTouchscreen (attrValues {
|
++ (optionals cfgDesktop.isTouchscreen (attrValues {
|
||||||
inherit
|
inherit
|
||||||
|
|
69
modules/ags/v1/config/global-types.d.ts
vendored
69
modules/ags/v1/config/global-types.d.ts
vendored
|
@ -1,69 +0,0 @@
|
||||||
// For ./ts/bar/hovers/keyboard.ts
|
|
||||||
export interface Keyboard {
|
|
||||||
address: string
|
|
||||||
name: string
|
|
||||||
rules: string
|
|
||||||
model: string
|
|
||||||
layout: string
|
|
||||||
variant: string
|
|
||||||
options: string
|
|
||||||
active_keymap: string
|
|
||||||
main: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
// For ./ts/media-player/gesture.ts
|
|
||||||
export interface Gesture {
|
|
||||||
attribute?: object
|
|
||||||
setup?(self: OverlayGeneric): void
|
|
||||||
props?: OverlayProps<unknown & Widget, unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
// For ./ts/media-player/mpris.ts
|
|
||||||
type PlayerDragProps = unknown & { dragging: boolean };
|
|
||||||
export type PlayerDrag = AgsCenterBox<
|
|
||||||
unknown & Widget, unknown & Widget, unknown & Widget, unknown & PlayerDragProps
|
|
||||||
>;
|
|
||||||
interface Colors {
|
|
||||||
imageAccent: string
|
|
||||||
buttonAccent: string
|
|
||||||
buttonText: string
|
|
||||||
hoverAccent: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// For ./ts/media-player
|
|
||||||
export interface PlayerBoxProps {
|
|
||||||
bgStyle: string
|
|
||||||
player: MprisPlayer
|
|
||||||
}
|
|
||||||
export type PlayerBox = AgsCenterBox<
|
|
||||||
unknown & Widget, unknown & Widget, unknown & Widget, PlayerBoxProps
|
|
||||||
>;
|
|
||||||
export type PlayerOverlay = AgsOverlay<AgsWidget, {
|
|
||||||
players: Map
|
|
||||||
setup: boolean
|
|
||||||
dragging: boolean
|
|
||||||
includesWidget(playerW: PlayerBox): PlayerBox
|
|
||||||
showTopOnly(): void
|
|
||||||
moveToTop(player: PlayerBox): void
|
|
||||||
}>;
|
|
||||||
export interface PlayerButtonType {
|
|
||||||
player: MprisPlayer
|
|
||||||
colors: Var<Colors>
|
|
||||||
children: StackProps['children']
|
|
||||||
onClick: string
|
|
||||||
prop: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// For ./ts/quick-settings
|
|
||||||
import { BluetoothDevice as BTDev } from 'types/service/bluetooth.ts';
|
|
||||||
export interface APType {
|
|
||||||
bssid: string
|
|
||||||
address: string
|
|
||||||
lastSeen: number
|
|
||||||
ssid: string
|
|
||||||
active: boolean
|
|
||||||
strength: number
|
|
||||||
iconName: string
|
|
||||||
}
|
|
||||||
export type APBox = AgsBox<unknown & Widget, { ap: Var<APType> }>;
|
|
||||||
export type DeviceBox = AgsBox<unknown & Widget, { dev: BTDev }>;
|
|
|
@ -1,116 +0,0 @@
|
||||||
.arrow {
|
|
||||||
transition: -gtk-icon-transform 0.3s ease-in-out;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media {
|
|
||||||
margin-top: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player {
|
|
||||||
all: unset;
|
|
||||||
padding: 10px;
|
|
||||||
min-width: 400px;
|
|
||||||
min-height: 200px;
|
|
||||||
border-radius: 30px;
|
|
||||||
border-top: 2px solid $contrast-bg;
|
|
||||||
border-bottom: 2px solid $contrast-bg;
|
|
||||||
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: 15px;
|
|
||||||
padding: 4px 4px 4px 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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 4px 4px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button label {
|
|
||||||
min-width: 35px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.position-indicator {
|
|
||||||
min-width: 18px;
|
|
||||||
margin: 7px;
|
|
||||||
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 {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,167 +0,0 @@
|
||||||
const { timeout } = Utils;
|
|
||||||
const { Box, EventBox, Overlay } = Widget;
|
|
||||||
|
|
||||||
const { Gtk } = imports.gi;
|
|
||||||
|
|
||||||
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
|
|
||||||
import {
|
|
||||||
CenterBoxGeneric,
|
|
||||||
Gesture,
|
|
||||||
OverlayGeneric,
|
|
||||||
PlayerBox,
|
|
||||||
} from 'global-types';
|
|
||||||
|
|
||||||
|
|
||||||
export default ({
|
|
||||||
setup = () => { /**/ },
|
|
||||||
...props
|
|
||||||
}: Gesture) => {
|
|
||||||
const widget = EventBox();
|
|
||||||
const gesture = Gtk.GestureDrag.new(widget);
|
|
||||||
|
|
||||||
// Have empty PlayerBox to define the size of the widget
|
|
||||||
const emptyPlayer = Box({
|
|
||||||
class_name: 'player',
|
|
||||||
attribute: { empty: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
const content = Overlay({
|
|
||||||
...props,
|
|
||||||
attribute: {
|
|
||||||
players: new Map(),
|
|
||||||
setup: false,
|
|
||||||
dragging: false,
|
|
||||||
|
|
||||||
includesWidget: (playerW: OverlayGeneric) => {
|
|
||||||
return content.overlays.find((w) => w === playerW);
|
|
||||||
},
|
|
||||||
|
|
||||||
showTopOnly: () => content.overlays.forEach((over) => {
|
|
||||||
over.visible = over === content.overlays.at(-1);
|
|
||||||
}),
|
|
||||||
|
|
||||||
moveToTop: (player: CenterBoxGeneric) => {
|
|
||||||
player.visible = true;
|
|
||||||
content.reorder_overlay(player, -1);
|
|
||||||
timeout(ANIM_DURATION, () => {
|
|
||||||
content.attribute.showTopOnly();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
child: emptyPlayer,
|
|
||||||
|
|
||||||
setup: (self) => {
|
|
||||||
setup(self);
|
|
||||||
|
|
||||||
self
|
|
||||||
.hook(gesture, (_, realGesture) => {
|
|
||||||
if (realGesture) {
|
|
||||||
self.overlays.forEach((over) => {
|
|
||||||
over.visible = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
self.attribute.showTopOnly();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't allow gesture when only one player
|
|
||||||
if (self.overlays.length <= 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.attribute.dragging = true;
|
|
||||||
let offset = gesture.get_offset()[1];
|
|
||||||
const playerBox = self.overlays.at(-1) as PlayerBox;
|
|
||||||
|
|
||||||
if (!offset) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Slide right
|
|
||||||
if (offset >= 0) {
|
|
||||||
playerBox.setCss(`
|
|
||||||
margin-left: ${offset}px;
|
|
||||||
margin-right: -${offset}px;
|
|
||||||
${playerBox.attribute.bgStyle}
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Slide left
|
|
||||||
else {
|
|
||||||
offset = Math.abs(offset);
|
|
||||||
playerBox.setCss(`
|
|
||||||
margin-left: -${offset}px;
|
|
||||||
margin-right: ${offset}px;
|
|
||||||
${playerBox.attribute.bgStyle}
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}, 'drag-update')
|
|
||||||
|
|
||||||
|
|
||||||
.hook(gesture, () => {
|
|
||||||
// Don't allow gesture when only one player
|
|
||||||
if (self.overlays.length <= 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.attribute.dragging = false;
|
|
||||||
const offset = gesture.get_offset()[1];
|
|
||||||
|
|
||||||
const playerBox = self.overlays.at(-1) as PlayerBox;
|
|
||||||
|
|
||||||
// If crosses threshold after letting go, slide away
|
|
||||||
if (offset && Math.abs(offset) > MAX_OFFSET) {
|
|
||||||
// Disable inputs during animation
|
|
||||||
widget.sensitive = false;
|
|
||||||
|
|
||||||
// Slide away right
|
|
||||||
if (offset >= 0) {
|
|
||||||
playerBox.setCss(`
|
|
||||||
${TRANSITION}
|
|
||||||
margin-left: ${OFFSCREEN}px;
|
|
||||||
margin-right: -${OFFSCREEN}px;
|
|
||||||
opacity: 0.7; ${playerBox.attribute.bgStyle}
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Slide away left
|
|
||||||
else {
|
|
||||||
playerBox.setCss(`
|
|
||||||
${TRANSITION}
|
|
||||||
margin-left: -${OFFSCREEN}px;
|
|
||||||
margin-right: ${OFFSCREEN}px;
|
|
||||||
opacity: 0.7; ${playerBox.attribute.bgStyle}
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
timeout(ANIM_DURATION, () => {
|
|
||||||
// Put the player in the back after anim
|
|
||||||
self.reorder_overlay(playerBox, 0);
|
|
||||||
// Recenter player
|
|
||||||
playerBox.setCss(playerBox.attribute.bgStyle);
|
|
||||||
|
|
||||||
widget.sensitive = true;
|
|
||||||
|
|
||||||
self.attribute.showTopOnly();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Recenter with transition for animation
|
|
||||||
playerBox.setCss(`${TRANSITION}
|
|
||||||
${playerBox.attribute.bgStyle}`);
|
|
||||||
}
|
|
||||||
}, 'drag-end');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
widget.add(content);
|
|
||||||
|
|
||||||
return widget;
|
|
||||||
};
|
|
|
@ -1,473 +0,0 @@
|
||||||
const Mpris = await Service.import('mpris');
|
|
||||||
|
|
||||||
const { Button, Icon, Label, Stack, Slider, CenterBox, Box } = Widget;
|
|
||||||
const { execAsync, lookUpIcon, readFileAsync } = Utils;
|
|
||||||
|
|
||||||
import Separator from '../misc/separator.ts';
|
|
||||||
import CursorBox from '../misc/cursorbox.ts';
|
|
||||||
|
|
||||||
const ICON_SIZE = 32;
|
|
||||||
|
|
||||||
const icons = {
|
|
||||||
mpris: {
|
|
||||||
fallback: 'audio-x-generic-symbolic',
|
|
||||||
shuffle: {
|
|
||||||
enabled: '',
|
|
||||||
disabled: '',
|
|
||||||
},
|
|
||||||
loop: {
|
|
||||||
none: '',
|
|
||||||
track: '',
|
|
||||||
playlist: '',
|
|
||||||
},
|
|
||||||
playing: ' ',
|
|
||||||
paused: ' ',
|
|
||||||
stopped: ' ',
|
|
||||||
prev: '',
|
|
||||||
next: '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Types
|
|
||||||
import { MprisPlayer } from 'types/service/mpris.ts';
|
|
||||||
import { Variable as Var } from 'types/variable';
|
|
||||||
|
|
||||||
import {
|
|
||||||
AgsWidget,
|
|
||||||
CenterBoxPropsGeneric,
|
|
||||||
Colors,
|
|
||||||
PlayerBox,
|
|
||||||
PlayerButtonType,
|
|
||||||
PlayerDrag,
|
|
||||||
PlayerOverlay,
|
|
||||||
} from 'global-types';
|
|
||||||
|
|
||||||
|
|
||||||
export const CoverArt = (
|
|
||||||
player: MprisPlayer,
|
|
||||||
colors: Var<Colors>,
|
|
||||||
props: CenterBoxPropsGeneric,
|
|
||||||
) => CenterBox({
|
|
||||||
...props,
|
|
||||||
vertical: true,
|
|
||||||
|
|
||||||
attribute: {
|
|
||||||
bgStyle: '',
|
|
||||||
player,
|
|
||||||
},
|
|
||||||
|
|
||||||
setup: (self: PlayerBox) => {
|
|
||||||
// Give temp cover art
|
|
||||||
readFileAsync(player.cover_path).catch(() => {
|
|
||||||
if (!colors.value && !player.track_cover_url) {
|
|
||||||
colors.setValue({
|
|
||||||
imageAccent: '#6b4fa2',
|
|
||||||
buttonAccent: '#ecdcff',
|
|
||||||
buttonText: '#25005a',
|
|
||||||
hoverAccent: '#d4baff',
|
|
||||||
});
|
|
||||||
|
|
||||||
self.attribute.bgStyle = `
|
|
||||||
background: radial-gradient(circle,
|
|
||||||
rgba(0, 0, 0, 0.4) 30%,
|
|
||||||
${(colors as Var<Colors>).value.imageAccent}),
|
|
||||||
rgb(0, 0, 0);
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
`;
|
|
||||||
self.setCss(self.attribute.bgStyle);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.hook(player, () => {
|
|
||||||
execAsync(['bash', '-c', `[[ -f "${player.cover_path}" ]] &&
|
|
||||||
coloryou "${player.cover_path}" | grep -v Warning`])
|
|
||||||
.then((out) => {
|
|
||||||
if (!Mpris.players.find((p) => player === p)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
colors.setValue(JSON.parse(out));
|
|
||||||
|
|
||||||
self.attribute.bgStyle = `
|
|
||||||
background: radial-gradient(circle,
|
|
||||||
rgba(0, 0, 0, 0.4) 30%,
|
|
||||||
${colors.value.imageAccent}),
|
|
||||||
url("${player.cover_path}");
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (!(self.get_parent() as PlayerDrag)
|
|
||||||
.attribute.dragging) {
|
|
||||||
self.setCss(self.attribute.bgStyle);
|
|
||||||
}
|
|
||||||
}).catch((err) => {
|
|
||||||
if (err !== '') {
|
|
||||||
print(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const TitleLabel = (player: MprisPlayer) => Label({
|
|
||||||
xalign: 0,
|
|
||||||
max_width_chars: 40,
|
|
||||||
truncate: 'end',
|
|
||||||
justification: 'left',
|
|
||||||
class_name: 'title',
|
|
||||||
label: player.bind('track_title'),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ArtistLabel = (player: MprisPlayer) => Label({
|
|
||||||
xalign: 0,
|
|
||||||
max_width_chars: 40,
|
|
||||||
truncate: 'end',
|
|
||||||
justification: 'left',
|
|
||||||
class_name: 'artist',
|
|
||||||
label: player.bind('track_artists')
|
|
||||||
.transform((a) => a.join(', ') || ''),
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
export const PlayerIcon = (player: MprisPlayer, overlay: PlayerOverlay) => {
|
|
||||||
const playerIcon = (
|
|
||||||
p: MprisPlayer,
|
|
||||||
widget?: PlayerOverlay,
|
|
||||||
playerBox?: PlayerBox,
|
|
||||||
) => CursorBox({
|
|
||||||
tooltip_text: p.identity || '',
|
|
||||||
|
|
||||||
on_primary_click_release: () => {
|
|
||||||
if (widget && playerBox) {
|
|
||||||
widget.attribute.moveToTop(playerBox);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
child: Icon({
|
|
||||||
class_name: widget ? 'position-indicator' : 'player-icon',
|
|
||||||
size: widget ? 0 : ICON_SIZE,
|
|
||||||
|
|
||||||
setup: (self) => {
|
|
||||||
self.hook(p, () => {
|
|
||||||
self.icon = lookUpIcon(p.entry) ?
|
|
||||||
p.entry :
|
|
||||||
icons.mpris.fallback;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
return Box().hook(Mpris, (self) => {
|
|
||||||
const grandPa = self.get_parent()?.get_parent() as AgsWidget;
|
|
||||||
|
|
||||||
if (!grandPa) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const thisIndex = overlay.overlays
|
|
||||||
.indexOf(grandPa);
|
|
||||||
|
|
||||||
self.children = (overlay.overlays as PlayerBox[])
|
|
||||||
.map((playerBox, i) => {
|
|
||||||
self.children.push(Separator(2));
|
|
||||||
|
|
||||||
return i === thisIndex ?
|
|
||||||
playerIcon(player) :
|
|
||||||
playerIcon(playerBox.attribute.player, overlay, playerBox);
|
|
||||||
})
|
|
||||||
.reverse();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const { Gdk } = imports.gi;
|
|
||||||
const display = Gdk.Display.get_default();
|
|
||||||
|
|
||||||
export const PositionSlider = (
|
|
||||||
player: MprisPlayer,
|
|
||||||
colors: Var<Colors>,
|
|
||||||
) => Slider({
|
|
||||||
class_name: 'position-slider',
|
|
||||||
vpack: 'center',
|
|
||||||
hexpand: true,
|
|
||||||
draw_value: false,
|
|
||||||
|
|
||||||
on_change: ({ value }) => {
|
|
||||||
player.position = player.length * value;
|
|
||||||
},
|
|
||||||
|
|
||||||
setup: (self) => {
|
|
||||||
const update = () => {
|
|
||||||
if (!self.dragging) {
|
|
||||||
self.visible = player.length > 0;
|
|
||||||
if (player.length > 0) {
|
|
||||||
self.value = player.position / player.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
self
|
|
||||||
.poll(1000, () => update())
|
|
||||||
.hook(player, () => update(), 'position')
|
|
||||||
.hook(colors, () => {
|
|
||||||
if (colors.value) {
|
|
||||||
const c = colors.value;
|
|
||||||
|
|
||||||
self.setCss(`
|
|
||||||
highlight { background-color: ${c.buttonAccent}; }
|
|
||||||
slider { background-color: ${c.buttonAccent}; }
|
|
||||||
slider:hover { background-color: ${c.hoverAccent}; }
|
|
||||||
trough { background-color: ${c.buttonText}; }
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// OnClick
|
|
||||||
.on('button-press-event', () => {
|
|
||||||
if (!display) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
|
||||||
display,
|
|
||||||
'grabbing',
|
|
||||||
));
|
|
||||||
})
|
|
||||||
|
|
||||||
// OnRelease
|
|
||||||
.on('button-release-event', () => {
|
|
||||||
if (!display) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
|
||||||
display,
|
|
||||||
'pointer',
|
|
||||||
));
|
|
||||||
})
|
|
||||||
|
|
||||||
// OnHover
|
|
||||||
.on('enter-notify-event', () => {
|
|
||||||
if (!display) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
|
||||||
display,
|
|
||||||
'pointer',
|
|
||||||
));
|
|
||||||
self.toggleClassName('hover', true);
|
|
||||||
})
|
|
||||||
|
|
||||||
// OnHoverLost
|
|
||||||
.on('leave-notify-event', () => {
|
|
||||||
self.window.set_cursor(null);
|
|
||||||
self.toggleClassName('hover', false);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const PlayerButton = ({
|
|
||||||
player,
|
|
||||||
colors,
|
|
||||||
children = {},
|
|
||||||
onClick,
|
|
||||||
prop,
|
|
||||||
}: PlayerButtonType) => CursorBox({
|
|
||||||
child: Button({
|
|
||||||
attribute: { hovered: false },
|
|
||||||
child: Stack({ children }),
|
|
||||||
|
|
||||||
on_primary_click_release: () => player[onClick](),
|
|
||||||
|
|
||||||
on_hover: (self) => {
|
|
||||||
self.attribute.hovered = true;
|
|
||||||
|
|
||||||
if (prop === 'playBackStatus' && colors.value) {
|
|
||||||
const c = colors.value;
|
|
||||||
|
|
||||||
Object.values(children).forEach((ch: AgsWidget) => {
|
|
||||||
ch.setCss(`
|
|
||||||
background-color: ${c.hoverAccent};
|
|
||||||
color: ${c.buttonText};
|
|
||||||
min-height: 40px;
|
|
||||||
min-width: 36px;
|
|
||||||
margin-bottom: 1px;
|
|
||||||
margin-right: 1px;
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
on_hover_lost: (self) => {
|
|
||||||
self.attribute.hovered = false;
|
|
||||||
if (prop === 'playBackStatus' && colors.value) {
|
|
||||||
const c = colors.value;
|
|
||||||
|
|
||||||
Object.values(children).forEach((ch: AgsWidget) => {
|
|
||||||
ch.setCss(`
|
|
||||||
background-color: ${c.buttonAccent};
|
|
||||||
color: ${c.buttonText};
|
|
||||||
min-height: 42px;
|
|
||||||
min-width: 38px;
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setup: (self) => {
|
|
||||||
self
|
|
||||||
.hook(player, () => {
|
|
||||||
self.child.shown = `${player[prop]}`;
|
|
||||||
})
|
|
||||||
.hook(colors, () => {
|
|
||||||
if (!Mpris.players.find((p) => player === p)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (colors.value) {
|
|
||||||
const c = colors.value;
|
|
||||||
|
|
||||||
if (prop === 'playBackStatus') {
|
|
||||||
if (self.attribute.hovered) {
|
|
||||||
Object.values(children)
|
|
||||||
.forEach((ch: AgsWidget) => {
|
|
||||||
ch.setCss(`
|
|
||||||
background-color: ${c.hoverAccent};
|
|
||||||
color: ${c.buttonText};
|
|
||||||
min-height: 40px;
|
|
||||||
min-width: 36px;
|
|
||||||
margin-bottom: 1px;
|
|
||||||
margin-right: 1px;
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Object.values(children)
|
|
||||||
.forEach((ch: AgsWidget) => {
|
|
||||||
ch.setCss(`
|
|
||||||
background-color: ${c.buttonAccent};
|
|
||||||
color: ${c.buttonText};
|
|
||||||
min-height: 42px;
|
|
||||||
min-width: 38px;
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
self.setCss(`
|
|
||||||
* { color: ${c.buttonAccent}; }
|
|
||||||
*:hover { color: ${c.hoverAccent}; }
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ShuffleButton = (
|
|
||||||
player: MprisPlayer,
|
|
||||||
colors: Var<Colors>,
|
|
||||||
) => PlayerButton({
|
|
||||||
player,
|
|
||||||
colors,
|
|
||||||
children: {
|
|
||||||
true: Label({
|
|
||||||
class_name: 'shuffle enabled',
|
|
||||||
label: icons.mpris.shuffle.enabled,
|
|
||||||
}),
|
|
||||||
false: Label({
|
|
||||||
class_name: 'shuffle disabled',
|
|
||||||
label: icons.mpris.shuffle.disabled,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
onClick: 'shuffle',
|
|
||||||
prop: 'shuffleStatus',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const LoopButton = (
|
|
||||||
player: MprisPlayer,
|
|
||||||
colors: Var<Colors>,
|
|
||||||
) => PlayerButton({
|
|
||||||
player,
|
|
||||||
colors,
|
|
||||||
children: {
|
|
||||||
None: Label({
|
|
||||||
class_name: 'loop none',
|
|
||||||
label: icons.mpris.loop.none,
|
|
||||||
}),
|
|
||||||
Track: Label({
|
|
||||||
class_name: 'loop track',
|
|
||||||
label: icons.mpris.loop.track,
|
|
||||||
}),
|
|
||||||
Playlist: Label({
|
|
||||||
class_name: 'loop playlist',
|
|
||||||
label: icons.mpris.loop.playlist,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
onClick: 'loop',
|
|
||||||
prop: 'loopStatus',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const PlayPauseButton = (
|
|
||||||
player: MprisPlayer,
|
|
||||||
colors: Var<Colors>,
|
|
||||||
) => PlayerButton({
|
|
||||||
player,
|
|
||||||
colors,
|
|
||||||
children: {
|
|
||||||
Playing: Label({
|
|
||||||
class_name: 'pausebutton playing',
|
|
||||||
label: icons.mpris.playing,
|
|
||||||
}),
|
|
||||||
Paused: Label({
|
|
||||||
class_name: 'pausebutton paused',
|
|
||||||
label: icons.mpris.paused,
|
|
||||||
}),
|
|
||||||
Stopped: Label({
|
|
||||||
class_name: 'pausebutton stopped paused',
|
|
||||||
label: icons.mpris.stopped,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
onClick: 'playPause',
|
|
||||||
prop: 'playBackStatus',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const PreviousButton = (
|
|
||||||
player: MprisPlayer,
|
|
||||||
colors: Var<Colors>,
|
|
||||||
) => PlayerButton({
|
|
||||||
player,
|
|
||||||
colors,
|
|
||||||
children: {
|
|
||||||
true: Label({
|
|
||||||
class_name: 'previous',
|
|
||||||
label: icons.mpris.prev,
|
|
||||||
}),
|
|
||||||
false: Label({
|
|
||||||
class_name: 'previous',
|
|
||||||
label: icons.mpris.prev,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
onClick: 'previous',
|
|
||||||
prop: 'canGoPrev',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const NextButton = (
|
|
||||||
player: MprisPlayer,
|
|
||||||
colors: Var<Colors>,
|
|
||||||
) => PlayerButton({
|
|
||||||
player,
|
|
||||||
colors,
|
|
||||||
children: {
|
|
||||||
true: Label({
|
|
||||||
class_name: 'next',
|
|
||||||
label: icons.mpris.next,
|
|
||||||
}),
|
|
||||||
false: Label({
|
|
||||||
class_name: 'next',
|
|
||||||
label: icons.mpris.next,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
onClick: 'next',
|
|
||||||
prop: 'canGoNext',
|
|
||||||
});
|
|
|
@ -1,201 +0,0 @@
|
||||||
const Mpris = await Service.import('mpris');
|
|
||||||
|
|
||||||
const { Box, CenterBox } = Widget;
|
|
||||||
|
|
||||||
import * as mpris from './mpris.ts';
|
|
||||||
import PlayerGesture from './gesture.ts';
|
|
||||||
import Separator from '../misc/separator.ts';
|
|
||||||
|
|
||||||
const FAVE_PLAYER = 'org.mpris.MediaPlayer2.spotify';
|
|
||||||
const SPACING = 8;
|
|
||||||
|
|
||||||
// Types
|
|
||||||
import { MprisPlayer } from 'types/service/mpris.ts';
|
|
||||||
import { Variable as Var } from 'types/variable';
|
|
||||||
import { Colors, PlayerBox, PlayerOverlay } from 'global-types';
|
|
||||||
|
|
||||||
|
|
||||||
const Top = (
|
|
||||||
player: MprisPlayer,
|
|
||||||
overlay: PlayerOverlay,
|
|
||||||
) => Box({
|
|
||||||
class_name: 'top',
|
|
||||||
hpack: 'start',
|
|
||||||
vpack: 'start',
|
|
||||||
|
|
||||||
children: [
|
|
||||||
mpris.PlayerIcon(player, overlay),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const Center = (
|
|
||||||
player: MprisPlayer,
|
|
||||||
colors: Var<Colors>,
|
|
||||||
) => Box({
|
|
||||||
class_name: 'center',
|
|
||||||
|
|
||||||
children: [
|
|
||||||
CenterBox({
|
|
||||||
vertical: true,
|
|
||||||
|
|
||||||
start_widget: Box({
|
|
||||||
class_name: 'metadata',
|
|
||||||
vertical: true,
|
|
||||||
hpack: 'start',
|
|
||||||
vpack: 'center',
|
|
||||||
hexpand: true,
|
|
||||||
|
|
||||||
children: [
|
|
||||||
mpris.TitleLabel(player),
|
|
||||||
mpris.ArtistLabel(player),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
|
|
||||||
CenterBox({
|
|
||||||
vertical: true,
|
|
||||||
|
|
||||||
center_widget: mpris.PlayPauseButton(player, colors),
|
|
||||||
}),
|
|
||||||
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const Bottom = (
|
|
||||||
player: MprisPlayer,
|
|
||||||
colors: Var<Colors>,
|
|
||||||
) => Box({
|
|
||||||
class_name: 'bottom',
|
|
||||||
|
|
||||||
children: [
|
|
||||||
mpris.PreviousButton(player, colors),
|
|
||||||
Separator(SPACING),
|
|
||||||
|
|
||||||
mpris.PositionSlider(player, colors),
|
|
||||||
Separator(SPACING),
|
|
||||||
|
|
||||||
mpris.NextButton(player, colors),
|
|
||||||
Separator(SPACING),
|
|
||||||
|
|
||||||
mpris.ShuffleButton(player, colors),
|
|
||||||
Separator(SPACING),
|
|
||||||
|
|
||||||
mpris.LoopButton(player, colors),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const PlayerBox = (
|
|
||||||
player: MprisPlayer,
|
|
||||||
colors: Var<Colors>,
|
|
||||||
overlay: PlayerOverlay,
|
|
||||||
) => {
|
|
||||||
const widget = mpris.CoverArt(player, colors, {
|
|
||||||
class_name: `player ${player.name}`,
|
|
||||||
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: PlayerOverlay) => {
|
|
||||||
self
|
|
||||||
.hook(Mpris, (_, bus_name) => {
|
|
||||||
const players = self.attribute.players;
|
|
||||||
|
|
||||||
if (players.has(bus_name)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sometimes the signal doesn't give the bus_name
|
|
||||||
if (!bus_name) {
|
|
||||||
const player = Mpris.players.find((p) => {
|
|
||||||
return !players.has(p.bus_name);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (player) {
|
|
||||||
bus_name = player.bus_name;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the one on top so we can move it up later
|
|
||||||
const previousFirst = self.overlays.at(-1) as PlayerBox;
|
|
||||||
|
|
||||||
// Make the new player
|
|
||||||
const player = Mpris.getPlayer(bus_name);
|
|
||||||
const colorsVar = Variable({
|
|
||||||
imageAccent: '#6b4fa2',
|
|
||||||
buttonAccent: '#ecdcff',
|
|
||||||
buttonText: '#25005a',
|
|
||||||
hoverAccent: '#d4baff',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!player) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
players.set(
|
|
||||||
bus_name,
|
|
||||||
PlayerBox(player, colorsVar, self),
|
|
||||||
);
|
|
||||||
self.overlays = Array.from(players.values())
|
|
||||||
.map((widget) => widget) as PlayerBox[];
|
|
||||||
|
|
||||||
const includes = self.attribute
|
|
||||||
.includesWidget(previousFirst);
|
|
||||||
|
|
||||||
// Select favorite player at startup
|
|
||||||
const attrs = self.attribute;
|
|
||||||
|
|
||||||
if (!attrs.setup && players.has(FAVE_PLAYER)) {
|
|
||||||
attrs.moveToTop(players.get(FAVE_PLAYER));
|
|
||||||
attrs.setup = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move previousFirst on top again
|
|
||||||
else if (includes) {
|
|
||||||
attrs.moveToTop(previousFirst);
|
|
||||||
}
|
|
||||||
}, 'player-added')
|
|
||||||
|
|
||||||
.hook(Mpris, (_, bus_name) => {
|
|
||||||
const players = self.attribute.players;
|
|
||||||
|
|
||||||
if (!bus_name || !players.has(bus_name)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the one on top so we can move it up later
|
|
||||||
const previousFirst = self.overlays.at(-1) as PlayerBox;
|
|
||||||
|
|
||||||
// Remake overlays without deleted one
|
|
||||||
players.delete(bus_name);
|
|
||||||
self.overlays = Array.from(players.values())
|
|
||||||
.map((widget) => widget) as PlayerBox[];
|
|
||||||
|
|
||||||
// Move previousFirst on top again
|
|
||||||
const includes = self.attribute
|
|
||||||
.includesWidget(previousFirst);
|
|
||||||
|
|
||||||
if (includes) {
|
|
||||||
self.attribute.moveToTop(previousFirst);
|
|
||||||
}
|
|
||||||
}, 'player-closed');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return Box({
|
|
||||||
class_name: 'media',
|
|
||||||
child: content,
|
|
||||||
});
|
|
||||||
};
|
|
1
packages/coloryou/LICENSE
Normal file
1
packages/coloryou/LICENSE
Normal file
File diff suppressed because one or more lines are too long
45
packages/coloryou/coloryou.py
Executable file
45
packages/coloryou/coloryou.py
Executable file
|
@ -0,0 +1,45 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
"""
|
||||||
|
The original script:
|
||||||
|
https://github.com/dharmx/vile/blob/7d486c128c7e553912673755f97b118aaab0193d/src/shell/playerctl.py#L2
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from material_color_utilities_python import themeFromImage, hexFromArgb, Image
|
||||||
|
|
||||||
|
def range_type(value_string):
|
||||||
|
value = int(value_string)
|
||||||
|
if value not in range(0, 101):
|
||||||
|
raise argparse.ArgumentTypeError("%s is out of range, choose in [0-100]" % value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog='coloryou',
|
||||||
|
description='This program extract Material You colors from an image. It returns them as a JSON object for scripting.')
|
||||||
|
|
||||||
|
parser.add_argument("image_path", help="A full path to your image", type=str)
|
||||||
|
parser.add_argument('-i', '--image', dest='image', default=40, type=range_type, metavar='i', choices=range(0,101), help="Value should be within [0, 100] (default: %(default)s). Set the tone for the main image accent.")
|
||||||
|
|
||||||
|
parser.add_argument('-b', '--button', dest='button', default=90, type=range_type, metavar='i', choices=range(0,101), help="Value should be within [0, 100] (default: %(default)s). Set the tone for the button accent.")
|
||||||
|
|
||||||
|
parser.add_argument('-t', '--text', dest='text', default=10, type=range_type, metavar='i', choices=range(0,101), help="Value should be within [0, 100] (default: %(default)s). Set the tone for the button text accent.")
|
||||||
|
|
||||||
|
parser.add_argument('-o', '--hover', dest='hover', default=80, type=range_type, metavar='i', choices=range(0,101), help="Value should be within [0, 100] (default: %(default)s). Set the tone for the hovering effect accent.")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
img = Image.open(args.image_path)
|
||||||
|
basewidth = 64
|
||||||
|
wpercent = (basewidth/float(img.size[0]))
|
||||||
|
hsize = int((float(img.size[1])*float(wpercent)))
|
||||||
|
img = img.resize((basewidth,hsize),Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
|
theme = themeFromImage(img).get("palettes").get("primary")
|
||||||
|
parsed_colors = {"imageAccent": hexFromArgb(theme.tone(args.image)),
|
||||||
|
"buttonAccent": hexFromArgb(theme.tone(args.button)),
|
||||||
|
"buttonText": hexFromArgb(theme.tone(args.text)),
|
||||||
|
"hoverAccent": hexFromArgb(theme.tone(args.hover))}
|
||||||
|
|
||||||
|
print(json.dumps(parsed_colors))
|
1
packages/coloryou/coloryou/LICENSE
Normal file
1
packages/coloryou/coloryou/LICENSE
Normal file
File diff suppressed because one or more lines are too long
45
packages/coloryou/coloryou/coloryou.py
Executable file
45
packages/coloryou/coloryou/coloryou.py
Executable file
|
@ -0,0 +1,45 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
"""
|
||||||
|
The original script:
|
||||||
|
https://github.com/dharmx/vile/blob/7d486c128c7e553912673755f97b118aaab0193d/src/shell/playerctl.py#L2
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from material_color_utilities_python import themeFromImage, hexFromArgb, Image
|
||||||
|
|
||||||
|
def range_type(value_string):
|
||||||
|
value = int(value_string)
|
||||||
|
if value not in range(0, 101):
|
||||||
|
raise argparse.ArgumentTypeError("%s is out of range, choose in [0-100]" % value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog='coloryou',
|
||||||
|
description='This program extract Material You colors from an image. It returns them as a JSON object for scripting.')
|
||||||
|
|
||||||
|
parser.add_argument("image_path", help="A full path to your image", type=str)
|
||||||
|
parser.add_argument('-i', '--image', dest='image', default=40, type=range_type, metavar='i', choices=range(0,101), help="Value should be within [0, 100] (default: %(default)s). Set the tone for the main image accent.")
|
||||||
|
|
||||||
|
parser.add_argument('-b', '--button', dest='button', default=90, type=range_type, metavar='i', choices=range(0,101), help="Value should be within [0, 100] (default: %(default)s). Set the tone for the button accent.")
|
||||||
|
|
||||||
|
parser.add_argument('-t', '--text', dest='text', default=10, type=range_type, metavar='i', choices=range(0,101), help="Value should be within [0, 100] (default: %(default)s). Set the tone for the button text accent.")
|
||||||
|
|
||||||
|
parser.add_argument('-o', '--hover', dest='hover', default=80, type=range_type, metavar='i', choices=range(0,101), help="Value should be within [0, 100] (default: %(default)s). Set the tone for the hovering effect accent.")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
img = Image.open(args.image_path)
|
||||||
|
basewidth = 64
|
||||||
|
wpercent = (basewidth/float(img.size[0]))
|
||||||
|
hsize = int((float(img.size[1])*float(wpercent)))
|
||||||
|
img = img.resize((basewidth,hsize),Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
|
theme = themeFromImage(img).get("palettes").get("primary")
|
||||||
|
parsed_colors = {"imageAccent": hexFromArgb(theme.tone(args.image)),
|
||||||
|
"buttonAccent": hexFromArgb(theme.tone(args.button)),
|
||||||
|
"buttonText": hexFromArgb(theme.tone(args.text)),
|
||||||
|
"hoverAccent": hexFromArgb(theme.tone(args.hover))}
|
||||||
|
|
||||||
|
print(json.dumps(parsed_colors))
|
20
packages/coloryou/coloryou/default.nix
Normal file
20
packages/coloryou/coloryou/default.nix
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{ python3Packages }:
|
||||||
|
|
||||||
|
python3Packages.buildPythonPackage rec {
|
||||||
|
pname = "coloryou";
|
||||||
|
version = "0.0.1";
|
||||||
|
|
||||||
|
src = ./.;
|
||||||
|
|
||||||
|
propagatedBuildInputs = with python3Packages; [ utils material-color-utilities ];
|
||||||
|
|
||||||
|
postInstall = ''
|
||||||
|
mv -v $out/bin/coloryou.py $out/bin/coloryou
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
description = ''
|
||||||
|
Get Material You colors from an image.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
2
packages/coloryou/coloryou/requirements.txt
Normal file
2
packages/coloryou/coloryou/requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
material-color-utilities
|
||||||
|
utils
|
7
packages/coloryou/coloryou/setup.py
Normal file
7
packages/coloryou/coloryou/setup.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from distutils.core import setup
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='coloryou',
|
||||||
|
version='0.0.1',
|
||||||
|
scripts=['coloryou.py',],
|
||||||
|
)
|
8
packages/coloryou/coloryou/shell.nix
Normal file
8
packages/coloryou/coloryou/shell.nix
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
with import <nixpkgs> {};
|
||||||
|
with pkgs.python311Packages;
|
||||||
|
|
||||||
|
buildPythonPackage rec {
|
||||||
|
name = "coloryou";
|
||||||
|
src = ./.;
|
||||||
|
propagatedBuildInputs = [ material-color-utilities utils ];
|
||||||
|
}
|
20
packages/coloryou/default.nix
Normal file
20
packages/coloryou/default.nix
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{ python3Packages }:
|
||||||
|
|
||||||
|
python3Packages.buildPythonPackage rec {
|
||||||
|
pname = "coloryou";
|
||||||
|
version = "0.0.1";
|
||||||
|
|
||||||
|
src = ./.;
|
||||||
|
|
||||||
|
propagatedBuildInputs = with python3Packages; [ utils material-color-utilities ];
|
||||||
|
|
||||||
|
postInstall = ''
|
||||||
|
mv -v $out/bin/coloryou.py $out/bin/coloryou
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
description = ''
|
||||||
|
Get Material You colors from an image.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
2
packages/coloryou/requirements.txt
Normal file
2
packages/coloryou/requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
material-color-utilities
|
||||||
|
utils
|
7
packages/coloryou/setup.py
Normal file
7
packages/coloryou/setup.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from distutils.core import setup
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='coloryou',
|
||||||
|
version='0.0.1',
|
||||||
|
scripts=['coloryou.py',],
|
||||||
|
)
|
8
packages/coloryou/shell.nix
Normal file
8
packages/coloryou/shell.nix
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
with import <nixpkgs> {};
|
||||||
|
with pkgs.python311Packages;
|
||||||
|
|
||||||
|
buildPythonPackage rec {
|
||||||
|
name = "coloryou";
|
||||||
|
src = ./.;
|
||||||
|
propagatedBuildInputs = [ material-color-utilities utils ];
|
||||||
|
}
|
|
@ -4,6 +4,8 @@
|
||||||
pkgs,
|
pkgs,
|
||||||
...
|
...
|
||||||
}: {
|
}: {
|
||||||
|
coloryou = pkgs.callPackage ./coloryou {};
|
||||||
|
|
||||||
gpu-screen-recorder = pkgs.callPackage ./gpu-screen-recorder/gpu-screen-recorder.nix {
|
gpu-screen-recorder = pkgs.callPackage ./gpu-screen-recorder/gpu-screen-recorder.nix {
|
||||||
inherit (inputs) gpu-screen-recorder-src;
|
inherit (inputs) gpu-screen-recorder-src;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Reference in a new issue