chore: use util methods, improvements

This commit is contained in:
retrozinndev
2025-07-23 22:22:18 -03:00
parent 7b158b8c89
commit d4c1fcf327
5 changed files with 68 additions and 149 deletions
+3 -8
View File
@@ -23,14 +23,9 @@ export type AskPopupProps = {
export function AskPopup(props: AskPopupProps): Astal.Window { export function AskPopup(props: AskPopupProps): Astal.Window {
let accepted: boolean = false; let accepted: boolean = false;
return <CustomDialog return <CustomDialog namespace={"ask-popup"} widthRequest={400} heightRequest={250}
namespace={"ask-popup"} title={props.title ?? tr("ask_popup.title")} text={props.text}
widthRequest={400} onFinish={() => !accepted && props.onCancel?.()} options={[
heightRequest={250}
title={props.title ?? tr("ask_popup.title")}
text={props.text}
onFinish={() => !accepted && props.onCancel?.()}
options={[
{ text: props.cancelText ?? tr("cancel") }, { text: props.cancelText ?? tr("cancel") },
{ {
text: props.acceptText ?? tr("accept"), text: props.acceptText ?? tr("accept"),
+40 -43
View File
@@ -4,7 +4,7 @@ import { PopupWindow } from "./PopupWindow";
import { Separator } from "./Separator"; import { Separator } from "./Separator";
import { tr } from "../i18n/intl"; import { tr } from "../i18n/intl";
import { Accessor, For, With } from "ags"; import { Accessor, For, With } from "ags";
import { variableToBoolean, WidgetNodeType } from "../scripts/utils"; import { transformWidget, variableToBoolean, WidgetNodeType } from "../scripts/utils";
export type CustomDialogProps = { export type CustomDialogProps = {
@@ -28,61 +28,58 @@ export interface CustomDialogOption {
closeOnClick?: boolean | Accessor<boolean>; closeOnClick?: boolean | Accessor<boolean>;
} }
function CustomDialogOption(props: CustomDialogOption & { dialog: Astal.Window }) { function CustomDialogOption({closeOnClick = true, ...props}: CustomDialogOption & { dialog: Astal.Window }) {
function onClicked() { function onClicked() {
props.onClick?.(); props.onClick?.();
props.closeOnClick && props.dialog.close(); closeOnClick &&
props.dialog?.close();
} }
return <Gtk.Button class="option" hexpand={true} label={props.text} return <Gtk.Button class="option" hexpand label={props.text}
onClicked={onClicked} onActivate={onClicked}/> onClicked={onClicked} onActivate={onClicked}/>
} }
export function CustomDialog({ options = [{ text: tr("accept") }], ...props}: CustomDialogProps) { export function CustomDialog({ options = [{ text: tr("accept") }], ...props}: CustomDialogProps) {
let dialog: Astal.Window; return Windows.getDefault().createWindowForFocusedMonitor((mon) =>
return Windows.getDefault().createWindowForFocusedMonitor((mon: number) => <PopupWindow namespace={props.namespace ?? "custom-dialog"} monitor={mon}
<PopupWindow namespace={props.namespace ?? "custom-dialog"} monitor={mon}
cssBackgroundWindow={props.cssBackground ?? "background: rgba(0, 0, 0, .3);"} cssBackgroundWindow={props.cssBackground ?? "background: rgba(0, 0, 0, .3);"}
exclusivity={Astal.Exclusivity.IGNORE} layer={Astal.Layer.OVERLAY} exclusivity={Astal.Exclusivity.IGNORE} layer={Astal.Layer.OVERLAY}
halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER}
widthRequest={props.widthRequest ?? 400} heightRequest={props.heightRequest ?? 220} widthRequest={props.widthRequest ?? 400} heightRequest={props.heightRequest ?? 220}
onDestroy={props.onFinish} $={(self) => dialog = self}> onDestroy={props.onFinish} $={(self) => self.set_child(
<Gtk.Box class={props.className ?? "custom-dialog-container"} <Gtk.Box class={props.className ?? "custom-dialog-container"}
orientation={Gtk.Orientation.VERTICAL}> orientation={Gtk.Orientation.VERTICAL}>
<Gtk.Label class={"title"} visible={variableToBoolean(props.title)} label={props.title} />
<Gtk.Label class={"text"} visible={variableToBoolean(props.text)} label={props.text} />
<Gtk.Box class={"custom-children custom-child"} visible={variableToBoolean(props.children)}
orientation={props.childOrientation ?? Gtk.Orientation.VERTICAL}>
{ <Gtk.Label class={"title"} visible={variableToBoolean(props.title)} label={props.title} />
(props.children instanceof Accessor) ? <Gtk.Label class={"text"} visible={variableToBoolean(props.text)} label={props.text}
(Array.isArray(props.children) ? vexpand/>
<For each={props.children! as Accessor<Array<JSX.Element>>}> <Gtk.Box class={"custom-children custom-child"} visible={variableToBoolean(props.children)}
{(widget) => widget && widget} orientation={props.childOrientation ?? Gtk.Orientation.VERTICAL}>
</For> {
: <With value={props.children as Accessor<JSX.Element>}> (props.children instanceof Accessor) ?
{(widget) => widget && widget} (Array.isArray(props.children) ?
</With>) <For each={props.children! as Accessor<Array<JSX.Element>>}>
: (Array.isArray(props.children) ? {(widget) => widget && widget}
props.children.map(widget => widget && widget).filter(w => w) </For>
: props.children) : <With value={props.children as Accessor<JSX.Element>}>
} {(widget) => widget && widget}
</Gtk.Box> </With>)
<Separator alpha={.2} visible={options && options.length > 0} : (Array.isArray(props.children) ?
spacing={8} orientation={Gtk.Orientation.VERTICAL} /> props.children.map(widget => widget && widget).filter(w => w)
: props.children)
}
</Gtk.Box>
<Separator alpha={.2} visible={options && options.length > 0}
spacing={8} orientation={Gtk.Orientation.VERTICAL} />
{(<Gtk.Box class={"options"} orientation={props.optionsOrientation ?? Gtk.Orientation.HORIZONTAL} <Gtk.Box class={"options"} orientation={props.optionsOrientation ?? Gtk.Orientation.HORIZONTAL}
hexpand={true} heightRequest={38} homogeneous={true}> hexpand={true} heightRequest={38} homogeneous={true}>
{
(options instanceof Accessor) ? {transformWidget(options, (props) => <CustomDialogOption {...props} dialog={self} />)}
<For each={options}> </Gtk.Box>
{(option) => <CustomDialogOption {...option} dialog={dialog} />} </Gtk.Box> as Gtk.Box
</For> )}
: options.map(option => /> as Astal.Window
<CustomDialogOption {...option} dialog={dialog} />) )();
}
</Gtk.Box>)}
</Gtk.Box>
</PopupWindow>)();
} }
+23 -29
View File
@@ -2,11 +2,14 @@ import { Gdk, Gtk } from "ags/gtk4";
import { Separator } from "./Separator"; import { Separator } from "./Separator";
import { HistoryNotification, Notifications } from "../scripts/notifications"; import { HistoryNotification, Notifications } from "../scripts/notifications";
import { getAppIcon, getSymbolicIcon } from "../scripts/apps"; import { getAppIcon, getSymbolicIcon } from "../scripts/apps";
import { onCleanup } from "ags";
import AstalNotifd from "gi://AstalNotifd"; import AstalNotifd from "gi://AstalNotifd";
import Pango from "gi://Pango?version=1.0"; import Pango from "gi://Pango?version=1.0";
import GLib from "gi://GLib?version=2.0"; import GLib from "gi://GLib?version=2.0";
import GObject from "gi://GObject?version=2.0"; import GObject from "gi://GObject?version=2.0";
import { escapeUnintendedMarkup, pathToURI } from "../scripts/utils";
function getNotificationImage(notif: AstalNotifd.Notification|HistoryNotification): (string|undefined) { function getNotificationImage(notif: AstalNotifd.Notification|HistoryNotification): (string|undefined) {
const img = notif.image || notif.appIcon; const img = notif.image || notif.appIcon;
@@ -14,16 +17,7 @@ function getNotificationImage(notif: AstalNotifd.Notification|HistoryNotificatio
if(!img || !img.includes('/')) if(!img || !img.includes('/'))
return undefined; return undefined;
switch(true) { return pathToURI(img);
case /^[/]/.test(img):
return `file://${img}`;
case /^[~]/.test(img):
case /^file:\/\/[~]/i.test(img):
return `file://${GLib.get_home_dir()}/${img.replace(/^(file\:\/\/|[~]|file\:\/\[~])/i, "")}`;
}
return img;
} }
export function NotificationWidget({ notification, actionClicked, holdOnHover, showTime, actionClose }: { export function NotificationWidget({ notification, actionClicked, holdOnHover, showTime, actionClose }: {
@@ -40,6 +34,9 @@ export function NotificationWidget({ notification, actionClicked, holdOnHover, s
const conns: Map<GObject.Object, Array<number>> = new Map(); const conns: Map<GObject.Object, Array<number>> = new Map();
onCleanup(() =>
conns.forEach((ids, obj) => ids.forEach(id => obj.disconnect(id))));
return <Gtk.Box hexpand class={`notification ${ return <Gtk.Box hexpand class={`notification ${
Notifications.getDefault().getUrgencyString(notification.urgency) Notifications.getDefault().getUrgencyString(notification.urgency)
}`} orientation={Gtk.Orientation.VERTICAL} spacing={5} }`} orientation={Gtk.Orientation.VERTICAL} spacing={5}
@@ -54,7 +51,7 @@ export function NotificationWidget({ notification, actionClicked, holdOnHover, s
eventControllerMotion.connect("enter", () => eventControllerMotion.connect("enter", () =>
holdOnHover && Notifications.getDefault().holdNotification(notification.id)), holdOnHover && Notifications.getDefault().holdNotification(notification.id)),
eventControllerMotion.connect("leave", () => eventControllerMotion.connect("leave", () =>
holdOnHover && Notifications.getDefault().removeNotification(notification.id)) holdOnHover && notification && Notifications.getDefault().removeNotification(notification.id))
]); ]);
conns.set(gestureClick, [ conns.set(gestureClick, [
@@ -63,12 +60,10 @@ export function NotificationWidget({ notification, actionClicked, holdOnHover, s
actionClicked?.(notification); actionClicked?.(notification);
}) })
]); ]);
}} onDestroy={(_) => {
conns.forEach((ids, obj) => ids.forEach(id => obj.disconnect(id)));
}}> }}>
<Gtk.Box class={"top"} hexpand> <Gtk.Box class={"top"} hexpand>
<Gtk.Image css={"font-size: 16px;"} $={(self) => { <Gtk.Image class="app-icon" $={(self) => {
const icon = getSymbolicIcon(notification.appIcon ?? notification.appName) ?? const icon = getSymbolicIcon(notification.appIcon ?? notification.appName) ??
getSymbolicIcon(notification.appName) ?? getAppIcon(notification.appName); getSymbolicIcon(notification.appName) ?? getAppIcon(notification.appName);
@@ -86,32 +81,31 @@ export function NotificationWidget({ notification, actionClicked, holdOnHover, s
label={GLib.DateTime.new_from_unix_local(notification.time).format("%H:%M") ?? ""} /> label={GLib.DateTime.new_from_unix_local(notification.time).format("%H:%M") ?? ""} />
<Gtk.Button halign={Gtk.Align.END} iconName={"window-close-symbolic"} <Gtk.Button halign={Gtk.Align.END} iconName={"window-close-symbolic"}
class={"close icon"}/> class={"close"} onClicked={() => actionClose?.(notification)}/>
</Gtk.Box> </Gtk.Box>
<Separator alpha={.1} orientation={Gtk.Orientation.VERTICAL} /> <Separator alpha={.1} orientation={Gtk.Orientation.VERTICAL} />
<Gtk.Box class={"content"} $={(self) => { <Gtk.Box class={"content"}>
const image = getNotificationImage(notification); {getNotificationImage(notification) &&
<Gtk.Box class={"image"} hexpand={false} vexpand={false}
image && css={`background-image: url("${getNotificationImage(notification)}");`}
self.prepend(Gtk.Picture.new_for_filename(image)); />
}}> }
<Gtk.Box class={"text"} orientation={Gtk.Orientation.VERTICAL} <Gtk.Box class={"text"} orientation={Gtk.Orientation.VERTICAL}
vexpand={true}> vexpand={true}>
<Gtk.Label class={"summary"} useMarkup={true} hexpand={false} xalign={0} <Gtk.Label class={"summary"} useMarkup={true} hexpand xalign={0}
vexpand ellipsize={Pango.EllipsizeMode.END} label={ vexpand={false} ellipsize={Pango.EllipsizeMode.END} label={
notification.summary.replace(/[&]/g, "&amp;")} escapeUnintendedMarkup(notification.summary)}
/> />
<Gtk.Label class={"body"} useMarkup={true} xalign={0} wrap={true} hexpand={false} <Gtk.Label class={"body"} useMarkup={true} xalign={0} wrap={true} hexpand
vexpand wrapMode={Pango.WrapMode.WORD_CHAR} label={ vexpand wrapMode={Pango.WrapMode.WORD_CHAR} valign={Gtk.Align.START} label={
notification.body.replace(/[&]/g, "&amp;")} escapeUnintendedMarkup(notification.body)}
/> />
</Gtk.Box> </Gtk.Box>
</Gtk.Box> </Gtk.Box>
<Gtk.Box class={"action button-row"} hexpand={true} visible={ <Gtk.Box class={"actions button-row"} hexpand={true} visible={
(notification instanceof AstalNotifd.Notification) && (notification instanceof AstalNotifd.Notification) &&
(notification.actions.filter(action => action.label.toLowerCase() !== "view").length > 0) (notification.actions.filter(action => action.label.toLowerCase() !== "view").length > 0)
}> }>
+1 -1
View File
@@ -6,7 +6,7 @@ import { Config } from "../../scripts/config";
export const Clock = () => export const Clock = () =>
<Gtk.Button class={createBinding(Windows.getDefault(), "openWindows").as((wins) => <Gtk.Button class={createBinding(Windows.getDefault(), "openWindows").as((wins) =>
`clock ${Object.hasOwn(wins, "center-window") ? "open" : ""}`)} `clock ${wins.includes("center-window") ? "open" : ""}`)}
$={(self) => { $={(self) => {
const conns: Array<number> = [ const conns: Array<number> = [
self.connect("clicked", (_) => Windows.getDefault().toggle("center-window")), self.connect("clicked", (_) => Windows.getDefault().toggle("center-window")),
-67
View File
@@ -1,67 +0,0 @@
import { Accessor, With } from "ags";
import { register } from "ags/gobject";
import { Gtk } from "ags/gtk4";
import Pango from "gi://Pango?version=1.0";
import { variableToBoolean } from "../../scripts/utils";
export { ResultWidget, ResultWidgetProps };
type ResultWidgetProps = {
icon?: string | Accessor<string> | JSX.Element | Accessor<JSX.Element>;
title: string | Accessor<string>;
description?: string | Accessor<string>;
closeOnClick?: boolean;
setup?: () => void;
onClick?: () => void;
};
@register({ GTypeName: "ResultWidget" })
class ResultWidget extends Gtk.Box {
public readonly onClick: () => void;
public readonly setup?: () => void;
public icon?: (string | Accessor<string> | JSX.Element | Accessor<JSX.Element>);
public closeOnClick: boolean = true;
constructor(props: ResultWidgetProps) {
super({
cssClasses: ["result"],
hexpand: true
});
this.icon = props.icon;
this.setup = props.setup;
this.closeOnClick = props.closeOnClick ?? true;
this.onClick = () => props.onClick?.();
if(this.icon !== undefined) {
if(this.icon instanceof Accessor) {
if(typeof this.icon.get() === "string") {
this.prepend(<Gtk.Image iconName={
this.icon as Accessor<string>
} /> as Gtk.Image);
} else {
this.prepend(<Gtk.Box>
<With value={this.icon as Accessor<Gtk.Widget>}>
{(widget) => widget}
</With>
</Gtk.Box> as Gtk.Box);
}
} else {
if(typeof this.icon === "string")
this.prepend(<Gtk.Image iconName={this.icon as string} /> as Gtk.Image);
else
this.prepend(this.icon as Gtk.Widget);
}
}
this.append(<Gtk.Box orientation={Gtk.Orientation.VERTICAL} valign={Gtk.Align.CENTER}>
<Gtk.Label class={"title"} xalign={0} ellipsize={Pango.EllipsizeMode.END}
label={props.title} />
<Gtk.Label class={"description"} visible={variableToBoolean(props.description)}
ellipsize={Pango.EllipsizeMode.END} xalign={0} label={props.description ?? ""} />
</Gtk.Box> as Gtk.Box);
}
}