refactor: make the whole thing more generic

This commit is contained in:
2024-04-02 16:28:57 +02:00
parent 7b648e1955
commit 651f3ad808
193 changed files with 763 additions and 521 deletions

View File

@@ -0,0 +1,16 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import options from "../options.js";
/** @param {import('types/widgets/box').BoxProps=} props */
export default (props) =>
Widget.Box({ ...props, class_name: "avatar" })
.hook(options.desktop.avatar, (box) =>
box.setCss(`
background-image: url('${options.desktop.avatar.value}');
background-size: cover;
`),
)
.on("size-allocate", (box) => {
const h = box.get_allocated_height();
box.set_size_request(Math.ceil(h * 1.1), -1);
});

View File

@@ -0,0 +1,14 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Battery from "resource:///com/github/Aylur/ags/service/battery.js";
export default () =>
Widget.Icon({
class_name: "battery",
icon: Battery.bind("icon_name"),
setup: (icon) =>
icon.hook(Battery, () => {
icon.toggleClassName("charging", Battery.charging);
icon.toggleClassName("charged", Battery.charged);
icon.toggleClassName("low", Battery.percent < 30);
}),
});

View File

@@ -0,0 +1,17 @@
import { clock } from "../variables.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
/**
* @param {import('types/widgets/label').Props & {
* format?: string,
* interval?: number,
* }} o
*/
export default ({ format = "%H:%M:%S %B %e. %A", ...rest } = {}) =>
Widget.Label({
class_name: "clock",
label: clock.bind("value").transform((time) => {
return time.format(format) || "wrong format";
}),
...rest,
});

View File

@@ -0,0 +1,48 @@
import Gtk from "gi://Gtk?version=3.0";
import { subclass, register } from "resource:///com/github/Aylur/ags/widget.js";
import AgsLabel from "resource:///com/github/Aylur/ags/widgets/label.js";
class FontIcon extends AgsLabel {
static {
register(this);
}
/** @param {string | import('types/widgets/label').Props<any> & { icon?: string }} params */
constructor(params = "") {
// @ts-expect-error
const { icon = "", ...rest } = params;
super(typeof params === "string" ? {} : rest);
this.toggleClassName("font-icon");
if (typeof params === "object") this.icon = icon;
if (typeof params === "string") this.icon = params;
}
get icon() {
return this.label;
}
set icon(icon) {
this.label = icon;
}
get size() {
return this.get_style_context().get_property(
"font-size",
Gtk.StateFlags.NORMAL,
);
}
/** @returns {[number, number]} */
vfunc_get_preferred_height() {
return [this.size, this.size];
}
/** @returns {[number, number]} */
vfunc_get_preferred_width() {
return [this.size, this.size];
}
}
export default subclass(FontIcon);

View File

@@ -0,0 +1,62 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
/**
* @typedef {import('types/widgets/eventbox').EventBoxProps & {
* indicator?: import('types/widgets/box').BoxProps['child']
* direction?: 'left' | 'right' | 'down' | 'up'
* duration?: number
* setupRevealer?: (rev: ReturnType<typeof Widget.Revealer>) => void
* setupEventBox?: (rev: ReturnType<typeof Widget.EventBox>) => void
* }} HoverRevealProps
*/
/**
* @param {HoverRevealProps} props
*/
export default ({
indicator,
child,
direction = "left",
duration = 300,
setupEventBox,
setupRevealer,
...rest
}) => {
let open = false;
const vertical = direction === "down" || direction === "up";
const posStart = direction === "down" || direction === "right";
const posEnd = direction === "up" || direction === "left";
const revealer = Widget.Revealer({
transition: `slide_${direction}`,
setup: setupRevealer,
transition_duration: duration,
child,
});
const eventbox = Widget.EventBox({
...rest,
setup: setupEventBox,
on_hover: () => {
if (open) return;
revealer.reveal_child = true;
Utils.timeout(duration, () => (open = true));
},
on_hover_lost: () => {
if (!open) return;
revealer.reveal_child = false;
open = false;
},
child: Widget.Box({
vertical,
children: [posStart && indicator, revealer, posEnd && indicator],
}),
});
return Widget.Box({
children: [eventbox],
});
};

View File

