329 lines
12 KiB
Python
329 lines
12 KiB
Python
from typing import Any, Callable, Dict, Optional, Set, Tuple
|
|
|
|
from gi.repository import Gdk, GdkPixbuf, GLib, GObject, Gtk, Pango, Handy
|
|
|
|
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.EventBox):
|
|
def __init__(self, state):
|
|
super().__init__(above_child=False)
|
|
self.get_style_context().add_class("background")
|
|
|
|
self.state = state
|
|
self.state.add_control(self)
|
|
|
|
action_bar = Gtk.ActionBar()
|
|
|
|
action_bar.pack_start(self.create_song_display())
|
|
|
|
buttons = Gtk.Stack(transition_type=Gtk.StackTransitionType.CROSSFADE)
|
|
|
|
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))
|
|
|
|
buttons.add(close_buttons)
|
|
|
|
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)
|
|
|
|
def flap_reveal(*_):
|
|
if self.state.flap_open:
|
|
buttons.set_visible_child(open_buttons)
|
|
else:
|
|
buttons.set_visible_child(close_buttons)
|
|
|
|
self.state.play_queue_open = False
|
|
self.state.connect("notify::flap-open", flap_reveal)
|
|
|
|
action_bar.pack_end(buttons)
|
|
|
|
self.add(action_bar)
|
|
|
|
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="<b>Devices</b>",
|
|
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)
|
|
|
|
self.play_queue_list = Gtk.TreeView(
|
|
model=self.state.play_queue_store,
|
|
reorderable=True,
|
|
headers_visible=False,
|
|
)
|
|
self.play_queue_list.get_style_context().add_class("background")
|
|
|
|
# Album Art column. This function defines what image to use for the play queue
|
|
# song icon.
|
|
def filename_to_pixbuf(
|
|
column: Any,
|
|
cell: Gtk.CellRendererPixbuf,
|
|
model: Gtk.ListStore,
|
|
tree_iter: Gtk.TreeIter,
|
|
flags: Any,
|
|
):
|
|
cell.set_property("sensitive", model.get_value(tree_iter, 0))
|
|
filename = model.get_value(tree_iter, 1)
|
|
if not filename:
|
|
cell.set_property("icon_name", "")
|
|
return
|
|
|
|
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(filename, 50, 50, True)
|
|
|
|
# If this is the playing song, then overlay the play icon.
|
|
if model.get_value(tree_iter, 3):
|
|
play_overlay_pixbuf = GdkPixbuf.Pixbuf.new_from_file(
|
|
str(resolve_path("ui/images/play-queue-play.png"))
|
|
)
|
|
|
|
play_overlay_pixbuf.composite(
|
|
pixbuf, 0, 0, 50, 50, 0, 0, 1, 1, GdkPixbuf.InterpType.NEAREST, 200
|
|
)
|
|
|
|
cell.set_property("pixbuf", pixbuf)
|
|
|
|
renderer = Gtk.CellRendererPixbuf()
|
|
renderer.set_fixed_size(55, 60)
|
|
column = Gtk.TreeViewColumn("", renderer)
|
|
column.set_cell_data_func(renderer, filename_to_pixbuf)
|
|
column.set_resizable(True)
|
|
self.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)
|
|
self.play_queue_list.append_column(column)
|
|
|
|
renderer = Gtk.CellRendererPixbuf(icon_name="view-more-symbolic")
|
|
renderer.set_fixed_size(30, 24)
|
|
column = Gtk.TreeViewColumn("", renderer)
|
|
column.set_resizable(True)
|
|
self.play_queue_list.append_column(column)
|
|
|
|
renderer = Gtk.CellRendererPixbuf(icon_name="window-close-symbolic")
|
|
renderer.set_fixed_size(30, 24)
|
|
column = Gtk.TreeViewColumn("", renderer)
|
|
column.set_resizable(True)
|
|
self.play_queue_list.append_column(column)
|
|
|
|
play_queue_scrollbox.add(self.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)
|