from typing import Any, Callable, Dict, Optional, Set, Tuple from gi.repository import Gdk, GdkPixbuf, GLib, GObject, Gtk, Pango, Handy import bleach from .. import util from ..common import IconButton, IconToggleButton, SpinnerImage, SpinnerPicture from ...config import AppConfiguration from ...util import resolve_path from . import common class MobileHandle(Gtk.ActionBar): def __init__(self, state): super().__init__() self.get_style_context().add_class("background") self.state = state self.state.add_control(self) self.pack_start(self.create_song_display()) buttons = Gtk.Overlay() open_buttons = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, halign=Gtk.Align.END) self.play_queue_button = IconToggleButton( "view-list-symbolic", "Open play queue", relief=True, icon_size=Gtk.IconSize.LARGE_TOOLBAR, ) def play_queue_button_toggled(*_): if self.play_queue_button.get_active() != self.state.play_queue_open: self.state.play_queue_open = self.play_queue_button.get_active() self.play_queue_button.connect("toggled", play_queue_button_toggled) def on_play_queue_open(*_): self.play_queue_button.set_active(self.state.play_queue_open) self.state.connect("notify::play-queue-open", on_play_queue_open) open_buttons.pack_start(self.play_queue_button, False, False, 5) self.menu_button = IconButton( "view-more-symbolic", "Menu", relief=True, icon_size=Gtk.IconSize.LARGE_TOOLBAR) open_buttons.pack_start(self.menu_button, False, False, 5) buttons.add(open_buttons) close_buttons = Handy.Squeezer(halign=Gtk.Align.END, homogeneous=False) expanded_close_buttons = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) expanded_close_buttons.pack_start(common.create_prev_button(self.state), False, False, 5) expanded_close_buttons.pack_start(common.create_play_button(self.state, large=False), False, False, 5) expanded_close_buttons.pack_start(common.create_next_button(self.state), False, False, 5) close_buttons.add(expanded_close_buttons) close_buttons.add(common.create_play_button(self.state, large=False, halign=Gtk.Align.END)) close_buttons.get_style_context().add_class("background") buttons.add_overlay(close_buttons) def flap_reveal(*_): progress = 1 - self.state.flap_reveal_progress close_buttons.set_opacity(progress) close_buttons.set_visible(progress > 0.05) if progress < 0.8: self.state.play_queue_open = False self.state.connect("notify::flap-reveal-progress", flap_reveal) self.pack_end(buttons) def create_song_display(self): box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, hexpand=True) box.set_property('width-request', 200) self.cover_art = SpinnerImage( image_name="player-controls-album-artwork", image_size=50, ) box.pack_start(self.cover_art, False, False, 0) details_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) def make_label(name: str) -> Gtk.Label: return Gtk.Label( name=name, halign=Gtk.Align.START, xalign=0, use_markup=True, ellipsize=Pango.EllipsizeMode.END, ) self.song_title = make_label("song-title") details_box.add(self.song_title) self.artist_name = make_label("artist-name") details_box.add(self.artist_name) box.pack_start(details_box, False, False, 5) return box def update(self, app_config: AppConfiguration, force: bool = False): if app_config.state.current_song is not None: self.song_title.set_markup(bleach.clean(app_config.state.current_song.title)) # TODO (#71): use walrus once MYPY gets its act together album = app_config.state.current_song.album artist = app_config.state.current_song.artist if artist: self.artist_name.set_markup(bleach.clean(artist.name)) self.artist_name.show() elif album: self.artist_name.set_markup(bleach.clean(album.name)) self.artist_name.show() else: self.artist_name.set_markup("") self.artist_name.hide() else: # Clear out the cover art and song tite if no song self.song_title.set_markup("") self.artist_name.set_markup("") def set_cover_art(self, cover_art_filename: str, loading: bool): self.cover_art.set_from_file(cover_art_filename) self.cover_art.set_loading(loading) class MobileFlap(Handy.Flap): def __init__(self, state): super().__init__(orientation=Gtk.Orientation.VERTICAL, fold_policy=Handy.FlapFoldPolicy.ALWAYS, vexpand=True, swipe_to_open=False) self.state = state self.state.add_control(self) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, vexpand=True) box.get_style_context().add_class("background") box.pack_start(Gtk.Separator(), False, False, 0) overlay = Gtk.Overlay(hexpand=True, vexpand=True, height_request=100) self.cover_art = SpinnerPicture(image_name="player-controls-album-artwork") overlay.add(self.cover_art) overlay_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) overlay.add_overlay(overlay_box) overlay_row_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) overlay_box.pack_end(overlay_row_box, False, False, 0) overlay_row_box.pack_start(common.create_label(self.state, "progress-label"), False, False, 5) overlay_row_box.pack_end(common.create_label(self.state, "duration-label"), False, False, 5) # Device self.device_button = IconButton( "chromecast-symbolic", "Show available audio output devices", icon_size=Gtk.IconSize.LARGE_TOOLBAR, ) def on_device_click(_: Any): if self.device_popover.is_visible(): self.device_popover.popdown() else: self.device_popover.popup() self.device_popover.show_all() self.device_button.connect("clicked", self.state.popup_devices) overlay_row_box.set_center_widget(self.device_button) self.device_popover = Gtk.PopoverMenu(modal=False, name="device-popover") self.device_popover.set_relative_to(self.device_button) device_popover_box = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, name="device-popover-box", ) device_popover_header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.popover_label = Gtk.Label( label="Devices", use_markup=True, halign=Gtk.Align.START, margin=5, ) device_popover_header.add(self.popover_label) refresh_devices = IconButton("view-refresh-symbolic", "Refresh device list") refresh_devices.set_action_name("app.refresh-devices") device_popover_header.pack_end(refresh_devices, False, False, 0) device_popover_box.add(device_popover_header) device_list_and_loading = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.device_list = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) device_list_and_loading.add(self.device_list) device_popover_box.pack_end(device_list_and_loading, True, True, 0) self.device_popover.add(device_popover_box) box.pack_start(overlay, True, True, 0) # Song Scrubber scrubber = Gtk.Scale( orientation=Gtk.Orientation.HORIZONTAL, adjustment=self.state.scrubber, name="song-scrubber", draw_value=False, restrict_to_fill_level=False) self.state.bind_property("scrubber-cache", scrubber, "fill-level") box.pack_start(scrubber, False, False, 0) buttons = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) # Repeat button repeat_button = common.create_repeat_button(self.state, valign=Gtk.Align.CENTER, margin_left=10) buttons.pack_start(repeat_button, False, False, 0) center_buttons = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) # Previous button center_buttons.add(common.create_prev_button(self.state, valign=Gtk.Align.CENTER)) # Play button center_buttons.add(common.create_play_button(self.state)) # Next button center_buttons.add(common.create_next_button(self.state, valign=Gtk.Align.CENTER)) buttons.set_center_widget(center_buttons) # Shuffle button shuffle_button = common.create_shuffle_button(self.state, valign=Gtk.Align.CENTER, margin_right=10) buttons.pack_end(shuffle_button, False, False, 0) box.pack_start(buttons, False, False, 0) box.pack_start(Gtk.Box(), False, False, 5) self.set_content(box) play_queue_scrollbox = Gtk.ScrolledWindow(vexpand=True) play_queue_list = Gtk.TreeView( model=self.state.play_queue_store, reorderable=True, headers_visible=False, ) play_queue_list.get_style_context().add_class("background") renderer = Gtk.CellRendererPixbuf() renderer.set_fixed_size(55, 60) column = Gtk.TreeViewColumn("", renderer) column.set_cell_data_func(renderer, common.filename_to_pixbuf(self.state)) column.set_resizable(True) play_queue_list.append_column(column) renderer = Gtk.CellRendererText(markup=True, ellipsize=Pango.EllipsizeMode.END) column = Gtk.TreeViewColumn("", renderer, markup=2, sensitive=0) column.set_expand(True) play_queue_list.append_column(column) renderer = Gtk.CellRendererPixbuf(icon_name="view-more-symbolic") renderer.set_fixed_size(45, 60) view_more_column = Gtk.TreeViewColumn("", renderer) view_more_column.set_resizable(True) play_queue_list.append_column(view_more_column) renderer = Gtk.CellRendererPixbuf(icon_name="window-close-symbolic") renderer.set_fixed_size(45, 60) close_column = Gtk.TreeViewColumn("", renderer) close_column.set_resizable(True) play_queue_list.append_column(close_column) def on_play_queue_button_press(tree: Any, event: Gdk.EventButton) -> bool: if event.button == 1: clicked_path = tree.get_path_at_pos(event.x, event.y)[0] song_idx = clicked_path.get_indices()[0] tree_width = tree.get_allocation().width if event.x > tree_width - close_column.get_width(): self.state.emit("songs-removed", [song_idx]) elif event.x > tree_width - close_column.get_width() - view_more_column.get_width(): common.show_play_queue_popover(self.state, play_queue_list, [clicked_path], event.x, event.y) else: self.state.emit("song-clicked", song_idx, [m[-1] for m in self.state.play_queue_store], {"no_reshuffle": True}) return True return False play_queue_list.connect("button-press-event", on_play_queue_button_press) play_queue_scrollbox.add(play_queue_list) self.set_flap(play_queue_scrollbox) def on_play_queue_open(*_): if not self.get_child_visible(): self.set_reveal_flap(False) return self.set_reveal_flap(self.state.play_queue_open) self.state.connect("notify::play-queue-open", on_play_queue_open) def update(self, app_config: AppConfiguration, force: bool = False): pass def set_cover_art(self, cover_art_filename: str, loading: bool): self.cover_art.set_from_file(cover_art_filename) self.cover_art.set_loading(loading)