♻️ refactor: reorganize windows and widgets in a modular way
plus, better code for bluetooth device pairing and connecting
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import { Astal, Gdk, Gtk } from "ags/gtk4";
|
||||
import { execApp, getAppIcon, getApps, getAstalApps } from "../modules/apps";
|
||||
import { getPopupWindowContainer, PopupWindow } from "../widget/PopupWindow";
|
||||
import { execApp, getAppIcon, getApps, getAstalApps } from "../../modules/apps";
|
||||
import { getPopupWindowContainer, PopupWindow } from "../../widget/PopupWindow";
|
||||
|
||||
import AstalApps from "gi://AstalApps";
|
||||
import Pango from "gi://Pango?version=1.0";
|
||||
import { createState, For } from "ags";
|
||||
import { escapeUnintendedMarkup } from "../modules/utils";
|
||||
import { escapeUnintendedMarkup } from "../../modules/utils";
|
||||
|
||||
|
||||
const ignoredKeys = [
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Astal, Gtk } from "ags/gtk4";
|
||||
import { Tray } from "../widget/bar/Tray";
|
||||
import { Workspaces } from "../widget/bar/Workspaces";
|
||||
import { FocusedClient } from "../widget/bar/FocusedClient";
|
||||
import { Apps } from "../widget/bar/Apps";
|
||||
import { Clock } from "../widget/bar/Clock";
|
||||
import { Status } from "../widget/bar/Status";
|
||||
import { Media } from "../widget/bar/Media";
|
||||
import { Tray } from "./widgets/Tray";
|
||||
import { Workspaces } from "./widgets/Workspaces";
|
||||
import { FocusedClient } from "./widgets/FocusedClient";
|
||||
import { Apps } from "./widgets/Apps";
|
||||
import { Clock } from "./widgets/Clock";
|
||||
import { Status } from "./widgets/Status";
|
||||
import { Media } from "./widgets/Media";
|
||||
|
||||
|
||||
export const Bar = (mon: number) => {
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Gtk } from "ags/gtk4";
|
||||
import { Windows } from "../../../windows";
|
||||
import { createBinding } from "ags";
|
||||
import { tr } from "../../../i18n/intl";
|
||||
|
||||
|
||||
export const Apps = () =>
|
||||
<Gtk.Button class={createBinding(Windows.getDefault(), "openWindows").as((openWindows) =>
|
||||
`apps ${Object.hasOwn(openWindows, "apps-window") ? "open" : ""}`
|
||||
)} iconName={"applications-other-symbolic"} halign={Gtk.Align.CENTER}
|
||||
hexpand tooltipText={tr("apps")} onClicked={() =>
|
||||
Windows.getDefault().open("apps-window")}
|
||||
/>;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Gtk } from "ags/gtk4";
|
||||
import { Windows } from "../../../windows";
|
||||
import { createBinding } from "ags";
|
||||
import { time } from "../../../modules/utils";
|
||||
import { generalConfig } from "../../../app";
|
||||
|
||||
|
||||
export const Clock = () =>
|
||||
<Gtk.Button class={createBinding(Windows.getDefault(), "openWindows").as((wins) =>
|
||||
`clock ${wins.includes("center-window") ? "open" : ""}`)}
|
||||
$={(self) => {
|
||||
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(
|
||||
generalConfig.getProperty("clock.date_format", "string"))
|
||||
?? "An error occurred"
|
||||
)}
|
||||
/>;
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Gtk } from "ags/gtk4";
|
||||
import { createBinding, With } from "ags";
|
||||
import { variableToBoolean } from "../../../modules/utils";
|
||||
import { getAppIcon, getSymbolicIcon } from "../../../modules/apps";
|
||||
|
||||
import Pango from "gi://Pango?version=1.0";
|
||||
import AstalHyprland from "gi://AstalHyprland";
|
||||
|
||||
|
||||
const hyprland = AstalHyprland.get_default();
|
||||
|
||||
// Fix empty focused-client on opening a window on an empty workspace
|
||||
hyprland.connect("notify::clients", () => hyprland.notify("focused-client"));
|
||||
|
||||
export const FocusedClient = () => {
|
||||
const focusedClient = createBinding(hyprland, "focusedClient");
|
||||
|
||||
return <Gtk.Box class={"focused-client"} visible={variableToBoolean(focusedClient)}>
|
||||
<With value={focusedClient}>
|
||||
{(focusedClient) => focusedClient?.class && <Gtk.Box>
|
||||
<Gtk.Image iconName={createBinding(focusedClient, "class").as((clss) =>
|
||||
getSymbolicIcon(clss) ?? getAppIcon(clss) ??
|
||||
getAppIcon(focusedClient.initialClass) ??
|
||||
"application-x-executable-symbolic"
|
||||
)} vexpand
|
||||
/>
|
||||
|
||||
<Gtk.Box valign={Gtk.Align.CENTER} class={"text-content"}
|
||||
orientation={Gtk.Orientation.VERTICAL}>
|
||||
|
||||
<Gtk.Label class={"class"} xalign={0} maxWidthChars={55}
|
||||
ellipsize={Pango.EllipsizeMode.END}
|
||||
label={createBinding(focusedClient, "class")}
|
||||
tooltipText={createBinding(focusedClient, "class")}
|
||||
/>
|
||||
|
||||
<Gtk.Label class={"title"} xalign={0} maxWidthChars={50}
|
||||
ellipsize={Pango.EllipsizeMode.END}
|
||||
label={createBinding(focusedClient, "title")}
|
||||
tooltipText={createBinding(focusedClient, "title")}
|
||||
/>
|
||||
</Gtk.Box>
|
||||
</Gtk.Box>}
|
||||
</With>
|
||||
</Gtk.Box>;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { createBinding, onCleanup, With } from "ags";
|
||||
import { Gtk } from "ags/gtk4";
|
||||
import { Separator } from "../../../widget/Separator";
|
||||
import { Windows } from "../../../windows";
|
||||
import { Clipboard } from "../../../modules/clipboard";
|
||||
import { getPlayerIconFromBusName, variableToBoolean } from "../../../modules/utils";
|
||||
import { accessMediaUrl, player, setPlayer } from "../../../modules/media";
|
||||
|
||||
import GObject from "ags/gobject";
|
||||
import AstalMpris from "gi://AstalMpris";
|
||||
import Pango from "gi://Pango?version=1.0";
|
||||
|
||||
|
||||
export const Media = () => {
|
||||
const connections: Map<GObject.Object, Array<number>|number> = new Map();
|
||||
|
||||
onCleanup(() => connections.forEach((id, obj) =>
|
||||
Array.isArray(id) ?
|
||||
id.forEach(id => obj.disconnect(id))
|
||||
: obj.disconnect(id)
|
||||
));
|
||||
|
||||
return <Gtk.Box class={"media"} visible={player((pl) => pl.available)}>
|
||||
<Gtk.EventControllerScroll $={(self) => {
|
||||
self.set_flags(Gtk.EventControllerScrollFlags.VERTICAL)
|
||||
}} onScroll={(_, __, dy) => {
|
||||
if(AstalMpris.get_default().players.length === 1 &&
|
||||
player.get()?.busName === AstalMpris.get_default().players[0].busName)
|
||||
return true;
|
||||
|
||||
const players = AstalMpris.get_default().players;
|
||||
|
||||
for(let i = 0; i < players.length; i++) {
|
||||
const pl = players[i];
|
||||
|
||||
if(pl.busName !== player.get().busName)
|
||||
continue;
|
||||
|
||||
if(dy > 0 && players[i-1]) {
|
||||
setPlayer(players[i-1]);
|
||||
break;
|
||||
}
|
||||
|
||||
if(dy < 0 && players[i+1]) {
|
||||
setPlayer(players[i+1]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
<Gtk.GestureClick onReleased={() => Windows.getDefault().toggle("center-window")} />
|
||||
<Gtk.EventControllerMotion onEnter={(self) => {
|
||||
const revealer = self.get_widget()!.get_last_child() as Gtk.Revealer;
|
||||
revealer.set_reveal_child(true);
|
||||
}} onLeave={(self) => {
|
||||
const revealer = self.get_widget()!.get_last_child() as Gtk.Revealer;
|
||||
revealer.set_reveal_child(false);
|
||||
}}
|
||||
/>
|
||||
<Gtk.Box spacing={4} visible={player(pl => pl.available)}>
|
||||
<With value={player(pl => pl.available)}>
|
||||
{(available: boolean) => available && <Gtk.Box>
|
||||
<Gtk.Image class={"player-icon"} iconName={
|
||||
createBinding(player.get(), "busName").as(getPlayerIconFromBusName)}
|
||||
/>
|
||||
<Gtk.Label class={"title"} label={createBinding(player.get(), "title").as(title =>
|
||||
title ?? "No Title")} maxWidthChars={20} ellipsize={Pango.EllipsizeMode.END}
|
||||
/>
|
||||
<Separator orientation={Gtk.Orientation.HORIZONTAL} size={1} margin={5}
|
||||
alpha={.3} spacing={6} />
|
||||
<Gtk.Label class={"artist"} label={createBinding(player.get(), "artist").as(artist =>
|
||||
artist ?? "No Artist")} maxWidthChars={18} ellipsize={Pango.EllipsizeMode.END}
|
||||
/>
|
||||
</Gtk.Box>}
|
||||
</With>
|
||||
</Gtk.Box>
|
||||
<Gtk.Revealer transitionType={Gtk.RevealerTransitionType.SLIDE_RIGHT} transitionDuration={260}
|
||||
revealChild={false}>
|
||||
|
||||
<With value={player(pl => pl.available)}>
|
||||
{(available: boolean) => available && <Gtk.Box class={"buttons"} spacing={4}>
|
||||
<Gtk.Box class={"extra button-row"}>
|
||||
<Gtk.Button class={"link"} iconName={"edit-paste-symbolic"}
|
||||
visible={variableToBoolean(accessMediaUrl(player.get()))}
|
||||
tooltipText={"Copy link to Clipboard"} onClicked={() => {
|
||||
const url = accessMediaUrl(player.get()).get();
|
||||
url && Clipboard.getDefault().copyAsync(url);
|
||||
}}
|
||||
/>
|
||||
</Gtk.Box>
|
||||
<Gtk.Box class={"media-controls button-row"}>
|
||||
<Gtk.Button class={"previous"} iconName={"media-skip-backward-symbolic"}
|
||||
tooltipText={"Previous"} onClicked={() =>
|
||||
player.get().canGoPrevious && player.get().previous()}
|
||||
/>
|
||||
<Gtk.Button class={"play-pause"} iconName={createBinding(player.get(), "playbackStatus").as(status =>
|
||||
status === AstalMpris.PlaybackStatus.PAUSED ?
|
||||
"media-playback-start-symbolic"
|
||||
: "media-playback-pause-symbolic")}
|
||||
tooltipText={
|
||||
createBinding(player.get(), "playbackStatus").as(status =>
|
||||
status === AstalMpris.PlaybackStatus.PAUSED ? "Play" : "Pause")
|
||||
} onClicked={() => player.get().play_pause()}
|
||||
/>
|
||||
<Gtk.Button class={"next"} iconName={"media-skip-forward-symbolic"}
|
||||
tooltipText={"Next"} onClicked={() => player.get().canGoNext &&
|
||||
player.get().next()}
|
||||
/>
|
||||
</Gtk.Box>
|
||||
</Gtk.Box>}
|
||||
</With>
|
||||
</Gtk.Revealer>
|
||||
</Gtk.Box>
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { Gtk } from "ags/gtk4";
|
||||
import { Wireplumber } from "../../../modules/volume";
|
||||
import { Notifications } from "../../../modules/notifications";
|
||||
import { Windows } from "../../../windows";
|
||||
import { Recording } from "../../../modules/recording";
|
||||
import { Accessor, createBinding, createComputed, With } from "ags";
|
||||
import { variableToBoolean } from "../../../modules/utils";
|
||||
import { Bluetooth } from "../../../modules/bluetooth";
|
||||
|
||||
import GObject from "ags/gobject";
|
||||
import AstalBluetooth from "gi://AstalBluetooth";
|
||||
import AstalNetwork from "gi://AstalNetwork";
|
||||
import AstalWp from "gi://AstalWp";
|
||||
|
||||
|
||||
export const Status = () =>
|
||||
<Gtk.Button class={createBinding(Windows.getDefault(), "openWindows").as((openWins) =>
|
||||
openWins.includes("control-center") ? "open status" : "status")}
|
||||
onClicked={() => Windows.getDefault().toggle("control-center")}>
|
||||
|
||||
<Gtk.Box>
|
||||
<Gtk.Box class={"volume-indicators"} spacing={5}>
|
||||
<VolumeStatus class="sink" endpoint={Wireplumber.getDefault().getDefaultSink()}
|
||||
icon={createBinding(Wireplumber.getDefault().getDefaultSink(), "volumeIcon").as(icon =>
|
||||
!Wireplumber.getDefault().isMutedSink() &&
|
||||
Wireplumber.getDefault().getSinkVolume() > 0 ?
|
||||
icon
|
||||
: "audio-volume-muted-symbolic")
|
||||
} />
|
||||
|
||||
<VolumeStatus class="source" endpoint={Wireplumber.getDefault().getDefaultSource()}
|
||||
icon={createBinding(Wireplumber.getDefault().getDefaultSource(), "volumeIcon").as(icon =>
|
||||
!Wireplumber.getDefault().isMutedSource() &&
|
||||
Wireplumber.getDefault().getSourceVolume() > 0 ?
|
||||
icon
|
||||
: "microphone-sensitivity-muted-symbolic")
|
||||
} />
|
||||
</Gtk.Box>
|
||||
<Gtk.Revealer revealChild={createBinding(Recording.getDefault(), "recording")}
|
||||
transitionDuration={500} transitionType={Gtk.RevealerTransitionType.SLIDE_LEFT}>
|
||||
|
||||
<Gtk.Box>
|
||||
<Gtk.Image class={"recording state"} iconName={"media-record-symbolic"}
|
||||
css={"margin-right: 6px;"} />
|
||||
|
||||
<Gtk.Label class={"rec-time"} label={
|
||||
createBinding(Recording.getDefault(), "recordingTime")
|
||||
} />
|
||||
</Gtk.Box>
|
||||
</Gtk.Revealer>
|
||||
<StatusIcons />
|
||||
</Gtk.Box>
|
||||
</Gtk.Button> as Gtk.Button;
|
||||
|
||||
function VolumeStatus(props: { class?: string, endpoint: AstalWp.Endpoint, icon?: (string|Accessor<string>) }) {
|
||||
return <Gtk.Box spacing={2} class={props.class} $={(self) => {
|
||||
const conns: Map<GObject.Object, number> = new Map();
|
||||
const controllerScroll = Gtk.EventControllerScroll.new(Gtk.EventControllerScrollFlags.VERTICAL
|
||||
| Gtk.EventControllerScrollFlags.KINETIC);
|
||||
|
||||
conns.set(controllerScroll, controllerScroll.connect("scroll", (_, _dx, dy) => {
|
||||
console.log`Scrolled! dx: ${_dx}; dy: ${dy}`;
|
||||
dy > 0 ?
|
||||
Wireplumber.getDefault().decreaseEndpointVolume(props.endpoint, 5)
|
||||
: Wireplumber.getDefault().increaseEndpointVolume(props.endpoint, 5);
|
||||
|
||||
return true;
|
||||
}));
|
||||
|
||||
conns.set(self, self.connect("destroy", () => conns.forEach((id, obj) =>
|
||||
obj.disconnect(id))));
|
||||
}}>
|
||||
|
||||
{props.icon && <Gtk.Image iconName={props.icon} />}
|
||||
<Gtk.Label class={"volume"} label={createBinding(props.endpoint, "volume").as(vol =>
|
||||
`${Math.floor(vol * 100)}%`)} />
|
||||
</Gtk.Box> as Gtk.Box;
|
||||
}
|
||||
|
||||
function StatusIcons() {
|
||||
return <Gtk.Box class={"status-icons"} spacing={8}>
|
||||
<Gtk.Image iconName={createComputed([
|
||||
createBinding(AstalBluetooth.get_default(), "isPowered"),
|
||||
createBinding(AstalBluetooth.get_default(), "isConnected")
|
||||
], (powered, connected) => {
|
||||
return powered ? (
|
||||
connected ?
|
||||
"bluetooth-active-symbolic"
|
||||
: "bluetooth-symbolic"
|
||||
) : "bluetooth-disabled-symbolic"
|
||||
})} class={"bluetooth state"} visible={
|
||||
createBinding(Bluetooth.getDefault(), "adapter").as(Boolean)
|
||||
}
|
||||
/>
|
||||
|
||||
<Gtk.Box visible={createBinding(AstalNetwork.get_default(), "primary").as(primary =>
|
||||
primary !== AstalNetwork.Primary.UNKNOWN)}>
|
||||
|
||||
<With value={createBinding(AstalNetwork.get_default(), "primary")}>
|
||||
{(primary: AstalNetwork.Primary) => {
|
||||
let device: AstalNetwork.Wifi|AstalNetwork.Wired;
|
||||
switch(primary) {
|
||||
case AstalNetwork.Primary.WIRED:
|
||||
device = AstalNetwork.get_default().wired;
|
||||
break;
|
||||
case AstalNetwork.Primary.WIFI:
|
||||
device = AstalNetwork.get_default().wifi;
|
||||
break;
|
||||
|
||||
default:
|
||||
return <Gtk.Image iconName={"network-no-route-symbolic"} />;
|
||||
}
|
||||
|
||||
return <Gtk.Image iconName={createBinding(device, "iconName")} />;
|
||||
}}
|
||||
</With>
|
||||
</Gtk.Box>
|
||||
|
||||
<Gtk.Box>
|
||||
<Gtk.Image class={"bell state"} iconName={createBinding(
|
||||
Notifications.getDefault().getNotifd(), "dontDisturb").as(dnd => dnd ?
|
||||
"minus-circle-filled-symbolic"
|
||||
: "preferences-system-notifications-symbolic")
|
||||
}
|
||||
/>
|
||||
<Gtk.Image iconName={"circle-filled-symbolic"} class={"notification-count"}
|
||||
visible={variableToBoolean(createBinding(Notifications.getDefault(), "history"))}
|
||||
/>
|
||||
</Gtk.Box>
|
||||
</Gtk.Box>
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { createBinding, createComputed, For, With } from "ags";
|
||||
import { Gdk, Gtk } from "ags/gtk4";
|
||||
import { variableToBoolean } from "../../../modules/utils";
|
||||
|
||||
import GObject from "gi://GObject?version=2.0";
|
||||
import AstalTray from "gi://AstalTray"
|
||||
import Gio from "gi://Gio?version=2.0";
|
||||
|
||||
|
||||
const astalTray = AstalTray.get_default();
|
||||
|
||||
export const Tray = () => {
|
||||
const items = createBinding(astalTray, "items").as(items => items.filter(item => item?.gicon));
|
||||
|
||||
return <Gtk.Box class={"tray"} visible={variableToBoolean(items)} spacing={10}>
|
||||
<For each={items}>
|
||||
{(item: AstalTray.TrayItem) => <Gtk.Box class={"item"}>
|
||||
<With value={createComputed([
|
||||
createBinding(item, "actionGroup"),
|
||||
createBinding(item, "menuModel")
|
||||
])}>
|
||||
{([actionGroup, menuModel]: [Gio.ActionGroup, Gio.MenuModel]) => {
|
||||
const popover = Gtk.PopoverMenu.new_from_model(menuModel);
|
||||
popover.insert_action_group("dbusmenu", actionGroup);
|
||||
popover.hasArrow = false;
|
||||
|
||||
return <Gtk.Box class={"item"} tooltipMarkup={
|
||||
createBinding(item, "tooltipMarkup")
|
||||
} tooltipText={
|
||||
createBinding(item, "tooltipText")
|
||||
} $={(self) => {
|
||||
const conns: Map<GObject.Object, number> = new Map();
|
||||
const gestureClick = Gtk.GestureClick.new();
|
||||
gestureClick.set_button(0);
|
||||
|
||||
self.add_controller(gestureClick);
|
||||
|
||||
conns.set(gestureClick, gestureClick.connect("released", (gesture, _, x, y) => {
|
||||
if(gesture.get_current_button() === Gdk.BUTTON_PRIMARY) {
|
||||
item.activate(x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
if(gesture.get_current_button() === Gdk.BUTTON_SECONDARY) {
|
||||
item.about_to_show();
|
||||
popover.popup();
|
||||
}
|
||||
}))
|
||||
}}>
|
||||
<Gtk.Image gicon={createBinding(item, "gicon")} pixelSize={16} />
|
||||
{popover}
|
||||
</Gtk.Box>;
|
||||
}}
|
||||
</With>
|
||||
</Gtk.Box>}
|
||||
</For>
|
||||
</Gtk.Box>
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import { Gtk } from "ags/gtk4";
|
||||
import { getAppIcon, getSymbolicIcon } from "../../../modules/apps";
|
||||
import { Separator } from "../../../widget/Separator";
|
||||
import { generalConfig } from "../../../app";
|
||||
import { createBinding, createComputed, createState, For, With } from "ags";
|
||||
import { variableToBoolean } from "../../../modules/utils";
|
||||
|
||||
import AstalHyprland from "gi://AstalHyprland";
|
||||
|
||||
|
||||
const [showNumbers, setShowNumbers] = createState(false);
|
||||
export const showWorkspaceNumber = (show: boolean) =>
|
||||
setShowNumbers(show);
|
||||
|
||||
|
||||
export const Workspaces = () => {
|
||||
const workspaces = createBinding(AstalHyprland.get_default(), "workspaces"),
|
||||
defaultWorkspaces = workspaces.as(wss =>
|
||||
wss.filter(ws => ws.id > 0).sort((a, b) => a.id - b.id)),
|
||||
specialWorkspaces = workspaces.as(wss =>
|
||||
wss.filter(ws => ws.id < 0).sort((a, b) => a.id - b.id)),
|
||||
focusedWorkspace = createBinding(AstalHyprland.get_default(), "focusedWorkspace");
|
||||
|
||||
|
||||
return <Gtk.Box class={"workspaces-row"} visible={createComputed([
|
||||
workspaces.as(wss => wss.length <= 1),
|
||||
generalConfig.bindProperty("workspaces.hide_if_single", "boolean")
|
||||
], (hideable, enabled) => enabled && hideable ? false : true
|
||||
)}>
|
||||
<Gtk.Box class={"special-workspaces"} spacing={4}>
|
||||
<For each={specialWorkspaces}>
|
||||
{(ws: AstalHyprland.Workspace) =>
|
||||
<Gtk.Button class={"workspace"}
|
||||
tooltipText={createBinding(ws, "name").as(name => {
|
||||
name = name.replace(/^special\:/, "");
|
||||
return name.charAt(0).toUpperCase().concat(name.substring(1, name.length));
|
||||
})} onClicked={() => AstalHyprland.get_default().dispatch(
|
||||
"togglespecialworkspace", ws.name.replace(/^special[:]/, "")
|
||||
)}>
|
||||
|
||||
<With value={createBinding(ws, "lastClient")}>
|
||||
{(lastClient: AstalHyprland.Client|null) => lastClient &&
|
||||
<Gtk.Image class="last-client" iconName={
|
||||
createBinding(lastClient, "initialClass").as(initialClass =>
|
||||
getSymbolicIcon(initialClass) ?? getAppIcon(initialClass) ??
|
||||
"application-x-executable-symbolic")}
|
||||
/>
|
||||
}
|
||||
</With>
|
||||
</Gtk.Button>
|
||||
}
|
||||
</For>
|
||||
</Gtk.Box>
|
||||
<Gtk.Revealer transitionType={Gtk.RevealerTransitionType.SLIDE_RIGHT}
|
||||
transitionDuration={220} revealChild={variableToBoolean(specialWorkspaces)}>
|
||||
|
||||
<Separator alpha={.2} orientation={Gtk.Orientation.HORIZONTAL}
|
||||
margin={12} spacing={8} visible={variableToBoolean(specialWorkspaces)}
|
||||
/>
|
||||
</Gtk.Revealer>
|
||||
<Gtk.Box class={"default-workspaces"} spacing={4}>
|
||||
<Gtk.EventControllerScroll $={(self) => self.set_flags(Gtk.EventControllerScrollFlags.VERTICAL)}
|
||||
onScroll={(_, __, dy) => {
|
||||
dy > 0 ?
|
||||
AstalHyprland.get_default().dispatch("workspace", "e-1")
|
||||
: AstalHyprland.get_default().dispatch("workspace", "e+1");
|
||||
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
<Gtk.EventControllerMotion onEnter={() => setShowNumbers(true)}
|
||||
onLeave={() => setShowNumbers(false)}
|
||||
/>
|
||||
<For each={defaultWorkspaces}>
|
||||
{(ws: AstalHyprland.Workspace, i) => {
|
||||
const showId = createComputed([
|
||||
generalConfig.bindProperty("workspaces.always_show_id", "boolean").as(Boolean),
|
||||
generalConfig.bindProperty("workspaces.enable_helper", "boolean").as(Boolean),
|
||||
showNumbers,
|
||||
i
|
||||
], (alwaysShowIds, enableHelper, showIds, i) => {
|
||||
if(enableHelper && !alwaysShowIds) {
|
||||
const previousWorkspace = defaultWorkspaces.get()[i-1];
|
||||
const nextWorkspace = defaultWorkspaces.get()[i+1];
|
||||
|
||||
if((defaultWorkspaces.get().filter((_, ii) => ii < i).length > 0 &&
|
||||
previousWorkspace?.id < (ws.id-1)) ||
|
||||
(defaultWorkspaces.get().filter((_, ii) => ii > i).length > 0 &&
|
||||
nextWorkspace?.id > (ws.id+1))
|
||||
|| (i === 0 && ws.id > 1)) {
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return alwaysShowIds || showIds;
|
||||
});
|
||||
|
||||
return <Gtk.Button class={createComputed([
|
||||
createBinding(AstalHyprland.get_default(), "focusedWorkspace"),
|
||||
showId
|
||||
], (focusedWs, showWsNumbers) =>
|
||||
`workspace ${focusedWs.id === ws.id ? "focus" : ""} ${
|
||||
showWsNumbers ? "show" : ""}`
|
||||
)} tooltipText={createComputed([
|
||||
createBinding(ws, "lastClient"),
|
||||
createBinding(AstalHyprland.get_default(), "focusedWorkspace")
|
||||
], (lastClient, focusWs) => focusWs.id === ws.id ? "" :
|
||||
`workspace ${ws.id}${ lastClient ? ` - ${
|
||||
!lastClient.title.toLowerCase().includes(lastClient.class) ?
|
||||
`${lastClient.get_class()}: `
|
||||
: ""
|
||||
} ${lastClient.title}` : "" }`
|
||||
)} onClicked={() => focusedWorkspace.get()?.id !== ws.id && ws.focus()}>
|
||||
|
||||
<With value={createBinding(ws, "lastClient")}>
|
||||
{(lastClient: AstalHyprland.Client) =>
|
||||
<Gtk.Box class={"last-client"} hexpand>
|
||||
<Gtk.Revealer transitionDuration={280} revealChild={showId}
|
||||
transitionType={focusedWorkspace.as(
|
||||
fws => fws.id !== ws.id ?
|
||||
Gtk.RevealerTransitionType.SLIDE_LEFT
|
||||
: Gtk.RevealerTransitionType.SLIDE_RIGHT
|
||||
)}>
|
||||
<Gtk.Label label={createBinding(ws, "id").as(String)}
|
||||
class={"id"} hexpand
|
||||
/>
|
||||
</Gtk.Revealer>
|
||||
{lastClient && <Gtk.Image class={"last-client-icon"} iconName={
|
||||
createBinding(lastClient, "initialClass").as(initialClass =>
|
||||
getSymbolicIcon(initialClass) ?? getAppIcon(initialClass) ??
|
||||
"application-x-executable-symbolic")}
|
||||
hexpand vexpand visible={createBinding(AstalHyprland.get_default(), "focusedWorkspace")
|
||||
.as(fws => fws.id !== ws.id)}
|
||||
/>}
|
||||
</Gtk.Box>
|
||||
}
|
||||
</With>
|
||||
</Gtk.Button>
|
||||
}}
|
||||
</For>
|
||||
</Gtk.Box>
|
||||
</Gtk.Box>
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Gdk, Gtk } from "ags/gtk4";
|
||||
import { Separator } from "../widget/Separator";
|
||||
import { PopupWindow } from "../widget/PopupWindow";
|
||||
import { BigMedia } from "../widget/center-window/BigMedia";
|
||||
import { time, variableToBoolean } from "../modules/utils";
|
||||
import { Separator } from "../../widget/Separator";
|
||||
import { PopupWindow } from "../../widget/PopupWindow";
|
||||
import { BigMedia } from "./widgets/BigMedia";
|
||||
import { time, variableToBoolean } from "../../modules/utils";
|
||||
import { createBinding } from "ags";
|
||||
import { player } from "../../modules/media";
|
||||
|
||||
import AstalMpris from "gi://AstalMpris";
|
||||
import { player } from "../modules/media";
|
||||
|
||||
export const CenterWindow = (mon: number) =>
|
||||
<PopupWindow namespace={"center-window"} marginTop={10} monitor={mon}
|
||||
@@ -0,0 +1,237 @@
|
||||
import { createBinding, For } from "ags";
|
||||
import { register } from "ags/gobject";
|
||||
import { Astal, Gtk } from "ags/gtk4";
|
||||
import { Clipboard } from "../../../modules/clipboard";
|
||||
import { accessMediaUrl } from "../../../modules/media";
|
||||
import { player, setPlayer } from "../../../modules/media";
|
||||
import { pathToURI, variableToBoolean } from "../../../modules/utils";
|
||||
|
||||
import AstalMpris from "gi://AstalMpris";
|
||||
import Pango from "gi://Pango?version=1.0";
|
||||
import Adw from "gi://Adw?version=1";
|
||||
import GLib from "gi://GLib?version=2.0";
|
||||
|
||||
|
||||
let dragTimer: (GLib.Source|undefined);
|
||||
|
||||
export const BigMedia = () => {
|
||||
const availablePlayers = createBinding(AstalMpris.get_default(), "players").as(pls =>
|
||||
pls.filter(p => p.available));
|
||||
|
||||
const carousel = <Adw.Carousel orientation={Gtk.Orientation.HORIZONTAL} spacing={6}
|
||||
onPageChanged={(self, num) => {
|
||||
const page = self.get_nth_page(num);
|
||||
if(page instanceof PlayerWidget && player.get().busName !== page.player.busName)
|
||||
setPlayer(page.player);
|
||||
}}>
|
||||
<For each={availablePlayers.as(players => players.sort(pl =>
|
||||
pl.busName === player.get().busName ? -1 : 1))}>
|
||||
|
||||
{(player: AstalMpris.Player) => <PlayerWidget player={player} />}
|
||||
</For>
|
||||
</Adw.Carousel> as Adw.Carousel;
|
||||
|
||||
return <Gtk.Box class={"big-media"} orientation={Gtk.Orientation.VERTICAL} widthRequest={255}
|
||||
visible={variableToBoolean(availablePlayers)}>
|
||||
|
||||
{carousel}
|
||||
<Gtk.Revealer revealChild={availablePlayers.as(pls => pls.length > 1)} transitionDuration={300}
|
||||
transitionType={Gtk.RevealerTransitionType.SLIDE_UP}>
|
||||
|
||||
<Adw.CarouselIndicatorDots orientation={Gtk.Orientation.HORIZONTAL} carousel={carousel} />
|
||||
</Gtk.Revealer>
|
||||
</Gtk.Box> as Gtk.Box;
|
||||
}
|
||||
|
||||
@register({ GTypeName: "PlayerWidget" })
|
||||
class PlayerWidget extends Gtk.Box {
|
||||
#player!: AstalMpris.Player;
|
||||
#copyClickTimeout?: GLib.Source;
|
||||
|
||||
get player() { return this.#player; }
|
||||
|
||||
constructor({ player }: { player: AstalMpris.Player }) {
|
||||
super();
|
||||
|
||||
this.setPlayer(player);
|
||||
this.set_orientation(Gtk.Orientation.VERTICAL);
|
||||
this.set_hexpand(true);
|
||||
|
||||
this.append(
|
||||
<Gtk.Revealer hexpand={false} revealChild={
|
||||
createBinding(player, "coverArt").as(Boolean)
|
||||
} transitionType={Gtk.RevealerTransitionType.SLIDE_LEFT} transitionDuration={300}>
|
||||
|
||||
<Gtk.Box class={"image"} css={createBinding(player, "artUrl").as((art) =>
|
||||
`background-image: url("${pathToURI(art)}");`)}
|
||||
hexpand={false} vexpand={false} widthRequest={132} heightRequest={128}
|
||||
valign={Gtk.Align.START} halign={Gtk.Align.CENTER}
|
||||
/>
|
||||
</Gtk.Revealer> as Gtk.Revealer
|
||||
);
|
||||
|
||||
this.append(
|
||||
<Gtk.Box class={"info"} orientation={Gtk.Orientation.VERTICAL}
|
||||
valign={Gtk.Align.CENTER} vexpand hexpand>
|
||||
|
||||
<Gtk.Label class={"title"} tooltipText={
|
||||
createBinding(player, "title").as(title => title ?? "No Title")
|
||||
} label={
|
||||
createBinding(player, "title").as(title => title ?? "No Title")
|
||||
} ellipsize={Pango.EllipsizeMode.END} maxWidthChars={25}
|
||||
/>
|
||||
<Gtk.Label class={"artist"} tooltipText={
|
||||
createBinding(player, "artist").as(artist => artist ?? "No Artist")
|
||||
} label={
|
||||
createBinding(player, "artist").as(artist => artist ?? "No Artist")
|
||||
} ellipsize={Pango.EllipsizeMode.END} maxWidthChars={28}
|
||||
/>
|
||||
</Gtk.Box> as Gtk.Box
|
||||
);
|
||||
|
||||
this.append(
|
||||
<Gtk.Box class={"progress"} hexpand visible={createBinding(player, "canSeek")}>
|
||||
<Astal.Slider hexpand max={createBinding(player, "length").as(Math.floor)}
|
||||
value={createBinding(player, "position").as(Math.floor)}
|
||||
onChangeValue={(_, type, value) => {
|
||||
if(type === undefined || type === null)
|
||||
return;
|
||||
|
||||
if(!dragTimer) {
|
||||
dragTimer = setTimeout(() =>
|
||||
player.position = Math.floor(value)
|
||||
, 200);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
dragTimer.destroy();
|
||||
dragTimer = setTimeout(() =>
|
||||
player.position = Math.floor(value)
|
||||
, 200);
|
||||
}}
|
||||
/>
|
||||
</Gtk.Box> as Gtk.Box
|
||||
);
|
||||
|
||||
this.append(
|
||||
<Gtk.CenterBox class={"bottom"} hexpand marginBottom={6}>
|
||||
<Gtk.Label class={"elapsed"} xalign={0} yalign={0}
|
||||
halign={Gtk.Align.START} label={createBinding(player, "position").as(pos => {
|
||||
const sec = Math.floor(pos % 60);
|
||||
return pos > 0 && player.length > 0 ?
|
||||
`${Math.floor(pos / 60)}:${sec < 10 ? "0" : ""}${sec}`
|
||||
: "0:00";
|
||||
})} $type="start"
|
||||
/>
|
||||
|
||||
<Gtk.Box spacing={4} $type="center">
|
||||
<Gtk.Box class={"extra button-row"}>
|
||||
<Gtk.Button class={"link"}
|
||||
tooltipText={"Copy link to clipboard"}
|
||||
visible={variableToBoolean(accessMediaUrl(player))}
|
||||
onClicked={(self) => {
|
||||
const url = accessMediaUrl(player).get();
|
||||
// a widget that supports adding multiple icons and allows switching
|
||||
// through them would be pretty nice!! (i'll probably do this later)
|
||||
url &&
|
||||
Clipboard.getDefault().copyAsync(url).then(() => {
|
||||
if(this.#copyClickTimeout && !this.#copyClickTimeout.is_destroyed())
|
||||
this.#copyClickTimeout.destroy();
|
||||
|
||||
(self.get_child() as Gtk.Stack).set_visible_child_name("done-icon");
|
||||
this.#copyClickTimeout = setTimeout(() => {
|
||||
(self.get_child() as Gtk.Stack).set_visible_child_name("copy-icon");
|
||||
this.#copyClickTimeout!.destroy();
|
||||
this.#copyClickTimeout = undefined;
|
||||
}, 1100);
|
||||
}).catch(() => {
|
||||
if(this.#copyClickTimeout && !this.#copyClickTimeout.is_destroyed())
|
||||
this.#copyClickTimeout.destroy();
|
||||
|
||||
(self.get_child() as Gtk.Stack).set_visible_child_name("error-icon");
|
||||
this.#copyClickTimeout = setTimeout(() => {
|
||||
(self.get_child() as Gtk.Stack).set_visible_child_name("copy-icon");
|
||||
this.#copyClickTimeout!.destroy();
|
||||
this.#copyClickTimeout = undefined;
|
||||
}, 900);
|
||||
});
|
||||
}}>
|
||||
|
||||
<Gtk.Stack transitionType={Gtk.StackTransitionType.CROSSFADE}
|
||||
transitionDuration={340}>
|
||||
|
||||
<Gtk.StackPage name={"copy-icon"} child={
|
||||
<Gtk.Image iconName={"edit-paste-symbolic"} /> as Gtk.Widget
|
||||
} />
|
||||
<Gtk.StackPage name={"done-icon"} child={
|
||||
<Gtk.Image iconName={"object-select-symbolic"} /> as Gtk.Widget
|
||||
} />
|
||||
<Gtk.StackPage name={"error-icon"} child={
|
||||
<Gtk.Image iconName={"window-close-symbolic"} /> as Gtk.Widget
|
||||
} />
|
||||
</Gtk.Stack>
|
||||
</Gtk.Button>
|
||||
</Gtk.Box>
|
||||
<Gtk.Box class={"media-controls button-row"}>
|
||||
<Gtk.Button class={"shuffle"} visible={createBinding(player, "shuffleStatus").as(status =>
|
||||
status !== AstalMpris.Shuffle.UNSUPPORTED)} iconName={
|
||||
createBinding(player, "shuffleStatus").as(status => status === AstalMpris.Shuffle.ON ?
|
||||
"media-playlist-shuffle-symbolic"
|
||||
: "media-playlist-consecutive-symbolic")} tooltipText={
|
||||
createBinding(player, "shuffleStatus").as(status => status === AstalMpris.Shuffle.ON ?
|
||||
"Shuffle"
|
||||
: "No shuffle")} onClicked={() => player.shuffle()}
|
||||
/>
|
||||
<Gtk.Button class={"previous"} iconName={"media-skip-backward-symbolic"}
|
||||
tooltipText={"Previous"} onClicked={() => player.canGoPrevious && player.previous()}
|
||||
/>
|
||||
<Gtk.Button class={"play-pause"} tooltipText={
|
||||
createBinding(player, "playbackStatus").as(status =>
|
||||
status === AstalMpris.PlaybackStatus.PLAYING ? "Pause" : "Play")}
|
||||
iconName={createBinding(player, "playbackStatus").as(status =>
|
||||
status === AstalMpris.PlaybackStatus.PLAYING ?
|
||||
"media-playback-pause-symbolic"
|
||||
: "media-playback-start-symbolic")} onClicked={() => player.play_pause()}
|
||||
/>
|
||||
<Gtk.Button class={"next"} iconName={"media-skip-forward-symbolic"}
|
||||
tooltipText={"Next"} onClicked={() => player.canGoNext && player.next()}
|
||||
/>
|
||||
<Gtk.Button class={"repeat"} iconName={createBinding(player, "loopStatus").as(status => {
|
||||
if(status === AstalMpris.Loop.TRACK)
|
||||
return "media-playlist-repeat-song-symbolic";
|
||||
|
||||
if(status === AstalMpris.Loop.PLAYLIST)
|
||||
return "media-playlist-repeat-symbolic";
|
||||
|
||||
return "loop-arrow-symbolic";
|
||||
})} visible={createBinding(player, "loopStatus").as(status =>
|
||||
status !== AstalMpris.Loop.UNSUPPORTED)}
|
||||
tooltipText={createBinding(player, "loopStatus").as(status => {
|
||||
if(status === AstalMpris.Loop.TRACK)
|
||||
return "Loop song";
|
||||
|
||||
if(status === AstalMpris.Loop.PLAYLIST)
|
||||
return "Loop playlist";
|
||||
|
||||
return "No loop";
|
||||
})} onClicked={() => player.loop()}
|
||||
/>
|
||||
</Gtk.Box>
|
||||
</Gtk.Box>
|
||||
<Gtk.Label class={"length"} xalign={1} yalign={0}
|
||||
halign={Gtk.Align.END} label={createBinding(player, "length").as(len => { /* bananananananana */
|
||||
const sec = Math.floor(len % 60);
|
||||
return (len > 0 && Number.isFinite(len)) ?
|
||||
`${Math.floor(len / 60)}:${sec < 10 ? "0" : ""}${sec}`
|
||||
: "0:00";
|
||||
})} $type="end"
|
||||
/>
|
||||
</Gtk.CenterBox> as Gtk.CenterBox
|
||||
);
|
||||
}
|
||||
|
||||
setPlayer(player: AstalMpris.Player) {
|
||||
this.#player = player;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Astal, Gtk } from "ags/gtk4";
|
||||
import { PopupWindow } from "../widget/PopupWindow";
|
||||
import { QuickActions } from "../widget/control-center/QuickActions";
|
||||
import { NotifHistory } from "../widget/control-center/NotifHistory";
|
||||
import { Tiles } from "../widget/control-center/Tiles";
|
||||
import { Sliders } from "../widget/control-center/Sliders";
|
||||
import { PopupWindow } from "../../widget/PopupWindow";
|
||||
import { QuickActions } from "./widgets/QuickActions";
|
||||
import { NotifHistory } from "./widgets/NotifHistory";
|
||||
import { Tiles } from "./widgets/tiles";
|
||||
import { Sliders } from "./widgets/Sliders";
|
||||
|
||||
|
||||
export const ControlCenter = (mon: number) =>
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Gtk } from "ags/gtk4";
|
||||
import { HistoryNotification, Notifications } from "../../../modules/notifications";
|
||||
import { NotificationWidget } from "../../../widget/Notification";
|
||||
import { tr } from "../../../i18n/intl";
|
||||
import { createBinding, For } from "ags";
|
||||
|
||||
import AstalNotifd from "gi://AstalNotifd";
|
||||
|
||||
|
||||
export const NotifHistory = () =>
|
||||
<Gtk.Box orientation={Gtk.Orientation.VERTICAL}
|
||||
class={createBinding(Notifications.getDefault(), "history").as(history =>
|
||||
`notif-history ${history.length < 1 ? "hide" : ""}`)} vexpand={false}>
|
||||
|
||||
<Gtk.ScrolledWindow class={"history-scrollable"} hscrollbarPolicy={Gtk.PolicyType.NEVER}
|
||||
vscrollbarPolicy={Gtk.PolicyType.AUTOMATIC} propagateNaturalHeight={true}
|
||||
onShow={(self) => {
|
||||
if(!(self.get_child()! as Gtk.Viewport).get_child()) return;
|
||||
|
||||
self.minContentHeight =
|
||||
((self.get_child()! as Gtk.Viewport).get_child() as Gtk.Box
|
||||
).get_first_child()!.get_allocation().height
|
||||
|| 0;
|
||||
}}>
|
||||
|
||||
<Gtk.Box class={"notifications"} hexpand={true} orientation={Gtk.Orientation.VERTICAL}
|
||||
spacing={4} valign={Gtk.Align.START}>
|
||||
|
||||
<For each={createBinding(Notifications.getDefault(), "history")}>
|
||||
{(notif: AstalNotifd.Notification|HistoryNotification) =>
|
||||
<NotificationWidget notification={notif} showTime={true}
|
||||
actionClose={(n) => Notifications.getDefault().removeHistory(n.id)}
|
||||
actionClicked={(n) => Notifications.getDefault().removeHistory(n.id)}
|
||||
/>}
|
||||
</For>
|
||||
</Gtk.Box>
|
||||
</Gtk.ScrolledWindow>
|
||||
|
||||
<Gtk.Box class={"button-row"} hexpand>
|
||||
<Gtk.Button class={"clear-all"} halign={Gtk.Align.END}
|
||||
onClicked={() => Notifications.getDefault().clearHistory()}>
|
||||
|
||||
<Gtk.Box hexpand>
|
||||
<Gtk.Image class={"icon"} iconName={"edit-clear-all-symbolic"}
|
||||
css={"margin-right: 6px;"} />
|
||||
<Gtk.Label label={tr("clear")} />
|
||||
</Gtk.Box>
|
||||
</Gtk.Button>
|
||||
</Gtk.Box>
|
||||
</Gtk.Box> as Gtk.Box;
|
||||
@@ -0,0 +1,176 @@
|
||||
import { Gtk } from "ags/gtk4";
|
||||
import { Separator } from "../../../widget/Separator";
|
||||
import { Accessor, createRoot } from "ags";
|
||||
import { transformWidget, variableToBoolean, WidgetNodeType } from "../../../modules/utils";
|
||||
|
||||
import Pango from "gi://Pango?version=1.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 = {
|
||||
title: string | Accessor<string>;
|
||||
description?: string | Accessor<string>;
|
||||
tooltipText?: string | Accessor<string>;
|
||||
tooltipMarkup?: string | Accessor<string>;
|
||||
actionClicked?: () => void;
|
||||
};
|
||||
|
||||
export type HeaderButton = {
|
||||
label?: string|Accessor<string>;
|
||||
icon: string|Accessor<string>;
|
||||
tooltipText?: string | Accessor<string>;
|
||||
tooltipMarkup?: string | Accessor<string>;
|
||||
actionClicked?: () => void;
|
||||
};
|
||||
|
||||
export class Page {
|
||||
#title: string;
|
||||
#description?: string;
|
||||
#orientation: Gtk.Orientation|Accessor<
|
||||
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;
|
||||
|
||||
constructor(props: PageProps) {
|
||||
this.#id = props.id;
|
||||
this.#title = props.title;
|
||||
this.#description = props.description;
|
||||
this.#create = props.content;
|
||||
this.actionClosed = props.actionClosed;
|
||||
|
||||
if(props.orientation != null)
|
||||
this.#orientation = props.orientation;
|
||||
|
||||
if(props.spacing != null)
|
||||
this.#spacing = props.spacing;
|
||||
|
||||
if(props.headerButtons != null)
|
||||
this.#headerButtons = props.headerButtons;
|
||||
|
||||
if(props.$ != null)
|
||||
this.#setup = props.$;
|
||||
}
|
||||
|
||||
public create(): Gtk.Box {
|
||||
return createRoot((dispose) =>
|
||||
<Gtk.Box hexpand class={`page container ${this.#id ?? ""}`} cssName={"page"} name={"page"}
|
||||
orientation={Gtk.Orientation.VERTICAL} onUnmap={() => dispose()}
|
||||
$={this.#setup}>
|
||||
|
||||
<Gtk.Box class={"header"} orientation={Gtk.Orientation.VERTICAL}>
|
||||
<Gtk.Box class={"top"} hexpand>
|
||||
<Gtk.Box orientation={Gtk.Orientation.VERTICAL} hexpand>
|
||||
<Gtk.Label class={"title"} label={this.#title} xalign={0}
|
||||
ellipsize={Pango.EllipsizeMode.END} />
|
||||
|
||||
<Gtk.Label class={"description"} label={this.#description}
|
||||
xalign={0} ellipsize={Pango.EllipsizeMode.END}
|
||||
visible={variableToBoolean(this.#description)} />
|
||||
</Gtk.Box>
|
||||
<Gtk.Box class={"button-row"} visible={variableToBoolean(this.#headerButtons)}
|
||||
hexpand={false}>
|
||||
|
||||
{this.#headerButtons && transformWidget(this.#headerButtons, (button) =>
|
||||
<Gtk.Button class={"header-button"} label={button.label}
|
||||
iconName={button.icon} onClicked={() => button.actionClicked?.()}
|
||||
tooltipText={button.tooltipText} tooltipMarkup={button.tooltipMarkup}
|
||||
/>
|
||||
)}
|
||||
</Gtk.Box>
|
||||
</Gtk.Box>
|
||||
</Gtk.Box>
|
||||
<Gtk.Box class={"content"} hexpand={false} orientation={this.#orientation}
|
||||
spacing={this.#spacing}>
|
||||
|
||||
{this.#create()}
|
||||
</Gtk.Box>
|
||||
<Separator alpha={.2} spacing={6} orientation={Gtk.Orientation.VERTICAL}
|
||||
visible={variableToBoolean(this.#bottomButtons)}
|
||||
/>
|
||||
<Gtk.Box class={"bottom-buttons"} orientation={Gtk.Orientation.VERTICAL}
|
||||
visible={variableToBoolean(this.#bottomButtons)} spacing={2}>
|
||||
|
||||
{this.#bottomButtons && transformWidget(this.#bottomButtons, (button) =>
|
||||
<Gtk.Button onClicked={() => button?.actionClicked?.()} 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>
|
||||
</Gtk.Box> as Gtk.Box
|
||||
);
|
||||
}
|
||||
|
||||
public static getContent(pageWidget: Gtk.Box) {
|
||||
return pageWidget.get_first_child()!.get_next_sibling()! as Gtk.Box;
|
||||
}
|
||||
}
|
||||
|
||||
export function PageButton({ onUnmap, ...props }: {
|
||||
class?: string | Accessor<string>;
|
||||
icon?: string | Accessor<string>;
|
||||
title: string | Accessor<string>;
|
||||
endWidget?: WidgetNodeType;
|
||||
description?: string | Accessor<string>;
|
||||
extraButtons?: Array<WidgetNodeType> | WidgetNodeType;
|
||||
maxWidthChars?: number | Accessor<number>;
|
||||
onUnmap?: (self: Gtk.Box) => void;
|
||||
actionClicked?: (self: Gtk.Button) => void;
|
||||
tooltipText?: string | Accessor<string>;
|
||||
tooltipMarkup?: string | Accessor<string>;
|
||||
}): Gtk.Box {
|
||||
return <Gtk.Box onUnmap={(self) => onUnmap?.(self)} class={"page-button"}>
|
||||
<Gtk.Button onClicked={props.actionClicked} class={props.class} hexpand
|
||||
tooltipText={props.tooltipText} tooltipMarkup={props.tooltipMarkup}>
|
||||
|
||||
<Gtk.Box class={"container"} hexpand>
|
||||
{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 vexpand={false}>
|
||||
<Gtk.Label class={"title"} xalign={0} tooltipText={props.title}
|
||||
ellipsize={Pango.EllipsizeMode.END} label={props.title}
|
||||
maxWidthChars={props.maxWidthChars ?? 28}
|
||||
/>
|
||||
<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"} visible={variableToBoolean(props.extraButtons)}>
|
||||
{props.extraButtons}
|
||||
</Gtk.Box>
|
||||
</Gtk.Box> as Gtk.Box;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Gtk } from "ags/gtk4";
|
||||
import { Windows } from "../../../windows";
|
||||
import { Wallpaper } from "../../../modules/wallpaper";
|
||||
import { execApp } from "../../../modules/apps";
|
||||
import { Accessor } from "ags";
|
||||
import { createPoll } from "ags/time";
|
||||
|
||||
import GLib from "gi://GLib?version=2.0";
|
||||
import Gio from "gi://Gio?version=2.0";
|
||||
|
||||
|
||||
const userFace: Gio.File = Gio.File.new_for_path(`${GLib.get_home_dir()}/.face`);
|
||||
const uptime: Accessor<string> = createPoll("Just turned on", 1000, "uptime -p");
|
||||
|
||||
function LockButton(): Gtk.Button {
|
||||
return <Gtk.Button iconName={"system-lock-screen-symbolic"}
|
||||
onClicked={() => {
|
||||
Windows.getDefault().close("control-center");
|
||||
execApp("hyprlock");
|
||||
}}
|
||||
/> as Gtk.Button;
|
||||
}
|
||||
|
||||
function ColorPickerButton(): Gtk.Button {
|
||||
return <Gtk.Button iconName={"color-select-symbolic"}
|
||||
onClicked={() => {
|
||||
Windows.getDefault().close("control-center");
|
||||
execApp("sh $HOME/.config/hypr/scripts/color-picker.sh");
|
||||
}}
|
||||
/> as Gtk.Button;
|
||||
}
|
||||
|
||||
function ScreenshotButton(): Gtk.Button {
|
||||
return <Gtk.Button iconName={"applets-screenshooter-symbolic"}
|
||||
onClicked={() => {
|
||||
Windows.getDefault().close("control-center");
|
||||
execApp(`sh ${GLib.get_user_config_dir()}/hypr/scripts/screenshot.sh`);
|
||||
}}
|
||||
/> as Gtk.Button;
|
||||
}
|
||||
|
||||
function SelectWallpaperButton(): Gtk.Button {
|
||||
return <Gtk.Button iconName={"preferences-desktop-wallpaper-symbolic"}
|
||||
onClicked={() => {
|
||||
Windows.getDefault().close("control-center");
|
||||
Wallpaper.getDefault().pickWallpaper();
|
||||
}}
|
||||
/> as Gtk.Button;
|
||||
}
|
||||
|
||||
function LogoutButton(): Gtk.Button {
|
||||
return <Gtk.Button iconName={"system-shutdown-symbolic"}
|
||||
onClicked={() => {
|
||||
Windows.getDefault().close("control-center");
|
||||
Windows.getDefault().open("logout-menu");
|
||||
}}
|
||||
/> as Gtk.Button;
|
||||
}
|
||||
|
||||
export const QuickActions = () =>
|
||||
<Gtk.Box class={"quickactions"}>
|
||||
<Gtk.Box halign={Gtk.Align.START} class={"left"} hexpand>
|
||||
{userFace.query_exists(null) &&
|
||||
<Gtk.Box class={"user-face"} css={
|
||||
`background-image: url("file://${userFace.get_path()!}");`}
|
||||
/>
|
||||
}
|
||||
<Gtk.Box orientation={Gtk.Orientation.VERTICAL}>
|
||||
<Gtk.Box class={"user-host"}>
|
||||
<Gtk.Label class={"user"} xalign={0}
|
||||
label={GLib.get_user_name()} />
|
||||
<Gtk.Label class={"host"} xalign={0} yalign={.8}
|
||||
label={`@${GLib.get_host_name()}`} />
|
||||
</Gtk.Box>
|
||||
|
||||
<Gtk.Box>
|
||||
<Gtk.Image iconName={"hourglass-symbolic"} />
|
||||
<Gtk.Label class={"uptime"} xalign={0} tooltipText={"Up time"}
|
||||
label={uptime.as(str => str.replace(/^up /, ""))} />
|
||||
</Gtk.Box>
|
||||
</Gtk.Box>
|
||||
</Gtk.Box>
|
||||
|
||||
<Gtk.Box class={"right button-row"} halign={Gtk.Align.END} hexpand>
|
||||
<LockButton />
|
||||
<ColorPickerButton />
|
||||
<ScreenshotButton />
|
||||
<SelectWallpaperButton />
|
||||
<LogoutButton />
|
||||
</Gtk.Box>
|
||||
</Gtk.Box> as Gtk.Box;
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Astal, Gtk } from "ags/gtk4";
|
||||
import { Wireplumber } from "../../../modules/volume";
|
||||
import { Pages } from "./pages";
|
||||
import { PageSound } from "./pages/Sound";
|
||||
import { PageMicrophone } from "./pages/Microphone";
|
||||
import { createBinding, With } from "ags";
|
||||
import { Backlights } from "../../../modules/backlight";
|
||||
import { PageBacklight } from "./pages/Backlight";
|
||||
|
||||
import AstalWp from "gi://AstalWp";
|
||||
|
||||
|
||||
export let slidersPages: Pages|undefined;
|
||||
|
||||
export function Sliders() {
|
||||
return <Gtk.Box class={"sliders"} orientation={Gtk.Orientation.VERTICAL}
|
||||
hexpand spacing={10} onUnmap={() => slidersPages = undefined}>
|
||||
|
||||
<With value={createBinding(Wireplumber.getWireplumber(), "defaultSpeaker")}>
|
||||
{(sink: AstalWp.Endpoint) => <Gtk.Box class={"sink speaker"} spacing={3}>
|
||||
<Gtk.Button onClicked={() => Wireplumber.getDefault().toggleMuteSink()}
|
||||
iconName={createBinding(sink, "volumeIcon").as((icon) =>
|
||||
(!Wireplumber.getDefault().isMutedSink() &&
|
||||
Wireplumber.getDefault().getSinkVolume() > 0
|
||||
) ? icon : "audio-volume-muted-symbolic"
|
||||
)} />
|
||||
|
||||
<Astal.Slider drawValue={false} hexpand value={createBinding(sink, "volume")}
|
||||
max={Wireplumber.getDefault().getMaxSinkVolume() / 100}
|
||||
onChangeValue={(_, __, value) => sink.set_volume(value)} />
|
||||
|
||||
<Gtk.Button class={"more"} iconName={"go-next-symbolic"} onClicked={() =>
|
||||
slidersPages?.toggle(PageSound)} />
|
||||
</Gtk.Box>}
|
||||
</With>
|
||||
<With value={createBinding(Wireplumber.getWireplumber(), "defaultMicrophone")}>
|
||||
{(source: AstalWp.Endpoint) => <Gtk.Box class={"source microphone"} spacing={3}>
|
||||
<Gtk.Button onClicked={() => Wireplumber.getDefault().toggleMuteSource()}
|
||||
iconName={createBinding(source, "volumeIcon").as((icon) =>
|
||||
(!Wireplumber.getDefault().isMutedSource() &&
|
||||
Wireplumber.getDefault().getSourceVolume() > 0
|
||||
) ? icon : "microphone-sensitivity-muted-symbolic"
|
||||
)} />
|
||||
|
||||
<Astal.Slider drawValue={false} hexpand value={createBinding(source, "volume")}
|
||||
max={Wireplumber.getDefault().getMaxSourceVolume() / 100}
|
||||
onChangeValue={(_, __, value) => source.set_volume(value)} />
|
||||
|
||||
<Gtk.Button class={"more"} iconName={"go-next-symbolic"} onClicked={() =>
|
||||
slidersPages?.toggle(PageMicrophone)} />
|
||||
</Gtk.Box>}
|
||||
</With>
|
||||
<Gtk.Box visible={createBinding(Backlights.getDefault(), "available")}>
|
||||
<With value={createBinding(Backlights.getDefault(), "default")}>
|
||||
{(bklight: Backlights.Backlight|null) => bklight &&
|
||||
<Gtk.Box class={"backlight"} spacing={3}>
|
||||
<Gtk.Button onClicked={() => {
|
||||
bklight.brightness = bklight.maxBrightness
|
||||
}} iconName={"display-brightness-symbolic"}
|
||||
/>
|
||||
|
||||
<Astal.Slider drawValue={false} hexpand value={createBinding(bklight, "brightness")}
|
||||
max={bklight.maxBrightness}
|
||||
onChangeValue={(_, __, value) => {
|
||||
bklight.brightness = value
|
||||
}}
|
||||
/>
|
||||
<Gtk.Button class={"more"} iconName={"go-next-symbolic"} onClicked={() =>
|
||||
slidersPages?.toggle(PageBacklight)} />
|
||||
</Gtk.Box>
|
||||
}
|
||||
</With>
|
||||
</Gtk.Box>
|
||||
<Pages $={(self) => slidersPages = self} />
|
||||
</Gtk.Box>
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Astal, Gtk } from "ags/gtk4";
|
||||
import { tr } from "../../../../i18n/intl";
|
||||
import { Backlights } from "../../../../modules/backlight";
|
||||
import { Page, PageButton } from "../Page";
|
||||
import { createBinding, For, With } from "ags";
|
||||
import { addSliderMarksFromMinMax } from "../../../../modules/utils";
|
||||
import { userData } from "../../../../app";
|
||||
|
||||
|
||||
export const PageBacklight = new Page({
|
||||
id: "backlight",
|
||||
title: tr("control_center.pages.backlight.title"),
|
||||
description: tr("control_center.pages.backlight.description"),
|
||||
$: () => {
|
||||
const dataDefaultBacklight = userData.getProperty("control_center.default_backlight", "any");
|
||||
if(typeof dataDefaultBacklight === "string" &&
|
||||
Backlights.getDefault().default?.name !== dataDefaultBacklight) {
|
||||
|
||||
const bk = Backlights.getDefault().backlights.filter(b => b.name === dataDefaultBacklight)[0];
|
||||
if(!bk) return;
|
||||
|
||||
Backlights.getDefault().setDefault(bk);
|
||||
}
|
||||
},
|
||||
content: () => (
|
||||
<With value={createBinding(Backlights.getDefault(), "backlights")}>
|
||||
{(bklights: Array<Backlights.Backlight>) => bklights.length > 0 &&
|
||||
<Gtk.Box orientation={Gtk.Orientation.VERTICAL} spacing={4}>
|
||||
<Gtk.Box class={"list"} visible={createBinding(Backlights.getDefault(), "backlights")
|
||||
.as((bklights) => bklights.length > 1)}>
|
||||
|
||||
<Gtk.Label label={"Default"} />
|
||||
<For each={createBinding(Backlights.getDefault(), "backlights")}>
|
||||
{(bk: Backlights.Backlight) =>
|
||||
<PageButton class={createBinding(bk, "isDefault").as(is => is ? "highlight" : "")}
|
||||
title={bk.name}
|
||||
icon={"video-display-symbolic"}
|
||||
actionClicked={() => {
|
||||
if(Backlights.getDefault().default?.path !== bk.path) {
|
||||
Backlights.getDefault().setDefault(bk);
|
||||
// save data
|
||||
userData.setProperty(
|
||||
"control_center.default_backlight",
|
||||
bk.name,
|
||||
true
|
||||
);
|
||||
}
|
||||
}}
|
||||
endWidget={
|
||||
<Gtk.Image iconName={"object-select-symbolic"}
|
||||
visible={createBinding(bk, "isDefault")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
</For>
|
||||
</Gtk.Box>
|
||||
<Gtk.Box class={"sliders"} orientation={Gtk.Orientation.VERTICAL} spacing={6}>
|
||||
{bklights.map((bklight, i) =>
|
||||
<Gtk.Box class={"bklight"} orientation={Gtk.Orientation.VERTICAL}
|
||||
spacing={4}>
|
||||
|
||||
<Gtk.Label class={"subheader"} label={`Backlight ${i+1} (${bklight.name})`}
|
||||
xalign={0} />
|
||||
<Astal.Slider $={(self) => addSliderMarksFromMinMax(self)}
|
||||
min={0} max={bklight.maxBrightness}
|
||||
value={createBinding(bklight, "brightness")}
|
||||
onChangeValue={(_, __, value) => {
|
||||
bklight.brightness = value
|
||||
}}
|
||||
/>
|
||||
</Gtk.Box>
|
||||
)}
|
||||
</Gtk.Box>
|
||||
</Gtk.Box>
|
||||
}
|
||||
</With>
|
||||
),
|
||||
headerButtons: [{
|
||||
icon: "arrow-circular-top-right",
|
||||
tooltipText: tr("control_center.pages.backlight.refresh"),
|
||||
actionClicked: () => Backlights.getDefault().scan()
|
||||
}]
|
||||
});
|
||||
@@ -0,0 +1,229 @@
|
||||
import { Gtk } from "ags/gtk4";
|
||||
import { Page, PageButton } from "../Page";
|
||||
import { tr } from "../../../../i18n/intl";
|
||||
import { Windows } from "../../../../windows";
|
||||
import { Notifications } from "../../../../modules/notifications";
|
||||
import { execApp } from "../../../../modules/apps";
|
||||
import { execAsync } from "ags/process";
|
||||
import { createBinding, createComputed, For, With } from "ags";
|
||||
import { Bluetooth } from "../../../../modules/bluetooth";
|
||||
|
||||
import AstalNotifd from "gi://AstalNotifd";
|
||||
import AstalBluetooth from "gi://AstalBluetooth";
|
||||
import Adw from "gi://Adw?version=1";
|
||||
import Gio from "gi://Gio?version=2.0";
|
||||
|
||||
|
||||
export const BluetoothPage = new Page({
|
||||
id: "bluetooth",
|
||||
title: tr("control_center.pages.bluetooth.title"),
|
||||
spacing: 6,
|
||||
description: tr("control_center.pages.bluetooth.description"),
|
||||
headerButtons: createBinding(Bluetooth.getDefault(), "adapter").as(adapter => adapter ? [{
|
||||
icon: createBinding(adapter, "discovering")
|
||||
.as(discovering => !discovering ?
|
||||
"arrow-circular-top-right-symbolic"
|
||||
: "media-playback-stop-symbolic"
|
||||
),
|
||||
tooltipText: createBinding(adapter, "discovering")
|
||||
.as((discovering) => !discovering ?
|
||||
tr("control_center.pages.bluetooth.start_discovering")
|
||||
: tr("control_center.pages.bluetooth.stop_discovering")),
|
||||
actionClicked: () => {
|
||||
if(adapter.discovering) {
|
||||
adapter.stop_discovery();
|
||||
return;
|
||||
}
|
||||
|
||||
adapter.start_discovery();
|
||||
}
|
||||
}]: []),
|
||||
actionClosed: () => Bluetooth.getDefault().adapter?.discovering &&
|
||||
Bluetooth.getDefault().adapter?.stop_discovery(),
|
||||
bottomButtons: [{
|
||||
title: tr("control_center.pages.more_settings"),
|
||||
actionClicked: () => {
|
||||
Windows.getDefault().close("control-center");
|
||||
execApp("overskride", "[float; animation slide right]");
|
||||
}
|
||||
}],
|
||||
content: () => {
|
||||
const adapter = createBinding(Bluetooth.getDefault(), "adapter");
|
||||
const adapters = createBinding(AstalBluetooth.get_default(), "adapters");
|
||||
const devices = createBinding(AstalBluetooth.get_default(), "devices");
|
||||
|
||||
return [
|
||||
<Gtk.Box class={"adapters"} visible={adapters.as(adptrs => adptrs.length > 1)
|
||||
} spacing={2} orientation={Gtk.Orientation.VERTICAL}>
|
||||
|
||||
<Gtk.Label class={"sub-header"} label={tr("control_center.pages.bluetooth.adapters")}
|
||||
xalign={0} />
|
||||
<With value={adapters.as(adpts => adpts.length > 1)}>
|
||||
{(hasMoreAdapters: boolean) => hasMoreAdapters &&
|
||||
<Gtk.Box orientation={Gtk.Orientation.VERTICAL} spacing={2}>
|
||||
<For each={adapters}>
|
||||
{(adapter: AstalBluetooth.Adapter) => {
|
||||
const isSelected = createBinding(Bluetooth.getDefault(), "adapter").as(a =>
|
||||
adapter.address === a?.address);
|
||||
|
||||
return <PageButton class={isSelected.as(is => is ? "selected" : "")}
|
||||
title={adapter.alias ?? "Adapter"} icon={"bluetooth-active-symbolic"}
|
||||
description={createBinding(adapter, "address")}
|
||||
actionClicked={() =>
|
||||
adapter.address !== Bluetooth.getDefault().adapter?.address &&
|
||||
selectAdapter(adapter)
|
||||
}
|
||||
endWidget={
|
||||
<Gtk.Image iconName={"object-select-symbolic"} visible={isSelected} />
|
||||
}
|
||||
/>;
|
||||
}}
|
||||
</For>
|
||||
</Gtk.Box>
|
||||
}
|
||||
</With>
|
||||
</Gtk.Box>,
|
||||
<Gtk.Box class={"connections"} orientation={Gtk.Orientation.VERTICAL} hexpand
|
||||
spacing={2}>
|
||||
|
||||
<Gtk.Box class={"paired"} orientation={Gtk.Orientation.VERTICAL} spacing={4}
|
||||
visible={devices.as(devs => devs.filter(dev =>
|
||||
(dev.adapter as AstalBluetooth.Adapter).address === adapter.get()?.address &&
|
||||
dev.paired || dev.connected || dev.trusted).length > 0)
|
||||
}>
|
||||
|
||||
<Gtk.Label class={"sub-header"} label={tr("devices")} xalign={0} />
|
||||
<For each={devices.as(devs => devs.filter(dev =>
|
||||
(dev.adapter as AstalBluetooth.Adapter).address === adapter.get()?.address &&
|
||||
dev.paired || dev.connected || dev.trusted))
|
||||
}>
|
||||
|
||||
{(dev: AstalBluetooth.Device) => <DeviceWidget device={dev} />}
|
||||
</For>
|
||||
</Gtk.Box>
|
||||
<Gtk.Box class={"discovered"} orientation={Gtk.Orientation.VERTICAL} spacing={4}
|
||||
visible={devices.as(devs => devs.filter(dev =>
|
||||
(dev.adapter as AstalBluetooth.Adapter).address === adapter.get()?.address &&
|
||||
!dev.connected && !dev.paired && !dev.trusted).length > 0)
|
||||
}>
|
||||
|
||||
<Gtk.Label class={"sub-header"} label={tr("control_center.pages.bluetooth.new_devices")}
|
||||
xalign={0} />
|
||||
<For each={devices.as(devs => devs.filter(dev =>
|
||||
(dev.adapter as AstalBluetooth.Adapter).address === adapter.get()?.address &&
|
||||
!dev.connected && !dev.paired && !dev.trusted))
|
||||
}>
|
||||
|
||||
{(dev: AstalBluetooth.Device) => <DeviceWidget device={dev} />}
|
||||
</For>
|
||||
</Gtk.Box>
|
||||
</Gtk.Box>
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
function DeviceWidget({ device }: { device: AstalBluetooth.Device }): Gtk.Widget {
|
||||
const pair = async () => {
|
||||
if(device.paired) return;
|
||||
|
||||
device.pair();
|
||||
device.set_trusted(true);
|
||||
};
|
||||
|
||||
return <PageButton class={createBinding(device, "connected").as(conn =>
|
||||
conn ? "selected" : "")} title={
|
||||
createBinding(device, "alias").as(alias => alias ?? "Unknown Device")}
|
||||
icon={createBinding(device, "icon").as(ico => ico ?? "bluetooth-active-symbolic")}
|
||||
tooltipText={
|
||||
createBinding(device, "connected").as(connected =>
|
||||
!connected ? tr("connect") : "")
|
||||
} actionClicked={() => {
|
||||
if(device.connected) return;
|
||||
|
||||
pair().then(() => {
|
||||
device.connect_device((_, res) => {
|
||||
|
||||
// get error
|
||||
try { device.connect_device_finish(res); }
|
||||
catch(e: any) {
|
||||
Notifications.getDefault().sendNotification({
|
||||
appName: "bluetooth",
|
||||
summary: "Connection Error",
|
||||
body: `An error occurred while attempting to connect to ${
|
||||
device.alias ?? device.name}: ${(e as Gio.IOErrorEnum).message}`
|
||||
});
|
||||
}
|
||||
});
|
||||
}).catch((err: Gio.IOErrorEnum) =>
|
||||
Notifications.getDefault().sendNotification({
|
||||
appName: "bluetooth",
|
||||
summary: "Pairing Error",
|
||||
body: `Couldn't pair with ${device.alias ?? device.name}: ${err.message}`,
|
||||
urgency: AstalNotifd.Urgency.NORMAL
|
||||
})
|
||||
);
|
||||
}}
|
||||
endWidget={<Gtk.Box spacing={6}>
|
||||
<Adw.Spinner visible={createBinding(device, "connecting")} />
|
||||
<Gtk.Box visible={createComputed([
|
||||
createBinding(device, "batteryPercentage"),
|
||||
createBinding(device, "connected")
|
||||
]).as(([batt, connected]) => connected && (batt > -1))
|
||||
} spacing={4}>
|
||||
<Gtk.Label halign={Gtk.Align.END} label={
|
||||
createBinding(device, "batteryPercentage").as(batt =>
|
||||
`${Math.floor(batt * 100)}%`)
|
||||
} visible={createBinding(device, "connected")}
|
||||
/>
|
||||
|
||||
<Gtk.Image iconName={
|
||||
createBinding(device, "batteryPercentage").as(batt =>
|
||||
`battery-level-${Math.floor(batt * 100)}-symbolic`)
|
||||
} css={"font-size: 16px; margin-left: 6px;"} />
|
||||
</Gtk.Box>
|
||||
</Gtk.Box>} extraButtons={<With value={createComputed([
|
||||
createBinding(device, "connected"),
|
||||
createBinding(device, "trusted")
|
||||
])}>
|
||||
{([connected, trusted]: [boolean, boolean]) =>
|
||||
<Gtk.Box visible={connected || trusted}>
|
||||
{<Gtk.Button iconName={connected ?
|
||||
"list-remove-symbolic"
|
||||
: "user-trash-symbolic"} tooltipText={tr(connected ?
|
||||
"disconnect"
|
||||
: "control_center.pages.bluetooth.unpair_device"
|
||||
)} onClicked={() => {
|
||||
if(!connected) {
|
||||
Bluetooth.getDefault().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 Gtk.Widget;
|
||||
}
|
||||
|
||||
function selectAdapter(adapter: AstalBluetooth.Adapter): void {
|
||||
AstalBluetooth.get_default().adapters.filter(ad => {
|
||||
if(ad.alias !== adapter.alias)
|
||||
return true;
|
||||
|
||||
ad.set_powered(true);
|
||||
return false;
|
||||
}).forEach(ad => ad.set_powered(false));
|
||||
|
||||
execAsync(`bluetoothctl select ${adapter.address}`).catch(e =>
|
||||
console.error(`Bluetooth: Couldn't select adapter. Stderr: ${e}`));
|
||||
|
||||
Bluetooth.getDefault().adapter = adapter;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Page, PageButton } from "../Page";
|
||||
import { Wireplumber } from "../../../../modules/volume";
|
||||
import { Gtk } from "ags/gtk4";
|
||||
import { tr } from "../../../../i18n/intl";
|
||||
import { createBinding, For } from "ags";
|
||||
import { lookupIcon } from "../../../../modules/apps";
|
||||
|
||||
import AstalWp from "gi://AstalWp?version=0.1";
|
||||
|
||||
|
||||
export const PageMicrophone = new Page({
|
||||
id: "microphone",
|
||||
title: tr("control_center.pages.microphone.title"),
|
||||
description: tr("control_center.pages.microphone.description"),
|
||||
content: () => [
|
||||
<Gtk.Label class={"sub-header"} label={tr("devices")} xalign={0} />,
|
||||
<Gtk.Box orientation={Gtk.Orientation.VERTICAL} spacing={4}>
|
||||
<For each={createBinding(Wireplumber.getWireplumber().get_audio()!, "microphones")}>
|
||||
{(source: AstalWp.Endpoint) => <PageButton class={
|
||||
createBinding(source, "isDefault").as(isDefault => isDefault ? "selected" : "")
|
||||
} icon={createBinding(source, "icon").as(ico => lookupIcon(ico) ?
|
||||
ico : "audio-input-microphone-symbolic")} title={
|
||||
createBinding(source, "description").as(desc => desc ?? "Microphone")
|
||||
} actionClicked={() => !source.isDefault && source.set_is_default(true)}
|
||||
endWidget={
|
||||
<Gtk.Image iconName={"object-select-symbolic"} visible={
|
||||
createBinding(source, "isDefault")} css={"font-size: 18px;"}
|
||||
/>
|
||||
}
|
||||
/>}
|
||||
</For>
|
||||
</Gtk.Box>
|
||||
]
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
import { Gtk } from "ags/gtk4";
|
||||
import { Page, PageButton } from "../Page";
|
||||
import { Windows } from "../../../../windows";
|
||||
import { tr } from "../../../../i18n/intl";
|
||||
import { execApp } from "../../../../modules/apps";
|
||||
import { Notifications } from "../../../../modules/notifications";
|
||||
import { AskPopup, AskPopupProps } from "../../../../widget/AskPopup";
|
||||
import { encoder, variableToBoolean } from "../../../../modules/utils";
|
||||
import { createBinding, For, With } from "ags";
|
||||
|
||||
import GLib from "gi://GLib?version=2.0";
|
||||
import NM from "gi://NM";
|
||||
import AstalNetwork from "gi://AstalNetwork";
|
||||
|
||||
|
||||
export const PageNetwork = new Page({
|
||||
id: "network",
|
||||
title: tr("control_center.pages.network.title"),
|
||||
headerButtons: createBinding(AstalNetwork.get_default(), "primary").as(primary =>
|
||||
primary === AstalNetwork.Primary.WIFI ? [{
|
||||
icon: "arrow-circular-top-right-symbolic",
|
||||
tooltipText: "Re-scan networks",
|
||||
actionClicked: () => AstalNetwork.get_default().wifi.scan()
|
||||
}] : []
|
||||
),
|
||||
bottomButtons: [{
|
||||
title: tr("control_center.pages.more_settings"),
|
||||
actionClicked: () => {
|
||||
Windows.getDefault().close("control-center");
|
||||
execApp("nm-connection-editor", "[animationstyle gnomed]");
|
||||
}
|
||||
}],
|
||||
content: () => [
|
||||
<Gtk.Box class={"devices"} hexpand orientation={Gtk.Orientation.VERTICAL}
|
||||
visible={variableToBoolean(createBinding(AstalNetwork.get_default().client, "devices"))}
|
||||
spacing={4}>
|
||||
|
||||
<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}`
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
}}/>
|
||||
]} actionClicked={() => {
|
||||
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>
|
||||
]
|
||||
});
|
||||
|
||||
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,43 @@
|
||||
import { Page } from "../Page";
|
||||
import { NightLight } from "../../../../modules/nightlight";
|
||||
import { tr } from "../../../../i18n/intl";
|
||||
import { Astal, Gtk } from "ags/gtk4";
|
||||
import { addSliderMarksFromMinMax } from "../../../../modules/utils";
|
||||
import { createBinding } from "ags";
|
||||
|
||||
export const PageNightLight = new Page({
|
||||
id: "night-light",
|
||||
title: tr("control_center.pages.night_light.title"),
|
||||
description: tr("control_center.pages.night_light.description"),
|
||||
content: () => [
|
||||
<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)
|
||||
}}
|
||||
/>
|
||||
]
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Page, PageButton } from "../Page";
|
||||
import { Astal, Gtk } from "ags/gtk4";
|
||||
import { getAppIcon, lookupIcon } from "../../../../modules/apps";
|
||||
import { Wireplumber } from "../../../../modules/volume";
|
||||
import { tr } from "../../../../i18n/intl";
|
||||
import { createBinding, For } from "ags";
|
||||
import { variableToBoolean } from "../../../../modules/utils";
|
||||
|
||||
import AstalWp from "gi://AstalWp";
|
||||
import GObject from "gi://GObject?version=2.0";
|
||||
import Pango from "gi://Pango?version=1.0";
|
||||
|
||||
|
||||
export const PageSound = new Page({
|
||||
id: "sound",
|
||||
title: tr("control_center.pages.sound.title"),
|
||||
description: tr("control_center.pages.sound.description"),
|
||||
content: () => [
|
||||
<Gtk.Label class={"sub-header"} label={tr("devices")} xalign={0} />,
|
||||
<Gtk.Box orientation={Gtk.Orientation.VERTICAL} spacing={4}>
|
||||
<For each={createBinding(Wireplumber.getWireplumber().audio!, "speakers")}>
|
||||
{(sink: AstalWp.Endpoint) =>
|
||||
<PageButton class={createBinding(sink, "isDefault").as(isDefault =>
|
||||
isDefault ? "selected" : "")}
|
||||
icon={createBinding(sink, "icon").as(ico =>
|
||||
lookupIcon(ico) ? ico : "audio-card-symbolic")}
|
||||
title={createBinding(sink, "description").as(desc =>
|
||||
desc ?? "Speaker")}
|
||||
actionClicked={() => !sink.isDefault && sink.set_is_default(true)}
|
||||
endWidget={
|
||||
<Gtk.Image iconName={"object-select-symbolic"}
|
||||
visible={createBinding(sink, "isDefault")}
|
||||
css={"font-size: 18px;"}
|
||||
/>
|
||||
}
|
||||
/>}
|
||||
</For>
|
||||
</Gtk.Box>,
|
||||
<Gtk.Box orientation={Gtk.Orientation.VERTICAL} spacing={8}>
|
||||
<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 $={(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_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 =>
|
||||
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, "description").as(desc =>
|
||||
desc ?? "Unnamed audio stream")}
|
||||
ellipsize={Pango.EllipsizeMode.END}
|
||||
tooltipText={createBinding(stream, "name")}
|
||||
class={"name"} xalign={0}
|
||||
/>
|
||||
</Gtk.Revealer>
|
||||
|
||||
<Astal.Slider drawValue={false} value={createBinding(stream, "volume")}
|
||||
onChangeValue={(_, __, value) => stream.set_volume(value)}
|
||||
hexpand min={0} max={1.5}
|
||||
/>
|
||||
</Gtk.Box>
|
||||
</Gtk.Box>
|
||||
}
|
||||
</For>
|
||||
</Gtk.Box>
|
||||
]
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import { register } from "ags/gobject";
|
||||
import { Gtk } from "ags/gtk4";
|
||||
import { Page } from "../Page";
|
||||
import { timeout } from "ags/time";
|
||||
|
||||
import AstalIO from "gi://AstalIO";
|
||||
|
||||
|
||||
export { Pages };
|
||||
export type PagesProps = {
|
||||
initialPage?: Page;
|
||||
transitionDuration?: number;
|
||||
};
|
||||
|
||||
@register({ GTypeName: "Pages" })
|
||||
class Pages extends Gtk.Box {
|
||||
#timeouts: Array<[AstalIO.Time, (() => void)|undefined]> = [];
|
||||
#page: (Page|undefined);
|
||||
#transDuration: number;
|
||||
#transType: Gtk.RevealerTransitionType = Gtk.RevealerTransitionType.SLIDE_DOWN;
|
||||
|
||||
get isOpen() { return Boolean(this.#page); }
|
||||
get page() { return this.#page; }
|
||||
|
||||
constructor(props?: PagesProps) {
|
||||
super({
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
cssName: "pages",
|
||||
name: "pages"
|
||||
});
|
||||
|
||||
this.add_css_class("pages");
|
||||
|
||||
this.#transDuration = props?.transitionDuration ?? 280;
|
||||
|
||||
if(props?.initialPage)
|
||||
this.open(props.initialPage);
|
||||
|
||||
|
||||
const destroyId = this.connect("destroy", () => {
|
||||
this.disconnect(destroyId);
|
||||
this.#timeouts.forEach((tmout) => {
|
||||
tmout[0].cancel();
|
||||
(async () => tmout[1]?.())().catch((err: Error) => {
|
||||
console.error(`${err.message}\n${err.stack}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
toggle(newPage?: Page, onToggled?: () => void): void {
|
||||
if(!newPage || (this.#page?.id === newPage.id)) {
|
||||
this.close(onToggled);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!this.isOpen) {
|
||||
newPage && this.open(newPage, onToggled);
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.#page?.id !== newPage.id) {
|
||||
this.close();
|
||||
this.open(newPage, onToggled);
|
||||
}
|
||||
}
|
||||
|
||||
open(newPage: Page, onOpen?: () => void) {
|
||||
this.#page = newPage;
|
||||
|
||||
this.prepend(
|
||||
<Gtk.Revealer revealChild={false} transitionType={this.#transType}
|
||||
transitionDuration={this.#transDuration}>
|
||||
|
||||
{newPage.create()}
|
||||
</Gtk.Revealer> as Gtk.Revealer
|
||||
);
|
||||
|
||||
(this.get_first_child() as Gtk.Revealer)?.set_reveal_child(true);
|
||||
onOpen?.();
|
||||
}
|
||||
|
||||
close(onClosed?: () => void): void {
|
||||
const page = this.get_first_child() as Gtk.Revealer|null;
|
||||
if(!page) return;
|
||||
|
||||
this.#page?.actionClosed?.();
|
||||
this.#page = undefined;
|
||||
|
||||
page.set_reveal_child(false);
|
||||
this.#timeouts.push([
|
||||
timeout(page.transitionDuration, () => {
|
||||
this.remove(page);
|
||||
onClosed?.();
|
||||
}),
|
||||
onClosed
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Tile } from "./Tile";
|
||||
import { BluetoothPage } from "../pages/Bluetooth";
|
||||
import { TilesPages } from "../tiles";
|
||||
import { createBinding, createComputed } from "ags";
|
||||
import { Bluetooth } from "../../../../modules/bluetooth";
|
||||
|
||||
import AstalBluetooth from "gi://AstalBluetooth";
|
||||
|
||||
|
||||
export const TileBluetooth = () =>
|
||||
<Tile title={"Bluetooth"} visible={createBinding(Bluetooth.getDefault(), "isAvailable")}
|
||||
description={createBinding(AstalBluetooth.get_default(), "adapters").as((connected) => {
|
||||
if(!connected) return "";
|
||||
|
||||
const connectedDevs = AstalBluetooth.get_default().devices.filter(dev => dev.connected);
|
||||
const connectedDev = connectedDevs[connectedDevs.length - 1]; // last connected device is on display
|
||||
return connectedDev ? connectedDev.get_alias() : ""
|
||||
})}
|
||||
onEnabled={() => Bluetooth.getDefault().adapter?.set_powered(true)}
|
||||
onDisabled={() => Bluetooth.getDefault().adapter?.set_powered(false)}
|
||||
onClicked={() => TilesPages?.toggle(BluetoothPage)}
|
||||
enableOnClicked hasArrow
|
||||
state={createBinding(AstalBluetooth.get_default(), "isPowered")}
|
||||
icon={createComputed([
|
||||
createBinding(AstalBluetooth.get_default(), "isPowered"),
|
||||
createBinding(AstalBluetooth.get_default(), "isConnected")
|
||||
],
|
||||
(powered: boolean, isConnected: boolean) =>
|
||||
powered ? ( isConnected ?
|
||||
"bluetooth-active-symbolic"
|
||||
: "bluetooth-symbolic"
|
||||
) : "bluetooth-disabled-symbolic")
|
||||
}
|
||||
/>;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Notifications } from "../../../../modules/notifications";
|
||||
import { Tile } from "./Tile";
|
||||
import { tr } from "../../../../i18n/intl";
|
||||
import { createBinding } from "ags";
|
||||
|
||||
export const TileDND = () =>
|
||||
<Tile title={tr("control_center.tiles.dnd.title")}
|
||||
description={createBinding(Notifications.getDefault().getNotifd(), "dontDisturb").as(
|
||||
(dnd: boolean) => dnd ? tr("control_center.tiles.enabled") : tr("control_center.tiles.disabled"))}
|
||||
onDisabled={() => Notifications.getDefault().getNotifd().dontDisturb = false}
|
||||
onEnabled={() => Notifications.getDefault().getNotifd().dontDisturb = true}
|
||||
icon={"minus-circle-filled-symbolic"}
|
||||
state={Notifications.getDefault().getNotifd().dontDisturb}
|
||||
/>;
|
||||
@@ -0,0 +1,85 @@
|
||||
import { execAsync } from "ags/process";
|
||||
import { Tile } from "./Tile";
|
||||
import { PageNetwork } from "../pages/Network";
|
||||
import { tr } from "../../../../i18n/intl";
|
||||
import { TilesPages } from "../tiles";
|
||||
import { Gtk } from "ags/gtk4";
|
||||
import { createBinding, createComputed, With } from "ags";
|
||||
|
||||
import AstalNetwork from "gi://AstalNetwork";
|
||||
|
||||
|
||||
export const TileNetwork = () => <Gtk.Box>
|
||||
<With value={createComputed([
|
||||
createBinding(AstalNetwork.get_default(), "primary"),
|
||||
createBinding(AstalNetwork.get_default(), "wired"),
|
||||
createBinding(AstalNetwork.get_default(), "wifi")
|
||||
])}>
|
||||
|
||||
{([primary, wired, wifi]: [AstalNetwork.Primary, AstalNetwork.Wired, AstalNetwork.Wifi]) => {
|
||||
if(primary === AstalNetwork.Primary.WIFI) {
|
||||
return <Tile title={tr("control_center.tiles.network.wireless")}
|
||||
description={createComputed([
|
||||
createBinding(wifi, "ssid"), createBinding(wifi, "internet")
|
||||
], (ssid, internet) => ssid ? ssid : (() => {
|
||||
switch(internet) {
|
||||
case AstalNetwork.Internet.CONNECTED:
|
||||
return tr("connected");
|
||||
case AstalNetwork.Internet.DISCONNECTED:
|
||||
return tr("disconnected");
|
||||
case AstalNetwork.Internet.CONNECTING:
|
||||
return tr("connecting") + "...";
|
||||
}
|
||||
})()
|
||||
)} onEnabled={() => wifi.set_enabled(true)}
|
||||
onDisabled={() => wifi.set_enabled(false)}
|
||||
hasArrow onClicked={() => TilesPages?.toggle(PageNetwork)}
|
||||
icon={"network-wireless-signal-excellent-symbolic"}
|
||||
state={createBinding(wifi, "enabled")}
|
||||
/>
|
||||
|
||||
} else if(primary === AstalNetwork.Primary.WIRED) {
|
||||
return <Tile title={tr("control_center.tiles.network.wired")}
|
||||
description={createBinding(wired, "internet").as((internet: AstalNetwork.Internet) => {
|
||||
switch(internet) {
|
||||
case AstalNetwork.Internet.CONNECTED:
|
||||
return tr("connected");
|
||||
case AstalNetwork.Internet.DISCONNECTED:
|
||||
return tr("disconnected");
|
||||
case AstalNetwork.Internet.CONNECTING:
|
||||
return tr("connecting") + "...";
|
||||
}
|
||||
})}
|
||||
hasArrow onEnabled={() => execAsync("nmcli n on")}
|
||||
onDisabled={() => execAsync("nmcli n off")}
|
||||
onClicked={() => TilesPages?.toggle(PageNetwork)}
|
||||
icon={createBinding(wired, "internet").as((internet: AstalNetwork.Internet) => {
|
||||
switch(internet) {
|
||||
case AstalNetwork.Internet.CONNECTED:
|
||||
return "network-wired-symbolic";
|
||||
case AstalNetwork.Internet.DISCONNECTED:
|
||||
return "network-wired-disconnected-symbolic";
|
||||
}
|
||||
|
||||
return "network-wired-no-route-symbolic";
|
||||
})}
|
||||
state={createBinding(wired, "internet").as((internet: AstalNetwork.Internet) =>
|
||||
internet === AstalNetwork.Internet.CONNECTING
|
||||
|| internet === AstalNetwork.Internet.CONNECTED
|
||||
)}
|
||||
/>
|
||||
}
|
||||
|
||||
return <Tile
|
||||
title={tr("control_center.tiles.network.network")}
|
||||
description={tr("disconnected")}
|
||||
onEnabled={() => execAsync("nmcli n on")}
|
||||
onDisabled={() => execAsync("nmcli n off")}
|
||||
hasArrow onClicked={() => TilesPages?.toggle(PageNetwork)}
|
||||
icon={"network-wired-disconnected-symbolic"}
|
||||
state={createBinding(wired, "internet").as((internet: AstalNetwork.Internet) =>
|
||||
internet === AstalNetwork.Internet.CONNECTING || internet === AstalNetwork.Internet.CONNECTED)}
|
||||
/>
|
||||
}}
|
||||
</With>
|
||||
</Gtk.Box>;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Tile } from "./Tile";
|
||||
import { NightLight } from "../../../../modules/nightlight";
|
||||
import { PageNightLight } from "../pages/NightLight";
|
||||
import { tr } from "../../../../i18n/intl";
|
||||
import { TilesPages } from "../tiles";
|
||||
import { isInstalled } from "../../../../modules/utils";
|
||||
import { createBinding, createComputed } from "ags";
|
||||
|
||||
|
||||
export const TileNightLight = () =>
|
||||
<Tile title={tr("control_center.tiles.night_light.title")}
|
||||
icon={"weather-clear-night-symbolic"}
|
||||
description={createComputed([
|
||||
createBinding(NightLight.getDefault(), "identity"),
|
||||
createBinding(NightLight.getDefault(), "temperature"),
|
||||
createBinding(NightLight.getDefault(), "gamma")
|
||||
], (identity, temp, gamma) => !identity ?
|
||||
`${temp === NightLight.getDefault().identityTemperature ?
|
||||
tr("control_center.tiles.night_light.default_desc") : `${temp}K`
|
||||
} ${gamma < NightLight.getDefault().maxGamma ? `(${gamma}%)` : ""}`
|
||||
: tr("control_center.tiles.disabled")
|
||||
)}
|
||||
hasArrow visible={isInstalled("hyprsunset")}
|
||||
onDisabled={() => NightLight.getDefault().identity = true}
|
||||
onEnabled={() => NightLight.getDefault().identity = false}
|
||||
enableOnClicked
|
||||
onClicked={() => TilesPages?.toggle(PageNightLight)}
|
||||
state={createBinding(NightLight.getDefault(), "identity").as(identity => !identity)}
|
||||
/>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Tile } from "./Tile";
|
||||
import { Recording } from "../../../../modules/recording";
|
||||
import { tr } from "../../../../i18n/intl";
|
||||
import { isInstalled } from "../../../../modules/utils";
|
||||
import { createBinding, createComputed } from "ags";
|
||||
|
||||
|
||||
export const TileRecording = () =>
|
||||
<Tile title={tr("control_center.tiles.recording.title")}
|
||||
description={createComputed([
|
||||
createBinding(Recording.getDefault(), "recording"),
|
||||
createBinding(Recording.getDefault(), "recordingTime")
|
||||
], (recording, time) => {
|
||||
if(!recording || !Recording.getDefault().startedAt)
|
||||
return tr("control_center.tiles.recording.disabled_desc") || "Start recording";
|
||||
|
||||
return time;
|
||||
})}
|
||||
icon={"media-record-symbolic"}
|
||||
visible={isInstalled("wf-recorder")}
|
||||
onDisabled={() => Recording.getDefault().stopRecording()}
|
||||
onEnabled={() => Recording.getDefault().startRecording()}
|
||||
state={createBinding(Recording.getDefault(), "recording")}
|
||||
/>;
|
||||
@@ -0,0 +1,151 @@
|
||||
import { Gtk } from "ags/gtk4";
|
||||
import { createBinding } from "ags";
|
||||
import { omitObjectKeys, variableToBoolean } from "../../../../modules/utils";
|
||||
import { property, register, signal } from "ags/gobject";
|
||||
|
||||
import Pango from "gi://Pango?version=1.0";
|
||||
|
||||
|
||||
@register({ GTypeName: "Tile" })
|
||||
export class Tile extends Gtk.Box {
|
||||
@signal(Boolean) toggled(_state: boolean) {}
|
||||
@signal() enabled() {}
|
||||
@signal() disabled() {}
|
||||
@signal() clicked() {}
|
||||
|
||||
@property(String)
|
||||
public icon: string;
|
||||
@property(String)
|
||||
public title: string;
|
||||
@property(String)
|
||||
public description: string = "";
|
||||
@property(Boolean)
|
||||
public enableOnClicked: boolean = true;
|
||||
@property(Boolean)
|
||||
public state: boolean = false;
|
||||
@property(Boolean)
|
||||
public hasArrow: boolean = false;
|
||||
|
||||
declare $signals: Gtk.Box.SignalSignatures & {
|
||||
"toggled": (_state: boolean) => void;
|
||||
"enabled": () => void;
|
||||
"disabled": () => void;
|
||||
"clicked": () => void;
|
||||
};
|
||||
|
||||
public enable(): void {
|
||||
if(this.state) return;
|
||||
|
||||
this.state = true;
|
||||
!this.has_css_class("enabled") &&
|
||||
this.add_css_class("enabled");
|
||||
this.emit("toggled", true);
|
||||
this.emit("enabled");
|
||||
}
|
||||
|
||||
public disable(): void {
|
||||
if(!this.state) return;
|
||||
|
||||
this.state = false;
|
||||
this.remove_css_class("enabled");
|
||||
this.emit("toggled", false);
|
||||
this.emit("disabled");
|
||||
}
|
||||
|
||||
constructor(props: Partial<Omit<Gtk.Box.ConstructorProps, "orientation">> & {
|
||||
icon: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
state?: boolean;
|
||||
enableOnClicked?: boolean;
|
||||
hasArrow?: boolean;
|
||||
}) {
|
||||
super(omitObjectKeys(props, [
|
||||
"icon",
|
||||
"title",
|
||||
"description",
|
||||
"state",
|
||||
"enableOnClicked"
|
||||
]));
|
||||
|
||||
this.add_css_class("tile");
|
||||
|
||||
this.icon = props.icon;
|
||||
this.title = props.title;
|
||||
this.hexpand = true;
|
||||
|
||||
if(props.hasArrow != null)
|
||||
this.hasArrow = props.hasArrow;
|
||||
|
||||
if(props.description != null)
|
||||
this.description = props.description;
|
||||
|
||||
if(props.state != null)
|
||||
this.state = props.state;
|
||||
|
||||
if(props.enableOnClicked != null)
|
||||
this.enableOnClicked = props.enableOnClicked;
|
||||
|
||||
if(this.state)
|
||||
this.add_css_class("enabled"); // fix no highlight with state = true on construct
|
||||
|
||||
this.prepend(
|
||||
<Gtk.Box hexpand={false} vexpand class={"icon"}>
|
||||
<Gtk.Image iconName={createBinding(this, "icon")} halign={Gtk.Align.CENTER} />
|
||||
<Gtk.GestureClick onReleased={() => {
|
||||
this.state ? this.disable() : this.enable();
|
||||
}} />
|
||||
</Gtk.Box> as Gtk.Box
|
||||
);
|
||||
|
||||
this.append(
|
||||
<Gtk.Box class={"content"} orientation={Gtk.Orientation.VERTICAL} vexpand
|
||||
valign={Gtk.Align.CENTER} hexpand>
|
||||
|
||||
<Gtk.Label class={"title"} label={createBinding(this, "title")}
|
||||
xalign={0} ellipsize={Pango.EllipsizeMode.END} hexpand={false}
|
||||
maxWidthChars={10} />
|
||||
<Gtk.Label class={"description"} label={createBinding(this, "description")}
|
||||
xalign={0} ellipsize={Pango.EllipsizeMode.END} visible={
|
||||
variableToBoolean(createBinding(this, "description"))
|
||||
} maxWidthChars={12} hexpand={false}
|
||||
/>
|
||||
|
||||
<Gtk.GestureClick onReleased={() => {
|
||||
this.emit("clicked");
|
||||
if(this.enableOnClicked && !this.state)
|
||||
this.enable();
|
||||
|
||||
return true;
|
||||
}} />
|
||||
</Gtk.Box> as Gtk.Box
|
||||
);
|
||||
|
||||
if(this.hasArrow)
|
||||
this.append(
|
||||
<Gtk.Image class={"arrow"} iconName={"go-next-symbolic"} halign={Gtk.Align.END}>
|
||||
<Gtk.GestureClick onReleased={() => {
|
||||
this.emit("clicked");
|
||||
if(this.enableOnClicked && !this.state)
|
||||
this.enable();
|
||||
|
||||
return true;
|
||||
}} />
|
||||
</Gtk.Image> as Gtk.Image
|
||||
);
|
||||
}
|
||||
|
||||
emit<Signal extends keyof typeof this.$signals>(
|
||||
signal: Signal,
|
||||
...args: Parameters<(typeof this.$signals)[Signal]>
|
||||
): void {
|
||||
super.emit(signal, ...args);
|
||||
}
|
||||
|
||||
connect<Signal extends keyof typeof this.$signals>(
|
||||
signal: Signal,
|
||||
callback: (typeof this.$signals)[Signal]
|
||||
): number {
|
||||
return super.connect(signal, callback);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Gtk } from "ags/gtk4";
|
||||
import { TileNetwork } from "./Network";
|
||||
import { TileBluetooth } from "./Bluetooth";
|
||||
import { TileDND } from "./DoNotDisturb";
|
||||
import { TileRecording } from "./Recording";
|
||||
import { TileNightLight } from "./NightLight";
|
||||
import { Pages } from "../pages";
|
||||
|
||||
|
||||
export let TilesPages: Pages|undefined;
|
||||
export const tileList: Array<() => JSX.Element|Gtk.Widget> = [
|
||||
TileNetwork,
|
||||
TileBluetooth,
|
||||
TileRecording,
|
||||
TileDND,
|
||||
TileNightLight
|
||||
] as Array<() => Gtk.Widget>;
|
||||
|
||||
export function Tiles(): Gtk.Widget {
|
||||
return <Gtk.Box class={"tiles-container"} orientation={Gtk.Orientation.VERTICAL}
|
||||
onUnmap={() => TilesPages = undefined}>
|
||||
|
||||
<Gtk.FlowBox orientation={Gtk.Orientation.HORIZONTAL} rowSpacing={6}
|
||||
columnSpacing={6} minChildrenPerLine={2} activateOnSingleClick
|
||||
maxChildrenPerLine={2} hexpand homogeneous>
|
||||
|
||||
{tileList.map(t => t())}
|
||||
</Gtk.FlowBox>
|
||||
|
||||
<Pages class={"tile-pages"} $={(self) => TilesPages = self} />
|
||||
</Gtk.Box> as Gtk.Box;
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Astal, Gtk } from "ags/gtk4";
|
||||
import { createBinding, createComputed, For } from "ags";
|
||||
import { Notifications } from "../modules/notifications";
|
||||
import { NotificationWidget } from "../widget/Notification";
|
||||
import { generalConfig } from "../app";
|
||||
import { Notifications } from "../../modules/notifications";
|
||||
import { NotificationWidget } from "../../widget/Notification";
|
||||
import { generalConfig } from "../../app";
|
||||
|
||||
import AstalNotifd from "gi://AstalNotifd";
|
||||
import Adw from "gi://Adw?version=1";
|
||||
|
||||
|
||||
const size = 450;
|
||||
|
||||
export const FloatingNotifications = (mon: number) =>
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Astal, Gdk, Gtk } from "ags/gtk4";
|
||||
import { execAsync } from "ags/process";
|
||||
import { generalConfig } from "../app";
|
||||
import { AskPopup } from "../widget/AskPopup";
|
||||
import { Notifications } from "../modules/notifications";
|
||||
import { NightLight } from "../modules/nightlight";
|
||||
import { time } from "../modules/utils";
|
||||
import { generalConfig } from "../../app";
|
||||
import { AskPopup } from "../../widget/AskPopup";
|
||||
import { Notifications } from "../../modules/notifications";
|
||||
import { NightLight } from "../../modules/nightlight";
|
||||
import { time } from "../../modules/utils";
|
||||
|
||||
import GObject from "ags/gobject";
|
||||
import AstalNotifd from "gi://AstalNotifd";
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Astal, Gtk } from "ags/gtk4";
|
||||
import { Accessor, createBinding, createState, With } from "ags";
|
||||
import { Wireplumber } from "../modules/volume";
|
||||
import { Windows } from "../windows";
|
||||
import { Backlights } from "../modules/backlight";
|
||||
import { construct, variableToBoolean } from "../modules/utils";
|
||||
import { Wireplumber } from "../../modules/volume";
|
||||
import { Windows } from "../../windows";
|
||||
import { Backlights } from "../../modules/backlight";
|
||||
import { construct, variableToBoolean } from "../../modules/utils";
|
||||
|
||||
import GObject, { ParamSpec, property, register } from "ags/gobject";
|
||||
import Pango from "gi://Pango?version=1.0";
|
||||
Reference in New Issue
Block a user