perf(modules/media): better implementation

This commit is contained in:
retrozinndev
2025-10-07 17:24:45 -03:00
parent 14a3855df8
commit 62d7fd519c
8 changed files with 310 additions and 172 deletions
+11 -18
View File
@@ -18,17 +18,17 @@ import { Wallpaper } from "./modules/wallpaper";
import { Stylesheet } from "./modules/stylesheet"; import { Stylesheet } from "./modules/stylesheet";
import { Clipboard } from "./modules/clipboard"; import { Clipboard } from "./modules/clipboard";
import { Gdk, Gtk } from "ags/gtk4"; import { Gdk, Gtk } from "ags/gtk4";
import { createBinding, createRoot, getScope, Scope } from "ags"; import { createBinding, createComputed, createRoot, getScope, Scope } from "ags";
import { OSDModes, triggerOSD } from "./window/osd"; import { OSDModes, triggerOSD } from "./window/osd";
import { programArgs, programInvocationName } from "system"; import { programArgs, programInvocationName } from "system";
import { setConsoleLogDomain } from "console"; import { setConsoleLogDomain } from "console";
import { initPlayer } from "./modules/media";
import { createSubscription, encoder, secureBaseBinding } from "./modules/utils"; import { createSubscription, encoder, secureBaseBinding } from "./modules/utils";
import { exec } from "ags/process"; import { exec } from "ags/process";
import { NightLight } from "./modules/nightlight"; import { NightLight } from "./modules/nightlight";
import { Backlights } from "./modules/backlight"; import { Backlights } from "./modules/backlight";
import GObject, { register } from "ags/gobject"; import GObject, { register } from "ags/gobject";
import Media from "./modules/media";
import GLib from "gi://GLib?version=2.0"; import GLib from "gi://GLib?version=2.0";
import Gio from "gi://Gio?version=2.0"; import Gio from "gi://Gio?version=2.0";
import Adw from "gi://Adw?version=1"; import Adw from "gi://Adw?version=1";
@@ -275,7 +275,7 @@ you should use the socket in the XDG_RUNTIME_DIR/colorshell.sock for a faster re
NightLight.getDefault(); NightLight.getDefault();
initPlayer(); Media.getDefault();
Clipboard.getDefault(); Clipboard.getDefault();
console.log("Colorshell: Initializing Wallpaper and Stylesheet modules"); console.log("Colorshell: Initializing Wallpaper and Stylesheet modules");
@@ -286,21 +286,14 @@ you should use the socket in the XDG_RUNTIME_DIR/colorshell.sock for a faster re
runnerPlugins.forEach(plugin => Runner.addPlugin(plugin)); runnerPlugins.forEach(plugin => Runner.addPlugin(plugin));
createSubscription( createSubscription(
secureBaseBinding<AstalWp.Endpoint>( createComputed([
createBinding(AstalWp.get_default(), "defaultSpeaker"), secureBaseBinding<AstalWp.Endpoint>(createBinding(
"volume", AstalWp.get_default(), "defaultSpeaker"
null ), "volume", null),
), secureBaseBinding<AstalWp.Endpoint>(createBinding(
() => !Windows.getDefault().isOpen("control-center") && AstalWp.get_default(), "defaultSpeaker"
triggerOSD(OSDModes.sink) ), "mute", null)
); ]),
createSubscription(
secureBaseBinding<AstalWp.Endpoint>(
createBinding(AstalWp.get_default(), "defaultSpeaker"),
"mute",
null
),
() => !Windows.getDefault().isOpen("control-center") && () => !Windows.getDefault().isOpen("control-center") &&
triggerOSD(OSDModes.sink) triggerOSD(OSDModes.sink)
); );
+84 -10
View File
@@ -1,5 +1,5 @@
import { Scope } from "ags"; import { Scope } from "ags";
import { createScopedConnection, encoder } from "../modules/utils"; import { createScopedConnection, decoder, encoder } from "../modules/utils";
import windows from "./modules/windows"; import windows from "./modules/windows";
import volume from "./modules/volume"; import volume from "./modules/volume";
@@ -11,7 +11,6 @@ import GLib from "gi://GLib?version=2.0";
export namespace Cli { export namespace Cli {
let rootScope: Scope; let rootScope: Scope;
let service: Gio.SocketService;
let initialized: boolean = false; let initialized: boolean = false;
const modules: Array<Module> = [ const modules: Array<Module> = [
// main module, no need for prefix // main module, no need for prefix
@@ -65,7 +64,12 @@ export namespace Cli {
value?: string; value?: string;
/** help message for the argument */ /** help message for the argument */
help?: string; help?: string;
onCalled: (value?: string) => void; onCalled?: (value?: string) => void;
};
export type ArgumentData = {
argument: Argument;
data?: string;
}; };
export type Command = { export type Command = {
@@ -76,7 +80,7 @@ export namespace Cli {
/** data passed to the command. (only works when arguments are disabled) */ /** data passed to the command. (only works when arguments are disabled) */
data?: string; data?: string;
arguments?: Array<Argument>; arguments?: Array<Argument>;
onCalled: (args: Array<string>, data?: string) => Output; onCalled: (args: Array<ArgumentData>, data?: string) => Output;
}; };
export type Module = { export type Module = {
@@ -91,17 +95,17 @@ export namespace Cli {
}; };
/** initialize the cli */ /** initialize the cli */
export function init(scope: Scope, socketService: Gio.SocketService): void { export function init(scope: Scope, communicationMethod: Gio.SocketService|Gio.ApplicationCommandLine, app?: Gio.Application): void {
if(initialized) return; if(initialized) return;
initialized = true; initialized = true;
rootScope = scope; rootScope = scope;
service = socketService;
DEVEL && modules.push(devel); DEVEL && modules.push(devel);
scope.run(() => { scope.run(() => {
if(communicationMethod instanceof Gio.SocketService) {
createScopedConnection( createScopedConnection(
service, "incoming", (conn) => { communicationMethod, "incoming", (conn) => {
try { try {
return handleIncoming(conn); return handleIncoming(conn);
} catch(_) {} } catch(_) {}
@@ -109,6 +113,41 @@ export namespace Cli {
return false; return false;
} }
); );
return;
}
if(!app)
throw new Error("GApplication not specified for GApplicationCommandLine communication method")
if(app.flags !& Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
throw new Error("GApplication does not have the HANDLES_COMMAND_LINE flag or doesn't implement it")
createScopedConnection(
app,
"command-line",
(cmd) => {
let hasError: boolean = false;
try {
handleArgs(
cmd.get_arguments().toSpliced(0, 1),
(str, type) => {
if(type === "err") {
cmd.printerr_literal(str);
hasError = true;
return;
}
cmd.print_literal(str);
}
);
} catch(_) {
// TODO better error message
hasError = true;
}
return hasError ? 1 : 0;
}
);
}); });
} }
@@ -131,7 +170,7 @@ export namespace Cli {
parsedArgs?.splice(0, 1); // remove the unnecessary `colorshell` part parsedArgs?.splice(0, 1); // remove the unnecessary `colorshell` part
if(success) { if(success) {
handleArgs(parsedArgs!); handleArgs(parsedArgs!, conn.outputStream);
conn.outputStream.flush(null); conn.outputStream.flush(null);
conn.close(null); conn.close(null);
@@ -153,8 +192,43 @@ export namespace Cli {
}); });
} }
/** translate app arguments to modules/commands */ /** translate app arguments to modules/commands
function handleArgs(args: Array<string>): void { * order: module ?arg -> command ?arg */
function handleArgs(args: Array<string>, writeTo: Gio.OutputStream|((str: string, type: "out"|"err") => void)): void {
let mod: Module; let mod: Module;
let command: Command|undefined;
const modArgs: Array<Argument> = [];
const cmdArgs: Array<Argument> = [];
function print(out: Output): void {
const content = `${outputToString(out)}\n`;
const type: "out"|"err" = typeof out === "object" ?
out.type
: "out";
typeof writeTo === "function" ?
writeTo(content, type)
: writeTo.write_bytes(
encoder.encode(`${outputToString(out)}\n`),
null
);
}
for(let i = 0; i < args.length; i++) {
const arg = args[i];
if(i === 0) {
}
}
}
function outputToString(out: Output): string {
if(typeof out === "object")
return out.content instanceof Uint8Array ?
decoder.decode(out.content)
: out.content;
return out;
} }
} }
+4 -4
View File
@@ -6,10 +6,10 @@ import { timeout } from "ags/time";
import { Runner } from "../runner/Runner"; import { Runner } from "../runner/Runner";
import { showWorkspaceNumber } from "../window/bar/widgets/Workspaces"; import { showWorkspaceNumber } from "../window/bar/widgets/Workspaces";
import { playSystemBell } from "./utils"; import { playSystemBell } from "./utils";
import { player, setPlayer } from "./media";
import { Shell } from "../app"; import { Shell } from "../app";
import { generalConfig } from "../config"; import { generalConfig } from "../config";
import Media from "./media";
import AstalIO from "gi://AstalIO"; import AstalIO from "gi://AstalIO";
import AstalMpris from "gi://AstalMpris"; import AstalMpris from "gi://AstalMpris";
@@ -166,8 +166,8 @@ Options:
return 0; return 0;
} }
const activePlayer: AstalMpris.Player|undefined = player.get().available ? const activePlayer: AstalMpris.Player|undefined = Media.getDefault().player.available ?
player.get() Media.getDefault().player
: undefined; : undefined;
const players = AstalMpris.get_default().players.filter(pl => pl.available); const players = AstalMpris.get_default().players.filter(pl => pl.available);
@@ -250,7 +250,7 @@ specified bus name does not exist/is not available!`);
return 1; return 1;
} }
setPlayer(players.filter(pl => pl.busName === args[2])[0]); Media.getDefault().player = players.filter(pl => pl.busName === args[2])[0];
cmd.print_literal(`Done setting player to \`${args[2]}\`!`); cmd.print_literal(`Done setting player to \`${args[2]}\`!`);
return 0; return 0;
} }
+52 -43
View File
@@ -1,61 +1,66 @@
import { Accessor, createConnection, createRoot, createState, onCleanup } from "ags"; import { Accessor, createConnection, getScope, Scope } from "ags";
import { decoder } from "./utils"; import { createScopedConnection, decoder } from "./utils";
import GObject from "ags/gobject";
import AstalMpris from "gi://AstalMpris"; import AstalMpris from "gi://AstalMpris";
import GObject from "gi://GObject?version=2.0";
import { property, register } from "ags/gobject";
export const dummyPlayer = { @register({ GTypeName: "Media" })
export default class Media extends GObject.Object {
private static instance: Media;
public static readonly dummyPlayer = {
available: false, available: false,
busName: "dummy_player", busName: "dummy_player",
bus_name: "dummy_player" bus_name: "dummy_player"
} as AstalMpris.Player; } as AstalMpris.Player;
export let [player, setPlayer] = createState(dummyPlayer); @property(AstalMpris.Player)
player: AstalMpris.Player = Media.dummyPlayer;
let disposeFun: undefined|(() => void); constructor(scope: Scope) {
super();
export function initPlayer(): void { scope.run(() => {
if(disposeFun) { const firstPlayer = AstalMpris.get_default().players[0];
console.error("Media: cannot initialize, there's already an instance"); if(firstPlayer)
return; this.player = firstPlayer;
createScopedConnection(
AstalMpris.get_default(),
"player-added",
(player) => {
if(player.available)
this.player = player;
} }
);
createRoot((dispose) => { createScopedConnection(
const connections = new Map<GObject.Object, Array<number>>(); AstalMpris.get_default(),
disposeFun = dispose; "player-closed", (closedPlayer) => {
setPlayer(AstalMpris.get_default().players[0] ?? dummyPlayer);
connections.set(AstalMpris.get_default(), [
AstalMpris.get_default().connect("player-added", (_, player) =>
player.available && setPlayer(player)),
AstalMpris.get_default().connect("player-closed", (_, closedPlayer) => {
const players = AstalMpris.get_default().players.filter(pl => pl?.available && const players = AstalMpris.get_default().players.filter(pl => pl?.available &&
pl.busName !== closedPlayer.busName); pl.busName !== closedPlayer.busName);
// go back to first player(if available) when the active player is closed
if(players.length > 0 && players[0]) { if(players.length > 0 && players[0]) {
setPlayer(players[0]); this.player = players[0];
return; return;
} }
setPlayer(dummyPlayer); this.player = Media.dummyPlayer;
}) }
]);
onCleanup(() => {
connections.forEach((ids, obj) =>
Array.isArray(ids) ?
ids.forEach(id => obj.disconnect(id))
: obj.disconnect(ids)
); );
disposeFun = undefined;
}); });
}); }
}
export function accessMediaUrl(player: AstalMpris.Player): Accessor<string|undefined> { public static getDefault(): Media {
if(!this.instance)
this.instance = new Media(getScope());
return this.instance;
}
public static accessMediaUrl(player: AstalMpris.Player): Accessor<string|undefined> {
return createConnection(player.get_meta("xesam:url"), return createConnection(player.get_meta("xesam:url"),
[player, "notify::metadata", () => player.get_meta("xesam:url")] [player, "notify::metadata", () => player.get_meta("xesam:url")]
).as(url => { ).as(url => {
@@ -65,13 +70,17 @@ export function accessMediaUrl(player: AstalMpris.Player): Accessor<string|undef
decoder.decode(byteString.toArray()) decoder.decode(byteString.toArray())
: undefined; : undefined;
}) })
}
export function disposePlayer(): void {
if(disposeFun) {
disposeFun();
return;
} }
console.error("Media: Couldn't dispose player, there's no instance to dispose of");
public static getMediaUrl(player: AstalMpris.Player): string|undefined {
if(!player.available) return;
const meta = player.get_meta("xesam:url");
const byteString = meta?.get_data_as_bytes();
return byteString ?
decoder.decode(byteString.toArray())
: undefined;
}
} }
+64 -18
View File
@@ -1,53 +1,99 @@
import { createBinding, createComputed } from "ags"; import { createBinding, createComputed } from "ags";
import { Runner } from "../Runner"; import { Runner } from "../Runner";
import { player } from "../../modules/media"; import { secureBaseBinding } from "../../modules/utils";
import { tr } from "../../i18n/intl";
import Media from "../../modules/media";
import AstalMpris from "gi://AstalMpris"; import AstalMpris from "gi://AstalMpris";
export const PluginMedia = { export const PluginMedia = {
prefix: ":", prefix: ":",
handle: () => !player.get().available ? { handle: () => !Media.getDefault().player.available ? {
icon: "folder-music-symbolic", icon: "folder-music-symbolic",
title: "Couldn't find any players", title: "Couldn't find any players",
closeOnClick: false, closeOnClick: false,
description: "No media / player found with mpris" description: "No media / player found with mpris"
} : [ } : [
{ {
icon: createBinding(player.get(), "playbackStatus").as((status) => status === AstalMpris.PlaybackStatus.PLAYING ? icon: secureBaseBinding<AstalMpris.Player>(
createBinding(Media.getDefault(), "player"),
"playbackStatus",
AstalMpris.PlaybackStatus.PAUSED
).as((status) => status === AstalMpris.PlaybackStatus.PLAYING ?
"media-playback-pause-symbolic" "media-playback-pause-symbolic"
: "media-playback-start-symbolic"), : "media-playback-start-symbolic"),
closeOnClick: false, closeOnClick: false,
title: createComputed([ title: createComputed([
createBinding(player.get(), "title"), secureBaseBinding<AstalMpris.Player>(
createBinding(player.get(), "artist"), createBinding(Media.getDefault(), "player"),
createBinding(player.get(), "playbackStatus") "title",
], (title, artist, status) => `${ status === AstalMpris.PlaybackStatus.PLAYING ? null
).as(t => t ?? tr("media.no_title")),
secureBaseBinding<AstalMpris.Player>(
createBinding(Media.getDefault(), "player"),
"artist",
null
).as(t => t ?? tr("media.no_artist")),
secureBaseBinding<AstalMpris.Player>(
createBinding(Media.getDefault(), "player"),
"playbackStatus",
AstalMpris.PlaybackStatus.PAUSED
)
], (title, artist, status) => `${status === AstalMpris.PlaybackStatus.PLAYING ?
"Pause" : "Play" "Pause" : "Play"
} ${title} | ${artist}`), } ${title} | ${artist}`),
actionClick: () => player.get().play_pause() actionClick: () => Media.getDefault().player.play_pause()
}, },
{ {
icon: "media-skip-backward-symbolic", icon: "media-skip-backward-symbolic",
closeOnClick: false, closeOnClick: false,
title: createComputed([ title: createComputed([
createBinding(player.get(), "title"), secureBaseBinding<AstalMpris.Player>(
createBinding(player.get(), "artist") createBinding(Media.getDefault(), "player"),
], (title, artist) => "title",
`Go Previous ${ title ? title : player.get().busName }${ artist ? ` | ${artist}` : "" }` null
).as(t => t ?? tr("media.no_title")),
secureBaseBinding<AstalMpris.Player>(
createBinding(Media.getDefault(), "player"),
"artist",
null
).as(t => t ?? tr("media.no_artist")),
secureBaseBinding<AstalMpris.Player>(
createBinding(Media.getDefault(), "player"),
"identity",
"Music Player"
)
], (title, artist, identity) =>
`Go Previous ${title ? title : identity}${artist ? ` | ${artist}` : ""}`
), ),
actionClick: () => player.get().canGoPrevious && player.get().previous() actionClick: () => Media.getDefault().player.canGoPrevious &&
Media.getDefault().player.previous()
}, },
{ {
icon: "media-skip-forward-symbolic", icon: "media-skip-forward-symbolic",
closeOnClick: false, closeOnClick: false,
title: createComputed([ title: createComputed([
createBinding(player.get(), "title"), secureBaseBinding<AstalMpris.Player>(
createBinding(player.get(), "artist") createBinding(Media.getDefault(), "player"),
], (title, artist) => "title",
`Go Next ${ title ? title : player.get().busName }${ artist ? ` | ${artist}` : "" }` null
).as(t => t ?? tr("media.no_title")),
secureBaseBinding<AstalMpris.Player>(
createBinding(Media.getDefault(), "player"),
"artist",
null
).as(t => t ?? tr("media.no_artist")),
secureBaseBinding<AstalMpris.Player>(
createBinding(Media.getDefault(), "player"),
"identity",
"Music Player"
)
], (title, artist, identity) =>
`Go Next ${title ? title : identity}${artist ? ` | ${artist}` : ""}`
), ),
actionClick: () => player.get().canGoNext && player.get().next() actionClick: () => Media.getDefault().player.canGoNext &&
Media.getDefault().player.next()
} }
] ]
} as Runner.Plugin; } as Runner.Plugin;
+60 -43
View File
@@ -1,32 +1,25 @@
import { createBinding, onCleanup, With } from "ags"; import { createBinding, With } from "ags";
import { Gtk } from "ags/gtk4"; import { Gtk } from "ags/gtk4";
import { Separator } from "../../../widget/Separator"; import { Separator } from "../../../widget/Separator";
import { Windows } from "../../../windows"; import { Windows } from "../../../windows";
import { Clipboard } from "../../../modules/clipboard"; import { Clipboard } from "../../../modules/clipboard";
import { getPlayerIconFromBusName, variableToBoolean } from "../../../modules/utils"; import { getPlayerIconFromBusName, secureBaseBinding, variableToBoolean } from "../../../modules/utils";
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"; import { tr } from "../../../i18n/intl";
import { default as Player } from "../../../modules/media";
import AstalMpris from "gi://AstalMpris";
import Pango from "gi://Pango?version=1.0";
export const Media = () => {
const connections: Map<GObject.Object, Array<number>|number> = new Map();
onCleanup(() => connections.forEach((id, obj) => export const Media = () =>
Array.isArray(id) ? <Gtk.Box class={"media"} visible={secureBaseBinding<AstalMpris.Player>(createBinding(
id.forEach(id => obj.disconnect(id)) Player.getDefault(), "player"
: obj.disconnect(id) ), "available", false)}>
));
return <Gtk.Box class={"media"} visible={player((pl) => pl.available)}>
<Gtk.EventControllerScroll $={(self) => { <Gtk.EventControllerScroll $={(self) => {
self.set_flags(Gtk.EventControllerScrollFlags.VERTICAL) self.set_flags(Gtk.EventControllerScrollFlags.VERTICAL)
}} onScroll={(_, __, dy) => { }} onScroll={(_, __, dy) => {
if(AstalMpris.get_default().players.length === 1 && if(AstalMpris.get_default().players.length === 1 &&
player.get()?.busName === AstalMpris.get_default().players[0].busName) Player.getDefault().player.busName === AstalMpris.get_default().players[0].busName)
return true; return true;
const players = AstalMpris.get_default().players; const players = AstalMpris.get_default().players;
@@ -34,16 +27,16 @@ export const Media = () => {
for(let i = 0; i < players.length; i++) { for(let i = 0; i < players.length; i++) {
const pl = players[i]; const pl = players[i];
if(pl.busName !== player.get().busName) if(pl.busName !== Player.getDefault().player.busName)
continue; continue;
if(dy > 0 && players[i-1]) { if(dy > 0 && players[i-1]) {
setPlayer(players[i-1]); Player.getDefault().player = players[i-1];
break; break;
} }
if(dy < 0 && players[i+1]) { if(dy < 0 && players[i+1]) {
setPlayer(players[i+1]); Player.getDefault().player = players[i+1];
break; break;
} }
} }
@@ -60,19 +53,33 @@ export const Media = () => {
revealer.set_reveal_child(false); revealer.set_reveal_child(false);
}} }}
/> />
<Gtk.Box spacing={4} visible={player(pl => pl.available)}> <Gtk.Box spacing={4} visible={secureBaseBinding<AstalMpris.Player>(createBinding(
<With value={player(pl => pl.available)}> Player.getDefault(), "player"
), "available", false)
}>
<With value={secureBaseBinding<AstalMpris.Player>(createBinding(
Player.getDefault(), "player"
), "available", false)
}>
{(available: boolean) => available && <Gtk.Box> {(available: boolean) => available && <Gtk.Box>
<Gtk.Image class={"player-icon"} iconName={ <Gtk.Image class={"player-icon"} iconName={
createBinding(player.get(), "busName").as(getPlayerIconFromBusName)} secureBaseBinding<AstalMpris.Player>(createBinding(
Player.getDefault(), "player"
), "busName", "org.MediaPlayer2.folder-music-symbolic").as(
getPlayerIconFromBusName
)}
/> />
<Gtk.Label class={"title"} label={createBinding(player.get(), "title").as(title => <Gtk.Label class={"title"} label={secureBaseBinding<AstalMpris.Player>(createBinding(
title ?? tr("media.no_title"))} maxWidthChars={20} ellipsize={Pango.EllipsizeMode.END} Player.getDefault(), "player"
), "title", "").as(title => title ?? tr("media.no_title"))}
maxWidthChars={20} ellipsize={Pango.EllipsizeMode.END}
/> />
<Separator orientation={Gtk.Orientation.HORIZONTAL} size={1} margin={5} <Separator orientation={Gtk.Orientation.HORIZONTAL} size={1} margin={5}
alpha={.3} spacing={6} /> alpha={.3} spacing={6} />
<Gtk.Label class={"artist"} label={createBinding(player.get(), "artist").as(artist => <Gtk.Label class={"artist"} label={secureBaseBinding<AstalMpris.Player>(createBinding(
artist ?? tr("media.no_artist"))} maxWidthChars={18} ellipsize={Pango.EllipsizeMode.END} Player.getDefault(), "player"
), "artist", "").as(artist => artist ?? tr("media.no_artist"))}
maxWidthChars={18} ellipsize={Pango.EllipsizeMode.END}
/> />
</Gtk.Box>} </Gtk.Box>}
</With> </With>
@@ -80,13 +87,16 @@ export const Media = () => {
<Gtk.Revealer transitionType={Gtk.RevealerTransitionType.SLIDE_RIGHT} transitionDuration={260} <Gtk.Revealer transitionType={Gtk.RevealerTransitionType.SLIDE_RIGHT} transitionDuration={260}
revealChild={false}> revealChild={false}>
<With value={player(pl => pl.available)}> <With value={secureBaseBinding<AstalMpris.Player>(createBinding(
Player.getDefault(), "player"
), "available", false)
}>
{(available: boolean) => available && <Gtk.Box class={"buttons"} spacing={4}> {(available: boolean) => available && <Gtk.Box class={"buttons"} spacing={4}>
<Gtk.Box class={"extra button-row"}> <Gtk.Box class={"extra button-row"}>
<Gtk.Button class={"link"} iconName={"edit-paste-symbolic"} <Gtk.Button class={"link"} iconName={"edit-paste-symbolic"}
visible={variableToBoolean(accessMediaUrl(player.get()))} visible={variableToBoolean(Player.accessMediaUrl(Player.getDefault().player))}
tooltipText={tr("copy_to_clipboard")} onClicked={() => { tooltipText={tr("copy_to_clipboard")} onClicked={() => {
const url = accessMediaUrl(player.get()).get(); const url = Player.getMediaUrl(Player.getDefault().player);
url && Clipboard.getDefault().copyAsync(url); url && Clipboard.getDefault().copyAsync(url);
}} }}
/> />
@@ -94,25 +104,32 @@ export const Media = () => {
<Gtk.Box class={"media-controls button-row"}> <Gtk.Box class={"media-controls button-row"}>
<Gtk.Button class={"previous"} iconName={"media-skip-backward-symbolic"} <Gtk.Button class={"previous"} iconName={"media-skip-backward-symbolic"}
tooltipText={tr("media.previous")} onClicked={() => tooltipText={tr("media.previous")} onClicked={() =>
player.get().canGoPrevious && player.get().previous()} Player.getDefault().player.canGoPrevious &&
Player.getDefault().player.previous()
}
/> />
<Gtk.Button class={"play-pause"} iconName={createBinding(player.get(), "playbackStatus").as(status => <Gtk.Button class={"play-pause"} iconName={secureBaseBinding<AstalMpris.Player>(
status === AstalMpris.PlaybackStatus.PAUSED ? createBinding(Player.getDefault(), "player"),
"playbackStatus",
AstalMpris.PlaybackStatus.PAUSED
).as(status => status === AstalMpris.PlaybackStatus.PAUSED ?
"media-playback-start-symbolic" "media-playback-start-symbolic"
: "media-playback-pause-symbolic")} : "media-playback-pause-symbolic"
tooltipText={createBinding(player.get(), "playbackStatus").as(status => )}
status === AstalMpris.PlaybackStatus.PAUSED ? tooltipText={secureBaseBinding<AstalMpris.Player>(
tr("media.play") createBinding(Player.getDefault(), "player"),
: tr("media.pause") "playbackStatus",
)} onClicked={() => player.get().play_pause()} AstalMpris.PlaybackStatus.PAUSED
).as(status => status === AstalMpris.PlaybackStatus.PAUSED ?
tr("media.play") : tr("media.pause")
)} onClicked={() => Player.getDefault().player.play_pause()}
/> />
<Gtk.Button class={"next"} iconName={"media-skip-forward-symbolic"} <Gtk.Button class={"next"} iconName={"media-skip-forward-symbolic"}
tooltipText={tr("media.next")} onClicked={() => player.get().canGoNext && tooltipText={tr("media.next")} onClicked={() => Player.getDefault().player.canGoNext &&
player.get().next()} Player.getDefault().player.next()}
/> />
</Gtk.Box> </Gtk.Box>
</Gtk.Box>} </Gtk.Box>}
</With> </With>
</Gtk.Revealer> </Gtk.Revealer>
</Gtk.Box> </Gtk.Box> as Gtk.Box;
}
+4 -3
View File
@@ -4,17 +4,18 @@ import { PopupWindow } from "../../widget/PopupWindow";
import { BigMedia } from "./widgets/BigMedia"; import { BigMedia } from "./widgets/BigMedia";
import { time, variableToBoolean } from "../../modules/utils"; import { time, variableToBoolean } from "../../modules/utils";
import { createBinding } from "ags"; import { createBinding } from "ags";
import { player } from "../../modules/media";
import Media from "../../modules/media";
import AstalMpris from "gi://AstalMpris"; import AstalMpris from "gi://AstalMpris";
export const CenterWindow = (mon: number) => export const CenterWindow = (mon: number) =>
<PopupWindow namespace={"center-window"} marginTop={10} monitor={mon} <PopupWindow namespace={"center-window"} marginTop={10} monitor={mon}
halign={Gtk.Align.CENTER} valign={Gtk.Align.START} halign={Gtk.Align.CENTER} valign={Gtk.Align.START}
actionKeyPressed={(_, keyval) => { actionKeyPressed={(_, keyval) => {
if(keyval === Gdk.KEY_space) { if(keyval === Gdk.KEY_space) {
player.get().available && Media.getDefault().player.available &&
player.get().play_pause(); Media.getDefault().player.play_pause();
return true; return true;
} }
}}> }}>
+11 -13
View File
@@ -2,19 +2,16 @@ import { createBinding, For } from "ags";
import { register } from "ags/gobject"; import { register } from "ags/gobject";
import { Astal, Gtk } from "ags/gtk4"; import { Astal, Gtk } from "ags/gtk4";
import { Clipboard } from "../../../modules/clipboard"; import { Clipboard } from "../../../modules/clipboard";
import { accessMediaUrl } from "../../../modules/media";
import { player, setPlayer } from "../../../modules/media";
import { pathToURI, variableToBoolean } from "../../../modules/utils"; import { pathToURI, variableToBoolean } from "../../../modules/utils";
import { tr } from "../../../i18n/intl"; import { tr } from "../../../i18n/intl";
import Media from "../../../modules/media";
import AstalMpris from "gi://AstalMpris"; import AstalMpris from "gi://AstalMpris";
import Pango from "gi://Pango?version=1.0"; import Pango from "gi://Pango?version=1.0";
import Adw from "gi://Adw?version=1"; import Adw from "gi://Adw?version=1";
import GLib from "gi://GLib?version=2.0"; import GLib from "gi://GLib?version=2.0";
let dragTimer: (GLib.Source|undefined);
export const BigMedia = () => { export const BigMedia = () => {
const availablePlayers = createBinding(AstalMpris.get_default(), "players").as(pls => const availablePlayers = createBinding(AstalMpris.get_default(), "players").as(pls =>
pls.filter(p => p.available)); pls.filter(p => p.available));
@@ -22,11 +19,11 @@ export const BigMedia = () => {
const carousel = <Adw.Carousel orientation={Gtk.Orientation.HORIZONTAL} spacing={6} const carousel = <Adw.Carousel orientation={Gtk.Orientation.HORIZONTAL} spacing={6}
onPageChanged={(self, num) => { onPageChanged={(self, num) => {
const page = self.get_nth_page(num); const page = self.get_nth_page(num);
if(page instanceof PlayerWidget && player.get().busName !== page.player.busName) if(page instanceof PlayerWidget && Media.getDefault().player.busName !== page.player.busName)
setPlayer(page.player); Media.getDefault().player = page.player;
}}> }}>
<For each={availablePlayers.as(players => players.sort(pl => <For each={availablePlayers.as(players => players.sort(pl =>
pl.busName === player.get().busName ? -1 : 1))}> pl.busName === Media.getDefault().player.busName ? -1 : 1))}>
{(player: AstalMpris.Player) => <PlayerWidget player={player} />} {(player: AstalMpris.Player) => <PlayerWidget player={player} />}
</For> </For>
@@ -48,6 +45,7 @@ export const BigMedia = () => {
class PlayerWidget extends Gtk.Box { class PlayerWidget extends Gtk.Box {
#player!: AstalMpris.Player; #player!: AstalMpris.Player;
#copyClickTimeout?: GLib.Source; #copyClickTimeout?: GLib.Source;
#dragTimer?: GLib.Source;
get player() { return this.#player; } get player() { return this.#player; }
@@ -97,16 +95,16 @@ class PlayerWidget extends Gtk.Box {
onChangeValue={(_, type, value) => { onChangeValue={(_, type, value) => {
if(type == null) return; if(type == null) return;
if(!dragTimer) { if(!this.#dragTimer) {
dragTimer = setTimeout(() => this.#dragTimer = setTimeout(() =>
player.position = Math.floor(value) player.position = Math.floor(value)
, 200); , 200);
return; return;
} }
dragTimer.destroy(); this.#dragTimer?.destroy();
dragTimer = setTimeout(() => this.#dragTimer = setTimeout(() =>
player.position = Math.floor(value) player.position = Math.floor(value)
, 200); , 200);
}} }}
@@ -129,9 +127,9 @@ class PlayerWidget extends Gtk.Box {
<Gtk.Box class={"extra button-row"}> <Gtk.Box class={"extra button-row"}>
<Gtk.Button class={"link"} <Gtk.Button class={"link"}
tooltipText={tr("copy_to_clipboard")} tooltipText={tr("copy_to_clipboard")}
visible={variableToBoolean(accessMediaUrl(player))} visible={variableToBoolean(Media.accessMediaUrl(player))}
onClicked={(self) => { onClicked={(self) => {
const url = accessMediaUrl(player).get(); const url = Media.accessMediaUrl(player).get();
// a widget that supports adding multiple icons and allows switching // a widget that supports adding multiple icons and allows switching
// through them would be pretty nice!! (i'll probably do this later) // through them would be pretty nice!! (i'll probably do this later)
url && url &&