diff --git a/.gitignore b/.gitignore index 329cd9f..91fcc75 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ node_modules/ -@girs/ +@types/ build/ pnpm-lock.yaml diff --git a/scripts/build.sh b/scripts/build.sh index d73dab5..db9790f 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -40,10 +40,6 @@ else mkdir -p $output fi -# link node_modules to src, so ags(esbuild) knows there are modules to bundle -echo "[info] linking modules" -ln -s node_modules src/node_modules - echo "[info] compiling gresource" gres_target=`[[ "$keep_gresource" ]] && echo -n "$output/resources.gresource" || \ echo -n "${gresources_target:-$output/resources.gresource}"` @@ -53,12 +49,9 @@ glib-compile-resources resources.gresource.xml \ --target "$gres_target" echo "[info] bundling project" -ags bundle src/app.ts $output/colorshell \ +ags --gtk 4 bundle src/app.ts $output/colorshell \ -r ./src \ -d "DEVEL=`[[ $is_devel ]] && echo -n true || echo -n false`" \ -d "COLORSHELL_VERSION='`cat package.json | jq -r .version`'" \ -d "GRESOURCES_FILE='${gresources_target:-$output/resources.gresource}'" \ || rm -rf src/node_modules - -echo "[info] cleaning" -rm -rf src/node_modules diff --git a/scripts/types.sh b/scripts/types.sh index 05229be..27234f1 100644 --- a/scripts/types.sh +++ b/scripts/types.sh @@ -5,4 +5,4 @@ fi echo "Building types, this can take long..." -pnpx @ts-for-gir/cli generate --ignoreVersionConflicts -o ./@girs +pnpx @ts-for-gir/cli generate --ignoreVersionConflicts diff --git a/src/app.ts b/src/app.ts index fc04ac1..ddcc993 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,7 +1,3 @@ -// fix ags needing --gtk 4 -// import app from "ags/gtk4/app"; - -// fix can't convert non-null pointer to JS value (thanks Aylur!) import "ags/overrides"; import "./config"; import { @@ -29,13 +25,13 @@ import { setConsoleLogDomain } from "console"; import { initPlayer } from "./modules/media"; import { createSubscription, encoder, secureBaseBinding } from "./modules/utils"; import { exec } from "ags/process"; +import { NightLight } from "./modules/nightlight"; import { Backlights } from "./modules/backlight"; import GObject, { register } from "ags/gobject"; import GLib from "gi://GLib?version=2.0"; import Gio from "gi://Gio?version=2.0"; import Adw from "gi://Adw?version=1"; -import { NightLight } from "./modules/nightlight"; const runnerPlugins: Array = [ diff --git a/src/config.ts b/src/config.ts index a2275fe..86535cf 100644 --- a/src/config.ts +++ b/src/config.ts @@ -48,11 +48,11 @@ const generalConfigDefaults = { const userDataDefaults = { /** last default adapter */ - bluetooth_default_adapter: undefined, + bluetooth_default_adapter: undefined as unknown as string, control_center: { /** last default backlight */ - default_backlight: undefined + default_backlight: undefined as unknown as string }, night_light: { @@ -75,5 +75,6 @@ export const userData = new Config< export const generalConfig = new Config( - `${GLib.get_user_config_dir()}/colorshell/config.json`, generalConfigDefaults + `${GLib.get_user_config_dir()}/colorshell/config.json`, + generalConfigDefaults ); diff --git a/src/i18n/lang/en_US.ts b/src/i18n/lang/en_US.ts index 1dc1f58..1d98732 100644 --- a/src/i18n/lang/en_US.ts +++ b/src/i18n/lang/en_US.ts @@ -20,7 +20,21 @@ export default { connect: "Connect", disconnect: "Disconnect", + copy_to_clipboard: "Copy to clipboard", + media: { + play: "Play", + pause: "Pause", + next: "Next", + previous: "Previous", + loop: "Loop", + no_loop: "No loop", + song_loop: "Loop song", + shuffle_order: "Shuffle", + follow_order: "Follow order", + no_artist: "No artist", + no_title: "No title" + }, control_center: { tiles: { enabled: "Enabled", @@ -88,4 +102,4 @@ export default { ask_popup: { title: "Question" } -} as i18nStruct; +} satisfies i18nStruct; diff --git a/src/i18n/lang/pt_BR.ts b/src/i18n/lang/pt_BR.ts index 1b6ae4e..67805a3 100644 --- a/src/i18n/lang/pt_BR.ts +++ b/src/i18n/lang/pt_BR.ts @@ -20,7 +20,21 @@ export default { apps: "Aplicativos", clear: "Limpar", + copy_to_clipboard: "Copiar para a Área de Transferência", + media: { + next: "Próxima faixa", + pause: "Pausar", + play: "Tocar", + previous: "Faixa anterior", + loop: "Repetir", + no_loop: "Não repetir", + song_loop: "Repetir faixa", + follow_order: "Seguir ordem", + shuffle_order: "Ordem aleatória", + no_title: "Sem título", + no_artist: "Sem artista" + }, control_center: { tiles: { enabled: "Ligado", @@ -88,4 +102,4 @@ export default { ask_popup: { title: "Pergunta" } -} as i18nStruct; +} satisfies i18nStruct; diff --git a/src/i18n/struct.ts b/src/i18n/struct.ts index 0e9e98e..5a60843 100644 --- a/src/i18n/struct.ts +++ b/src/i18n/struct.ts @@ -17,9 +17,23 @@ export type i18nStruct = { disconnect: string, connect: string, - apps: string; - clear: string; + apps: string, + clear: string, + copy_to_clipboard: string, + media: { + loop: string, + song_loop: string, + no_loop: string, + shuffle_order: string, + follow_order: string, + pause: string, + play: string, + next: string, + previous: string, + no_artist: string, + no_title: string + }, control_center: { tiles: { enabled: string, diff --git a/src/modules/bluetooth.ts b/src/modules/bluetooth.ts index 98e5677..d660e3a 100644 --- a/src/modules/bluetooth.ts +++ b/src/modules/bluetooth.ts @@ -9,6 +9,14 @@ import AstalBluetooth from "gi://AstalBluetooth"; /** AstalBluetooth helper (implements the default adapter feature) */ @register({ GTypeName: "Bluetooth" }) export class Bluetooth extends GObject.Object { + declare $signals: { + "notify": () => void; + "notify::adapter": (adapter: AstalBluetooth.Adapter|null) => void; + "notify::is-available": (available: boolean) => void; + "notify::save-default-adapter": (save: boolean) => void; + "notify::last-device": (device: AstalBluetooth.Device|null) => void; + }; + private static instance: Bluetooth; private astalBl = AstalBluetooth.get_default(); @@ -16,12 +24,18 @@ export class Bluetooth extends GObject.Object { #adapter: AstalBluetooth.Adapter|null = this.astalBl.adapter ?? null; #scope!: Scope; #isAvailable: boolean = false; + #lastDevice: AstalBluetooth.Device|null = null; + + @property(Boolean) + saveDefaultAdapter: boolean = true; @getter(Boolean) get isAvailable() { return this.#isAvailable; } - @property(Boolean) saveDefaultAdapter = true; - + /** last connected device, can be null */ + @getter(AstalBluetooth.Device) + get lastDevice() { return this.#lastDevice!; } + @getter(gtype(AstalBluetooth.Adapter)) get adapter() { return this.#adapter; } @@ -94,6 +108,16 @@ export class Bluetooth extends GObject.Object { ] ); + this.#lastDevice = this.getLastConnectedDevice(); + this.notify("last-device"); + + this.#connections.set(AstalBluetooth.get_default(), [ + AstalBluetooth.get_default().connect("notify::devices", (_) => { + this.#lastDevice = this.getLastConnectedDevice(); + this.notify("last-device"); + }) + ]); + this.#scope.onCleanup(() => this.#connections.forEach((ids, gobj) => Array.isArray(ids) ? ids.forEach(id => gobj.disconnect(id)) @@ -112,4 +136,22 @@ export class Bluetooth extends GObject.Object { vfunc_dispose(): void { this.#scope.dispose(); } + + private getLastConnectedDevice(): AstalBluetooth.Device|null { + + const connectedDevices = AstalBluetooth.get_default().devices + .filter(d => d.connected); + + const lastDevice = connectedDevices[connectedDevices.length - 1]; + + console.log(`last device: ${lastDevice?.address}`); + + return lastDevice ?? null; + } + + connect( + signal: Signal, callback: (typeof this["$signals"])[Signal] + ): number { + return super.connect(signal as string, callback as () => void); + } } diff --git a/src/modules/compositor.ts b/src/modules/compositor.ts deleted file mode 100644 index 9b100d0..0000000 --- a/src/modules/compositor.ts +++ /dev/null @@ -1,71 +0,0 @@ -import GLib from "gi://GLib?version=2.0"; - -import GObject, { getter, property, register } from "ags/gobject"; - - -/** WIP Global implementation of a system that supports -* a variety of Wayland Compositors */ -export namespace Compositor { - -let instance: _Compositor; - -@register({ GTypeName: "CompositorMonitor" }) -class _CompositorMonitor extends GObject.Object { - public readonly width: number; - public readonly height: number; - - @property(Boolean) - public readonly mirror: boolean; - - constructor(width: number, height: number, mirror: boolean = false) { - super(); - - this.width = width; - this.height = height; - this.mirror = mirror; - } -} - -@register({ GTypeName: "CompositorWorkspace" }) -class _CompositorWorkspace extends GObject.Object { - public readonly id: number; - - @getter(_CompositorMonitor) - public readonly monitor: _CompositorMonitor; - - constructor(monitor: _CompositorMonitor, id: number) { - super(); - - this.monitor = monitor; - this.id = id; - } -} - -@register({ GTypeName: "Compositor" }) -class _Compositor extends GObject.Object { - #workspaces: Array<_CompositorWorkspace> = []; - - @property() - public get workspaces() { return this.#workspaces; } -}; - - -export function getDefault(): _Compositor { - if(!instance) - instance = new _Compositor(); - - return instance; -} - -export const Compositor = _Compositor, - CompositorWorkspace = _CompositorWorkspace, - CompositorMonitor = _CompositorMonitor; - -/** Uses the XDG_CURRENT_DESKTOP variable to detect running compositor's name. - * --- - * @returns running wayland compositor's name (lowercase) or `undefined` if variable's not set */ -export function getName(): string|undefined { - return GLib.getenv("XDG_CURRENT_DESKTOP") ?? undefined; -} - -} diff --git a/src/modules/config.ts b/src/modules/config.ts index c9fd9b2..e79fa7b 100644 --- a/src/modules/config.ts +++ b/src/modules/config.ts @@ -2,7 +2,7 @@ import { timeout } from "ags/time"; import { monitorFile, readFileAsync, writeFileAsync } from "ags/file"; import { Notifications } from "./notifications"; import { Accessor } from "ags"; -import GObject, { getter, ParamSpec, register } from "ags/gobject"; +import GObject, { getter, gtype, register } from "ags/gobject"; import Gio from "gi://Gio?version=2.0"; import AstalIO from "gi://AstalIO"; @@ -13,7 +13,7 @@ export { Config }; type ValueTypes = "string" | "boolean" | "object" | "number" | "any"; @register({ GTypeName: "Config" }) -class Config, V extends string|object|any> extends GObject.Object { +class Config extends GObject.Object { declare $signals: GObject.Object.SignalSignatures & { "notify::entries": (entries: Record) => void; }; @@ -22,7 +22,7 @@ class Config, V extends string|objec * in the `entries` field */ public readonly defaults: Record; - @getter(Object as unknown as ParamSpec>) + @getter(gtype>(Object)) public get entries() { return this.#entries; } #file: Gio.File; diff --git a/src/window/bar/widgets/Media.tsx b/src/window/bar/widgets/Media.tsx index f3c1f8f..f6fbf17 100644 --- a/src/window/bar/widgets/Media.tsx +++ b/src/window/bar/widgets/Media.tsx @@ -9,6 +9,7 @@ import { accessMediaUrl, player, setPlayer } from "../../../modules/media"; import GObject from "ags/gobject"; import AstalMpris from "gi://AstalMpris"; import Pango from "gi://Pango?version=1.0"; +import { tr } from "../../../i18n/intl"; export const Media = () => { @@ -66,12 +67,12 @@ export const Media = () => { createBinding(player.get(), "busName").as(getPlayerIconFromBusName)} /> - title ?? "No Title")} maxWidthChars={20} ellipsize={Pango.EllipsizeMode.END} + title ?? tr("media.no_title"))} maxWidthChars={20} ellipsize={Pango.EllipsizeMode.END} /> - artist ?? "No Artist")} maxWidthChars={18} ellipsize={Pango.EllipsizeMode.END} + artist ?? tr("media.no_artist"))} maxWidthChars={18} ellipsize={Pango.EllipsizeMode.END} /> } @@ -84,7 +85,7 @@ export const Media = () => { { + tooltipText={tr("copy_to_clipboard")} onClicked={() => { const url = accessMediaUrl(player.get()).get(); url && Clipboard.getDefault().copyAsync(url); }} @@ -92,20 +93,21 @@ export const Media = () => { + tooltipText={tr("media.previous")} onClicked={() => player.get().canGoPrevious && player.get().previous()} /> status === AstalMpris.PlaybackStatus.PAUSED ? "media-playback-start-symbolic" : "media-playback-pause-symbolic")} - tooltipText={ - createBinding(player.get(), "playbackStatus").as(status => - status === AstalMpris.PlaybackStatus.PAUSED ? "Play" : "Pause") - } onClicked={() => player.get().play_pause()} + tooltipText={createBinding(player.get(), "playbackStatus").as(status => + status === AstalMpris.PlaybackStatus.PAUSED ? + tr("media.play") + : tr("media.pause") + )} onClicked={() => player.get().play_pause()} /> player.get().canGoNext && + tooltipText={tr("media.next")} onClicked={() => player.get().canGoNext && player.get().next()} /> diff --git a/src/window/center-window/widgets/BigMedia.tsx b/src/window/center-window/widgets/BigMedia.tsx index adeb85c..2155cf4 100644 --- a/src/window/center-window/widgets/BigMedia.tsx +++ b/src/window/center-window/widgets/BigMedia.tsx @@ -5,6 +5,7 @@ import { Clipboard } from "../../../modules/clipboard"; import { accessMediaUrl } from "../../../modules/media"; import { player, setPlayer } from "../../../modules/media"; import { pathToURI, variableToBoolean } from "../../../modules/utils"; +import { tr } from "../../../i18n/intl"; import AstalMpris from "gi://AstalMpris"; import Pango from "gi://Pango?version=1.0"; @@ -31,7 +32,7 @@ export const BigMedia = () => { as Adw.Carousel; - return {carousel} @@ -75,15 +76,15 @@ class PlayerWidget extends Gtk.Box { valign={Gtk.Align.CENTER} vexpand hexpand> title ?? "No Title") + createBinding(player, "title").as(title => title ?? tr("media.no_title")) } label={ - createBinding(player, "title").as(title => title ?? "No Title") + createBinding(player, "title").as(title => title ?? tr("media.no_title")) } ellipsize={Pango.EllipsizeMode.END} maxWidthChars={25} /> artist ?? "No Artist") + createBinding(player, "artist").as(artist => artist ?? tr("media.no_artist")) } label={ - createBinding(player, "artist").as(artist => artist ?? "No Artist") + createBinding(player, "artist").as(artist => artist ?? tr("media.no_artist")) } ellipsize={Pango.EllipsizeMode.END} maxWidthChars={28} /> as Gtk.Box @@ -128,7 +129,7 @@ class PlayerWidget extends Gtk.Box { { const url = accessMediaUrl(player).get(); @@ -180,22 +181,22 @@ class PlayerWidget extends Gtk.Box { "media-playlist-shuffle-symbolic" : "media-playlist-consecutive-symbolic")} tooltipText={ createBinding(player, "shuffleStatus").as(status => status === AstalMpris.Shuffle.ON ? - "Shuffle" - : "No shuffle")} onClicked={() => player.shuffle()} + tr("media.shuffle") + : tr("media.follow_order"))} onClicked={() => player.shuffle()} /> player.canGoPrevious && player.previous()} + tooltipText={tr("media.previous")} onClicked={() => player.canGoPrevious && player.previous()} /> - status === AstalMpris.PlaybackStatus.PLAYING ? "Pause" : "Play")} + status === AstalMpris.PlaybackStatus.PLAYING ? tr("media.pause") : tr("media.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()} + tooltipText={tr("media.next")} onClicked={() => player.canGoNext && player.next()} /> { if(status === AstalMpris.Loop.TRACK) @@ -209,12 +210,12 @@ class PlayerWidget extends Gtk.Box { status !== AstalMpris.Loop.UNSUPPORTED)} tooltipText={createBinding(player, "loopStatus").as(status => { if(status === AstalMpris.Loop.TRACK) - return "Loop song"; + return tr("media.song_loop"); if(status === AstalMpris.Loop.PLAYLIST) - return "Loop playlist"; + return tr("media.loop"); - return "No loop"; + return tr("media.no_loop"); })} onClicked={() => player.loop()} /> diff --git a/tsconfig.json b/tsconfig.json index dec454e..f8a1697 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "moduleResolution": "bundler", "skipLibCheck": true, "types": [ - "./@girs" + "./@types" ], "strict": true, "jsx": "react-jsx",