✨ chore: control-center and center-window widgets to gtk4 and ags v3
This commit is contained in:
@@ -1,210 +0,0 @@
|
||||
import { bind, Gio, Variable } from "astal";
|
||||
import { Gtk, Widget } from "astal/gtk3";
|
||||
import AstalBluetooth from "gi://AstalBluetooth";
|
||||
import { Page, PageButton } from "./Page";
|
||||
import { tr } from "../../../i18n/intl";
|
||||
import { Windows } from "../../../windows";
|
||||
import { Notifications } from "../../../scripts/notifications";
|
||||
import AstalNotifd from "gi://AstalNotifd";
|
||||
import { execApp } from "../../../scripts/apps";
|
||||
|
||||
export const BluetoothPage: (() => Page) = () => new Page({
|
||||
id: "bluetooth",
|
||||
title: tr("control_center.pages.bluetooth.title"),
|
||||
description: tr("control_center.pages.bluetooth.description"),
|
||||
className: "bluetooth",
|
||||
headerButtons: [
|
||||
new Widget.Button({
|
||||
className: "discover",
|
||||
image: new Widget.Icon({
|
||||
icon: bind(AstalBluetooth.get_default().adapter, "discovering").as((discovering) =>
|
||||
!discovering ?
|
||||
"arrow-circular-top-right-symbolic"
|
||||
: "media-playback-stop-symbolic")
|
||||
} as Widget.IconProps),
|
||||
tooltipText: bind(AstalBluetooth.get_default().adapter, "discovering").as((discovering) =>
|
||||
!discovering ?
|
||||
tr("control_center.pages.bluetooth.start_discovering")
|
||||
: tr("control_center.pages.bluetooth.stop_discovering")),
|
||||
onClick: () => {
|
||||
if(AstalBluetooth.get_default().adapter.discovering) {
|
||||
AstalBluetooth.get_default().adapter.stop_discovery();
|
||||
return;
|
||||
}
|
||||
|
||||
AstalBluetooth.get_default().adapter.start_discovery();
|
||||
}
|
||||
} as Widget.ButtonProps)
|
||||
],
|
||||
onClose: () => AstalBluetooth.get_default().adapter.discovering &&
|
||||
AstalBluetooth.get_default().adapter.stop_discovery(),
|
||||
bottomButtons: [{
|
||||
title: tr("control_center.pages.more_settings"),
|
||||
onClick: () => {
|
||||
Windows.close("control-center");
|
||||
execApp("overskride", "[float; animation slide right]");
|
||||
}
|
||||
}],
|
||||
spacing: 2,
|
||||
children: [
|
||||
new Widget.Box({
|
||||
className: "adapters",
|
||||
visible: bind(AstalBluetooth.get_default(), "adapters").as((adapters) =>
|
||||
adapters.length > 1),
|
||||
spacing: 2,
|
||||
children: bind(AstalBluetooth.get_default(), "adapters").as((adapters) => [
|
||||
new Widget.Label({
|
||||
className: "sub-header",
|
||||
label: tr("control_center.pages.bluetooth.adapters")
|
||||
} as Widget.LabelProps),
|
||||
...adapters.map(adapter =>
|
||||
PageButton({
|
||||
title: adapter.alias ?? "Adapter",
|
||||
icon: "bluetooth-active-symbolic",
|
||||
onClick: () => AstalBluetooth.get_default(),
|
||||
})
|
||||
)
|
||||
]
|
||||
)
|
||||
} as Widget.BoxProps),
|
||||
new Widget.Box({
|
||||
className: "connections",
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
hexpand: true,
|
||||
spacing: 2,
|
||||
children: [
|
||||
new Widget.Box({
|
||||
className: "paired",
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
spacing: 2,
|
||||
visible: bind(AstalBluetooth.get_default(), "devices").as((devs) =>
|
||||
devs.filter(dev => dev.paired || dev.connected || dev.trusted).length > 0),
|
||||
children: bind(AstalBluetooth.get_default(), "devices").as((devs) => {
|
||||
const connectedDevices = devs.filter((dev) => dev.connected || dev.paired || dev.trusted)
|
||||
|
||||
return [
|
||||
new Widget.Label({
|
||||
className: "sub-header",
|
||||
label: tr("devices"),
|
||||
xalign: 0,
|
||||
} as Widget.LabelProps),
|
||||
...connectedDevices.map((dev) => DeviceWidget(dev))
|
||||
]
|
||||
})
|
||||
} as Widget.BoxProps),
|
||||
new Widget.Box({
|
||||
className: "discovered",
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
spacing: 2,
|
||||
visible: bind(AstalBluetooth.get_default(), "devices").as((devs) =>
|
||||
devs.filter((dev) => !dev.connected && !dev.paired && !dev.trusted).length > 0),
|
||||
children: bind(AstalBluetooth.get_default(), "devices").as((devices) => {
|
||||
const discoveredDevices = devices.filter((dev) => !dev.connected && !dev.paired && !dev.trusted);
|
||||
|
||||
return [
|
||||
new Widget.Label({
|
||||
className: "sub-header",
|
||||
label: tr("control_center.pages.bluetooth.new_devices"),
|
||||
xalign: 0
|
||||
} as Widget.LabelProps),
|
||||
...discoveredDevices.map((dev: AstalBluetooth.Device) => DeviceWidget(dev))
|
||||
]
|
||||
})
|
||||
} as Widget.BoxProps)
|
||||
]
|
||||
} as Widget.BoxProps)
|
||||
]
|
||||
});
|
||||
|
||||
function DeviceWidget(dev: AstalBluetooth.Device): Gtk.Widget {
|
||||
const devActions: Variable<Array<Widget.Button>> = Variable.derive([
|
||||
bind(dev, "connected"),
|
||||
bind(dev, "paired"),
|
||||
bind(dev, "trusted")
|
||||
], (connected, paired, trusted) => paired ? [
|
||||
new Widget.Button({
|
||||
image: new Widget.Icon({
|
||||
icon: connected ?
|
||||
"list-remove-symbolic"
|
||||
: "user-trash-symbolic"
|
||||
} as Widget.IconProps),
|
||||
tooltipText: tr(connected ? "disconnect" : "control_center.pages.bluetooth.unpair_device"),
|
||||
onClick: () => {
|
||||
if(!connected) {
|
||||
AstalBluetooth.get_default().adapter?.remove_device(dev);
|
||||
return;
|
||||
}
|
||||
|
||||
dev.disconnect_device(null);
|
||||
},
|
||||
} as Widget.ButtonProps),
|
||||
new Widget.Button({
|
||||
image: new Widget.Icon({
|
||||
icon: trusted ?
|
||||
"shield-safe-symbolic"
|
||||
: "shield-danger-symbolic"
|
||||
} as Widget.IconProps),
|
||||
tooltipText: tr(`control_center.pages.bluetooth.${trusted ? "un": ""}trust_device`),
|
||||
onClick: () => dev.set_trusted(!trusted)
|
||||
} as Widget.ButtonProps)
|
||||
] : []);
|
||||
|
||||
return PageButton({
|
||||
className: bind(dev, "connected").as((connected) => connected ? "connected" : ""),
|
||||
title: bind(dev, "alias").as(alias => alias ?? "Unknown Device"),
|
||||
icon: dev.icon ?? "bluetooth-active-symbolic",
|
||||
description: bind(dev, "connecting").as(connecting =>
|
||||
connecting ? `${tr("connecting")}...` : ""),
|
||||
tooltipText: bind(dev, "connected").as(connected => !connected ?
|
||||
tr("connect")
|
||||
: ""),
|
||||
onDestroy: () => devActions.drop(),
|
||||
onClick: () => {
|
||||
if(dev.connected) return;
|
||||
|
||||
let skipConnection: boolean = false;
|
||||
if(!dev.paired)
|
||||
(async () => dev.pair())().catch((err: Gio.IOErrorEnum) => {
|
||||
skipConnection = true;
|
||||
Notifications.getDefault().sendNotification({
|
||||
appName: "bluetooth",
|
||||
summary: "Device pairing error",
|
||||
body: `Couldn't connect to ${dev.alias ?? dev.name}, an error occurred: ${err.message || err.stack}`,
|
||||
urgency: AstalNotifd.Urgency.NORMAL
|
||||
})
|
||||
}).then(() => dev.set_trusted(true));
|
||||
|
||||
if(!skipConnection)
|
||||
(async () => dev.connect_device(null))().catch((err: Gio.IOErrorEnum) =>
|
||||
Notifications.getDefault().sendNotification({
|
||||
appName: "bluetooth",
|
||||
summary: "Device connection error",
|
||||
body: `Couldn't connect to ${dev.alias ?? dev.name}, an error occurred: ${err.message || err.stack}`,
|
||||
urgency: AstalNotifd.Urgency.NORMAL
|
||||
})
|
||||
);
|
||||
},
|
||||
endWidget: new Widget.Box({
|
||||
visible: bind(dev, "batteryPercentage").as((batt: number) =>
|
||||
batt <= -1 ? false : true),
|
||||
children: [
|
||||
new Widget.Box({
|
||||
visible: bind(dev, "connected"),
|
||||
children: [
|
||||
new Widget.Label({
|
||||
halign: Gtk.Align.END,
|
||||
label: bind(dev, "batteryPercentage").as((batt: number) =>
|
||||
`${Math.floor(batt * 100)}%`)
|
||||
} as Widget.LabelProps),
|
||||
new Widget.Icon({
|
||||
icon: bind(dev, "batteryPercentage").as(batt =>
|
||||
`battery-level-${Math.floor(batt * 100)}-symbolic`),
|
||||
css: "font-size: 16px; margin-left: 6px;"
|
||||
} as Widget.IconProps)
|
||||
]
|
||||
} as Widget.BoxProps)
|
||||
]
|
||||
} as Widget.BoxProps),
|
||||
extraButtons: devActions()
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import { Gtk } from "ags/gtk4";
|
||||
import { Page, PageButton } from "./Page";
|
||||
import { tr } from "../../../i18n/intl";
|
||||
import { Windows } from "../../../windows";
|
||||
import { Notifications } from "../../../scripts/notifications";
|
||||
import { execApp } from "../../../scripts/apps";
|
||||
|
||||
import AstalNotifd from "gi://AstalNotifd";
|
||||
import AstalBluetooth from "gi://AstalBluetooth";
|
||||
import { variableToBoolean } from "../../../scripts/utils";
|
||||
import { createBinding, createComputed, For, With } from "ags";
|
||||
|
||||
|
||||
export const BluetoothPage = () => <Page
|
||||
id={"bluetooth"} title={tr("control_center.pages.bluetooth.title")}
|
||||
description={tr("control_center.pages.bluetooth.description")}
|
||||
class={"bluetooth"} headerButtons={[
|
||||
<Gtk.Button class={"discover"} iconName={createBinding(
|
||||
AstalBluetooth.get_default().adapter, "discovering"
|
||||
).as(discovering => discovering ?
|
||||
"arrow-circular-top-right-symbolic"
|
||||
: "media-playback-stop-symbolic")} tooltipText={
|
||||
createBinding(AstalBluetooth.get_default().adapter, "discovering").as((discovering) =>
|
||||
!discovering ?
|
||||
tr("control_center.pages.bluetooth.start_discovering")
|
||||
: tr("control_center.pages.bluetooth.stop_discovering"))}
|
||||
onClicked={() => {
|
||||
if(AstalBluetooth.get_default().adapter.discovering) {
|
||||
AstalBluetooth.get_default().adapter.stop_discovery();
|
||||
return;
|
||||
}
|
||||
|
||||
AstalBluetooth.get_default().adapter.start_discovery();
|
||||
}}
|
||||
/>
|
||||
]}
|
||||
onClose={() => AstalBluetooth.get_default().adapter.discovering &&
|
||||
AstalBluetooth.get_default().adapter.stop_discovery()}
|
||||
bottomButtons={[{
|
||||
title: tr("control_center.pages.more_settings"),
|
||||
onClick: () => {
|
||||
Windows.getDefault().close("control-center");
|
||||
execApp("overskride", "[float; animation slide right]");
|
||||
}
|
||||
}]} spacing={2}>
|
||||
<Gtk.Box class={"adapters"} visible={variableToBoolean(createBinding(
|
||||
AstalBluetooth.get_default(), "adapters"))} spacing={2}>
|
||||
|
||||
<Gtk.Label class={"sub-header"} label={tr("control_center.pages.bluetooth.adapters")} />
|
||||
<For each={createBinding(AstalBluetooth.get_default(), "adapters")}>
|
||||
{(adapter: AstalBluetooth.Adapter) =>
|
||||
<PageButton title={adapter.alias ?? "Adapter"}
|
||||
icon={"bluetooth-active-symbolic"} />
|
||||
}
|
||||
</For>
|
||||
</Gtk.Box>
|
||||
<Gtk.Box class={"connections"} orientation={Gtk.Orientation.VERTICAL} hexpand={true}
|
||||
spacing={2}>
|
||||
|
||||
<Gtk.Box class={"paired"} orientation={Gtk.Orientation.VERTICAL} spacing={2}
|
||||
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={2}
|
||||
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} />
|
||||
<For each={createBinding(AstalBluetooth.get_default(), "devices").as(devs =>
|
||||
devs.filter(dev => !dev.connected && !dev.paired && !dev.trusted))}>
|
||||
{(dev: AstalBluetooth.Device) => <DeviceWidget device={dev} />}
|
||||
</For>
|
||||
</Gtk.Box>
|
||||
</Gtk.Box>
|
||||
</Page> as Page;
|
||||
|
||||
function DeviceWidget({ device }: { device: AstalBluetooth.Device }): Gtk.Widget {
|
||||
return <PageButton class={createBinding(device, "connected").as(conn =>
|
||||
conn ? "connected" : "")} title={
|
||||
createBinding(device, "alias").as(alias => alias ?? "Unknown Device")}
|
||||
icon={createBinding(device, "icon").as(ico => ico ?? "bluetooth-active-symbolic")}
|
||||
description={
|
||||
createBinding(device, "connecting").as(connecting =>
|
||||
connecting ? `${tr("connecting")}...` : "")}
|
||||
tooltipText={
|
||||
createBinding(device, "connected").as(connected =>
|
||||
!connected ? tr("connect") : "")
|
||||
} onClick={() => {
|
||||
if(device.connected) return;
|
||||
|
||||
let skipConnection: boolean = false;
|
||||
if(!device.paired)
|
||||
(async () => device.pair())().catch((err: Error) => {
|
||||
skipConnection = true;
|
||||
Notifications.getDefault().sendNotification({
|
||||
appName: "bluetooth",
|
||||
summary: "Device pairing error",
|
||||
body: `Couldn't connect to ${device.alias ?? device.name}, an error occurred: ${err.message || err.stack}`,
|
||||
urgency: AstalNotifd.Urgency.NORMAL
|
||||
})
|
||||
}).then(() => device.set_trusted(true));
|
||||
|
||||
if(!skipConnection)
|
||||
(async () => device.connect_device(null))().catch((err: Error) =>
|
||||
Notifications.getDefault().sendNotification({
|
||||
appName: "bluetooth",
|
||||
summary: "Device connection error",
|
||||
body: `Couldn't connect to ${device.alias ?? device.name}, an error occurred: ${err.message || err.stack}`,
|
||||
urgency: AstalNotifd.Urgency.NORMAL
|
||||
})
|
||||
);
|
||||
}}
|
||||
endWidget={<Gtk.Box visible={createComputed([
|
||||
createBinding(device, "batteryPercentage"),
|
||||
createBinding(device, "connected")
|
||||
]).as(([batt, connected]) => connected && (batt > -1))
|
||||
}>
|
||||
<Gtk.Label halign={Gtk.Align.END} label={
|
||||
createBinding(device, "batteryPercentage").as(batt =>
|
||||
`${Math.floor(batt * 100)}%`)} />
|
||||
|
||||
<Gtk.Image iconName={
|
||||
createBinding(device, "batteryPercentage").as(batt =>
|
||||
`battery-level-${Math.floor(batt * 100)}-symbolic`)
|
||||
} css={"font-size: 16px; margin-left: 6px;"} />
|
||||
</Gtk.Box>} extraButtons={<With value={createComputed([
|
||||
createBinding(device, "connected"),
|
||||
createBinding(device, "trusted")
|
||||
])}>
|
||||
{([connected, trusted]: [boolean, boolean]) => trusted &&
|
||||
<Gtk.Box class={"button-row"}>
|
||||
{<Gtk.Button iconName={connected ?
|
||||
"list-remove-symbolic"
|
||||
: "user-trash-symbolic"} tooltipText={tr(connected ?
|
||||
"disconnect"
|
||||
: "control_center.pages.bluetooth.unpair_device"
|
||||
)} onClicked={() => {
|
||||
if(!connected) {
|
||||
AstalBluetooth.get_default().adapter?.remove_device(device);
|
||||
return;
|
||||
}
|
||||
|
||||
device.disconnect_device(null);
|
||||
}} />}
|
||||
|
||||
<Gtk.Button iconName={trusted ?
|
||||
"shield-safe-symbolic"
|
||||
: "shield-danger-symbolic"} tooltipText={tr(
|
||||
`control_center.pages.bluetooth.${trusted ? "un" : ""}trust_device`
|
||||
)} onClicked={() => device.set_trusted(!trusted)}
|
||||
/>
|
||||
</Gtk.Box>}
|
||||
</With>}
|
||||
/> as Page;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { bind } from "astal";
|
||||
import { Page, PageButton, PageProps } from "./Page";
|
||||
import { Wireplumber } from "../../../scripts/volume";
|
||||
import { Astal, Widget } from "astal/gtk3";
|
||||
import { tr } from "../../../i18n/intl";
|
||||
|
||||
|
||||
export function PageMicrophone(): Page {
|
||||
return new Page({
|
||||
id: "microphone",
|
||||
title: tr("control_center.pages.microphone.title"),
|
||||
description: tr("control_center.pages.microphone.description"),
|
||||
children: bind(Wireplumber.getWireplumber().get_audio()!, "microphones").as((microphones) => [
|
||||
new Widget.Label({
|
||||
className: "sub-header",
|
||||
label: tr("devices"),
|
||||
xalign: 0
|
||||
} as Widget.LabelProps),
|
||||
...microphones.map((microphone) =>
|
||||
PageButton({
|
||||
className: bind(microphone, "isDefault").as(isDefault => isDefault ? "default" : ""),
|
||||
icon: bind(microphone, "icon").as(icon =>
|
||||
Astal.Icon.lookup_icon(icon) ? icon : "audio-input-microphone-symbolic"),
|
||||
title: bind(microphone, "description").as(desc => desc ?? "Microphone"),
|
||||
onClick: () => microphone.set_is_default(true),
|
||||
endWidget: new Widget.Icon({
|
||||
icon: "object-select-symbolic",
|
||||
visible: bind(microphone, "isDefault"),
|
||||
css: "font-size: 18px;"
|
||||
} as Widget.IconProps)
|
||||
})
|
||||
)
|
||||
])
|
||||
} as PageProps);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Page, PageButton } from "./Page";
|
||||
import { Wireplumber } from "../../../scripts/volume";
|
||||
import { Gtk } from "ags/gtk4";
|
||||
import { tr } from "../../../i18n/intl";
|
||||
import { createBinding, For } from "ags";
|
||||
import AstalWp from "gi://AstalWp?version=0.1";
|
||||
import { lookupIcon } from "../../../scripts/apps";
|
||||
|
||||
|
||||
export function PageMicrophone(): Page {
|
||||
return <Page id={"microphone"} title={tr("control_center.pages.microphone.title")}
|
||||
description={tr("control_center.pages.microphone.description")}>
|
||||
|
||||
<Gtk.Label class={"sub-header"} label={tr("devices")} xalign={0} />
|
||||
<For each={createBinding(Wireplumber.getWireplumber().get_audio()!, "microphones")}>
|
||||
{(source: AstalWp.Endpoint) => <PageButton class={
|
||||
createBinding(source, "isDefault").as(isDefault => isDefault ? "default" : "")
|
||||
} icon={createBinding(source, "icon").as(ico => lookupIcon(ico) ?
|
||||
ico : "audio-input-microphone-symbolic")} title={
|
||||
createBinding(source, "description").as(desc => desc ?? "Microphone")
|
||||
} onClick={() => !source.isDefault && source.set_is_default(true)}
|
||||
endWidget={
|
||||
<Gtk.Image iconName={"object-select-symbolic"} visible={
|
||||
createBinding(source, "isDefault")} css={"font-size: 18px;"}
|
||||
/>
|
||||
}
|
||||
/>}
|
||||
</For>
|
||||
</Page> as Page;
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
import { Gtk, Widget } from "astal/gtk3";
|
||||
import { Page, PageButton } from "./Page";
|
||||
import AstalNetwork from "gi://AstalNetwork";
|
||||
import { bind, GLib } from "astal";
|
||||
import NM from "gi://NM";
|
||||
import { Windows } from "../../../windows";
|
||||
import { tr } from "../../../i18n/intl";
|
||||
import { execApp } from "../../../scripts/apps";
|
||||
import { EntryPopup, EntryPopupProps } from "../../EntryPopup";
|
||||
import { Notifications } from "../../../scripts/notifications";
|
||||
import { AskPopup, AskPopupProps } from "../../AskPopup";
|
||||
import { encoder } from "../../../scripts/utils";
|
||||
|
||||
export const PageNetwork: (() => Page) = () => new Page({
|
||||
id: "network",
|
||||
title: tr("control_center.pages.network.title"),
|
||||
className: "network",
|
||||
headerButtons: [
|
||||
new Widget.Button({
|
||||
className: "reload",
|
||||
image: new Widget.Icon({
|
||||
icon: "arrow-circular-top-right-symbolic"
|
||||
} as Widget.IconProps),
|
||||
visible: bind(AstalNetwork.get_default(), "primary").as((primary) =>
|
||||
primary === AstalNetwork.Primary.WIFI),
|
||||
tooltipText: "Re-scan connections",
|
||||
onClick: () => AstalNetwork.get_default().wifi.scan()
|
||||
} as Widget.ButtonProps)
|
||||
],
|
||||
bottomButtons: [{
|
||||
title: tr("control_center.pages.more_settings"),
|
||||
onClick: () => {
|
||||
Windows.close("control-center");
|
||||
execApp("nm-connection-editor", "[animationstyle gnomed]");
|
||||
}
|
||||
}],
|
||||
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) => {
|
||||
devices = devices.filter(dev => dev.interface !== "lo");
|
||||
|
||||
return [
|
||||
new Widget.Label({
|
||||
label: tr("devices"),
|
||||
xalign: 0,
|
||||
className: "sub-header",
|
||||
} as Widget.LabelProps),
|
||||
...devices.filter(device => device.real).map(dev => PageButton({
|
||||
className: "device",
|
||||
icon: bind(dev, "deviceType").as(deviceType =>
|
||||
deviceType === NM.DeviceType.WIFI ?
|
||||
"network-wireless-symbolic"
|
||||
: "network-wired-symbolic"),
|
||||
title: bind(dev, "interface").as(iface => iface ??
|
||||
tr("control_center.pages.network.interface")),
|
||||
extraButtons: [
|
||||
new Widget.Button({
|
||||
image: new Widget.Icon({
|
||||
icon: "view-more-symbolic"
|
||||
} as Widget.IconProps),
|
||||
onClick: () => {
|
||||
Windows.close("control-center");
|
||||
execApp(
|
||||
`nm-connection-editor --edit ${dev.activeConnection?.connection.get_uuid()}`,
|
||||
"[animationstyle gnomed; float]"
|
||||
);
|
||||
}
|
||||
} 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, "accessPoints").as((aps) => [
|
||||
new Widget.Label({
|
||||
className: "sub-header",
|
||||
label: "Wi-Fi"
|
||||
} as Widget.LabelProps),
|
||||
...aps.filter(ap => ap.ssid).map(ap => PageButton({
|
||||
className: bind(AstalNetwork.get_default().wifi, "activeAccessPoint").as(activeAP =>
|
||||
activeAP.ssid === ap.ssid ? "active" : ""),
|
||||
title: bind(ap, "ssid").as(ssid =>
|
||||
ssid ?? "Unknown SSID"),
|
||||
icon: bind(ap, "iconName"),
|
||||
endWidget: new Widget.Icon({
|
||||
// @ts-ignore ts-for-gir generated the types wrong
|
||||
icon: bind(ap, "flags").as(flags => flags & NM["80211ApFlags" as keyof typeof NM].PRIVACY ?
|
||||
"channel-secure-symbolic"
|
||||
: "channel-insecure-symbolic"),
|
||||
css: "font-size: 18px;"
|
||||
} as Widget.IconProps),
|
||||
extraButtons: [
|
||||
new Widget.Button({
|
||||
image: new Widget.Icon({
|
||||
icon: "window-close-symbolic",
|
||||
css: "font-size: 18px;"
|
||||
} as Widget.IconProps)
|
||||
} as Widget.ButtonProps)
|
||||
],
|
||||
onClick: () => {
|
||||
const ssid: string = ap.ssid ?? "Unknown SSID",
|
||||
ssidBytes = GLib.Bytes.new(encoder.encode(ssid));
|
||||
|
||||
const connection = new NM.Connection();
|
||||
const setting = NM.SettingWireless.new();
|
||||
setting.ssid = ssidBytes;
|
||||
setting.bssid = ap.bssid;
|
||||
|
||||
connection.add_setting(setting);
|
||||
|
||||
// @ts-ignore same as previous, type gen issues
|
||||
// Check if access point has encryption(needs a password)
|
||||
if(ap.flags & NM["80211ApFlags" as keyof typeof NM].PRIVACY) {
|
||||
const passwdPopup = EntryPopup({
|
||||
isPassword: true,
|
||||
title: `${tr("connect")}: ${ssid}`,
|
||||
acceptText: tr("connect"),
|
||||
closeOnAccept: false,
|
||||
text: `Input password for ${ssid}`,
|
||||
onAccept: (input) => {
|
||||
const pskSetting = NM.SettingWirelessSecurity.new();
|
||||
pskSetting.keyMgmt = "wpa-psk";
|
||||
|
||||
// @ts-ignore type gen issues (the type exists)
|
||||
if(ap.flags & NM["80211ApSecurityFlags" as keyof typeof NM].KEY_MGMT_SAE)
|
||||
pskSetting.keyMgmt = "sae";
|
||||
|
||||
pskSetting.psk = input;
|
||||
|
||||
AstalNetwork.get_default().get_client().add_connection_async(
|
||||
connection, true, null, (client, asyncRes) => {
|
||||
const remoteConnection = client!.add_connection_finish(asyncRes);
|
||||
if(!remoteConnection) {
|
||||
notifyConnectionError(ssid);
|
||||
return;
|
||||
}
|
||||
|
||||
passwdPopup.close();
|
||||
saveToDisk(remoteConnection, ssid);
|
||||
}
|
||||
);
|
||||
},
|
||||
} as EntryPopupProps);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
AstalNetwork.get_default().get_client().add_connection_async(connection, false, null, (_, asyncRes) => {
|
||||
const remoteConnection = AstalNetwork.get_default().get_client().add_connection_finish(asyncRes);
|
||||
|
||||
if(!remoteConnection) {
|
||||
notifyConnectionError(ssid);
|
||||
return;
|
||||
}
|
||||
|
||||
activateWirelessConnection(remoteConnection, ssid);
|
||||
});
|
||||
}
|
||||
}))
|
||||
]
|
||||
) : [],
|
||||
} as Widget.BoxProps)
|
||||
]
|
||||
});
|
||||
|
||||
function activateWirelessConnection(connection: NM.RemoteConnection, ssid: string): void {
|
||||
AstalNetwork.get_default().get_client().activate_connection_async(
|
||||
connection, AstalNetwork.get_default().wifi.get_device(), null, null, (_, asyncRes) => {
|
||||
const activeConnection = AstalNetwork.get_default().get_client().activate_connection_finish(asyncRes);
|
||||
if(!activeConnection) {
|
||||
Notifications.getDefault().sendNotification({
|
||||
appName: "network",
|
||||
summary: "Couldn't activate wireless connection",
|
||||
body: `An error occurred while activating the wireless connection "${ssid}"`
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function notifyConnectionError(ssid: string): void {
|
||||
Notifications.getDefault().sendNotification({
|
||||
appName: "network",
|
||||
summary: "Coudn't connect Wi-Fi",
|
||||
body: `An error occurred while trying to connect to the "${ssid}" access point. \nMaybe the password is invalid?`
|
||||
});
|
||||
}
|
||||
function saveToDisk(remoteConnection: NM.RemoteConnection, ssid: string): void {
|
||||
AskPopup({
|
||||
text: `Save password for connection "${ssid}"?`,
|
||||
acceptText: "Yes",
|
||||
onAccept: () => remoteConnection.commit_changes_async(true, null, (_, asyncRes) =>
|
||||
!remoteConnection.commit_changes_finish(asyncRes) && Notifications.getDefault().sendNotification({
|
||||
appName: "network",
|
||||
summary: "Couldn't save Wi-Fi password",
|
||||
body: `An error occurred while trying to write the password for "${ssid}" to disk`
|
||||
}))
|
||||
} as AskPopupProps);
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { Gtk } from "ags/gtk4";
|
||||
import { Page, PageButton } from "./Page";
|
||||
import { Windows } from "../../../windows";
|
||||
import { tr } from "../../../i18n/intl";
|
||||
import { execApp } from "../../../scripts/apps";
|
||||
import { Notifications } from "../../../scripts/notifications";
|
||||
import { AskPopup, AskPopupProps } from "../../AskPopup";
|
||||
import { encoder, variableToBoolean } from "../../../scripts/utils";
|
||||
|
||||
import GLib from "gi://GLib?version=2.0";
|
||||
import NM from "gi://NM";
|
||||
import AstalNetwork from "gi://AstalNetwork";
|
||||
import { createBinding, For, With } from "ags";
|
||||
|
||||
|
||||
export const PageNetwork = () =>
|
||||
<Page id={"network"} title={tr("control_center.pages.network.title")}
|
||||
class={"network"} headerButtons={[
|
||||
<Gtk.Button class={"reload"} iconName={"arrow-circular-top-right-symbolic"}
|
||||
visible={createBinding(AstalNetwork.get_default(), "primary").as(primary =>
|
||||
primary === AstalNetwork.Primary.WIFI)}
|
||||
tooltipText={"Re-scan networks"} onClicked={() =>
|
||||
AstalNetwork.get_default().wifi.scan()}
|
||||
/>
|
||||
]} bottomButtons={[{
|
||||
title: tr("control_center.pages.more_settings"),
|
||||
onClick: () => {
|
||||
Windows.getDefault().close("control-center");
|
||||
execApp("nm-connection-editor", "[animationstyle gnomed]");
|
||||
}
|
||||
}]}>
|
||||
|
||||
<Gtk.Box class={"devices"} hexpand={true} orientation={Gtk.Orientation.VERTICAL}
|
||||
visible={variableToBoolean(createBinding(AstalNetwork.get_default().client, "devices"))}>
|
||||
|
||||
<Gtk.Label label={tr("devices")} xalign={0} class={"sub-header"} />
|
||||
<For each={createBinding(AstalNetwork.get_default().client, "devices").as(devs =>
|
||||
devs.filter(dev => dev.interface !== "lo" && dev.real /* filter local device */))}>
|
||||
|
||||
{(device: NM.Device) => <PageButton title={createBinding(device, "interface").as(iface =>
|
||||
iface ?? tr("control_center.pages.network.interface"))} class={"device"}
|
||||
icon={createBinding(device, "deviceType").as(type => type === NM.DeviceType.WIFI ?
|
||||
"network-wireless-symbolic" : "network-wired-symbolic")} extraButtons={[
|
||||
|
||||
<Gtk.Button iconName={"view-more-symbolic"} onClicked={() => {
|
||||
Windows.getDefault().close("control-center");
|
||||
execApp(
|
||||
`nm-connection-editor --edit ${device.activeConnection?.connection.get_uuid()}`,
|
||||
"[animationstyle gnomed; float]"
|
||||
);
|
||||
}} />
|
||||
]}
|
||||
/>}
|
||||
</For>
|
||||
</Gtk.Box>
|
||||
|
||||
<With value={createBinding(AstalNetwork.get_default(), "primary").as(primary =>
|
||||
primary === AstalNetwork.Primary.WIFI)}>
|
||||
|
||||
{(isWifi: boolean) => isWifi && <Gtk.Box class={"wireless-aps"} hexpand={true}
|
||||
orientation={Gtk.Orientation.VERTICAL}>
|
||||
|
||||
<Gtk.Label class={"sub-header"} label={"Wi-Fi"} />
|
||||
<For each={createBinding(AstalNetwork.get_default().wifi, "accessPoints")}>
|
||||
{(ap: AstalNetwork.AccessPoint) => <PageButton class={
|
||||
createBinding(AstalNetwork.get_default().wifi, "activeAccessPoint").as(activeAP =>
|
||||
activeAP.ssid === ap.ssid ? "active" : "")
|
||||
} title={createBinding(ap, "ssid").as(ssid => ssid ?? "No SSID")}
|
||||
icon={createBinding(ap, "iconName")} endWidget={<Gtk.Image iconName={
|
||||
createBinding(ap, "flags").as(flags =>
|
||||
// @ts-ignore
|
||||
flags & NM["80211ApFlags"].PRIVACY ?
|
||||
"channel-secure-symbolic"
|
||||
: "channel-insecure-symbolic")}
|
||||
css={"font-size: 18px;"}
|
||||
/>} extraButtons={[
|
||||
<Gtk.Button iconName={"window-close-symbolic"} visible={
|
||||
createBinding(AstalNetwork.get_default().wifi, "activeAccessPoint").as(activeAp =>
|
||||
activeAp.ssid === ap.ssid)
|
||||
} css={"font-size: 18px;"} onClicked={() => {
|
||||
const active = AstalNetwork.get_default().wifi.activeAccessPoint;
|
||||
|
||||
if(active?.ssid === ap.ssid) {
|
||||
AstalNetwork.get_default().wifi.deactivate_connection((_, res) => {
|
||||
try {
|
||||
AstalNetwork.get_default().wifi.deactivate_connection_finish(res);
|
||||
} catch(e: any) {
|
||||
e = e as Error;
|
||||
|
||||
console.error(
|
||||
`Network: couldn't deactivate connection with access point(SSID: ${
|
||||
ap.ssid}. Stderr: \n${e.message}\n${e.stack}`
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
}}/>
|
||||
]} onClick={() => {
|
||||
const uuid = NM.utils_uuid_generate();
|
||||
const ssidBytes = GLib.Bytes.new(encoder.encode(ap.ssid));
|
||||
|
||||
const connection = NM.SimpleConnection.new();
|
||||
const connSetting = NM.SettingConnection.new();
|
||||
const wifiSetting = NM.SettingWireless.new();
|
||||
const wifiSecuritySetting = NM.SettingWirelessSecurity.new();
|
||||
const setting8021x = NM.Setting8021x.new();
|
||||
|
||||
// @ts-ignore yep, type-gen issues again
|
||||
if(ap.rsnFlags !& NM["80211ApSecurityFlags"].KEY_MGMT_802_1X &&
|
||||
// @ts-ignore
|
||||
ap.wpaFlags !& NM["80211ApSecurityFlags"].KEY_MGMT_802_1X) {
|
||||
return;
|
||||
}
|
||||
|
||||
connSetting.uuid = uuid;
|
||||
connection.add_setting(connSetting);
|
||||
|
||||
connection.add_setting(wifiSetting);
|
||||
wifiSetting.ssid = ssidBytes;
|
||||
|
||||
wifiSecuritySetting.keyMgmt = "wpa-eap";
|
||||
connection.add_setting(wifiSecuritySetting);
|
||||
|
||||
setting8021x.add_eap_method("ttls");
|
||||
setting8021x.phase2Auth = "mschapv2";
|
||||
connection.add_setting(setting8021x);
|
||||
}}
|
||||
/>}
|
||||
</For>
|
||||
</Gtk.Box>}
|
||||
</With>
|
||||
</Page> as Page;
|
||||
|
||||
function activateWirelessConnection(connection: NM.RemoteConnection, ssid: string): void {
|
||||
AstalNetwork.get_default().get_client().activate_connection_async(
|
||||
connection, AstalNetwork.get_default().wifi.get_device(), null, null, (_, asyncRes) => {
|
||||
const activeConnection = AstalNetwork.get_default().get_client().activate_connection_finish(asyncRes);
|
||||
if(!activeConnection) {
|
||||
Notifications.getDefault().sendNotification({
|
||||
appName: "network",
|
||||
summary: "Couldn't activate wireless connection",
|
||||
body: `An error occurred while activating the wireless connection "${ssid}"`
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function notifyConnectionError(ssid: string): void {
|
||||
Notifications.getDefault().sendNotification({
|
||||
appName: "network",
|
||||
summary: "Coudn't connect Wi-Fi",
|
||||
body: `An error occurred while trying to connect to the "${ssid}" access point. \nMaybe the password is invalid?`
|
||||
});
|
||||
}
|
||||
function saveToDisk(remoteConnection: NM.RemoteConnection, ssid: string): void {
|
||||
AskPopup({
|
||||
text: `Save password for connection "${ssid}"?`,
|
||||
acceptText: "Yes",
|
||||
onAccept: () => remoteConnection.commit_changes_async(true, null, (_, asyncRes) =>
|
||||
!remoteConnection.commit_changes_finish(asyncRes) && Notifications.getDefault().sendNotification({
|
||||
appName: "network",
|
||||
summary: "Couldn't save Wi-Fi password",
|
||||
body: `An error occurred while trying to write the password for "${ssid}" to disk`
|
||||
}))
|
||||
} as AskPopupProps);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { Widget } from "astal/gtk3";
|
||||
import { Page, PageProps } from "./Page";
|
||||
import { bind } from "astal";
|
||||
import { NightLight } from "../../../scripts/nightlight";
|
||||
import { addSliderMarksFromMinMax } from "../../../scripts/widget-utils";
|
||||
import { tr } from "../../../i18n/intl";
|
||||
|
||||
export const PageNightLight: (() => Page) = () => new Page({
|
||||
id: "night-light",
|
||||
title: tr("control_center.pages.night_light.title"),
|
||||
description: tr("control_center.pages.night_light.description"),
|
||||
className: "night-light",
|
||||
children: [
|
||||
new Widget.Label({
|
||||
className: "sub-header",
|
||||
label: tr("control_center.pages.night_light.temperature"),
|
||||
xalign: 0
|
||||
} as Widget.LabelProps),
|
||||
new Widget.Slider({
|
||||
className: "temperature",
|
||||
setup: (slider) => {
|
||||
slider.value = NightLight.getDefault().temperature;
|
||||
addSliderMarksFromMinMax(slider, 5, "{}K");
|
||||
},
|
||||
value: bind(NightLight.getDefault(), "temperature"),
|
||||
tooltipText: bind(NightLight.getDefault(), "temperature").as((temp) => `${temp}K`),
|
||||
min: 1000,
|
||||
max: NightLight.getDefault().maxTemperature,
|
||||
onDragged: (slider) =>
|
||||
NightLight.getDefault().temperature = (Math.floor(slider.value)),
|
||||
} as Widget.SliderProps),
|
||||
new Widget.Label({
|
||||
className: "sub-header",
|
||||
label: tr("control_center.pages.night_light.gamma"),
|
||||
css: "margin-top: 6px;",
|
||||
xalign: 0
|
||||
} as Widget.LabelProps),
|
||||
new Widget.Slider({
|
||||
className: "gamma",
|
||||
setup: (slider) => {
|
||||
slider.value = NightLight.getDefault().gamma;
|
||||
addSliderMarksFromMinMax(slider, 5, "{}%");
|
||||
},
|
||||
value: bind(NightLight.getDefault(), "gamma"),
|
||||
max: NightLight.getDefault().maxGamma,
|
||||
tooltipText: bind(NightLight.getDefault(), "gamma").as((gamma) => `${gamma}%`),
|
||||
onDragged: (slider) =>
|
||||
NightLight.getDefault().gamma = (Math.floor(slider.value)),
|
||||
} as Widget.SliderProps)
|
||||
]
|
||||
} as PageProps);
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Page } from "./Page";
|
||||
import { NightLight } from "../../../scripts/nightlight";
|
||||
import { tr } from "../../../i18n/intl";
|
||||
import { Astal, Gtk } from "ags/gtk4";
|
||||
import { addSliderMarksFromMinMax } from "../../../scripts/utils";
|
||||
import { createBinding } from "ags";
|
||||
|
||||
export const PageNightLight: (() => Page) = () =>
|
||||
<Page id={"night-light"} title={tr("control_center.pages.night_light.title")}
|
||||
description={tr("control_center.pages.night_light.description")}
|
||||
class={"night-light"}>
|
||||
|
||||
<Gtk.Label class={"sub-header"} label={tr(
|
||||
"control_center.pages.night_light.temperature"
|
||||
)} xalign={0} />
|
||||
<Astal.Slider class={"temperature"} $={(self) => {
|
||||
self.value = NightLight.getDefault().temperature;
|
||||
addSliderMarksFromMinMax(self, 5, "{}K");
|
||||
}} value={createBinding(NightLight.getDefault(), "temperature")}
|
||||
tooltipText={createBinding(NightLight.getDefault(), "temperature").as(temp =>
|
||||
`${temp}K`)} min={NightLight.getDefault().minTemperature}
|
||||
max={NightLight.getDefault().maxTemperature}
|
||||
onChangeValue={(_, type, value) => {
|
||||
if(type != undefined && type !== null)
|
||||
NightLight.getDefault().temperature = Math.floor(value)
|
||||
}}
|
||||
/>
|
||||
<Gtk.Label class={"sub-header"} label={tr(
|
||||
"control_center.pages.night_light.gamma"
|
||||
)} xalign={0} />
|
||||
<Astal.Slider class={"gamma"} $={(self) => {
|
||||
self.value = NightLight.getDefault().gamma;
|
||||
addSliderMarksFromMinMax(self, 5, "{}%");
|
||||
}} value={createBinding(NightLight.getDefault(), "gamma")}
|
||||
tooltipText={createBinding(NightLight.getDefault(), "gamma").as(gamma =>
|
||||
`${gamma}%`)} max={NightLight.getDefault().maxGamma}
|
||||
onChangeValue={(_, type, value) => {
|
||||
if(type != undefined && type !== null)
|
||||
NightLight.getDefault().gamma = Math.floor(value)
|
||||
}}
|
||||
/>
|
||||
</Page> as Page;
|
||||
@@ -1,257 +0,0 @@
|
||||
import { Binding, register } from "astal";
|
||||
import { Gtk, Widget } from "astal/gtk3";
|
||||
import { Separator, SeparatorProps } from "../../Separator";
|
||||
|
||||
export type PageProps = {
|
||||
setup?: () => void;
|
||||
onClose?: () => void;
|
||||
id: string;
|
||||
className?: string | Binding<string>;
|
||||
title: string | Binding<string>;
|
||||
description?: string | Binding<string>;
|
||||
headerButtons?: Array<Gtk.Button> | Binding<Array<Gtk.Button>>;
|
||||
bottomButtons?: Array<BottomButton> | Binding<Array<BottomButton>>;
|
||||
orientation?: Gtk.Orientation | Binding<Gtk.Orientation>;
|
||||
spacing?: number;
|
||||
child?: Gtk.Widget | Binding<Gtk.Widget>;
|
||||
children?: Array<Gtk.Widget> | Binding<Array<Gtk.Widget>>;
|
||||
};
|
||||
|
||||
export type BottomButton = {
|
||||
title: string | Binding<string>;
|
||||
description?: string | Binding<string>;
|
||||
tooltipText?: string | Binding<string>;
|
||||
tooltipMarkup?: string | Binding<string>;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export { Page };
|
||||
|
||||
@register({ GTypeName: "Page" })
|
||||
class Page extends Widget.Box {
|
||||
readonly #id: string | number;
|
||||
readonly bottomButtons?: Array<BottomButton>;
|
||||
|
||||
#title: string | Binding<string>;
|
||||
#description?: string | Binding<string>;
|
||||
|
||||
public get title() { return this.#title; }
|
||||
public get description() { return this.#description; }
|
||||
public get id() { return this.#id; }
|
||||
public onClose?: () => void;
|
||||
|
||||
constructor(props: PageProps) {
|
||||
super({
|
||||
hexpand: true,
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
className: (props.className instanceof Binding) ?
|
||||
props.className.as((clsName) => `page ${ clsName ?? "" }`)
|
||||
: `page ${props.className ?? ""}`,
|
||||
setup: props.setup,
|
||||
children: [
|
||||
new Widget.Box({
|
||||
className: "header",
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
hexpand: true,
|
||||
children: [
|
||||
new Widget.Box({
|
||||
className: "top",
|
||||
children: [
|
||||
new Widget.Label({
|
||||
hexpand: true,
|
||||
className: "title",
|
||||
truncate: true,
|
||||
visible: (props.title instanceof Binding) ?
|
||||
props.title.as(Boolean)
|
||||
: (props.title ? true : false),
|
||||
label: props.title,
|
||||
halign: Gtk.Align.START
|
||||
} as Widget.LabelProps),
|
||||
new Widget.Box({
|
||||
className: "button-row",
|
||||
visible: (props.headerButtons instanceof Binding) ?
|
||||
props.headerButtons.as(Boolean)
|
||||
: (props.headerButtons ? true : false),
|
||||
children: props.headerButtons
|
||||
} as Widget.BoxProps)
|
||||
]
|
||||
} as Widget.BoxProps),
|
||||
new Widget.Label({
|
||||
className: "description",
|
||||
hexpand: true,
|
||||
truncate: true,
|
||||
xalign: 0,
|
||||
visible: (props.description instanceof Binding) ?
|
||||
props.description.as(Boolean)
|
||||
: props.description ? true : false,
|
||||
label: props.description
|
||||
} as Widget.LabelProps),
|
||||
]
|
||||
} as Widget.BoxProps),
|
||||
new Widget.Box({
|
||||
className: "content",
|
||||
spacing: props.spacing ?? 4,
|
||||
orientation: props.orientation ?? Gtk.Orientation.VERTICAL,
|
||||
expand: true,
|
||||
setup: props.setup,
|
||||
child: props.child,
|
||||
children: props.children
|
||||
} as Widget.BoxProps),
|
||||
Separator({
|
||||
alpha: .2,
|
||||
spacing: 6,
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
visible: (props.bottomButtons instanceof Binding) ?
|
||||
props.bottomButtons.as(buttons => buttons.length > 0)
|
||||
: (!props.bottomButtons ? false : props.bottomButtons.length > 0)
|
||||
} as SeparatorProps),
|
||||
new Widget.Box({
|
||||
className: "bottom-buttons",
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
visible: (props.bottomButtons instanceof Binding) ?
|
||||
props.bottomButtons.as(buttons => buttons.length > 0)
|
||||
: (!props.bottomButtons ? false : props.bottomButtons.length > 0),
|
||||
spacing: 2,
|
||||
children: (props.bottomButtons instanceof Binding) ?
|
||||
props.bottomButtons.as(buttons => buttons.map(button =>
|
||||
new Widget.Button({
|
||||
onClicked: button.onClick,
|
||||
tooltipMarkup: button.tooltipMarkup,
|
||||
tooltipText: button.tooltipText,
|
||||
child: new Widget.Box({
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
children: [
|
||||
new Widget.Label({
|
||||
className: "title",
|
||||
label: button.title,
|
||||
xalign: 0
|
||||
} as Widget.LabelProps),
|
||||
new Widget.Label({
|
||||
className: "description",
|
||||
label: button.description,
|
||||
visible: Boolean(button.description),
|
||||
xalign: 0
|
||||
} as Widget.LabelProps)
|
||||
]
|
||||
} as Widget.BoxProps)
|
||||
} as Widget.ButtonProps)
|
||||
)
|
||||
)
|
||||
: (!props.bottomButtons ? [] : props.bottomButtons.map(button =>
|
||||
new Widget.Button({
|
||||
onClicked: button.onClick,
|
||||
tooltipMarkup: button.tooltipMarkup,
|
||||
tooltipText: button.tooltipText,
|
||||
child: new Widget.Box({
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
children: [
|
||||
new Widget.Label({
|
||||
className: "title",
|
||||
label: button.title,
|
||||
xalign: 0
|
||||
} as Widget.LabelProps),
|
||||
new Widget.Label({
|
||||
className: "description",
|
||||
label: button.description,
|
||||
visible: Boolean(button.description),
|
||||
xalign: 0
|
||||
} as Widget.LabelProps)
|
||||
]
|
||||
} as Widget.BoxProps)
|
||||
} as Widget.ButtonProps)
|
||||
))
|
||||
} as Widget.BoxProps)
|
||||
]
|
||||
});
|
||||
|
||||
this.#id = props.id;
|
||||
this.#title = props.title;
|
||||
this.#description = props.description;
|
||||
|
||||
this.onClose = props.onClose;
|
||||
}
|
||||
}
|
||||
|
||||
export function PageButton({ onDestroy, ...props }: {
|
||||
className?: string | Binding<string>;
|
||||
icon?: string | Binding<string>;
|
||||
title: string | Binding<string>;
|
||||
endWidget?: Gtk.Widget | Binding<Gtk.Widget>;
|
||||
description?: string | Binding<string>;
|
||||
extraButtons?: Array<Widget.Button> | Binding<Array<Gtk.Widget>>;
|
||||
onDestroy?: (self: Widget.Box) => void;
|
||||
onClick?: (self: Widget.Button) => void;
|
||||
tooltipText?: string | Binding<string>;
|
||||
tooltipMarkup?: string | Binding<string>;
|
||||
}): Gtk.Widget {
|
||||
return new Widget.Box({
|
||||
onDestroy,
|
||||
children: [
|
||||
new Widget.Button({
|
||||
onClick: props.onClick,
|
||||
className: props.className,
|
||||
hexpand: true,
|
||||
tooltipText: props.tooltipText,
|
||||
tooltipMarkup: props.tooltipMarkup,
|
||||
child: new Widget.Box({
|
||||
className: "page-button",
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
expand: true,
|
||||
children: [
|
||||
new Widget.Icon({
|
||||
className: "icon",
|
||||
icon: props.icon,
|
||||
visible: props.icon,
|
||||
hexpand: false,
|
||||
css: "font-size: 20px; margin-right: 6px;"
|
||||
} as Widget.IconProps),
|
||||
new Widget.Box({
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
hexpand: true,
|
||||
vexpand: false,
|
||||
children: [
|
||||
new Widget.Label({
|
||||
className: "title",
|
||||
xalign: 0,
|
||||
// truncating is not working, so I had to do this
|
||||
label: (props.title instanceof Binding) ?
|
||||
props.title.as((title) =>
|
||||
`${title.substring(0, 35)}${
|
||||
title.length > 35 ? '…' : ""}`)
|
||||
: `${props.title.substring(0, 35)}${
|
||||
props.title.length > 35 ? '…' : ""}`,
|
||||
tooltipText: props.title,
|
||||
truncate: true,
|
||||
} as Widget.LabelProps),
|
||||
new Widget.Label({
|
||||
className: "description",
|
||||
xalign: 0,
|
||||
visible: (props.description instanceof Binding) ?
|
||||
props.description.as(Boolean)
|
||||
: Boolean(props.description),
|
||||
label: props.description,
|
||||
truncate: true,
|
||||
tooltipText: props.description
|
||||
} as Widget.LabelProps)
|
||||
]
|
||||
} as Widget.BoxProps),
|
||||
new Widget.Box({
|
||||
visible: (props.endWidget instanceof Binding) ?
|
||||
props.endWidget.as(Boolean)
|
||||
: props.endWidget,
|
||||
halign: Gtk.Align.END,
|
||||
child: props.endWidget
|
||||
} as Widget.BoxProps)
|
||||
]
|
||||
} as Widget.BoxProps)
|
||||
} as Widget.ButtonProps),
|
||||
new Widget.Box({
|
||||
className: "extra-buttons button-row",
|
||||
visible: (props.extraButtons instanceof Binding) ?
|
||||
props.extraButtons.as(extra => extra.length > 0)
|
||||
: (props.extraButtons ? props.extraButtons.length > 0 : false),
|
||||
children: props.extraButtons
|
||||
} as Widget.BoxProps)
|
||||
]
|
||||
} as Widget.BoxProps);
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import { register } from "ags/gobject";
|
||||
import { Gtk } from "ags/gtk4";
|
||||
import { Separator } from "../../Separator";
|
||||
import { Accessor, For } from "ags";
|
||||
import { transform, transformWidget, variableToBoolean, WidgetNodeType } from "../../../scripts/utils";
|
||||
import Pango from "gi://Pango?version=1.0";
|
||||
|
||||
export type PageProps = {
|
||||
$?: () => void;
|
||||
onClose?: () => void;
|
||||
id: string;
|
||||
class?: string | Accessor<string>;
|
||||
title: string | Accessor<string>;
|
||||
description?: string | Accessor<string>;
|
||||
headerButtons?: Array<JSX.Element> | Accessor<Array<JSX.Element>>;
|
||||
bottomButtons?: Array<BottomButton> | Accessor<Array<BottomButton>>;
|
||||
orientation?: Gtk.Orientation | Accessor<Gtk.Orientation>;
|
||||
spacing?: number;
|
||||
children?: WidgetNodeType;
|
||||
};
|
||||
|
||||
export type BottomButton = {
|
||||
title: string | Accessor<string>;
|
||||
description?: string | Accessor<string>;
|
||||
tooltipText?: string | Accessor<string>;
|
||||
tooltipMarkup?: string | Accessor<string>;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export { Page };
|
||||
|
||||
@register({ GTypeName: "Page" })
|
||||
class Page extends Gtk.Box {
|
||||
readonly #id: string | number;
|
||||
readonly bottomButtons?: Array<BottomButton>;
|
||||
|
||||
#subs: Array<() => void> = [];
|
||||
#title: string | Accessor<string>;
|
||||
#description?: string | Accessor<string>;
|
||||
|
||||
public get title() { return this.#title; }
|
||||
public get description() { return this.#description; }
|
||||
public get id() { return this.#id; }
|
||||
public onClose?: () => void;
|
||||
|
||||
constructor(props: PageProps) {
|
||||
super({
|
||||
hexpand: true,
|
||||
orientation: Gtk.Orientation.VERTICAL
|
||||
});
|
||||
|
||||
this.#id = props.id;
|
||||
this.#title = props.title;
|
||||
this.#description = props.description;
|
||||
|
||||
if(props.class instanceof Accessor) {
|
||||
this.#subs.push(props.class.subscribe(() => {
|
||||
const clss = (props.class as Accessor<string>).get();
|
||||
|
||||
this.cssClasses = ["page", ...clss.split(' ').filter(s => s !== "")];
|
||||
}));
|
||||
} else {
|
||||
if(props.class)
|
||||
this.cssClasses = ["page",
|
||||
...(props.class as string).split(' ').filter(s => s)];
|
||||
else
|
||||
this.add_css_class("page");
|
||||
}
|
||||
|
||||
this.prepend(<Gtk.Box class={"header"} orientation={Gtk.Orientation.VERTICAL}
|
||||
hexpand={true}>
|
||||
|
||||
<Gtk.Box class={"top"}>
|
||||
<Gtk.Label hexpand={true} class={"title"} ellipsize={Pango.EllipsizeMode.END}
|
||||
visible={variableToBoolean(props.title)} label={props.title}
|
||||
halign={Gtk.Align.START} />
|
||||
|
||||
{props.headerButtons && <Gtk.Box class={"button-row"} visible={variableToBoolean(props.headerButtons)}>
|
||||
{
|
||||
(props.headerButtons instanceof Accessor) ?
|
||||
<For each={props.headerButtons}>
|
||||
{(button) => button}
|
||||
</For>
|
||||
: props.headerButtons
|
||||
}
|
||||
</Gtk.Box>}
|
||||
|
||||
<Gtk.Label class={"description"} hexpand={true} ellipsize={Pango.EllipsizeMode.END}
|
||||
xalign={0} visible={variableToBoolean(props.description)} label={props.description} />
|
||||
|
||||
</Gtk.Box>
|
||||
</Gtk.Box> as Gtk.Box);
|
||||
|
||||
this.append(<Gtk.Box class={"content"} spacing={props.spacing ?? 4} hexpand={true} vexpand={true}
|
||||
orientation={props.orientation ?? Gtk.Orientation.VERTICAL}>
|
||||
|
||||
{props.children}
|
||||
</Gtk.Box> as Gtk.Box);
|
||||
|
||||
this.append(<Separator alpha={.2} spacing={6} orientation={Gtk.Orientation.VERTICAL}
|
||||
visible={(props.bottomButtons instanceof Accessor) ?
|
||||
props.bottomButtons.as(buttons => buttons.length > 0)
|
||||
: (!props.bottomButtons ? false : props.bottomButtons.length > 0)}
|
||||
/> as Gtk.Widget);
|
||||
|
||||
this.append(<Gtk.Box class={"bottom-buttons"} orientation={Gtk.Orientation.VERTICAL}
|
||||
visible={variableToBoolean(props.bottomButtons)} spacing={2}>
|
||||
|
||||
{transformWidget(props.bottomButtons, (button) =>
|
||||
<Gtk.Button onClicked={button?.onClick} tooltipText={button?.tooltipText}
|
||||
tooltipMarkup={button?.tooltipMarkup}>
|
||||
|
||||
<Gtk.Label class={"title"} label={button?.title} xalign={0} />
|
||||
<Gtk.Label class={"description"} label={button?.description}
|
||||
xalign={0} visible={variableToBoolean(button?.description)} />
|
||||
</Gtk.Button>
|
||||
)}
|
||||
</Gtk.Box> as Gtk.Box);
|
||||
|
||||
this.onClose = props.onClose;
|
||||
props.$?.();
|
||||
}
|
||||
}
|
||||
|
||||
function BottomButton(props: BottomButton) {
|
||||
return <Gtk.Button onClicked={props.onClick} tooltipMarkup={props.tooltipMarkup}
|
||||
tooltipText={props.tooltipText}>
|
||||
|
||||
<Gtk.Box orientation={Gtk.Orientation.VERTICAL}>
|
||||
<Gtk.Label class={"title"} label={props.title} xalign={0} />
|
||||
<Gtk.Label class={"description"} label={props.description}
|
||||
visible={Boolean(props.description)} xalign={0} />
|
||||
</Gtk.Box>
|
||||
</Gtk.Button> as Gtk.Button;
|
||||
}
|
||||
|
||||
export function PageButton({ onDestroy, ...props }: {
|
||||
class?: string | Accessor<string>;
|
||||
icon?: string | Accessor<string>;
|
||||
title: string | Accessor<string>;
|
||||
endWidget?: WidgetNodeType;
|
||||
description?: string | Accessor<string>;
|
||||
extraButtons?: Array<WidgetNodeType> | WidgetNodeType;
|
||||
onDestroy?: (self: Gtk.Box) => void;
|
||||
onClick?: (self: Gtk.Button) => void;
|
||||
tooltipText?: string | Accessor<string>;
|
||||
tooltipMarkup?: string | Accessor<string>;
|
||||
}) {
|
||||
return <Gtk.Box onDestroy={onDestroy}>
|
||||
<Gtk.Button onClicked={props.onClick} class={props.class} hexpand={true}
|
||||
tooltipText={props.tooltipText} tooltipMarkup={props.tooltipMarkup}>
|
||||
|
||||
<Gtk.Box class={"page-button"} hexpand={true} vexpand={true}>
|
||||
{props.icon && <Gtk.Image iconName={props.icon} visible={variableToBoolean(props.icon)}
|
||||
css={"font-size: 20px; margin-right: 6px;"} />}
|
||||
|
||||
<Gtk.Box orientation={Gtk.Orientation.VERTICAL} hexpand={true} vexpand={false}>
|
||||
<Gtk.Label class={"title"} xalign={0} tooltipText={props.title}
|
||||
ellipsize={Pango.EllipsizeMode.END} label={
|
||||
transform(props.title, (title) =>
|
||||
`${title.substring(0, 35)}${title.length > 35 ? '…' : ""}`)
|
||||
}
|
||||
/>
|
||||
<Gtk.Label class={"description"} xalign={0} visible={variableToBoolean(props.description)}
|
||||
label={props.description} ellipsize={Pango.EllipsizeMode.END}
|
||||
tooltipText={props.description} />
|
||||
</Gtk.Box>
|
||||
|
||||
<Gtk.Box visible={variableToBoolean(props.endWidget)} halign={Gtk.Align.END}>
|
||||
{props.endWidget && props.endWidget}
|
||||
</Gtk.Box>
|
||||
</Gtk.Box>
|
||||
</Gtk.Button>
|
||||
|
||||
<Gtk.Box class={"extra-buttons button-row"} visible={variableToBoolean(props.extraButtons)}>
|
||||
{props.extraButtons}
|
||||
</Gtk.Box>
|
||||
</Gtk.Box> as Gtk.Box;
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import { Page, PageButton, PageProps } from "./Page";
|
||||
import { bind, Variable } from "astal";
|
||||
import { Astal, Gtk, Widget } from "astal/gtk3";
|
||||
import { getAppIcon } from "../../../scripts/apps";
|
||||
import { Wireplumber } from "../../../scripts/volume";
|
||||
import { tr } from "../../../i18n/intl";
|
||||
|
||||
export function PageSound(): Page {
|
||||
const endpoints = Variable.derive([
|
||||
bind(Wireplumber.getWireplumber().get_audio()!, "speakers"),
|
||||
bind(Wireplumber.getWireplumber().get_audio()!, "streams")
|
||||
]);
|
||||
|
||||
return new Page({
|
||||
id: "sound",
|
||||
title: tr("control_center.pages.sound.title"),
|
||||
description: tr("control_center.pages.sound.description"),
|
||||
onClose: endpoints.drop,
|
||||
children: endpoints(([speakers, streams]) => [
|
||||
new Widget.Label({
|
||||
className: "sub-header",
|
||||
label: tr("devices"),
|
||||
xalign: 0
|
||||
} as Widget.LabelProps),
|
||||
...speakers.map((speaker) =>
|
||||
PageButton({
|
||||
className: bind(speaker, "isDefault").as(isDefault => isDefault ? "default" : ""),
|
||||
icon: bind(speaker, "icon").as(icon =>
|
||||
Astal.Icon.lookup_icon(icon)? icon : "audio-card-symbolic"),
|
||||
title: bind(speaker, "description").as(desc => desc ?? "Speaker"),
|
||||
onClick: () => speaker.set_is_default(true),
|
||||
endWidget: new Widget.Icon({
|
||||
icon: "object-select-symbolic",
|
||||
visible: bind(speaker, "isDefault"),
|
||||
css: "font-size: 18px;"
|
||||
} as Widget.IconProps)
|
||||
})
|
||||
),
|
||||
new Widget.Label({
|
||||
className: "sub-header",
|
||||
label: tr("apps"),
|
||||
visible: streams.length > 0,
|
||||
xalign: 0
|
||||
} as Widget.LabelProps),
|
||||
...streams.map((stream) =>
|
||||
new Widget.EventBox({
|
||||
hexpand: true,
|
||||
setup: (eventbox) => {
|
||||
const connections: Array<number> = [];
|
||||
|
||||
eventbox.add(new Widget.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
children: [
|
||||
new Widget.Icon({
|
||||
icon: bind(stream, "name").as(name =>
|
||||
getAppIcon(name.split(' ')[0]) ?? "application-x-executable-symbolic"),
|
||||
css: "font-size: 18px; margin-right: 6px;"
|
||||
} as Widget.IconProps),
|
||||
new Widget.Box({
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
hexpand: true,
|
||||
children: [
|
||||
new Widget.Revealer({
|
||||
transitionDuration: 180,
|
||||
transitionType: Gtk.RevealerTransitionType.SLIDE_DOWN,
|
||||
setup: (self) => connections.push(
|
||||
eventbox.connect("hover", () => self.revealChild = true),
|
||||
eventbox.connect("hover-lost", () => self.revealChild = false)
|
||||
),
|
||||
onDestroy: () => connections.map(id => eventbox.disconnect(id)),
|
||||
child: new Widget.Label({
|
||||
label: bind(stream, "name").as(name => name || "Unknown"),
|
||||
truncate: true,
|
||||
tooltipText: bind(stream, "name"),
|
||||
className: "name",
|
||||
xalign: 0
|
||||
} as Widget.LabelProps)
|
||||
} as Widget.RevealerProps),
|
||||
new Widget.Slider({
|
||||
min: 0,
|
||||
drawValue: false,
|
||||
max: 100,
|
||||
setup: (self) => self.value = Math.floor(stream.volume * 100),
|
||||
value: bind(stream, "volume").as((vol) => Math.floor(vol * 100)),
|
||||
onDragged: (self) => stream.volume = self.value / 100
|
||||
} as Widget.SliderProps)
|
||||
]
|
||||
} as Widget.BoxProps)
|
||||
]
|
||||
} as Widget.BoxProps))
|
||||
}
|
||||
} as Widget.EventBoxProps)
|
||||
)
|
||||
])
|
||||
} as PageProps);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Page, PageButton } from "./Page";
|
||||
import { Astal, Gtk } from "ags/gtk4";
|
||||
import { getAppIcon, lookupIcon } from "../../../scripts/apps";
|
||||
import { Wireplumber } from "../../../scripts/volume";
|
||||
import { tr } from "../../../i18n/intl";
|
||||
import { createBinding, For } from "ags";
|
||||
import AstalWp from "gi://AstalWp";
|
||||
import { variableToBoolean } from "../../../scripts/utils";
|
||||
import GObject from "gi://GObject?version=2.0";
|
||||
import Pango from "gi://Pango?version=1.0";
|
||||
|
||||
export const PageSound = () =>
|
||||
<Page id={"sound"} title={tr("control_center.pages.sound.title")}
|
||||
description={tr("control_center.pages.sound.description")}>
|
||||
|
||||
<Gtk.Label class={"sub-header"} label={tr("devices")} xalign={0} />
|
||||
<For each={createBinding(Wireplumber.getWireplumber().audio!, "speakers")}>
|
||||
{(sink: AstalWp.Endpoint) =>
|
||||
<PageButton class={createBinding(sink, "isDefault").as(isDefault =>
|
||||
isDefault ? "default" : "")}
|
||||
icon={createBinding(sink, "icon").as(ico =>
|
||||
lookupIcon(ico) ? ico : "audio-card-symbolic")}
|
||||
title={createBinding(sink, "description").as(desc =>
|
||||
desc ?? "Speaker")}
|
||||
onClick={() => !sink.isDefault && sink.set_is_default(true)}
|
||||
endWidget={
|
||||
<Gtk.Image iconName={"object-select-symbolic"}
|
||||
visible={createBinding(sink, "isDefault")}
|
||||
css={"font-size: 18px;"}
|
||||
/>
|
||||
}
|
||||
/>}
|
||||
</For>
|
||||
|
||||
<Gtk.Label class={"sub-header"} label={tr("apps")} xalign={0}
|
||||
visible={variableToBoolean(
|
||||
createBinding(Wireplumber.getWireplumber().audio!, "streams")
|
||||
)}
|
||||
/>
|
||||
<For each={createBinding(Wireplumber.getWireplumber().audio!, "streams")}>
|
||||
{(stream: AstalWp.Stream) =>
|
||||
<Gtk.Box hexpand={true} $={(self) => {
|
||||
const conns: Map<GObject.Object, Array<number>> = new Map();
|
||||
const controllerMotion = Gtk.EventControllerMotion.new();
|
||||
|
||||
self.add_controller(controllerMotion);
|
||||
|
||||
conns.set(controllerMotion, [
|
||||
controllerMotion.connect("enter", () => {
|
||||
const revealer = self.get_last_child()!.get_first_child() as Gtk.Revealer;
|
||||
revealer.set_reveal_child(true);
|
||||
}),
|
||||
controllerMotion.connect("leave", () => {
|
||||
const revealer = self.get_first_child()!.get_first_child() as Gtk.Revealer;
|
||||
revealer.set_reveal_child(true);
|
||||
})
|
||||
]);
|
||||
|
||||
conns.set(self, [
|
||||
self.connect("destroy", () => conns.forEach((ids, obj) =>
|
||||
ids.forEach(id => obj.disconnect(id))
|
||||
))
|
||||
]);
|
||||
}}>
|
||||
|
||||
<Gtk.Image iconName={createBinding(stream, "name").as(name =>
|
||||
getAppIcon(name.split(' ')[0]) ?? "application-x-executable-symbolic")}
|
||||
css={"font-size: 18px; margin-right: 6px;"} />
|
||||
|
||||
<Gtk.Box orientation={Gtk.Orientation.VERTICAL} hexpand={true}>
|
||||
<Gtk.Revealer transitionDuration={180}
|
||||
transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}>
|
||||
|
||||
<Gtk.Label label={createBinding(stream, "name").as(name =>
|
||||
name ?? "Unnamed audio stream")}
|
||||
ellipsize={Pango.EllipsizeMode.END}
|
||||
tooltipText={createBinding(stream, "name")}
|
||||
class={"name"} xalign={0}
|
||||
/>
|
||||
</Gtk.Revealer>
|
||||
|
||||
<Astal.Slider drawValue={false} max={100} $={(self) => {
|
||||
self.value = Math.floor(stream.volume * 100);
|
||||
}} value={createBinding(stream, "volume").as(vol =>
|
||||
Math.floor(vol * 100))}
|
||||
onChangeValue={(_, type, value) => {
|
||||
if(type !== undefined && type !== null)
|
||||
stream.volume = Math.floor(value / 100);
|
||||
}}
|
||||
/>
|
||||
</Gtk.Box>
|
||||
</Gtk.Box>
|
||||
}
|
||||
</For>
|
||||
</Page> as Page;
|
||||
Reference in New Issue
Block a user