214 lines
5.4 KiB
TypeScript
214 lines
5.4 KiB
TypeScript
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;
|
|
};
|