parent
1ce40f2c1a
commit
6ca0d7248b
329 changed files with 178 additions and 139 deletions
modules/ags/v1/config/ts
26
modules/ags/v1/config/ts/bar/heart.ts
Normal file
26
modules/ags/v1/config/ts/bar/heart.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
const { Label } = Widget;
|
||||
|
||||
import CursorBox from '../../misc/cursorbox.ts';
|
||||
import Persist from '../../misc/persist.ts';
|
||||
|
||||
const HeartState = Variable('');
|
||||
|
||||
Persist({
|
||||
name: 'heart',
|
||||
gobject: HeartState,
|
||||
prop: 'value',
|
||||
condition: '',
|
||||
whenFalse: '',
|
||||
});
|
||||
|
||||
|
||||
export default () => CursorBox({
|
||||
on_primary_click_release: () => {
|
||||
HeartState.setValue(HeartState.value === '' ? '' : '');
|
||||
},
|
||||
|
||||
child: Label({
|
||||
class_name: 'heart-toggle',
|
||||
label: HeartState.bind(),
|
||||
}),
|
||||
});
|
54
modules/ags/v1/config/ts/bar/keyboard-layout.ts
Normal file
54
modules/ags/v1/config/ts/bar/keyboard-layout.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
const Hyprland = await Service.import('hyprland');
|
||||
const { Icon, Label } = Widget;
|
||||
|
||||
import HoverRevealer from './hover-revealer.ts';
|
||||
|
||||
const DEFAULT_KB = 'at-translated-set-2-keyboard';
|
||||
|
||||
// Types
|
||||
import { Keyboard, LabelGeneric } from 'global-types';
|
||||
|
||||
|
||||
const getKbdLayout = (self: LabelGeneric, _: string, layout: string) => {
|
||||
if (layout) {
|
||||
if (layout === 'error') {
|
||||
return;
|
||||
}
|
||||
|
||||
const shortName = layout.match(/\(([A-Za-z]+)\)/);
|
||||
|
||||
self.label = shortName ? shortName[1] : layout;
|
||||
}
|
||||
else {
|
||||
// At launch, kb layout is undefined
|
||||
Hyprland.messageAsync('j/devices').then((obj) => {
|
||||
const keyboards = Array.from(JSON.parse(obj)
|
||||
.keyboards) as Keyboard[];
|
||||
const kb = keyboards.find((v) => v.name === DEFAULT_KB);
|
||||
|
||||
if (kb) {
|
||||
layout = kb.active_keymap;
|
||||
|
||||
const shortName = layout
|
||||
.match(/\(([A-Za-z]+)\)/);
|
||||
|
||||
self.label = shortName ? shortName[1] : layout;
|
||||
}
|
||||
else {
|
||||
self.label = 'None';
|
||||
}
|
||||
}).catch(print);
|
||||
}
|
||||
};
|
||||
|
||||
export default () => HoverRevealer({
|
||||
class_name: 'keyboard',
|
||||
spacing: 4,
|
||||
|
||||
icon: Icon({
|
||||
icon: 'input-keyboard-symbolic',
|
||||
size: 20,
|
||||
}),
|
||||
label: Label({ css: 'font-size: 20px;' })
|
||||
.hook(Hyprland, getKbdLayout, 'keyboard-layout'),
|
||||
});
|
167
modules/ags/v1/config/ts/media-player/gesture.ts
Normal file
167
modules/ags/v1/config/ts/media-player/gesture.ts
Normal file
|
@ -0,0 +1,167 @@
|
|||
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;
|
||||
};
|
473
modules/ags/v1/config/ts/media-player/mpris.ts
Normal file
473
modules/ags/v1/config/ts/media-player/mpris.ts
Normal file
|
@ -0,0 +1,473 @@
|
|||
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',
|
||||
});
|
201
modules/ags/v1/config/ts/media-player/player.ts
Normal file
201
modules/ags/v1/config/ts/media-player/player.ts
Normal file
|
@ -0,0 +1,201 @@
|
|||
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,
|
||||
});
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue