🌐 feat(i18n): add translations for media controls
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
||||
node_modules/
|
||||
@girs/
|
||||
@types/
|
||||
build/
|
||||
|
||||
pnpm-lock.yaml
|
||||
|
||||
+1
-8
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-5
@@ -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<Runner.Plugin> = [
|
||||
|
||||
+4
-3
@@ -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<keyof typeof generalConfigDefaults,
|
||||
typeof generalConfigDefaults[keyof typeof generalConfigDefaults]>(
|
||||
`${GLib.get_user_config_dir()}/colorshell/config.json`, generalConfigDefaults
|
||||
`${GLib.get_user_config_dir()}/colorshell/config.json`,
|
||||
generalConfigDefaults
|
||||
);
|
||||
|
||||
+15
-1
@@ -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;
|
||||
|
||||
+15
-1
@@ -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;
|
||||
|
||||
+16
-2
@@ -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,
|
||||
|
||||
@@ -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,11 +24,17 @@ 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|null>(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 extends keyof (typeof this)["$signals"]>(
|
||||
signal: Signal, callback: (typeof this["$signals"])[Signal]
|
||||
): number {
|
||||
return super.connect(signal as string, callback as () => void);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<K extends NonNullable<string|number|symbol>, V extends string|object|any> extends GObject.Object {
|
||||
class Config<K extends string, V = any> extends GObject.Object {
|
||||
declare $signals: GObject.Object.SignalSignatures & {
|
||||
"notify::entries": (entries: Record<K, V>) => void;
|
||||
};
|
||||
@@ -22,7 +22,7 @@ class Config<K extends NonNullable<string|number|symbol>, V extends string|objec
|
||||
* in the `entries` field */
|
||||
public readonly defaults: Record<K, V>;
|
||||
|
||||
@getter(Object as unknown as ParamSpec<Record<K, V>>)
|
||||
@getter(gtype<Record<K, V>>(Object))
|
||||
public get entries() { return this.#entries; }
|
||||
|
||||
#file: Gio.File;
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
<Gtk.Label class={"title"} label={createBinding(player.get(), "title").as(title =>
|
||||
title ?? "No Title")} maxWidthChars={20} ellipsize={Pango.EllipsizeMode.END}
|
||||
title ?? tr("media.no_title"))} maxWidthChars={20} ellipsize={Pango.EllipsizeMode.END}
|
||||
/>
|
||||
<Separator orientation={Gtk.Orientation.HORIZONTAL} size={1} margin={5}
|
||||
alpha={.3} spacing={6} />
|
||||
<Gtk.Label class={"artist"} label={createBinding(player.get(), "artist").as(artist =>
|
||||
artist ?? "No Artist")} maxWidthChars={18} ellipsize={Pango.EllipsizeMode.END}
|
||||
artist ?? tr("media.no_artist"))} maxWidthChars={18} ellipsize={Pango.EllipsizeMode.END}
|
||||
/>
|
||||
</Gtk.Box>}
|
||||
</With>
|
||||
@@ -84,7 +85,7 @@ export const Media = () => {
|
||||
<Gtk.Box class={"extra button-row"}>
|
||||
<Gtk.Button class={"link"} iconName={"edit-paste-symbolic"}
|
||||
visible={variableToBoolean(accessMediaUrl(player.get()))}
|
||||
tooltipText={"Copy link to Clipboard"} onClicked={() => {
|
||||
tooltipText={tr("copy_to_clipboard")} onClicked={() => {
|
||||
const url = accessMediaUrl(player.get()).get();
|
||||
url && Clipboard.getDefault().copyAsync(url);
|
||||
}}
|
||||
@@ -92,20 +93,21 @@ export const Media = () => {
|
||||
</Gtk.Box>
|
||||
<Gtk.Box class={"media-controls button-row"}>
|
||||
<Gtk.Button class={"previous"} iconName={"media-skip-backward-symbolic"}
|
||||
tooltipText={"Previous"} onClicked={() =>
|
||||
tooltipText={tr("media.previous")} onClicked={() =>
|
||||
player.get().canGoPrevious && player.get().previous()}
|
||||
/>
|
||||
<Gtk.Button class={"play-pause"} iconName={createBinding(player.get(), "playbackStatus").as(status =>
|
||||
status === AstalMpris.PlaybackStatus.PAUSED ?
|
||||
"media-playback-start-symbolic"
|
||||
: "media-playback-pause-symbolic")}
|
||||
tooltipText={
|
||||
createBinding(player.get(), "playbackStatus").as(status =>
|
||||
status === AstalMpris.PlaybackStatus.PAUSED ? "Play" : "Pause")
|
||||
} onClicked={() => player.get().play_pause()}
|
||||
tooltipText={createBinding(player.get(), "playbackStatus").as(status =>
|
||||
status === AstalMpris.PlaybackStatus.PAUSED ?
|
||||
tr("media.play")
|
||||
: tr("media.pause")
|
||||
)} onClicked={() => player.get().play_pause()}
|
||||
/>
|
||||
<Gtk.Button class={"next"} iconName={"media-skip-forward-symbolic"}
|
||||
tooltipText={"Next"} onClicked={() => player.get().canGoNext &&
|
||||
tooltipText={tr("media.next")} onClicked={() => player.get().canGoNext &&
|
||||
player.get().next()}
|
||||
/>
|
||||
</Gtk.Box>
|
||||
|
||||
@@ -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 = () => {
|
||||
</For>
|
||||
</Adw.Carousel> as Adw.Carousel;
|
||||
|
||||
return <Gtk.Box class={"big-media"} orientation={Gtk.Orientation.VERTICAL} widthRequest={255}
|
||||
return <Gtk.Box class={"big-media"} orientation={Gtk.Orientation.VERTICAL} widthRequest={270}
|
||||
visible={variableToBoolean(availablePlayers)}>
|
||||
|
||||
{carousel}
|
||||
@@ -75,15 +76,15 @@ class PlayerWidget extends Gtk.Box {
|
||||
valign={Gtk.Align.CENTER} vexpand hexpand>
|
||||
|
||||
<Gtk.Label class={"title"} tooltipText={
|
||||
createBinding(player, "title").as(title => 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}
|
||||
/>
|
||||
<Gtk.Label class={"artist"} tooltipText={
|
||||
createBinding(player, "artist").as(artist => 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}
|
||||
/>
|
||||
</Gtk.Box> as Gtk.Box
|
||||
@@ -128,7 +129,7 @@ class PlayerWidget extends Gtk.Box {
|
||||
<Gtk.Box spacing={4} $type="center">
|
||||
<Gtk.Box class={"extra button-row"}>
|
||||
<Gtk.Button class={"link"}
|
||||
tooltipText={"Copy link to clipboard"}
|
||||
tooltipText={tr("copy_to_clipboard")}
|
||||
visible={variableToBoolean(accessMediaUrl(player))}
|
||||
onClicked={(self) => {
|
||||
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()}
|
||||
/>
|
||||
<Gtk.Button class={"previous"} iconName={"media-skip-backward-symbolic"}
|
||||
tooltipText={"Previous"} onClicked={() => player.canGoPrevious && player.previous()}
|
||||
tooltipText={tr("media.previous")} onClicked={() => player.canGoPrevious && player.previous()}
|
||||
/>
|
||||
<Gtk.Button class={"play-pause"} tooltipText={
|
||||
createBinding(player, "playbackStatus").as(status =>
|
||||
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()}
|
||||
/>
|
||||
<Gtk.Button class={"next"} iconName={"media-skip-forward-symbolic"}
|
||||
tooltipText={"Next"} onClicked={() => player.canGoNext && player.next()}
|
||||
tooltipText={tr("media.next")} onClicked={() => player.canGoNext && player.next()}
|
||||
/>
|
||||
<Gtk.Button class={"repeat"} iconName={createBinding(player, "loopStatus").as(status => {
|
||||
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()}
|
||||
/>
|
||||
</Gtk.Box>
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
"moduleResolution": "bundler",
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"./@girs"
|
||||
"./@types"
|
||||
],
|
||||
"strict": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
Reference in New Issue
Block a user