✨ feat(control-center/tiles): update tiles look and structure
This commit is contained in:
+50
-21
@@ -10,6 +10,7 @@ import { generalConfig, Shell } from "../app";
|
||||
|
||||
import AstalIO from "gi://AstalIO";
|
||||
import AstalMpris from "gi://AstalMpris";
|
||||
import { Gtk } from "ags/gtk4";
|
||||
|
||||
|
||||
export type RemoteCaller = {
|
||||
@@ -21,30 +22,33 @@ let wsTimeout: AstalIO.Time|undefined;
|
||||
const help = `Manage Astal Windows and do more stuff. From retrozinndev's colorshell, \
|
||||
made using GTK4, AGS, Gnim and Astal libraries by Aylur.
|
||||
|
||||
Window Management:
|
||||
open [window]: opens the specified window.
|
||||
close [window]: closes all instances of specified window.
|
||||
toggle [window]: toggle-open/close the specified window.
|
||||
windows: list shell windows and their respective status.
|
||||
reload: quit this instance and start a new one.
|
||||
reopen: restart all open-windows.
|
||||
quit: exit the main instance of the shell.
|
||||
Window Management:
|
||||
open [window]: opens the specified window.
|
||||
close [window]: closes all instances of specified window.
|
||||
toggle [window]: toggle-open/close the specified window.
|
||||
windows: list shell windows and their respective status.
|
||||
reload: quit this instance and start a new one.
|
||||
reopen: restart all open-windows.
|
||||
quit: exit the main instance of the shell.
|
||||
|
||||
Audio Controls:
|
||||
volume: speaker and microphone volume controller, see "volume help".
|
||||
Audio Controls:
|
||||
volume: speaker and microphone volume controller, see "volume help".
|
||||
|
||||
Media Controls:
|
||||
media: manage colorshell's active player, see "media help".
|
||||
|
||||
Other options:
|
||||
runner [initial_text]: open the application runner, optionally add an initial search.
|
||||
peek-workspace-num [millis]: peek the workspace numbers on bar window.
|
||||
v, version: display current colorshell version.
|
||||
h, help: shows this help message.
|
||||
Media Controls:
|
||||
media: manage colorshell's active player, see "media help".
|
||||
${DEVEL ? `
|
||||
Development Tools:
|
||||
dev: tools to help debugging colorshell
|
||||
` : ""}
|
||||
Other options:
|
||||
runner [initial_text]: open the application runner, optionally add an initial search.
|
||||
peek-workspace-num [millis]: peek the workspace numbers on bar window.
|
||||
v, version: display current colorshell version.
|
||||
h, help: shows this help message.
|
||||
|
||||
2025 (c) retrozinndev's colorshell, licensed under the MIT License.
|
||||
https://github.com/retrozinndev/colorshell
|
||||
`.split('\n').map(l => l.replace(/^ {8}/, "")).join('\n');
|
||||
2025 (c) retrozinndev's colorshell, licensed under the MIT License.
|
||||
https://github.com/retrozinndev/colorshell
|
||||
`.trim();
|
||||
|
||||
export function handleArguments(cmd: RemoteCaller, args: Array<string>): number {
|
||||
switch(args[0]) {
|
||||
@@ -59,6 +63,9 @@ export function handleArguments(cmd: RemoteCaller, args: Array<string>): number
|
||||
}${DEVEL ? " (devel)" : ""}\nhttps://github.com/retrozinndev/colorshell`);
|
||||
return 0;
|
||||
|
||||
case "dev":
|
||||
return handleDevArgs(cmd, args);
|
||||
|
||||
case "open":
|
||||
case "close":
|
||||
case "toggle":
|
||||
@@ -115,6 +122,28 @@ export function handleArguments(cmd: RemoteCaller, args: Array<string>): number
|
||||
return 1;
|
||||
}
|
||||
|
||||
function handleDevArgs(cmd: RemoteCaller, args: Array<string>): number {
|
||||
if(/h|help/.test(args[1])) {
|
||||
cmd.print_literal(`
|
||||
Debugging tools for colorshell.
|
||||
|
||||
Options:
|
||||
inspector: open GTK's visual debugger
|
||||
`.trim());
|
||||
return 0;
|
||||
}
|
||||
|
||||
switch(args[1]) {
|
||||
case "inspector":
|
||||
cmd.print_literal("Opening inspector...");
|
||||
Gtk.Window.set_interactive_debugging(true);
|
||||
return 0;
|
||||
}
|
||||
|
||||
cmd.printerr_literal("Error: command not found! try checking `dev help`");
|
||||
return 1;
|
||||
}
|
||||
|
||||
function handleMediaArgs(cmd: RemoteCaller, args: Array<string>): number {
|
||||
if(/h|help/.test(args[1])) {
|
||||
const mediaHelp = `
|
||||
|
||||
@@ -7,16 +7,17 @@ import { createBinding, createComputed } from "ags";
|
||||
|
||||
export const TileBluetooth = () =>
|
||||
<Tile title={"Bluetooth"} visible={
|
||||
createBinding(AstalBluetooth.get_default(), "adapter").as(Boolean)
|
||||
} description={createBinding(AstalBluetooth.get_default(), "isConnected").as((connected) => {
|
||||
const connectedDev = AstalBluetooth.get_default().devices.filter(dev => dev.connected)?.[0];
|
||||
return connected && connectedDev ? connectedDev.get_alias() : ""
|
||||
})} onToggledOn={() => AstalBluetooth.get_default().adapter?.set_powered(true)}
|
||||
onToggledOff={() => AstalBluetooth.get_default().adapter?.set_powered(false)}
|
||||
onClickMore={() => TilesPages?.toggle(BluetoothPage)}
|
||||
enableOnClickMore={true} iconSize={16}
|
||||
toggleState={createBinding(AstalBluetooth.get_default(), "isPowered")}
|
||||
icon={createComputed([
|
||||
createBinding(AstalBluetooth.get_default(), "adapter").as(Boolean)
|
||||
} description={createBinding(AstalBluetooth.get_default(), "isConnected").as((connected) => {
|
||||
const connectedDev = AstalBluetooth.get_default().devices.filter(dev => dev.connected)?.[0];
|
||||
return connected && connectedDev ? connectedDev.get_alias() : ""
|
||||
})}
|
||||
onEnabled={() => AstalBluetooth.get_default().adapter?.set_powered(true)}
|
||||
onDisabled={() => AstalBluetooth.get_default().adapter?.set_powered(false)}
|
||||
onClicked={() => TilesPages?.toggle(BluetoothPage)}
|
||||
enableOnClicked hasArrow
|
||||
state={createBinding(AstalBluetooth.get_default(), "isPowered")}
|
||||
icon={createComputed([
|
||||
createBinding(AstalBluetooth.get_default(), "isPowered"),
|
||||
createBinding(AstalBluetooth.get_default(), "isConnected")
|
||||
],
|
||||
@@ -24,5 +25,6 @@ export const TileBluetooth = () =>
|
||||
powered ? ( isConnected ?
|
||||
"bluetooth-active-symbolic"
|
||||
: "bluetooth-symbolic"
|
||||
) : "bluetooth-disabled-symbolic")}
|
||||
) : "bluetooth-disabled-symbolic")
|
||||
}
|
||||
/>;
|
||||
|
||||
@@ -7,9 +7,8 @@ export const TileDND = () =>
|
||||
<Tile title={tr("control_center.tiles.dnd.title")}
|
||||
description={createBinding(Notifications.getDefault().getNotifd(), "dontDisturb").as(
|
||||
(dnd: boolean) => dnd ? tr("control_center.tiles.enabled") : tr("control_center.tiles.disabled"))}
|
||||
onToggledOff={() => Notifications.getDefault().getNotifd().dontDisturb = false}
|
||||
onToggledOn={() => Notifications.getDefault().getNotifd().dontDisturb = true}
|
||||
onDisabled={() => Notifications.getDefault().getNotifd().dontDisturb = false}
|
||||
onEnabled={() => Notifications.getDefault().getNotifd().dontDisturb = true}
|
||||
icon={"minus-circle-filled-symbolic"}
|
||||
iconSize={16}
|
||||
toggleState={Notifications.getDefault().getNotifd().dontDisturb}
|
||||
state={Notifications.getDefault().getNotifd().dontDisturb}
|
||||
/>;
|
||||
|
||||
@@ -29,11 +29,11 @@ export const TileNetwork = () => <Gtk.Box>
|
||||
return tr("connecting") + "...";
|
||||
}
|
||||
})()
|
||||
)} onToggledOn={() => wifi.set_enabled(true)}
|
||||
onToggledOff={() => wifi.set_enabled(false)}
|
||||
onClickMore={() => TilesPages?.toggle(PageNetwork)}
|
||||
)} onEnabled={() => wifi.set_enabled(true)}
|
||||
onDisabled={() => wifi.set_enabled(false)}
|
||||
hasArrow onClicked={() => TilesPages?.toggle(PageNetwork)}
|
||||
icon={"network-wireless-signal-excellent-symbolic"}
|
||||
toggleState={createBinding(wifi, "enabled")}
|
||||
state={createBinding(wifi, "enabled")}
|
||||
/>
|
||||
|
||||
} else if(primary === AstalNetwork.Primary.WIRED) {
|
||||
@@ -48,9 +48,9 @@ export const TileNetwork = () => <Gtk.Box>
|
||||
return tr("connecting") + "...";
|
||||
}
|
||||
})}
|
||||
onToggledOn={() => execAsync("nmcli n on")}
|
||||
onToggledOff={() => execAsync("nmcli n off")}
|
||||
onClickMore={() => TilesPages?.toggle(PageNetwork)}
|
||||
hasArrow onEnabled={() => execAsync("nmcli n on")}
|
||||
onDisabled={() => execAsync("nmcli n off")}
|
||||
onClicked={() => TilesPages?.toggle(PageNetwork)}
|
||||
icon={createBinding(wired, "internet").as((internet: AstalNetwork.Internet) => {
|
||||
switch(internet) {
|
||||
case AstalNetwork.Internet.CONNECTED:
|
||||
@@ -61,8 +61,7 @@ export const TileNetwork = () => <Gtk.Box>
|
||||
|
||||
return "network-wired-no-route-symbolic";
|
||||
})}
|
||||
iconSize={16}
|
||||
toggleState={createBinding(wired, "internet").as((internet: AstalNetwork.Internet) =>
|
||||
state={createBinding(wired, "internet").as((internet: AstalNetwork.Internet) =>
|
||||
internet === AstalNetwork.Internet.CONNECTING
|
||||
|| internet === AstalNetwork.Internet.CONNECTED
|
||||
)}
|
||||
@@ -72,12 +71,11 @@ export const TileNetwork = () => <Gtk.Box>
|
||||
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)}
|
||||
onEnabled={() => execAsync("nmcli n on")}
|
||||
onDisabled={() => execAsync("nmcli n off")}
|
||||
hasArrow onClicked={() => TilesPages?.toggle(PageNetwork)}
|
||||
icon={"network-wired-disconnected-symbolic"}
|
||||
iconSize={16}
|
||||
toggleState={createBinding(wired, "internet").as((internet: AstalNetwork.Internet) =>
|
||||
state={createBinding(wired, "internet").as((internet: AstalNetwork.Internet) =>
|
||||
internet === AstalNetwork.Internet.CONNECTING || internet === AstalNetwork.Internet.CONNECTED)}
|
||||
/>
|
||||
}}
|
||||
|
||||
@@ -16,10 +16,10 @@ export const TileNightLight = () =>
|
||||
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)}
|
||||
hasArrow visible={isInstalled("hyprsunset")}
|
||||
onDisabled={() => NightLight.getDefault().identity = true}
|
||||
onEnabled={() => NightLight.getDefault().identity = false}
|
||||
enableOnClicked
|
||||
onClicked={() => TilesPages?.toggle(PageNightLight)}
|
||||
state={createBinding(NightLight.getDefault(), "identity").as(identity => !identity)}
|
||||
/>
|
||||
|
||||
@@ -24,8 +24,7 @@ export const TileRecording = () =>
|
||||
})}
|
||||
icon={"media-record-symbolic"}
|
||||
visible={isInstalled("wf-recorder")}
|
||||
onToggledOff={() => Recording.getDefault().stopRecording()}
|
||||
onToggledOn={() => Recording.getDefault().startRecording()}
|
||||
toggleState={createBinding(Recording.getDefault(), "recording")}
|
||||
iconSize={16}
|
||||
onDisabled={() => Recording.getDefault().stopRecording()}
|
||||
onEnabled={() => Recording.getDefault().startRecording()}
|
||||
state={createBinding(Recording.getDefault(), "recording")}
|
||||
/>;
|
||||
|
||||
@@ -1,30 +1,13 @@
|
||||
import { Gtk } from "ags/gtk4";
|
||||
import { tr } from "../../../i18n/intl";
|
||||
import { Accessor, createBinding, createComputed, createState, getScope, onCleanup } from "ags";
|
||||
import { createBinding } from "ags";
|
||||
import { omitObjectKeys, variableToBoolean } from "../../../modules/utils";
|
||||
import GObject, { property, register, signal } from "ags/gobject";
|
||||
|
||||
import { property, register, signal } from "ags/gobject";
|
||||
import Pango from "gi://Pango?version=1.0";
|
||||
|
||||
|
||||
export { Tile, TileProps };
|
||||
export { Tile };
|
||||
|
||||
type TileProps = {
|
||||
class?: string | Accessor<string>;
|
||||
icon?: string | Accessor<string>;
|
||||
visible?: boolean | Accessor<boolean>;
|
||||
iconSize?: number | Accessor<number>;
|
||||
title: string | Accessor<string>;
|
||||
description?: string | Accessor<string>;
|
||||
toggleState?: boolean | Accessor<boolean>;
|
||||
enableOnClickMore?: boolean | Accessor<boolean>;
|
||||
onUnmap?: () => void;
|
||||
onToggledOn: () => void;
|
||||
onToggledOff: () => void;
|
||||
onClickMore?: () => void;
|
||||
};
|
||||
|
||||
/* TODO: finish the tile class
|
||||
@register({ GTypeName: "Tile" })
|
||||
class Tile extends Gtk.Box {
|
||||
@signal(Boolean) toggled(_state: boolean) {}
|
||||
@@ -42,6 +25,8 @@ class Tile extends Gtk.Box {
|
||||
public enableOnClicked: boolean = true;
|
||||
@property(Boolean)
|
||||
public state: boolean = false;
|
||||
@property(Boolean)
|
||||
public hasArrow: boolean = false;
|
||||
|
||||
declare $signals: Gtk.Box.SignalSignatures & {
|
||||
"toggled": (_state: boolean) => void;
|
||||
@@ -53,25 +38,29 @@ class Tile extends Gtk.Box {
|
||||
public enable(): void {
|
||||
if(this.state) return;
|
||||
|
||||
this.state = true;
|
||||
!this.has_css_class("enabled") &&
|
||||
this.add_css_class("enabled");
|
||||
this.emit("toggled", true);
|
||||
this.emit("enabled");
|
||||
this.state = true;
|
||||
}
|
||||
|
||||
public disable(): void {
|
||||
if(!this.state) return;
|
||||
|
||||
this.state = false;
|
||||
this.remove_css_class("enabled");
|
||||
this.emit("toggled", false);
|
||||
this.emit("disabled");
|
||||
this.state = false;
|
||||
}
|
||||
|
||||
constructor(props: Omit<Gtk.Box.ConstructorProps, "orientation"> & {
|
||||
constructor(props: Partial<Omit<Gtk.Box.ConstructorProps, "orientation">> & {
|
||||
icon: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
state?: boolean;
|
||||
enableOnClicked?: boolean;
|
||||
hasArrow?: boolean;
|
||||
}) {
|
||||
super(omitObjectKeys(props, [
|
||||
"icon",
|
||||
@@ -80,10 +69,15 @@ class Tile extends Gtk.Box {
|
||||
"state",
|
||||
"enableOnClicked"
|
||||
]));
|
||||
|
||||
|
||||
this.add_css_class("tile");
|
||||
|
||||
this.icon = props.icon;
|
||||
this.title = props.title;
|
||||
|
||||
if(props.hasArrow != null)
|
||||
this.hasArrow = props.hasArrow;
|
||||
|
||||
if(props.description != null)
|
||||
this.description = props.description;
|
||||
|
||||
@@ -93,32 +87,52 @@ class Tile extends Gtk.Box {
|
||||
if(props.enableOnClicked != null)
|
||||
this.enableOnClicked = props.enableOnClicked;
|
||||
|
||||
const connections = new Map<GObject.Object, number>();
|
||||
const gestureClick = Gtk.GestureClick.new();
|
||||
|
||||
this.add_controller(gestureClick);
|
||||
|
||||
connections.set(gestureClick, gestureClick.connect("released", () => {
|
||||
this.emit("clicked");
|
||||
if(this.enableOnClicked && !this.state)
|
||||
this.enable();
|
||||
return true;
|
||||
}));
|
||||
if(this.state)
|
||||
this.add_css_class("enabled"); // fix no highlight with state = true on construct
|
||||
|
||||
this.prepend(
|
||||
<Gtk.Box hexpand={false} vexpand>
|
||||
<Gtk.Image iconName={createBinding(this, "icon")} />
|
||||
<Gtk.Box hexpand={false} vexpand class={"icon"}>
|
||||
<Gtk.Image iconName={createBinding(this, "icon")} halign={Gtk.Align.CENTER} />
|
||||
<Gtk.GestureClick onReleased={() => {
|
||||
this.state ? this.disable() : this.enable();
|
||||
}} />
|
||||
</Gtk.Box> as Gtk.Box
|
||||
);
|
||||
|
||||
this.append(
|
||||
<Gtk.Box class={"content"} orientation={Gtk.Orientation.VERTICAL}>
|
||||
<Gtk.Label class={"title"} label={createBinding(this, "title")} />
|
||||
<Gtk.Label class={"description"} label={createBinding(this, "description")} />
|
||||
<Gtk.Box class={"content"} orientation={Gtk.Orientation.VERTICAL} vexpand
|
||||
valign={Gtk.Align.CENTER} hexpand>
|
||||
|
||||
<Gtk.Label class={"title"} label={createBinding(this, "title")}
|
||||
xalign={0} ellipsize={Pango.EllipsizeMode.END} />
|
||||
<Gtk.Label class={"description"} label={createBinding(this, "description")}
|
||||
xalign={0} ellipsize={Pango.EllipsizeMode.END} visible={
|
||||
variableToBoolean(createBinding(this, "description"))
|
||||
}
|
||||
/>
|
||||
|
||||
<Gtk.GestureClick onReleased={() => {
|
||||
this.emit("clicked");
|
||||
if(this.enableOnClicked && !this.state)
|
||||
this.enable();
|
||||
|
||||
return true;
|
||||
}} />
|
||||
</Gtk.Box> as Gtk.Box
|
||||
);
|
||||
|
||||
getScope()?.onCleanup(() => connections.forEach((id, obj) => obj.disconnect(id)));
|
||||
if(this.hasArrow)
|
||||
this.append(
|
||||
<Gtk.Image class={"arrow"} iconName={"go-next-symbolic"}>
|
||||
<Gtk.GestureClick onReleased={() => {
|
||||
this.emit("clicked");
|
||||
if(this.enableOnClicked && !this.state)
|
||||
this.enable();
|
||||
|
||||
return true;
|
||||
}} />
|
||||
</Gtk.Image> as Gtk.Image
|
||||
);
|
||||
}
|
||||
|
||||
emit<Signal extends keyof typeof this.$signals>(
|
||||
@@ -135,81 +149,3 @@ class Tile extends Gtk.Box {
|
||||
return super.connect(signal, callback);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
function Tile(props: TileProps): Gtk.Widget {
|
||||
const subs: Array<() => void> = [];
|
||||
const [toggled, setToggled] = createState(((props.toggleState instanceof Accessor) ?
|
||||
props.toggleState.get()
|
||||
: props.toggleState) ?? false);
|
||||
|
||||
|
||||
(props.toggleState instanceof Accessor) && subs.push(
|
||||
props.toggleState.subscribe(() =>
|
||||
setToggled((props.toggleState as Accessor<boolean>).get() ?? false))
|
||||
);
|
||||
|
||||
onCleanup(() => subs.forEach(s => s()));
|
||||
|
||||
return <Gtk.Box hexpand visible={props.visible} onUnmap={props.onUnmap}
|
||||
canFocus focusable={false} class={
|
||||
(props.class instanceof Accessor) ?
|
||||
createComputed([props.class, toggled], (clss, isToggled) =>
|
||||
`tile ${clss} ${isToggled ? "toggled" : ""} ${
|
||||
props.onClickMore ? "has-more" : ""
|
||||
}`
|
||||
)
|
||||
: toggled.as(isToggled =>
|
||||
`tile ${props.class ? props.class : ""} ${isToggled ? "toggled" : ""} ${
|
||||
props.onClickMore ? "has-more" : ""
|
||||
}`
|
||||
)
|
||||
}>
|
||||
<Gtk.Button class={"toggle-button"} onClicked={() => {
|
||||
if(toggled.get()) {
|
||||
setToggled(false);
|
||||
props.onToggledOff?.();
|
||||
return;
|
||||
}
|
||||
|
||||
setToggled(true);
|
||||
props.onToggledOn?.();
|
||||
}}>
|
||||
|
||||
<Gtk.Box class={"content"} hexpand={true} vexpand={true}>
|
||||
{props.icon && <Gtk.Image class={"icon"} iconName={props.icon} css={
|
||||
(props.iconSize instanceof Accessor) ?
|
||||
props.iconSize.as(size => `font-size: ${size}px;`)
|
||||
: (props.iconSize ?
|
||||
`font-size: ${props.iconSize ?? 16}px;`
|
||||
: undefined)
|
||||
} />}
|
||||
|
||||
<Gtk.Box orientation={Gtk.Orientation.VERTICAL} class={"text"} vexpand={true} hexpand={true}
|
||||
valign={Gtk.Align.CENTER}>
|
||||
|
||||
<Gtk.Label class={"title"} xalign={0} halign={Gtk.Align.START} ellipsize={Pango.EllipsizeMode.END}
|
||||
label={props.title} />
|
||||
|
||||
{props.description && <Gtk.Label class={"description"} ellipsize={Pango.EllipsizeMode.END}
|
||||
visible={variableToBoolean(props.description)} xalign={0} label={
|
||||
(props.description instanceof Accessor) ?
|
||||
props.description.as(str => str ?? "")
|
||||
: (props.description ?? "")
|
||||
} halign={Gtk.Align.START}
|
||||
/>}
|
||||
|
||||
</Gtk.Box>
|
||||
</Gtk.Box>
|
||||
</Gtk.Button>
|
||||
|
||||
<Gtk.Button class={"more icon"} iconName={"go-next-symbolic"} widthRequest={32}
|
||||
visible={Boolean(props.onClickMore)} halign={Gtk.Align.END} onClicked={() => {
|
||||
((props.enableOnClickMore instanceof Accessor) ?
|
||||
props.enableOnClickMore.get()
|
||||
: props.enableOnClickMore) && props.onToggledOn?.();
|
||||
|
||||
props.onClickMore?.();
|
||||
}} tooltipText={tr("control_center.tiles.more")} />
|
||||
</Gtk.Box> as Gtk.Widget;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user