feat(control-center/page): restore bottom buttons for pages

it was accidentally removed previously, but now it's backgit add .git add .
This commit is contained in:
retrozinndev
2025-10-24 18:24:21 -03:00
parent 9d1e91f72e
commit e473344eef
17 changed files with 247 additions and 233 deletions
+1 -3
View File
@@ -13,9 +13,7 @@
"sync-config": "sh ./scripts/sync-config.sh" "sync-config": "sh ./scripts/sync-config.sh"
}, },
"devDependencies": { "devDependencies": {
"ags": "link:../../../../usr/share/ags/js" "ags": "link:../../../../usr/share/ags/js",
},
"dependencies": {
"gnim-utils": "github:retrozinndev/gnim-utils" "gnim-utils": "github:retrozinndev/gnim-utils"
} }
} }
+1 -1
View File
@@ -203,7 +203,7 @@
$radius: 18px; $radius: 18px;
$padding: 4px; $padding: 4px;
background: rgba(colors.$bg-primary, .5); background: color.scale($color: colors.$bg-primary, $lightness: -25%);
border-radius: $radius; border-radius: $radius;
padding: $padding; padding: $padding;
min-height: 40px; min-height: 40px;
+24 -27
View File
@@ -4,6 +4,7 @@ import { userData } from "../config";
import GObject, { getter, gtype, property, register, setter } from "ags/gobject"; import GObject, { getter, gtype, property, register, setter } from "ags/gobject";
import AstalBluetooth from "gi://AstalBluetooth"; import AstalBluetooth from "gi://AstalBluetooth";
import { createScopedConnection } from "gnim-utils";
/** AstalBluetooth helper (implements the default adapter feature) */ /** AstalBluetooth helper (implements the default adapter feature) */
@@ -62,7 +63,7 @@ export class Bluetooth extends GObject.Object {
constructor() { constructor() {
super(); super();
createRoot((_) => { createRoot(async () => {
this.#scope = getScope(); this.#scope = getScope();
if(this.astalBl.adapters.length > 0) { if(this.astalBl.adapters.length > 0) {
@@ -77,37 +78,33 @@ export class Bluetooth extends GObject.Object {
if(dataDefaultAdapter !== undefined && foundAdapter !== undefined) if(dataDefaultAdapter !== undefined && foundAdapter !== undefined)
this.adapter = foundAdapter; this.adapter = foundAdapter;
this.#connections.set( createScopedConnection(AstalBluetooth.get_default(), "adapter-added", (adapter) => {
AstalBluetooth.get_default(), [ if(this.astalBl.adapters.length === 1) // adapter was just added
AstalBluetooth.get_default().connect("adapter-added", (self, adapter) => { this.adapter = adapter;
if(self.adapters.length === 1) // adapter was just added });
this.adapter = adapter; createScopedConnection(AstalBluetooth.get_default(), "adapter-removed", (adapter) => {
}), if(this.astalBl.adapters.length < 1) {
AstalBluetooth.get_default().connect("adapter-removed", (self, adapter) => { this.adapter = null;
if(self.adapters.length < 1) { this.#isAvailable = false;
this.adapter = null; this.notify("is-available");
this.#isAvailable = false; }
this.notify("is-available");
}
if(this.#adapter?.address !== adapter.address) if(this.#adapter?.address !== adapter.address)
return; return;
// the removed adapter was the default // the removed adapter was the default
if(self.adapters.length < 1) { if(this.astalBl.adapters.length < 1) {
this.adapter = null; this.adapter = null;
this.#isAvailable = false; this.#isAvailable = false;
this.notify("is-available"); this.notify("is-available");
return; return;
} }
this.#adapter = self.adapters[0];
})
]
);
this.#adapter = this.astalBl.adapters[0];
});
// async to prevent slow start // async to prevent slow start
setTimeout(() => { setTimeout(() => {
this.#lastDevice = this.getLastConnectedDevice(); this.#lastDevice = this.getLastConnectedDevice();
+3 -5
View File
@@ -2,12 +2,7 @@ import { createPoll } from "ags/time";
import { exec, execAsync } from "ags/process"; import { exec, execAsync } from "ags/process";
import { Astal, Gtk } from "ags/gtk4"; import { Astal, Gtk } from "ags/gtk4";
import { getSymbolicIcon } from "./apps"; import { getSymbolicIcon } from "./apps";
import GLib from "gi://GLib?version=2.0";
import Gio from "gi://Gio?version=2.0";
export { export {
type JSXNode as WidgetNodeType,
toBoolean as variableToBoolean, toBoolean as variableToBoolean,
construct, construct,
transform, transform,
@@ -19,6 +14,9 @@ export {
createSecureAccessorBinding as secureBaseBinding, createSecureAccessorBinding as secureBaseBinding,
} from "gnim-utils"; } from "gnim-utils";
import GLib from "gi://GLib?version=2.0";
import Gio from "gi://Gio?version=2.0";
export const decoder = new TextDecoder("utf-8"), export const decoder = new TextDecoder("utf-8"),
encoder = new TextEncoder(); encoder = new TextEncoder();
+3 -3
View File
@@ -3,8 +3,8 @@ import { Windows } from "../windows";
import { getPopupWindowContainer, PopupWindow } from "./PopupWindow"; import { getPopupWindowContainer, PopupWindow } from "./PopupWindow";
import { Separator } from "./Separator"; import { Separator } from "./Separator";
import { tr } from "../i18n/intl"; import { tr } from "../i18n/intl";
import { Accessor } from "ags"; import { Accessor, Node } from "ags";
import { transformWidget, variableToBoolean, WidgetNodeType } from "../modules/utils"; import { transformWidget, variableToBoolean } from "../modules/utils";
export type CustomDialogProps = { export type CustomDialogProps = {
@@ -16,7 +16,7 @@ export type CustomDialogProps = {
heightRequest?: number | Accessor<number>; heightRequest?: number | Accessor<number>;
widthRequest?: number | Accessor<number>; widthRequest?: number | Accessor<number>;
childOrientation?: Gtk.Orientation | Accessor<Gtk.Orientation>; childOrientation?: Gtk.Orientation | Accessor<Gtk.Orientation>;
children?: WidgetNodeType; children?: Node;
onFinish?: () => void; onFinish?: () => void;
options?: Array<CustomDialogOption> | Accessor<Array<CustomDialogOption>>; options?: Array<CustomDialogOption> | Accessor<Array<CustomDialogOption>>;
optionsOrientation?: Gtk.Orientation | Accessor<Gtk.Orientation>; optionsOrientation?: Gtk.Orientation | Accessor<Gtk.Orientation>;
+1 -6
View File
@@ -8,12 +8,7 @@ import { generalConfig } from "../../../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 ${wins.includes("center-window") ? "open" : ""}`)} `clock ${wins.includes("center-window") ? "open" : ""}`)}
$={(self) => { onClicked={() => Windows.getDefault().toggle("center-window")}
const conns: Array<number> = [
self.connect("clicked", (_) => Windows.getDefault().toggle("center-window")),
self.connect("destroy", (_) => conns.forEach(id => self.disconnect(id)))
];
}}
label={time((dt) => dt.format( label={time((dt) => dt.format(
generalConfig.getProperty("clock.date_format", "string")) generalConfig.getProperty("clock.date_format", "string"))
?? "An error occurred" ?? "An error occurred"
+94 -69
View File
@@ -1,24 +1,13 @@
import { Gtk } from "ags/gtk4"; import { Gtk } from "ags/gtk4";
import { Separator } from "../../../widget/Separator"; import { Separator } from "../../../widget/Separator";
import { Accessor, createRoot } from "ags"; import { Accessor, createBinding, createRoot, For, Node } from "ags";
import { transformWidget, variableToBoolean, WidgetNodeType } from "../../../modules/utils"; import { gtype, property, register } from "ags/gobject";
import { variableToBoolean } from "../../../modules/utils";
import Pango from "gi://Pango?version=1.0"; import Pango from "gi://Pango?version=1.0";
import GObject from "gi://GObject?version=2.0";
export type PageProps = {
id: string;
title: string;
description?: string;
$?: (self: Gtk.Box) => void;
headerButtons?: Array<HeaderButton> | Accessor<Array<HeaderButton>>;
bottomButtons?: Array<BottomButton> | Accessor<Array<BottomButton>>;
orientation?: Gtk.Orientation | Accessor<Gtk.Orientation>;
spacing?: number | Accessor<number>;
content: () => WidgetNodeType;
actionClosed?: () => void;
};
export type BottomButton = { export type BottomButton = {
title: string | Accessor<string>; title: string | Accessor<string>;
description?: string | Accessor<string>; description?: string | Accessor<string>;
@@ -35,93 +24,129 @@ export type HeaderButton = {
actionClicked?: () => void; actionClicked?: () => void;
}; };
export class Page { @register({ GTypeName: "Page" })
#title: string; export class Page extends GObject.Object {
#description?: string; readonly #id: string;
#orientation: Gtk.Orientation|Accessor< readonly #create: () => Node;
Gtk.Orientation> = Gtk.Orientation.VERTICAL;
#spacing: number|Accessor<number> = 4;
#headerButtons?: Array<HeaderButton>|Accessor<Array<HeaderButton>>;
#bottomButtons?: Array<BottomButton>|Accessor<Array<BottomButton>>;
#setup?: (self: Gtk.Box) => void;
readonly #id?: string;
readonly #create: () => WidgetNodeType;
public get id() { return this.#id; }
public get title() { return this.#title; }
public get description() { return this.#description; }
public get headerButtons() { return this.#headerButtons; }
public get bottomButtons() { return this.#bottomButtons; }
public readonly actionClosed?: () => void; public readonly actionClosed?: () => void;
public readonly actionOpen?: () => void;
public get id() { return this.#id; }
@property(String)
title: string;
@property(gtype<string|null>(String))
description: string|null = null;
@property(gtype<Gtk.Orientation>(Number))
orientation: Gtk.Orientation = Gtk.Orientation.VERTICAL;
@property(Number)
spacing: number = 4;
@property(Array<HeaderButton>)
headerButtons: Array<HeaderButton> = [];
@property(Array<BottomButton>)
bottomButtons: Array<BottomButton> = [];
constructor(props: {
id: string;
title: string;
description?: string;
headerButtons?: Array<HeaderButton>;
bottomButtons?: Array<BottomButton>;
orientation?: Gtk.Orientation;
spacing?: number;
content: () => Node;
actionOpen?: () => void;
actionClosed?: () => void;
}) {
super();
constructor(props: PageProps) {
this.#id = props.id; this.#id = props.id;
this.#title = props.title;
this.#description = props.description;
this.#create = props.content; this.#create = props.content;
this.title = props.title;
this.actionClosed = props.actionClosed; this.actionClosed = props.actionClosed;
this.actionOpen = props.actionOpen;
if(props.orientation != null) if(props.orientation != null)
this.#orientation = props.orientation; this.orientation = props.orientation;
if(props.description != null)
this.description = props.description;
if(props.spacing != null) if(props.spacing != null)
this.#spacing = props.spacing; this.spacing = props.spacing;
if(props.headerButtons != null) if(props.headerButtons != null)
this.#headerButtons = props.headerButtons; this.headerButtons = props.headerButtons;
if(props.$ != null) if(props.bottomButtons != null)
this.#setup = props.$; this.bottomButtons = props.bottomButtons;
if(props.actionOpen != null)
this.actionOpen = props.actionOpen;
if(props.actionClosed != null)
this.actionClosed = props.actionClosed;
} }
public create(): Gtk.Box { public create(): Gtk.Box {
return createRoot((dispose) => return createRoot((dispose) =>
<Gtk.Box hexpand class={`page container ${this.#id ?? ""}`} cssName={"page"} name={"page"} <Gtk.Box hexpand class={`page container ${this.#id ?? ""}`} cssName={"page"} name={"page"}
orientation={Gtk.Orientation.VERTICAL} onUnmap={() => dispose()} orientation={Gtk.Orientation.VERTICAL}
$={this.#setup}> onDestroy={() => dispose()}>
<Gtk.Box class={"header"} orientation={Gtk.Orientation.VERTICAL}> <Gtk.Box class={"header"} orientation={Gtk.Orientation.VERTICAL}>
<Gtk.Box class={"top"} hexpand> <Gtk.Box class={"top"} hexpand>
<Gtk.Box orientation={Gtk.Orientation.VERTICAL} hexpand> <Gtk.Box orientation={Gtk.Orientation.VERTICAL} hexpand>
<Gtk.Label class={"title"} label={this.#title} xalign={0} <Gtk.Label class={"title"} label={createBinding(this, "title")} xalign={0}
ellipsize={Pango.EllipsizeMode.END} /> ellipsize={Pango.EllipsizeMode.END} />
<Gtk.Label class={"description"} label={this.#description} <Gtk.Label class={"description"} label={createBinding(this, "description").as(desc =>
xalign={0} ellipsize={Pango.EllipsizeMode.END} desc ?? ""
visible={variableToBoolean(this.#description)} /> )} xalign={0} ellipsize={Pango.EllipsizeMode.END}
visible={variableToBoolean(createBinding(this, "description"))} />
</Gtk.Box> </Gtk.Box>
<Gtk.Box class={"button-row"} visible={variableToBoolean(this.#headerButtons)} <Gtk.Box class={"button-row"} visible={variableToBoolean(
hexpand={false}> createBinding(this, "headerButtons")
)} hexpand={false}>
{this.#headerButtons && transformWidget(this.#headerButtons, (button) => <For each={createBinding(this, "headerButtons")}>
<Gtk.Button class={"header-button"} label={button.label} {(button: HeaderButton) =>
iconName={button.icon} onClicked={() => button.actionClicked?.()} <Gtk.Button class={"header-button"} label={button.label}
tooltipText={button.tooltipText} tooltipMarkup={button.tooltipMarkup} iconName={button.icon} onClicked={() => button.actionClicked?.()}
/> tooltipText={button.tooltipText} tooltipMarkup={button.tooltipMarkup}
)} />
}
</For>
</Gtk.Box> </Gtk.Box>
</Gtk.Box> </Gtk.Box>
</Gtk.Box> </Gtk.Box>
<Gtk.Box class={"content"} hexpand={false} orientation={this.#orientation} <Gtk.Box class={"content"} hexpand={false} orientation={createBinding(this, "orientation")}
spacing={this.#spacing}> spacing={createBinding(this, "spacing")}>
{this.#create()} {this.#create()}
</Gtk.Box> </Gtk.Box>
<Separator alpha={.2} spacing={6} orientation={Gtk.Orientation.VERTICAL} <Separator alpha={.2} spacing={6} orientation={Gtk.Orientation.VERTICAL}
visible={variableToBoolean(this.#bottomButtons)} visible={variableToBoolean(createBinding(this, "bottomButtons"))}
/> />
<Gtk.Box class={"bottom-buttons"} orientation={Gtk.Orientation.VERTICAL} <Gtk.Box class={"bottom-buttons"} orientation={Gtk.Orientation.VERTICAL}
visible={variableToBoolean(this.#bottomButtons)} spacing={2}> visible={variableToBoolean(createBinding(this, "bottomButtons"))} spacing={2}>
{this.#bottomButtons && transformWidget(this.#bottomButtons, (button) => <For each={createBinding(this, "bottomButtons")}>
<Gtk.Button onClicked={() => button?.actionClicked?.()} tooltipText={button?.tooltipText} {(button: BottomButton) =>
tooltipMarkup={button?.tooltipMarkup}> <PageButton actionClicked={() => button.actionClicked?.()}
tooltipText={button.tooltipText}
<Gtk.Label class={"title"} label={button?.title} xalign={0} /> tooltipMarkup={button.tooltipMarkup}
<Gtk.Label class={"description"} label={button?.description} title={button.title}
xalign={0} visible={variableToBoolean(button?.description)} /> description={button.description}
</Gtk.Button> />
)} }
</For>
</Gtk.Box> </Gtk.Box>
</Gtk.Box> as Gtk.Box </Gtk.Box> as Gtk.Box
); );
@@ -136,9 +161,9 @@ export function PageButton({ onUnmap, ...props }: {
class?: string | Accessor<string>; class?: string | Accessor<string>;
icon?: string | Accessor<string>; icon?: string | Accessor<string>;
title: string | Accessor<string>; title: string | Accessor<string>;
endWidget?: WidgetNodeType; endWidget?: Node;
description?: string | Accessor<string>; description?: string | Accessor<string>;
extraButtons?: Array<WidgetNodeType> | WidgetNodeType; extraButtons?: Node;
maxWidthChars?: number | Accessor<number>; maxWidthChars?: number | Accessor<number>;
onUnmap?: (self: Gtk.Box) => void; onUnmap?: (self: Gtk.Box) => void;
actionClicked?: (self: Gtk.Button) => void; actionClicked?: (self: Gtk.Button) => void;
@@ -170,7 +195,7 @@ export function PageButton({ onUnmap, ...props }: {
</Gtk.Button> </Gtk.Button>
<Gtk.Box class={"extra-buttons"} visible={variableToBoolean(props.extraButtons)}> <Gtk.Box class={"extra-buttons"} visible={variableToBoolean(props.extraButtons)}>
{props.extraButtons} {props.extraButtons as Node}
</Gtk.Box> </Gtk.Box>
</Gtk.Box> as Gtk.Box; </Gtk.Box> as Gtk.Box;
} }
@@ -7,11 +7,11 @@ import { addSliderMarksFromMinMax } from "../../../../modules/utils";
import { userData } from "../../../../config"; import { userData } from "../../../../config";
export const PageBacklight = new Page({ export const PageBacklight = <Page
id: "backlight", id={"backlight"}
title: tr("control_center.pages.backlight.title"), title={tr("control_center.pages.backlight.title")}
description: tr("control_center.pages.backlight.description"), description={tr("control_center.pages.backlight.description")}
$: () => { actionOpen={() => {
const dataDefaultBacklight = userData.getProperty("control_center.default_backlight", "any"); const dataDefaultBacklight = userData.getProperty("control_center.default_backlight", "any");
if(typeof dataDefaultBacklight === "string" && if(typeof dataDefaultBacklight === "string" &&
Backlights.getDefault().default?.name !== dataDefaultBacklight) { Backlights.getDefault().default?.name !== dataDefaultBacklight) {
@@ -21,8 +21,8 @@ export const PageBacklight = new Page({
Backlights.getDefault().setDefault(bk); Backlights.getDefault().setDefault(bk);
} }
}, }}
content: () => ( content={() => (
<With value={createBinding(Backlights.getDefault(), "backlights")}> <With value={createBinding(Backlights.getDefault(), "backlights")}>
{(bklights: Array<Backlights.Backlight>) => bklights.length > 0 && {(bklights: Array<Backlights.Backlight>) => bklights.length > 0 &&
<Gtk.Box orientation={Gtk.Orientation.VERTICAL} spacing={4}> <Gtk.Box orientation={Gtk.Orientation.VERTICAL} spacing={4}>
@@ -75,10 +75,10 @@ export const PageBacklight = new Page({
</Gtk.Box> </Gtk.Box>
} }
</With> </With>
), )}
headerButtons: [{ headerButtons={[{
icon: "arrow-circular-top-right", icon: "arrow-circular-top-right",
tooltipText: tr("control_center.pages.backlight.refresh"), tooltipText: tr("control_center.pages.backlight.refresh"),
actionClicked: () => Backlights.getDefault().scan() actionClicked: () => Backlights.getDefault().scan()
}] }]}
}); /> as Page;
@@ -5,6 +5,7 @@ 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 { createBinding, createComputed, For, With } from "ags"; import { createBinding, createComputed, For, With } from "ags";
import { variableToBoolean } from "../../../../modules/utils";
import { Bluetooth } from "../../../../modules/bluetooth"; import { Bluetooth } from "../../../../modules/bluetooth";
import AstalNotifd from "gi://AstalNotifd"; import AstalNotifd from "gi://AstalNotifd";
@@ -13,12 +14,12 @@ import Adw from "gi://Adw?version=1";
import Gio from "gi://Gio?version=2.0"; import Gio from "gi://Gio?version=2.0";
export const BluetoothPage = new Page({ export const BluetoothPage = <Page
id: "bluetooth", id={"bluetooth"}
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: createBinding(Bluetooth.getDefault(), "adapter").as(adapter => adapter ? [{ headerButtons={createBinding(Bluetooth.getDefault(), "adapter").as(adapter => adapter ? [{
icon: createBinding(adapter, "discovering") icon: createBinding(adapter, "discovering")
.as(discovering => !discovering ? .as(discovering => !discovering ?
"arrow-circular-top-right-symbolic" "arrow-circular-top-right-symbolic"
@@ -36,20 +37,25 @@ export const BluetoothPage = new Page({
adapter.start_discovery(); adapter.start_discovery();
} }
}]: []), }]: [])}
actionClosed: () => Bluetooth.getDefault().adapter?.discovering && actionClosed={() => Bluetooth.getDefault().adapter?.discovering &&
Bluetooth.getDefault().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: () => {
Windows.getDefault().close("control-center"); Windows.getDefault().close("control-center");
execApp("overskride", "[float; animation slide right]"); execApp("overskride", "[float; animation slide right]");
} }
}], }]}
content: () => { content={() => {
const adapter = createBinding(Bluetooth.getDefault(), "adapter");
const adapters = createBinding(AstalBluetooth.get_default(), "adapters"); const adapters = createBinding(AstalBluetooth.get_default(), "adapters");
const devices = createBinding(AstalBluetooth.get_default(), "devices"); const devices = createBinding(AstalBluetooth.get_default(), "devices");
const knownDevices = devices.as(devs => devs.filter(dev =>
dev.trusted || dev.paired || dev.connected
).sort(dev => dev.connected ? 1 : 0));
const discoveredDevices = devices.as(devs => devs.filter(dev =>
!dev.trusted && !dev.paired && !dev.connected)
);
return [ return [
<Gtk.Box class={"adapters"} visible={adapters.as(adptrs => adptrs.length > 1) <Gtk.Box class={"adapters"} visible={adapters.as(adptrs => adptrs.length > 1)
@@ -86,40 +92,26 @@ export const BluetoothPage = new Page({
spacing={2}> spacing={2}>
<Gtk.Box class={"paired"} orientation={Gtk.Orientation.VERTICAL} spacing={4} <Gtk.Box class={"paired"} orientation={Gtk.Orientation.VERTICAL} spacing={4}
visible={devices.as(devs => devs.filter(dev => visible={variableToBoolean(knownDevices)}>
(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} /> <Gtk.Label class={"sub-header"} label={tr("devices")} xalign={0} />
<For each={devices.as(devs => devs.filter(dev => <For each={knownDevices}>
(dev.adapter as AstalBluetooth.Adapter).address === adapter.get()?.address &&
dev.paired || dev.connected || dev.trusted))
}>
{(dev: AstalBluetooth.Device) => <DeviceWidget device={dev} />} {(dev: AstalBluetooth.Device) => <DeviceWidget device={dev} />}
</For> </For>
</Gtk.Box> </Gtk.Box>
<Gtk.Box class={"discovered"} orientation={Gtk.Orientation.VERTICAL} spacing={4} <Gtk.Box class={"discovered"} orientation={Gtk.Orientation.VERTICAL} spacing={4}
visible={devices.as(devs => devs.filter(dev => visible={variableToBoolean(discoveredDevices)}>
(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")} <Gtk.Label class={"sub-header"} label={tr("control_center.pages.bluetooth.new_devices")}
xalign={0} /> xalign={0} />
<For each={devices.as(devs => devs.filter(dev => <For each={discoveredDevices}>
(dev.adapter as AstalBluetooth.Adapter).address === adapter.get()?.address &&
!dev.connected && !dev.paired && !dev.trusted))
}>
{(dev: AstalBluetooth.Device) => <DeviceWidget device={dev} />} {(dev: AstalBluetooth.Device) => <DeviceWidget device={dev} />}
</For> </For>
</Gtk.Box> </Gtk.Box>
</Gtk.Box> </Gtk.Box>
]; ];
} }}
}); /> as Page;
function DeviceWidget({ device }: { device: AstalBluetooth.Device }): Gtk.Widget { function DeviceWidget({ device }: { device: AstalBluetooth.Device }): Gtk.Widget {
const pair = async () => { const pair = async () => {
@@ -8,11 +8,11 @@ import { lookupIcon } from "../../../../modules/apps";
import AstalWp from "gi://AstalWp?version=0.1"; import AstalWp from "gi://AstalWp?version=0.1";
export const PageMicrophone = new Page({ export const PageMicrophone = <Page
id: "microphone", id={"microphone"}
title: tr("control_center.pages.microphone.title"), title={tr("control_center.pages.microphone.title")}
description: tr("control_center.pages.microphone.description"), description={tr("control_center.pages.microphone.description")}
content: () => [ content={() => [
<Gtk.Label class={"sub-header"} label={tr("devices")} xalign={0} />, <Gtk.Label class={"sub-header"} label={tr("devices")} xalign={0} />,
<Gtk.Box orientation={Gtk.Orientation.VERTICAL} spacing={4}> <Gtk.Box orientation={Gtk.Orientation.VERTICAL} spacing={4}>
<For each={createBinding(Wireplumber.getWireplumber().get_audio()!, "microphones")}> <For each={createBinding(Wireplumber.getWireplumber().get_audio()!, "microphones")}>
@@ -30,5 +30,5 @@ export const PageMicrophone = new Page({
/>} />}
</For> </For>
</Gtk.Box> </Gtk.Box>
] ]}
}); /> as Page;
@@ -13,24 +13,24 @@ import NM from "gi://NM";
import AstalNetwork from "gi://AstalNetwork"; import AstalNetwork from "gi://AstalNetwork";
export const PageNetwork = new Page({ export const PageNetwork = <Page
id: "network", id={"network"}
title: tr("control_center.pages.network.title"), title={tr("control_center.pages.network.title")}
headerButtons: createBinding(AstalNetwork.get_default(), "primary").as(primary => headerButtons={createBinding(AstalNetwork.get_default(), "primary").as(primary =>
primary === AstalNetwork.Primary.WIFI ? [{ primary === AstalNetwork.Primary.WIFI ? [{
icon: "arrow-circular-top-right-symbolic", icon: "arrow-circular-top-right-symbolic",
tooltipText: "Re-scan networks", tooltipText: "Re-scan networks",
actionClicked: () => AstalNetwork.get_default().wifi.scan() actionClicked: () => AstalNetwork.get_default().wifi.scan()
}] : [] }] : []
), )}
bottomButtons: [{ bottomButtons={[{
title: tr("control_center.pages.more_settings"), title: tr("control_center.pages.more_settings"),
actionClicked: () => { actionClicked: () => {
Windows.getDefault().close("control-center"); Windows.getDefault().close("control-center");
execApp("nm-connection-editor", "[animationstyle gnomed]"); execApp("nm-connection-editor", "[animationstyle gnomed]");
} }
}], }]}
content: () => [ content={() => [
<Gtk.Box class={"devices"} hexpand orientation={Gtk.Orientation.VERTICAL} <Gtk.Box class={"devices"} hexpand orientation={Gtk.Orientation.VERTICAL}
visible={variableToBoolean(createBinding(AstalNetwork.get_default().client, "devices"))} visible={variableToBoolean(createBinding(AstalNetwork.get_default().client, "devices"))}
spacing={4}> spacing={4}>
@@ -130,8 +130,8 @@ export const PageNetwork = new Page({
</For> </For>
</Gtk.Box>} </Gtk.Box>}
</With> </With>
] ]}
}); /> as Page;
function activateWirelessConnection(connection: NM.RemoteConnection, ssid: string): void { function activateWirelessConnection(connection: NM.RemoteConnection, ssid: string): void {
AstalNetwork.get_default().get_client().activate_connection_async( AstalNetwork.get_default().get_client().activate_connection_async(
@@ -5,11 +5,12 @@ import { Astal, Gtk } from "ags/gtk4";
import { addSliderMarksFromMinMax } from "../../../../modules/utils"; import { addSliderMarksFromMinMax } from "../../../../modules/utils";
import { createBinding } from "ags"; import { createBinding } from "ags";
export const PageNightLight = new Page({
id: "night-light", export const PageNightLight = <Page
title: tr("control_center.pages.night_light.title"), id={"night-light"}
description: tr("control_center.pages.night_light.description"), title={tr("control_center.pages.night_light.title")}
content: () => [ description={tr("control_center.pages.night_light.description")}
content={() => [
<Gtk.Label class={"sub-header"} label={tr( <Gtk.Label class={"sub-header"} label={tr(
"control_center.pages.night_light.temperature" "control_center.pages.night_light.temperature"
)} xalign={0} />, )} xalign={0} />,
@@ -39,5 +40,5 @@ export const PageNightLight = new Page({
NightLight.getDefault().gamma = Math.floor(value) NightLight.getDefault().gamma = Math.floor(value)
}} }}
/> />
] ]}
}); /> as Page;
@@ -4,18 +4,18 @@ import { getAppIcon, lookupIcon } from "../../../../modules/apps";
import { Wireplumber } from "../../../../modules/volume"; import { Wireplumber } from "../../../../modules/volume";
import { tr } from "../../../../i18n/intl"; import { tr } from "../../../../i18n/intl";
import { createBinding, For } from "ags"; import { createBinding, For } from "ags";
import { variableToBoolean } from "../../../../modules/utils"; import { createScopedConnection, variableToBoolean } from "../../../../modules/utils";
import AstalWp from "gi://AstalWp"; import AstalWp from "gi://AstalWp";
import GObject from "gi://GObject?version=2.0"; import GObject from "gi://GObject?version=2.0";
import Pango from "gi://Pango?version=1.0"; import Pango from "gi://Pango?version=1.0";
export const PageSound = new Page({ export const PageSound = <Page
id: "sound", id={"sound"}
title: tr("control_center.pages.sound.title"), title={tr("control_center.pages.sound.title")}
description: tr("control_center.pages.sound.description"), description={tr("control_center.pages.sound.description")}
content: () => [ content={() => [
<Gtk.Label class={"sub-header"} label={tr("devices")} xalign={0} />, <Gtk.Label class={"sub-header"} label={tr("devices")} xalign={0} />,
<Gtk.Box orientation={Gtk.Orientation.VERTICAL} spacing={4}> <Gtk.Box orientation={Gtk.Orientation.VERTICAL} spacing={4}>
<For each={createBinding(Wireplumber.getWireplumber().audio!, "speakers")}> <For each={createBinding(Wireplumber.getWireplumber().audio!, "speakers")}>
@@ -45,27 +45,17 @@ export const PageSound = new Page({
<For each={createBinding(Wireplumber.getWireplumber().audio!, "streams")}> <For each={createBinding(Wireplumber.getWireplumber().audio!, "streams")}>
{(stream: AstalWp.Stream) => {(stream: AstalWp.Stream) =>
<Gtk.Box hexpand $={(self) => { <Gtk.Box hexpand $={(self) => {
const conns: Map<GObject.Object, Array<number>> = new Map();
const controllerMotion = Gtk.EventControllerMotion.new(); const controllerMotion = Gtk.EventControllerMotion.new();
self.add_controller(controllerMotion); self.add_controller(controllerMotion);
createScopedConnection(controllerMotion, "enter", () => {
conns.set(controllerMotion, [ const revealer = self.get_last_child()!.get_first_child() as Gtk.Revealer;
controllerMotion.connect("enter", () => { revealer.set_reveal_child(true);
const revealer = self.get_last_child()!.get_first_child() as Gtk.Revealer; });
revealer.set_reveal_child(true); createScopedConnection(controllerMotion, "leave", () => {
}), const revealer = self.get_last_child()!.get_first_child() as Gtk.Revealer;
controllerMotion.connect("leave", () => { revealer.set_reveal_child(false);
const revealer = self.get_last_child()!.get_first_child() as Gtk.Revealer; });
revealer.set_reveal_child(false);
})
]);
conns.set(self, [
self.connect("destroy", () => conns.forEach((ids, obj) =>
ids.forEach(id => obj.disconnect(id))
))
]);
}}> }}>
<Gtk.Image iconName={createBinding(stream, "name").as(name => <Gtk.Image iconName={createBinding(stream, "name").as(name =>
getAppIcon(name.split(' ')[0]) ?? "application-x-executable-symbolic")} getAppIcon(name.split(' ')[0]) ?? "application-x-executable-symbolic")}
@@ -92,5 +82,5 @@ export const PageSound = new Page({
} }
</For> </For>
</Gtk.Box> </Gtk.Box>
] ]}
}); /> as Page;
@@ -1,26 +1,27 @@
import { register } from "ags/gobject"; import GObject, { getter, gtype, register } from "ags/gobject";
import { Gtk } from "ags/gtk4"; import { Gtk } from "ags/gtk4";
import { Page } from "../Page"; import { Page } from "../Page";
import GLib from "gi://GLib?version=2.0"; import GLib from "gi://GLib?version=2.0";
export type PagesProps = {
initialPage?: Page;
transitionDuration?: number;
};
@register({ GTypeName: "Pages" }) @register({ GTypeName: "Pages" })
export class Pages extends Gtk.Box { export class Pages extends Gtk.Box {
#timeouts: Array<[GLib.Source, (() => void)|undefined]> = []; #timeouts: Array<[GLib.Source, (() => void)|undefined]> = [];
#page: (Page|undefined); #page: Page|undefined;
#transDuration: number; #transDuration: number;
#transType: Gtk.RevealerTransitionType = Gtk.RevealerTransitionType.SLIDE_DOWN; #transType: Gtk.RevealerTransitionType = Gtk.RevealerTransitionType.SLIDE_DOWN;
@getter(Boolean)
get isOpen() { return Boolean(this.#page); } get isOpen() { return Boolean(this.#page); }
@getter(gtype<Page|undefined>(Page))
get page() { return this.#page; } get page() { return this.#page; }
constructor(props?: PagesProps) { constructor(props?: {
initialPage?: Page;
transitionDuration?: number;
}) {
super({ super({
orientation: Gtk.Orientation.VERTICAL, orientation: Gtk.Orientation.VERTICAL,
cssName: "pages", cssName: "pages",
@@ -36,7 +37,9 @@ export class Pages extends Gtk.Box {
const destroyId = this.connect("destroy", () => { const destroyId = this.connect("destroy", () => {
this.disconnect(destroyId); GObject.signal_handler_is_connected(this, destroyId) &&
this.disconnect(destroyId);
this.#timeouts.forEach((tmout) => { this.#timeouts.forEach((tmout) => {
tmout[0].destroy(); tmout[0].destroy();
(async () => tmout[1]?.())().catch((err: Error) => { (async () => tmout[1]?.())().catch((err: Error) => {
@@ -9,12 +9,16 @@ import AstalBluetooth from "gi://AstalBluetooth";
export const TileBluetooth = () => export const TileBluetooth = () =>
<Tile title={"Bluetooth"} visible={createBinding(Bluetooth.getDefault(), "isAvailable")} <Tile title={"Bluetooth"} visible={createBinding(Bluetooth.getDefault(), "isAvailable")}
description={createBinding(AstalBluetooth.get_default(), "adapters").as((connected) => { description={createComputed([
if(!connected) return ""; createBinding(Bluetooth.getDefault(), "adapter"),
createBinding(AstalBluetooth.get_default(), "devices")
], (adapter, devices) => {
const lastConnectedDevice = devices.filter(d => d.connected)[devices.length - 1];
const connectedDevs = AstalBluetooth.get_default().devices.filter(dev => dev.connected); if(!adapter || !lastConnectedDevice)
const connectedDev = connectedDevs[connectedDevs.length - 1]; // last connected device is on display return "";
return connectedDev ? connectedDev.get_alias() : ""
return lastConnectedDevice.alias;
})} })}
onEnabled={() => Bluetooth.getDefault().adapter?.set_powered(true)} onEnabled={() => Bluetooth.getDefault().adapter?.set_powered(true)}
onDisabled={() => Bluetooth.getDefault().adapter?.set_powered(false)} onDisabled={() => Bluetooth.getDefault().adapter?.set_powered(false)}
@@ -5,6 +5,7 @@ import { TileDND } from "./DoNotDisturb";
import { TileRecording } from "./Recording"; import { TileRecording } from "./Recording";
import { TileNightLight } from "./NightLight"; import { TileNightLight } from "./NightLight";
import { Pages } from "../pages"; import { Pages } from "../pages";
import { createRoot, getScope } from "ags";
export let TilesPages: Pages|undefined; export let TilesPages: Pages|undefined;
@@ -17,16 +18,20 @@ export const tileList: Array<() => JSX.Element|Gtk.Widget> = [
] as Array<() => Gtk.Widget>; ] as Array<() => Gtk.Widget>;
export function Tiles(): Gtk.Widget { export function Tiles(): Gtk.Widget {
return <Gtk.Box class={"tiles-container"} orientation={Gtk.Orientation.VERTICAL} return createRoot((dispose) => {
onUnmap={() => TilesPages = undefined}> getScope().onCleanup(() => TilesPages = undefined);
<Gtk.FlowBox orientation={Gtk.Orientation.HORIZONTAL} rowSpacing={6} return <Gtk.Box class={"tiles-container"} orientation={Gtk.Orientation.VERTICAL}
columnSpacing={6} minChildrenPerLine={2} activateOnSingleClick onDestroy={() => dispose()}>
maxChildrenPerLine={2} hexpand homogeneous>
{tileList.map(t => t())} <Gtk.FlowBox orientation={Gtk.Orientation.HORIZONTAL} rowSpacing={6}
</Gtk.FlowBox> columnSpacing={6} minChildrenPerLine={2} activateOnSingleClick
maxChildrenPerLine={2} hexpand homogeneous>
<Pages class={"tile-pages"} $={(self) => TilesPages = self} /> {tileList.map(t => t())}
</Gtk.Box> as Gtk.Box; </Gtk.FlowBox>
<Pages class={"tile-pages"} $={(self) => TilesPages = self} />
</Gtk.Box> as Gtk.Box;
});
} }
+8 -2
View File
@@ -203,7 +203,10 @@ export class Windows extends GObject.Object {
this.#scope.onMount(scope.dispose); this.#scope.onMount(scope.dispose);
scope.onCleanup(() => instance.disconnect(connection)); scope.onCleanup(() =>
GObject.signal_handler_is_connected(instance, connection) &&
instance.disconnect(connection)
);
return instance; return instance;
}) })
@@ -231,7 +234,10 @@ export class Windows extends GObject.Object {
const connection = instance.connect("close-request", () => dispose()); const connection = instance.connect("close-request", () => dispose());
this.#scope.onMount(dispose) this.#scope.onMount(dispose)
scope.onCleanup(() => instance.disconnect(connection)); scope.onCleanup(() =>
GObject.signal_handler_is_connected(instance, connection) &&
instance.disconnect(connection)
);
return instance; return instance;
}); });