@@ -0,0 +1,63 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import RegularWindow from "./RegularWindow.js";
import Gtk from "gi://Gtk";
export default () => {
const selected = Widget.Label({
css: "font-size: 1.2em;",
});
const flowbox = Widget.FlowBox({
min_children_per_line: 10,
setup: (self) => {
self.connect("child-activated", (_, child) => {
selected.label = child.get_child().iconName;
});
Gtk.IconTheme.get_default()
.list_icons(null)
.sort()
.map((icon) => {
!icon.endsWith(".symbolic") &&
self.insert(
Widget.Icon({
icon,
size: 38,
}),
-1,
);
});
self.show_all();
},
});
const entry = Widget.Entry({
on_change: ({ text }) =>
flowbox.get_children().forEach((child) => {
child.visible = child.get_child().iconName.includes(text);
}),
});
return RegularWindow({
name: "icons",
visible: true,
child: Widget.Box({
css: "padding: 30px;",
spacing: 20,
vertical: true,
children: [
entry,
Widget.Scrollable({
hscroll: "never",
vscroll: "always",
hexpand: true,
vexpand: true,
css: "min-width: 500px;" + "min-height: 500px;",
child: flowbox,
}),
selected,
],
}),
});
};

View File

@@ -0,0 +1,132 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import GLib from "gi://GLib";
/** @param {import('types/service/notifications').Notification} n */
const NotificationIcon = ({ app_entry, app_icon, image }) => {
if (image) {
return Widget.Box({
vpack: "start",
hexpand: false,
class_name: "icon img",
css: `
background-image: url("${image}");
background-size: cover;
background-repeat: no-repeat;
background-position: center;
min-width: 78px;
min-height: 78px;
`,
});
}
let icon = "dialog-information-symbolic";
if (Utils.lookUpIcon(app_icon)) icon = app_icon;
if (Utils.lookUpIcon(app_entry || "")) icon = app_entry || "";
return Widget.Box({
vpack: "start",
hexpand: false,
class_name: "icon",
css: `
min-width: 78px;
min-height: 78px;
`,
child: Widget.Icon({
icon,
size: 58,
hpack: "center",
hexpand: true,
vpack: "center",
vexpand: true,
}),
});
};
/** @param {import('types/service/notifications').Notification} notification */
export default (notification) => {
const content = Widget.Box({
class_name: "content",
children: [
NotificationIcon(notification),
Widget.Box({
hexpand: true,
vertical: true,
children: [
Widget.Box({
children: [
Widget.Label({
class_name: "title",
xalign: 0,
justification: "left",
hexpand: true,
max_width_chars: 24,
truncate: "end",
wrap: true,
label: notification.summary,
use_markup: true,
}),
Widget.Label({
class_name: "time",
vpack: "start",
label: GLib.DateTime.new_from_unix_local(
notification.time,
).format("%H:%M"),
}),
Widget.Button({
class_name: "close-button",
vpack: "start",
child: Widget.Icon("window-close-symbolic"),
on_clicked: () => notification.close(),
}),
],
}),
Widget.Label({
class_name: "description",
hexpand: true,
use_markup: true,
xalign: 0,
justification: "left",
label: notification.body,
wrap: true,
}),
],
}),
],
});
const actionsbox = Widget.Revealer({
transition: "slide_down",
child: Widget.EventBox({
child: Widget.Box({
class_name: "actions horizontal",
children: notification.actions.map((action) =>
Widget.Button({
class_name: "action-button",
on_clicked: () => notification.invoke(action.id),
hexpand: true,
child: Widget.Label(action.label),
}),
),
}),
}),
});
return Widget.EventBox({
class_name: `notification ${notification.urgency}`,
vexpand: false,
on_primary_click: () => notification.dismiss(),
on_hover() {
actionsbox.reveal_child = true;
},
on_hover_lost() {
actionsbox.reveal_child = true;
notification.dismiss();
},
child: Widget.Box({
vertical: true,
children: [content, notification.actions.length > 0 && actionsbox],
}),
});
};

View File

