This commit is contained in:
Benjamin Schaaf
2021-02-06 23:51:54 +11:00
parent d7d774c579
commit 56ae24b479
5 changed files with 329 additions and 89 deletions

View File

@@ -15,7 +15,7 @@ from ..adapters import (
) )
from ..config import AppConfiguration from ..config import AppConfiguration
from ..ui import util from ..ui import util
from ..ui.common import AlbumWithSongs, IconButton, LoadError, SpinnerImage from ..ui.common import AlbumWithSongs, IconButton, LoadError, SpinnerImage, Sizer
def _to_type(query_type: AlbumSearchQuery.Type) -> str: def _to_type(query_type: AlbumSearchQuery.Type) -> str:
@@ -46,7 +46,7 @@ def _from_str(type_str: str) -> AlbumSearchQuery.Type:
}[type_str] }[type_str]
class AlbumsPanel(Gtk.Box): class AlbumsPanel(Handy.Leaflet):
__gsignals__ = { __gsignals__ = {
"song-clicked": ( "song-clicked": (
GObject.SignalFlags.RUN_FIRST, GObject.SignalFlags.RUN_FIRST,
@@ -60,18 +60,25 @@ class AlbumsPanel(Gtk.Box):
), ),
} }
current_query: AlbumSearchQuery = AlbumSearchQuery(AlbumSearchQuery.Type.RANDOM)
offline_mode = False offline_mode = False
provider_id: Optional[str] = None
populating_genre_combo = False populating_genre_combo = False
grid_order_token: int = 0
album_sort_direction: str = "ascending" album_sort_direction: str = "ascending"
album_page_size: int = 30 # album_page_size: int = 30
album_page: int = 0 album_page: int = 0
grid_pages_count: int = 0 grid_pages_count: int = 0
def __init__(self): current_albums_result: Result = None
super().__init__(orientation=Gtk.Orientation.VERTICAL)
leaflet = Handy.Leaflet(transition_type=Handy.LeafletTransitionType.SLIDE, can_swipe_forward=False) albums = []
albums_by_id = {}
def __init__(self):
super().__init__(transition_type=Handy.LeafletTransitionType.SLIDE, can_swipe_forward=False, interpolate_size=False)
self.grid_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
actionbar = Gtk.ActionBar() actionbar = Gtk.ActionBar()
@@ -123,6 +130,57 @@ class AlbumsPanel(Gtk.Box):
self.sort_toggle.connect("clicked", self.on_sort_toggle_clicked) self.sort_toggle.connect("clicked", self.on_sort_toggle_clicked)
actionbar.pack_start(self.sort_toggle) actionbar.pack_start(self.sort_toggle)
self.refresh_button = IconButton(
"view-refresh-symbolic", "Refresh list of albums", relief=True
)
self.refresh_button.connect("clicked", self.on_refresh_clicked)
actionbar.pack_end(self.refresh_button)
# actionbar.pack_end(Gtk.Label(label="albums per page"))
# self.show_count_dropdown, _ = self.make_combobox(
# ((x, x, True) for x in ("20", "30", "40", "50")),
# self.on_show_count_dropdown_change,
# )
# actionbar.pack_end(self.show_count_dropdown)
# actionbar.pack_end(Gtk.Label(label="Show"))
self.grid_box.pack_start(actionbar, False, False, 0)
# 700 shows ~3 albums
grid_sizer = Sizer(natural_width=700)
scrolled_window = Gtk.ScrolledWindow(
hscrollbar_policy=Gtk.PolicyType.NEVER)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.grid = Gtk.FlowBox(
hexpand=True,
margin_top=5,
row_spacing=5,
column_spacing=5,
homogeneous=True,
max_children_per_line=999,
valign=Gtk.Align.START,
halign=Gtk.Align.CENTER,
selection_mode=Gtk.SelectionMode.SINGLE)
self.grid.connect("child-activated", self.on_album_clicked)
# self.grid.connect(
# "song-clicked",
# lambda _, *args: self.emit("song-clicked", *args),
# )
# self.grid.connect(
# "refresh-window",
# lambda _, *args: self.emit("refresh-window", *args),
# )
# self.grid.connect("cover-clicked", self.on_album_clicked)
# self.grid.connect("num-pages-changed", self.on_grid_num_pages_changed)
box.add(self.grid)
bottom_actionbar = Gtk.ActionBar()
# Add the page widget. # Add the page widget.
page_widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) page_widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
self.prev_page = IconButton( self.prev_page = IconButton(
@@ -145,39 +203,46 @@ class AlbumsPanel(Gtk.Box):
) )
self.next_page.connect("clicked", self.on_next_page_clicked) self.next_page.connect("clicked", self.on_next_page_clicked)
page_widget.add(self.next_page) page_widget.add(self.next_page)
actionbar.set_center_widget(page_widget) bottom_actionbar.set_center_widget(page_widget)
self.refresh_button = IconButton( box.add(bottom_actionbar)
"view-refresh-symbolic", "Refresh list of albums", relief=True
)
self.refresh_button.connect("clicked", self.on_refresh_clicked)
actionbar.pack_end(self.refresh_button)
actionbar.pack_end(Gtk.Label(label="albums per page")) scrolled_window.add(box)
self.show_count_dropdown, _ = self.make_combobox( grid_sizer.add(scrolled_window)
((x, x, True) for x in ("20", "30", "40", "50")), self.grid_box.pack_start(grid_sizer, True, True, 0)
self.on_show_count_dropdown_change,
)
actionbar.pack_end(self.show_count_dropdown)
actionbar.pack_end(Gtk.Label(label="Show"))
# self.add(actionbar) self.add(self.grid_box)
leaflet.add(actionbar)
self.details_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.details_box.get_style_context().add_class("details-panel")
details_top_bar_revealer = Gtk.Revealer(reveal_child=False)
details_top_bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
back_button = IconButton("go-previous-symbolic")
def back_clicked(*_):
self.set_visible_child(self.grid_box)
back_button.connect("clicked", back_clicked)
details_top_bar.pack_start(back_button, False, False, 0)
details_top_bar_revealer.add(details_top_bar)
self.details_box.pack_start(details_top_bar_revealer, False, False, 0)
self.album_with_songs = AlbumWithSongs(cover_art_size=100)
self.details_box.pack_start(self.album_with_songs, True, True, 0)
self.add(self.details_box)
def folded_changed(*_):
if not self.get_folded():
self.set_visible_child(self.grid_box)
details_top_bar_revealer.set_reveal_child(self.get_folded())
self.connect("notify::folded", folded_changed)
scrolled_window = Gtk.ScrolledWindow()
self.grid = AlbumsGrid()
self.grid.connect(
"song-clicked",
lambda _, *args: self.emit("song-clicked", *args),
)
self.grid.connect(
"refresh-window",
lambda _, *args: self.emit("refresh-window", *args),
)
self.grid.connect("cover-clicked", self.on_grid_cover_clicked)
self.grid.connect("num-pages-changed", self.on_grid_num_pages_changed)
scrolled_window.add(self.grid)
self.add(scrolled_window)
def make_combobox( def make_combobox(
self, self,
@@ -234,7 +299,7 @@ class AlbumsPanel(Gtk.Box):
except Exception: except Exception:
self.updating_query = False self.updating_query = False
def update(self, app_config: AppConfiguration = None, force: bool = False): def update(self, app_config: AppConfiguration, force: bool = False):
self.updating_query = True self.updating_query = True
supported_type_strings = { supported_type_strings = {
@@ -246,9 +311,38 @@ class AlbumsPanel(Gtk.Box):
# (En|Dis)able getting genres. # (En|Dis)able getting genres.
self.sort_type_combo_store[1][2] = AdapterManager.can_get_genres() self.sort_type_combo_store[1][2] = AdapterManager.can_get_genres()
if app_config: if (self.current_query != app_config.state.current_album_search_query
or self.offline_mode != app_config.offline_mode
or self.provider_id != app_config.current_provider_id):
self.current_query = app_config.state.current_album_search_query self.current_query = app_config.state.current_album_search_query
self.offline_mode = app_config.offline_mode self.offline_mode = app_config.offline_mode
self.provider_id = app_config.current_provider_id
if self.current_albums_result is not None:
self.current_albums_result.cancel()
self.current_albums_result = AdapterManager.get_albums(
self.current_query, use_ground_truth_adapter=force)
if self.current_albums_result.data_is_available:
# Don't idle add if the data is already available.
self.current_albums_result.add_done_callback(self._albums_loaded)
else:
# self.spinner.show()
self.current_albums_result.add_done_callback(
lambda f: GLib.idle_add(self._albums_received, f)
)
if (self.album_sort_direction != app_config.state.album_sort_direction
or self.album_page != app_config.state.album_page):
self.album_sort_direction = app_config.state.album_sort_direction
self.album_page = app_config.state.album_page
self._update_albums()
# self.current_query = app_config.state.current_album_search_query
# self.offline_mode = app_config.offline_mode
self.alphabetical_type_combo.set_active_id( self.alphabetical_type_combo.set_active_id(
{ {
@@ -266,7 +360,6 @@ class AlbumsPanel(Gtk.Box):
# Update the page display # Update the page display
if app_config: if app_config:
self.album_page = app_config.state.album_page self.album_page = app_config.state.album_page
self.album_page_size = app_config.state.album_page_size
self.refresh_button.set_sensitive(not app_config.offline_mode) self.refresh_button.set_sensitive(not app_config.offline_mode)
self.prev_page.set_sensitive(self.album_page > 0) self.prev_page.set_sensitive(self.album_page > 0)
@@ -309,16 +402,93 @@ class AlbumsPanel(Gtk.Box):
+ self._get_opposite_sort_dir(self.album_sort_direction) + self._get_opposite_sort_dir(self.album_sort_direction)
) )
self.show_count_dropdown.set_active_id(
str(app_config.state.album_page_size)
)
# Has to be last because it resets self.updating_query # Has to be last because it resets self.updating_query
self.populate_genre_combo(app_config, force=force) self.populate_genre_combo(app_config, force=force)
# At this point, the current query should be totally updated. selected_album = self.albums_by_id.get(app_config.state.selected_album_id, None)
self.grid_order_token = self.grid.update_params(app_config) self.album_with_songs.update(selected_album, app_config, force=force)
self.grid.update(self.grid_order_token, app_config, force=force)
def _albums_loaded(self, result: Result[Iterable[API.Album]]):
self.current_albums_result = None
# TODO: Error handling
self.albums = []
try:
self.albums = list(result.result())
except CacheMissError as e:
print(e)
except Exception as e:
print(e)
self.albums_by_id = {album.id: album for album in self.albums}
self._update_albums()
def _update_albums(self):
# Update ordering
if self.album_sort_direction == "descending":
self.albums.reverse()
# TODO: Update instead of re-create
for child in self.grid.get_children():
self.grid.remove(child)
for album in self.albums:
self.grid.add(self._create_cover_art_widget(album))
def _make_label(self, text: str, name: str) -> Gtk.Label:
return Gtk.Label(
name=name,
label=text,
tooltip_text=text,
ellipsize=Pango.EllipsizeMode.END,
max_width_chars=22,
halign=Gtk.Align.START,
)
def _create_cover_art_widget(self, album) -> Gtk.Box:
widget_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
# Cover art image
artwork = SpinnerImage(
loading=False,
image_name="grid-artwork",
spinner_name="grid-artwork-spinner",
image_size=200,
)
widget_box.pack_start(artwork, False, False, 0)
# Header for the widget
header_label = self._make_label(album.name, "grid-header-label")
widget_box.pack_start(header_label, False, False, 0)
# Extra info for the widget
info_text = util.dot_join(
album.artist.name if album.artist else "-", album.year
)
if info_text:
info_label = self._make_label(info_text, "grid-info-label")
widget_box.pack_start(info_label, False, False, 0)
# Download the cover art.
def on_artwork_downloaded(filename: Result[str]):
artwork.set_from_file(filename.result())
artwork.set_loading(False)
cover_art_filename_future = AdapterManager.get_cover_art_uri(
album.cover_art, "file"
)
if cover_art_filename_future.data_is_available:
on_artwork_downloaded(cover_art_filename_future)
else:
artwork.set_loading(True)
cover_art_filename_future.add_done_callback(
lambda f: GLib.idle_add(on_artwork_downloaded, f)
)
widget_box.show_all()
return widget_box
def _get_opposite_sort_dir(self, sort_dir: str) -> str: def _get_opposite_sort_dir(self, sort_dir: str) -> str:
return ("ascending", "descending")[0 if sort_dir == "descending" else 1] return ("ascending", "descending")[0 if sort_dir == "descending" else 1]
@@ -469,10 +639,15 @@ class AlbumsPanel(Gtk.Box):
False, False,
) )
def on_grid_cover_clicked(self, grid: Any, id: str): def on_album_clicked(self, _:Any, child: Gtk.FlowBoxChild):
album = self.albums[child.get_index()]
if self.get_folded() and self.get_visible_child() == self.grid_box:
self.set_visible_child(self.details_box)
self.emit( self.emit(
"refresh-window", "refresh-window",
{"selected_album_id": id}, {"selected_album_id": album.id},
False, False,
) )

View File

@@ -339,3 +339,7 @@ entry.invalid {
inset 0 -5px 5px @box_shadow_color; inset 0 -5px 5px @box_shadow_color;
background-color: @box_shadow_color; background-color: @box_shadow_color;
} }
.details-panel {
box-shadow: inset 5px 0 5px @box_shadow_color;
}

View File

@@ -1,6 +1,7 @@
from .album_with_songs import AlbumWithSongs from .album_with_songs import AlbumWithSongs
from .icon_button import IconButton, IconMenuButton, IconToggleButton from .icon_button import IconButton, IconMenuButton, IconToggleButton
from .load_error import LoadError from .load_error import LoadError
from .sizer import Sizer
from .song_list_column import SongListColumn from .song_list_column import SongListColumn
from .spinner_image import SpinnerImage from .spinner_image import SpinnerImage
from .spinner_picture import SpinnerPicture from .spinner_picture import SpinnerPicture
@@ -11,6 +12,7 @@ __all__ = (
"IconMenuButton", "IconMenuButton",
"IconToggleButton", "IconToggleButton",
"LoadError", "LoadError",
"Sizer",
"SongListColumn", "SongListColumn",
"SpinnerImage", "SpinnerImage",
"SpinnerPicture", "SpinnerPicture",

View File

@@ -23,55 +23,43 @@ class AlbumWithSongs(Gtk.Box):
), ),
} }
album = None
offline_mode = True offline_mode = True
cover_art_result = None
def __init__( def __init__(
self, self,
album: API.Album,
cover_art_size: int = 200, cover_art_size: int = 200,
show_artist_name: bool = True, show_artist_name: bool = True,
): ):
Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL) Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
self.album = album
self.show_artist_name = show_artist_name
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
artist_artwork = SpinnerImage( self.artist_artwork = SpinnerImage(
loading=False, loading=False,
image_name="artist-album-list-artwork", image_name="artist-album-list-artwork",
spinner_name="artist-artwork-spinner", spinner_name="artist-artwork-spinner",
image_size=cover_art_size, image_size=cover_art_size,
) )
# Account for 10px margin on all sides with "+ 20". # Account for 10px margin on all sides with "+ 20".
# artist_artwork.set_size_request(cover_art_size + 20, cover_art_size + 20) self.artist_artwork.set_size_request(cover_art_size + 20, cover_art_size + 20)
box.pack_start(artist_artwork, False, False, 0) box.pack_start(self.artist_artwork, False, False, 0)
box.pack_start(Gtk.Box(), True, True, 0) box.pack_start(Gtk.Box(), True, True, 0)
self.pack_start(box, False, False, 0) self.pack_start(box, False, False, 0)
def cover_art_future_done(f: Result):
artist_artwork.set_from_file(f.result())
artist_artwork.set_loading(False)
cover_art_filename_future = AdapterManager.get_cover_art_uri(
album.cover_art,
"file",
before_download=lambda: artist_artwork.set_loading(True),
)
cover_art_filename_future.add_done_callback(
lambda f: GLib.idle_add(cover_art_future_done, f)
)
album_details = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) album_details = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
album_title_and_buttons = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) album_title_and_buttons = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
# TODO (#43): deal with super long-ass titles # TODO (#43): deal with super long-ass titles
album_title_and_buttons.add( self.title = Gtk.Label(
Gtk.Label(
label=album.name,
name="artist-album-list-album-name", name="artist-album-list-album-name",
halign=Gtk.Align.START, halign=Gtk.Align.START,
ellipsize=Pango.EllipsizeMode.END, ellipsize=Pango.EllipsizeMode.END,
) )
) album_title_and_buttons.add(self.title)
self.play_btn = IconButton( self.play_btn = IconButton(
"media-playback-start-symbolic", "media-playback-start-symbolic",
@@ -113,20 +101,11 @@ class AlbumWithSongs(Gtk.Box):
album_details.add(album_title_and_buttons) album_details.add(album_title_and_buttons)
stats: List[Any] = [ self.stats = Gtk.Label(
album.artist.name if show_artist_name and album.artist else None,
album.year,
album.genre.name if album.genre else None,
util.format_sequence_duration(album.duration) if album.duration else None,
]
album_details.add(
Gtk.Label(
label=util.dot_join(*stats),
halign=Gtk.Align.START, halign=Gtk.Align.START,
margin_left=10, margin_left=10,
) )
) album_details.add(self.stats)
self.loading_indicator_container = Gtk.Box() self.loading_indicator_container = Gtk.Box()
album_details.add(self.loading_indicator_container) album_details.add(self.loading_indicator_container)
@@ -169,7 +148,7 @@ class AlbumWithSongs(Gtk.Box):
self.pack_end(album_details, True, True, 0) self.pack_end(album_details, True, True, 0)
self.update_album_songs(album.id) # self.update_album_songs(album.id)
# Event Handlers # Event Handlers
# ========================================================================= # =========================================================================
@@ -257,7 +236,9 @@ class AlbumWithSongs(Gtk.Box):
def deselect_all(self): def deselect_all(self):
self.album_songs.get_selection().unselect_all() self.album_songs.get_selection().unselect_all()
def update(self, app_config: AppConfiguration = None, force: bool = False): def update(self, album: API.Album, app_config: AppConfiguration, force: bool = False):
update_songs = False
if app_config: if app_config:
# Deselect everything and reset the error container if switching between # Deselect everything and reset the error container if switching between
# online and offline. # online and offline.
@@ -266,8 +247,42 @@ class AlbumWithSongs(Gtk.Box):
for c in self.error_container.get_children(): for c in self.error_container.get_children():
self.error_container.remove(c) self.error_container.remove(c)
update_songs = True
self.offline_mode = app_config.offline_mode self.offline_mode = app_config.offline_mode
if album != self.album:
self.album = album
self.title.set_label(album.name)
self.stats.set_label(util.dot_join(
album.artist.name if self.show_artist_name and album.artist else None,
album.year,
album.genre.name if album.genre else None,
util.format_sequence_duration(album.duration) if album.duration else None,
))
if self.cover_art_result is not None:
self.cover_art_result.cancel()
def cover_art_future_done(f: Result):
self.artist_artwork.set_from_file(f.result())
self.artist_artwork.set_loading(False)
self.cover_art_result = None
self.cover_art_result = AdapterManager.get_cover_art_uri(
album.cover_art,
"file",
before_download=lambda: self.artist_artwork.set_loading(True),
)
self.cover_art_result.add_done_callback(
lambda f: GLib.idle_add(cover_art_future_done, f)
)
update_songs = True
if update_songs:
self.update_album_songs(self.album.id, app_config=app_config, force=force) self.update_album_songs(self.album.id, app_config=app_config, force=force)
def set_loading(self, loading: bool): def set_loading(self, loading: bool):

View File

@@ -0,0 +1,44 @@
from gi.repository import Gtk, GObject
class Sizer(Gtk.Bin):
""" A widget that lets you control the natural size like the size request """
natural_width = GObject.Property(type=int, default=0)
natural_height = GObject.Property(type=int, default=0)
def __init__(self, **kwargs):
Gtk.Bin.__init__(self, **kwargs)
def do_get_preferred_width(self) -> (int, int):
minimum, natural = Gtk.Bin.do_get_preferred_width(self)
if self.natural_width > 0:
natural = max(minimum, self.natural_width)
return (minimum, natural)
def do_get_preferred_height(self) -> (int, int):
minimum, natural = Gtk.Bin.do_get_preferred_height(self)
if self.natural_height > 0:
natural = max(minimum, self.natural_height)
return (minimum, natural)
def do_get_preferred_width_for_height(self, height: int) -> (int, int):
minimum, natural = Gtk.Bin.do_get_preferred_width_for_height(self, height)
if self.natural_width > 0:
natural = max(minimum, self.natural_width)
return (minimum, natural)
def do_get_preferred_height_for_width(self, width: int) -> (int, int):
minimum, natural = Gtk.Bin.do_get_preferred_height_for_width(self, width)
if self.natural_height > 0:
natural = max(minimum, self.natural_height)
return (minimum, natural)