✨ feat: a lot of new stuff!
support for default bluetooth adapter, notification popup position in configuration, code improvements
This commit is contained in:
@@ -89,6 +89,7 @@
|
|||||||
|
|
||||||
& calendar.view {
|
& calendar.view {
|
||||||
$border-radius: 14px;
|
$border-radius: 14px;
|
||||||
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
background: colors.$bg-primary;
|
background: colors.$bg-primary;
|
||||||
border-radius: $border-radius;
|
border-radius: $border-radius;
|
||||||
@@ -104,7 +105,10 @@
|
|||||||
margin: 4px;
|
margin: 4px;
|
||||||
|
|
||||||
label.day-number {
|
label.day-number {
|
||||||
min-height: 22px;
|
$size: 24px;
|
||||||
|
|
||||||
|
min-height: $size;
|
||||||
|
min-width: $size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -342,7 +342,13 @@ const generalConfigDefaults = {
|
|||||||
notifications: {
|
notifications: {
|
||||||
timeout_low: 4000,
|
timeout_low: 4000,
|
||||||
timeout_normal: 6000,
|
timeout_normal: 6000,
|
||||||
timeout_critical: 0
|
timeout_critical: 0,
|
||||||
|
/** notification popup horizontal position. can be "left" or "right"
|
||||||
|
* @default "right" */
|
||||||
|
position_h: "right",
|
||||||
|
/** vertical notification popup position. can be "top" or "bottom"
|
||||||
|
* @default "top" */
|
||||||
|
position_v: "top"
|
||||||
},
|
},
|
||||||
|
|
||||||
night_light: {
|
night_light: {
|
||||||
|
|||||||
+2
-2
@@ -28,10 +28,10 @@ export function getAstalApps(): AstalApps.Apps {
|
|||||||
/** handles running with uwsm if it's installed */
|
/** handles running with uwsm if it's installed */
|
||||||
export function execApp(app: AstalApps.Application|string, dispatchExecArgs?: string) {
|
export function execApp(app: AstalApps.Application|string, dispatchExecArgs?: string) {
|
||||||
const executable = (typeof app === "string") ? app
|
const executable = (typeof app === "string") ? app
|
||||||
: app.executable.replace(/(%f|%F|%u|%U|%i|%c|%k)/g, "");
|
: app.executable.replace(/%[fFcuUik]/g, "");
|
||||||
|
|
||||||
AstalHyprland.get_default().dispatch("exec",
|
AstalHyprland.get_default().dispatch("exec",
|
||||||
`${dispatchExecArgs ? `${dispatchExecArgs} ` : ""}${uwsmIsActive ? "uwsm app -- " : ""}${executable}`
|
`${dispatchExecArgs ? `${dispatchExecArgs} ` : ""}${uwsmIsActive ? "uwsm-app -- " : ""}${executable}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { createRoot, getScope, Scope } from "ags";
|
||||||
|
import GObject, { getter, gtype, register, setter } from "ags/gobject";
|
||||||
|
import AstalBluetooth from "gi://AstalBluetooth";
|
||||||
|
|
||||||
|
|
||||||
|
/** AstalBluetooth helper (implements the default adapter feature) */
|
||||||
|
@register({ GTypeName: "Bluetooth" })
|
||||||
|
export class Bluetooth extends GObject.Object {
|
||||||
|
private static instance: Bluetooth;
|
||||||
|
private astalBl = AstalBluetooth.get_default();
|
||||||
|
|
||||||
|
#connections: Map<GObject.Object, Array<number>|number> = new Map();
|
||||||
|
#adapter: AstalBluetooth.Adapter|null = this.astalBl.adapter ?? null;
|
||||||
|
#scope!: Scope;
|
||||||
|
#isAvailable: boolean = false;
|
||||||
|
|
||||||
|
@getter(Boolean)
|
||||||
|
get isAvailable() { return this.#isAvailable; }
|
||||||
|
|
||||||
|
@getter(gtype<AstalBluetooth.Adapter|null>(AstalBluetooth.Adapter))
|
||||||
|
get adapter() { return this.#adapter; }
|
||||||
|
|
||||||
|
@setter(gtype<AstalBluetooth.Adapter|null>(AstalBluetooth.Adapter))
|
||||||
|
set adapter(newAdapter: AstalBluetooth.Adapter|null) {
|
||||||
|
this.#adapter = newAdapter;
|
||||||
|
this.notify("adapter");
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
createRoot((_) => {
|
||||||
|
this.#scope = getScope();
|
||||||
|
|
||||||
|
this.#connections.set(
|
||||||
|
AstalBluetooth.get_default(),
|
||||||
|
AstalBluetooth.get_default().connect("adapter-added", (self, adapter) => {
|
||||||
|
if(self.adapters.length === 1) // adapter was just added
|
||||||
|
this.adapter = adapter;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.#connections.set(
|
||||||
|
AstalBluetooth.get_default(),
|
||||||
|
AstalBluetooth.get_default().connect("adapter-removed", (self, adapter) => {
|
||||||
|
if(self.adapters.length < 1) {
|
||||||
|
this.adapter = null;
|
||||||
|
this.#isAvailable = false;
|
||||||
|
this.notify("is-available");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.#adapter?.address !== adapter.address)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// the removed adapter was the default
|
||||||
|
|
||||||
|
if(self.adapters.length < 1) {
|
||||||
|
this.adapter = null;
|
||||||
|
this.#isAvailable = false;
|
||||||
|
this.notify("is-available");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#adapter = self.adapters[0];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getDefault(): Bluetooth {
|
||||||
|
if(!this.instance)
|
||||||
|
this.instance = new Bluetooth();
|
||||||
|
|
||||||
|
return this.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
vfunc_dispose(): void {
|
||||||
|
this.#scope.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
-31
@@ -1,6 +1,7 @@
|
|||||||
import { execAsync } from "ags/process";
|
import { execAsync } from "ags/process";
|
||||||
import { getter, register, signal } from "ags/gobject";
|
import { getter, register, signal } from "ags/gobject";
|
||||||
import { Gdk } from "ags/gtk4";
|
import { Gdk } from "ags/gtk4";
|
||||||
|
import { createRoot, getScope, Scope } from "ags";
|
||||||
import { makeDirectory } from "./utils";
|
import { makeDirectory } from "./utils";
|
||||||
import { Notifications } from "./notifications";
|
import { Notifications } from "./notifications";
|
||||||
import { time } from "./utils";
|
import { time } from "./utils";
|
||||||
@@ -10,10 +11,8 @@ import GLib from "gi://GLib?version=2.0";
|
|||||||
import Gio from "gi://Gio?version=2.0";
|
import Gio from "gi://Gio?version=2.0";
|
||||||
|
|
||||||
|
|
||||||
export { Recording };
|
|
||||||
|
|
||||||
@register({ GTypeName: "Recording" })
|
@register({ GTypeName: "Recording" })
|
||||||
class Recording extends GObject.Object {
|
export class Recording extends GObject.Object {
|
||||||
private static instance: Recording;
|
private static instance: Recording;
|
||||||
|
|
||||||
@signal() started() {};
|
@signal() started() {};
|
||||||
@@ -21,6 +20,7 @@ class Recording extends GObject.Object {
|
|||||||
|
|
||||||
#recording: boolean = false;
|
#recording: boolean = false;
|
||||||
#path: string = "~/Recordings";
|
#path: string = "~/Recordings";
|
||||||
|
#recordingScope?: Scope;
|
||||||
|
|
||||||
/** Default extension: mp4(h264) */
|
/** Default extension: mp4(h264) */
|
||||||
#extension: string = "mp4";
|
#extension: string = "mp4";
|
||||||
@@ -64,7 +64,22 @@ class Recording extends GObject.Object {
|
|||||||
this.notify("extension");
|
this.notify("extension");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Recording output file name. %NULL if screen is not being recorded */
|
@getter(String)
|
||||||
|
public get recordingTime() {
|
||||||
|
if(!this.#recording || !this.#startedAt)
|
||||||
|
return "not recording";
|
||||||
|
|
||||||
|
const startedAtSeconds = time.get().to_unix() - Recording.getDefault().startedAt!;
|
||||||
|
if(startedAtSeconds <= 0) return "00:00";
|
||||||
|
|
||||||
|
const seconds = Math.floor(startedAtSeconds % 60);
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
|
||||||
|
return `${hours > 0 ? `${hours < 10 ? '0' : ""}${hours}` : ""}${ minutes < 10 ? `0${minutes}` : minutes }:${ seconds < 10 ? `0${seconds}` : seconds }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Recording output file name. null if screen is not being recorded */
|
||||||
public get output() { return this.#output; }
|
public get output() { return this.#output; }
|
||||||
|
|
||||||
/** Currently unsupported property */
|
/** Currently unsupported property */
|
||||||
@@ -90,41 +105,51 @@ class Recording extends GObject.Object {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public startRecording(area?: Gdk.Rectangle) {
|
public startRecording(area?: Gdk.Rectangle) {
|
||||||
if(this.recording)
|
if(this.#recording)
|
||||||
throw new Error("Screen Recording is already running!");
|
throw new Error("Screen Recording is already running!");
|
||||||
|
|
||||||
this.#output = `${time.get().format("%Y-%m-%d-%H%M%S")}_rec.${this.extension || "mp4"}`;
|
createRoot(() => {
|
||||||
this.#recording = true;
|
this.#recordingScope = getScope();
|
||||||
this.notify("recording");
|
|
||||||
this.emit("started");
|
|
||||||
makeDirectory(this.path);
|
|
||||||
|
|
||||||
const cancellable = Gio.Cancellable.new();
|
this.#output = `${time.get().format("%Y-%m-%d-%H%M%S")}_rec.${this.extension || "mp4"}`;
|
||||||
cancellable.cancel = () => {};
|
this.#recording = true;
|
||||||
|
this.notify("recording");
|
||||||
|
this.emit("started");
|
||||||
|
makeDirectory(this.path);
|
||||||
|
|
||||||
const areaString = `${area?.x ?? 0},${area?.y ?? 0} ${area?.width ?? 1}x${area?.height ?? 1}`;
|
const areaString = `${area?.x ?? 0},${area?.y ?? 0} ${area?.width ?? 1}x${area?.height ?? 1}`;
|
||||||
|
|
||||||
this.#process = Gio.Subprocess.new([
|
this.#process = Gio.Subprocess.new([
|
||||||
"wf-recorder",
|
"wf-recorder",
|
||||||
...(area ? [ `-g`, areaString ] : []),
|
...(area ? [ `-g`, areaString ] : []),
|
||||||
"-f",
|
"-f",
|
||||||
`${this.path}/${this.output!}`
|
`${this.path}/${this.output!}`
|
||||||
], Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE);
|
], Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE);
|
||||||
|
|
||||||
this.#process.wait_async(cancellable, () => {
|
this.#process.wait_async(null, () => {
|
||||||
this.stopRecording();
|
this.stopRecording();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#startedAt = time.get().to_unix();
|
||||||
|
this.notify("started-at");
|
||||||
|
|
||||||
|
const timeSub = time.subscribe(() => {
|
||||||
|
this.notify("recording-time");
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#recordingScope.onCleanup(timeSub);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.#startedAt = time.get().to_unix();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public stopRecording() {
|
public stopRecording() {
|
||||||
if(!this.#process) return;
|
if(!this.#process || !this.#recording) return;
|
||||||
|
|
||||||
!this.#process.get_if_exited() && execAsync([
|
!this.#process.get_if_exited() && execAsync([
|
||||||
"kill", "-s", "SIGTERM", this.#process.get_identifier()!
|
"kill", "-s", "SIGTERM", this.#process.get_identifier()!
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
this.#recordingScope?.dispose();
|
||||||
|
|
||||||
const path = this.#path;
|
const path = this.#path;
|
||||||
const output = this.#output;
|
const output = this.#output;
|
||||||
|
|
||||||
@@ -138,13 +163,8 @@ class Recording extends GObject.Object {
|
|||||||
Notifications.getDefault().sendNotification({
|
Notifications.getDefault().sendNotification({
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
text: "View",
|
text: "View", // will be hidden(can be triggered by clicking in the notification)
|
||||||
onAction: () => {
|
id: "view",
|
||||||
execAsync(["nautilus", "-s", output!, path]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "Open",
|
|
||||||
onAction: () => {
|
onAction: () => {
|
||||||
execAsync(["xdg-open", `${path}/${output}`]);
|
execAsync(["xdg-open", `${path}/${output}`]);
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-6
@@ -1,6 +1,6 @@
|
|||||||
import { createPoll } from "ags/time";
|
import { createPoll } from "ags/time";
|
||||||
import { exec, execAsync } from "ags/process";
|
import { exec, execAsync } from "ags/process";
|
||||||
import { Accessor, For, With } from "ags";
|
import { Accessor, For, getScope, onCleanup, With } from "ags";
|
||||||
import { Astal, Gtk } from "ags/gtk4";
|
import { Astal, Gtk } from "ags/gtk4";
|
||||||
import { getSymbolicIcon } from "./apps";
|
import { getSymbolicIcon } from "./apps";
|
||||||
|
|
||||||
@@ -217,7 +217,7 @@ export function addSliderMarksFromMinMax(slider: Astal.Slider, amountOfMarks: nu
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** initialize and sub class properties with accessors */
|
/** initialize and sub class properties with accessors */
|
||||||
export function construct(klass: object, props: Record<any, any|Accessor<any>>): Array<() => void> {
|
export function construct<Class extends object>(klass: Class, props: Record<any, any|Accessor<any>>): Array<() => void> {
|
||||||
|
|
||||||
const subs: Array<() => void> = [];
|
const subs: Array<() => void> = [];
|
||||||
const isGObject = klass instanceof GObject.Object;
|
const isGObject = klass instanceof GObject.Object;
|
||||||
@@ -228,16 +228,52 @@ export function construct(klass: object, props: Record<any, any|Accessor<any>>):
|
|||||||
if(v === undefined) return;
|
if(v === undefined) return;
|
||||||
if(v instanceof Accessor) {
|
if(v instanceof Accessor) {
|
||||||
subs.push(v.subscribe(() => {
|
subs.push(v.subscribe(() => {
|
||||||
klass[k as keyof typeof klass] = v.get() as never;
|
klass[k as keyof Class] = v.get() as Class[keyof Class];
|
||||||
if(isGObject) klass.notify(k);
|
if(isGObject)
|
||||||
|
klass.notify(k.replace(/[A-Z]/g, (s) => `-${s.toLowerCase()}`
|
||||||
|
));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
klass[k as keyof typeof klass] = v.get() as never;
|
klass[k as keyof Class] = v.get() as Class[keyof Class];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
klass[k as keyof typeof klass] = v as never;
|
klass[k as keyof Class] = v as Class[keyof Class];
|
||||||
});
|
});
|
||||||
|
|
||||||
return subs;
|
return subs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** open connections to gobjects that are closed when the scope
|
||||||
|
* is disposed
|
||||||
|
* @experimental
|
||||||
|
* */
|
||||||
|
export function createConnetions<
|
||||||
|
GObj extends GObject.Object,
|
||||||
|
Signals extends GObj["$signals"],
|
||||||
|
Signal extends keyof Signals,
|
||||||
|
Callback extends Signals[Signal]
|
||||||
|
>(...conns: Array<[GObj, Signal, Callback]>): void {
|
||||||
|
const scope = getScope();
|
||||||
|
|
||||||
|
const connections: Map<GObj, Array<number>> = new Map();
|
||||||
|
|
||||||
|
scope.onCleanup(() => connections.forEach((ids, gobj) =>
|
||||||
|
ids.forEach(id => gobj.disconnect(id))
|
||||||
|
));
|
||||||
|
|
||||||
|
function add(gobj: GObj, id: number): void {
|
||||||
|
if(connections.has(gobj)) {
|
||||||
|
connections.get(gobj)!.push(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
connections.set(gobj, [id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
conns.forEach(([gobj, sig, callback]) => {
|
||||||
|
// type stuff
|
||||||
|
add(gobj, gobj.connect(sig as string, callback as never));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import GObject from "ags/gobject";
|
|||||||
import AstalBluetooth from "gi://AstalBluetooth";
|
import AstalBluetooth from "gi://AstalBluetooth";
|
||||||
import AstalNetwork from "gi://AstalNetwork";
|
import AstalNetwork from "gi://AstalNetwork";
|
||||||
import AstalWp from "gi://AstalWp";
|
import AstalWp from "gi://AstalWp";
|
||||||
|
import { Bluetooth } from "../../modules/bluetooth";
|
||||||
|
|
||||||
|
|
||||||
export const Status = () =>
|
export const Status = () =>
|
||||||
@@ -22,14 +23,16 @@ export const Status = () =>
|
|||||||
<VolumeStatus class="sink" endpoint={Wireplumber.getDefault().getDefaultSink()}
|
<VolumeStatus class="sink" endpoint={Wireplumber.getDefault().getDefaultSink()}
|
||||||
icon={createBinding(Wireplumber.getDefault().getDefaultSink(), "volumeIcon").as(icon =>
|
icon={createBinding(Wireplumber.getDefault().getDefaultSink(), "volumeIcon").as(icon =>
|
||||||
!Wireplumber.getDefault().isMutedSink() &&
|
!Wireplumber.getDefault().isMutedSink() &&
|
||||||
Wireplumber.getDefault().getSinkVolume() > 0 ? icon
|
Wireplumber.getDefault().getSinkVolume() > 0 ?
|
||||||
|
icon
|
||||||
: "audio-volume-muted-symbolic")
|
: "audio-volume-muted-symbolic")
|
||||||
} />
|
} />
|
||||||
|
|
||||||
<VolumeStatus class="source" endpoint={Wireplumber.getDefault().getDefaultSource()}
|
<VolumeStatus class="source" endpoint={Wireplumber.getDefault().getDefaultSource()}
|
||||||
icon={createBinding(Wireplumber.getDefault().getDefaultSource(), "volumeIcon").as(icon =>
|
icon={createBinding(Wireplumber.getDefault().getDefaultSource(), "volumeIcon").as(icon =>
|
||||||
!Wireplumber.getDefault().isMutedSource() &&
|
!Wireplumber.getDefault().isMutedSource() &&
|
||||||
Wireplumber.getDefault().getSourceVolume() > 0 ? icon
|
Wireplumber.getDefault().getSourceVolume() > 0 ?
|
||||||
|
icon
|
||||||
: "microphone-sensitivity-muted-symbolic")
|
: "microphone-sensitivity-muted-symbolic")
|
||||||
} />
|
} />
|
||||||
</Gtk.Box>
|
</Gtk.Box>
|
||||||
@@ -40,22 +43,9 @@ export const Status = () =>
|
|||||||
<Gtk.Image class={"recording state"} iconName={"media-record-symbolic"}
|
<Gtk.Image class={"recording state"} iconName={"media-record-symbolic"}
|
||||||
css={"margin-right: 6px;"} />
|
css={"margin-right: 6px;"} />
|
||||||
|
|
||||||
<Gtk.Label class={"rec-time"} label={createComputed([
|
<Gtk.Label class={"rec-time"} label={
|
||||||
createBinding(Recording.getDefault(), "recording"),
|
createBinding(Recording.getDefault(), "recordingTime")
|
||||||
time
|
} />
|
||||||
], (recording, dateTime) => {
|
|
||||||
if(!recording || !Recording.getDefault().startedAt)
|
|
||||||
return "...";
|
|
||||||
|
|
||||||
const startedAtSeconds = dateTime.to_unix() - Recording.getDefault().startedAt!;
|
|
||||||
if(startedAtSeconds <= 0) return "00:00";
|
|
||||||
|
|
||||||
const minutes = Math.floor(startedAtSeconds / 60);
|
|
||||||
const seconds = Math.floor(startedAtSeconds % 60);
|
|
||||||
|
|
||||||
return `${ minutes < 10 ? `0${minutes}` : minutes }:${ seconds < 10 ? `0${seconds}` : seconds }`;
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Gtk.Box>
|
</Gtk.Box>
|
||||||
</Gtk.Revealer>
|
</Gtk.Revealer>
|
||||||
<StatusIcons />
|
<StatusIcons />
|
||||||
@@ -99,7 +89,7 @@ function StatusIcons() {
|
|||||||
: "bluetooth-symbolic"
|
: "bluetooth-symbolic"
|
||||||
) : "bluetooth-disabled-symbolic"
|
) : "bluetooth-disabled-symbolic"
|
||||||
})} class={"bluetooth state"} visible={
|
})} class={"bluetooth state"} visible={
|
||||||
createBinding(AstalBluetooth.get_default(), "adapter").as(Boolean)
|
createBinding(Bluetooth.getDefault(), "adapter").as(Boolean)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ import { tr } from "../../../i18n/intl";
|
|||||||
import { Windows } from "../../../windows";
|
import { Windows } from "../../../windows";
|
||||||
import { Notifications } from "../../../modules/notifications";
|
import { Notifications } from "../../../modules/notifications";
|
||||||
import { execApp } from "../../../modules/apps";
|
import { execApp } from "../../../modules/apps";
|
||||||
|
import { execAsync } from "ags/process";
|
||||||
import { createBinding, createComputed, For, With } from "ags";
|
import { createBinding, createComputed, For, With } from "ags";
|
||||||
|
import { Bluetooth } from "../../../modules/bluetooth";
|
||||||
|
|
||||||
import AstalNotifd from "gi://AstalNotifd";
|
import AstalNotifd from "gi://AstalNotifd";
|
||||||
import AstalBluetooth from "gi://AstalBluetooth";
|
import AstalBluetooth from "gi://AstalBluetooth";
|
||||||
|
import Adw from "gi://Adw?version=1";
|
||||||
|
|
||||||
|
|
||||||
export const BluetoothPage = new Page({
|
export const BluetoothPage = new Page({
|
||||||
@@ -15,27 +18,27 @@ export const BluetoothPage = new Page({
|
|||||||
title: tr("control_center.pages.bluetooth.title"),
|
title: tr("control_center.pages.bluetooth.title"),
|
||||||
spacing: 6,
|
spacing: 6,
|
||||||
description: tr("control_center.pages.bluetooth.description"),
|
description: tr("control_center.pages.bluetooth.description"),
|
||||||
headerButtons: [{
|
headerButtons: createBinding(Bluetooth.getDefault(), "adapter").as(adapter => adapter ? [{
|
||||||
icon: createBinding(AstalBluetooth.get_default().adapter, "discovering")
|
icon: createBinding(adapter, "discovering")
|
||||||
.as(discovering => !discovering ?
|
.as(discovering => !discovering ?
|
||||||
"arrow-circular-top-right-symbolic"
|
"arrow-circular-top-right-symbolic"
|
||||||
: "media-playback-stop-symbolic"
|
: "media-playback-stop-symbolic"
|
||||||
),
|
),
|
||||||
tooltipText: createBinding(AstalBluetooth.get_default().adapter, "discovering")
|
tooltipText: createBinding(adapter, "discovering")
|
||||||
.as((discovering) => !discovering ?
|
.as((discovering) => !discovering ?
|
||||||
tr("control_center.pages.bluetooth.start_discovering")
|
tr("control_center.pages.bluetooth.start_discovering")
|
||||||
: tr("control_center.pages.bluetooth.stop_discovering")),
|
: tr("control_center.pages.bluetooth.stop_discovering")),
|
||||||
actionClicked: () => {
|
actionClicked: () => {
|
||||||
if(AstalBluetooth.get_default().adapter.discovering) {
|
if(adapter.discovering) {
|
||||||
AstalBluetooth.get_default().adapter.stop_discovery();
|
adapter.stop_discovery();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
AstalBluetooth.get_default().adapter.start_discovery();
|
adapter.start_discovery();
|
||||||
}
|
}
|
||||||
}],
|
}]: []),
|
||||||
actionClosed: () => AstalBluetooth.get_default().adapter?.discovering &&
|
actionClosed: () => Bluetooth.getDefault().adapter?.discovering &&
|
||||||
AstalBluetooth.get_default().adapter.stop_discovery(),
|
Bluetooth.getDefault().adapter?.stop_discovery(),
|
||||||
bottomButtons: [{
|
bottomButtons: [{
|
||||||
title: tr("control_center.pages.more_settings"),
|
title: tr("control_center.pages.more_settings"),
|
||||||
actionClicked: () => {
|
actionClicked: () => {
|
||||||
@@ -43,63 +46,79 @@ export const BluetoothPage = new Page({
|
|||||||
execApp("overskride", "[float; animation slide right]");
|
execApp("overskride", "[float; animation slide right]");
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
content: () => [
|
content: () => {
|
||||||
<Gtk.Box class={"adapters"} visible={createBinding(AstalBluetooth.get_default(), "adapters")
|
const adapter = createBinding(Bluetooth.getDefault(), "adapter");
|
||||||
.as(adptrs => adptrs.length > 1)
|
const adapters = createBinding(AstalBluetooth.get_default(), "adapters");
|
||||||
} spacing={2} orientation={Gtk.Orientation.VERTICAL}>
|
const devices = createBinding(AstalBluetooth.get_default(), "devices");
|
||||||
|
|
||||||
<Gtk.Label class={"sub-header"} label={tr("control_center.pages.bluetooth.adapters")}
|
return [
|
||||||
xalign={0} />
|
<Gtk.Box class={"adapters"} visible={adapters.as(adptrs => adptrs.length > 1)
|
||||||
<With value={createBinding(AstalBluetooth.get_default(), "adapters").as(adpts =>
|
} spacing={2} orientation={Gtk.Orientation.VERTICAL}>
|
||||||
adpts.length > 1)}>
|
|
||||||
|
|
||||||
{(hasMoreAdapters: boolean) => hasMoreAdapters &&
|
<Gtk.Label class={"sub-header"} label={tr("control_center.pages.bluetooth.adapters")}
|
||||||
<Gtk.Box orientation={Gtk.Orientation.VERTICAL} spacing={2}>
|
|
||||||
<For each={createBinding(AstalBluetooth.get_default(), "adapters")}>
|
|
||||||
{(adapter: AstalBluetooth.Adapter) => {
|
|
||||||
const isSelected = createBinding(AstalBluetooth.get_default(), "adapter").as(a =>
|
|
||||||
a.address === adapter.address);
|
|
||||||
|
|
||||||
return <PageButton class={isSelected.as(is => is ? "selected" : "")}
|
|
||||||
title={adapter.alias ?? "Adapter"} icon={"bluetooth-active-symbolic"}
|
|
||||||
description={createBinding(adapter, "address")}
|
|
||||||
endWidget={
|
|
||||||
<Gtk.Image iconName={"object-select-symbolic"} visible={isSelected} />
|
|
||||||
}
|
|
||||||
/>;
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</Gtk.Box>
|
|
||||||
}
|
|
||||||
</With>
|
|
||||||
</Gtk.Box>,
|
|
||||||
<Gtk.Box class={"connections"} orientation={Gtk.Orientation.VERTICAL} hexpand={true}
|
|
||||||
spacing={2}>
|
|
||||||
|
|
||||||
<Gtk.Box class={"paired"} orientation={Gtk.Orientation.VERTICAL} spacing={4}
|
|
||||||
visible={createBinding(AstalBluetooth.get_default(), "devices").as(devs =>
|
|
||||||
devs.filter(dev => dev.paired || dev.connected || dev.trusted).length > 0)}>
|
|
||||||
|
|
||||||
<Gtk.Label class={"sub-header"} label={tr("devices")} xalign={0} />
|
|
||||||
<For each={createBinding(AstalBluetooth.get_default(), "devices").as(devs =>
|
|
||||||
devs.filter(dev => dev.paired || dev.connected || dev.trusted))}>
|
|
||||||
|
|
||||||
{(dev: AstalBluetooth.Device) => <DeviceWidget device={dev} />}
|
|
||||||
</For>
|
|
||||||
</Gtk.Box>
|
|
||||||
<Gtk.Box class={"discovered"} orientation={Gtk.Orientation.VERTICAL} spacing={4}
|
|
||||||
visible={createBinding(AstalBluetooth.get_default(), "devices").as(devs =>
|
|
||||||
devs.filter(dev => !dev.connected && !dev.paired && !dev.trusted).length > 0)}>
|
|
||||||
|
|
||||||
<Gtk.Label class={"sub-header"} label={tr("control_center.pages.bluetooth.new_devices")}
|
|
||||||
xalign={0} />
|
xalign={0} />
|
||||||
<For each={createBinding(AstalBluetooth.get_default(), "devices").as(devs =>
|
<With value={adapters.as(adpts => adpts.length > 1)}>
|
||||||
devs.filter(dev => !dev.connected && !dev.paired && !dev.trusted))}>
|
{(hasMoreAdapters: boolean) => hasMoreAdapters &&
|
||||||
{(dev: AstalBluetooth.Device) => <DeviceWidget device={dev} />}
|
<Gtk.Box orientation={Gtk.Orientation.VERTICAL} spacing={2}>
|
||||||
</For>
|
<For each={adapters}>
|
||||||
|
{(adapter: AstalBluetooth.Adapter) => {
|
||||||
|
const isSelected = createBinding(Bluetooth.getDefault(), "adapter").as(a =>
|
||||||
|
adapter.address === a?.address);
|
||||||
|
|
||||||
|
return <PageButton class={isSelected.as(is => is ? "selected" : "")}
|
||||||
|
title={adapter.alias ?? "Adapter"} icon={"bluetooth-active-symbolic"}
|
||||||
|
description={createBinding(adapter, "address")}
|
||||||
|
actionClicked={() =>
|
||||||
|
adapter.address !== Bluetooth.getDefault().adapter?.address &&
|
||||||
|
selectAdapter(adapter)
|
||||||
|
}
|
||||||
|
endWidget={
|
||||||
|
<Gtk.Image iconName={"object-select-symbolic"} visible={isSelected} />
|
||||||
|
}
|
||||||
|
/>;
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</Gtk.Box>
|
||||||
|
}
|
||||||
|
</With>
|
||||||
|
</Gtk.Box>,
|
||||||
|
<Gtk.Box class={"connections"} orientation={Gtk.Orientation.VERTICAL} hexpand
|
||||||
|
spacing={2}>
|
||||||
|
|
||||||
|
<Gtk.Box class={"paired"} orientation={Gtk.Orientation.VERTICAL} spacing={4}
|
||||||
|
visible={devices.as(devs => devs.filter(dev =>
|
||||||
|
(dev.adapter as AstalBluetooth.Adapter).address === adapter.get()?.address &&
|
||||||
|
dev.paired || dev.connected || dev.trusted).length > 0)
|
||||||
|
}>
|
||||||
|
|
||||||
|
<Gtk.Label class={"sub-header"} label={tr("devices")} xalign={0} />
|
||||||
|
<For each={devices.as(devs => devs.filter(dev =>
|
||||||
|
(dev.adapter as AstalBluetooth.Adapter).address === adapter.get()?.address &&
|
||||||
|
dev.paired || dev.connected || dev.trusted))
|
||||||
|
}>
|
||||||
|
|
||||||
|
{(dev: AstalBluetooth.Device) => <DeviceWidget device={dev} />}
|
||||||
|
</For>
|
||||||
|
</Gtk.Box>
|
||||||
|
<Gtk.Box class={"discovered"} orientation={Gtk.Orientation.VERTICAL} spacing={4}
|
||||||
|
visible={devices.as(devs => devs.filter(dev =>
|
||||||
|
(dev.adapter as AstalBluetooth.Adapter).address === adapter.get()?.address &&
|
||||||
|
!dev.connected && !dev.paired && !dev.trusted).length > 0)
|
||||||
|
}>
|
||||||
|
|
||||||
|
<Gtk.Label class={"sub-header"} label={tr("control_center.pages.bluetooth.new_devices")}
|
||||||
|
xalign={0} />
|
||||||
|
<For each={devices.as(devs => devs.filter(dev =>
|
||||||
|
(dev.adapter as AstalBluetooth.Adapter).address === adapter.get()?.address &&
|
||||||
|
!dev.connected && !dev.paired && !dev.trusted))
|
||||||
|
}>
|
||||||
|
|
||||||
|
{(dev: AstalBluetooth.Device) => <DeviceWidget device={dev} />}
|
||||||
|
</For>
|
||||||
|
</Gtk.Box>
|
||||||
</Gtk.Box>
|
</Gtk.Box>
|
||||||
</Gtk.Box>
|
];
|
||||||
]
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function DeviceWidget({ device }: { device: AstalBluetooth.Device }): Gtk.Widget {
|
function DeviceWidget({ device }: { device: AstalBluetooth.Device }): Gtk.Widget {
|
||||||
@@ -107,9 +126,6 @@ function DeviceWidget({ device }: { device: AstalBluetooth.Device }): Gtk.Widget
|
|||||||
conn ? "selected" : "")} title={
|
conn ? "selected" : "")} title={
|
||||||
createBinding(device, "alias").as(alias => alias ?? "Unknown Device")}
|
createBinding(device, "alias").as(alias => alias ?? "Unknown Device")}
|
||||||
icon={createBinding(device, "icon").as(ico => ico ?? "bluetooth-active-symbolic")}
|
icon={createBinding(device, "icon").as(ico => ico ?? "bluetooth-active-symbolic")}
|
||||||
description={
|
|
||||||
createBinding(device, "connecting").as(connecting =>
|
|
||||||
connecting ? `${tr("connecting")}...` : "")}
|
|
||||||
tooltipText={
|
tooltipText={
|
||||||
createBinding(device, "connected").as(connected =>
|
createBinding(device, "connected").as(connected =>
|
||||||
!connected ? tr("connect") : "")
|
!connected ? tr("connect") : "")
|
||||||
@@ -138,19 +154,24 @@ function DeviceWidget({ device }: { device: AstalBluetooth.Device }): Gtk.Widget
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
endWidget={<Gtk.Box visible={createComputed([
|
endWidget={<Gtk.Box spacing={6}>
|
||||||
createBinding(device, "batteryPercentage"),
|
<Adw.Spinner visible={createBinding(device, "connecting")} />
|
||||||
createBinding(device, "connected")
|
<Gtk.Box visible={createComputed([
|
||||||
]).as(([batt, connected]) => connected && (batt > -1))
|
createBinding(device, "batteryPercentage"),
|
||||||
}>
|
createBinding(device, "connected")
|
||||||
<Gtk.Label halign={Gtk.Align.END} label={
|
]).as(([batt, connected]) => connected && (batt > -1))
|
||||||
createBinding(device, "batteryPercentage").as(batt =>
|
} spacing={4}>
|
||||||
`${Math.floor(batt * 100)}%`)} />
|
<Gtk.Label halign={Gtk.Align.END} label={
|
||||||
|
createBinding(device, "batteryPercentage").as(batt =>
|
||||||
|
`${Math.floor(batt * 100)}%`)
|
||||||
|
} visible={createBinding(device, "connected")}
|
||||||
|
/>
|
||||||
|
|
||||||
<Gtk.Image iconName={
|
<Gtk.Image iconName={
|
||||||
createBinding(device, "batteryPercentage").as(batt =>
|
createBinding(device, "batteryPercentage").as(batt =>
|
||||||
`battery-level-${Math.floor(batt * 100)}-symbolic`)
|
`battery-level-${Math.floor(batt * 100)}-symbolic`)
|
||||||
} css={"font-size: 16px; margin-left: 6px;"} />
|
} css={"font-size: 16px; margin-left: 6px;"} />
|
||||||
|
</Gtk.Box>
|
||||||
</Gtk.Box>} extraButtons={<With value={createComputed([
|
</Gtk.Box>} extraButtons={<With value={createComputed([
|
||||||
createBinding(device, "connected"),
|
createBinding(device, "connected"),
|
||||||
createBinding(device, "trusted")
|
createBinding(device, "trusted")
|
||||||
@@ -164,7 +185,7 @@ function DeviceWidget({ device }: { device: AstalBluetooth.Device }): Gtk.Widget
|
|||||||
: "control_center.pages.bluetooth.unpair_device"
|
: "control_center.pages.bluetooth.unpair_device"
|
||||||
)} onClicked={() => {
|
)} onClicked={() => {
|
||||||
if(!connected) {
|
if(!connected) {
|
||||||
AstalBluetooth.get_default().adapter?.remove_device(device);
|
Bluetooth.getDefault().adapter?.remove_device(device);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,3 +203,18 @@ function DeviceWidget({ device }: { device: AstalBluetooth.Device }): Gtk.Widget
|
|||||||
</With>}
|
</With>}
|
||||||
/> as Gtk.Widget;
|
/> as Gtk.Widget;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectAdapter(adapter: AstalBluetooth.Adapter): void {
|
||||||
|
AstalBluetooth.get_default().adapters.filter(ad => {
|
||||||
|
if(ad.alias !== adapter.alias)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
ad.set_powered(true);
|
||||||
|
return false;
|
||||||
|
}).forEach(ad => ad.set_powered(false));
|
||||||
|
|
||||||
|
execAsync(`bluetoothctl select ${adapter.address}`).catch(e =>
|
||||||
|
console.error(`Bluetooth: Couldn't select adapter. Stderr: ${e}`));
|
||||||
|
|
||||||
|
Bluetooth.getDefault().adapter = adapter;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,17 +3,21 @@ import AstalBluetooth from "gi://AstalBluetooth";
|
|||||||
import { BluetoothPage } from "../pages/Bluetooth";
|
import { BluetoothPage } from "../pages/Bluetooth";
|
||||||
import { TilesPages } from "../Tiles";
|
import { TilesPages } from "../Tiles";
|
||||||
import { createBinding, createComputed } from "ags";
|
import { createBinding, createComputed } from "ags";
|
||||||
|
import { Bluetooth } from "../../../modules/bluetooth";
|
||||||
|
|
||||||
|
|
||||||
export const TileBluetooth = () =>
|
export const TileBluetooth = () =>
|
||||||
<Tile title={"Bluetooth"} visible={
|
<Tile title={"Bluetooth"} visible={
|
||||||
createBinding(AstalBluetooth.get_default(), "adapter").as(Boolean)
|
createBinding(Bluetooth.getDefault(), "adapter").as(Boolean)
|
||||||
} description={createBinding(AstalBluetooth.get_default(), "isConnected").as((connected) => {
|
} description={createBinding(AstalBluetooth.get_default(), "isConnected").as((connected) => {
|
||||||
const connectedDev = AstalBluetooth.get_default().devices.filter(dev => dev.connected)?.[0];
|
if(!connected) return "";
|
||||||
return connected && connectedDev ? connectedDev.get_alias() : ""
|
|
||||||
|
const connectedDevs = AstalBluetooth.get_default().devices.filter(dev => dev.connected);
|
||||||
|
const connectedDev = connectedDevs[connectedDevs.length - 1]; // last connected device is on display
|
||||||
|
return connectedDev ? connectedDev.get_alias() : ""
|
||||||
})}
|
})}
|
||||||
onEnabled={() => AstalBluetooth.get_default().adapter?.set_powered(true)}
|
onEnabled={() => Bluetooth.getDefault().adapter?.set_powered(true)}
|
||||||
onDisabled={() => AstalBluetooth.get_default().adapter?.set_powered(false)}
|
onDisabled={() => Bluetooth.getDefault().adapter?.set_powered(false)}
|
||||||
onClicked={() => TilesPages?.toggle(BluetoothPage)}
|
onClicked={() => TilesPages?.toggle(BluetoothPage)}
|
||||||
enableOnClicked hasArrow
|
enableOnClicked hasArrow
|
||||||
state={createBinding(AstalBluetooth.get_default(), "isPowered")}
|
state={createBinding(AstalBluetooth.get_default(), "isPowered")}
|
||||||
|
|||||||
@@ -1,16 +1,50 @@
|
|||||||
import { Astal, Gtk } from "ags/gtk4";
|
import { Astal, Gtk } from "ags/gtk4";
|
||||||
import { createBinding, For } from "ags";
|
import { createBinding, createComputed, For } from "ags";
|
||||||
import { Notifications } from "../modules/notifications";
|
import { Notifications } from "../modules/notifications";
|
||||||
import { NotificationWidget } from "../widget/Notification";
|
import { NotificationWidget } from "../widget/Notification";
|
||||||
|
import { generalConfig } from "../app";
|
||||||
|
|
||||||
import AstalNotifd from "gi://AstalNotifd?version=0.1";
|
import AstalNotifd from "gi://AstalNotifd";
|
||||||
import Adw from "gi://Adw?version=1";
|
import Adw from "gi://Adw?version=1";
|
||||||
|
|
||||||
const size = 450;
|
const size = 450;
|
||||||
|
|
||||||
export const FloatingNotifications = (mon: number) =>
|
export const FloatingNotifications = (mon: number) =>
|
||||||
<Astal.Window namespace={"floating-notifications"} monitor={mon} layer={Astal.Layer.OVERLAY}
|
<Astal.Window namespace={"floating-notifications"} monitor={mon} layer={Astal.Layer.OVERLAY}
|
||||||
anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.RIGHT} exclusivity={Astal.Exclusivity.NORMAL}
|
anchor={createComputed([
|
||||||
|
generalConfig.bindProperty("notifications.position_h", "string"),
|
||||||
|
generalConfig.bindProperty("notifications.position_v", "string")
|
||||||
|
]).as(([posH, posV]) => {
|
||||||
|
let horizontal: Astal.WindowAnchor = Astal.WindowAnchor.RIGHT,
|
||||||
|
vertical: Astal.WindowAnchor = Astal.WindowAnchor.TOP;
|
||||||
|
|
||||||
|
switch(posH) {
|
||||||
|
case "left":
|
||||||
|
horizontal = Astal.WindowAnchor.LEFT;
|
||||||
|
break;
|
||||||
|
case "center":
|
||||||
|
horizontal = Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT;
|
||||||
|
break;
|
||||||
|
case "right":
|
||||||
|
horizontal = Astal.WindowAnchor.RIGHT;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(posV) {
|
||||||
|
case "top":
|
||||||
|
vertical = Astal.WindowAnchor.TOP;
|
||||||
|
break;
|
||||||
|
case "center":
|
||||||
|
vertical = Astal.WindowAnchor.TOP | Astal.WindowAnchor.BOTTOM;
|
||||||
|
break;
|
||||||
|
case "bottom":
|
||||||
|
vertical = Astal.WindowAnchor.BOTTOM;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return horizontal | vertical;
|
||||||
|
|
||||||
|
})} exclusivity={Astal.Exclusivity.NORMAL}
|
||||||
resizable={false} widthRequest={450}>
|
resizable={false} widthRequest={450}>
|
||||||
|
|
||||||
<Gtk.Box class={"floating-notifications-container"} spacing={12}
|
<Gtk.Box class={"floating-notifications-container"} spacing={12}
|
||||||
@@ -18,22 +52,34 @@ export const FloatingNotifications = (mon: number) =>
|
|||||||
|
|
||||||
<For each={createBinding(Notifications.getDefault(), "notifications")}>
|
<For each={createBinding(Notifications.getDefault(), "notifications")}>
|
||||||
{(notif: AstalNotifd.Notification) =>
|
{(notif: AstalNotifd.Notification) =>
|
||||||
<Adw.Clamp maximumSize={size}>
|
<Gtk.Stack transitionType={createComputed([
|
||||||
<Gtk.Box class={"float-notification"} widthRequest={size} vexpand={false}>
|
generalConfig.bindProperty("notifications.position_h", "string"),
|
||||||
|
generalConfig.bindProperty("notifications.position_v", "string")
|
||||||
|
]).as(([posH, posV]) => {
|
||||||
|
//TODO: support different animations depending on screen position
|
||||||
|
return Gtk.StackTransitionType.SLIDE_RIGHT
|
||||||
|
})} transitionDuration={300}>
|
||||||
|
<Gtk.StackPage name={"notification"} child={
|
||||||
|
<Adw.Clamp maximumSize={size}>
|
||||||
|
<Gtk.Box class={"float-notification"} widthRequest={size} vexpand={false}
|
||||||
|
valign={Gtk.Align.CENTER} halign={Gtk.Align.CENTER}>
|
||||||
|
|
||||||
<NotificationWidget notification={notif} showTime={false}
|
<NotificationWidget notification={notif} showTime={false}
|
||||||
actionClose={() => Notifications.getDefault().removeNotification(notif)}
|
actionClose={() => Notifications.getDefault().removeNotification(notif)}
|
||||||
holdOnHover actionClicked={() => {
|
holdOnHover actionClicked={() => {
|
||||||
const viewAction = notif.actions.filter(a =>
|
const viewAction = notif.actions.filter(a =>
|
||||||
a.id.toLowerCase() === "view" ||
|
a.id.toLowerCase() === "view" ||
|
||||||
a.label.toLowerCase() === "view"
|
a.label.toLowerCase() === "view"
|
||||||
)?.[0];
|
)?.[0];
|
||||||
|
|
||||||
viewAction && notif.invoke(viewAction.id);
|
viewAction && notif.invoke(viewAction.id);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Gtk.Box>
|
</Gtk.Box>
|
||||||
</Adw.Clamp>
|
</Adw.Clamp> as Gtk.Widget
|
||||||
|
}>
|
||||||
|
</Gtk.StackPage>
|
||||||
|
</Gtk.Stack>
|
||||||
}
|
}
|
||||||
</For>
|
</For>
|
||||||
</Gtk.Box>
|
</Gtk.Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user