⚡ 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 { 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)
|
||||||
);
|
);
|
||||||
|
|||||||
+89
-15
@@ -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,22 +95,57 @@ 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(() => {
|
||||||
createScopedConnection(
|
if(communicationMethod instanceof Gio.SocketService) {
|
||||||
service, "incoming", (conn) => {
|
createScopedConnection(
|
||||||
try {
|
communicationMethod, "incoming", (conn) => {
|
||||||
return handleIncoming(conn);
|
try {
|
||||||
} catch(_) {}
|
return handleIncoming(conn);
|
||||||
|
} catch(_) {}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
+65
-56
@@ -1,77 +1,86 @@
|
|||||||
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" })
|
||||||
available: false,
|
export default class Media extends GObject.Object {
|
||||||
busName: "dummy_player",
|
private static instance: Media;
|
||||||
bus_name: "dummy_player"
|
public static readonly dummyPlayer = {
|
||||||
} as AstalMpris.Player;
|
available: false,
|
||||||
|
busName: "dummy_player",
|
||||||
|
bus_name: "dummy_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;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
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]) {
|
||||||
|
this.player = players[0];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.player = Media.dummyPlayer;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
createRoot((dispose) => {
|
public static getDefault(): Media {
|
||||||
const connections = new Map<GObject.Object, Array<number>>();
|
if(!this.instance)
|
||||||
disposeFun = dispose;
|
this.instance = new Media(getScope());
|
||||||
|
|
||||||
setPlayer(AstalMpris.get_default().players[0] ?? dummyPlayer);
|
return this.instance;
|
||||||
|
}
|
||||||
|
|
||||||
connections.set(AstalMpris.get_default(), [
|
public static accessMediaUrl(player: AstalMpris.Player): Accessor<string|undefined> {
|
||||||
AstalMpris.get_default().connect("player-added", (_, player) =>
|
return createConnection(player.get_meta("xesam:url"),
|
||||||
player.available && setPlayer(player)),
|
[player, "notify::metadata", () => player.get_meta("xesam:url")]
|
||||||
|
).as(url => {
|
||||||
|
const byteString = url?.get_data_as_bytes();
|
||||||
|
|
||||||
AstalMpris.get_default().connect("player-closed", (_, closedPlayer) => {
|
return byteString ?
|
||||||
const players = AstalMpris.get_default().players.filter(pl => pl?.available &&
|
decoder.decode(byteString.toArray())
|
||||||
pl.busName !== closedPlayer.busName);
|
: undefined;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if(players.length > 0 && players[0]) {
|
|
||||||
setPlayer(players[0]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setPlayer(dummyPlayer);
|
public static getMediaUrl(player: AstalMpris.Player): string|undefined {
|
||||||
})
|
if(!player.available) return;
|
||||||
]);
|
|
||||||
|
|
||||||
onCleanup(() => {
|
const meta = player.get_meta("xesam:url");
|
||||||
connections.forEach((ids, obj) =>
|
const byteString = meta?.get_data_as_bytes();
|
||||||
Array.isArray(ids) ?
|
|
||||||
ids.forEach(id => obj.disconnect(id))
|
|
||||||
: obj.disconnect(ids)
|
|
||||||
);
|
|
||||||
disposeFun = undefined;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function accessMediaUrl(player: AstalMpris.Player): Accessor<string|undefined> {
|
|
||||||
return createConnection(player.get_meta("xesam:url"),
|
|
||||||
[player, "notify::metadata", () => player.get_meta("xesam:url")]
|
|
||||||
).as(url => {
|
|
||||||
const byteString = url?.get_data_as_bytes();
|
|
||||||
|
|
||||||
return byteString ?
|
return byteString ?
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|||||||
+64
-18
@@ -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;
|
||||||
|
|||||||
@@ -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"),
|
||||||
"media-playback-start-symbolic"
|
"playbackStatus",
|
||||||
: "media-playback-pause-symbolic")}
|
AstalMpris.PlaybackStatus.PAUSED
|
||||||
tooltipText={createBinding(player.get(), "playbackStatus").as(status =>
|
).as(status => status === AstalMpris.PlaybackStatus.PAUSED ?
|
||||||
status === AstalMpris.PlaybackStatus.PAUSED ?
|
"media-playback-start-symbolic"
|
||||||
tr("media.play")
|
: "media-playback-pause-symbolic"
|
||||||
: tr("media.pause")
|
)}
|
||||||
)} onClicked={() => player.get().play_pause()}
|
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"}
|
<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,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;
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
|
|||||||
@@ -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 &&
|
||||||
|
|||||||
Reference in New Issue
Block a user