⚡ perf(modules/media): better implementation
This commit is contained in:
+11
-18
@@ -18,17 +18,17 @@ import { Wallpaper } from "./modules/wallpaper";
|
||||
import { Stylesheet } from "./modules/stylesheet";
|
||||
import { Clipboard } from "./modules/clipboard";
|
||||
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 { programArgs, programInvocationName } from "system";
|
||||
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 Media from "./modules/media";
|
||||
import GLib from "gi://GLib?version=2.0";
|
||||
import Gio from "gi://Gio?version=2.0";
|
||||
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();
|
||||
|
||||
initPlayer();
|
||||
Media.getDefault();
|
||||
Clipboard.getDefault();
|
||||
|
||||
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));
|
||||
|
||||
createSubscription(
|
||||
secureBaseBinding<AstalWp.Endpoint>(
|
||||
createBinding(AstalWp.get_default(), "defaultSpeaker"),
|
||||
"volume",
|
||||
null
|
||||
),
|
||||
() => !Windows.getDefault().isOpen("control-center") &&
|
||||
triggerOSD(OSDModes.sink)
|
||||
);
|
||||
|
||||
createSubscription(
|
||||
secureBaseBinding<AstalWp.Endpoint>(
|
||||
createBinding(AstalWp.get_default(), "defaultSpeaker"),
|
||||
"mute",
|
||||
null
|
||||
),
|
||||
createComputed([
|
||||
secureBaseBinding<AstalWp.Endpoint>(createBinding(
|
||||
AstalWp.get_default(), "defaultSpeaker"
|
||||
), "volume", null),
|
||||
secureBaseBinding<AstalWp.Endpoint>(createBinding(
|
||||
AstalWp.get_default(), "defaultSpeaker"
|
||||
), "mute", null)
|
||||
]),
|
||||
() => !Windows.getDefault().isOpen("control-center") &&
|
||||
triggerOSD(OSDModes.sink)
|
||||
);
|
||||
|
||||
+84
-10
@@ -1,5 +1,5 @@
|
||||
import { Scope } from "ags";
|
||||
import { createScopedConnection, encoder } from "../modules/utils";
|
||||
import { createScopedConnection, decoder, encoder } from "../modules/utils";
|
||||
|
||||
import windows from "./modules/windows";
|
||||
import volume from "./modules/volume";
|
||||
@@ -11,7 +11,6 @@ import GLib from "gi://GLib?version=2.0";
|
||||
|
||||
export namespace Cli {
|
||||
let rootScope: Scope;
|
||||
let service: Gio.SocketService;
|
||||
let initialized: boolean = false;
|
||||
const modules: Array<Module> = [
|
||||
// main module, no need for prefix
|
||||
@@ -65,7 +64,12 @@ export namespace Cli {
|
||||
value?: string;
|
||||
/** help message for the argument */
|
||||
help?: string;
|
||||
onCalled: (value?: string) => void;
|
||||
onCalled?: (value?: string) => void;
|
||||
};
|
||||
|
||||
export type ArgumentData = {
|
||||
argument: Argument;
|
||||
data?: string;
|
||||
};
|
||||
|
||||
export type Command = {
|
||||
@@ -76,7 +80,7 @@ export namespace Cli {
|
||||
/** data passed to the command. (only works when arguments are disabled) */
|
||||
data?: string;
|
||||
arguments?: Array<Argument>;
|
||||
onCalled: (args: Array<string>, data?: string) => Output;
|
||||
onCalled: (args: Array<ArgumentData>, data?: string) => Output;
|
||||
};
|
||||
|
||||
export type Module = {
|
||||
@@ -91,17 +95,17 @@ export namespace 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;
|
||||
|
||||
initialized = true;
|
||||
rootScope = scope;
|
||||
service = socketService;
|
||||
DEVEL && modules.push(devel);
|
||||
|
||||
scope.run(() => {
|
||||
if(communicationMethod instanceof Gio.SocketService) {
|
||||
createScopedConnection(
|
||||
service, "incoming", (conn) => {
|
||||
communicationMethod, "incoming", (conn) => {
|
||||
try {
|
||||
return handleIncoming(conn);
|
||||
} catch(_) {}
|
||||
@@ -109,6 +113,41 @@ export namespace Cli {
|
||||
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
|
||||
|
||||
if(success) {
|
||||
handleArgs(parsedArgs!);
|
||||
handleArgs(parsedArgs!, conn.outputStream);
|
||||
|
||||
conn.outputStream.flush(null);
|
||||
conn.close(null);
|
||||
@@ -153,8 +192,43 @@ export namespace Cli {
|
||||
});
|
||||
}
|
||||
|
||||
/** translate app arguments to modules/commands */
|
||||
function handleArgs(args: Array<string>): void {
|
||||
/** translate app arguments to modules/commands
|
||||
* order: module ?arg -> command ?arg */
|
||||
function handleArgs(args: Array<string>, writeTo: Gio.OutputStream|((str: string, type: "out"|"err") => void)): void {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ import { timeout } from "ags/time";
|
||||
import { Runner } from "../runner/Runner";
|
||||
import { showWorkspaceNumber } from "../window/bar/widgets/Workspaces";
|
||||
import { playSystemBell } from "./utils";
|
||||
import { player, setPlayer } from "./media";
|
||||
import { Shell } from "../app";
|
||||
import { generalConfig } from "../config";
|
||||
|
||||
import Media from "./media";
|
||||
import AstalIO from "gi://AstalIO";
|
||||
import AstalMpris from "gi://AstalMpris";
|
||||
|
||||
@@ -166,8 +166,8 @@ Options:
|
||||
return 0;
|
||||
}
|
||||
|
||||
const activePlayer: AstalMpris.Player|undefined = player.get().available ?
|
||||
player.get()
|
||||
const activePlayer: AstalMpris.Player|undefined = Media.getDefault().player.available ?
|
||||
Media.getDefault().player
|
||||
: undefined;
|
||||
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;
|
||||
}
|
||||
|
||||
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]}\`!`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
+52
-43
@@ -1,61 +1,66 @@
|
||||
import { Accessor, createConnection, createRoot, createState, onCleanup } from "ags";
|
||||
import { decoder } from "./utils";
|
||||
import { Accessor, createConnection, getScope, Scope } from "ags";
|
||||
import { createScopedConnection, decoder } from "./utils";
|
||||
|
||||
import GObject from "ags/gobject";
|
||||
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,
|
||||
busName: "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 {
|
||||
if(disposeFun) {
|
||||
console.error("Media: cannot initialize, there's already an instance");
|
||||
return;
|
||||
scope.run(() => {
|
||||
const firstPlayer = AstalMpris.get_default().players[0];
|
||||
if(firstPlayer)
|
||||
this.player = firstPlayer;
|
||||
|
||||
createScopedConnection(
|
||||
AstalMpris.get_default(),
|
||||
"player-added",
|
||||
(player) => {
|
||||
if(player.available)
|
||||
this.player = player;
|
||||
}
|
||||
);
|
||||
|
||||
createRoot((dispose) => {
|
||||
const connections = new Map<GObject.Object, Array<number>>();
|
||||
disposeFun = dispose;
|
||||
|
||||
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) => {
|
||||
createScopedConnection(
|
||||
AstalMpris.get_default(),
|
||||
"player-closed", (closedPlayer) => {
|
||||
const players = AstalMpris.get_default().players.filter(pl => pl?.available &&
|
||||
pl.busName !== closedPlayer.busName);
|
||||
|
||||
// go back to first player(if available) when the active player is closed
|
||||
if(players.length > 0 && players[0]) {
|
||||
setPlayer(players[0]);
|
||||
this.player = players[0];
|
||||
return;
|
||||
}
|
||||
|
||||
setPlayer(dummyPlayer);
|
||||
})
|
||||
]);
|
||||
|
||||
onCleanup(() => {
|
||||
connections.forEach((ids, obj) =>
|
||||
Array.isArray(ids) ?
|
||||
ids.forEach(id => obj.disconnect(id))
|
||||
: obj.disconnect(ids)
|
||||
this.player = Media.dummyPlayer;
|
||||
}
|
||||
);
|
||||
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"),
|
||||
[player, "notify::metadata", () => player.get_meta("xesam:url")]
|
||||
).as(url => {
|
||||
@@ -65,13 +70,17 @@ export function accessMediaUrl(player: AstalMpris.Player): Accessor<string|undef
|
||||
decoder.decode(byteString.toArray())
|
||||
: 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
@@ -1,53 +1,99 @@
|
||||
import { createBinding, createComputed } from "ags";
|
||||
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";
|
||||
|
||||
|
||||
export const PluginMedia = {
|
||||
prefix: ":",
|
||||
handle: () => !player.get().available ? {
|
||||
handle: () => !Media.getDefault().player.available ? {
|
||||
icon: "folder-music-symbolic",
|
||||
title: "Couldn't find any players",
|
||||
closeOnClick: false,
|
||||
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-start-symbolic"),
|
||||
closeOnClick: false,
|
||||
title: createComputed([
|
||||
createBinding(player.get(), "title"),
|
||||
createBinding(player.get(), "artist"),
|
||||
createBinding(player.get(), "playbackStatus")
|
||||
], (title, artist, status) => `${ status === AstalMpris.PlaybackStatus.PLAYING ?
|
||||
secureBaseBinding<AstalMpris.Player>(
|
||||
createBinding(Media.getDefault(), "player"),
|
||||
"title",
|
||||
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"
|
||||
} ${title} | ${artist}`),
|
||||
actionClick: () => player.get().play_pause()
|
||||
actionClick: () => Media.getDefault().player.play_pause()
|
||||
},
|
||||
{
|
||||
icon: "media-skip-backward-symbolic",
|
||||
closeOnClick: false,
|
||||
title: createComputed([
|
||||
createBinding(player.get(), "title"),
|
||||
createBinding(player.get(), "artist")
|
||||
], (title, artist) =>
|
||||
`Go Previous ${ title ? title : player.get().busName }${ artist ? ` | ${artist}` : "" }`
|
||||
secureBaseBinding<AstalMpris.Player>(
|
||||
createBinding(Media.getDefault(), "player"),
|
||||
"title",
|
||||
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",
|
||||
closeOnClick: false,
|
||||
title: createComputed([
|
||||
createBinding(player.get(), "title"),
|
||||
createBinding(player.get(), "artist")
|
||||
], (title, artist) =>
|
||||
`Go Next ${ title ? title : player.get().busName }${ artist ? ` | ${artist}` : "" }`
|
||||
secureBaseBinding<AstalMpris.Player>(
|
||||
createBinding(Media.getDefault(), "player"),
|
||||
"title",
|
||||
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;
|
||||
|
||||
@@ -1,32 +1,25 @@
|
||||
import { createBinding, onCleanup, With } from "ags";
|
||||
import { createBinding, With } from "ags";
|
||||
import { Gtk } from "ags/gtk4";
|
||||
import { Separator } from "../../../widget/Separator";
|
||||
import { Windows } from "../../../windows";
|
||||
import { Clipboard } from "../../../modules/clipboard";
|
||||
import { getPlayerIconFromBusName, 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 { getPlayerIconFromBusName, secureBaseBinding, variableToBoolean } from "../../../modules/utils";
|
||||
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) =>
|
||||
Array.isArray(id) ?
|
||||
id.forEach(id => obj.disconnect(id))
|
||||
: obj.disconnect(id)
|
||||
));
|
||||
|
||||
return <Gtk.Box class={"media"} visible={player((pl) => pl.available)}>
|
||||
export const Media = () =>
|
||||
<Gtk.Box class={"media"} visible={secureBaseBinding<AstalMpris.Player>(createBinding(
|
||||
Player.getDefault(), "player"
|
||||
), "available", false)}>
|
||||
<Gtk.EventControllerScroll $={(self) => {
|
||||
self.set_flags(Gtk.EventControllerScrollFlags.VERTICAL)
|
||||
}} onScroll={(_, __, dy) => {
|
||||
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;
|
||||
|
||||
const players = AstalMpris.get_default().players;
|
||||
@@ -34,16 +27,16 @@ export const Media = () => {
|
||||
for(let i = 0; i < players.length; i++) {
|
||||
const pl = players[i];
|
||||
|
||||
if(pl.busName !== player.get().busName)
|
||||
if(pl.busName !== Player.getDefault().player.busName)
|
||||
continue;
|
||||
|
||||
if(dy > 0 && players[i-1]) {
|
||||
setPlayer(players[i-1]);
|
||||
Player.getDefault().player = players[i-1];
|
||||
break;
|
||||
}
|
||||
|
||||
if(dy < 0 && players[i+1]) {
|
||||
setPlayer(players[i+1]);
|
||||
Player.getDefault().player = players[i+1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -60,19 +53,33 @@ export const Media = () => {
|
||||
revealer.set_reveal_child(false);
|
||||
}}
|
||||
/>
|
||||
<Gtk.Box spacing={4} visible={player(pl => pl.available)}>
|
||||
<With value={player(pl => pl.available)}>
|
||||
<Gtk.Box spacing={4} visible={secureBaseBinding<AstalMpris.Player>(createBinding(
|
||||
Player.getDefault(), "player"
|
||||
), "available", false)
|
||||
}>
|
||||
<With value={secureBaseBinding<AstalMpris.Player>(createBinding(
|
||||
Player.getDefault(), "player"
|
||||
), "available", false)
|
||||
}>
|
||||
{(available: boolean) => available && <Gtk.Box>
|
||||
<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 =>
|
||||
title ?? tr("media.no_title"))} maxWidthChars={20} ellipsize={Pango.EllipsizeMode.END}
|
||||
<Gtk.Label class={"title"} label={secureBaseBinding<AstalMpris.Player>(createBinding(
|
||||
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}
|
||||
alpha={.3} spacing={6} />
|
||||
<Gtk.Label class={"artist"} label={createBinding(player.get(), "artist").as(artist =>
|
||||
artist ?? tr("media.no_artist"))} maxWidthChars={18} ellipsize={Pango.EllipsizeMode.END}
|
||||
<Gtk.Label class={"artist"} label={secureBaseBinding<AstalMpris.Player>(createBinding(
|
||||
Player.getDefault(), "player"
|
||||
), "artist", "").as(artist => artist ?? tr("media.no_artist"))}
|
||||
maxWidthChars={18} ellipsize={Pango.EllipsizeMode.END}
|
||||
/>
|
||||
</Gtk.Box>}
|
||||
</With>
|
||||
@@ -80,13 +87,16 @@ export const Media = () => {
|
||||
<Gtk.Revealer transitionType={Gtk.RevealerTransitionType.SLIDE_RIGHT} transitionDuration={260}
|
||||
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}>
|
||||
<Gtk.Box class={"extra button-row"}>
|
||||
<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={() => {
|
||||
const url = accessMediaUrl(player.get()).get();
|
||||
const url = Player.getMediaUrl(Player.getDefault().player);
|
||||
url && Clipboard.getDefault().copyAsync(url);
|
||||
}}
|
||||
/>
|
||||
@@ -94,25 +104,32 @@ export const Media = () => {
|
||||
<Gtk.Box class={"media-controls button-row"}>
|
||||
<Gtk.Button class={"previous"} iconName={"media-skip-backward-symbolic"}
|
||||
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 =>
|
||||
status === AstalMpris.PlaybackStatus.PAUSED ?
|
||||
<Gtk.Button class={"play-pause"} iconName={secureBaseBinding<AstalMpris.Player>(
|
||||
createBinding(Player.getDefault(), "player"),
|
||||
"playbackStatus",
|
||||
AstalMpris.PlaybackStatus.PAUSED
|
||||
).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 ?
|
||||
tr("media.play")
|
||||
: tr("media.pause")
|
||||
)} onClicked={() => player.get().play_pause()}
|
||||
: "media-playback-pause-symbolic"
|
||||
)}
|
||||
tooltipText={secureBaseBinding<AstalMpris.Player>(
|
||||
createBinding(Player.getDefault(), "player"),
|
||||
"playbackStatus",
|
||||
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"}
|
||||
tooltipText={tr("media.next")} onClicked={() => player.get().canGoNext &&
|
||||
player.get().next()}
|
||||
tooltipText={tr("media.next")} onClicked={() => Player.getDefault().player.canGoNext &&
|
||||
Player.getDefault().player.next()}
|
||||
/>
|
||||
</Gtk.Box>
|
||||
</Gtk.Box>}
|
||||
</With>
|
||||
</Gtk.Revealer>
|
||||
</Gtk.Box>
|
||||
}
|
||||
</Gtk.Box> as Gtk.Box;
|
||||
|
||||
@@ -4,17 +4,18 @@ import { PopupWindow } from "../../widget/PopupWindow";
|
||||
import { BigMedia } from "./widgets/BigMedia";
|
||||
import { time, variableToBoolean } from "../../modules/utils";
|
||||
import { createBinding } from "ags";
|
||||
import { player } from "../../modules/media";
|
||||
|
||||
import Media from "../../modules/media";
|
||||
import AstalMpris from "gi://AstalMpris";
|
||||
|
||||
|
||||
export const CenterWindow = (mon: number) =>
|
||||
<PopupWindow namespace={"center-window"} marginTop={10} monitor={mon}
|
||||
halign={Gtk.Align.CENTER} valign={Gtk.Align.START}
|
||||
actionKeyPressed={(_, keyval) => {
|
||||
if(keyval === Gdk.KEY_space) {
|
||||
player.get().available &&
|
||||
player.get().play_pause();
|
||||
Media.getDefault().player.available &&
|
||||
Media.getDefault().player.play_pause();
|
||||
return true;
|
||||
}
|
||||
}}>
|
||||
|
||||
@@ -2,19 +2,16 @@ import { createBinding, For } from "ags";
|
||||
import { register } from "ags/gobject";
|
||||
import { Astal, Gtk } from "ags/gtk4";
|
||||
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 Media from "../../../modules/media";
|
||||
import AstalMpris from "gi://AstalMpris";
|
||||
import Pango from "gi://Pango?version=1.0";
|
||||
import Adw from "gi://Adw?version=1";
|
||||
import GLib from "gi://GLib?version=2.0";
|
||||
|
||||
|
||||
let dragTimer: (GLib.Source|undefined);
|
||||
|
||||
export const BigMedia = () => {
|
||||
const availablePlayers = createBinding(AstalMpris.get_default(), "players").as(pls =>
|
||||
pls.filter(p => p.available));
|
||||
@@ -22,11 +19,11 @@ export const BigMedia = () => {
|
||||
const carousel = <Adw.Carousel orientation={Gtk.Orientation.HORIZONTAL} spacing={6}
|
||||
onPageChanged={(self, num) => {
|
||||
const page = self.get_nth_page(num);
|
||||
if(page instanceof PlayerWidget && player.get().busName !== page.player.busName)
|
||||
setPlayer(page.player);
|
||||
if(page instanceof PlayerWidget && Media.getDefault().player.busName !== page.player.busName)
|
||||
Media.getDefault().player = page.player;
|
||||
}}>
|
||||
<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} />}
|
||||
</For>
|
||||
@@ -48,6 +45,7 @@ export const BigMedia = () => {
|
||||
class PlayerWidget extends Gtk.Box {
|
||||
#player!: AstalMpris.Player;
|
||||
#copyClickTimeout?: GLib.Source;
|
||||
#dragTimer?: GLib.Source;
|
||||
|
||||
get player() { return this.#player; }
|
||||
|
||||
@@ -97,16 +95,16 @@ class PlayerWidget extends Gtk.Box {
|
||||
onChangeValue={(_, type, value) => {
|
||||
if(type == null) return;
|
||||
|
||||
if(!dragTimer) {
|
||||
dragTimer = setTimeout(() =>
|
||||
if(!this.#dragTimer) {
|
||||
this.#dragTimer = setTimeout(() =>
|
||||
player.position = Math.floor(value)
|
||||
, 200);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
dragTimer.destroy();
|
||||
dragTimer = setTimeout(() =>
|
||||
this.#dragTimer?.destroy();
|
||||
this.#dragTimer = setTimeout(() =>
|
||||
player.position = Math.floor(value)
|
||||
, 200);
|
||||
}}
|
||||
@@ -129,9 +127,9 @@ class PlayerWidget extends Gtk.Box {
|
||||
<Gtk.Box class={"extra button-row"}>
|
||||
<Gtk.Button class={"link"}
|
||||
tooltipText={tr("copy_to_clipboard")}
|
||||
visible={variableToBoolean(accessMediaUrl(player))}
|
||||
visible={variableToBoolean(Media.accessMediaUrl(player))}
|
||||
onClicked={(self) => {
|
||||
const url = accessMediaUrl(player).get();
|
||||
const url = Media.accessMediaUrl(player).get();
|
||||
// a widget that supports adding multiple icons and allows switching
|
||||
// through them would be pretty nice!! (i'll probably do this later)
|
||||
url &&
|
||||
|
||||
Reference in New Issue
Block a user