diff --git a/libremsonic/from_json.py b/libremsonic/from_json.py index 04904f4..af4ed1f 100644 --- a/libremsonic/from_json.py +++ b/libremsonic/from_json.py @@ -33,8 +33,8 @@ def from_json(cls, data): # Having to use this because things changed in Python 3.7. class_name = cls._name - # TODO: this is not very elegant since it doesn't allow things which - # sublass from List or Dict. + # This is not very elegant since it doesn't allow things which sublass + # from List or Dict. For my purposes, this doesn't matter. if class_name == 'List': list_type = cls.__args__[0] instance: List[list_type] = list() diff --git a/libremsonic/ui/albums.py b/libremsonic/ui/albums.py index caa8e54..05f9f6e 100644 --- a/libremsonic/ui/albums.py +++ b/libremsonic/ui/albums.py @@ -1,5 +1,5 @@ import gi -from typing import Optional +from typing import Optional, Union gi.require_version('Gtk', '3.0') from gi.repository import Gio, Gtk, GObject, Pango @@ -7,9 +7,11 @@ from gi.repository import Gio, Gtk, GObject, Pango from libremsonic.state_manager import ApplicationState from libremsonic.cache_manager import CacheManager from libremsonic.ui import util -from libremsonic.ui.common import CoverArtGrid +from libremsonic.ui.common import AlbumWithSongs, CoverArtGrid -from libremsonic.server.api_objects import Child +from libremsonic.server.api_objects import Child, AlbumWithSongsID3 + +Album = Union[Child, AlbumWithSongsID3] class AlbumsPanel(Gtk.ScrolledWindow): @@ -36,15 +38,12 @@ class AlbumsPanel(Gtk.ScrolledWindow): class AlbumModel(GObject.Object): - def __init__(self, title, cover_art, artist, year): - self.title = title - self.cover_art = cover_art - self.artist = artist - self.year = year + def __init__(self, album: Album): + self.album: Album = album super().__init__() def __repr__(self): - return f'' + return f'' class AlbumsGrid(CoverArtGrid): @@ -53,10 +52,11 @@ class AlbumsGrid(CoverArtGrid): # Override Methods # ========================================================================= def get_header_text(self, item: AlbumModel) -> str: - return item.title + return (item.album.title + if type(item.album) == Child else item.album.name) def get_info_text(self, item: AlbumModel) -> Optional[str]: - return util.dot_join(item.artist, item.year) + return util.dot_join(item.album.artist, item.album.year) def get_model_list_future(self, before_download): return CacheManager.get_albums( @@ -65,15 +65,13 @@ class AlbumsGrid(CoverArtGrid): ) def create_model_from_element(self, album): - return AlbumModel( - album.title if type(album) == Child else album.name, - album.coverArt, - album.artist, - album.year, - ) + return AlbumModel(album) + + def create_detail_element_from_model(self, album: AlbumModel): + return AlbumWithSongs(album.album) def get_cover_art_filename_future(self, item, before_download): return CacheManager.get_cover_art_filename( - item.cover_art, + item.album.coverArt, before_download=before_download, ) diff --git a/libremsonic/ui/artists.py b/libremsonic/ui/artists.py index d9f3d16..b83b14e 100644 --- a/libremsonic/ui/artists.py +++ b/libremsonic/ui/artists.py @@ -8,16 +8,13 @@ from gi.repository import Gtk, GObject, Pango, GLib from libremsonic.state_manager import ApplicationState from libremsonic.cache_manager import CacheManager from libremsonic.ui import util -from libremsonic.ui.common import SpinnerImage +from libremsonic.ui.common import AlbumWithSongs, SpinnerImage from libremsonic.server.api_objects import ( AlbumID3, - ArtistID3, ArtistInfo2, ArtistWithAlbumsID3, - AlbumWithSongsID3, Child, - Directory, ) @@ -392,192 +389,3 @@ class AlbumsListWithSongs(Gtk.Overlay): for child in self.box.get_children(): if album_component != child: child.deselect_all() - - -class AlbumWithSongs(Gtk.Box): - __gsignals__ = { - 'song-selected': ( - GObject.SignalFlags.RUN_FIRST, - GObject.TYPE_NONE, - (), - ), - 'song-clicked': ( - GObject.SignalFlags.RUN_FIRST, - GObject.TYPE_NONE, - (str, object), - ), - } - - def __init__(self, album): - Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL) - self.album = album - - box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - artist_artwork = SpinnerImage( - loading=False, - image_name='artist-album-list-artwork', - spinner_name='artist-artwork-spinner', - ) - box.pack_start(artist_artwork, False, False, 0) - box.pack_start(Gtk.Box(), True, True, 0) - self.pack_start(box, False, False, 0) - - def cover_art_future_done(f): - artist_artwork.set_from_file(f.result()) - artist_artwork.set_loading(False) - - cover_art_filename_future = CacheManager.get_cover_art_filename( - album.coverArt, - before_download=lambda: artist_artwork.set_loading(True), - size=200, - ) - 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.add( - Gtk.Label( - label=album.get('name', album.get('title')), - name='artist-album-list-album-name', - halign=Gtk.Align.START, - )) - - stats = [album.year, album.genre] - if album.get('duration'): - stats.append(util.format_sequence_duration(album.duration)) - - album_details.add( - Gtk.Label( - label=util.dot_join(*stats), - halign=Gtk.Align.START, - margin_left=10, - )) - - self.album_song_store = Gtk.ListStore( - str, # cache status - str, # title - str, # duration - str, # song ID - ) - - def create_column(header, text_idx, bold=False, align=0, width=None): - renderer = Gtk.CellRendererText( - xalign=align, - weight=Pango.Weight.BOLD if bold else Pango.Weight.NORMAL, - ellipsize=Pango.EllipsizeMode.END, - ) - renderer.set_fixed_size(width or -1, 35) - - column = Gtk.TreeViewColumn(header, renderer, text=text_idx) - column.set_resizable(True) - column.set_expand(not width) - return column - - self.loading_indicator = Gtk.Spinner( - name='album-list-song-list-spinner', - active=True, - ) - album_details.add(self.loading_indicator) - - self.album_songs = Gtk.TreeView( - model=self.album_song_store, - name='album-songs-list', - headers_visible=False, # TODO use the config value for this - margin_top=15, - margin_left=10, - margin_right=10, - margin_bottom=10, - ) - self.album_songs.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE) - - # Song status column. - renderer = Gtk.CellRendererPixbuf() - renderer.set_fixed_size(30, 35) - column = Gtk.TreeViewColumn('', renderer, icon_name=0) - column.set_resizable(True) - self.album_songs.append_column(column) - - self.album_songs.append_column(create_column('TITLE', 1, bold=True)) - self.album_songs.append_column( - create_column('DURATION', 2, align=1, width=40)) - - self.album_songs.connect('row-activated', self.on_song_activated) - self.album_songs.connect('button-press-event', - self.on_song_button_press) - self.album_songs.get_selection().connect('changed', - self.on_song_selection_change) - album_details.add(self.album_songs) - - self.pack_end(album_details, True, True, 0) - - self.update_album_songs(album.id) - - def on_song_selection_change(self, event): - if not self.album_songs.has_focus(): - self.emit('song-selected') - - def on_song_activated(self, treeview, idx, column): - # The song ID is in the last column of the model. - song_id = self.album_song_store[idx][-1] - self.emit('song-clicked', song_id, - [m[-1] for m in self.album_song_store]) - - def on_song_button_press(self, tree, event): - if event.button == 3: # Right click - clicked_path = tree.get_path_at_pos(event.x, event.y) - if not clicked_path: - return False - - store, paths = tree.get_selection().get_selected_rows() - allow_deselect = False - - def on_download_state_change(song_id=None): - self.update_album_songs(self.album.id) - - # Use the new selection instead of the old one for calculating what - # to do the right click on. - if clicked_path[0] not in paths: - paths = [clicked_path[0]] - allow_deselect = True - - song_ids = [self.album_song_store[p][-1] for p in paths] - - # Used to adjust for the header row. - bin_coords = tree.convert_tree_to_bin_window_coords( - event.x, event.y) - widget_coords = tree.convert_tree_to_widget_coords( - event.x, event.y) - - util.show_song_popover( - song_ids, - event.x, - event.y + abs(bin_coords.by - widget_coords.wy), - tree, - on_download_state_change=on_download_state_change, - ) - - # If the click was on a selected row, don't deselect anything. - if not allow_deselect: - return True - - def deselect_all(self): - self.album_songs.get_selection().unselect_all() - - @util.async_callback( - lambda *a, **k: CacheManager.get_album(*a, **k), - before_download=lambda self: self.loading_indicator.show(), - on_failure=lambda self, e: self.loading_indicator.hide(), - ) - def update_album_songs( - self, - album: Union[AlbumWithSongsID3, Child, Directory], - ): - new_store = [[ - util.get_cached_status_icon(CacheManager.get_cached_status(song)), - util.esc(song.title), - util.format_song_duration(song.duration), - song.id, - ] for song in album.get('child', album.get('song', []))] - - util.diff_store(self.album_song_store, new_store) - self.loading_indicator.hide() diff --git a/libremsonic/ui/common/__init__.py b/libremsonic/ui/common/__init__.py index 4ac7186..9b8f607 100644 --- a/libremsonic/ui/common/__init__.py +++ b/libremsonic/ui/common/__init__.py @@ -1,5 +1,6 @@ -from .edit_form_dialog import EditFormDialog +from .album_with_songs import AlbumWithSongs from .cover_art_grid import CoverArtGrid +from .edit_form_dialog import EditFormDialog from .spinner_image import SpinnerImage -__all__ = ('EditFormDialog', 'CoverArtGrid', 'SpinnerImage') +__all__ = ('AlbumWithSongs', 'CoverArtGrid', 'EditFormDialog', 'SpinnerImage') diff --git a/libremsonic/ui/common/cover_art_grid.py b/libremsonic/ui/common/cover_art_grid.py index d85512d..d6e9cde 100644 --- a/libremsonic/ui/common/cover_art_grid.py +++ b/libremsonic/ui/common/cover_art_grid.py @@ -34,7 +34,6 @@ class CoverArtGrid(Gtk.ScrolledWindow): grid_detail_grid_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.grid_top = Gtk.FlowBox( - vexpand=True, hexpand=True, row_spacing=5, column_spacing=5, @@ -54,8 +53,6 @@ class CoverArtGrid(Gtk.ScrolledWindow): grid_detail_grid_box.add(self.grid_top) self.detail_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - self.detail_box.add(Gtk.Label('foo')) - self.detail_box.add(Gtk.Label('bar')) grid_detail_grid_box.add(self.detail_box) self.grid_bottom = Gtk.FlowBox( @@ -239,6 +236,11 @@ class CoverArtGrid(Gtk.ScrolledWindow): 'create_model_from_element must be implemented by the inheritor ' 'of CoverArtGrid.') + def create_detail_element_from_model(self, model): + raise NotImplementedError( + 'create_detail_element_from_model must be implemented by the ' + 'inheritor of CoverArtGrid.') + def get_cover_art_filename_future(self, item, before_download) -> Future: raise NotImplementedError( 'get_cover_art_filename_future must be implemented by the ' @@ -256,6 +258,12 @@ class CoverArtGrid(Gtk.ScrolledWindow): else: self.selected_list_store_index = selected + for c in self.detail_box.get_children(): + self.detail_box.remove(c) + model = self.list_store[self.selected_list_store_index] + self.detail_box.pack_start( + self.create_detail_element_from_model(model), True, True, 5) + self.reflow_grids() def on_grid_resize(self, flowbox, rect):