✨ feat(ags/notifications): add support to actions in floating notifications(popups)
This commit is contained in:
@@ -125,11 +125,13 @@ class Notifications extends GObject.Object {
|
||||
}
|
||||
|
||||
public removeNotification(notif: (AstalNotifd.Notification|number)): void {
|
||||
const notifId = (notif instanceof AstalNotifd.Notification) ? notif.id : notif;
|
||||
const notification = (notif instanceof AstalNotifd.Notification) ? notif : AstalNotifd.get_default().get_notification(notif);
|
||||
this.#notifications = this.#notifications.filter((item: AstalNotifd.Notification) =>
|
||||
item.id !== notifId);
|
||||
item.id !== notification.id);
|
||||
|
||||
notification.dismiss();
|
||||
this.notify("notifications");
|
||||
this.emit("notification-removed", notifId);
|
||||
this.emit("notification-removed", notification.id);
|
||||
}
|
||||
|
||||
connect(signal: string, callback: (...args: any[]) => void): number {
|
||||
|
||||
+41
-38
@@ -1,11 +1,10 @@
|
||||
import { execAsync, GLib, GObject, register, signal, writeFile } from "astal";
|
||||
import { Subscribable } from "astal/binding";
|
||||
import { execAsync, GLib, GObject, property, register, signal } from "astal";
|
||||
import { Connectable } from "astal/binding";
|
||||
import { Gdk } from "astal/gtk3";
|
||||
import { getDateTime } from "./time";
|
||||
import AstalWp from "gi://AstalWp";
|
||||
|
||||
@register({ GTypeName: "ScreenRecording" })
|
||||
class Recording extends GObject.Object implements Subscribable {
|
||||
class Recording extends GObject.Object implements Connectable {
|
||||
|
||||
private static instance: Recording;
|
||||
|
||||
@@ -17,27 +16,44 @@ class Recording extends GObject.Object implements Subscribable {
|
||||
declare outputChanged: (newPath: string) => void;
|
||||
|
||||
#recording: boolean = false;
|
||||
#subs = new Set<(isRec: boolean) => void>();
|
||||
#path: string = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_VIDEOS) || `${GLib.get_home_dir()}/Recordings`;
|
||||
/** Default extension: mp4(h264) */
|
||||
#extension: string = "mp4";
|
||||
#recordAudio: boolean|AstalWp.Endpoint = false; // TODO
|
||||
|
||||
private notifySub() {
|
||||
const subs = this.#subs;
|
||||
for(const sub of subs) {
|
||||
sub(this.recording);
|
||||
}
|
||||
}
|
||||
#recordAudio: boolean = false;
|
||||
#monitor: (number|null) = null;
|
||||
#area: (Gdk.Rectangle|null) = null;
|
||||
|
||||
@property(Boolean)
|
||||
public get recording() { return this.#recording; }
|
||||
private set recording(newValue: boolean) { this.#recording = newValue; }
|
||||
private set recording(newValue: boolean) {
|
||||
(!newValue && this.recording) ?
|
||||
this.stopRecording()
|
||||
: this.startRecording(this.#monitor || 0, this.#area || undefined);
|
||||
|
||||
this.#recording = newValue;
|
||||
this.notify("recording");
|
||||
}
|
||||
|
||||
@property(String)
|
||||
public get path() { return this.#path; }
|
||||
public set path(newPath: string) { this.#path = newPath; }
|
||||
public set path(newPath: string) {
|
||||
this.#path = newPath;
|
||||
this.notify("path");
|
||||
}
|
||||
|
||||
@property(String)
|
||||
public get extension() { return this.#extension; }
|
||||
public set extension(newExt: string) { this.#extension = newExt; }
|
||||
public set extension(newExt: string) {
|
||||
this.#extension = newExt;
|
||||
this.notify("extension");
|
||||
}
|
||||
|
||||
@property(Boolean)
|
||||
public get recordAudio() { return this.#recordAudio; }
|
||||
public set recordAudio(newValue: boolean) {
|
||||
this.#recordAudio = newValue;
|
||||
this.notify("record-audio");
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -50,39 +66,26 @@ class Recording extends GObject.Object implements Subscribable {
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
public get() {
|
||||
return this.recording;
|
||||
}
|
||||
|
||||
private emit(id: string, ...args: any[]) {
|
||||
super.emit(id, ...args);
|
||||
this.notifySub();
|
||||
}
|
||||
|
||||
|
||||
public startRecording(area?: Gdk.Rectangle) {
|
||||
public startRecording(monitor?: number, area?: Gdk.Rectangle) {
|
||||
const output = `${getDateTime().get().format("%Y-%m-%d-%H%M%S")}_rec.${this.extension}`;
|
||||
execAsync([ "wf-recorder",
|
||||
this.#recording = true;
|
||||
this.emit("started");
|
||||
execAsync([
|
||||
"wf-recorder",
|
||||
`${Boolean(area) ?
|
||||
`-g ${area?.x || 0},${area?.y || 0} ${area?.width || 1}x${area?.height || 1}`
|
||||
: ""}`,
|
||||
"-f", output ]
|
||||
).then(() => {
|
||||
`-f ${output}`
|
||||
]).then(() => {
|
||||
this.emit("stopped", `${this.path}/${output}`);
|
||||
this.#recording = false;
|
||||
this.notify("recording");
|
||||
});
|
||||
writeFile("", "");
|
||||
this.emit("started");
|
||||
this.notifySub();
|
||||
}
|
||||
|
||||
public stopRecording() {
|
||||
|
||||
}
|
||||
|
||||
public subscribe(callback: (isRec: boolean) => void) {
|
||||
this.#subs.add(callback);
|
||||
return () => this.#subs.delete(callback);
|
||||
}
|
||||
}
|
||||
|
||||
export { Recording };
|
||||
|
||||
+29
-2
@@ -57,7 +57,7 @@ window.ask-popup {
|
||||
}
|
||||
|
||||
.notification {
|
||||
background: colors.$bg-primary;
|
||||
background: colors.$bg-translucent-secondary;
|
||||
border-radius: 16px;
|
||||
|
||||
& > .top {
|
||||
@@ -87,8 +87,9 @@ window.ask-popup {
|
||||
}
|
||||
|
||||
& .content {
|
||||
padding: 4px;
|
||||
padding: 6px;
|
||||
padding-top: 0;
|
||||
|
||||
& .image {
|
||||
$size: 78px;
|
||||
min-width: $size;
|
||||
@@ -110,6 +111,32 @@ window.ask-popup {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
& .actions {
|
||||
padding: 6px;
|
||||
|
||||
& button.action {
|
||||
@include mixins.hover-shadow;
|
||||
|
||||
border-radius: 4px;
|
||||
background: colors.$bg-secondary;
|
||||
padding: 6px;
|
||||
|
||||
& label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 12px;
|
||||
border-bottom-left-radius: 12px;
|
||||
}
|
||||
&:last-child {
|
||||
border-top-right-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tooltip {
|
||||
|
||||
@@ -7,6 +7,7 @@ $bg-secondary: funs.toRGB(color.adjust($color: wal.$color1, $lightness: -16%));
|
||||
$bg-tertiary: funs.toRGB(color.adjust($color: $bg-secondary, $lightness: 10%));
|
||||
$bg-light: wal.$foreground;
|
||||
$bg-translucent: funs.toRGB(color.change($color: $bg-primary, $alpha: 75%));
|
||||
$bg-translucent-secondary: funs.toRGB(color.change($color: $bg-translucent, $alpha: 78%));
|
||||
$fg-primary: wal.$foreground;
|
||||
$fg-light: $bg-primary;
|
||||
$fg-disabled: funs.toRGB(color.adjust($color: wal.$foreground, $lightness: -11%));
|
||||
|
||||
@@ -145,6 +145,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
& .sub-header {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.bluetooth {
|
||||
.connections button {
|
||||
@include mixins.hover-shadow;
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
|
||||
& > .notification {
|
||||
margin: 6px;
|
||||
box-shadow: 0 0 4px .5px colors.$bg-translucent;
|
||||
box-shadow: 0 0 4px .5px colors.$bg-primary;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,11 +35,13 @@
|
||||
&:first-child {
|
||||
border-top-left-radius: 10px;
|
||||
border-bottom-left-radius: 10px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-top-right-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +101,22 @@ export function NotificationWidget(notification: AstalNotifd.Notification|number
|
||||
]
|
||||
} as Widget.BoxProps)
|
||||
]
|
||||
} as Widget.BoxProps),
|
||||
new Widget.Box({
|
||||
className: "actions button-row",
|
||||
hexpand: true,
|
||||
visible: notification.actions.length > 0,
|
||||
children: notification.actions.map((action: AstalNotifd.Action) =>
|
||||
new Widget.Button({
|
||||
className: "action",
|
||||
label: action.label,
|
||||
hexpand: true,
|
||||
onClicked: () => {
|
||||
notification.invoke(action.id);
|
||||
onClose && onClose(notification);
|
||||
}
|
||||
} as Widget.ButtonProps)
|
||||
)
|
||||
} as Widget.BoxProps)
|
||||
]
|
||||
} as Widget.BoxProps)
|
||||
|
||||
@@ -74,8 +74,8 @@ export function PopupWindow(props: PopupWindowProps): Widget.Window {
|
||||
widthRequest: props?.widthRequest,
|
||||
heightRequest: props?.heightRequest,
|
||||
hexpand: props?.hexpand || false,
|
||||
visible: true,
|
||||
vexpand: props?.vexpand || false,
|
||||
visible: true,
|
||||
css: `.popup {
|
||||
margin-top: ${props.marginTop || 0}px;
|
||||
margin-bottom: ${props.marginBottom || 0}px;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { bind } from "astal";
|
||||
import { Gtk, Widget } from "astal/gtk3";
|
||||
import AstalNotifd from "gi://AstalNotifd";
|
||||
import { Notifications } from "../../scripts/notifications";
|
||||
import { NotificationWidget } from "../Notification";
|
||||
|
||||
export const NotifHistory: Gtk.Widget = new Widget.Scrollable({
|
||||
hscroll: Gtk.PolicyType.NEVER,
|
||||
@@ -10,65 +11,8 @@ export const NotifHistory: Gtk.Widget = new Widget.Scrollable({
|
||||
child: new Widget.Box({
|
||||
className: "notifications",
|
||||
children: bind(Notifications.getDefault(), "history").as((history: Array<AstalNotifd.Notification>) =>
|
||||
history.map((notification: AstalNotifd.Notification) =>
|
||||
new Widget.Box({
|
||||
className: "notification",
|
||||
hexpand: true,
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
children: [
|
||||
new Widget.Box({
|
||||
className: "top",
|
||||
expand: true,
|
||||
children: [
|
||||
new Widget.Box({
|
||||
className: "app",
|
||||
children: [
|
||||
new Widget.Icon({
|
||||
icon: notification.appIcon || notification.appName.toLowerCase(),
|
||||
iconSize: Gtk.IconSize.LARGE_TOOLBAR
|
||||
}),
|
||||
new Widget.Label({
|
||||
className: "name",
|
||||
label: notification.appName || "Unknown"
|
||||
} as Widget.LabelProps)
|
||||
]
|
||||
} as Widget.BoxProps),
|
||||
new Widget.Button({
|
||||
className: "remove",
|
||||
label: "",
|
||||
onClick: () => Notifications.getDefault().removeHistory(notification.id)
|
||||
} as Widget.ButtonProps)
|
||||
]
|
||||
} as Widget.BoxProps),
|
||||
new Widget.Box({
|
||||
className: "content",
|
||||
expand: true,
|
||||
children: [
|
||||
new Widget.Box({
|
||||
className: "image",
|
||||
visible: notification.image !== "",
|
||||
css: `.image { background-image: url('${notification.image}') }`
|
||||
} as Widget.BoxProps),
|
||||
new Widget.Box({
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
children: [
|
||||
new Widget.Label({
|
||||
className: "summary",
|
||||
useMarkup: true,
|
||||
label: notification.summary
|
||||
} as Widget.LabelProps),
|
||||
new Widget.Label({
|
||||
className: "body",
|
||||
useMarkup: true,
|
||||
label: notification.body
|
||||
} as Widget.LabelProps)
|
||||
]
|
||||
} as Widget.BoxProps)
|
||||
]
|
||||
} as Widget.BoxProps)
|
||||
]
|
||||
} as Widget.BoxProps)
|
||||
)
|
||||
)
|
||||
history.map((notification: AstalNotifd.Notification) => NotificationWidget(notification,
|
||||
() => Notifications.getDefault().removeHistory(notification.id))
|
||||
))
|
||||
} as Widget.BoxProps)
|
||||
} as Widget.ScrollableProps)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Widget } from "astal/gtk3";
|
||||
import { Gtk, Widget } from "astal/gtk3";
|
||||
import { Page } from "./Page";
|
||||
import AstalNetwork from "gi://AstalNetwork";
|
||||
import { bind } from "astal";
|
||||
@@ -18,6 +18,59 @@ export const PageNetwork = new Page({
|
||||
} as Widget.ButtonProps)
|
||||
],
|
||||
pageChild: () => new Widget.Box({
|
||||
expand: true,
|
||||
children: [
|
||||
new Widget.Box({
|
||||
className: "devices",
|
||||
hexpand: true,
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
visible: bind(AstalNetwork.get_default().get_client(), "devices").as((devs) => devs.length > 0),
|
||||
children: bind(AstalNetwork.get_default().get_client(), "devices").as((devices) => [
|
||||
new Widget.Label({
|
||||
label: "Devices",
|
||||
xalign: 0,
|
||||
className: "sub-header",
|
||||
} as Widget.LabelProps),
|
||||
...devices.map(dev => new Widget.Button({
|
||||
className: "device",
|
||||
child: new Widget.Label({
|
||||
className: "interface name",
|
||||
xalign: 0,
|
||||
label: dev.interface
|
||||
} as Widget.LabelProps),
|
||||
} as Widget.ButtonProps))
|
||||
])
|
||||
} as Widget.BoxProps),
|
||||
new Widget.Box({
|
||||
className: "wireless-aps",
|
||||
visible: bind(AstalNetwork.get_default(), "primary").as((primary) => primary === AstalNetwork.Primary.WIFI),
|
||||
hexpand: true,
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
children: AstalNetwork.get_default().wifi ? bind(AstalNetwork.get_default().wifi.get_device(), "accessPoints").as((aps) =>
|
||||
aps.map(ap => new Widget.Button({
|
||||
hexpand: true,
|
||||
onClick: () => console.log("connect to " + ap.get_ssid().toArray().toString()), // TODO I don't have a WiFi board :(
|
||||
child: new Widget.Box({
|
||||
hexpand: true,
|
||||
children: [
|
||||
new Widget.Icon({
|
||||
halign: Gtk.Align.START,
|
||||
className: "icon",
|
||||
icon: "network-wireless-signal-excellent-symbolic"
|
||||
} as Widget.IconProps),
|
||||
new Widget.Label({
|
||||
className: "ssid",
|
||||
halign: Gtk.Align.START,
|
||||
label: ap.ssid.toArray().toString()
|
||||
} as Widget.LabelProps),
|
||||
new Widget.Label({
|
||||
className: "status",
|
||||
} as Widget.LabelProps)
|
||||
]
|
||||
} as Widget.BoxProps)
|
||||
} as Widget.ButtonProps))) : [],
|
||||
} as Widget.BoxProps),
|
||||
]
|
||||
} as Widget.BoxProps)
|
||||
});
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ export namespace Runner {
|
||||
width?: number;
|
||||
height?: number;
|
||||
entryPlaceHolder?: string;
|
||||
resultsPlaceholder?: () => Array<Gtk.Widget>;
|
||||
resultsPlaceholder?: () => Array<ResultWidget>;
|
||||
};
|
||||
|
||||
export const prefixes = new Map<string, (entry: string) => (ResultWidget|Array<ResultWidget>|null)>([
|
||||
@@ -58,7 +58,7 @@ export namespace Runner {
|
||||
export function RunnerWindow(props?: RunnerProps): (Widget.Window|null) {
|
||||
let subs: Array<() => void> = [];
|
||||
const entryText: Variable<string> = new Variable<string>("");
|
||||
let results: (Array<ResultWidget>|null) = null;
|
||||
let results: (Array<ResultWidget>|null) = props?.resultsPlaceholder ? props.resultsPlaceholder() : null;
|
||||
let selectedResultIndex = 0;
|
||||
|
||||
const searchEntry = new Widget.Entry({
|
||||
|
||||
Reference in New Issue
Block a user