diff --git a/ags/widget/center-window/BigMedia.ts b/ags/widget/center-window/BigMedia.ts deleted file mode 100644 index d283975..0000000 --- a/ags/widget/center-window/BigMedia.ts +++ /dev/null @@ -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) => - players[0] ? true : false), - children: bind(AstalMpris.get_default(), "players").as((players: Array) => - 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 { - return bind(player, "artUrl").as((artUrl: string) => { - - if(!artUrl) - return undefined; - - if(artUrl.startsWith("/")) - return "file://" + artUrl; - - return artUrl; - }); -} diff --git a/ags/widget/center-window/BigMedia.tsx b/ags/widget/center-window/BigMedia.tsx new file mode 100644 index 0000000..cc91c90 --- /dev/null +++ b/ags/widget/center-window/BigMedia.tsx @@ -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 pl.available)}> + + + {(player: AstalMpris.Player) => player.available && + { + 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( + as Gtk.Picture + ); + }); + + const destroyId = self.connect("destroy", () => { + self.disconnect(destroyId); + artSub(); + }); + }}> + + + + title ?? "No Title") + } ellipsize={Pango.EllipsizeMode.END} maxWidthChars={25} + /> + artist ?? "No Artist") + } ellipsize={Pango.EllipsizeMode.END} maxWidthChars={28} + /> + + + + { + 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)); + }} + /> + + + + { + const sec = Math.floor(pos % 60); + return pos > 0 && player.length > 0 ? + `${Math.floor(pos / 60)}:${sec < 10 ? "0" : ""}${sec}` + : "0:00"; + })} + /> + + + { + 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}`); + }); + }} + /> + + 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} + /> + player.canGoPrevious && player.previous()} + /> + + 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} + /> + player.canGoNext && player.next()} + /> + { + 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} + /> + + { /* bananananananana */ + const sec = Math.floor(len % 60); + return (len > 0 && Number.isFinite(len)) ? + `${Math.floor(len / 60)}:${sec < 10 ? "0" : ""}${sec}` + : "0:00"; + })} + /> + + + } + + 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; +} diff --git a/ags/widget/center-window/Calendar.ts b/ags/widget/center-window/Calendar.ts deleted file mode 100644 index 4fa5271..0000000 --- a/ags/widget/center-window/Calendar.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { register, Variable } from "astal"; -import { Gtk, Widget } from "astal/gtk3"; - -type CalendarProps = Pick & { - - 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(true); - #showHeader = new Variable(true); - #fillGrid = new Variable(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)); - } -} diff --git a/ags/widget/control-center/NotifHistory.ts b/ags/widget/control-center/NotifHistory.ts deleted file mode 100644 index a572805..0000000 --- a/ags/widget/control-center/NotifHistory.ts +++ /dev/null @@ -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) => - 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); -} diff --git a/ags/widget/control-center/NotifHistory.tsx b/ags/widget/control-center/NotifHistory.tsx new file mode 100644 index 0000000..62e6c29 --- /dev/null +++ b/ags/widget/control-center/NotifHistory.tsx @@ -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 = () => + + `history ${history.length < 1 ? "hide" : ""}`)}> + + { + 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; + }}> + + + + + {(notif: AstalNotifd.Notification|HistoryNotification) => + Notifications.getDefault().removeHistory(n.id)} + actionClicked={(n) => Notifications.getDefault().removeHistory(n.id)} + />} + + + + + + + + as Gtk.Box; diff --git a/ags/widget/control-center/Pages.ts b/ags/widget/control-center/Pages.ts deleted file mode 100644 index ef108d2..0000000 --- a/ags/widget/control-center/Pages.ts +++ /dev/null @@ -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).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?.(); - }); - }); - } -} diff --git a/ags/widget/control-center/Pages.tsx b/ags/widget/control-center/Pages.tsx new file mode 100644 index 0000000..9475f64 --- /dev/null +++ b/ags/widget/control-center/Pages.tsx @@ -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 = + + {newPage as unknown as Gtk.Widget} + 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; + } +} diff --git a/ags/widget/control-center/QuickActions.ts b/ags/widget/control-center/QuickActions.ts deleted file mode 100644 index b3b40fc..0000000 --- a/ags/widget/control-center/QuickActions.ts +++ /dev/null @@ -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("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); -} diff --git a/ags/widget/control-center/QuickActions.tsx b/ags/widget/control-center/QuickActions.tsx new file mode 100644 index 0000000..1fd29d7 --- /dev/null +++ b/ags/widget/control-center/QuickActions.tsx @@ -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 = createPoll("Just turned on", 1000, "uptime -p"); + +function LockButton(): Gtk.Button { + return { + Windows.getDefault().close("control-center"); + execApp("hyprlock"); + }} + /> as Gtk.Button; +} + +function ColorPickerButton(): Gtk.Button { + return { + Windows.getDefault().close("control-center"); + execApp("sh $HOME/.config/hypr/scripts/color-picker.sh"); + }} + /> as Gtk.Button; +} + +function ScreenshotButton(): Gtk.Button { + return { + Windows.getDefault().close("control-center"); + execApp(`sh ${GLib.get_user_config_dir()}/hypr/scripts/screenshot.sh`); + }} + /> as Gtk.Button; +} + +function SelectWallpaperButton(): Gtk.Button { + return { + Windows.getDefault().close("control-center"); + Wallpaper.getDefault().pickWallpaper(); + }} + /> as Gtk.Button; +} + +function LogoutButton(): Gtk.Button { + return { + Windows.getDefault().close("control-center"); + Windows.getDefault().open("logout-menu"); + }} + /> as Gtk.Button; +} + +export const QuickActions = () => + + + + + + + + str.replace(/^up /, ""))} /> + + + + + + + + + + + + as Gtk.Box; diff --git a/ags/widget/control-center/Sliders.ts b/ags/widget/control-center/Sliders.ts deleted file mode 100644 index 156134a..0000000 --- a/ags/widget/control-center/Sliders.ts +++ /dev/null @@ -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); -} diff --git a/ags/widget/control-center/Sliders.tsx b/ags/widget/control-center/Sliders.tsx new file mode 100644 index 0000000..a993b1f --- /dev/null +++ b/ags/widget/control-center/Sliders.tsx @@ -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 = as Pages; + + return + + + {(sink: AstalWp.Endpoint) => + + (!Wireplumber.getDefault().isMutedSink() && + Wireplumber.getDefault().getSinkVolume() > 0) ? + icon + : "audio-volume-muted-symbolic" + )} /> + + 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)} /> + + + slidersPages.toggle(PageSound())} /> + } + + + {(source: AstalWp.Endpoint) => + + (!Wireplumber.getDefault().isMutedSink() && + Wireplumber.getDefault().getSinkVolume() > 0) ? + icon + : "microphone-sensitivity-muted-symbolic" + )} /> + + 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)} /> + + + slidersPages.toggle(PageMicrophone())} /> + } + + {slidersPages} + +} diff --git a/ags/widget/control-center/Tiles.ts b/ags/widget/control-center/Tiles.ts deleted file mode 100644 index 5630a2a..0000000 --- a/ags/widget/control-center/Tiles.ts +++ /dev/null @@ -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); -} diff --git a/ags/widget/control-center/Tiles.tsx b/ags/widget/control-center/Tiles.tsx new file mode 100644 index 0000000..4651916 --- /dev/null +++ b/ags/widget/control-center/Tiles.tsx @@ -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 TilesPages = null} $={(self) => { + if(!TilesPages) + TilesPages = as Pages; + + self.append(TilesPages as unknown as Gtk.Widget); + }}> + + + + {tileList.map(tile => tile())} + + as Gtk.Box; +} diff --git a/ags/widget/control-center/pages/Bluetooth.ts b/ags/widget/control-center/pages/Bluetooth.ts deleted file mode 100644 index 5d93a4f..0000000 --- a/ags/widget/control-center/pages/Bluetooth.ts +++ /dev/null @@ -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> = 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() - }); -} diff --git a/ags/widget/control-center/pages/Bluetooth.tsx b/ags/widget/control-center/pages/Bluetooth.tsx new file mode 100644 index 0000000..9b9e9c5 --- /dev/null +++ b/ags/widget/control-center/pages/Bluetooth.tsx @@ -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 = () => 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}> + + + + + {(adapter: AstalBluetooth.Adapter) => + + } + + + + + + devs.filter(dev => dev.paired || dev.connected || dev.trusted).length > 0)}> + + + + devs.filter(dev => dev.paired || dev.connected || dev.trusted))}> + + {(dev: AstalBluetooth.Device) => } + + + + devs.filter(dev => !dev.connected && !dev.paired && !dev.trusted).length > 0)}> + + + + devs.filter(dev => !dev.connected && !dev.paired && !dev.trusted))}> + {(dev: AstalBluetooth.Device) => } + + + + as Page; + +function DeviceWidget({ device }: { device: AstalBluetooth.Device }): Gtk.Widget { + return + 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={ connected && (batt > -1)) + }> + + `${Math.floor(batt * 100)}%`)} /> + + + `battery-level-${Math.floor(batt * 100)}-symbolic`) + } css={"font-size: 16px; margin-left: 6px;"} /> + } extraButtons={ + {([connected, trusted]: [boolean, boolean]) => trusted && + + { { + if(!connected) { + AstalBluetooth.get_default().adapter?.remove_device(device); + return; + } + + device.disconnect_device(null); + }} />} + + device.set_trusted(!trusted)} + /> + } + } + /> as Page; +} diff --git a/ags/widget/control-center/pages/Microphone.ts b/ags/widget/control-center/pages/Microphone.ts deleted file mode 100644 index 3d101cc..0000000 --- a/ags/widget/control-center/pages/Microphone.ts +++ /dev/null @@ -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); -} diff --git a/ags/widget/control-center/pages/Microphone.tsx b/ags/widget/control-center/pages/Microphone.tsx new file mode 100644 index 0000000..7d1b111 --- /dev/null +++ b/ags/widget/control-center/pages/Microphone.tsx @@ -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 + + + + {(source: AstalWp.Endpoint) => 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={ + + } + />} + + as Page; +} diff --git a/ags/widget/control-center/pages/Network.ts b/ags/widget/control-center/pages/Network.ts deleted file mode 100644 index 3f39b77..0000000 --- a/ags/widget/control-center/pages/Network.ts +++ /dev/null @@ -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); -} diff --git a/ags/widget/control-center/pages/Network.tsx b/ags/widget/control-center/pages/Network.tsx new file mode 100644 index 0000000..080dfe8 --- /dev/null +++ b/ags/widget/control-center/pages/Network.tsx @@ -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 = () => + + 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]"); + } + }]}> + + + + + + devs.filter(dev => dev.interface !== "lo" && dev.real /* filter local device */))}> + + {(device: NM.Device) => + 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={[ + + { + Windows.getDefault().close("control-center"); + execApp( + `nm-connection-editor --edit ${device.activeConnection?.connection.get_uuid()}`, + "[animationstyle gnomed; float]" + ); + }} /> + ]} + />} + + + + + primary === AstalNetwork.Primary.WIFI)}> + + {(isWifi: boolean) => isWifi && + + + + {(ap: AstalNetwork.AccessPoint) => + activeAP.ssid === ap.ssid ? "active" : "") + } title={createBinding(ap, "ssid").as(ssid => ssid ?? "No SSID")} + icon={createBinding(ap, "iconName")} endWidget={ + // @ts-ignore + flags & NM["80211ApFlags"].PRIVACY ? + "channel-secure-symbolic" + : "channel-insecure-symbolic")} + css={"font-size: 18px;"} + />} extraButtons={[ + + 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); + }} + />} + + } + + 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); +} diff --git a/ags/widget/control-center/pages/NightLight.ts b/ags/widget/control-center/pages/NightLight.ts deleted file mode 100644 index c0d1047..0000000 --- a/ags/widget/control-center/pages/NightLight.ts +++ /dev/null @@ -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); diff --git a/ags/widget/control-center/pages/NightLight.tsx b/ags/widget/control-center/pages/NightLight.tsx new file mode 100644 index 0000000..d0883d7 --- /dev/null +++ b/ags/widget/control-center/pages/NightLight.tsx @@ -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) = () => + + + + { + 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) + }} + /> + + { + 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) + }} + /> + as Page; diff --git a/ags/widget/control-center/pages/Page.ts b/ags/widget/control-center/pages/Page.ts deleted file mode 100644 index a30f15a..0000000 --- a/ags/widget/control-center/pages/Page.ts +++ /dev/null @@ -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; - title: string | Binding; - description?: string | Binding; - headerButtons?: Array | Binding>; - bottomButtons?: Array | Binding>; - orientation?: Gtk.Orientation | Binding; - spacing?: number; - child?: Gtk.Widget | Binding; - children?: Array | Binding>; -}; - -export type BottomButton = { - title: string | Binding; - description?: string | Binding; - tooltipText?: string | Binding; - tooltipMarkup?: string | Binding; - onClick?: () => void; -}; - -export { Page }; - -@register({ GTypeName: "Page" }) -class Page extends Widget.Box { - readonly #id: string | number; - readonly bottomButtons?: Array; - - #title: string | Binding; - #description?: string | Binding; - - 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; - icon?: string | Binding; - title: string | Binding; - endWidget?: Gtk.Widget | Binding; - description?: string | Binding; - extraButtons?: Array | Binding>; - onDestroy?: (self: Widget.Box) => void; - onClick?: (self: Widget.Button) => void; - tooltipText?: string | Binding; - tooltipMarkup?: string | Binding; -}): 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); -} diff --git a/ags/widget/control-center/pages/Page.tsx b/ags/widget/control-center/pages/Page.tsx new file mode 100644 index 0000000..4b38b62 --- /dev/null +++ b/ags/widget/control-center/pages/Page.tsx @@ -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; + title: string | Accessor; + description?: string | Accessor; + headerButtons?: Array | Accessor>; + bottomButtons?: Array | Accessor>; + orientation?: Gtk.Orientation | Accessor; + spacing?: number; + children?: WidgetNodeType; +}; + +export type BottomButton = { + title: string | Accessor; + description?: string | Accessor; + tooltipText?: string | Accessor; + tooltipMarkup?: string | Accessor; + onClick?: () => void; +}; + +export { Page }; + +@register({ GTypeName: "Page" }) +class Page extends Gtk.Box { + readonly #id: string | number; + readonly bottomButtons?: Array; + + #subs: Array<() => void> = []; + #title: string | Accessor; + #description?: string | Accessor; + + 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).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( + + + + + {props.headerButtons && + { + (props.headerButtons instanceof Accessor) ? + + {(button) => button} + + : props.headerButtons + } + } + + + + + as Gtk.Box); + + this.append( + + {props.children} + as Gtk.Box); + + this.append( buttons.length > 0) + : (!props.bottomButtons ? false : props.bottomButtons.length > 0)} + /> as Gtk.Widget); + + this.append( + + {transformWidget(props.bottomButtons, (button) => + + + + + + )} + as Gtk.Box); + + this.onClose = props.onClose; + props.$?.(); + } +} + +function BottomButton(props: BottomButton) { + return + + + + + + as Gtk.Button; +} + +export function PageButton({ onDestroy, ...props }: { + class?: string | Accessor; + icon?: string | Accessor; + title: string | Accessor; + endWidget?: WidgetNodeType; + description?: string | Accessor; + extraButtons?: Array | WidgetNodeType; + onDestroy?: (self: Gtk.Box) => void; + onClick?: (self: Gtk.Button) => void; + tooltipText?: string | Accessor; + tooltipMarkup?: string | Accessor; +}) { + return + + + + {props.icon && } + + + + `${title.substring(0, 35)}${title.length > 35 ? '…' : ""}`) + } + /> + + + + + {props.endWidget && props.endWidget} + + + + + + {props.extraButtons} + + as Gtk.Box; +} diff --git a/ags/widget/control-center/pages/Sound.ts b/ags/widget/control-center/pages/Sound.ts deleted file mode 100644 index cd0d9fc..0000000 --- a/ags/widget/control-center/pages/Sound.ts +++ /dev/null @@ -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 = []; - - 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); -} diff --git a/ags/widget/control-center/pages/Sound.tsx b/ags/widget/control-center/pages/Sound.tsx new file mode 100644 index 0000000..9cc1b76 --- /dev/null +++ b/ags/widget/control-center/pages/Sound.tsx @@ -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 = () => + + + + + {(sink: AstalWp.Endpoint) => + + 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={ + + } + />} + + + + + {(stream: AstalWp.Stream) => + { + const conns: Map> = 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)) + )) + ]); + }}> + + + getAppIcon(name.split(' ')[0]) ?? "application-x-executable-symbolic")} + css={"font-size: 18px; margin-right: 6px;"} /> + + + + + + name ?? "Unnamed audio stream")} + ellipsize={Pango.EllipsizeMode.END} + tooltipText={createBinding(stream, "name")} + class={"name"} xalign={0} + /> + + + { + 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); + }} + /> + + + } + + as Page; diff --git a/ags/widget/control-center/tiles/Bluetooth.ts b/ags/widget/control-center/tiles/Bluetooth.ts deleted file mode 100644 index 655ab06..0000000 --- a/ags/widget/control-center/tiles/Bluetooth.ts +++ /dev/null @@ -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 = 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)(); -} diff --git a/ags/widget/control-center/tiles/Bluetooth.tsx b/ags/widget/control-center/tiles/Bluetooth.tsx new file mode 100644 index 0000000..3c1ad2b --- /dev/null +++ b/ags/widget/control-center/tiles/Bluetooth.tsx @@ -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 = () => + { + 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")} + />; diff --git a/ags/widget/control-center/tiles/DoNotDisturb.ts b/ags/widget/control-center/tiles/DoNotDisturb.ts deleted file mode 100644 index 5a08588..0000000 --- a/ags/widget/control-center/tiles/DoNotDisturb.ts +++ /dev/null @@ -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 -}); diff --git a/ags/widget/control-center/tiles/DoNotDisturb.tsx b/ags/widget/control-center/tiles/DoNotDisturb.tsx new file mode 100644 index 0000000..ed8e440 --- /dev/null +++ b/ags/widget/control-center/tiles/DoNotDisturb.tsx @@ -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 = () => + 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} + />; diff --git a/ags/widget/control-center/tiles/Network.ts b/ags/widget/control-center/tiles/Network.ts deleted file mode 100644 index a68daaa..0000000 --- a/ags/widget/control-center/tiles/Network.ts +++ /dev/null @@ -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); diff --git a/ags/widget/control-center/tiles/Network.tsx b/ags/widget/control-center/tiles/Network.tsx new file mode 100644 index 0000000..5dd44fd --- /dev/null +++ b/ags/widget/control-center/tiles/Network.tsx @@ -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 = () => + + + {([primary, wired, wifi]: [AstalNetwork.Primary, AstalNetwork.Wired, AstalNetwork.Wifi]) => { + if(primary === AstalNetwork.Primary.WIFI) { + return 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 { + 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 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)} + /> + }} + + as Gtk.Box; diff --git a/ags/widget/control-center/tiles/NightLight.ts b/ags/widget/control-center/tiles/NightLight.ts deleted file mode 100644 index dbbf1bc..0000000 --- a/ags/widget/control-center/tiles/NightLight.ts +++ /dev/null @@ -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); diff --git a/ags/widget/control-center/tiles/NightLight.tsx b/ags/widget/control-center/tiles/NightLight.tsx new file mode 100644 index 0000000..0ee5cc5 --- /dev/null +++ b/ags/widget/control-center/tiles/NightLight.tsx @@ -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 = () => + `${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)} + /> diff --git a/ags/widget/control-center/tiles/Recording.ts b/ags/widget/control-center/tiles/Recording.ts deleted file mode 100644 index 47c7826..0000000 --- a/ags/widget/control-center/tiles/Recording.ts +++ /dev/null @@ -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 = 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)(); -} diff --git a/ags/widget/control-center/tiles/Recording.tsx b/ags/widget/control-center/tiles/Recording.tsx new file mode 100644 index 0000000..32ba2c1 --- /dev/null +++ b/ags/widget/control-center/tiles/Recording.tsx @@ -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 = () => + { + 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; diff --git a/ags/widget/control-center/tiles/Tile.ts b/ags/widget/control-center/tiles/Tile.ts deleted file mode 100644 index 8e0af31..0000000 --- a/ags/widget/control-center/tiles/Tile.ts +++ /dev/null @@ -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; - icon?: string | Binding; - visible?: boolean | Binding; - iconSize?: number | Binding; - title: string | Binding; - description?: string | Binding; - toggleState?: boolean | Binding; - enableOnClickMore?: boolean | Binding; - onDestroy?: () => void; - onToggledOn: () => void; - onToggledOff: () => void; - onClickMore?: () => void; -} - -export function Tile(props: TileProps): (() => Gtk.Widget) { - const subs: Array<() => void> = []; - const toggled = new Variable(((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 - }) - ] - }); -} diff --git a/ags/widget/control-center/tiles/Tile.tsx b/ags/widget/control-center/tiles/Tile.tsx new file mode 100644 index 0000000..fba15ba --- /dev/null +++ b/ags/widget/control-center/tiles/Tile.tsx @@ -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; + icon?: string | Accessor; + visible?: boolean | Accessor; + iconSize?: number | Accessor; + title: string | Accessor; + description?: string | Accessor; + toggleState?: boolean | Accessor; + enableOnClickMore?: boolean | Accessor; + 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).get() ?? false)) + ); + + return + `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?.(); + }}> + + { + const gestureClick = Gtk.GestureClick.new(); + const conns: Map = 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?.(); + } + })); + }}> + + + {props.icon && `font-size: ${size}px;`) + : (props.iconSize ? + `font-size: ${props.iconSize ?? 16}px;` + : undefined) + } />} + + + + + + {props.description && str ?? "") + : (props.description ?? "") + } halign={Gtk.Align.START} + />} + + + + + + { + ((props.enableOnClickMore instanceof Accessor) ? + props.enableOnClickMore.get() + : props.enableOnClickMore) && props.onToggledOn?.(); + + props.onClickMore?.(); + }} tooltipText={tr("control_center.tiles.more")} /> + as Gtk.Widget; +}