Source code for sublime_music.ui.albums

import datetime
import itertools
import logging
import math
from typing import Any, Callable, Iterable, List, Optional, Tuple, cast

from gi.repository import Gdk, Gio, GLib, GObject, Gtk, Pango

from ..adapters import AdapterManager, AlbumSearchQuery, CacheMissError, Result, api_objects as API
from ..config import AppConfiguration
from ..ui import util
from ..ui.common import AlbumWithSongs, IconButton, LoadError, SpinnerImage


def _to_type(query_type: AlbumSearchQuery.Type) -> str:
    return {
        AlbumSearchQuery.Type.RANDOM: "random",
        AlbumSearchQuery.Type.NEWEST: "newest",
        AlbumSearchQuery.Type.FREQUENT: "frequent",
        AlbumSearchQuery.Type.RECENT: "recent",
        AlbumSearchQuery.Type.STARRED: "starred",
        AlbumSearchQuery.Type.ALPHABETICAL_BY_NAME: "alphabetical",
        AlbumSearchQuery.Type.ALPHABETICAL_BY_ARTIST: "alphabetical",
        AlbumSearchQuery.Type.YEAR_RANGE: "year_range",
        AlbumSearchQuery.Type.GENRE: "genre",
    }[query_type]


def _from_str(type_str: str) -> AlbumSearchQuery.Type:
    return {
        "random": AlbumSearchQuery.Type.RANDOM,
        "newest": AlbumSearchQuery.Type.NEWEST,
        "frequent": AlbumSearchQuery.Type.FREQUENT,
        "recent": AlbumSearchQuery.Type.RECENT,
        "starred": AlbumSearchQuery.Type.STARRED,
        "alphabetical_by_name": AlbumSearchQuery.Type.ALPHABETICAL_BY_NAME,
        "alphabetical_by_artist": AlbumSearchQuery.Type.ALPHABETICAL_BY_ARTIST,
        "year_range": AlbumSearchQuery.Type.YEAR_RANGE,
        "genre": AlbumSearchQuery.Type.GENRE,
    }[type_str]


