From f5a1067097d702cade831f754dc24b3835138613 Mon Sep 17 00:00:00 2001 From: Angelo Verlain Date: Tue, 24 Oct 2023 20:13:54 +0200 Subject: [PATCH] optimize binded buttons - play - repeat - shuffle --- .../ui/components/player/now-playing/view.blp | 2 + src/components/player/full.ts | 63 ++------------ src/components/player/mini.ts | 24 +---- src/components/player/now-playing/view.ts | 21 ++--- src/components/player/video/full.ts | 26 +----- src/components/player/video/mini.ts | 25 +----- src/player/helpers.ts | 87 ++++++++++++++++++- src/player/queue.ts | 39 ++++++--- src/util/action.ts | 42 +++++++++ 9 files changed, 178 insertions(+), 151 deletions(-) diff --git a/data/ui/components/player/now-playing/view.blp b/data/ui/components/player/now-playing/view.blp index 8360845f..dbbfc464 100644 --- a/data/ui/components/player/now-playing/view.blp +++ b/data/ui/components/player/now-playing/view.blp @@ -134,6 +134,7 @@ template $PlayerNowPlayingView : Adw.NavigationPage { styles [ "circular", "large", + "flat", ] } @@ -181,6 +182,7 @@ template $PlayerNowPlayingView : Adw.NavigationPage { styles [ "circular", "large", + "flat", ] } } diff --git a/src/components/player/full.ts b/src/components/player/full.ts index f521a420..3a16c653 100644 --- a/src/components/player/full.ts +++ b/src/components/player/full.ts @@ -2,7 +2,6 @@ import Gtk from "gi://Gtk?version=4.0"; import GObject from "gi://GObject"; import GLib from "gi://GLib"; -import { RepeatMode } from "../../player/queue.js"; import { PlayerScale } from "./scale.js"; import { QueueTrack } from "libmuse/types/parsers/queue.js"; import { escape_label, pretty_subtitles } from "src/util/text.js"; @@ -11,6 +10,7 @@ import { micro_to_string, seconds_to_string } from "src/util/time.js"; import { PlayerPreview } from "./preview.js"; import { SignalListeners } from "src/util/signal-listener.js"; import { get_player } from "src/application.js"; +import { bind_play_icon, bind_repeat_button } from "src/player/helpers.js"; GObject.type_ensure(PlayerPreview.$gtype); @@ -38,11 +38,7 @@ export class FullPlayerView extends Gtk.ActionBar { _title!: Gtk.Label; _subtitle!: Gtk.Label; - _shuffle_button!: Gtk.ToggleButton; - _prev_button!: Gtk.Button; - _play_button!: Gtk.Button; _play_image!: Gtk.Image; - _next_button!: Gtk.Button; _repeat_button!: Gtk.ToggleButton; _progress_label!: Gtk.Label; _duration_label!: Gtk.Label; @@ -93,15 +89,9 @@ export class FullPlayerView extends Gtk.ActionBar { this.song_changed.bind(this), ); - this.listeners.connect(this.player, "notify::is-buffering", () => { - this.update_play_button(); - }); - - this.listeners.connect(this.player, "notify::playing", () => { - this.update_play_button(); - }); - - this.update_play_button(); + this.listeners.add_bindings( + bind_play_icon(this._play_image), + ); this.listeners.connect(this.player, "notify::duration", () => { this._duration_label.label = micro_to_string(this.player.duration); @@ -109,17 +99,9 @@ export class FullPlayerView extends Gtk.ActionBar { // buttons - this.listeners.connect(this.player.queue, "notify::repeat", () => { - this.update_repeat_button(); - }); - - this.update_repeat_button(); - - this.listeners.connect(this.player.queue, "notify::shuffle", () => { - this._shuffle_button.set_active(this.player.queue.shuffle); - }); - - this._shuffle_button.set_active(this.player.queue.shuffle); + this.listeners.add_bindings( + ...bind_repeat_button(this._repeat_button), + ); // setting up volume button @@ -172,37 +154,6 @@ export class FullPlayerView extends Gtk.ActionBar { }); } - update_repeat_button() { - const repeat_mode = this.player.queue.repeat; - - this._repeat_button.set_active( - repeat_mode === RepeatMode.ALL || repeat_mode === RepeatMode.ONE, - ); - - switch (repeat_mode) { - case RepeatMode.ALL: - this._repeat_button.icon_name = "media-playlist-repeat-symbolic"; - this._repeat_button.tooltip_text = _("Repeat All Songs"); - break; - case RepeatMode.ONE: - this._repeat_button.icon_name = "media-playlist-repeat-song-symbolic"; - this._repeat_button.tooltip_text = _("Repeat the Current Song"); - break; - case RepeatMode.NONE: - this._repeat_button.icon_name = "media-playlist-consecutive-symbolic"; - this._repeat_button.tooltip_text = _("Enable Repeat"); - break; - } - } - - update_play_button() { - if (this.player.playing) { - this._play_image.icon_name = "media-playback-pause-symbolic"; - } else { - this._play_image.icon_name = "media-playback-start-symbolic"; - } - } - show_song(track: QueueTrack) { // labels diff --git a/src/components/player/mini.ts b/src/components/player/mini.ts index 6260541b..6a0349e5 100644 --- a/src/components/player/mini.ts +++ b/src/components/player/mini.ts @@ -7,6 +7,7 @@ import { QueueTrack } from "libmuse/types/parsers/queue.js"; import { MuzikaPlayer } from "src/player"; import { SignalListeners } from "src/util/signal-listener.js"; import { get_player } from "src/application.js"; +import { bind_play_icon } from "src/player/helpers.js"; export class MiniPlayerView extends Gtk.Overlay { static { @@ -59,15 +60,9 @@ export class MiniPlayerView extends Gtk.Overlay { this.song_changed.bind(this), ); - this.listeners.connect(this.player, "notify::buffering", () => { - this.update_play_button(); - }); - - this.listeners.connect(this.player, "notify::playing", () => { - this.update_play_button(); - }); - - this.update_play_button(); + this.listeners.add_bindings( + bind_play_icon(this._play_button), + ); this.listeners.connect(this.player, "notify::duration", () => { this.progress_bar.set_duration(this.player.duration); @@ -82,17 +77,6 @@ export class MiniPlayerView extends Gtk.Overlay { this.progress_bar.update_position(this.player.timestamp); } - update_play_button() { - this.progress_bar.buffering = this.player.is_buffering && - this.player.playing; - - if (this.player.playing) { - this._play_button.icon_name = "media-playback-pause-symbolic"; - } else { - this._play_button.icon_name = "media-playback-start-symbolic"; - } - } - show_song(track: QueueTrack) { this._title.label = track.title; this._subtitle.label = track.artists[0].name; diff --git a/src/components/player/now-playing/view.ts b/src/components/player/now-playing/view.ts index 13644081..e663e467 100644 --- a/src/components/player/now-playing/view.ts +++ b/src/components/player/now-playing/view.ts @@ -12,6 +12,7 @@ import { SignalListeners } from "src/util/signal-listener"; import { load_thumbnails } from "src/components/webimage"; import { micro_to_string } from "src/util/time"; import { FixedRatioThumbnail } from "src/components/fixed-ratio-thumbnail"; +import { bind_play_icon, bind_repeat_button } from "src/player/helpers"; export class PlayerNowPlayingView extends Adw.NavigationPage { static { @@ -30,6 +31,7 @@ export class PlayerNowPlayingView extends Adw.NavigationPage { "play_button", "switcher_bar", "overlay_bin", + "repeat_button", ], Properties: { switcher_stack: GObject.param_spec_object( @@ -65,6 +67,7 @@ export class PlayerNowPlayingView extends Adw.NavigationPage { private _play_button!: Gtk.Button; private _switcher_bar!: Adw.ViewSwitcherBar; private _overlay_bin!: Adw.Bin; + private _repeat_button!: Gtk.ToggleButton; get switcher_stack() { return this._switcher_bar.stack; @@ -134,22 +137,8 @@ export class PlayerNowPlayingView extends Adw.NavigationPage { }, null, ), - // @ts-expect-error incorrect types - this.player.bind_property_full( - "playing", - this._play_button, - "icon-name", - GObject.BindingFlags.DEFAULT | GObject.BindingFlags.SYNC_CREATE, - (_, __) => { - return [ - true, - this.player.playing - ? "media-playback-pause-symbolic" - : "media-playback-start-symbolic", - ]; - }, - null, - ), + bind_play_icon(this._play_button), + ...bind_repeat_button(this._repeat_button), ); } diff --git a/src/components/player/video/full.ts b/src/components/player/video/full.ts index b60c6d3a..fd315e5b 100644 --- a/src/components/player/video/full.ts +++ b/src/components/player/video/full.ts @@ -8,6 +8,7 @@ import { SignalListeners } from "src/util/signal-listener"; import { micro_to_string, seconds_to_string } from "src/util/time"; import { generate_song_menu } from "./util"; import { VolumeControls } from "./volume-controls"; +import { bind_play_icon } from "src/player/helpers"; export class FullVideoControls extends Adw.Bin { static { @@ -94,18 +95,8 @@ export class FullVideoControls extends Adw.Bin { this.media_info_changed.bind(this), ); - this.update_play_button(); - - this.listeners.connect(player, "notify::is-buffering", () => { - this.update_play_button(); - }); - - this.listeners.connect( - player, - "notify::playing", - () => { - this.update_play_button(); - }, + this.listeners.add_bindings( + bind_play_icon(this._play_button), ); this._duration_label.label = micro_to_string(player.duration); @@ -139,17 +130,6 @@ export class FullVideoControls extends Adw.Bin { ); } - private update_play_button() { - const player = get_player(); - - - if (player.playing) { - this._play_button.icon_name = "media-playback-pause-symbolic"; - } else { - this._play_button.icon_name = "media-playback-start-symbolic"; - } - } - clear_listeners() { this.inhibit_hide = false; this.listeners.clear(); diff --git a/src/components/player/video/mini.ts b/src/components/player/video/mini.ts index de465b97..fd37867e 100644 --- a/src/components/player/video/mini.ts +++ b/src/components/player/video/mini.ts @@ -7,6 +7,7 @@ import { SignalListeners } from "src/util/signal-listener"; import { micro_to_string, seconds_to_string } from "src/util/time"; import { generate_song_menu } from "./util"; import { VolumeControls } from "./volume-controls"; +import { bind_play_icon } from "src/player/helpers"; export class MiniVideoControls extends Adw.Bin { static { @@ -92,18 +93,8 @@ export class MiniVideoControls extends Adw.Bin { this.media_info_changed.bind(this), ); - this.update_play_button(); - - this.listeners.connect(player, "notify::is-buffering", () => { - this.update_play_button(); - }); - - this.listeners.connect( - player, - "notify::playing", - () => { - this.update_play_button(); - }, + this.listeners.add_bindings( + bind_play_icon(this._play_button), ); this._duration_label.label = micro_to_string(player.duration); @@ -137,16 +128,6 @@ export class MiniVideoControls extends Adw.Bin { ); } - private update_play_button() { - const player = get_player(); - - if (player.playing) { - this._play_button.icon_name = "media-playback-pause-symbolic"; - } else { - this._play_button.icon_name = "media-playback-start-symbolic"; - } - } - clear_listeners() { this.inhibit_hide = false; this.listeners.clear(); diff --git a/src/player/helpers.ts b/src/player/helpers.ts index 8a8aa7dc..a52c49cd 100644 --- a/src/player/helpers.ts +++ b/src/player/helpers.ts @@ -1,8 +1,12 @@ +import GObject from "gi://GObject"; +import Gtk from "gi://Gtk?version=4.0"; + import { get_queue, get_queue_ids, QueueOptions } from "src/muse"; import { omit } from "lodash-es"; -import { QueueSettings } from "./queue"; +import { QueueSettings, RepeatMode } from "./queue"; import type { QueueTrack } from "libmuse/types/parsers/queue"; +import { get_player } from "src/application"; function create_cache_map() { return new Map(); @@ -81,3 +85,84 @@ export async function get_song(videoId: string) { return cache_maps.songs.get(videoId)!; } + +export function bind_repeat_button(button: Gtk.ToggleButton) { + const queue = get_player().queue; + + return [ + // @ts-expect-error incorrect types + queue.bind_property_full( + "repeat", + button, + "icon-name", + GObject.BindingFlags.DEFAULT | GObject.BindingFlags.SYNC_CREATE, + () => { + let icon = "image-missing-symbolic"; + + switch (queue.repeat) { + case RepeatMode.ALL: + icon = "media-playlist-repeat-symbolic"; + break; + case RepeatMode.ONE: + icon = "media-playlist-repeat-song-symbolic"; + break; + case RepeatMode.NONE: + icon = "media-playlist-consecutive-symbolic"; + break; + } + + return [true, icon]; + }, + null, + ), + + // @ts-expect-error incorrect types + queue.bind_property_full( + "repeat", + button, + "tooltip-text", + GObject.BindingFlags.DEFAULT | GObject.BindingFlags.SYNC_CREATE, + () => { + let text: string; + + switch (queue.repeat) { + case RepeatMode.ALL: + text = _("Repeat All Songs"); + break; + case RepeatMode.ONE: + text = _("Repeat the Current Song"); + break; + case RepeatMode.NONE: + text = _("Enable Repeat"); + break; + } + + if (!text) return [false]; + + return [true, text]; + }, + null, + ), + ]; +} + +export function bind_play_icon(image: Gtk.Image | Gtk.Button) { + const player = get_player(); + + // @ts-expect-error incorrect types + return player.bind_property_full( + "playing", + image, + "icon-name", + GObject.BindingFlags.DEFAULT | GObject.BindingFlags.SYNC_CREATE, + () => { + return [ + true, + player.playing + ? "media-playback-pause-symbolic" + : "media-playback-start-symbolic", + ]; + }, + null, + ); +} diff --git a/src/player/queue.ts b/src/player/queue.ts index 1b937cfc..8874272b 100644 --- a/src/player/queue.ts +++ b/src/player/queue.ts @@ -5,7 +5,7 @@ import GLib from "gi://GLib"; import { get_queue, Queue as MuseQueue } from "../muse.js"; import { ObjectContainer } from "../util/objectcontainer.js"; import { QueueTrack } from "libmuse/types/parsers/queue.js"; -import { AddActionEntries } from "src/util/action.js"; +import { AddActionEntries, build_action } from "src/util/action.js"; import { Application } from "src/application.js"; import { Window } from "src/window.js"; import { list_model_to_array } from "src/util/list.js"; @@ -350,18 +350,6 @@ export class Queue extends GObject.Object { const action_group = Gio.SimpleActionGroup.new(); (action_group.add_action_entries as AddActionEntries)([ - { - name: "toggle-repeat", - activate: () => { - this.toggle_repeat(); - }, - }, - { - name: "toggle-shuffle", - activate: () => { - this.shuffle = !this.shuffle; - }, - }, { name: "play-playlist", parameter_type: "s", @@ -482,6 +470,31 @@ export class Queue extends GObject.Object { }, ]); + action_group.add_action(build_action({ + name: "toggle-repeat", + parameter_type: null, + state: GLib.Variant.new_boolean(false), + activate: this.toggle_repeat.bind(this), + bind_state_full: [this, "repeat", () => { + return [ + true, + GLib.Variant.new_boolean( + this.repeat == RepeatMode.NONE ? false : true, + ), + ]; + }], + })); + + action_group.add_action(build_action({ + name: "toggle-shuffle", + parameter_type: null, + state: GLib.Variant.new_boolean(false), + activate: () => this.shuffle = !this.shuffle, + bind_state_full: [this, "shuffle", () => { + return [true, GLib.Variant.new_boolean(this.shuffle)]; + }], + })); + this.connect("notify::can-play-previous", () => { action_group.action_enabled_changed("previous", this.can_play_previous); }); diff --git a/src/util/action.ts b/src/util/action.ts index 6c9491c0..e9784f7a 100644 --- a/src/util/action.ts +++ b/src/util/action.ts @@ -1,5 +1,6 @@ import GLib from "gi://GLib"; import Gio from "gi://Gio"; +import GObject from "gi://GObject"; export type ActionEntry = { name: string; @@ -16,3 +17,44 @@ export type ActionEntry = { }; export type AddActionEntries = (entries: ActionEntry[]) => void; + +export interface ActionDeclaration { + name: string; + parameter_type: GLib.VariantType | null; + state?: GLib.Variant | null; + activate?(action: Gio.SimpleAction, parameter: GLib.Variant): void; + bind_state_full?: [ + object: GObject.Object, + property: string, + transform: ( + binding: GObject.Binding, + from_value: any, + ) => [boolean, GLib.Variant], + ]; +} + +export function build_action(decl: ActionDeclaration) { + const action = new Gio.SimpleAction({ + name: decl.name, + parameter_type: decl.parameter_type ?? null as any, + state: decl.state ?? undefined, + }); + + if (decl.activate) action.connect("activate", decl.activate); + + if (decl.bind_state_full) { + const [object, property, transform] = decl.bind_state_full; + + // @ts-expect-error incorrect types + object.bind_property_full( + property, + action, + "state", + GObject.BindingFlags.DEFAULT | GObject.BindingFlags.DEFAULT, + transform, + null, + ); + } + + return action; +}