@@ -0,0 +1,83 @@
import AgsWindow from "resource:///com/github/Aylur/ags/widgets/window.js";
import App from "resource:///com/github/Aylur/ags/app.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import options from "../options.js";
import GObject from "gi://GObject";
const keyGrabber = Widget.Window({
name: "key-grabber",
popup: true,
anchor: ["top", "left", "right", "bottom"],
css: "background-color: transparent;",
visible: false,
exclusivity: "ignore",
keymode: "on-demand",
layer: "top",
attribute: { list: [] },
setup: (self) =>
self.on("notify::visible", ({ visible }) => {
if (!visible)
self.attribute?.list.forEach((name) => App.closeWindow(name));
}),
child: Widget.EventBox({ vexpand: true }).on("button-press-event", () => {
App.closeWindow("key-grabber");
keyGrabber.attribute?.list.forEach((name) => App.closeWindow(name));
}),
});
// add before any PopupWindow is instantiated
App.addWindow(keyGrabber);
export class PopupWindow extends AgsWindow {
static {
GObject.registerClass(this);
}
constructor({ name, child, transition = "none", visible = false, ...rest }) {
super({
...rest,
name,
popup: true,
keymode: "exclusive",
layer: "overlay",
class_names: ["popup-window", name],
});
child.toggleClassName("window-content");
this.revealer = Widget.Revealer({
transition,
child,
transition_duration: options.transition.value,
setup: (self) =>
self.hook(App, (_, wname, visible) => {
if (wname === name) this.revealer.reveal_child = visible;
}),
});
this.child = Widget.Box({
css: "padding: 1px;",
child: this.revealer,
});
this.show_all();
this.visible = visible;
keyGrabber.bind("visible", this, "visible");
keyGrabber.attribute?.list.push(name);
}
set transition(dir) {
this.revealer.transition = dir;
}
get transition() {
return this.revealer.transition;
}
}
/** @param {import('types/widgets/window').WindowProps & {
* name: string
* child: import('types/widgets/box').default
* transition?: import('types/widgets/revealer').RevealerProps['transition']
* }} config
*/
export default (config) => new PopupWindow(config);

View File

@@ -0,0 +1,60 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
/** @param {import('types/widgets/box').BoxProps & {
* width: number
* height: number
* }} o */
export default ({
height = 18,
width = 180,
vertical = false,
child,
...props
}) => {
const fill = Widget.Box({
class_name: "fill",
hexpand: vertical,
vexpand: !vertical,
hpack: vertical ? "fill" : "start",
vpack: vertical ? "end" : "fill",
children: [child],
});
let fill_size = 0;
return Widget.Box({
...props,
class_name: "progress",
css: `
min-width: ${width}px;
min-height: ${height}px;
`,
children: [fill],
attribute: (value) => {
if (value < 0) return;
const axis = vertical ? "height" : "width";
const axisv = vertical ? height : width;
const min = vertical ? width : height;
const preferred = (axisv - min) * value + min;
if (!fill_size) {
fill_size = preferred;
fill.setCss(`min-${axis}: ${preferred}px;`);
return;
}
const frames = 10;
const goal = preferred - fill_size;
const step = goal / frames;
for (let i = 0; i < frames; ++i) {
Utils.timeout(5 * i, () => {
fill_size += step;
fill.setCss(`min-${axis}: ${fill_size}px`);
});
}
},
});
};

View File

@@ -0,0 +1,4 @@
import { subclass } from "resource:///com/github/Aylur/ags/widget.js";
import Gtk from "gi://Gtk";
export default subclass(Gtk.Window, "RegularWindow");

View File

