✨ 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 {
|
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) =>
|
this.#notifications = this.#notifications.filter((item: AstalNotifd.Notification) =>
|
||||||
item.id !== notifId);
|
item.id !== notification.id);
|
||||||
|
|
||||||
|
notification.dismiss();
|
||||||
this.notify("notifications");
|
this.notify("notifications");
|
||||||
this.emit("notification-removed", notifId);
|
this.emit("notification-removed", notification.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
connect(signal: string, callback: (...args: any[]) => void): number {
|
connect(signal: string, callback: (...args: any[]) => void): number {
|
||||||
|
|||||||
+41
-38
@@ -1,11 +1,10 @@
|
|||||||
import { execAsync, GLib, GObject, register, signal, writeFile } from "astal";
|
import { execAsync, GLib, GObject, property, register, signal } from "astal";
|
||||||
import { Subscribable } from "astal/binding";
|
import { Connectable } from "astal/binding";
|
||||||
import { Gdk } from "astal/gtk3";
|
import { Gdk } from "astal/gtk3";
|
||||||
import { getDateTime } from "./time";
|
import { getDateTime } from "./time";
|
||||||
import AstalWp from "gi://AstalWp";
|
|
||||||
|
|
||||||
@register({ GTypeName: "ScreenRecording" })
|
@register({ GTypeName: "ScreenRecording" })
|
||||||
class Recording extends GObject.Object implements Subscribable {
|
class Recording extends GObject.Object implements Connectable {
|
||||||
|
|
||||||
private static instance: Recording;
|
private static instance: Recording;
|
||||||
|
|
||||||
@@ -17,27 +16,44 @@ class Recording extends GObject.Object implements Subscribable {
|
|||||||
declare outputChanged: (newPath: string) => void;
|
declare outputChanged: (newPath: string) => void;
|
||||||
|
|
||||||
#recording: boolean = false;
|
#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`;
|
#path: string = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_VIDEOS) || `${GLib.get_home_dir()}/Recordings`;
|
||||||
/** Default extension: mp4(h264) */
|
/** Default extension: mp4(h264) */
|
||||||
#extension: string = "mp4";
|
#extension: string = "mp4";
|
||||||
#recordAudio: boolean|AstalWp.Endpoint = false; // TODO
|
#recordAudio: boolean = false;
|
||||||
|
#monitor: (number|null) = null;
|
||||||
|
#area: (Gdk.Rectangle|null) = null;
|
||||||
|
|
||||||
private notifySub() {
|
@property(Boolean)
|
||||||
const subs = this.#subs;
|
public get recording() { return this.#recording; }
|
||||||
for(const sub of subs) {
|
private set recording(newValue: boolean) {
|
||||||
sub(this.recording);
|
(!newValue && this.recording) ?
|
||||||
}
|
this.stopRecording()
|
||||||
|
: this.startRecording(this.#monitor || 0, this.#area || undefined);
|
||||||
|
|
||||||
|
this.#recording = newValue;
|
||||||
|
this.notify("recording");
|
||||||
}
|
}
|
||||||
|
|
||||||
public get recording() { return this.#recording; }
|
@property(String)
|
||||||
private set recording(newValue: boolean) { this.#recording = newValue; }
|
|
||||||
|
|
||||||
public get path() { return this.#path; }
|
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 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() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -50,39 +66,26 @@ class Recording extends GObject.Object implements Subscribable {
|
|||||||
return this.instance;
|
return this.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get() {
|
public startRecording(monitor?: number, area?: Gdk.Rectangle) {
|
||||||
return this.recording;
|
|
||||||
}
|
|
||||||
|
|
||||||
private emit(id: string, ...args: any[]) {
|
|
||||||
super.emit(id, ...args);
|
|
||||||
this.notifySub();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public startRecording(area?: Gdk.Rectangle) {
|
|
||||||
const output = `${getDateTime().get().format("%Y-%m-%d-%H%M%S")}_rec.${this.extension}`;
|
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) ?
|
`${Boolean(area) ?
|
||||||
`-g ${area?.x || 0},${area?.y || 0} ${area?.width || 1}x${area?.height || 1}`
|
`-g ${area?.x || 0},${area?.y || 0} ${area?.width || 1}x${area?.height || 1}`
|
||||||
: ""}`,
|
: ""}`,
|
||||||
"-f", output ]
|
`-f ${output}`
|
||||||
).then(() => {
|
]).then(() => {
|
||||||
this.emit("stopped", `${this.path}/${output}`);
|
this.emit("stopped", `${this.path}/${output}`);
|
||||||
|
this.#recording = false;
|
||||||
|
this.notify("recording");
|
||||||
});
|
});
|
||||||
writeFile("", "");
|
|
||||||
this.emit("started");
|
|
||||||
this.notifySub();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public stopRecording() {
|
public stopRecording() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public subscribe(callback: (isRec: boolean) => void) {
|
|
||||||
this.#subs.add(callback);
|
|
||||||
return () => this.#subs.delete(callback);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Recording };
|
export { Recording };
|
||||||
|
|||||||
+29
-2
@@ -57,7 +57,7 @@ window.ask-popup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.notification {
|
.notification {
|
||||||
background: colors.$bg-primary;
|
background: colors.$bg-translucent-secondary;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
|
|
||||||
& > .top {
|
& > .top {
|
||||||
@@ -87,8 +87,9 @@ window.ask-popup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
& .content {
|
& .content {
|
||||||
padding: 4px;
|
padding: 6px;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
|
|
||||||
& .image {
|
& .image {
|
||||||
$size: 78px;
|
$size: 78px;
|
||||||
min-width: $size;
|
min-width: $size;
|
||||||
@@ -110,6 +111,32 @@ window.ask-popup {
|
|||||||
font-weight: 400;
|
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 {
|
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-tertiary: funs.toRGB(color.adjust($color: $bg-secondary, $lightness: 10%));
|
||||||
$bg-light: wal.$foreground;
|
$bg-light: wal.$foreground;
|
||||||
$bg-translucent: funs.toRGB(color.change($color: $bg-primary, $alpha: 75%));
|
$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-primary: wal.$foreground;
|
||||||
$fg-light: $bg-primary;
|
$fg-light: $bg-primary;
|
||||||
$fg-disabled: funs.toRGB(color.adjust($color: wal.$foreground, $lightness: -11%));
|
$fg-disabled: funs.toRGB(color.adjust($color: wal.$foreground, $lightness: -11%));
|
||||||
|
|||||||
@@ -145,6 +145,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& .sub-header {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
&.bluetooth {
|
&.bluetooth {
|
||||||
.connections button {
|
.connections button {
|
||||||
@include mixins.hover-shadow;
|
@include mixins.hover-shadow;
|
||||||
|
|||||||
@@ -9,6 +9,6 @@
|
|||||||
|
|
||||||
& > .notification {
|
& > .notification {
|
||||||
margin: 6px;
|
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 {
|
&:first-child {
|
||||||
border-top-left-radius: 10px;
|
border-top-left-radius: 10px;
|
||||||
border-bottom-left-radius: 10px;
|
border-bottom-left-radius: 10px;
|
||||||
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-top-right-radius: 10px;
|
border-top-right-radius: 10px;
|
||||||
border-bottom-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)
|
||||||
]
|
]
|
||||||
|
} 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)
|
||||||
]
|
]
|
||||||
} as Widget.BoxProps)
|
} as Widget.BoxProps)
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ export function PopupWindow(props: PopupWindowProps): Widget.Window {
|
|||||||
widthRequest: props?.widthRequest,
|
widthRequest: props?.widthRequest,
|
||||||
heightRequest: props?.heightRequest,
|
heightRequest: props?.heightRequest,
|
||||||
hexpand: props?.hexpand || false,
|
hexpand: props?.hexpand || false,
|
||||||
visible: true,
|
|
||||||
vexpand: props?.vexpand || false,
|
vexpand: props?.vexpand || false,
|
||||||
|
visible: true,
|
||||||
css: `.popup {
|
css: `.popup {
|
||||||
margin-top: ${props.marginTop || 0}px;
|
margin-top: ${props.marginTop || 0}px;
|
||||||
margin-bottom: ${props.marginBottom || 0}px;
|
margin-bottom: ${props.marginBottom || 0}px;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { bind } from "astal";
|
|||||||
import { Gtk, Widget } from "astal/gtk3";
|
import { Gtk, Widget } from "astal/gtk3";
|
||||||
import AstalNotifd from "gi://AstalNotifd";
|
import AstalNotifd from "gi://AstalNotifd";
|
||||||
import { Notifications } from "../../scripts/notifications";
|
import { Notifications } from "../../scripts/notifications";
|
||||||
|
import { NotificationWidget } from "../Notification";
|
||||||
|
|
||||||
export const NotifHistory: Gtk.Widget = new Widget.Scrollable({
|
export const NotifHistory: Gtk.Widget = new Widget.Scrollable({
|
||||||
hscroll: Gtk.PolicyType.NEVER,
|
hscroll: Gtk.PolicyType.NEVER,
|
||||||
@@ -10,65 +11,8 @@ export const NotifHistory: Gtk.Widget = new Widget.Scrollable({
|
|||||||
child: new Widget.Box({
|
child: new Widget.Box({
|
||||||
className: "notifications",
|
className: "notifications",
|
||||||
children: bind(Notifications.getDefault(), "history").as((history: Array<AstalNotifd.Notification>) =>
|
children: bind(Notifications.getDefault(), "history").as((history: Array<AstalNotifd.Notification>) =>
|
||||||
history.map((notification: AstalNotifd.Notification) =>
|
history.map((notification: AstalNotifd.Notification) => NotificationWidget(notification,
|
||||||
new Widget.Box({
|
() => Notifications.getDefault().removeHistory(notification.id))
|
||||||
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)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} as Widget.BoxProps)
|
} as Widget.BoxProps)
|
||||||
} as Widget.ScrollableProps)
|
} as Widget.ScrollableProps)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Widget } from "astal/gtk3";
|
import { Gtk, Widget } from "astal/gtk3";
|
||||||
import { Page } from "./Page";
|
import { Page } from "./Page";
|
||||||
import AstalNetwork from "gi://AstalNetwork";
|
import AstalNetwork from "gi://AstalNetwork";
|
||||||
import { bind } from "astal";
|
import { bind } from "astal";
|
||||||
@@ -18,6 +18,59 @@ export const PageNetwork = new Page({
|
|||||||
} as Widget.ButtonProps)
|
} as Widget.ButtonProps)
|
||||||
],
|
],
|
||||||
pageChild: () => new Widget.Box({
|
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)
|
} as Widget.BoxProps)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export namespace Runner {
|
|||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
entryPlaceHolder?: string;
|
entryPlaceHolder?: string;
|
||||||
resultsPlaceholder?: () => Array<Gtk.Widget>;
|
resultsPlaceholder?: () => Array<ResultWidget>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const prefixes = new Map<string, (entry: string) => (ResultWidget|Array<ResultWidget>|null)>([
|
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) {
|
export function RunnerWindow(props?: RunnerProps): (Widget.Window|null) {
|
||||||
let subs: Array<() => void> = [];
|
let subs: Array<() => void> = [];
|
||||||
const entryText: Variable<string> = new Variable<string>("");
|
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;
|
let selectedResultIndex = 0;
|
||||||
|
|
||||||
const searchEntry = new Widget.Entry({
|
const searchEntry = new Widget.Entry({
|
||||||
|
|||||||
Reference in New Issue
Block a user