[docs]class AlbumsPanel(Gtk.Box): __gsignals__ = { "song-clicked": ( GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (int, object, object), ), "refresh-window": ( GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (object, bool), ), } offline_mode = False populating_genre_combo = False grid_order_token: int = 0 album_sort_direction: str = "ascending" album_page_size: int = 30 album_page: int = 0 grid_pages_count: int = 0
[docs] def __init__(self): super().__init__(orientation=Gtk.Orientation.VERTICAL) actionbar = Gtk.ActionBar() # Sort by actionbar.add(Gtk.Label(label="Sort")) self.sort_type_combo, self.sort_type_combo_store = self.make_combobox( ( ("random", "randomly", True), ("genre", "by genre", AdapterManager.can_get_genres()), ("newest", "by most recently added", True), ("frequent", "by most played", True), ("recent", "by most recently played", True), ("alphabetical", "alphabetically", True), ("starred", "by starred only", True), ("year_range", "by year", True), ), self.on_type_combo_changed, ) actionbar.pack_start(self.sort_type_combo) self.alphabetical_type_combo, _ = self.make_combobox( (("by_name", "by album name", True), ("by_artist", "by artist name", True)), self.on_alphabetical_type_change, ) actionbar.pack_start(self.alphabetical_type_combo) self.genre_combo, self.genre_combo_store = self.make_combobox((), self.on_genre_change) actionbar.pack_start(self.genre_combo) next_decade = (datetime.datetime.now().year // 10) * 10 + 10 self.from_year_label = Gtk.Label(label="from") actionbar.pack_start(self.from_year_label) self.from_year_spin_button = Gtk.SpinButton.new_with_range(0, next_decade, 1) self.from_year_spin_button.connect("value-changed", self.on_year_changed) actionbar.pack_start(self.from_year_spin_button) self.to_year_label = Gtk.Label(label="to") actionbar.pack_start(self.to_year_label) self.to_year_spin_button = Gtk.SpinButton.new_with_range(0, next_decade, 1) self.to_year_spin_button.connect("value-changed", self.on_year_changed) actionbar.pack_start(self.to_year_spin_button) self.sort_toggle = IconButton( "view-sort-descending-symbolic", "Sort descending", relief=True ) self.sort_toggle.connect("clicked", self.on_sort_toggle_clicked) actionbar.pack_start(self.sort_toggle) # Add the page widget. page_widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) self.prev_page = IconButton( "go-previous-symbolic", "Go to the previous page", sensitive=False ) self.prev_page.connect("clicked", self.on_prev_page_clicked) page_widget.add(self.prev_page) page_widget.add(Gtk.Label(label="Page")) self.page_entry = Gtk.Entry() self.page_entry.set_width_chars(1) self.page_entry.set_max_width_chars(1) self.page_entry.connect("changed", self.on_page_entry_changed) self.page_entry.connect("insert-text", self.on_page_entry_insert_text) page_widget.add(self.page_entry) page_widget.add(Gtk.Label(label="of")) self.page_count_label = Gtk.Label(label="-") page_widget.add(self.page_count_label) self.next_page = IconButton("go-next-symbolic", "Go to the next page", sensitive=False) self.next_page.connect("clicked", self.on_next_page_clicked) page_widget.add(self.next_page) actionbar.set_center_widget(page_widget) 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.add(actionbar) 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)
[docs] def make_combobox( self, items: Iterable[Tuple[str, str, bool]], on_change: Callable[[Gtk.ComboBox], None], ) -> Tuple[Gtk.ComboBox, Gtk.ListStore]: store = Gtk.ListStore(str, str, bool) for item in items: store.append(item) combo = Gtk.ComboBox.new_with_model(store) combo.set_id_column(0) combo.connect("changed", on_change) renderer_text = Gtk.CellRendererText() combo.pack_start(renderer_text, True) combo.add_attribute(renderer_text, "text", 1) combo.add_attribute(renderer_text, "sensitive", 2) return combo, store
[docs] def populate_genre_combo( self, app_config: AppConfiguration | None = None, force: bool = False, ): if not AdapterManager.can_get_genres(): self.updating_query = False return def get_genres_done(f: Result): try: genre_names = (g.name for g in f.result() or []) new_store = [(name, name, True) for name in sorted(genre_names)] util.diff_song_store(self.genre_combo_store, new_store) if app_config: current_genre_id = self.get_id(self.genre_combo) genre = app_config.state.current_album_search_query.genre if genre and current_genre_id != (genre_name := genre.name): self.genre_combo.set_active_id(genre_name) finally: self.updating_query = False try: force = force and ( app_config is not None and (state := app_config.state) is not None and state.current_album_search_query.type == AlbumSearchQuery.Type.GENRE ) genres_future = AdapterManager.get_genres(force=force) genres_future.add_done_callback(lambda f: GLib.idle_add(get_genres_done, f)) except Exception: self.updating_query = False
[docs] def update(self, app_config: AppConfiguration | None = None, force: bool = False): self.updating_query = True supported_type_strings = { _to_type(t) for t in AdapterManager.get_supported_artist_query_types() } for i, el in enumerate(self.sort_type_combo_store): self.sort_type_combo_store[i][2] = el[0] in supported_type_strings # (En|Dis)able getting genres. self.sort_type_combo_store[1][2] = AdapterManager.can_get_genres() if app_config: self.current_query = app_config.state.current_album_search_query self.offline_mode = app_config.offline_mode self.alphabetical_type_combo.set_active_id( { AlbumSearchQuery.Type.ALPHABETICAL_BY_NAME: "by_name", AlbumSearchQuery.Type.ALPHABETICAL_BY_ARTIST: "by_artist", }.get(self.current_query.type) or "by_name" ) self.sort_type_combo.set_active_id(_to_type(self.current_query.type)) if year_range := self.current_query.year_range: self.from_year_spin_button.set_value(year_range[0]) self.to_year_spin_button.set_value(year_range[1]) # Update the page display if app_config: 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.prev_page.set_sensitive(self.album_page > 0) self.page_entry.set_text(str(self.album_page + 1)) # Show/hide the combo boxes. def show_if(sort_type: Iterable[AlbumSearchQuery.Type], *elements): for element in elements: if self.current_query.type in sort_type: element.show() else: element.hide() show_if( ( AlbumSearchQuery.Type.ALPHABETICAL_BY_NAME, AlbumSearchQuery.Type.ALPHABETICAL_BY_ARTIST, ), self.alphabetical_type_combo, ) show_if((AlbumSearchQuery.Type.GENRE,), self.genre_combo) show_if( (AlbumSearchQuery.Type.YEAR_RANGE,), self.from_year_label, self.from_year_spin_button, self.to_year_label, self.to_year_spin_button, ) # (En|Dis)able the sort button self.sort_toggle.set_sensitive(self.current_query.type != AlbumSearchQuery.Type.RANDOM) if app_config: self.album_sort_direction = app_config.state.album_sort_direction self.sort_toggle.set_icon(f"view-sort-{self.album_sort_direction}-symbolic") self.sort_toggle.set_tooltip_text( "Change sort order to " + 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 self.populate_genre_combo(app_config, force=force) # At this point, the current query should be totally updated. if app_config: self.grid_order_token = self.grid.update_params(app_config) self.grid.update(self.grid_order_token, app_config, force=force)
def _get_opposite_sort_dir(self, sort_dir: str) -> str: return ("ascending", "descending")[0 if sort_dir == "descending" else 1]
[docs] def get_id(self, combo: Gtk.ComboBox) -> Optional[str]: tree_iter = combo.get_active_iter() if tree_iter is not None: return combo.get_model()[tree_iter][0] return None
[docs] def on_sort_toggle_clicked(self, _): self.emit( "refresh-window", { "album_sort_direction": self._get_opposite_sort_dir(self.album_sort_direction), "album_page": 0, "selected_album_id": None, }, False, )
[docs] def on_refresh_clicked(self, _): self.emit("refresh-window", {}, True)
class _Genre(API.Genre): def __init__(self, name: str): self.name = name
[docs] def on_grid_num_pages_changed(self, grid: Any, pages: int): self.grid_pages_count = pages pages_str = str(self.grid_pages_count) self.page_count_label.set_text(pages_str) self.next_page.set_sensitive(self.album_page < self.grid_pages_count - 1) num_digits = len(pages_str) self.page_entry.set_width_chars(num_digits) self.page_entry.set_max_width_chars(num_digits)
[docs] def on_type_combo_changed(self, combo: Gtk.ComboBox): id = self.get_id(combo) assert id if id == "alphabetical": id += "_" + cast(str, self.get_id(self.alphabetical_type_combo)) self.emit_if_not_updating( "refresh-window", { "current_album_search_query": AlbumSearchQuery( _from_str(id), self.current_query.year_range, self.current_query.genre, ), "album_page": 0, "selected_album_id": None, }, False, )
[docs] def on_alphabetical_type_change(self, combo: Gtk.ComboBox): id = "alphabetical_" + cast(str, self.get_id(combo)) self.emit_if_not_updating( "refresh-window", { "current_album_search_query": AlbumSearchQuery( _from_str(id), self.current_query.year_range, self.current_query.genre, ), "album_page": 0, "selected_album_id": None, }, False, )
[docs] def on_genre_change(self, combo: Gtk.ComboBox): genre = self.get_id(combo) assert genre self.emit_if_not_updating( "refresh-window", { "current_album_search_query": AlbumSearchQuery( self.current_query.type, self.current_query.year_range, AlbumsPanel._Genre(genre), ), "album_page": 0, "selected_album_id": None, }, False, )
[docs] def on_year_changed(self, entry: Gtk.SpinButton) -> bool: year = int(entry.get_value()) assert self.current_query.year_range if self.to_year_spin_button == entry: new_year_tuple = (self.current_query.year_range[0], year) else: new_year_tuple = (year, self.current_query.year_range[1]) self.emit_if_not_updating( "refresh-window", { "current_album_search_query": AlbumSearchQuery( self.current_query.type, new_year_tuple, self.current_query.genre ), "album_page": 0, "selected_album_id": None, }, False, ) return False
[docs] def on_page_entry_changed(self, entry: Gtk.Entry) -> bool: if len(text := entry.get_text()) > 0: self.emit_if_not_updating( "refresh-window", {"album_page": int(text) - 1, "selected_album_id": None}, False, ) return False
[docs] def on_page_entry_insert_text( self, entry: Gtk.Entry, text: str, length: int, position: int ) -> bool: if self.updating_query: return False if not text.isdigit(): entry.emit_stop_by_name("insert-text") return True page_num = int(entry.get_text() + text) if self.grid_pages_count is None or self.grid_pages_count < page_num: entry.emit_stop_by_name("insert-text") return True return False
[docs] def on_prev_page_clicked(self, _): self.emit_if_not_updating( "refresh-window", {"album_page": self.album_page - 1, "selected_album_id": None}, False, )
[docs] def on_next_page_clicked(self, _): self.emit_if_not_updating( "refresh-window", {"album_page": self.album_page + 1, "selected_album_id": None}, False, )
[docs] def on_grid_cover_clicked(self, grid: Any, id: str): self.emit( "refresh-window", {"selected_album_id": id}, False, )
[docs] def on_show_count_dropdown_change(self, combo: Gtk.ComboBox): show_count = int(self.get_id(combo) or 30) self.emit( "refresh-window", {"album_page_size": show_count, "album_page": 0}, False, )
[docs] def emit_if_not_updating(self, *args): if self.updating_query: return self.emit(*args)
[docs]class AlbumsGrid(Gtk.Overlay): """Defines the albums panel.""" __gsignals__ = { "cover-clicked": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (object,)), "refresh-window": ( GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (object, bool), ), "song-clicked": ( GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (int, object, object), ), "num-pages-changed": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (int,)), } class _AlbumModel(GObject.Object): def __init__(self, album: API.Album): self.album = album super().__init__() @property def id(self) -> str: assert self.album.id return self.album.id def __repr__(self) -> str: return f"<AlbumsGrid._AlbumModel {self.album}>" current_query: AlbumSearchQuery = AlbumSearchQuery(AlbumSearchQuery.Type.RANDOM) current_models: List[_AlbumModel] = [] latest_applied_order_ratchet: int = 0 order_ratchet: int = 0 offline_mode: bool = False currently_selected_index: Optional[int] = None currently_selected_id: Optional[str] = None sort_dir: str = "" page_size: int = 30 page: int = 0 num_pages: Optional[int] = None next_page_fn = None provider_id: Optional[str] = None
[docs] def update_params(self, app_config: AppConfiguration) -> int: # If there's a diff, increase the ratchet. if ( self.current_query.strhash() != (search_query := app_config.state.current_album_search_query).strhash() ): self.order_ratchet += 1 self.current_query = search_query if self.offline_mode != (offline_mode := app_config.offline_mode): self.order_ratchet += 1 self.offline_mode = offline_mode if self.provider_id != (provider_id := app_config.current_provider_id): self.order_ratchet += 1 self.provider_id = provider_id return self.order_ratchet
[docs] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.items_per_row = 4 scrolled_window = Gtk.ScrolledWindow() grid_detail_grid_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.error_container = Gtk.Box() grid_detail_grid_box.add(self.error_container) def create_flowbox(**kwargs) -> Gtk.FlowBox: flowbox = Gtk.FlowBox( **kwargs, hexpand=True, row_spacing=5, column_spacing=5, margin_top=5, homogeneous=True, valign=Gtk.Align.START, halign=Gtk.Align.CENTER, selection_mode=Gtk.SelectionMode.SINGLE, ) flowbox.set_max_children_per_line(7) return flowbox self.grid_top = create_flowbox() self.grid_top.connect("child-activated", self.on_child_activated) self.grid_top.connect("size-allocate", self.on_grid_resize) self.list_store_top = Gio.ListStore() self.grid_top.bind_model(self.list_store_top, self._create_cover_art_widget) grid_detail_grid_box.add(self.grid_top) self.detail_box_revealer = Gtk.Revealer(valign=Gtk.Align.END) self.detail_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, name="artist-detail-box") self.detail_box.pack_start(Gtk.Box(), True, True, 0) self.detail_box_inner = Gtk.Box() self.detail_box.pack_start(self.detail_box_inner, False, False, 0) self.detail_box.pack_start(Gtk.Box(), True, True, 0) self.detail_box_revealer.add(self.detail_box) grid_detail_grid_box.add(self.detail_box_revealer) self.grid_bottom = create_flowbox(vexpand=True) self.grid_bottom.connect("child-activated", self.on_child_activated) self.list_store_bottom = Gio.ListStore() self.grid_bottom.bind_model(self.list_store_bottom, self._create_cover_art_widget) grid_detail_grid_box.add(self.grid_bottom) scrolled_window.add(grid_detail_grid_box) self.add(scrolled_window) self.spinner = Gtk.Spinner( name="grid-spinner", active=True, halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER, ) self.add_overlay(self.spinner)
[docs] def update( self, order_token: int, app_config: AppConfiguration | None = None, force: bool = False ): if order_token < self.latest_applied_order_ratchet: return force_grid_reload_from_master = False if app_config: self.currently_selected_id = app_config.state.selected_album_id if ( self.sort_dir != app_config.state.album_sort_direction or self.page_size != app_config.state.album_page_size or self.page != app_config.state.album_page ): force_grid_reload_from_master = True self.sort_dir = app_config.state.album_sort_direction self.page_size = app_config.state.album_page_size self.page = app_config.state.album_page self.update_grid( order_token, use_ground_truth_adapter=force, force_grid_reload_from_master=force_grid_reload_from_master, ) # Update the detail panel. children = self.detail_box_inner.get_children() if len(children) > 0 and hasattr(children[0], "update"): children[0].update(app_config=app_config, force=force)
error_dialog = None
[docs] def update_grid( self, order_token: int, use_ground_truth_adapter: bool = False, force_grid_reload_from_master: bool = False, ): if not AdapterManager.can_get_artists(): self.spinner.hide() return force_grid_reload_from_master = ( force_grid_reload_from_master or use_ground_truth_adapter or self.latest_applied_order_ratchet < order_token ) def do_update_grid(selected_index: Optional[int]): if self.sort_dir == "descending" and selected_index: selected_index = len(self.current_models) - selected_index - 1 self.reflow_grids( force_reload_from_master=force_grid_reload_from_master, selected_index=selected_index, models=self.current_models, ) self.spinner.hide() def reload_store(f: Result[Iterable[API.Album]]): # Don't override more recent results if order_token < self.latest_applied_order_ratchet: return self.latest_applied_order_ratchet = order_token is_partial = False try: albums = list(f.result()) except CacheMissError as e: albums = cast(Optional[List[API.Album]], e.partial_data) or [] is_partial = True except Exception as e: if self.error_dialog: self.spinner.hide() return # TODO (#122): make this non-modal self.error_dialog = Gtk.MessageDialog( transient_for=self.get_toplevel(), message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK, text="Failed to retrieve albums", ) self.error_dialog.format_secondary_markup( # TODO (#204) make this error better. f"Getting albums by {self.current_query.type} failed due to the " f"following error\n\n{e}" ) logging.exception("Failed to retrieve albums") self.error_dialog.run() self.error_dialog.destroy() self.error_dialog = None self.spinner.hide() return for c in self.error_container.get_children(): self.error_container.remove(c) if is_partial and ( len(albums) == 0 or self.current_query.type != AlbumSearchQuery.Type.RANDOM ): load_error = LoadError( "Album list", "load albums", has_data=albums is not None and len(albums) > 0, offline_mode=self.offline_mode, ) self.error_container.pack_start(load_error, True, True, 0) self.error_container.show_all() else: self.error_container.hide() selected_index = None self.current_models = [] for i, album in enumerate(albums): model = AlbumsGrid._AlbumModel(album) if model.id == self.currently_selected_id: selected_index = i self.current_models.append(model) self.emit( "num-pages-changed", math.ceil(len(self.current_models) / self.page_size), ) do_update_grid(selected_index) if force_grid_reload_from_master: albums_result = AdapterManager.get_albums( self.current_query, use_ground_truth_adapter=use_ground_truth_adapter ) if albums_result.data_is_available: # Don't idle add if the data is already available. albums_result.add_done_callback(reload_store) else: self.spinner.show() albums_result.add_done_callback(lambda f: GLib.idle_add(reload_store, f)) else: selected_index = None for i, album in enumerate(self.current_models): if album.id == self.currently_selected_id: selected_index = i self.emit( "num-pages-changed", math.ceil(len(self.current_models) / self.page_size), ) do_update_grid(selected_index)
# Event Handlers # =========================================================================
[docs] def on_child_activated(self, flowbox: Gtk.FlowBox, child: Gtk.Widget): click_top = flowbox == self.grid_top selected_index = child.get_index() if click_top: page_offset = self.page_size * self.page if self.currently_selected_index is not None and ( selected_index == self.currently_selected_index - page_offset ): self.emit("cover-clicked", None) else: self.emit("cover-clicked", self.list_store_top[selected_index].id) else: self.emit("cover-clicked", self.list_store_bottom[selected_index].id)
[docs] def on_grid_resize(self, flowbox: Gtk.FlowBox, rect: Gdk.Rectangle): # TODO (#124): this doesn't work at all consistency, especially with themes that # add extra padding. # 200 + (10 * 2) + (5 * 2) = 230 # picture + (padding * 2) + (margin * 2) new_items_per_row = min((rect.width // 230), 7) if new_items_per_row != self.items_per_row: self.items_per_row = new_items_per_row self.detail_box_inner.set_size_request(self.items_per_row * 230 - 10, -1) self.reflow_grids( force_reload_from_master=True, selected_index=self.currently_selected_index, )
# Helper Methods # ========================================================================= 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, item: _AlbumModel) -> 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(item.album.name, "grid-header-label") widget_box.pack_start(header_label, False, False, 0) # Extra info for the widget info_text = util.dot_join( item.album.artist.name if item.album.artist else "-", item.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(item.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
[docs] def reflow_grids( self, force_reload_from_master: bool = False, selected_index: int | None = None, models: List[_AlbumModel] | None = None, ): # Calculate the page that the currently_selected_index is in. If it's a # different page, then update the window. if selected_index is not None: page_of_selected_index = selected_index // self.page_size if page_of_selected_index != self.page: self.emit("refresh-window", {"album_page": page_of_selected_index}, False) return page_offset = self.page_size * self.page # Calculate the look-at window. if models: if self.sort_dir == "ascending": window = models[page_offset : (page_offset + self.page_size)] else: reverse_sorted_models = reversed(models) # remove to the offset for _ in range(page_offset): next(reverse_sorted_models, page_offset) window = list(itertools.islice(reverse_sorted_models, self.page_size)) else: window = list(self.list_store_top) + list(self.list_store_bottom) # Determine where the cuttoff is between the top and bottom grids. entries_before_fold = self.page_size if selected_index is not None and self.items_per_row: relative_selected_index = selected_index - page_offset entries_before_fold = ( (relative_selected_index // self.items_per_row) + 1 ) * self.items_per_row # Unreveal the current album details first if selected_index is None: self.detail_box_revealer.set_reveal_child(False) if force_reload_from_master: # Just remove everything and re-add all of the items. It's not worth trying # to diff in this case. self.list_store_top.splice( 0, len(self.list_store_top), window[:entries_before_fold], ) self.list_store_bottom.splice( 0, len(self.list_store_bottom), window[entries_before_fold:], ) elif selected_index or entries_before_fold != self.page_size: # This case handles when the selection changes and the entries need to be # re-allocated to the top and bottom grids # Move entries between the two stores. top_store_len = len(self.list_store_top) bottom_store_len = len(self.list_store_bottom) diff = abs(entries_before_fold - top_store_len) if diff > 0: if entries_before_fold - top_store_len > 0: # Move entries from the bottom store. self.list_store_top.splice(top_store_len, 0, self.list_store_bottom[:diff]) self.list_store_bottom.splice(0, min(diff, bottom_store_len), []) else: # Move entries to the bottom store. self.list_store_bottom.splice(0, 0, self.list_store_top[-diff:]) self.list_store_top.splice(top_store_len - diff, diff, []) if selected_index is not None: relative_selected_index = selected_index - page_offset to_select = self.grid_top.get_child_at_index(relative_selected_index) if not to_select: return self.grid_top.select_child(to_select) if self.currently_selected_index == selected_index: return for c in self.detail_box_inner.get_children(): self.detail_box_inner.remove(c) model = self.list_store_top[relative_selected_index] detail_element = AlbumWithSongs(model.album, cover_art_size=300) detail_element.connect( "song-clicked", lambda _, *args: self.emit("song-clicked", *args), ) detail_element.connect("song-selected", lambda *a: None) self.detail_box_inner.pack_start(detail_element, True, True, 0) self.detail_box_inner.show_all() self.detail_box_revealer.set_reveal_child(True) # TODO (#88): scroll so that the grid_top is visible, and the # detail_box is visible, with preference to the grid_top. May need # to add another flag for this function. else: self.grid_top.unselect_all() self.grid_bottom.unselect_all() self.currently_selected_index = selected_index