@@ -0,0 +1,263 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import icons from "../icons.js";
import { blurImg } from "../utils.js";
/**
* @param {import('types/service/mpris').MprisPlayer} player
* @param {import('types/widgets/box').BoxProps=} props
*/
export const CoverArt = (player, props) =>
Widget.Box({
...props,
class_name: "cover",
css: player
.bind("cover_path")
.transform((p) => `background-image: url("${p}")`),
});
/**
* @param {import('types/service/mpris').MprisPlayer} player
* @param {import('types/widgets/box').BoxProps=} props
*/
export const BlurredCoverArt = (player, props) =>
Widget.Box({
...props,
class_name: "blurred-cover",
setup: (self) =>
self.hook(
player,
(box) =>
blurImg(player.cover_path).then((img) => {
img && box.setCss(`background-image: url("${img}")`);
}),
"notify::cover-path",
),
});
/**
* @param {import('types/service/mpris').MprisPlayer} player
* @param {import('types/widgets/label').Props=} props
*/
export const TitleLabel = (player, props) =>
Widget.Label({
...props,
class_name: "title",
label: player.bind("track_title"),
});
/**
* @param {import('types/service/mpris').MprisPlayer} player
* @param {import('types/widgets/label').Props=} props
*/
export const ArtistLabel = (player, props) =>
Widget.Label({
...props,
class_name: "artist",
label: player.bind("track_artists").transform((a) => a.join(", ") || ""),
});
/**
* @param {import('types/service/mpris').MprisPlayer} player
* @param {import('types/widgets/icon').Props & { symbolic?: boolean }=} props
*/
export const PlayerIcon = (player, { symbolic = true, ...props } = {}) =>
Widget.Icon({
...props,
class_name: "player-icon",
tooltip_text: player.identity || "",
setup: (self) =>
self.hook(player, (icon) => {
const name = `${player.entry}${symbolic ? "-symbolic" : ""}`;
Utils.lookUpIcon(name)
? (icon.icon = name)
: (icon.icon = icons.mpris.fallback);
}),
});
/**
* @param {import('types/service/mpris').MprisPlayer} player
* @param {import('types/widgets/slider').SliderProps=} props
*/
export const PositionSlider = (player, props) =>
Widget.Slider({
...props,
class_name: "position-slider",
draw_value: false,
on_change: ({ value }) => (player.position = player.length * value),
setup: (self) => {
const update = () => {
if (self.dragging) return;
self.visible = player.length > 0;
if (player.length > 0) self.value = player.position / player.length;
};
self.hook(player, update);
self.hook(player, update, "position");
self.poll(1000, update);
},
});
/** @param {number} length */
function lengthStr(length) {
const min = Math.floor(length / 60);
const sec = Math.floor(length % 60);
const sec0 = sec < 10 ? "0" : "";
return `${min}:${sec0}${sec}`;
}
/** @param {import('types/service/mpris').MprisPlayer} player */
export const PositionLabel = (player) =>
Widget.Label({
setup: (self) => {
const update = (_, time) => {
player.length > 0
? (self.label = lengthStr(time || player.position))
: (self.visible = !!player);
};
self.hook(player, update, "position");
self.poll(1000, update);
},
});
/** @param {import('types/service/mpris').MprisPlayer} player */
export const LengthLabel = (player) =>
Widget.Label({
label: player.bind("length").transform((l) => lengthStr(l)),
visible: player.bind("length").transform((l) => l > 0),
});
/** @param {import('types/service/mpris').MprisPlayer} player */
export const Slash = (player) =>
Widget.Label({
label: "/",
visible: player.bind("length").transform((l) => l > 0),
});
/**
* @param {Object} o
* @param {import('types/service/mpris').MprisPlayer} o.player
* @param {import('types/widgets/stack').StackProps['children']} o.children
* @param {'shuffle' | 'loop' | 'playPause' | 'previous' | 'next'} o.onClick
* @param {string} o.prop
* @param {string} o.canProp
* @param {any} o.cantValue
*/
const PlayerButton = ({
player,
children,
onClick,
prop,
canProp,
cantValue,
}) =>
Widget.Button({
child: Widget.Stack({ children }).bind(
"shown",
player,
prop,
(p) => `${p}`,
),
on_clicked: () => player[onClick](),
visible: player.bind(canProp).transform((c) => c !== cantValue),
});
/** @param {import('types/service/mpris').MprisPlayer} player */
export const ShuffleButton = (player) =>
PlayerButton({
player,
children: {
true: Widget.Label({
class_name: "shuffle enabled",
label: icons.mpris.shuffle.enabled,
}),
false: Widget.Label({
class_name: "shuffle disabled",
label: icons.mpris.shuffle.disabled,
}),
},
onClick: "shuffle",
prop: "shuffle-status",
canProp: "shuffle-status",
cantValue: null,
});
/** @param {import('types/service/mpris').MprisPlayer} player */
export const LoopButton = (player) =>
PlayerButton({
player,
children: {
None: Widget.Label({
class_name: "loop none",
label: icons.mpris.loop.none,
}),
Track: Widget.Label({
class_name: "loop track",
label: icons.mpris.loop.track,
}),
Playlist: Widget.Label({
class_name: "loop playlist",
label: icons.mpris.loop.playlist,
}),
},
onClick: "loop",
prop: "loop-status",
canProp: "loop-status",
cantValue: null,
});
/** @param {import('types/service/mpris').MprisPlayer} player */
export const PlayPauseButton = (player) =>
PlayerButton({
player,
children: {
Playing: Widget.Label({
class_name: "playing",
label: icons.mpris.playing,
}),
Paused: Widget.Label({
class_name: "paused",
label: icons.mpris.paused,
}),
Stopped: Widget.Label({
class_name: "stopped",
label: icons.mpris.stopped,
}),
},
onClick: "playPause",
prop: "play-back-status",
canProp: "can-play",
cantValue: false,
});
/** @param {import('types/service/mpris').MprisPlayer} player */
export const PreviousButton = (player) =>
PlayerButton({
player,
children: {
true: Widget.Label({
class_name: "previous",
label: icons.mpris.prev,
}),
},
onClick: "previous",
prop: "can-go-prev",
canProp: "can-go-prev",
cantValue: false,
});
/** @param {import('types/service/mpris').MprisPlayer} player */
export const NextButton = (player) =>
PlayerButton({
player,
children: {
true: Widget.Label({
class_name: "next",
label: icons.mpris.next,
}),
},
onClick: "next",
prop: "can-go-next",
canProp: "can-go-next",
cantValue: false,
});