✨ chore: control-center and center-window widgets to gtk4 and ags v3
This commit is contained in:
@@ -1,227 +0,0 @@
|
|||||||
import { AstalIO, bind, Binding, exec, timeout } from "astal";
|
|
||||||
import { Gtk, Widget } from "astal/gtk3";
|
|
||||||
import AstalMpris from "gi://AstalMpris";
|
|
||||||
import { Clipboard } from "../../scripts/clipboard";
|
|
||||||
|
|
||||||
|
|
||||||
export function BigMedia(): Gtk.Widget {
|
|
||||||
let dragTimer: (AstalIO.Time|undefined);
|
|
||||||
|
|
||||||
return new Widget.Box({
|
|
||||||
className: "big-media",
|
|
||||||
orientation: Gtk.Orientation.VERTICAL,
|
|
||||||
homogeneous: false,
|
|
||||||
width_request: 250,
|
|
||||||
visible: bind(AstalMpris.get_default(), "players").as((players: Array<AstalMpris.Player>) =>
|
|
||||||
players[0] ? true : false),
|
|
||||||
children: bind(AstalMpris.get_default(), "players").as((players: Array<AstalMpris.Player>) =>
|
|
||||||
players[0] && [
|
|
||||||
new Widget.Box({
|
|
||||||
halign: Gtk.Align.CENTER,
|
|
||||||
child: new Widget.Box({
|
|
||||||
className: "image",
|
|
||||||
hexpand: false,
|
|
||||||
orientation: Gtk.Orientation.VERTICAL,
|
|
||||||
marginTop: 6,
|
|
||||||
visible: getAlbumArt(players[0]).as(Boolean),
|
|
||||||
css: getAlbumArt(players[0]).as((artUrl: string|undefined) =>
|
|
||||||
artUrl ? `.image { background-image: url('${artUrl}'); }` : undefined),
|
|
||||||
width_request: 132,
|
|
||||||
height_request: 128
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
} as Widget.BoxProps),
|
|
||||||
new Widget.Box({
|
|
||||||
className: "info",
|
|
||||||
orientation: Gtk.Orientation.VERTICAL,
|
|
||||||
vexpand: true,
|
|
||||||
valign: Gtk.Align.CENTER,
|
|
||||||
children: [
|
|
||||||
new Widget.Label({
|
|
||||||
className: "title",
|
|
||||||
tooltipText: bind(players[0], "title").as((title: string) => !title ? "No Title" : title),
|
|
||||||
label: bind(players[0], "title").as((title: string) => !title ? "No Title" : title),
|
|
||||||
truncate: true,
|
|
||||||
maxWidthChars: 25,
|
|
||||||
} as Widget.LabelProps),
|
|
||||||
new Widget.Label({
|
|
||||||
className: "artist",
|
|
||||||
tooltipText: bind(players[0], "artist").as((artist: string) => !artist ? "No Artist" : artist),
|
|
||||||
label: bind(players[0], "artist").as((artist: string) => !artist ? "No Artist" : artist),
|
|
||||||
maxWidthChars: 28,
|
|
||||||
truncate: true,
|
|
||||||
} as Widget.LabelProps)
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps),
|
|
||||||
new Widget.Box({
|
|
||||||
className: "progress",
|
|
||||||
hexpand: true,
|
|
||||||
visible: bind(players[0], "canSeek"),
|
|
||||||
children: [
|
|
||||||
new Widget.Slider({
|
|
||||||
min: 0,
|
|
||||||
hexpand: true,
|
|
||||||
max: bind(players[0], "length").as((length: number) =>
|
|
||||||
Math.floor(length)),
|
|
||||||
value: bind(players[0], "position").as((position: number) =>
|
|
||||||
Math.floor(position)),
|
|
||||||
onDragged: (slider: Widget.Slider) => {
|
|
||||||
if(dragTimer === undefined)
|
|
||||||
dragTimer = timeout(600, () =>
|
|
||||||
players[0].set_position(Math.round(slider.value)));
|
|
||||||
else {
|
|
||||||
dragTimer.cancel();
|
|
||||||
dragTimer = timeout(600, () =>
|
|
||||||
players[0].set_position(Math.round(slider.value)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}),
|
|
||||||
new Widget.CenterBox({
|
|
||||||
className: "bottom",
|
|
||||||
homogeneous: false,
|
|
||||||
hexpand: true,
|
|
||||||
marginBottom: 6,
|
|
||||||
startWidget: new Widget.Label({
|
|
||||||
className: "elapsed",
|
|
||||||
valign: Gtk.Align.START,
|
|
||||||
halign: Gtk.Align.START,
|
|
||||||
label: bind(players[0], "position").as((pos: number) => {
|
|
||||||
const sec: number = Math.floor(pos % 60);
|
|
||||||
return pos > 0 && players[0].length > 0 ?
|
|
||||||
`${Math.floor(pos / 60)}:${sec < 10 ? "0" : ""}${sec}`
|
|
||||||
: `0:00`;
|
|
||||||
})
|
|
||||||
} as Widget.LabelProps),
|
|
||||||
centerWidget: new Widget.Box({
|
|
||||||
className: "controls button-row",
|
|
||||||
children: [
|
|
||||||
new Widget.Button({
|
|
||||||
className: "link",
|
|
||||||
image: new Widget.Icon({
|
|
||||||
icon: "edit-paste-symbolic"
|
|
||||||
} as Widget.IconProps),
|
|
||||||
tooltipText: "Copy link to Clipboard",
|
|
||||||
visible: bind(players[0], "metadata").as(Boolean),
|
|
||||||
onClick: async () => {
|
|
||||||
const link = exec(`playerctl --player=${
|
|
||||||
players[0].busName.replace(/^org\.mpris\.MediaPlayer2\./i, "")
|
|
||||||
} metadata xesam:url`);
|
|
||||||
|
|
||||||
link && Clipboard.getDefault().copyAsync(link);
|
|
||||||
}
|
|
||||||
} as Widget.ButtonProps),
|
|
||||||
new Widget.Button({
|
|
||||||
className: "shuffle",
|
|
||||||
visible: bind(players[0], "shuffleStatus").as((shuffleStatus) =>
|
|
||||||
shuffleStatus !== AstalMpris.Shuffle.UNSUPPORTED),
|
|
||||||
image: new Widget.Icon({
|
|
||||||
icon: bind(players[0], "shuffleStatus").as((shuffleStatus) =>
|
|
||||||
shuffleStatus === AstalMpris.Shuffle.ON ?
|
|
||||||
"media-playlist-shuffle-symbolic"
|
|
||||||
: "media-playlist-consecutive-symbolic")
|
|
||||||
} as Widget.IconProps),
|
|
||||||
tooltipText: bind(players[0], "shuffleStatus").as((shuffleStatus) =>
|
|
||||||
shuffleStatus === AstalMpris.Shuffle.ON ?
|
|
||||||
"Shuffle"
|
|
||||||
: "No shuffle"),
|
|
||||||
onClick: () => players[0].shuffle()
|
|
||||||
} as Widget.ButtonProps),
|
|
||||||
new Widget.Button({
|
|
||||||
className: "previous",
|
|
||||||
image: new Widget.Icon({
|
|
||||||
icon: "media-skip-backward-symbolic"
|
|
||||||
} as Widget.IconProps),
|
|
||||||
tooltipText: "Previous",
|
|
||||||
onClick: () => players[0].canGoPrevious && players[0].previous()
|
|
||||||
} as Widget.ButtonProps),
|
|
||||||
new Widget.Button({
|
|
||||||
className: "pause",
|
|
||||||
tooltipText: bind(players[0], "playback_status").as((status) =>
|
|
||||||
status === AstalMpris.PlaybackStatus.PLAYING ? "Pause" : "Play"),
|
|
||||||
image: new Widget.Icon({
|
|
||||||
icon: bind(players[0], "playbackStatus").as((status) =>
|
|
||||||
status === AstalMpris.PlaybackStatus.PLAYING ?
|
|
||||||
"media-playback-pause-symbolic"
|
|
||||||
: "media-playback-start-symbolic"),
|
|
||||||
} as Widget.IconProps),
|
|
||||||
onClick: () => players[0].playbackStatus === AstalMpris.PlaybackStatus.PAUSED ?
|
|
||||||
players[0].play()
|
|
||||||
: players[0].pause()
|
|
||||||
} as Widget.ButtonProps),
|
|
||||||
new Widget.Button({
|
|
||||||
className: "next",
|
|
||||||
image: new Widget.Icon({
|
|
||||||
icon: "media-skip-forward-symbolic"
|
|
||||||
} as Widget.IconProps),
|
|
||||||
tooltipText: "Next",
|
|
||||||
onClick: () => players[0].canGoNext && players[0].next()
|
|
||||||
} as Widget.ButtonProps),
|
|
||||||
new Widget.Button({
|
|
||||||
className: "repeat",
|
|
||||||
visible: bind(players[0], "loopStatus").as((loopStatus) =>
|
|
||||||
loopStatus !== AstalMpris.Loop.UNSUPPORTED),
|
|
||||||
image: new Widget.Icon({
|
|
||||||
icon: bind(players[0], "loopStatus").as((loopStatus) => {
|
|
||||||
switch(loopStatus) {
|
|
||||||
case AstalMpris.Loop.TRACK:
|
|
||||||
return "media-playlist-repeat-song-symbolic";
|
|
||||||
|
|
||||||
case AstalMpris.Loop.PLAYLIST:
|
|
||||||
return "media-playlist-repeat-symbolic";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "loop-arrow-symbolic";
|
|
||||||
})
|
|
||||||
} as Widget.IconProps),
|
|
||||||
tooltipText: bind(players[0], "loopStatus").as((loopStatus) => {
|
|
||||||
switch(loopStatus) {
|
|
||||||
case AstalMpris.Loop.TRACK:
|
|
||||||
return "Loop song";
|
|
||||||
|
|
||||||
case AstalMpris.Loop.PLAYLIST:
|
|
||||||
return "Loop playlist";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "No loop";
|
|
||||||
}),
|
|
||||||
onClick: () => players[0].loop()
|
|
||||||
} as Widget.ButtonProps)
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps),
|
|
||||||
endWidget: new Widget.Label({
|
|
||||||
className: "length",
|
|
||||||
valign: Gtk.Align.START,
|
|
||||||
halign: Gtk.Align.END,
|
|
||||||
label: bind(players[0], "length").as((len/* bananananananana */: number) => {
|
|
||||||
const sec: number = Math.floor(len % 60);
|
|
||||||
return (len > 0 && Number.isFinite(len)) ?
|
|
||||||
`${Math.floor(len / 60)}:${sec < 10 ? "0" : ""}${sec}`
|
|
||||||
: "0:00";
|
|
||||||
})
|
|
||||||
} as Widget.LabelProps)
|
|
||||||
})
|
|
||||||
])
|
|
||||||
} as Widget.BoxProps);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function handles album art/cover of playing media. If a file is provided
|
|
||||||
* by the player, it adds the "file://" uri as a prefix, so you can use it in css.
|
|
||||||
*
|
|
||||||
* @param player the player you want to pull album art from
|
|
||||||
* @returns Binding to player.artUrl containing the album art uri, or an undefined binding ig none was found.
|
|
||||||
* */
|
|
||||||
function getAlbumArt(player: AstalMpris.Player): Binding<string | undefined> {
|
|
||||||
return bind(player, "artUrl").as((artUrl: string) => {
|
|
||||||
|
|
||||||
if(!artUrl)
|
|
||||||
return undefined;
|
|
||||||
|
|
||||||
if(artUrl.startsWith("/"))
|
|
||||||
return "file://" + artUrl;
|
|
||||||
|
|
||||||
return artUrl;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
import { timeout } from "ags/time";
|
||||||
|
import { execAsync } from "ags/process";
|
||||||
|
import { Astal, Gtk } from "ags/gtk4";
|
||||||
|
import { Clipboard } from "../../scripts/clipboard";
|
||||||
|
import { player } from "../bar/Media";
|
||||||
|
import { createBinding, With } from "ags";
|
||||||
|
|
||||||
|
import AstalMpris from "gi://AstalMpris";
|
||||||
|
import AstalIO from "gi://AstalIO";
|
||||||
|
import Gio from "gi://Gio?version=2.0";
|
||||||
|
import Pango from "gi://Pango?version=1.0";
|
||||||
|
|
||||||
|
|
||||||
|
export const BigMedia = () => {
|
||||||
|
let dragTimer: (AstalIO.Time|undefined);
|
||||||
|
|
||||||
|
return <Gtk.Box class={"big-media"} orientation={Gtk.Orientation.VERTICAL} widthRequest={250}
|
||||||
|
visible={player(pl => pl.available)}>
|
||||||
|
|
||||||
|
<With value={player}>
|
||||||
|
{(player: AstalMpris.Player) => player.available &&
|
||||||
|
<Gtk.Box halign={Gtk.Align.CENTER} $={(self) => {
|
||||||
|
const artSub = createBinding(player, "artUrl").subscribe(() => {
|
||||||
|
const firstChild = self.get_first_child();
|
||||||
|
const albumArt = getAlbumArt(player);
|
||||||
|
|
||||||
|
if(!albumArt) {
|
||||||
|
if(firstChild instanceof Gtk.Picture)
|
||||||
|
self.remove(firstChild);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(firstChild instanceof Gtk.Picture) {
|
||||||
|
firstChild.set_filename(albumArt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.prepend(
|
||||||
|
<Gtk.Picture file={Gio.File.new_for_path(albumArt)}
|
||||||
|
hexpand={false} vexpand={false} marginTop={6}
|
||||||
|
widthRequest={132} heightRequest={128}
|
||||||
|
/> as Gtk.Picture
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const destroyId = self.connect("destroy", () => {
|
||||||
|
self.disconnect(destroyId);
|
||||||
|
artSub();
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
|
||||||
|
<Gtk.Box class={"info"} orientation={Gtk.Orientation.VERTICAL}
|
||||||
|
valign={Gtk.Align.CENTER} vexpand={true}>
|
||||||
|
|
||||||
|
<Gtk.Label class={"title"} tooltipText={
|
||||||
|
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")
|
||||||
|
} ellipsize={Pango.EllipsizeMode.END} maxWidthChars={28}
|
||||||
|
/>
|
||||||
|
</Gtk.Box>
|
||||||
|
|
||||||
|
<Gtk.Box class={"progress"} hexpand={true} visible={createBinding(player, "canSeek")}>
|
||||||
|
<Astal.Slider hexpand={true} 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 = timeout(200, () =>
|
||||||
|
player.position = Math.floor(value));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dragTimer.cancel();
|
||||||
|
dragTimer = timeout(200, () =>
|
||||||
|
player.position = Math.floor(value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Gtk.Box>
|
||||||
|
|
||||||
|
<Gtk.CenterBox class={"bottom"} hexpand={true} marginBottom={6}>
|
||||||
|
<Gtk.Label class={"elapsed"} valign={Gtk.Align.START} 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";
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Gtk.Box class={"controls button-row"}>
|
||||||
|
<Gtk.Button class={"link"} iconName={"edit-paste-symbolic"}
|
||||||
|
tooltipText={"Copy link to clipboard"}
|
||||||
|
onClicked={() => {
|
||||||
|
execAsync(`playerctl --player=${
|
||||||
|
player.busName.replace(/^org\.mpris\.MediaPlayer2\./i, "")
|
||||||
|
} metadata xesam:url`).then(link => {
|
||||||
|
Clipboard.getDefault().copyAsync(link);
|
||||||
|
}).catch((e: Error) => {
|
||||||
|
console.error(`Media: couldn't copy media link. Stderr: \n${e.message}\n${e.stack}`);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<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.Label class={"length"} valign={Gtk.Align.START} 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";
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Gtk.CenterBox>
|
||||||
|
</Gtk.Box>
|
||||||
|
}
|
||||||
|
</With>
|
||||||
|
</Gtk.Box> as Gtk.Box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function handles album art/cover of playing media. If a file is provided
|
||||||
|
* by the player, it adds the "file://" uri as a prefix, so you can use it in css.
|
||||||
|
*
|
||||||
|
* @param player the player you want to pull album art from
|
||||||
|
* @returns Binding to player.artUrl containing the album art uri, or an undefined binding ig none was found.
|
||||||
|
* */
|
||||||
|
function getAlbumArt(player: AstalMpris.Player): string|undefined {
|
||||||
|
const artUrl = player.artUrl;
|
||||||
|
|
||||||
|
if(!artUrl)
|
||||||
|
return undefined;
|
||||||
|
|
||||||
|
if(artUrl.startsWith("/"))
|
||||||
|
return "file://" + artUrl;
|
||||||
|
|
||||||
|
return artUrl;
|
||||||
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { register, Variable } from "astal";
|
|
||||||
import { Gtk, Widget } from "astal/gtk3";
|
|
||||||
|
|
||||||
type CalendarProps = Pick<Widget.BoxProps,
|
|
||||||
"name"
|
|
||||||
| "className"
|
|
||||||
| "css"
|
|
||||||
| "expand"
|
|
||||||
| "halign"
|
|
||||||
| "valign"> & {
|
|
||||||
|
|
||||||
showWeekDays?: boolean;
|
|
||||||
showHeader?: boolean;
|
|
||||||
fillGrid?: boolean; // I need a better name for this LMAOOO
|
|
||||||
};
|
|
||||||
|
|
||||||
@register({ GTypeName: "Calendar" })
|
|
||||||
class Calendar extends Gtk.Box {
|
|
||||||
#showWeekDays = new Variable<boolean>(true);
|
|
||||||
#showHeader = new Variable<boolean>(true);
|
|
||||||
#fillGrid = new Variable<boolean>(false);
|
|
||||||
|
|
||||||
set fillGrid(newValue: boolean) { this.#fillGrid.set(newValue); }
|
|
||||||
get fillGrid() { return this.#fillGrid.get(); }
|
|
||||||
set showHeader(newValue: boolean) { this.#showHeader.set(newValue); }
|
|
||||||
get showHeader() { return this.#showHeader.get(); }
|
|
||||||
set showWeekDays(newValue: boolean) { this.#showWeekDays.set(newValue); }
|
|
||||||
get showWeekDays() { return this.#showWeekDays.get(); }
|
|
||||||
|
|
||||||
constructor(props?: CalendarProps) {
|
|
||||||
super();
|
|
||||||
this.add(new Widget.Box({
|
|
||||||
...props,
|
|
||||||
widthRequest: 128,
|
|
||||||
heightRequest: 128,
|
|
||||||
children: [
|
|
||||||
new Widget.Box({
|
|
||||||
className: "header",
|
|
||||||
heightRequest: 24,
|
|
||||||
hexpand: true,
|
|
||||||
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import { bind } from "astal";
|
|
||||||
import { Gtk, Widget } from "astal/gtk3";
|
|
||||||
import { HistoryNotification, Notifications } from "../../scripts/notifications";
|
|
||||||
import { NotificationWidget } from "../Notification";
|
|
||||||
import { tr } from "../../i18n/intl";
|
|
||||||
|
|
||||||
|
|
||||||
export const NotifHistory = () => {
|
|
||||||
return new Widget.Box({
|
|
||||||
orientation: Gtk.Orientation.VERTICAL,
|
|
||||||
className: bind(Notifications.getDefault(), "history").as(history => history.length > 0 ? "history" : "history hide"),
|
|
||||||
children: [
|
|
||||||
new Widget.Scrollable({
|
|
||||||
className: "history",
|
|
||||||
hscroll: Gtk.PolicyType.NEVER,
|
|
||||||
vscroll: Gtk.PolicyType.AUTOMATIC,
|
|
||||||
propagateNaturalHeight: true,
|
|
||||||
propagateNaturalWidth: false,
|
|
||||||
onDraw: (scrollable) => {
|
|
||||||
if(!(scrollable.get_child()! as Gtk.Viewport).get_child()) return;
|
|
||||||
|
|
||||||
scrollable.minContentHeight =
|
|
||||||
((scrollable.get_child()! as Gtk.Viewport).get_child() as Widget.Box
|
|
||||||
).get_children()?.[0].get_allocation().height
|
|
||||||
|| 0;
|
|
||||||
},
|
|
||||||
child: new Widget.Box({
|
|
||||||
className: "notifications",
|
|
||||||
hexpand: true,
|
|
||||||
orientation: Gtk.Orientation.VERTICAL,
|
|
||||||
homogeneous: false,
|
|
||||||
spacing: 4,
|
|
||||||
valign: Gtk.Align.START,
|
|
||||||
children: bind(Notifications.getDefault(), "history").as((history: Array<HistoryNotification>) =>
|
|
||||||
history.map((notification: HistoryNotification) => NotificationWidget(notification,
|
|
||||||
() => Notifications.getDefault().removeHistory(notification.id), true)
|
|
||||||
))
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
} as Widget.ScrollableProps),
|
|
||||||
new Widget.Box({
|
|
||||||
vexpand: false,
|
|
||||||
hexpand: true,
|
|
||||||
halign: Gtk.Align.END,
|
|
||||||
className: "button-row",
|
|
||||||
children: [
|
|
||||||
new Widget.Button({
|
|
||||||
className: "clear-all",
|
|
||||||
child: new Widget.Box({
|
|
||||||
children: [
|
|
||||||
new Widget.Icon({
|
|
||||||
css: "margin-right: 6px;",
|
|
||||||
icon: "edit-clear-all-symbolic"
|
|
||||||
} as Widget.IconProps),
|
|
||||||
new Widget.Label({
|
|
||||||
label: tr("clear")
|
|
||||||
} as Widget.LabelProps)
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps),
|
|
||||||
onClick: () => Notifications.getDefault().clearHistory(),
|
|
||||||
} as Widget.ButtonProps)
|
|
||||||
]
|
|
||||||
})
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { Gtk } from "ags/gtk4";
|
||||||
|
import { HistoryNotification, Notifications } from "../../scripts/notifications";
|
||||||
|
import { NotificationWidget } from "../Notification";
|
||||||
|
import { tr } from "../../i18n/intl";
|
||||||
|
import { createBinding, For } from "ags";
|
||||||
|
import AstalNotifd from "gi://AstalNotifd?version=0.1";
|
||||||
|
|
||||||
|
|
||||||
|
export const NotifHistory = () =>
|
||||||
|
<Gtk.Box orientation={Gtk.Orientation.VERTICAL}
|
||||||
|
class={createBinding(Notifications.getDefault(), "history").as(history =>
|
||||||
|
`history ${history.length < 1 ? "hide" : ""}`)}>
|
||||||
|
|
||||||
|
<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 hexpand={true} class={"button-row"} halign={Gtk.Align.END}>
|
||||||
|
<Gtk.Button class={"clear-all"} iconName={"edit-clear-all-symbolic"}
|
||||||
|
label={tr("clear")} onClicked={Notifications.getDefault().clearHistory} />
|
||||||
|
</Gtk.Box>
|
||||||
|
</Gtk.Box> as Gtk.Box;
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import { register, timeout } from "astal";
|
|
||||||
import { Gtk, Widget } from "astal/gtk3";
|
|
||||||
import { Page } from "./pages/Page";
|
|
||||||
|
|
||||||
|
|
||||||
export { Pages };
|
|
||||||
export type PagesProps = {
|
|
||||||
initialPage?: Page;
|
|
||||||
className?: string;
|
|
||||||
transitionDuration?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
@register({ GTypeName: "Pages" })
|
|
||||||
class Pages extends Widget.Box {
|
|
||||||
#page: (Page|undefined);
|
|
||||||
#transDuration: number;
|
|
||||||
#transType: Gtk.RevealerTransitionType = Gtk.RevealerTransitionType.SLIDE_DOWN;
|
|
||||||
|
|
||||||
get isOpen() { return (this.get_children().length > 0); }
|
|
||||||
|
|
||||||
constructor(props?: PagesProps) {
|
|
||||||
super({
|
|
||||||
className: props?.className,
|
|
||||||
orientation: Gtk.Orientation.VERTICAL
|
|
||||||
});
|
|
||||||
|
|
||||||
this.name = "pages";
|
|
||||||
|
|
||||||
if(props?.className !== null && props?.className !== undefined)
|
|
||||||
this.className = props?.className;
|
|
||||||
|
|
||||||
this.#transDuration = props?.transitionDuration ?? 280;
|
|
||||||
|
|
||||||
if(props?.initialPage)
|
|
||||||
this.open(props.initialPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
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, onOpened?: () => void) {
|
|
||||||
this.add(new Widget.Revealer({
|
|
||||||
transitionDuration: this.#transDuration,
|
|
||||||
transitionType: this.#transType,
|
|
||||||
revealChild: false,
|
|
||||||
child: newPage
|
|
||||||
} as Widget.RevealerProps));
|
|
||||||
this.#page = newPage;
|
|
||||||
|
|
||||||
this.reorder_child(this.get_children()[this.get_children().length - 1], 0);
|
|
||||||
(this.get_children()[0] as Widget.Revealer).set_reveal_child(true);
|
|
||||||
onOpened?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
close(onClosed?: () => void): void {
|
|
||||||
(this.get_children() as Array<Widget.Revealer>).forEach((pageRevealer, i, pageRevealers) => {
|
|
||||||
pageRevealer.set_reveal_child(false);
|
|
||||||
if(this.#page?.id === (pageRevealer.get_child() as Page).id)
|
|
||||||
this.#page = undefined;
|
|
||||||
|
|
||||||
timeout(this.#transDuration, () => {
|
|
||||||
this.remove(pageRevealer);
|
|
||||||
pageRevealer.destroy();
|
|
||||||
|
|
||||||
i === (pageRevealers.length - 1) &&
|
|
||||||
onClosed?.();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { register } from "ags/gobject";
|
||||||
|
import { Gtk } from "ags/gtk4";
|
||||||
|
import { Page } from "./pages/Page";
|
||||||
|
import AstalIO from "gi://AstalIO";
|
||||||
|
import { timeout } from "ags/time";
|
||||||
|
import { variableToBoolean } from "../../scripts/utils";
|
||||||
|
|
||||||
|
|
||||||
|
export { Pages };
|
||||||
|
export type PagesProps = {
|
||||||
|
initialPage?: Page;
|
||||||
|
class?: string;
|
||||||
|
transitionDuration?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
@register({ GTypeName: "Pages" })
|
||||||
|
class Pages extends Gtk.Box {
|
||||||
|
#timeouts: Array<[AstalIO.Time, (() => void)|undefined]> = [];
|
||||||
|
#page: (Page|undefined);
|
||||||
|
#pageWidget: (Gtk.Revealer|undefined);
|
||||||
|
#transDuration: number;
|
||||||
|
#transType: Gtk.RevealerTransitionType = Gtk.RevealerTransitionType.SLIDE_DOWN;
|
||||||
|
|
||||||
|
get isOpen() { return Boolean(this.get_first_child()); }
|
||||||
|
|
||||||
|
constructor(props?: PagesProps) {
|
||||||
|
super({
|
||||||
|
orientation: Gtk.Orientation.VERTICAL,
|
||||||
|
cssName: "pages"
|
||||||
|
});
|
||||||
|
|
||||||
|
this.name = "pages";
|
||||||
|
props?.class?.split(' ').filter(variableToBoolean).forEach(clss =>
|
||||||
|
this.add_css_class(clss));
|
||||||
|
|
||||||
|
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, onOpened?: () => void) {
|
||||||
|
const pageWidget = <Gtk.Revealer
|
||||||
|
transitionDuration={this.#transDuration}
|
||||||
|
transitionType={this.#transType}
|
||||||
|
revealChild={false}>
|
||||||
|
|
||||||
|
{newPage as unknown as Gtk.Widget}
|
||||||
|
</Gtk.Revealer> as Gtk.Revealer;
|
||||||
|
|
||||||
|
this.prepend(pageWidget);
|
||||||
|
|
||||||
|
this.#pageWidget = pageWidget;
|
||||||
|
this.#page = newPage;
|
||||||
|
|
||||||
|
this.reorder_child_after(this.get_last_child()!, null);
|
||||||
|
(this.get_first_child() as Gtk.Revealer).revealChild = true;
|
||||||
|
onOpened?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
close(onClosed?: () => void): void {
|
||||||
|
if(!this.#pageWidget) return;
|
||||||
|
|
||||||
|
this.#pageWidget.revealChild = false;
|
||||||
|
const closingPage = this.#pageWidget!;
|
||||||
|
|
||||||
|
this.#timeouts.push([
|
||||||
|
timeout(closingPage.transitionDuration, () => {
|
||||||
|
this.remove(closingPage);
|
||||||
|
onClosed?.();
|
||||||
|
}),
|
||||||
|
onClosed]);
|
||||||
|
|
||||||
|
this.#pageWidget = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
import { exec, GLib, Variable } from "astal";
|
|
||||||
import { Gtk, Widget } from "astal/gtk3";
|
|
||||||
import { Windows } from "../../windows";
|
|
||||||
import { Wallpaper } from "../../scripts/wallpaper";
|
|
||||||
import { execApp } from "../../scripts/apps";
|
|
||||||
|
|
||||||
|
|
||||||
function LockButton(): Widget.Button {
|
|
||||||
return new Widget.Button({
|
|
||||||
image: new Widget.Icon({
|
|
||||||
icon: "system-lock-screen-symbolic"
|
|
||||||
} as Widget.IconProps),
|
|
||||||
onClick: () => {
|
|
||||||
Windows.close("control-center");
|
|
||||||
execApp("hyprlock");
|
|
||||||
}
|
|
||||||
} as Widget.ButtonProps)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ColorPickerButton(): Widget.Button {
|
|
||||||
return new Widget.Button({
|
|
||||||
image: new Widget.Icon({
|
|
||||||
icon: "color-select-symbolic"
|
|
||||||
} as Widget.IconProps),
|
|
||||||
onClick: () => {
|
|
||||||
Windows.close("control-center");
|
|
||||||
execApp("sh $HOME/.config/hypr/scripts/color-picker.sh");
|
|
||||||
}
|
|
||||||
} as Widget.ButtonProps)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ScreenshotButton(): Widget.Button {
|
|
||||||
return new Widget.Button({
|
|
||||||
image: new Widget.Icon({
|
|
||||||
icon: "applets-screenshooter-symbolic"
|
|
||||||
} as Widget.IconProps),
|
|
||||||
onClick: () => {
|
|
||||||
Windows.close("control-center");
|
|
||||||
execApp(`sh ${GLib.get_user_config_dir()}/hypr/scripts/screenshot.sh`);
|
|
||||||
}
|
|
||||||
} as Widget.ButtonProps);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectWallpaperButton(): Widget.Button {
|
|
||||||
return new Widget.Button({
|
|
||||||
image: new Widget.Icon({
|
|
||||||
icon: "preferences-desktop-wallpaper-symbolic"
|
|
||||||
} as Widget.IconProps),
|
|
||||||
onClick: () => {
|
|
||||||
Windows.close("control-center");
|
|
||||||
Wallpaper.getDefault().pickWallpaper();
|
|
||||||
}
|
|
||||||
} as Widget.ButtonProps);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LogoutButton(): Widget.Button {
|
|
||||||
return new Widget.Button({
|
|
||||||
image: new Widget.Icon({
|
|
||||||
icon: "system-shutdown-symbolic"
|
|
||||||
} as Widget.IconProps),
|
|
||||||
onClick: () => {
|
|
||||||
Windows.close("control-center");
|
|
||||||
Windows.open("logout-menu");
|
|
||||||
}
|
|
||||||
} as Widget.ButtonProps);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const QuickActions = () => {
|
|
||||||
const uptime = new Variable<string>("Just turned on").poll(1000,
|
|
||||||
() => exec("uptime -p").replace(/^up /, ""));
|
|
||||||
|
|
||||||
return new Widget.Box({
|
|
||||||
className: "quickactions",
|
|
||||||
children: [
|
|
||||||
new Widget.Box({
|
|
||||||
orientation: Gtk.Orientation.VERTICAL,
|
|
||||||
halign: Gtk.Align.START,
|
|
||||||
hexpand: true,
|
|
||||||
className: "left",
|
|
||||||
children: [
|
|
||||||
new Widget.Label({
|
|
||||||
className: "hostname",
|
|
||||||
xalign: 0,
|
|
||||||
tooltipText: "Host name",
|
|
||||||
label: GLib.get_host_name()
|
|
||||||
} as Widget.LabelProps),
|
|
||||||
new Widget.Box({
|
|
||||||
children: [
|
|
||||||
new Widget.Icon({
|
|
||||||
icon: "hourglass-symbolic"
|
|
||||||
} as Widget.IconProps),
|
|
||||||
new Widget.Label({
|
|
||||||
className: "uptime",
|
|
||||||
xalign: 0,
|
|
||||||
tooltipText: "Uptime",
|
|
||||||
onDestroy: () => uptime.drop(),
|
|
||||||
label: uptime()
|
|
||||||
} as Widget.LabelProps)
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps),
|
|
||||||
new Widget.Box({
|
|
||||||
orientation: Gtk.Orientation.HORIZONTAL,
|
|
||||||
className: "right button-row",
|
|
||||||
halign: Gtk.Align.END,
|
|
||||||
hexpand: true,
|
|
||||||
children: [
|
|
||||||
LockButton(),
|
|
||||||
ColorPickerButton(),
|
|
||||||
ScreenshotButton(),
|
|
||||||
SelectWallpaperButton(),
|
|
||||||
LogoutButton()
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { Gtk } from "ags/gtk4";
|
||||||
|
import { Windows } from "../../windows";
|
||||||
|
import { Wallpaper } from "../../scripts/wallpaper";
|
||||||
|
import { execApp } from "../../scripts/apps";
|
||||||
|
import GLib from "gi://GLib?version=2.0";
|
||||||
|
import { Accessor } from "ags";
|
||||||
|
import { createPoll } from "ags/time";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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 orientation={Gtk.Orientation.VERTICAL} halign={Gtk.Align.START}
|
||||||
|
hexpand={true} class={"left"}>
|
||||||
|
|
||||||
|
<Gtk.Label class={"hostname"} xalign={0} tooltipText={"Host name"}
|
||||||
|
label={GLib.get_host_name()} />
|
||||||
|
|
||||||
|
<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 class={"right button-row"} halign={Gtk.Align.END}
|
||||||
|
hexpand={true}>
|
||||||
|
|
||||||
|
<LockButton />
|
||||||
|
<ColorPickerButton />
|
||||||
|
<ScreenshotButton />
|
||||||
|
<SelectWallpaperButton />
|
||||||
|
<LogoutButton />
|
||||||
|
</Gtk.Box>
|
||||||
|
</Gtk.Box> as Gtk.Box;
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import { bind } from "astal";
|
|
||||||
import { Gtk, Widget } from "astal/gtk3";
|
|
||||||
import { Wireplumber } from "../../scripts/volume";
|
|
||||||
import { Pages } from "./Pages";
|
|
||||||
import { PageSound } from "./pages/Sound";
|
|
||||||
import { PageMicrophone } from "./pages/Microphone";
|
|
||||||
|
|
||||||
export function Sliders() {
|
|
||||||
const slidersPages = new Pages();
|
|
||||||
|
|
||||||
return new Widget.Box({
|
|
||||||
className: "sliders",
|
|
||||||
orientation: Gtk.Orientation.VERTICAL,
|
|
||||||
expand: true,
|
|
||||||
spacing: 10,
|
|
||||||
children: [
|
|
||||||
new Widget.Box({
|
|
||||||
className: "sink speaker",
|
|
||||||
spacing: 3,
|
|
||||||
children: bind(Wireplumber.getWireplumber(), "defaultSpeaker").as((sink) => [
|
|
||||||
new Widget.Button({
|
|
||||||
onClick: () => Wireplumber.getDefault().toggleMuteSink(),
|
|
||||||
image: new Widget.Icon ({
|
|
||||||
icon: bind(sink, "volumeIcon").as((icon) =>
|
|
||||||
!Wireplumber.getDefault().isMutedSink() && Wireplumber.getDefault().getSinkVolume() > 0 ? icon : "audio-volume-muted-symbolic"),
|
|
||||||
} as Widget.IconProps),
|
|
||||||
} as Widget.ButtonProps),
|
|
||||||
new Widget.Slider({
|
|
||||||
drawValue: false,
|
|
||||||
hexpand: true,
|
|
||||||
setup: (slider) => slider.value = Math.floor(sink.volume * 100),
|
|
||||||
value: bind(sink, "volume").as((vol) => Math.floor(vol * 100)),
|
|
||||||
max: Wireplumber.getDefault().getMaxSinkVolume(),
|
|
||||||
onDragged: (slider) => sink.volume = slider.value / 100
|
|
||||||
} as Widget.SliderProps),
|
|
||||||
new Widget.Button({
|
|
||||||
className: "more",
|
|
||||||
image: new Widget.Icon({
|
|
||||||
icon: "go-next-symbolic",
|
|
||||||
} as Widget.IconProps),
|
|
||||||
onClick: (_) => slidersPages.toggle(PageSound())
|
|
||||||
} as Widget.ButtonProps)
|
|
||||||
])
|
|
||||||
} as Widget.BoxProps),
|
|
||||||
new Widget.Box({
|
|
||||||
className: "source microphone",
|
|
||||||
spacing: 3,
|
|
||||||
children: bind(Wireplumber.getWireplumber(), "defaultMicrophone").as((source) => [
|
|
||||||
new Widget.Button({
|
|
||||||
onClick: () => Wireplumber.getDefault().toggleMuteSource(),
|
|
||||||
image: new Widget.Icon ({
|
|
||||||
icon: bind(source, "volumeIcon").as((icon) =>
|
|
||||||
!Wireplumber.getDefault().isMutedSource() && Wireplumber.getDefault().getSourceVolume() > 0 ? icon : "microphone-sensitivity-muted-symbolic"),
|
|
||||||
} as Widget.IconProps),
|
|
||||||
} as Widget.ButtonProps),
|
|
||||||
new Widget.Slider({
|
|
||||||
drawValue: false,
|
|
||||||
hexpand: true,
|
|
||||||
setup: (slider) => slider.set_value(Math.floor(source.volume * 100)),
|
|
||||||
value: bind(source, "volume").as((vol) => Math.floor(vol * 100)),
|
|
||||||
max: Wireplumber.getDefault().getMaxSourceVolume(),
|
|
||||||
onDragged: (slider) => source.volume = slider.value / 100
|
|
||||||
} as Widget.SliderProps),
|
|
||||||
new Widget.Button({
|
|
||||||
className: "more",
|
|
||||||
image: new Widget.Icon({
|
|
||||||
icon: "go-next-symbolic",
|
|
||||||
} as Widget.IconProps),
|
|
||||||
onClick: (_) => slidersPages.toggle(PageMicrophone())
|
|
||||||
} as Widget.ButtonProps)
|
|
||||||
])
|
|
||||||
} as Widget.BoxProps),
|
|
||||||
slidersPages
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { Astal, Gtk } from "ags/gtk4";
|
||||||
|
import { Wireplumber } from "../../scripts/volume";
|
||||||
|
import { Pages } from "./Pages";
|
||||||
|
import { PageSound } from "./pages/Sound";
|
||||||
|
import { PageMicrophone } from "./pages/Microphone";
|
||||||
|
import { createBinding, With } from "ags";
|
||||||
|
import AstalWp from "gi://AstalWp";
|
||||||
|
|
||||||
|
|
||||||
|
export function Sliders() {
|
||||||
|
const slidersPages = <Pages /> as Pages;
|
||||||
|
|
||||||
|
return <Gtk.Box class={"sliders"} orientation={Gtk.Orientation.VERTICAL}
|
||||||
|
hexpand={true} spacing={10}>
|
||||||
|
|
||||||
|
<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={true}
|
||||||
|
$={(self) => self.value = Math.floor(sink.volume * 100)}
|
||||||
|
value={createBinding(sink, "volume").as(v => Math.floor(v * 100))}
|
||||||
|
max={Wireplumber.getDefault().getMaxSinkVolume()}
|
||||||
|
onChangeValue={(_, _scrollType, value) => sink.set_volume(value / 100)} />
|
||||||
|
|
||||||
|
<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().toggleMuteSink}
|
||||||
|
iconName={createBinding(source, "volumeIcon").as((icon) =>
|
||||||
|
(!Wireplumber.getDefault().isMutedSink() &&
|
||||||
|
Wireplumber.getDefault().getSinkVolume() > 0) ?
|
||||||
|
icon
|
||||||
|
: "microphone-sensitivity-muted-symbolic"
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<Astal.Slider drawValue={false} hexpand={true}
|
||||||
|
$={(self) => self.value = Math.floor(source.volume * 100)}
|
||||||
|
value={createBinding(source, "volume").as(v => Math.floor(v * 100))}
|
||||||
|
max={Wireplumber.getDefault().getMaxSinkVolume()}
|
||||||
|
onChangeValue={(_, _scrollType, value) => source.set_volume(value / 100)} />
|
||||||
|
|
||||||
|
<Gtk.Button class={"more"} iconName={"go-next-symbolic"} onClicked={(_) =>
|
||||||
|
slidersPages.toggle(PageMicrophone())} />
|
||||||
|
</Gtk.Box>}
|
||||||
|
</With>
|
||||||
|
{slidersPages}
|
||||||
|
</Gtk.Box>
|
||||||
|
}
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import { Gtk, Widget } from "astal/gtk3";
|
|
||||||
import { TileNetwork } from "./tiles/Network";
|
|
||||||
import { TileBluetooth } from "./tiles/Bluetooth";
|
|
||||||
import { TileDND } from "./tiles/DoNotDisturb";
|
|
||||||
import { TileRecording } from "./tiles/Recording";
|
|
||||||
import { TileNightLight } from "./tiles/NightLight";
|
|
||||||
import { Pages } from "./Pages";
|
|
||||||
import { GObject } from "astal";
|
|
||||||
|
|
||||||
export const tileList: Array<() => Gtk.Widget> = [
|
|
||||||
TileNetwork,
|
|
||||||
TileBluetooth,
|
|
||||||
TileRecording,
|
|
||||||
TileDND,
|
|
||||||
TileNightLight
|
|
||||||
];
|
|
||||||
|
|
||||||
export let TilesPages: (Pages|null) = null;
|
|
||||||
|
|
||||||
export function Tiles(): Gtk.Widget {
|
|
||||||
const tilesFlowBox: Gtk.FlowBox = new Gtk.FlowBox({
|
|
||||||
visible: true,
|
|
||||||
orientation: Gtk.Orientation.HORIZONTAL,
|
|
||||||
rowSpacing: 6,
|
|
||||||
columnSpacing: 6,
|
|
||||||
minChildrenPerLine: 2,
|
|
||||||
maxChildrenPerLine: 2,
|
|
||||||
expand: true,
|
|
||||||
homogeneous: true,
|
|
||||||
} as Gtk.FlowBox.ConstructorProps);
|
|
||||||
|
|
||||||
tileList.map((item: (() => Gtk.Widget)) => {
|
|
||||||
const tile = item();
|
|
||||||
tilesFlowBox.insert(tile, -1);
|
|
||||||
|
|
||||||
const children = tilesFlowBox.get_children();
|
|
||||||
children[children.length-1]!.set_can_focus(false);
|
|
||||||
const binding: GObject.Binding = tile.bind_property("visible",
|
|
||||||
children[children.length-1], "visible",
|
|
||||||
GObject.BindingFlags.SYNC_CREATE);
|
|
||||||
|
|
||||||
const destroyId: number = tile.connect("destroy-event", (self: typeof tile) => {
|
|
||||||
binding.unbind();
|
|
||||||
self.disconnect(destroyId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Widget.Box({
|
|
||||||
className: "tiles-container",
|
|
||||||
orientation: Gtk.Orientation.VERTICAL,
|
|
||||||
onDestroy: () => TilesPages = null,
|
|
||||||
setup: (box) => {
|
|
||||||
if(!TilesPages) TilesPages = new Pages({
|
|
||||||
className: "tile-pages"
|
|
||||||
});
|
|
||||||
|
|
||||||
box.set_children([
|
|
||||||
tilesFlowBox,
|
|
||||||
TilesPages!
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
} as Widget.BoxProps);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Gtk } from "ags/gtk4";
|
||||||
|
import { TileNetwork } from "./tiles/Network";
|
||||||
|
import { TileBluetooth } from "./tiles/Bluetooth";
|
||||||
|
import { TileDND } from "./tiles/DoNotDisturb";
|
||||||
|
import { TileRecording } from "./tiles/Recording";
|
||||||
|
import { TileNightLight } from "./tiles/NightLight";
|
||||||
|
import { Pages } from "./Pages";
|
||||||
|
|
||||||
|
|
||||||
|
export let TilesPages: (Pages|null) = null;
|
||||||
|
export const tileList: Array<() => Gtk.Widget> = [
|
||||||
|
TileNetwork,
|
||||||
|
TileBluetooth,
|
||||||
|
TileRecording,
|
||||||
|
TileDND,
|
||||||
|
TileNightLight
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Tiles(): Gtk.Widget {
|
||||||
|
return <Gtk.Box class={"tiles-container"} orientation={Gtk.Orientation.VERTICAL}
|
||||||
|
onDestroy={() => TilesPages = null} $={(self) => {
|
||||||
|
if(!TilesPages)
|
||||||
|
TilesPages = <Pages class="tile-pages" /> as Pages;
|
||||||
|
|
||||||
|
self.append(TilesPages as unknown as Gtk.Widget);
|
||||||
|
}}>
|
||||||
|
|
||||||
|
<Gtk.FlowBox orientation={Gtk.Orientation.HORIZONTAL} rowSpacing={6}
|
||||||
|
columnSpacing={6} minChildrenPerLine={2} activateOnSingleClick={true}
|
||||||
|
maxChildrenPerLine={2} hexpand={true} vexpand={true} homogeneous={true}>
|
||||||
|
|
||||||
|
{tileList.map(tile => tile())}
|
||||||
|
</Gtk.FlowBox>
|
||||||
|
</Gtk.Box> as Gtk.Box;
|
||||||
|
}
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
import { bind, Gio, Variable } from "astal";
|
|
||||||
import { Gtk, Widget } from "astal/gtk3";
|
|
||||||
import AstalBluetooth from "gi://AstalBluetooth";
|
|
||||||
import { Page, PageButton } from "./Page";
|
|
||||||
import { tr } from "../../../i18n/intl";
|
|
||||||
import { Windows } from "../../../windows";
|
|
||||||
import { Notifications } from "../../../scripts/notifications";
|
|
||||||
import AstalNotifd from "gi://AstalNotifd";
|
|
||||||
import { execApp } from "../../../scripts/apps";
|
|
||||||
|
|
||||||
export const BluetoothPage: (() => Page) = () => new Page({
|
|
||||||
id: "bluetooth",
|
|
||||||
title: tr("control_center.pages.bluetooth.title"),
|
|
||||||
description: tr("control_center.pages.bluetooth.description"),
|
|
||||||
className: "bluetooth",
|
|
||||||
headerButtons: [
|
|
||||||
new Widget.Button({
|
|
||||||
className: "discover",
|
|
||||||
image: new Widget.Icon({
|
|
||||||
icon: bind(AstalBluetooth.get_default().adapter, "discovering").as((discovering) =>
|
|
||||||
!discovering ?
|
|
||||||
"arrow-circular-top-right-symbolic"
|
|
||||||
: "media-playback-stop-symbolic")
|
|
||||||
} as Widget.IconProps),
|
|
||||||
tooltipText: bind(AstalBluetooth.get_default().adapter, "discovering").as((discovering) =>
|
|
||||||
!discovering ?
|
|
||||||
tr("control_center.pages.bluetooth.start_discovering")
|
|
||||||
: tr("control_center.pages.bluetooth.stop_discovering")),
|
|
||||||
onClick: () => {
|
|
||||||
if(AstalBluetooth.get_default().adapter.discovering) {
|
|
||||||
AstalBluetooth.get_default().adapter.stop_discovery();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
AstalBluetooth.get_default().adapter.start_discovery();
|
|
||||||
}
|
|
||||||
} as Widget.ButtonProps)
|
|
||||||
],
|
|
||||||
onClose: () => AstalBluetooth.get_default().adapter.discovering &&
|
|
||||||
AstalBluetooth.get_default().adapter.stop_discovery(),
|
|
||||||
bottomButtons: [{
|
|
||||||
title: tr("control_center.pages.more_settings"),
|
|
||||||
onClick: () => {
|
|
||||||
Windows.close("control-center");
|
|
||||||
execApp("overskride", "[float; animation slide right]");
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
spacing: 2,
|
|
||||||
children: [
|
|
||||||
new Widget.Box({
|
|
||||||
className: "adapters",
|
|
||||||
visible: bind(AstalBluetooth.get_default(), "adapters").as((adapters) =>
|
|
||||||
adapters.length > 1),
|
|
||||||
spacing: 2,
|
|
||||||
children: bind(AstalBluetooth.get_default(), "adapters").as((adapters) => [
|
|
||||||
new Widget.Label({
|
|
||||||
className: "sub-header",
|
|
||||||
label: tr("control_center.pages.bluetooth.adapters")
|
|
||||||
} as Widget.LabelProps),
|
|
||||||
...adapters.map(adapter =>
|
|
||||||
PageButton({
|
|
||||||
title: adapter.alias ?? "Adapter",
|
|
||||||
icon: "bluetooth-active-symbolic",
|
|
||||||
onClick: () => AstalBluetooth.get_default(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
} as Widget.BoxProps),
|
|
||||||
new Widget.Box({
|
|
||||||
className: "connections",
|
|
||||||
orientation: Gtk.Orientation.VERTICAL,
|
|
||||||
hexpand: true,
|
|
||||||
spacing: 2,
|
|
||||||
children: [
|
|
||||||
new Widget.Box({
|
|
||||||
className: "paired",
|
|
||||||
orientation: Gtk.Orientation.VERTICAL,
|
|
||||||
spacing: 2,
|
|
||||||
visible: bind(AstalBluetooth.get_default(), "devices").as((devs) =>
|
|
||||||
devs.filter(dev => dev.paired || dev.connected || dev.trusted).length > 0),
|
|
||||||
children: bind(AstalBluetooth.get_default(), "devices").as((devs) => {
|
|
||||||
const connectedDevices = devs.filter((dev) => dev.connected || dev.paired || dev.trusted)
|
|
||||||
|
|
||||||
return [
|
|
||||||
new Widget.Label({
|
|
||||||
className: "sub-header",
|
|
||||||
label: tr("devices"),
|
|
||||||
xalign: 0,
|
|
||||||
} as Widget.LabelProps),
|
|
||||||
...connectedDevices.map((dev) => DeviceWidget(dev))
|
|
||||||
]
|
|
||||||
})
|
|
||||||
} as Widget.BoxProps),
|
|
||||||
new Widget.Box({
|
|
||||||
className: "discovered",
|
|
||||||
orientation: Gtk.Orientation.VERTICAL,
|
|
||||||
spacing: 2,
|
|
||||||
visible: bind(AstalBluetooth.get_default(), "devices").as((devs) =>
|
|
||||||
devs.filter((dev) => !dev.connected && !dev.paired && !dev.trusted).length > 0),
|
|
||||||
children: bind(AstalBluetooth.get_default(), "devices").as((devices) => {
|
|
||||||
const discoveredDevices = devices.filter((dev) => !dev.connected && !dev.paired && !dev.trusted);
|
|
||||||
|
|
||||||
return [
|
|
||||||
new Widget.Label({
|
|
||||||
className: "sub-header",
|
|
||||||
label: tr("control_center.pages.bluetooth.new_devices"),
|
|
||||||
xalign: 0
|
|
||||||
} as Widget.LabelProps),
|
|
||||||
...discoveredDevices.map((dev: AstalBluetooth.Device) => DeviceWidget(dev))
|
|
||||||
]
|
|
||||||
})
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
function DeviceWidget(dev: AstalBluetooth.Device): Gtk.Widget {
|
|
||||||
const devActions: Variable<Array<Widget.Button>> = Variable.derive([
|
|
||||||
bind(dev, "connected"),
|
|
||||||
bind(dev, "paired"),
|
|
||||||
bind(dev, "trusted")
|
|
||||||
], (connected, paired, trusted) => paired ? [
|
|
||||||
new Widget.Button({
|
|
||||||
image: new Widget.Icon({
|
|
||||||
icon: connected ?
|
|
||||||
"list-remove-symbolic"
|
|
||||||
: "user-trash-symbolic"
|
|
||||||
} as Widget.IconProps),
|
|
||||||
tooltipText: tr(connected ? "disconnect" : "control_center.pages.bluetooth.unpair_device"),
|
|
||||||
onClick: () => {
|
|
||||||
if(!connected) {
|
|
||||||
AstalBluetooth.get_default().adapter?.remove_device(dev);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dev.disconnect_device(null);
|
|
||||||
},
|
|
||||||
} as Widget.ButtonProps),
|
|
||||||
new Widget.Button({
|
|
||||||
image: new Widget.Icon({
|
|
||||||
icon: trusted ?
|
|
||||||
"shield-safe-symbolic"
|
|
||||||
: "shield-danger-symbolic"
|
|
||||||
} as Widget.IconProps),
|
|
||||||
tooltipText: tr(`control_center.pages.bluetooth.${trusted ? "un": ""}trust_device`),
|
|
||||||
onClick: () => dev.set_trusted(!trusted)
|
|
||||||
} as Widget.ButtonProps)
|
|
||||||
] : []);
|
|
||||||
|
|
||||||
return PageButton({
|
|
||||||
className: bind(dev, "connected").as((connected) => connected ? "connected" : ""),
|
|
||||||
title: bind(dev, "alias").as(alias => alias ?? "Unknown Device"),
|
|
||||||
icon: dev.icon ?? "bluetooth-active-symbolic",
|
|
||||||
description: bind(dev, "connecting").as(connecting =>
|
|
||||||
connecting ? `${tr("connecting")}...` : ""),
|
|
||||||
tooltipText: bind(dev, "connected").as(connected => !connected ?
|
|
||||||
tr("connect")
|
|
||||||
: ""),
|
|
||||||
onDestroy: () => devActions.drop(),
|
|
||||||
onClick: () => {
|
|
||||||
if(dev.connected) return;
|
|
||||||
|
|
||||||
let skipConnection: boolean = false;
|
|
||||||
if(!dev.paired)
|
|
||||||
(async () => dev.pair())().catch((err: Gio.IOErrorEnum) => {
|
|
||||||
skipConnection = true;
|
|
||||||
Notifications.getDefault().sendNotification({
|
|
||||||
appName: "bluetooth",
|
|
||||||
summary: "Device pairing error",
|
|
||||||
body: `Couldn't connect to ${dev.alias ?? dev.name}, an error occurred: ${err.message || err.stack}`,
|
|
||||||
urgency: AstalNotifd.Urgency.NORMAL
|
|
||||||
})
|
|
||||||
}).then(() => dev.set_trusted(true));
|
|
||||||
|
|
||||||
if(!skipConnection)
|
|
||||||
(async () => dev.connect_device(null))().catch((err: Gio.IOErrorEnum) =>
|
|
||||||
Notifications.getDefault().sendNotification({
|
|
||||||
appName: "bluetooth",
|
|
||||||
summary: "Device connection error",
|
|
||||||
body: `Couldn't connect to ${dev.alias ?? dev.name}, an error occurred: ${err.message || err.stack}`,
|
|
||||||
urgency: AstalNotifd.Urgency.NORMAL
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
endWidget: new Widget.Box({
|
|
||||||
visible: bind(dev, "batteryPercentage").as((batt: number) =>
|
|
||||||
batt <= -1 ? false : true),
|
|
||||||
children: [
|
|
||||||
new Widget.Box({
|
|
||||||
visible: bind(dev, "connected"),
|
|
||||||
children: [
|
|
||||||
new Widget.Label({
|
|
||||||
halign: Gtk.Align.END,
|
|
||||||
label: bind(dev, "batteryPercentage").as((batt: number) =>
|
|
||||||
`${Math.floor(batt * 100)}%`)
|
|
||||||
} as Widget.LabelProps),
|
|
||||||
new Widget.Icon({
|
|
||||||
icon: bind(dev, "batteryPercentage").as(batt =>
|
|
||||||
`battery-level-${Math.floor(batt * 100)}-symbolic`),
|
|
||||||
css: "font-size: 16px; margin-left: 6px;"
|
|
||||||
} as Widget.IconProps)
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps),
|
|
||||||
extraButtons: devActions()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import { Gtk } from "ags/gtk4";
|
||||||
|
import { Page, PageButton } from "./Page";
|
||||||
|
import { tr } from "../../../i18n/intl";
|
||||||
|
import { Windows } from "../../../windows";
|
||||||
|
import { Notifications } from "../../../scripts/notifications";
|
||||||
|
import { execApp } from "../../../scripts/apps";
|
||||||
|
|
||||||
|
import AstalNotifd from "gi://AstalNotifd";
|
||||||
|
import AstalBluetooth from "gi://AstalBluetooth";
|
||||||
|
import { variableToBoolean } from "../../../scripts/utils";
|
||||||
|
import { createBinding, createComputed, For, With } from "ags";
|
||||||
|
|
||||||
|
|
||||||
|
export const BluetoothPage = () => <Page
|
||||||
|
id={"bluetooth"} title={tr("control_center.pages.bluetooth.title")}
|
||||||
|
description={tr("control_center.pages.bluetooth.description")}
|
||||||
|
class={"bluetooth"} headerButtons={[
|
||||||
|
<Gtk.Button class={"discover"} iconName={createBinding(
|
||||||
|
AstalBluetooth.get_default().adapter, "discovering"
|
||||||
|
).as(discovering => discovering ?
|
||||||
|
"arrow-circular-top-right-symbolic"
|
||||||
|
: "media-playback-stop-symbolic")} tooltipText={
|
||||||
|
createBinding(AstalBluetooth.get_default().adapter, "discovering").as((discovering) =>
|
||||||
|
!discovering ?
|
||||||
|
tr("control_center.pages.bluetooth.start_discovering")
|
||||||
|
: tr("control_center.pages.bluetooth.stop_discovering"))}
|
||||||
|
onClicked={() => {
|
||||||
|
if(AstalBluetooth.get_default().adapter.discovering) {
|
||||||
|
AstalBluetooth.get_default().adapter.stop_discovery();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AstalBluetooth.get_default().adapter.start_discovery();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
]}
|
||||||
|
onClose={() => AstalBluetooth.get_default().adapter.discovering &&
|
||||||
|
AstalBluetooth.get_default().adapter.stop_discovery()}
|
||||||
|
bottomButtons={[{
|
||||||
|
title: tr("control_center.pages.more_settings"),
|
||||||
|
onClick: () => {
|
||||||
|
Windows.getDefault().close("control-center");
|
||||||
|
execApp("overskride", "[float; animation slide right]");
|
||||||
|
}
|
||||||
|
}]} spacing={2}>
|
||||||
|
<Gtk.Box class={"adapters"} visible={variableToBoolean(createBinding(
|
||||||
|
AstalBluetooth.get_default(), "adapters"))} spacing={2}>
|
||||||
|
|
||||||
|
<Gtk.Label class={"sub-header"} label={tr("control_center.pages.bluetooth.adapters")} />
|
||||||
|
<For each={createBinding(AstalBluetooth.get_default(), "adapters")}>
|
||||||
|
{(adapter: AstalBluetooth.Adapter) =>
|
||||||
|
<PageButton title={adapter.alias ?? "Adapter"}
|
||||||
|
icon={"bluetooth-active-symbolic"} />
|
||||||
|
}
|
||||||
|
</For>
|
||||||
|
</Gtk.Box>
|
||||||
|
<Gtk.Box class={"connections"} orientation={Gtk.Orientation.VERTICAL} hexpand={true}
|
||||||
|
spacing={2}>
|
||||||
|
|
||||||
|
<Gtk.Box class={"paired"} orientation={Gtk.Orientation.VERTICAL} spacing={2}
|
||||||
|
visible={createBinding(AstalBluetooth.get_default(), "devices").as(devs =>
|
||||||
|
devs.filter(dev => dev.paired || dev.connected || dev.trusted).length > 0)}>
|
||||||
|
|
||||||
|
<Gtk.Label class={"sub-header"} label={tr("devices")} xalign={0} />
|
||||||
|
<For each={createBinding(AstalBluetooth.get_default(), "devices").as(devs =>
|
||||||
|
devs.filter(dev => dev.paired || dev.connected || dev.trusted))}>
|
||||||
|
|
||||||
|
{(dev: AstalBluetooth.Device) => <DeviceWidget device={dev} />}
|
||||||
|
</For>
|
||||||
|
</Gtk.Box>
|
||||||
|
<Gtk.Box class={"discovered"} orientation={Gtk.Orientation.VERTICAL} spacing={2}
|
||||||
|
visible={createBinding(AstalBluetooth.get_default(), "devices").as(devs =>
|
||||||
|
devs.filter(dev => !dev.connected && !dev.paired && !dev.trusted).length > 0)}>
|
||||||
|
|
||||||
|
<Gtk.Label class={"sub-header"} label={tr("control_center.pages.bluetooth.new_devices")}
|
||||||
|
xalign={0} />
|
||||||
|
<For each={createBinding(AstalBluetooth.get_default(), "devices").as(devs =>
|
||||||
|
devs.filter(dev => !dev.connected && !dev.paired && !dev.trusted))}>
|
||||||
|
{(dev: AstalBluetooth.Device) => <DeviceWidget device={dev} />}
|
||||||
|
</For>
|
||||||
|
</Gtk.Box>
|
||||||
|
</Gtk.Box>
|
||||||
|
</Page> as Page;
|
||||||
|
|
||||||
|
function DeviceWidget({ device }: { device: AstalBluetooth.Device }): Gtk.Widget {
|
||||||
|
return <PageButton class={createBinding(device, "connected").as(conn =>
|
||||||
|
conn ? "connected" : "")} title={
|
||||||
|
createBinding(device, "alias").as(alias => alias ?? "Unknown Device")}
|
||||||
|
icon={createBinding(device, "icon").as(ico => ico ?? "bluetooth-active-symbolic")}
|
||||||
|
description={
|
||||||
|
createBinding(device, "connecting").as(connecting =>
|
||||||
|
connecting ? `${tr("connecting")}...` : "")}
|
||||||
|
tooltipText={
|
||||||
|
createBinding(device, "connected").as(connected =>
|
||||||
|
!connected ? tr("connect") : "")
|
||||||
|
} onClick={() => {
|
||||||
|
if(device.connected) return;
|
||||||
|
|
||||||
|
let skipConnection: boolean = false;
|
||||||
|
if(!device.paired)
|
||||||
|
(async () => device.pair())().catch((err: Error) => {
|
||||||
|
skipConnection = true;
|
||||||
|
Notifications.getDefault().sendNotification({
|
||||||
|
appName: "bluetooth",
|
||||||
|
summary: "Device pairing error",
|
||||||
|
body: `Couldn't connect to ${device.alias ?? device.name}, an error occurred: ${err.message || err.stack}`,
|
||||||
|
urgency: AstalNotifd.Urgency.NORMAL
|
||||||
|
})
|
||||||
|
}).then(() => device.set_trusted(true));
|
||||||
|
|
||||||
|
if(!skipConnection)
|
||||||
|
(async () => device.connect_device(null))().catch((err: Error) =>
|
||||||
|
Notifications.getDefault().sendNotification({
|
||||||
|
appName: "bluetooth",
|
||||||
|
summary: "Device connection error",
|
||||||
|
body: `Couldn't connect to ${device.alias ?? device.name}, an error occurred: ${err.message || err.stack}`,
|
||||||
|
urgency: AstalNotifd.Urgency.NORMAL
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
endWidget={<Gtk.Box visible={createComputed([
|
||||||
|
createBinding(device, "batteryPercentage"),
|
||||||
|
createBinding(device, "connected")
|
||||||
|
]).as(([batt, connected]) => connected && (batt > -1))
|
||||||
|
}>
|
||||||
|
<Gtk.Label halign={Gtk.Align.END} label={
|
||||||
|
createBinding(device, "batteryPercentage").as(batt =>
|
||||||
|
`${Math.floor(batt * 100)}%`)} />
|
||||||
|
|
||||||
|
<Gtk.Image iconName={
|
||||||
|
createBinding(device, "batteryPercentage").as(batt =>
|
||||||
|
`battery-level-${Math.floor(batt * 100)}-symbolic`)
|
||||||
|
} css={"font-size: 16px; margin-left: 6px;"} />
|
||||||
|
</Gtk.Box>} extraButtons={<With value={createComputed([
|
||||||
|
createBinding(device, "connected"),
|
||||||
|
createBinding(device, "trusted")
|
||||||
|
])}>
|
||||||
|
{([connected, trusted]: [boolean, boolean]) => trusted &&
|
||||||
|
<Gtk.Box class={"button-row"}>
|
||||||
|
{<Gtk.Button iconName={connected ?
|
||||||
|
"list-remove-symbolic"
|
||||||
|
: "user-trash-symbolic"} tooltipText={tr(connected ?
|
||||||
|
"disconnect"
|
||||||
|
: "control_center.pages.bluetooth.unpair_device"
|
||||||
|
)} onClicked={() => {
|
||||||
|
if(!connected) {
|
||||||
|
AstalBluetooth.get_default().adapter?.remove_device(device);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
device.disconnect_device(null);
|
||||||
|
}} />}
|
||||||
|
|
||||||
|
<Gtk.Button iconName={trusted ?
|
||||||
|
"shield-safe-symbolic"
|
||||||
|
: "shield-danger-symbolic"} tooltipText={tr(
|
||||||
|
`control_center.pages.bluetooth.${trusted ? "un" : ""}trust_device`
|
||||||
|
)} onClicked={() => device.set_trusted(!trusted)}
|
||||||
|
/>
|
||||||
|
</Gtk.Box>}
|
||||||
|
</With>}
|
||||||
|
/> as Page;
|
||||||
|
}
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { bind } from "astal";
|
|
||||||
import { Page, PageButton, PageProps } from "./Page";
|
|
||||||
import { Wireplumber } from "../../../scripts/volume";
|
|
||||||
import { Astal, Widget } from "astal/gtk3";
|
|
||||||
import { tr } from "../../../i18n/intl";
|
|
||||||
|
|
||||||
|
|
||||||
export function PageMicrophone(): Page {
|
|
||||||
return new Page({
|
|
||||||
id: "microphone",
|
|
||||||
title: tr("control_center.pages.microphone.title"),
|
|
||||||
description: tr("control_center.pages.microphone.description"),
|
|
||||||
children: bind(Wireplumber.getWireplumber().get_audio()!, "microphones").as((microphones) => [
|
|
||||||
new Widget.Label({
|
|
||||||
className: "sub-header",
|
|
||||||
label: tr("devices"),
|
|
||||||
xalign: 0
|
|
||||||
} as Widget.LabelProps),
|
|
||||||
...microphones.map((microphone) =>
|
|
||||||
PageButton({
|
|
||||||
className: bind(microphone, "isDefault").as(isDefault => isDefault ? "default" : ""),
|
|
||||||
icon: bind(microphone, "icon").as(icon =>
|
|
||||||
Astal.Icon.lookup_icon(icon) ? icon : "audio-input-microphone-symbolic"),
|
|
||||||
title: bind(microphone, "description").as(desc => desc ?? "Microphone"),
|
|
||||||
onClick: () => microphone.set_is_default(true),
|
|
||||||
endWidget: new Widget.Icon({
|
|
||||||
icon: "object-select-symbolic",
|
|
||||||
visible: bind(microphone, "isDefault"),
|
|
||||||
css: "font-size: 18px;"
|
|
||||||
} as Widget.IconProps)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
])
|
|
||||||
} as PageProps);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Page, PageButton } from "./Page";
|
||||||
|
import { Wireplumber } from "../../../scripts/volume";
|
||||||
|
import { Gtk } from "ags/gtk4";
|
||||||
|
import { tr } from "../../../i18n/intl";
|
||||||
|
import { createBinding, For } from "ags";
|
||||||
|
import AstalWp from "gi://AstalWp?version=0.1";
|
||||||
|
import { lookupIcon } from "../../../scripts/apps";
|
||||||
|
|
||||||
|
|
||||||
|
export function PageMicrophone(): Page {
|
||||||
|
return <Page id={"microphone"} title={tr("control_center.pages.microphone.title")}
|
||||||
|
description={tr("control_center.pages.microphone.description")}>
|
||||||
|
|
||||||
|
<Gtk.Label class={"sub-header"} label={tr("devices")} xalign={0} />
|
||||||
|
<For each={createBinding(Wireplumber.getWireplumber().get_audio()!, "microphones")}>
|
||||||
|
{(source: AstalWp.Endpoint) => <PageButton class={
|
||||||
|
createBinding(source, "isDefault").as(isDefault => isDefault ? "default" : "")
|
||||||
|
} icon={createBinding(source, "icon").as(ico => lookupIcon(ico) ?
|
||||||
|
ico : "audio-input-microphone-symbolic")} title={
|
||||||
|
createBinding(source, "description").as(desc => desc ?? "Microphone")
|
||||||
|
} onClick={() => !source.isDefault && source.set_is_default(true)}
|
||||||
|
endWidget={
|
||||||
|
<Gtk.Image iconName={"object-select-symbolic"} visible={
|
||||||
|
createBinding(source, "isDefault")} css={"font-size: 18px;"}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>}
|
||||||
|
</For>
|
||||||
|
</Page> as Page;
|
||||||
|
}
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
import { Gtk, Widget } from "astal/gtk3";
|
|
||||||
import { Page, PageButton } from "./Page";
|
|
||||||
import AstalNetwork from "gi://AstalNetwork";
|
|
||||||
import { bind, GLib } from "astal";
|
|
||||||
import NM from "gi://NM";
|
|
||||||
import { Windows } from "../../../windows";
|
|
||||||
import { tr } from "../../../i18n/intl";
|
|
||||||
import { execApp } from "../../../scripts/apps";
|
|
||||||
import { EntryPopup, EntryPopupProps } from "../../EntryPopup";
|
|
||||||
import { Notifications } from "../../../scripts/notifications";
|
|
||||||
import { AskPopup, AskPopupProps } from "../../AskPopup";
|
|
||||||
import { encoder } from "../../../scripts/utils";
|
|
||||||
|
|
||||||
export const PageNetwork: (() => Page) = () => new Page({
|
|
||||||
id: "network",
|
|
||||||
title: tr("control_center.pages.network.title"),
|
|
||||||
className: "network",
|
|
||||||
headerButtons: [
|
|
||||||
new Widget.Button({
|
|
||||||
className: "reload",
|
|
||||||
image: new Widget.Icon({
|
|
||||||
icon: "arrow-circular-top-right-symbolic"
|
|
||||||
} as Widget.IconProps),
|
|
||||||
visible: bind(AstalNetwork.get_default(), "primary").as((primary) =>
|
|
||||||
primary === AstalNetwork.Primary.WIFI),
|
|
||||||
tooltipText: "Re-scan connections",
|
|
||||||
onClick: () => AstalNetwork.get_default().wifi.scan()
|
|
||||||
} as Widget.ButtonProps)
|
|
||||||
],
|
|
||||||
bottomButtons: [{
|
|
||||||
title: tr("control_center.pages.more_settings"),
|
|
||||||
onClick: () => {
|
|
||||||
Windows.close("control-center");
|
|
||||||
execApp("nm-connection-editor", "[animationstyle gnomed]");
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
children: [
|
|
||||||
new Widget.Box({
|
|
||||||
className: "devices",
|
|
||||||
hexpand: true,
|
|
||||||
orientation: Gtk.Orientation.VERTICAL,
|
|
||||||
visible: bind(AstalNetwork.get_default().get_client(), "devices").as((devs) => devs.length > 0),
|
|
||||||
children: bind(AstalNetwork.get_default().get_client(), "devices").as((devices) => {
|
|
||||||
devices = devices.filter(dev => dev.interface !== "lo");
|
|
||||||
|
|
||||||
return [
|
|
||||||
new Widget.Label({
|
|
||||||
label: tr("devices"),
|
|
||||||
xalign: 0,
|
|
||||||
className: "sub-header",
|
|
||||||
} as Widget.LabelProps),
|
|
||||||
...devices.filter(device => device.real).map(dev => PageButton({
|
|
||||||
className: "device",
|
|
||||||
icon: bind(dev, "deviceType").as(deviceType =>
|
|
||||||
deviceType === NM.DeviceType.WIFI ?
|
|
||||||
"network-wireless-symbolic"
|
|
||||||
: "network-wired-symbolic"),
|
|
||||||
title: bind(dev, "interface").as(iface => iface ??
|
|
||||||
tr("control_center.pages.network.interface")),
|
|
||||||
extraButtons: [
|
|
||||||
new Widget.Button({
|
|
||||||
image: new Widget.Icon({
|
|
||||||
icon: "view-more-symbolic"
|
|
||||||
} as Widget.IconProps),
|
|
||||||
onClick: () => {
|
|
||||||
Windows.close("control-center");
|
|
||||||
execApp(
|
|
||||||
`nm-connection-editor --edit ${dev.activeConnection?.connection.get_uuid()}`,
|
|
||||||
"[animationstyle gnomed; float]"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} as Widget.ButtonProps)
|
|
||||||
]
|
|
||||||
})
|
|
||||||
)
|
|
||||||
]
|
|
||||||
})
|
|
||||||
} as Widget.BoxProps),
|
|
||||||
new Widget.Box({
|
|
||||||
className: "wireless-aps",
|
|
||||||
visible: bind(AstalNetwork.get_default(), "primary").as((primary) => primary === AstalNetwork.Primary.WIFI),
|
|
||||||
hexpand: true,
|
|
||||||
orientation: Gtk.Orientation.VERTICAL,
|
|
||||||
children: AstalNetwork.get_default().wifi ? bind(AstalNetwork.get_default().wifi, "accessPoints").as((aps) => [
|
|
||||||
new Widget.Label({
|
|
||||||
className: "sub-header",
|
|
||||||
label: "Wi-Fi"
|
|
||||||
} as Widget.LabelProps),
|
|
||||||
...aps.filter(ap => ap.ssid).map(ap => PageButton({
|
|
||||||
className: bind(AstalNetwork.get_default().wifi, "activeAccessPoint").as(activeAP =>
|
|
||||||
activeAP.ssid === ap.ssid ? "active" : ""),
|
|
||||||
title: bind(ap, "ssid").as(ssid =>
|
|
||||||
ssid ?? "Unknown SSID"),
|
|
||||||
icon: bind(ap, "iconName"),
|
|
||||||
endWidget: new Widget.Icon({
|
|
||||||
// @ts-ignore ts-for-gir generated the types wrong
|
|
||||||
icon: bind(ap, "flags").as(flags => flags & NM["80211ApFlags" as keyof typeof NM].PRIVACY ?
|
|
||||||
"channel-secure-symbolic"
|
|
||||||
: "channel-insecure-symbolic"),
|
|
||||||
css: "font-size: 18px;"
|
|
||||||
} as Widget.IconProps),
|
|
||||||
extraButtons: [
|
|
||||||
new Widget.Button({
|
|
||||||
image: new Widget.Icon({
|
|
||||||
icon: "window-close-symbolic",
|
|
||||||
css: "font-size: 18px;"
|
|
||||||
} as Widget.IconProps)
|
|
||||||
} as Widget.ButtonProps)
|
|
||||||
],
|
|
||||||
onClick: () => {
|
|
||||||
const ssid: string = ap.ssid ?? "Unknown SSID",
|
|
||||||
ssidBytes = GLib.Bytes.new(encoder.encode(ssid));
|
|
||||||
|
|
||||||
const connection = new NM.Connection();
|
|
||||||
const setting = NM.SettingWireless.new();
|
|
||||||
setting.ssid = ssidBytes;
|
|
||||||
setting.bssid = ap.bssid;
|
|
||||||
|
|
||||||
connection.add_setting(setting);
|
|
||||||
|
|
||||||
// @ts-ignore same as previous, type gen issues
|
|
||||||
// Check if access point has encryption(needs a password)
|
|
||||||
if(ap.flags & NM["80211ApFlags" as keyof typeof NM].PRIVACY) {
|
|
||||||
const passwdPopup = EntryPopup({
|
|
||||||
isPassword: true,
|
|
||||||
title: `${tr("connect")}: ${ssid}`,
|
|
||||||
acceptText: tr("connect"),
|
|
||||||
closeOnAccept: false,
|
|
||||||
text: `Input password for ${ssid}`,
|
|
||||||
onAccept: (input) => {
|
|
||||||
const pskSetting = NM.SettingWirelessSecurity.new();
|
|
||||||
pskSetting.keyMgmt = "wpa-psk";
|
|
||||||
|
|
||||||
// @ts-ignore type gen issues (the type exists)
|
|
||||||
if(ap.flags & NM["80211ApSecurityFlags" as keyof typeof NM].KEY_MGMT_SAE)
|
|
||||||
pskSetting.keyMgmt = "sae";
|
|
||||||
|
|
||||||
pskSetting.psk = input;
|
|
||||||
|
|
||||||
AstalNetwork.get_default().get_client().add_connection_async(
|
|
||||||
connection, true, null, (client, asyncRes) => {
|
|
||||||
const remoteConnection = client!.add_connection_finish(asyncRes);
|
|
||||||
if(!remoteConnection) {
|
|
||||||
notifyConnectionError(ssid);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
passwdPopup.close();
|
|
||||||
saveToDisk(remoteConnection, ssid);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
} as EntryPopupProps);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
AstalNetwork.get_default().get_client().add_connection_async(connection, false, null, (_, asyncRes) => {
|
|
||||||
const remoteConnection = AstalNetwork.get_default().get_client().add_connection_finish(asyncRes);
|
|
||||||
|
|
||||||
if(!remoteConnection) {
|
|
||||||
notifyConnectionError(ssid);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
activateWirelessConnection(remoteConnection, ssid);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
]
|
|
||||||
) : [],
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
function activateWirelessConnection(connection: NM.RemoteConnection, ssid: string): void {
|
|
||||||
AstalNetwork.get_default().get_client().activate_connection_async(
|
|
||||||
connection, AstalNetwork.get_default().wifi.get_device(), null, null, (_, asyncRes) => {
|
|
||||||
const activeConnection = AstalNetwork.get_default().get_client().activate_connection_finish(asyncRes);
|
|
||||||
if(!activeConnection) {
|
|
||||||
Notifications.getDefault().sendNotification({
|
|
||||||
appName: "network",
|
|
||||||
summary: "Couldn't activate wireless connection",
|
|
||||||
body: `An error occurred while activating the wireless connection "${ssid}"`
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function notifyConnectionError(ssid: string): void {
|
|
||||||
Notifications.getDefault().sendNotification({
|
|
||||||
appName: "network",
|
|
||||||
summary: "Coudn't connect Wi-Fi",
|
|
||||||
body: `An error occurred while trying to connect to the "${ssid}" access point. \nMaybe the password is invalid?`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function saveToDisk(remoteConnection: NM.RemoteConnection, ssid: string): void {
|
|
||||||
AskPopup({
|
|
||||||
text: `Save password for connection "${ssid}"?`,
|
|
||||||
acceptText: "Yes",
|
|
||||||
onAccept: () => remoteConnection.commit_changes_async(true, null, (_, asyncRes) =>
|
|
||||||
!remoteConnection.commit_changes_finish(asyncRes) && Notifications.getDefault().sendNotification({
|
|
||||||
appName: "network",
|
|
||||||
summary: "Couldn't save Wi-Fi password",
|
|
||||||
body: `An error occurred while trying to write the password for "${ssid}" to disk`
|
|
||||||
}))
|
|
||||||
} as AskPopupProps);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import { Gtk } from "ags/gtk4";
|
||||||
|
import { Page, PageButton } from "./Page";
|
||||||
|
import { Windows } from "../../../windows";
|
||||||
|
import { tr } from "../../../i18n/intl";
|
||||||
|
import { execApp } from "../../../scripts/apps";
|
||||||
|
import { Notifications } from "../../../scripts/notifications";
|
||||||
|
import { AskPopup, AskPopupProps } from "../../AskPopup";
|
||||||
|
import { encoder, variableToBoolean } from "../../../scripts/utils";
|
||||||
|
|
||||||
|
import GLib from "gi://GLib?version=2.0";
|
||||||
|
import NM from "gi://NM";
|
||||||
|
import AstalNetwork from "gi://AstalNetwork";
|
||||||
|
import { createBinding, For, With } from "ags";
|
||||||
|
|
||||||
|
|
||||||
|
export const PageNetwork = () =>
|
||||||
|
<Page id={"network"} title={tr("control_center.pages.network.title")}
|
||||||
|
class={"network"} headerButtons={[
|
||||||
|
<Gtk.Button class={"reload"} iconName={"arrow-circular-top-right-symbolic"}
|
||||||
|
visible={createBinding(AstalNetwork.get_default(), "primary").as(primary =>
|
||||||
|
primary === AstalNetwork.Primary.WIFI)}
|
||||||
|
tooltipText={"Re-scan networks"} onClicked={() =>
|
||||||
|
AstalNetwork.get_default().wifi.scan()}
|
||||||
|
/>
|
||||||
|
]} bottomButtons={[{
|
||||||
|
title: tr("control_center.pages.more_settings"),
|
||||||
|
onClick: () => {
|
||||||
|
Windows.getDefault().close("control-center");
|
||||||
|
execApp("nm-connection-editor", "[animationstyle gnomed]");
|
||||||
|
}
|
||||||
|
}]}>
|
||||||
|
|
||||||
|
<Gtk.Box class={"devices"} hexpand={true} orientation={Gtk.Orientation.VERTICAL}
|
||||||
|
visible={variableToBoolean(createBinding(AstalNetwork.get_default().client, "devices"))}>
|
||||||
|
|
||||||
|
<Gtk.Label label={tr("devices")} xalign={0} class={"sub-header"} />
|
||||||
|
<For each={createBinding(AstalNetwork.get_default().client, "devices").as(devs =>
|
||||||
|
devs.filter(dev => dev.interface !== "lo" && dev.real /* filter local device */))}>
|
||||||
|
|
||||||
|
{(device: NM.Device) => <PageButton title={createBinding(device, "interface").as(iface =>
|
||||||
|
iface ?? tr("control_center.pages.network.interface"))} class={"device"}
|
||||||
|
icon={createBinding(device, "deviceType").as(type => type === NM.DeviceType.WIFI ?
|
||||||
|
"network-wireless-symbolic" : "network-wired-symbolic")} extraButtons={[
|
||||||
|
|
||||||
|
<Gtk.Button iconName={"view-more-symbolic"} onClicked={() => {
|
||||||
|
Windows.getDefault().close("control-center");
|
||||||
|
execApp(
|
||||||
|
`nm-connection-editor --edit ${device.activeConnection?.connection.get_uuid()}`,
|
||||||
|
"[animationstyle gnomed; float]"
|
||||||
|
);
|
||||||
|
}} />
|
||||||
|
]}
|
||||||
|
/>}
|
||||||
|
</For>
|
||||||
|
</Gtk.Box>
|
||||||
|
|
||||||
|
<With value={createBinding(AstalNetwork.get_default(), "primary").as(primary =>
|
||||||
|
primary === AstalNetwork.Primary.WIFI)}>
|
||||||
|
|
||||||
|
{(isWifi: boolean) => isWifi && <Gtk.Box class={"wireless-aps"} hexpand={true}
|
||||||
|
orientation={Gtk.Orientation.VERTICAL}>
|
||||||
|
|
||||||
|
<Gtk.Label class={"sub-header"} label={"Wi-Fi"} />
|
||||||
|
<For each={createBinding(AstalNetwork.get_default().wifi, "accessPoints")}>
|
||||||
|
{(ap: AstalNetwork.AccessPoint) => <PageButton class={
|
||||||
|
createBinding(AstalNetwork.get_default().wifi, "activeAccessPoint").as(activeAP =>
|
||||||
|
activeAP.ssid === ap.ssid ? "active" : "")
|
||||||
|
} title={createBinding(ap, "ssid").as(ssid => ssid ?? "No SSID")}
|
||||||
|
icon={createBinding(ap, "iconName")} endWidget={<Gtk.Image iconName={
|
||||||
|
createBinding(ap, "flags").as(flags =>
|
||||||
|
// @ts-ignore
|
||||||
|
flags & NM["80211ApFlags"].PRIVACY ?
|
||||||
|
"channel-secure-symbolic"
|
||||||
|
: "channel-insecure-symbolic")}
|
||||||
|
css={"font-size: 18px;"}
|
||||||
|
/>} extraButtons={[
|
||||||
|
<Gtk.Button iconName={"window-close-symbolic"} visible={
|
||||||
|
createBinding(AstalNetwork.get_default().wifi, "activeAccessPoint").as(activeAp =>
|
||||||
|
activeAp.ssid === ap.ssid)
|
||||||
|
} css={"font-size: 18px;"} onClicked={() => {
|
||||||
|
const active = AstalNetwork.get_default().wifi.activeAccessPoint;
|
||||||
|
|
||||||
|
if(active?.ssid === ap.ssid) {
|
||||||
|
AstalNetwork.get_default().wifi.deactivate_connection((_, res) => {
|
||||||
|
try {
|
||||||
|
AstalNetwork.get_default().wifi.deactivate_connection_finish(res);
|
||||||
|
} catch(e: any) {
|
||||||
|
e = e as Error;
|
||||||
|
|
||||||
|
console.error(
|
||||||
|
`Network: couldn't deactivate connection with access point(SSID: ${
|
||||||
|
ap.ssid}. Stderr: \n${e.message}\n${e.stack}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}/>
|
||||||
|
]} onClick={() => {
|
||||||
|
const uuid = NM.utils_uuid_generate();
|
||||||
|
const ssidBytes = GLib.Bytes.new(encoder.encode(ap.ssid));
|
||||||
|
|
||||||
|
const connection = NM.SimpleConnection.new();
|
||||||
|
const connSetting = NM.SettingConnection.new();
|
||||||
|
const wifiSetting = NM.SettingWireless.new();
|
||||||
|
const wifiSecuritySetting = NM.SettingWirelessSecurity.new();
|
||||||
|
const setting8021x = NM.Setting8021x.new();
|
||||||
|
|
||||||
|
// @ts-ignore yep, type-gen issues again
|
||||||
|
if(ap.rsnFlags !& NM["80211ApSecurityFlags"].KEY_MGMT_802_1X &&
|
||||||
|
// @ts-ignore
|
||||||
|
ap.wpaFlags !& NM["80211ApSecurityFlags"].KEY_MGMT_802_1X) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
connSetting.uuid = uuid;
|
||||||
|
connection.add_setting(connSetting);
|
||||||
|
|
||||||
|
connection.add_setting(wifiSetting);
|
||||||
|
wifiSetting.ssid = ssidBytes;
|
||||||
|
|
||||||
|
wifiSecuritySetting.keyMgmt = "wpa-eap";
|
||||||
|
connection.add_setting(wifiSecuritySetting);
|
||||||
|
|
||||||
|
setting8021x.add_eap_method("ttls");
|
||||||
|
setting8021x.phase2Auth = "mschapv2";
|
||||||
|
connection.add_setting(setting8021x);
|
||||||
|
}}
|
||||||
|
/>}
|
||||||
|
</For>
|
||||||
|
</Gtk.Box>}
|
||||||
|
</With>
|
||||||
|
</Page> as Page;
|
||||||
|
|
||||||
|
function activateWirelessConnection(connection: NM.RemoteConnection, ssid: string): void {
|
||||||
|
AstalNetwork.get_default().get_client().activate_connection_async(
|
||||||
|
connection, AstalNetwork.get_default().wifi.get_device(), null, null, (_, asyncRes) => {
|
||||||
|
const activeConnection = AstalNetwork.get_default().get_client().activate_connection_finish(asyncRes);
|
||||||
|
if(!activeConnection) {
|
||||||
|
Notifications.getDefault().sendNotification({
|
||||||
|
appName: "network",
|
||||||
|
summary: "Couldn't activate wireless connection",
|
||||||
|
body: `An error occurred while activating the wireless connection "${ssid}"`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyConnectionError(ssid: string): void {
|
||||||
|
Notifications.getDefault().sendNotification({
|
||||||
|
appName: "network",
|
||||||
|
summary: "Coudn't connect Wi-Fi",
|
||||||
|
body: `An error occurred while trying to connect to the "${ssid}" access point. \nMaybe the password is invalid?`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function saveToDisk(remoteConnection: NM.RemoteConnection, ssid: string): void {
|
||||||
|
AskPopup({
|
||||||
|
text: `Save password for connection "${ssid}"?`,
|
||||||
|
acceptText: "Yes",
|
||||||
|
onAccept: () => remoteConnection.commit_changes_async(true, null, (_, asyncRes) =>
|
||||||
|
!remoteConnection.commit_changes_finish(asyncRes) && Notifications.getDefault().sendNotification({
|
||||||
|
appName: "network",
|
||||||
|
summary: "Couldn't save Wi-Fi password",
|
||||||
|
body: `An error occurred while trying to write the password for "${ssid}" to disk`
|
||||||
|
}))
|
||||||
|
} as AskPopupProps);
|
||||||
|
}
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { Widget } from "astal/gtk3";
|
|
||||||
import { Page, PageProps } from "./Page";
|
|
||||||
import { bind } from "astal";
|
|
||||||
import { NightLight } from "../../../scripts/nightlight";
|
|
||||||
import { addSliderMarksFromMinMax } from "../../../scripts/widget-utils";
|
|
||||||
import { tr } from "../../../i18n/intl";
|
|
||||||
|
|
||||||
export const PageNightLight: (() => Page) = () => new Page({
|
|
||||||
id: "night-light",
|
|
||||||
title: tr("control_center.pages.night_light.title"),
|
|
||||||
description: tr("control_center.pages.night_light.description"),
|
|
||||||
className: "night-light",
|
|
||||||
children: [
|
|
||||||
new Widget.Label({
|
|
||||||
className: "sub-header",
|
|
||||||
label: tr("control_center.pages.night_light.temperature"),
|
|
||||||
xalign: 0
|
|
||||||
} as Widget.LabelProps),
|
|
||||||
new Widget.Slider({
|
|
||||||
className: "temperature",
|
|
||||||
setup: (slider) => {
|
|
||||||
slider.value = NightLight.getDefault().temperature;
|
|
||||||
addSliderMarksFromMinMax(slider, 5, "{}K");
|
|
||||||
},
|
|
||||||
value: bind(NightLight.getDefault(), "temperature"),
|
|
||||||
tooltipText: bind(NightLight.getDefault(), "temperature").as((temp) => `${temp}K`),
|
|
||||||
min: 1000,
|
|
||||||
max: NightLight.getDefault().maxTemperature,
|
|
||||||
onDragged: (slider) =>
|
|
||||||
NightLight.getDefault().temperature = (Math.floor(slider.value)),
|
|
||||||
} as Widget.SliderProps),
|
|
||||||
new Widget.Label({
|
|
||||||
className: "sub-header",
|
|
||||||
label: tr("control_center.pages.night_light.gamma"),
|
|
||||||
css: "margin-top: 6px;",
|
|
||||||
xalign: 0
|
|
||||||
} as Widget.LabelProps),
|
|
||||||
new Widget.Slider({
|
|
||||||
className: "gamma",
|
|
||||||
setup: (slider) => {
|
|
||||||
slider.value = NightLight.getDefault().gamma;
|
|
||||||
addSliderMarksFromMinMax(slider, 5, "{}%");
|
|
||||||
},
|
|
||||||
value: bind(NightLight.getDefault(), "gamma"),
|
|
||||||
max: NightLight.getDefault().maxGamma,
|
|
||||||
tooltipText: bind(NightLight.getDefault(), "gamma").as((gamma) => `${gamma}%`),
|
|
||||||
onDragged: (slider) =>
|
|
||||||
NightLight.getDefault().gamma = (Math.floor(slider.value)),
|
|
||||||
} as Widget.SliderProps)
|
|
||||||
]
|
|
||||||
} as PageProps);
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { Page } from "./Page";
|
||||||
|
import { NightLight } from "../../../scripts/nightlight";
|
||||||
|
import { tr } from "../../../i18n/intl";
|
||||||
|
import { Astal, Gtk } from "ags/gtk4";
|
||||||
|
import { addSliderMarksFromMinMax } from "../../../scripts/utils";
|
||||||
|
import { createBinding } from "ags";
|
||||||
|
|
||||||
|
export const PageNightLight: (() => Page) = () =>
|
||||||
|
<Page id={"night-light"} title={tr("control_center.pages.night_light.title")}
|
||||||
|
description={tr("control_center.pages.night_light.description")}
|
||||||
|
class={"night-light"}>
|
||||||
|
|
||||||
|
<Gtk.Label class={"sub-header"} label={tr(
|
||||||
|
"control_center.pages.night_light.temperature"
|
||||||
|
)} xalign={0} />
|
||||||
|
<Astal.Slider class={"temperature"} $={(self) => {
|
||||||
|
self.value = NightLight.getDefault().temperature;
|
||||||
|
addSliderMarksFromMinMax(self, 5, "{}K");
|
||||||
|
}} value={createBinding(NightLight.getDefault(), "temperature")}
|
||||||
|
tooltipText={createBinding(NightLight.getDefault(), "temperature").as(temp =>
|
||||||
|
`${temp}K`)} min={NightLight.getDefault().minTemperature}
|
||||||
|
max={NightLight.getDefault().maxTemperature}
|
||||||
|
onChangeValue={(_, type, value) => {
|
||||||
|
if(type != undefined && type !== null)
|
||||||
|
NightLight.getDefault().temperature = Math.floor(value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Gtk.Label class={"sub-header"} label={tr(
|
||||||
|
"control_center.pages.night_light.gamma"
|
||||||
|
)} xalign={0} />
|
||||||
|
<Astal.Slider class={"gamma"} $={(self) => {
|
||||||
|
self.value = NightLight.getDefault().gamma;
|
||||||
|
addSliderMarksFromMinMax(self, 5, "{}%");
|
||||||
|
}} value={createBinding(NightLight.getDefault(), "gamma")}
|
||||||
|
tooltipText={createBinding(NightLight.getDefault(), "gamma").as(gamma =>
|
||||||
|
`${gamma}%`)} max={NightLight.getDefault().maxGamma}
|
||||||
|
onChangeValue={(_, type, value) => {
|
||||||
|
if(type != undefined && type !== null)
|
||||||
|
NightLight.getDefault().gamma = Math.floor(value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Page> as Page;
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
import { Binding, register } from "astal";
|
|
||||||
import { Gtk, Widget } from "astal/gtk3";
|
|
||||||
import { Separator, SeparatorProps } from "../../Separator";
|
|
||||||
|
|
||||||
export type PageProps = {
|
|
||||||
setup?: () => void;
|
|
||||||
onClose?: () => void;
|
|
||||||
id: string;
|
|
||||||
className?: string | Binding<string>;
|
|
||||||
title: string | Binding<string>;
|
|
||||||
description?: string | Binding<string>;
|
|
||||||
headerButtons?: Array<Gtk.Button> | Binding<Array<Gtk.Button>>;
|
|
||||||
bottomButtons?: Array<BottomButton> | Binding<Array<BottomButton>>;
|
|
||||||
orientation?: Gtk.Orientation | Binding<Gtk.Orientation>;
|
|
||||||
spacing?: number;
|
|
||||||
child?: Gtk.Widget | Binding<Gtk.Widget>;
|
|
||||||
children?: Array<Gtk.Widget> | Binding<Array<Gtk.Widget>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BottomButton = {
|
|
||||||
title: string | Binding<string>;
|
|
||||||
description?: string | Binding<string>;
|
|
||||||
tooltipText?: string | Binding<string>;
|
|
||||||
tooltipMarkup?: string | Binding<string>;
|
|
||||||
onClick?: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export { Page };
|
|
||||||
|
|
||||||
@register({ GTypeName: "Page" })
|
|
||||||
class Page extends Widget.Box {
|
|
||||||
readonly #id: string | number;
|
|
||||||
readonly bottomButtons?: Array<BottomButton>;
|
|
||||||
|
|
||||||
#title: string | Binding<string>;
|
|
||||||
#description?: string | Binding<string>;
|
|
||||||
|
|
||||||
public get title() { return this.#title; }
|
|
||||||
public get description() { return this.#description; }
|
|
||||||
public get id() { return this.#id; }
|
|
||||||
public onClose?: () => void;
|
|
||||||
|
|
||||||
constructor(props: PageProps) {
|
|
||||||
super({
|
|
||||||
hexpand: true,
|
|
||||||
orientation: Gtk.Orientation.VERTICAL,
|
|
||||||
className: (props.className instanceof Binding) ?
|
|
||||||
props.className.as((clsName) => `page ${ clsName ?? "" }`)
|
|
||||||
: `page ${props.className ?? ""}`,
|
|
||||||
setup: props.setup,
|
|
||||||
children: [
|
|
||||||
new Widget.Box({
|
|
||||||
className: "header",
|
|
||||||
orientation: Gtk.Orientation.VERTICAL,
|
|
||||||
hexpand: true,
|
|
||||||
children: [
|
|
||||||
new Widget.Box({
|
|
||||||
className: "top",
|
|
||||||
children: [
|
|
||||||
new Widget.Label({
|
|
||||||
hexpand: true,
|
|
||||||
className: "title",
|
|
||||||
truncate: true,
|
|
||||||
visible: (props.title instanceof Binding) ?
|
|
||||||
props.title.as(Boolean)
|
|
||||||
: (props.title ? true : false),
|
|
||||||
label: props.title,
|
|
||||||
halign: Gtk.Align.START
|
|
||||||
} as Widget.LabelProps),
|
|
||||||
new Widget.Box({
|
|
||||||
className: "button-row",
|
|
||||||
visible: (props.headerButtons instanceof Binding) ?
|
|
||||||
props.headerButtons.as(Boolean)
|
|
||||||
: (props.headerButtons ? true : false),
|
|
||||||
children: props.headerButtons
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps),
|
|
||||||
new Widget.Label({
|
|
||||||
className: "description",
|
|
||||||
hexpand: true,
|
|
||||||
truncate: true,
|
|
||||||
xalign: 0,
|
|
||||||
visible: (props.description instanceof Binding) ?
|
|
||||||
props.description.as(Boolean)
|
|
||||||
: props.description ? true : false,
|
|
||||||
label: props.description
|
|
||||||
} as Widget.LabelProps),
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps),
|
|
||||||
new Widget.Box({
|
|
||||||
className: "content",
|
|
||||||
spacing: props.spacing ?? 4,
|
|
||||||
orientation: props.orientation ?? Gtk.Orientation.VERTICAL,
|
|
||||||
expand: true,
|
|
||||||
setup: props.setup,
|
|
||||||
child: props.child,
|
|
||||||
children: props.children
|
|
||||||
} as Widget.BoxProps),
|
|
||||||
Separator({
|
|
||||||
alpha: .2,
|
|
||||||
spacing: 6,
|
|
||||||
orientation: Gtk.Orientation.VERTICAL,
|
|
||||||
visible: (props.bottomButtons instanceof Binding) ?
|
|
||||||
props.bottomButtons.as(buttons => buttons.length > 0)
|
|
||||||
: (!props.bottomButtons ? false : props.bottomButtons.length > 0)
|
|
||||||
} as SeparatorProps),
|
|
||||||
new Widget.Box({
|
|
||||||
className: "bottom-buttons",
|
|
||||||
orientation: Gtk.Orientation.VERTICAL,
|
|
||||||
visible: (props.bottomButtons instanceof Binding) ?
|
|
||||||
props.bottomButtons.as(buttons => buttons.length > 0)
|
|
||||||
: (!props.bottomButtons ? false : props.bottomButtons.length > 0),
|
|
||||||
spacing: 2,
|
|
||||||
children: (props.bottomButtons instanceof Binding) ?
|
|
||||||
props.bottomButtons.as(buttons => buttons.map(button =>
|
|
||||||
new Widget.Button({
|
|
||||||
onClicked: button.onClick,
|
|
||||||
tooltipMarkup: button.tooltipMarkup,
|
|
||||||
tooltipText: button.tooltipText,
|
|
||||||
child: new Widget.Box({
|
|
||||||
orientation: Gtk.Orientation.VERTICAL,
|
|
||||||
children: [
|
|
||||||
new Widget.Label({
|
|
||||||
className: "title",
|
|
||||||
label: button.title,
|
|
||||||
xalign: 0
|
|
||||||
} as Widget.LabelProps),
|
|
||||||
new Widget.Label({
|
|
||||||
className: "description",
|
|
||||||
label: button.description,
|
|
||||||
visible: Boolean(button.description),
|
|
||||||
xalign: 0
|
|
||||||
} as Widget.LabelProps)
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
} as Widget.ButtonProps)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
: (!props.bottomButtons ? [] : props.bottomButtons.map(button =>
|
|
||||||
new Widget.Button({
|
|
||||||
onClicked: button.onClick,
|
|
||||||
tooltipMarkup: button.tooltipMarkup,
|
|
||||||
tooltipText: button.tooltipText,
|
|
||||||
child: new Widget.Box({
|
|
||||||
orientation: Gtk.Orientation.VERTICAL,
|
|
||||||
children: [
|
|
||||||
new Widget.Label({
|
|
||||||
className: "title",
|
|
||||||
label: button.title,
|
|
||||||
xalign: 0
|
|
||||||
} as Widget.LabelProps),
|
|
||||||
new Widget.Label({
|
|
||||||
className: "description",
|
|
||||||
label: button.description,
|
|
||||||
visible: Boolean(button.description),
|
|
||||||
xalign: 0
|
|
||||||
} as Widget.LabelProps)
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
} as Widget.ButtonProps)
|
|
||||||
))
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
this.#id = props.id;
|
|
||||||
this.#title = props.title;
|
|
||||||
this.#description = props.description;
|
|
||||||
|
|
||||||
this.onClose = props.onClose;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PageButton({ onDestroy, ...props }: {
|
|
||||||
className?: string | Binding<string>;
|
|
||||||
icon?: string | Binding<string>;
|
|
||||||
title: string | Binding<string>;
|
|
||||||
endWidget?: Gtk.Widget | Binding<Gtk.Widget>;
|
|
||||||
description?: string | Binding<string>;
|
|
||||||
extraButtons?: Array<Widget.Button> | Binding<Array<Gtk.Widget>>;
|
|
||||||
onDestroy?: (self: Widget.Box) => void;
|
|
||||||
onClick?: (self: Widget.Button) => void;
|
|
||||||
tooltipText?: string | Binding<string>;
|
|
||||||
tooltipMarkup?: string | Binding<string>;
|
|
||||||
}): Gtk.Widget {
|
|
||||||
return new Widget.Box({
|
|
||||||
onDestroy,
|
|
||||||
children: [
|
|
||||||
new Widget.Button({
|
|
||||||
onClick: props.onClick,
|
|
||||||
className: props.className,
|
|
||||||
hexpand: true,
|
|
||||||
tooltipText: props.tooltipText,
|
|
||||||
tooltipMarkup: props.tooltipMarkup,
|
|
||||||
child: new Widget.Box({
|
|
||||||
className: "page-button",
|
|
||||||
orientation: Gtk.Orientation.HORIZONTAL,
|
|
||||||
expand: true,
|
|
||||||
children: [
|
|
||||||
new Widget.Icon({
|
|
||||||
className: "icon",
|
|
||||||
icon: props.icon,
|
|
||||||
visible: props.icon,
|
|
||||||
hexpand: false,
|
|
||||||
css: "font-size: 20px; margin-right: 6px;"
|
|
||||||
} as Widget.IconProps),
|
|
||||||
new Widget.Box({
|
|
||||||
orientation: Gtk.Orientation.VERTICAL,
|
|
||||||
hexpand: true,
|
|
||||||
vexpand: false,
|
|
||||||
children: [
|
|
||||||
new Widget.Label({
|
|
||||||
className: "title",
|
|
||||||
xalign: 0,
|
|
||||||
// truncating is not working, so I had to do this
|
|
||||||
label: (props.title instanceof Binding) ?
|
|
||||||
props.title.as((title) =>
|
|
||||||
`${title.substring(0, 35)}${
|
|
||||||
title.length > 35 ? '…' : ""}`)
|
|
||||||
: `${props.title.substring(0, 35)}${
|
|
||||||
props.title.length > 35 ? '…' : ""}`,
|
|
||||||
tooltipText: props.title,
|
|
||||||
truncate: true,
|
|
||||||
} as Widget.LabelProps),
|
|
||||||
new Widget.Label({
|
|
||||||
className: "description",
|
|
||||||
xalign: 0,
|
|
||||||
visible: (props.description instanceof Binding) ?
|
|
||||||
props.description.as(Boolean)
|
|
||||||
: Boolean(props.description),
|
|
||||||
label: props.description,
|
|
||||||
truncate: true,
|
|
||||||
tooltipText: props.description
|
|
||||||
} as Widget.LabelProps)
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps),
|
|
||||||
new Widget.Box({
|
|
||||||
visible: (props.endWidget instanceof Binding) ?
|
|
||||||
props.endWidget.as(Boolean)
|
|
||||||
: props.endWidget,
|
|
||||||
halign: Gtk.Align.END,
|
|
||||||
child: props.endWidget
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
} as Widget.ButtonProps),
|
|
||||||
new Widget.Box({
|
|
||||||
className: "extra-buttons button-row",
|
|
||||||
visible: (props.extraButtons instanceof Binding) ?
|
|
||||||
props.extraButtons.as(extra => extra.length > 0)
|
|
||||||
: (props.extraButtons ? props.extraButtons.length > 0 : false),
|
|
||||||
children: props.extraButtons
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import { register } from "ags/gobject";
|
||||||
|
import { Gtk } from "ags/gtk4";
|
||||||
|
import { Separator } from "../../Separator";
|
||||||
|
import { Accessor, For } from "ags";
|
||||||
|
import { transform, transformWidget, variableToBoolean, WidgetNodeType } from "../../../scripts/utils";
|
||||||
|
import Pango from "gi://Pango?version=1.0";
|
||||||
|
|
||||||
|
export type PageProps = {
|
||||||
|
$?: () => void;
|
||||||
|
onClose?: () => void;
|
||||||
|
id: string;
|
||||||
|
class?: string | Accessor<string>;
|
||||||
|
title: string | Accessor<string>;
|
||||||
|
description?: string | Accessor<string>;
|
||||||
|
headerButtons?: Array<JSX.Element> | Accessor<Array<JSX.Element>>;
|
||||||
|
bottomButtons?: Array<BottomButton> | Accessor<Array<BottomButton>>;
|
||||||
|
orientation?: Gtk.Orientation | Accessor<Gtk.Orientation>;
|
||||||
|
spacing?: number;
|
||||||
|
children?: WidgetNodeType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BottomButton = {
|
||||||
|
title: string | Accessor<string>;
|
||||||
|
description?: string | Accessor<string>;
|
||||||
|
tooltipText?: string | Accessor<string>;
|
||||||
|
tooltipMarkup?: string | Accessor<string>;
|
||||||
|
onClick?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Page };
|
||||||
|
|
||||||
|
@register({ GTypeName: "Page" })
|
||||||
|
class Page extends Gtk.Box {
|
||||||
|
readonly #id: string | number;
|
||||||
|
readonly bottomButtons?: Array<BottomButton>;
|
||||||
|
|
||||||
|
#subs: Array<() => void> = [];
|
||||||
|
#title: string | Accessor<string>;
|
||||||
|
#description?: string | Accessor<string>;
|
||||||
|
|
||||||
|
public get title() { return this.#title; }
|
||||||
|
public get description() { return this.#description; }
|
||||||
|
public get id() { return this.#id; }
|
||||||
|
public onClose?: () => void;
|
||||||
|
|
||||||
|
constructor(props: PageProps) {
|
||||||
|
super({
|
||||||
|
hexpand: true,
|
||||||
|
orientation: Gtk.Orientation.VERTICAL
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#id = props.id;
|
||||||
|
this.#title = props.title;
|
||||||
|
this.#description = props.description;
|
||||||
|
|
||||||
|
if(props.class instanceof Accessor) {
|
||||||
|
this.#subs.push(props.class.subscribe(() => {
|
||||||
|
const clss = (props.class as Accessor<string>).get();
|
||||||
|
|
||||||
|
this.cssClasses = ["page", ...clss.split(' ').filter(s => s !== "")];
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
if(props.class)
|
||||||
|
this.cssClasses = ["page",
|
||||||
|
...(props.class as string).split(' ').filter(s => s)];
|
||||||
|
else
|
||||||
|
this.add_css_class("page");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.prepend(<Gtk.Box class={"header"} orientation={Gtk.Orientation.VERTICAL}
|
||||||
|
hexpand={true}>
|
||||||
|
|
||||||
|
<Gtk.Box class={"top"}>
|
||||||
|
<Gtk.Label hexpand={true} class={"title"} ellipsize={Pango.EllipsizeMode.END}
|
||||||
|
visible={variableToBoolean(props.title)} label={props.title}
|
||||||
|
halign={Gtk.Align.START} />
|
||||||
|
|
||||||
|
{props.headerButtons && <Gtk.Box class={"button-row"} visible={variableToBoolean(props.headerButtons)}>
|
||||||
|
{
|
||||||
|
(props.headerButtons instanceof Accessor) ?
|
||||||
|
<For each={props.headerButtons}>
|
||||||
|
{(button) => button}
|
||||||
|
</For>
|
||||||
|
: props.headerButtons
|
||||||
|
}
|
||||||
|
</Gtk.Box>}
|
||||||
|
|
||||||
|
<Gtk.Label class={"description"} hexpand={true} ellipsize={Pango.EllipsizeMode.END}
|
||||||
|
xalign={0} visible={variableToBoolean(props.description)} label={props.description} />
|
||||||
|
|
||||||
|
</Gtk.Box>
|
||||||
|
</Gtk.Box> as Gtk.Box);
|
||||||
|
|
||||||
|
this.append(<Gtk.Box class={"content"} spacing={props.spacing ?? 4} hexpand={true} vexpand={true}
|
||||||
|
orientation={props.orientation ?? Gtk.Orientation.VERTICAL}>
|
||||||
|
|
||||||
|
{props.children}
|
||||||
|
</Gtk.Box> as Gtk.Box);
|
||||||
|
|
||||||
|
this.append(<Separator alpha={.2} spacing={6} orientation={Gtk.Orientation.VERTICAL}
|
||||||
|
visible={(props.bottomButtons instanceof Accessor) ?
|
||||||
|
props.bottomButtons.as(buttons => buttons.length > 0)
|
||||||
|
: (!props.bottomButtons ? false : props.bottomButtons.length > 0)}
|
||||||
|
/> as Gtk.Widget);
|
||||||
|
|
||||||
|
this.append(<Gtk.Box class={"bottom-buttons"} orientation={Gtk.Orientation.VERTICAL}
|
||||||
|
visible={variableToBoolean(props.bottomButtons)} spacing={2}>
|
||||||
|
|
||||||
|
{transformWidget(props.bottomButtons, (button) =>
|
||||||
|
<Gtk.Button onClicked={button?.onClick} tooltipText={button?.tooltipText}
|
||||||
|
tooltipMarkup={button?.tooltipMarkup}>
|
||||||
|
|
||||||
|
<Gtk.Label class={"title"} label={button?.title} xalign={0} />
|
||||||
|
<Gtk.Label class={"description"} label={button?.description}
|
||||||
|
xalign={0} visible={variableToBoolean(button?.description)} />
|
||||||
|
</Gtk.Button>
|
||||||
|
)}
|
||||||
|
</Gtk.Box> as Gtk.Box);
|
||||||
|
|
||||||
|
this.onClose = props.onClose;
|
||||||
|
props.$?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function BottomButton(props: BottomButton) {
|
||||||
|
return <Gtk.Button onClicked={props.onClick} tooltipMarkup={props.tooltipMarkup}
|
||||||
|
tooltipText={props.tooltipText}>
|
||||||
|
|
||||||
|
<Gtk.Box orientation={Gtk.Orientation.VERTICAL}>
|
||||||
|
<Gtk.Label class={"title"} label={props.title} xalign={0} />
|
||||||
|
<Gtk.Label class={"description"} label={props.description}
|
||||||
|
visible={Boolean(props.description)} xalign={0} />
|
||||||
|
</Gtk.Box>
|
||||||
|
</Gtk.Button> as Gtk.Button;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PageButton({ onDestroy, ...props }: {
|
||||||
|
class?: string | Accessor<string>;
|
||||||
|
icon?: string | Accessor<string>;
|
||||||
|
title: string | Accessor<string>;
|
||||||
|
endWidget?: WidgetNodeType;
|
||||||
|
description?: string | Accessor<string>;
|
||||||
|
extraButtons?: Array<WidgetNodeType> | WidgetNodeType;
|
||||||
|
onDestroy?: (self: Gtk.Box) => void;
|
||||||
|
onClick?: (self: Gtk.Button) => void;
|
||||||
|
tooltipText?: string | Accessor<string>;
|
||||||
|
tooltipMarkup?: string | Accessor<string>;
|
||||||
|
}) {
|
||||||
|
return <Gtk.Box onDestroy={onDestroy}>
|
||||||
|
<Gtk.Button onClicked={props.onClick} class={props.class} hexpand={true}
|
||||||
|
tooltipText={props.tooltipText} tooltipMarkup={props.tooltipMarkup}>
|
||||||
|
|
||||||
|
<Gtk.Box class={"page-button"} hexpand={true} vexpand={true}>
|
||||||
|
{props.icon && <Gtk.Image iconName={props.icon} visible={variableToBoolean(props.icon)}
|
||||||
|
css={"font-size: 20px; margin-right: 6px;"} />}
|
||||||
|
|
||||||
|
<Gtk.Box orientation={Gtk.Orientation.VERTICAL} hexpand={true} vexpand={false}>
|
||||||
|
<Gtk.Label class={"title"} xalign={0} tooltipText={props.title}
|
||||||
|
ellipsize={Pango.EllipsizeMode.END} label={
|
||||||
|
transform(props.title, (title) =>
|
||||||
|
`${title.substring(0, 35)}${title.length > 35 ? '…' : ""}`)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Gtk.Label class={"description"} xalign={0} visible={variableToBoolean(props.description)}
|
||||||
|
label={props.description} ellipsize={Pango.EllipsizeMode.END}
|
||||||
|
tooltipText={props.description} />
|
||||||
|
</Gtk.Box>
|
||||||
|
|
||||||
|
<Gtk.Box visible={variableToBoolean(props.endWidget)} halign={Gtk.Align.END}>
|
||||||
|
{props.endWidget && props.endWidget}
|
||||||
|
</Gtk.Box>
|
||||||
|
</Gtk.Box>
|
||||||
|
</Gtk.Button>
|
||||||
|
|
||||||
|
<Gtk.Box class={"extra-buttons button-row"} visible={variableToBoolean(props.extraButtons)}>
|
||||||
|
{props.extraButtons}
|
||||||
|
</Gtk.Box>
|
||||||
|
</Gtk.Box> as Gtk.Box;
|
||||||
|
}
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import { Page, PageButton, PageProps } from "./Page";
|
|
||||||
import { bind, Variable } from "astal";
|
|
||||||
import { Astal, Gtk, Widget } from "astal/gtk3";
|
|
||||||
import { getAppIcon } from "../../../scripts/apps";
|
|
||||||
import { Wireplumber } from "../../../scripts/volume";
|
|
||||||
import { tr } from "../../../i18n/intl";
|
|
||||||
|
|
||||||
export function PageSound(): Page {
|
|
||||||
const endpoints = Variable.derive([
|
|
||||||
bind(Wireplumber.getWireplumber().get_audio()!, "speakers"),
|
|
||||||
bind(Wireplumber.getWireplumber().get_audio()!, "streams")
|
|
||||||
]);
|
|
||||||
|
|
||||||
return new Page({
|
|
||||||
id: "sound",
|
|
||||||
title: tr("control_center.pages.sound.title"),
|
|
||||||
description: tr("control_center.pages.sound.description"),
|
|
||||||
onClose: endpoints.drop,
|
|
||||||
children: endpoints(([speakers, streams]) => [
|
|
||||||
new Widget.Label({
|
|
||||||
className: "sub-header",
|
|
||||||
label: tr("devices"),
|
|
||||||
xalign: 0
|
|
||||||
} as Widget.LabelProps),
|
|
||||||
...speakers.map((speaker) =>
|
|
||||||
PageButton({
|
|
||||||
className: bind(speaker, "isDefault").as(isDefault => isDefault ? "default" : ""),
|
|
||||||
icon: bind(speaker, "icon").as(icon =>
|
|
||||||
Astal.Icon.lookup_icon(icon)? icon : "audio-card-symbolic"),
|
|
||||||
title: bind(speaker, "description").as(desc => desc ?? "Speaker"),
|
|
||||||
onClick: () => speaker.set_is_default(true),
|
|
||||||
endWidget: new Widget.Icon({
|
|
||||||
icon: "object-select-symbolic",
|
|
||||||
visible: bind(speaker, "isDefault"),
|
|
||||||
css: "font-size: 18px;"
|
|
||||||
} as Widget.IconProps)
|
|
||||||
})
|
|
||||||
),
|
|
||||||
new Widget.Label({
|
|
||||||
className: "sub-header",
|
|
||||||
label: tr("apps"),
|
|
||||||
visible: streams.length > 0,
|
|
||||||
xalign: 0
|
|
||||||
} as Widget.LabelProps),
|
|
||||||
...streams.map((stream) =>
|
|
||||||
new Widget.EventBox({
|
|
||||||
hexpand: true,
|
|
||||||
setup: (eventbox) => {
|
|
||||||
const connections: Array<number> = [];
|
|
||||||
|
|
||||||
eventbox.add(new Widget.Box({
|
|
||||||
orientation: Gtk.Orientation.HORIZONTAL,
|
|
||||||
children: [
|
|
||||||
new Widget.Icon({
|
|
||||||
icon: bind(stream, "name").as(name =>
|
|
||||||
getAppIcon(name.split(' ')[0]) ?? "application-x-executable-symbolic"),
|
|
||||||
css: "font-size: 18px; margin-right: 6px;"
|
|
||||||
} as Widget.IconProps),
|
|
||||||
new Widget.Box({
|
|
||||||
orientation: Gtk.Orientation.VERTICAL,
|
|
||||||
hexpand: true,
|
|
||||||
children: [
|
|
||||||
new Widget.Revealer({
|
|
||||||
transitionDuration: 180,
|
|
||||||
transitionType: Gtk.RevealerTransitionType.SLIDE_DOWN,
|
|
||||||
setup: (self) => connections.push(
|
|
||||||
eventbox.connect("hover", () => self.revealChild = true),
|
|
||||||
eventbox.connect("hover-lost", () => self.revealChild = false)
|
|
||||||
),
|
|
||||||
onDestroy: () => connections.map(id => eventbox.disconnect(id)),
|
|
||||||
child: new Widget.Label({
|
|
||||||
label: bind(stream, "name").as(name => name || "Unknown"),
|
|
||||||
truncate: true,
|
|
||||||
tooltipText: bind(stream, "name"),
|
|
||||||
className: "name",
|
|
||||||
xalign: 0
|
|
||||||
} as Widget.LabelProps)
|
|
||||||
} as Widget.RevealerProps),
|
|
||||||
new Widget.Slider({
|
|
||||||
min: 0,
|
|
||||||
drawValue: false,
|
|
||||||
max: 100,
|
|
||||||
setup: (self) => self.value = Math.floor(stream.volume * 100),
|
|
||||||
value: bind(stream, "volume").as((vol) => Math.floor(vol * 100)),
|
|
||||||
onDragged: (self) => stream.volume = self.value / 100
|
|
||||||
} as Widget.SliderProps)
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps))
|
|
||||||
}
|
|
||||||
} as Widget.EventBoxProps)
|
|
||||||
)
|
|
||||||
])
|
|
||||||
} as PageProps);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { Page, PageButton } from "./Page";
|
||||||
|
import { Astal, Gtk } from "ags/gtk4";
|
||||||
|
import { getAppIcon, lookupIcon } from "../../../scripts/apps";
|
||||||
|
import { Wireplumber } from "../../../scripts/volume";
|
||||||
|
import { tr } from "../../../i18n/intl";
|
||||||
|
import { createBinding, For } from "ags";
|
||||||
|
import AstalWp from "gi://AstalWp";
|
||||||
|
import { variableToBoolean } from "../../../scripts/utils";
|
||||||
|
import GObject from "gi://GObject?version=2.0";
|
||||||
|
import Pango from "gi://Pango?version=1.0";
|
||||||
|
|
||||||
|
export const PageSound = () =>
|
||||||
|
<Page id={"sound"} title={tr("control_center.pages.sound.title")}
|
||||||
|
description={tr("control_center.pages.sound.description")}>
|
||||||
|
|
||||||
|
<Gtk.Label class={"sub-header"} label={tr("devices")} xalign={0} />
|
||||||
|
<For each={createBinding(Wireplumber.getWireplumber().audio!, "speakers")}>
|
||||||
|
{(sink: AstalWp.Endpoint) =>
|
||||||
|
<PageButton class={createBinding(sink, "isDefault").as(isDefault =>
|
||||||
|
isDefault ? "default" : "")}
|
||||||
|
icon={createBinding(sink, "icon").as(ico =>
|
||||||
|
lookupIcon(ico) ? ico : "audio-card-symbolic")}
|
||||||
|
title={createBinding(sink, "description").as(desc =>
|
||||||
|
desc ?? "Speaker")}
|
||||||
|
onClick={() => !sink.isDefault && sink.set_is_default(true)}
|
||||||
|
endWidget={
|
||||||
|
<Gtk.Image iconName={"object-select-symbolic"}
|
||||||
|
visible={createBinding(sink, "isDefault")}
|
||||||
|
css={"font-size: 18px;"}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>}
|
||||||
|
</For>
|
||||||
|
|
||||||
|
<Gtk.Label class={"sub-header"} label={tr("apps")} xalign={0}
|
||||||
|
visible={variableToBoolean(
|
||||||
|
createBinding(Wireplumber.getWireplumber().audio!, "streams")
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<For each={createBinding(Wireplumber.getWireplumber().audio!, "streams")}>
|
||||||
|
{(stream: AstalWp.Stream) =>
|
||||||
|
<Gtk.Box hexpand={true} $={(self) => {
|
||||||
|
const conns: Map<GObject.Object, Array<number>> = new Map();
|
||||||
|
const controllerMotion = Gtk.EventControllerMotion.new();
|
||||||
|
|
||||||
|
self.add_controller(controllerMotion);
|
||||||
|
|
||||||
|
conns.set(controllerMotion, [
|
||||||
|
controllerMotion.connect("enter", () => {
|
||||||
|
const revealer = self.get_last_child()!.get_first_child() as Gtk.Revealer;
|
||||||
|
revealer.set_reveal_child(true);
|
||||||
|
}),
|
||||||
|
controllerMotion.connect("leave", () => {
|
||||||
|
const revealer = self.get_first_child()!.get_first_child() as Gtk.Revealer;
|
||||||
|
revealer.set_reveal_child(true);
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
conns.set(self, [
|
||||||
|
self.connect("destroy", () => conns.forEach((ids, obj) =>
|
||||||
|
ids.forEach(id => obj.disconnect(id))
|
||||||
|
))
|
||||||
|
]);
|
||||||
|
}}>
|
||||||
|
|
||||||
|
<Gtk.Image iconName={createBinding(stream, "name").as(name =>
|
||||||
|
getAppIcon(name.split(' ')[0]) ?? "application-x-executable-symbolic")}
|
||||||
|
css={"font-size: 18px; margin-right: 6px;"} />
|
||||||
|
|
||||||
|
<Gtk.Box orientation={Gtk.Orientation.VERTICAL} hexpand={true}>
|
||||||
|
<Gtk.Revealer transitionDuration={180}
|
||||||
|
transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}>
|
||||||
|
|
||||||
|
<Gtk.Label label={createBinding(stream, "name").as(name =>
|
||||||
|
name ?? "Unnamed audio stream")}
|
||||||
|
ellipsize={Pango.EllipsizeMode.END}
|
||||||
|
tooltipText={createBinding(stream, "name")}
|
||||||
|
class={"name"} xalign={0}
|
||||||
|
/>
|
||||||
|
</Gtk.Revealer>
|
||||||
|
|
||||||
|
<Astal.Slider drawValue={false} max={100} $={(self) => {
|
||||||
|
self.value = Math.floor(stream.volume * 100);
|
||||||
|
}} value={createBinding(stream, "volume").as(vol =>
|
||||||
|
Math.floor(vol * 100))}
|
||||||
|
onChangeValue={(_, type, value) => {
|
||||||
|
if(type !== undefined && type !== null)
|
||||||
|
stream.volume = Math.floor(value / 100);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Gtk.Box>
|
||||||
|
</Gtk.Box>
|
||||||
|
}
|
||||||
|
</For>
|
||||||
|
</Page> as Page;
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { bind, Variable } from "astal";
|
|
||||||
import { Tile, TileProps } from "./Tile";
|
|
||||||
import AstalBluetooth from "gi://AstalBluetooth";
|
|
||||||
import { BluetoothPage } from "../pages/Bluetooth";
|
|
||||||
import { TilesPages } from "../Tiles";
|
|
||||||
|
|
||||||
|
|
||||||
export const TileBluetooth = () => {
|
|
||||||
const icon: Variable<string> = Variable.derive([
|
|
||||||
bind(AstalBluetooth.get_default(), "isPowered"),
|
|
||||||
bind(AstalBluetooth.get_default(), "isConnected")
|
|
||||||
],
|
|
||||||
(powered: boolean, isConnected: boolean) =>
|
|
||||||
powered ? ( isConnected ?
|
|
||||||
"bluetooth-active-symbolic"
|
|
||||||
: "bluetooth-symbolic"
|
|
||||||
) : "bluetooth-disabled-symbolic"
|
|
||||||
);
|
|
||||||
return Tile({
|
|
||||||
title: "Bluetooth",
|
|
||||||
visible: bind(AstalBluetooth.get_default(), "adapter").as(Boolean),
|
|
||||||
description: bind(AstalBluetooth.get_default(), "isConnected").as((connected) => {
|
|
||||||
const connectedDev = AstalBluetooth.get_default().devices.filter(dev => dev.connected)?.[0];
|
|
||||||
return connected && connectedDev ? connectedDev.get_alias() : ""
|
|
||||||
}),
|
|
||||||
onDestroy: () => icon.drop(),
|
|
||||||
onToggledOn: () => AstalBluetooth.get_default().adapter?.set_powered(true),
|
|
||||||
onToggledOff: () => AstalBluetooth.get_default().adapter?.set_powered(false),
|
|
||||||
onClickMore: () => TilesPages?.toggle(BluetoothPage()),
|
|
||||||
enableOnClickMore: true,
|
|
||||||
icon: icon(),
|
|
||||||
iconSize: 16,
|
|
||||||
toggleState: bind(AstalBluetooth.get_default(), "isPowered")
|
|
||||||
} as TileProps)();
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Tile } from "./Tile";
|
||||||
|
import AstalBluetooth from "gi://AstalBluetooth";
|
||||||
|
import { BluetoothPage } from "../pages/Bluetooth";
|
||||||
|
import { TilesPages } from "../Tiles";
|
||||||
|
import { createBinding, createComputed } from "ags";
|
||||||
|
|
||||||
|
|
||||||
|
export const TileBluetooth = () =>
|
||||||
|
<Tile title={"Bluetooth"} visible={
|
||||||
|
createBinding(AstalBluetooth.get_default(), "adapter").as(Boolean)
|
||||||
|
} description={createBinding(AstalBluetooth.get_default(), "isConnected").as((connected) => {
|
||||||
|
const connectedDev = AstalBluetooth.get_default().devices.filter(dev => dev.connected)?.[0];
|
||||||
|
return connected && connectedDev ? connectedDev.get_alias() : ""
|
||||||
|
})} onToggledOn={() => AstalBluetooth.get_default().adapter?.set_powered(true)}
|
||||||
|
onToggledOff={() => AstalBluetooth.get_default().adapter?.set_powered(false)}
|
||||||
|
onClickMore={() => TilesPages?.toggle(BluetoothPage())}
|
||||||
|
enableOnClickMore={true} iconSize={16}
|
||||||
|
toggleState={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")}
|
||||||
|
/>;
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { bind } from "astal";
|
|
||||||
import { Notifications } from "../../../scripts/notifications";
|
|
||||||
import { Tile } from "./Tile";
|
|
||||||
import { tr } from "../../../i18n/intl";
|
|
||||||
|
|
||||||
export const TileDND = Tile({
|
|
||||||
title: tr("control_center.tiles.dnd.title"),
|
|
||||||
description: bind(Notifications.getDefault().getNotifd(), "dontDisturb").as(
|
|
||||||
(dnd: boolean) => dnd ? tr("control_center.tiles.enabled") : tr("control_center.tiles.disabled")),
|
|
||||||
onToggledOff: () => Notifications.getDefault().getNotifd().dontDisturb = false,
|
|
||||||
onToggledOn: () => Notifications.getDefault().getNotifd().dontDisturb = true,
|
|
||||||
icon: "minus-circle-filled-symbolic",
|
|
||||||
iconSize: 16,
|
|
||||||
toggleState: Notifications.getDefault().getNotifd().dontDisturb
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Notifications } from "../../../scripts/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"))}
|
||||||
|
onToggledOff={() => Notifications.getDefault().getNotifd().dontDisturb = false}
|
||||||
|
onToggledOn={() => Notifications.getDefault().getNotifd().dontDisturb = true}
|
||||||
|
icon={"minus-circle-filled-symbolic"}
|
||||||
|
iconSize={16}
|
||||||
|
toggleState={Notifications.getDefault().getNotifd().dontDisturb}
|
||||||
|
/>;
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import { bind, execAsync, Variable } from "astal";
|
|
||||||
import { Tile, TileProps } from "./Tile";
|
|
||||||
import AstalNetwork from "gi://AstalNetwork";
|
|
||||||
import { Widget } from "astal/gtk3";
|
|
||||||
import { PageNetwork } from "../pages/Network";
|
|
||||||
import { tr } from "../../../i18n/intl";
|
|
||||||
import { TilesPages } from "../Tiles";
|
|
||||||
|
|
||||||
export const TileNetwork = () => new Widget.Box({
|
|
||||||
child: Variable.derive([
|
|
||||||
bind(AstalNetwork.get_default(), "primary"),
|
|
||||||
bind(AstalNetwork.get_default(), "wired"),
|
|
||||||
bind(AstalNetwork.get_default(), "wifi")
|
|
||||||
],
|
|
||||||
(primary: AstalNetwork.Primary, wired: AstalNetwork.Wired, wifi: AstalNetwork.Wifi) => {
|
|
||||||
if(primary === AstalNetwork.Primary.WIFI) {
|
|
||||||
return Tile({
|
|
||||||
title: tr("control_center.tiles.network.wireless"),
|
|
||||||
description: Variable.derive(
|
|
||||||
[ bind(wifi, "ssid"), bind(wifi, "internet") ],
|
|
||||||
(ssid: string, internet: AstalNetwork.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") + "...";
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
)(),
|
|
||||||
onToggledOn: () => wifi.set_enabled(true),
|
|
||||||
onToggledOff: () => wifi.set_enabled(false),
|
|
||||||
onClickMore: () => TilesPages?.toggle(PageNetwork()),
|
|
||||||
icon: "network-wireless-signal-excellent-symbolic",
|
|
||||||
toggleState: bind(wifi, "enabled")
|
|
||||||
} as TileProps)();
|
|
||||||
|
|
||||||
} else if(primary === AstalNetwork.Primary.WIRED) {
|
|
||||||
return Tile({
|
|
||||||
title: tr("control_center.tiles.network.wired") || "Wired",
|
|
||||||
description: bind(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") + "...";
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
onToggledOn: () => execAsync("nmcli n on"),
|
|
||||||
onToggledOff: () => execAsync("nmcli n off"),
|
|
||||||
onClickMore: () => TilesPages?.toggle(PageNetwork()),
|
|
||||||
icon: bind(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";
|
|
||||||
}),
|
|
||||||
iconSize: 16,
|
|
||||||
toggleState: bind(wired, "internet").as((internet: AstalNetwork.Internet) =>
|
|
||||||
internet === AstalNetwork.Internet.CONNECTING
|
|
||||||
|| internet === AstalNetwork.Internet.CONNECTED
|
|
||||||
)
|
|
||||||
} as TileProps)();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Tile({
|
|
||||||
title: tr("control_center.tiles.network.network"),
|
|
||||||
description: tr("disconnected"),
|
|
||||||
onToggledOn: () => execAsync("nmcli n on"),
|
|
||||||
onToggledOff: () => execAsync("nmcli n off"),
|
|
||||||
onClickMore: () => TilesPages?.toggle(PageNetwork()),
|
|
||||||
icon: "network-wired-disconnected-symbolic",
|
|
||||||
iconSize: 16,
|
|
||||||
toggleState: bind(wired, "internet").as((internet: AstalNetwork.Internet) =>
|
|
||||||
internet === AstalNetwork.Internet.CONNECTING || internet === AstalNetwork.Internet.CONNECTED)
|
|
||||||
} as TileProps)();
|
|
||||||
})()
|
|
||||||
} as Widget.BoxProps);
|
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { execAsync } from "ags/process";
|
||||||
|
import { Tile } from "./Tile";
|
||||||
|
import AstalNetwork from "gi://AstalNetwork";
|
||||||
|
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";
|
||||||
|
|
||||||
|
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") + "...";
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
)} onToggledOn={() => wifi.set_enabled(true)}
|
||||||
|
onToggledOff={() => wifi.set_enabled(false)}
|
||||||
|
onClickMore={() => TilesPages?.toggle(PageNetwork())}
|
||||||
|
icon={"network-wireless-signal-excellent-symbolic"}
|
||||||
|
toggleState={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") + "...";
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
onToggledOn={() => execAsync("nmcli n on")}
|
||||||
|
onToggledOff={() => execAsync("nmcli n off")}
|
||||||
|
onClickMore={() => 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";
|
||||||
|
})}
|
||||||
|
iconSize={16}
|
||||||
|
toggleState={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")}
|
||||||
|
onToggledOn={() => execAsync("nmcli n on")}
|
||||||
|
onToggledOff={() => execAsync("nmcli n off")}
|
||||||
|
onClickMore={() => TilesPages?.toggle(PageNetwork())}
|
||||||
|
icon={"network-wired-disconnected-symbolic"}
|
||||||
|
iconSize={16}
|
||||||
|
toggleState={createBinding(wired, "internet").as((internet: AstalNetwork.Internet) =>
|
||||||
|
internet === AstalNetwork.Internet.CONNECTING || internet === AstalNetwork.Internet.CONNECTED)}
|
||||||
|
/>
|
||||||
|
}}
|
||||||
|
</With>
|
||||||
|
</Gtk.Box> as Gtk.Box;
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { bind, Variable } from "astal";
|
|
||||||
import { Tile, TileProps } from "./Tile";
|
|
||||||
import { NightLight } from "../../../scripts/nightlight";
|
|
||||||
import { PageNightLight } from "../pages/NightLight";
|
|
||||||
import { tr } from "../../../i18n/intl";
|
|
||||||
import { TilesPages } from "../Tiles";
|
|
||||||
import { isInstalled } from "../../../scripts/utils";
|
|
||||||
import { Widget } from "astal/gtk3";
|
|
||||||
|
|
||||||
export const TileNightLight = () => isInstalled("hyprsunset") ? Tile({
|
|
||||||
title: tr("control_center.tiles.night_light.title"),
|
|
||||||
icon: "weather-clear-night-symbolic",
|
|
||||||
description: Variable.derive([
|
|
||||||
bind(NightLight.getDefault(), "temperature"),
|
|
||||||
bind(NightLight.getDefault(), "gamma")
|
|
||||||
], (temp, gamma) => `${temp === NightLight.getDefault().identityTemperature ?
|
|
||||||
tr("control_center.tiles.night_light.default_desc") : `${temp}K`} ${
|
|
||||||
gamma < NightLight.getDefault().maxGamma ? `(${gamma}%)` : ""}`
|
|
||||||
)(),
|
|
||||||
onToggledOff: () => NightLight.getDefault().identity = true,
|
|
||||||
onToggledOn: () => NightLight.getDefault().identity = false,
|
|
||||||
enableOnClickMore: true,
|
|
||||||
onClickMore: () => TilesPages?.toggle(PageNightLight()),
|
|
||||||
toggleState: bind(NightLight.getDefault(), "identity").as(identity => !identity)
|
|
||||||
} as TileProps)()
|
|
||||||
: new Widget.Box({ visible: false } as Widget.BoxProps);
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Tile } from "./Tile";
|
||||||
|
import { NightLight } from "../../../scripts/nightlight";
|
||||||
|
import { PageNightLight } from "../pages/NightLight";
|
||||||
|
import { tr } from "../../../i18n/intl";
|
||||||
|
import { TilesPages } from "../Tiles";
|
||||||
|
import { isInstalled } from "../../../scripts/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(), "temperature"),
|
||||||
|
createBinding(NightLight.getDefault(), "gamma")
|
||||||
|
], (temp, gamma) => `${temp === NightLight.getDefault().identityTemperature ?
|
||||||
|
tr("control_center.tiles.night_light.default_desc") : `${temp}K`} ${
|
||||||
|
gamma < NightLight.getDefault().maxGamma ? `(${gamma}%)` : ""}`
|
||||||
|
)}
|
||||||
|
visible={isInstalled("hyprsunset")}
|
||||||
|
onToggledOff={() => NightLight.getDefault().identity = true}
|
||||||
|
onToggledOn={() => NightLight.getDefault().identity = false}
|
||||||
|
enableOnClickMore={true}
|
||||||
|
onClickMore={() => TilesPages?.toggle(PageNightLight())}
|
||||||
|
toggleState={createBinding(NightLight.getDefault(), "identity").as(identity => !identity)}
|
||||||
|
/>
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { Tile, TileProps } from "./Tile";
|
|
||||||
import { Recording } from "../../../scripts/recording";
|
|
||||||
import { bind, Variable } from "astal";
|
|
||||||
import { tr } from "../../../i18n/intl";
|
|
||||||
import { getDateTime } from "../../../scripts/time";
|
|
||||||
import { isInstalled } from "../../../scripts/utils";
|
|
||||||
|
|
||||||
const wfRecorderInstalled = isInstalled("wf-recorder");
|
|
||||||
|
|
||||||
export const TileRecording = () => {
|
|
||||||
const description: Variable<string> = Variable.derive([
|
|
||||||
bind(Recording.getDefault(), "recording"),
|
|
||||||
getDateTime()
|
|
||||||
], (recording, dateTime) => {
|
|
||||||
if(!recording || !Recording.getDefault().startedAt)
|
|
||||||
return tr("control_center.tiles.recording.disabled_desc") || "Start recording";
|
|
||||||
|
|
||||||
const startedAtSeconds = dateTime.to_unix() - Recording.getDefault().startedAt!.to_unix();
|
|
||||||
if(startedAtSeconds <= 0) return "00:00";
|
|
||||||
|
|
||||||
const minutes = Math.floor(startedAtSeconds / 60);
|
|
||||||
const seconds = Math.floor(startedAtSeconds % 60);
|
|
||||||
|
|
||||||
return `${ minutes < 10 ? `0${minutes}` : minutes }:${ seconds < 10 ? `0${seconds}` : seconds }`;
|
|
||||||
});
|
|
||||||
|
|
||||||
return Tile({
|
|
||||||
title: tr("control_center.tiles.recording.title") || "Screen Recording",
|
|
||||||
description: description(),
|
|
||||||
icon: "media-record-symbolic",
|
|
||||||
visible: wfRecorderInstalled,
|
|
||||||
onDestroy: () => description.drop(),
|
|
||||||
onToggledOff: () => Recording.getDefault().stopRecording(),
|
|
||||||
onToggledOn: () => Recording.getDefault().startRecording(),
|
|
||||||
toggleState: bind(Recording.getDefault(), "recording"),
|
|
||||||
iconSize: 16
|
|
||||||
} as TileProps)();
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { Tile } from "./Tile";
|
||||||
|
import { Recording } from "../../../scripts/recording";
|
||||||
|
import { tr } from "../../../i18n/intl";
|
||||||
|
import { isInstalled, time } from "../../../scripts/utils";
|
||||||
|
import { createBinding, createComputed } from "ags";
|
||||||
|
import { Gtk } from "ags/gtk4";
|
||||||
|
|
||||||
|
|
||||||
|
export const TileRecording = () =>
|
||||||
|
<Tile title={tr("control_center.tiles.recording.title")}
|
||||||
|
description={createComputed([
|
||||||
|
createBinding(Recording.getDefault(), "recording"),
|
||||||
|
time
|
||||||
|
], (recording, dateTime) => {
|
||||||
|
if(!recording || !Recording.getDefault().startedAt)
|
||||||
|
return tr("control_center.tiles.recording.disabled_desc") || "Start recording";
|
||||||
|
|
||||||
|
const startedAtSeconds = dateTime.to_unix() - Recording.getDefault().startedAt!;
|
||||||
|
if(startedAtSeconds <= 0) return "00:00";
|
||||||
|
|
||||||
|
const minutes = Math.floor(startedAtSeconds / 60);
|
||||||
|
const seconds = Math.floor(startedAtSeconds % 60);
|
||||||
|
|
||||||
|
return `${ minutes < 10 ? `0${minutes}` : minutes }:${ seconds < 10 ? `0${seconds}` : seconds }`;
|
||||||
|
})}
|
||||||
|
icon={"media-record-symbolic"}
|
||||||
|
visible={isInstalled("wf-recorder")}
|
||||||
|
onToggledOff={() => Recording.getDefault().stopRecording()}
|
||||||
|
onToggledOn={() => Recording.getDefault().startRecording()}
|
||||||
|
toggleState={createBinding(Recording.getDefault(), "recording")}
|
||||||
|
iconSize={16}
|
||||||
|
/> as Gtk.Widget;
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
import { Binding, Variable } from "astal";
|
|
||||||
import { Gtk, Widget } from "astal/gtk3";
|
|
||||||
import { tr } from "../../../i18n/intl";
|
|
||||||
|
|
||||||
export type TileProps = {
|
|
||||||
className?: string | Binding<string | undefined>;
|
|
||||||
icon?: string | Binding<string | undefined>;
|
|
||||||
visible?: boolean | Binding<boolean | undefined>;
|
|
||||||
iconSize?: number | Binding<number | undefined>;
|
|
||||||
title: string | Binding<string | undefined>;
|
|
||||||
description?: string | Binding<string | undefined>;
|
|
||||||
toggleState?: boolean | Binding<boolean | undefined>;
|
|
||||||
enableOnClickMore?: boolean | Binding<boolean | undefined>;
|
|
||||||
onDestroy?: () => void;
|
|
||||||
onToggledOn: () => void;
|
|
||||||
onToggledOff: () => void;
|
|
||||||
onClickMore?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Tile(props: TileProps): (() => Gtk.Widget) {
|
|
||||||
const subs: Array<() => void> = [];
|
|
||||||
const toggled = new Variable<boolean>(((props.toggleState instanceof Binding) ?
|
|
||||||
props.toggleState.get()
|
|
||||||
: props.toggleState) ?? false);
|
|
||||||
|
|
||||||
if(props?.toggleState instanceof Binding)
|
|
||||||
subs.push(props.toggleState.subscribe((state) =>
|
|
||||||
toggled.set(state ?? false)
|
|
||||||
));
|
|
||||||
|
|
||||||
return () => new Widget.Box({
|
|
||||||
className: (props.className instanceof Binding) ?
|
|
||||||
Variable.derive([
|
|
||||||
props.className,
|
|
||||||
toggled()
|
|
||||||
], (className, isToggled) =>
|
|
||||||
`tile ${className} ${isToggled ? "toggled" : ""} ${
|
|
||||||
props.onClickMore ? "has-more" : ""
|
|
||||||
}`
|
|
||||||
)()
|
|
||||||
: toggled().as((state: boolean) =>
|
|
||||||
`tile${state ? " toggled" : ""}${
|
|
||||||
props.onClickMore ? " has-more" : ""
|
|
||||||
}`
|
|
||||||
),
|
|
||||||
expand: true,
|
|
||||||
visible: props.visible,
|
|
||||||
onDestroy: () => {
|
|
||||||
subs.map(sub => sub?.());
|
|
||||||
props.onDestroy?.();
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
new Widget.Button({
|
|
||||||
className: "toggle-button",
|
|
||||||
onClick: () => {
|
|
||||||
if(toggled.get()) {
|
|
||||||
toggled.set(false);
|
|
||||||
props.onToggledOff && props.onToggledOff();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toggled.set(true);
|
|
||||||
props.onToggledOn && props.onToggledOn();
|
|
||||||
},
|
|
||||||
child: new Widget.Box({
|
|
||||||
className: "content",
|
|
||||||
expand: true,
|
|
||||||
hexpand: true,
|
|
||||||
children: [
|
|
||||||
new Widget.Icon({
|
|
||||||
className: "icon",
|
|
||||||
icon: props.icon,
|
|
||||||
visible: (props.icon instanceof Binding) ?
|
|
||||||
props.icon.as(Boolean)
|
|
||||||
: Boolean(props.icon),
|
|
||||||
css: `font-size: ${props.iconSize ?? 16}px;`
|
|
||||||
} as Widget.IconProps),
|
|
||||||
new Widget.Box({
|
|
||||||
className: "text",
|
|
||||||
orientation: Gtk.Orientation.VERTICAL,
|
|
||||||
vexpand: true,
|
|
||||||
hexpand: true,
|
|
||||||
valign: Gtk.Align.CENTER,
|
|
||||||
children: [
|
|
||||||
new Widget.Label({
|
|
||||||
className: "title",
|
|
||||||
xalign: 0,
|
|
||||||
halign: Gtk.Align.START,
|
|
||||||
truncate: true,
|
|
||||||
label: props.title
|
|
||||||
} as Widget.LabelProps),
|
|
||||||
new Widget.Label({
|
|
||||||
className: "description",
|
|
||||||
visible: (props.description instanceof Binding) ?
|
|
||||||
props.description.as(Boolean)
|
|
||||||
: Boolean(props.description),
|
|
||||||
halign: Gtk.Align.START,
|
|
||||||
truncate: true,
|
|
||||||
xalign: 0,
|
|
||||||
label: (props.description instanceof Binding) ?
|
|
||||||
props.description.as((desc) => desc ? desc : "")
|
|
||||||
: (props.description || "")
|
|
||||||
} as Widget.LabelProps)
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
]
|
|
||||||
} as Widget.BoxProps)
|
|
||||||
} as Widget.ButtonProps),
|
|
||||||
new Widget.Button({
|
|
||||||
className: "more icon",
|
|
||||||
visible: props.onClickMore !== undefined,
|
|
||||||
halign: Gtk.Align.END,
|
|
||||||
tooltipText: tr("control_center.tiles.more") || "More",
|
|
||||||
image: new Widget.Icon({
|
|
||||||
icon: "go-next-symbolic",
|
|
||||||
css: "icon { font-size: 16px; }"
|
|
||||||
}),
|
|
||||||
onClick: () => {
|
|
||||||
((props.enableOnClickMore instanceof Binding) ?
|
|
||||||
props.enableOnClickMore.get()
|
|
||||||
: props.enableOnClickMore) && props?.onToggledOn();
|
|
||||||
|
|
||||||
props.onClickMore && props?.onClickMore()
|
|
||||||
},
|
|
||||||
widthRequest: 32
|
|
||||||
})
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { Gdk, Gtk } from "ags/gtk4";
|
||||||
|
import { tr } from "../../../i18n/intl";
|
||||||
|
import { Accessor, createComputed, createState } from "ags";
|
||||||
|
import GObject from "gi://GObject?version=2.0";
|
||||||
|
import Pango from "gi://Pango?version=1.0";
|
||||||
|
import { variableToBoolean } from "../../../scripts/utils";
|
||||||
|
|
||||||
|
export type TileProps = {
|
||||||
|
class?: string | Accessor<string>;
|
||||||
|
icon?: string | Accessor<string>;
|
||||||
|
visible?: boolean | Accessor<boolean>;
|
||||||
|
iconSize?: number | Accessor<number>;
|
||||||
|
title: string | Accessor<string>;
|
||||||
|
description?: string | Accessor<string>;
|
||||||
|
toggleState?: boolean | Accessor<boolean>;
|
||||||
|
enableOnClickMore?: boolean | Accessor<boolean>;
|
||||||
|
onDestroy?: () => void;
|
||||||
|
onToggledOn: () => void;
|
||||||
|
onToggledOff: () => void;
|
||||||
|
onClickMore?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Tile(props: TileProps): Gtk.Widget {
|
||||||
|
const subs: Array<() => void> = [];
|
||||||
|
const [toggled, setToggled] = createState(((props.toggleState instanceof Accessor) ?
|
||||||
|
props.toggleState.get()
|
||||||
|
: props.toggleState) ?? false);
|
||||||
|
|
||||||
|
|
||||||
|
(props.toggleState instanceof Accessor) && subs.push(
|
||||||
|
props.toggleState.subscribe(() =>
|
||||||
|
setToggled((props.toggleState as Accessor<boolean>).get() ?? false))
|
||||||
|
);
|
||||||
|
|
||||||
|
return <Gtk.Box class={
|
||||||
|
(props.class instanceof Accessor) ?
|
||||||
|
createComputed([props.class, toggled], (clss, isToggled) =>
|
||||||
|
`tile ${clss} ${isToggled ? "toggled" : ""} ${
|
||||||
|
props.onClickMore ? "has-more" : ""
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
: toggled.as(isToggled =>
|
||||||
|
`tile ${props.class ? props.class : ""} ${isToggled ? "toggled" : ""} ${
|
||||||
|
props.onClickMore ? "has-more" : ""
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
} hexpand={true} visible={props.visible} onDestroy={(_) => {
|
||||||
|
subs.forEach(sub => sub());
|
||||||
|
props.onDestroy?.();
|
||||||
|
}}>
|
||||||
|
|
||||||
|
<Gtk.Button class={"toggle-button"} $={(self) => {
|
||||||
|
const gestureClick = Gtk.GestureClick.new();
|
||||||
|
const conns: Map<GObject.Object, number> = new Map();
|
||||||
|
|
||||||
|
self.add_controller(gestureClick);
|
||||||
|
|
||||||
|
conns.set(gestureClick, gestureClick.connect("released", (gesture) => {
|
||||||
|
if(gesture.get_current_button() === Gdk.BUTTON_PRIMARY) {
|
||||||
|
if(toggled.get()) {
|
||||||
|
setToggled(false);
|
||||||
|
props.onToggledOff?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setToggled(true);
|
||||||
|
props.onToggledOn?.();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}}>
|
||||||
|
|
||||||
|
<Gtk.Box class={"content"} hexpand={true} vexpand={true}>
|
||||||
|
{props.icon && <Gtk.Image class={"icon"} iconName={props.icon} css={
|
||||||
|
(props.iconSize instanceof Accessor) ?
|
||||||
|
props.iconSize.as(size => `font-size: ${size}px;`)
|
||||||
|
: (props.iconSize ?
|
||||||
|
`font-size: ${props.iconSize ?? 16}px;`
|
||||||
|
: undefined)
|
||||||
|
} />}
|
||||||
|
|
||||||
|
<Gtk.Box orientation={Gtk.Orientation.VERTICAL} class={"text"} vexpand={true} hexpand={true}
|
||||||
|
valign={Gtk.Align.CENTER}>
|
||||||
|
|
||||||
|
<Gtk.Label class={"title"} xalign={0} halign={Gtk.Align.START} ellipsize={Pango.EllipsizeMode.END}
|
||||||
|
label={props.title} />
|
||||||
|
|
||||||
|
{props.description && <Gtk.Label class={"description"} ellipsize={Pango.EllipsizeMode.END}
|
||||||
|
visible={variableToBoolean(props.description)} xalign={0} label={
|
||||||
|
(props.description instanceof Accessor) ?
|
||||||
|
props.description.as(str => str ?? "")
|
||||||
|
: (props.description ?? "")
|
||||||
|
} halign={Gtk.Align.START}
|
||||||
|
/>}
|
||||||
|
|
||||||
|
</Gtk.Box>
|
||||||
|
</Gtk.Box>
|
||||||
|
</Gtk.Button>
|
||||||
|
|
||||||
|
<Gtk.Button class={"more icon"} iconName={"go-next-symbolic"} widthRequest={32}
|
||||||
|
visible={Boolean(props.onClickMore)} halign={Gtk.Align.END} onClicked={() => {
|
||||||
|
((props.enableOnClickMore instanceof Accessor) ?
|
||||||
|
props.enableOnClickMore.get()
|
||||||
|
: props.enableOnClickMore) && props.onToggledOn?.();
|
||||||
|
|
||||||
|
props.onClickMore?.();
|
||||||
|
}} tooltipText={tr("control_center.tiles.more")} />
|
||||||
|
</Gtk.Box> as Gtk.Widget;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user