from typing import List, Union import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk, GObject, Pango, GLib, Gio from sublime.state_manager import ApplicationState from sublime.cache_manager import CacheManager from sublime.ui import util from sublime.ui.common import AlbumWithSongs, IconButton, SpinnerImage from sublime.server.api_objects import ( AlbumID3, ArtistInfo2, ArtistWithAlbumsID3, Child, ) class ArtistsPanel(Gtk.Paned): """Defines the arist panel.""" __gsignals__ = { 'song-clicked': ( GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (str, object, object), ), 'refresh-window': ( GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (object, bool), ), } def __init__(self, *args, **kwargs): Gtk.Paned.__init__(self, orientation=Gtk.Orientation.HORIZONTAL) self.artist_list = ArtistList() self.pack1(self.artist_list, False, False) self.artist_detail_panel = ArtistDetailPanel() self.artist_detail_panel.connect( 'song-clicked', lambda _, *args: self.emit('song-clicked', *args), ) self.pack2(self.artist_detail_panel, True, False) def update(self, state: ApplicationState, force=False): self.artist_list.update(state=state) self.artist_detail_panel.update(state=state) class ArtistList(Gtk.Box): class ArtistModel(GObject.GObject): artist_id = GObject.Property(type=str) name = GObject.Property(type=str) album_count = GObject.Property(type=str) def __init__(self, artist_id, name, album_count): GObject.GObject.__init__(self) self.artist_id = artist_id self.name = name self.album_count = album_count def __init__(self): Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) list_actions = Gtk.ActionBar() refresh = IconButton('view-refresh') refresh.connect('clicked', lambda *a: self.update(force=True)) list_actions.pack_end(refresh) self.add(list_actions) self.loading_indicator = Gtk.ListBox() spinner_row = Gtk.ListBoxRow() spinner = Gtk.Spinner( name='artist-list-spinner', active=True, ) spinner_row.add(spinner) self.loading_indicator.add(spinner_row) self.pack_start(self.loading_indicator, False, False, 0) list_scroll_window = Gtk.ScrolledWindow(min_content_width=250) def create_artist_row(model: ArtistList.ArtistModel): label_text = [f'{util.esc(model.name)}'] album_count = model.album_count if album_count: label_text.append( '{} {}'.format( album_count, util.pluralize('album', album_count))) row = Gtk.ListBoxRow( action_name='app.go-to-artist', action_target=GLib.Variant('s', model.artist_id), ) row.add( Gtk.Label( label='\n'.join(label_text), use_markup=True, margin=12, halign=Gtk.Align.START, ellipsize=Pango.EllipsizeMode.END, max_width_chars=30, )) row.show_all() return row self.artists_store = Gio.ListStore() self.list = Gtk.ListBox(name='artist-list') self.list.bind_model(self.artists_store, create_artist_row) list_scroll_window.add(self.list) self.pack_start(list_scroll_window, True, True, 0) @util.async_callback( lambda *a, **k: CacheManager.get_artists(*a, **k), before_download=lambda self: self.loading_indicator.show_all(), on_failure=lambda self, e: self.loading_indicator.hide(), ) def update(self, artists, state: ApplicationState): new_store = [] selected_idx = None for i, artist in enumerate(artists): if state and state.selected_artist_id == artist.id: selected_idx = i new_store.append( ArtistList.ArtistModel( artist.id, artist.name, artist.get('albumCount', ''), )) util.diff_model_store(self.artists_store, new_store) # Preserve selection if selected_idx is not None: row = self.list.get_row_at_index(selected_idx) self.list.select_row(row) self.loading_indicator.hide() class ArtistDetailPanel(Gtk.Box): """Defines the artists list.""" __gsignals__ = { 'song-clicked': ( GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (str, object, object), ), } def __init__(self, *args, **kwargs): super().__init__( *args, orientation=Gtk.Orientation.VERTICAL, name='artist-detail-panel', **kwargs, ) self.albums: Union[List[AlbumID3], List[Child]] = [] self.artist_id = None artist_info_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) # Artist info panel self.big_info_panel = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.artist_artwork = SpinnerImage( loading=False, image_name='artist-album-artwork', spinner_name='artist-artwork-spinner', ) self.big_info_panel.pack_start(self.artist_artwork, False, False, 0) # Action buttons, name, comment, number of songs, etc. artist_details_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) # Action buttons (note we are packing end here, so we have to put them # in right-to-left). self.artist_action_buttons = Gtk.Box( orientation=Gtk.Orientation.HORIZONTAL) view_refresh_button = IconButton('view-refresh-symbolic') view_refresh_button.connect('clicked', self.on_view_refresh_click) self.artist_action_buttons.pack_end( view_refresh_button, False, False, 5) download_all_btn = IconButton('folder-download-symbolic') download_all_btn.connect('clicked', self.on_download_all_click) self.artist_action_buttons.pack_end(download_all_btn, False, False, 5) artist_details_box.pack_start( self.artist_action_buttons, False, False, 5) artist_details_box.pack_start(Gtk.Box(), True, False, 0) self.artist_indicator = self.make_label(name='artist-indicator') artist_details_box.add(self.artist_indicator) self.artist_name = self.make_label(name='artist-name') artist_details_box.add(self.artist_name) self.artist_bio = self.make_label( name='artist-bio', justify=Gtk.Justification.LEFT) self.artist_bio.set_line_wrap(True) artist_details_box.add(self.artist_bio) self.similar_artists_box = Gtk.Box( orientation=Gtk.Orientation.HORIZONTAL) self.similar_artists_label = self.make_label(name='similar-artists') self.similar_artists_box.add(self.similar_artists_label) self.similar_artists_button_box = Gtk.Box( orientation=Gtk.Orientation.HORIZONTAL) self.similar_artists_box.add(self.similar_artists_button_box) artist_details_box.add(self.similar_artists_box) self.artist_stats = self.make_label(name='artist-stats') artist_details_box.add(self.artist_stats) self.big_info_panel.pack_start(artist_details_box, True, True, 10) artist_info_box.pack_start(self.big_info_panel, False, True, 0) self.albums_list = AlbumsListWithSongs() self.albums_list.connect( 'song-clicked', lambda _, *args: self.emit('song-clicked', *args), ) artist_info_box.pack_start(self.albums_list, True, True, 0) self.add(artist_info_box) def get_model_list_future(self, before_download): def do_get_model_list() -> List[Child]: return self.albums return CacheManager.executor.submit(do_get_model_list) def update(self, state: ApplicationState): if state.selected_artist_id is None: self.artist_action_buttons.hide() else: self.update_artist_view(state.selected_artist_id, state=state) # TODO need to handle when this is force updated. Need to delete a bunch of # stuff and un-cache things. @util.async_callback( lambda *a, **k: CacheManager.get_artist(*a, **k), before_download=lambda self: self.artist_artwork.set_loading(True), on_failure=lambda self, e: print('fail a', e), ) def update_artist_view( self, artist: ArtistWithAlbumsID3, state: ApplicationState, ): self.artist_id = artist.id self.artist_indicator.set_text('ARTIST') self.artist_name.set_markup(util.esc(f'{artist.name}')) self.artist_stats.set_markup(self.format_stats(artist)) self.update_artist_info(artist.id) self.update_artist_artwork(artist) self.albums = artist.get('album', artist.get('child', [])) self.albums_list.update(artist) @util.async_callback( lambda *a, **k: CacheManager.get_artist_info(*a, **k), ) def update_artist_info( self, artist_info: ArtistInfo2, state: ApplicationState, ): self.artist_bio.set_markup(util.esc(''.join(artist_info.biography))) if len(artist_info.similarArtist or []) > 0: self.similar_artists_label.set_markup('Similar Artists: ') for c in self.similar_artists_button_box.get_children(): self.similar_artists_button_box.remove(c) for artist in artist_info.similarArtist[:5]: self.similar_artists_button_box.add( Gtk.LinkButton( label=artist.name, name='similar-artist-button', action_name='app.go-to-artist', action_target=GLib.Variant('s', artist.id), )) self.similar_artists_box.show_all() else: self.similar_artists_box.hide() @util.async_callback( lambda *a, **k: CacheManager.get_artist_artwork(*a, **k), before_download=lambda self: self.artist_artwork.set_loading(True), on_failure=lambda self, e: self.artist_artwork.set_loading(False), ) def update_artist_artwork( self, cover_art_filename, state: ApplicationState, ): print(cover_art_filename) self.artist_artwork.set_from_file(cover_art_filename) self.artist_artwork.set_loading(False) # Event Handlers # ========================================================================= def on_view_refresh_click(self, *args): self.update_artist_view(self.artist_id, force=True) def on_download_all_click(self, btn): songs_for_download = [] artist = CacheManager.get_artist(self.artist_id).result() for album in (artist.get('album', artist.get('child', []))): album_songs = CacheManager.get_album(album.id).result() album_songs = album_songs.get('child', album_songs.get('song', [])) for song in album_songs: songs_for_download.append(song.id) CacheManager.batch_download_songs( songs_for_download, before_download=lambda: self.update_artist_view(self.artist_id), on_song_download_complete=lambda i: self.update_artist_view( self.artist_id), ) # Helper Methods # ========================================================================= def make_label(self, text=None, name=None, **params): return Gtk.Label( label=text, name=name, halign=Gtk.Align.START, xalign=0, **params, ) def format_stats(self, artist): album_count = artist.get('albumCount', len(artist.get('child', []))) components = [ '{} {}'.format(album_count, util.pluralize('album', album_count)), ] if artist.get('album'): song_count = sum(a.songCount for a in artist.album) duration = sum(a.duration for a in artist.album) components += [ '{} {}'.format(song_count, util.pluralize('song', song_count)), util.format_sequence_duration(duration), ] elif artist.get('child'): plays = sum(c.playCount for c in artist.child) components += [ '{} {}'.format(plays, util.pluralize('play', plays)), ] return util.dot_join(*components) class AlbumsListWithSongs(Gtk.Overlay): __gsignals__ = { 'song-clicked': ( GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (str, object, object), ), } def __init__(self): Gtk.Overlay.__init__(self) self.scrolled_window = Gtk.ScrolledWindow(vexpand=True) self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.scrolled_window.add(self.box) self.add(self.scrolled_window) self.spinner = Gtk.Spinner( name='albumslist-with-songs-spinner', active=False, halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER, ) self.add_overlay(self.spinner) self.albums = [] def update(self, artist): # TODO this introduces a flicker. Fix this. Need to do some diffing. for c in self.box.get_children(): self.box.remove(c) for album in artist.get('album', artist.get('child', [])): album_with_songs = AlbumWithSongs(album, show_artist_name=False) album_with_songs.connect( 'song-clicked', lambda _, *args: self.emit('song-clicked', *args), ) album_with_songs.connect('song-selected', self.on_song_selected) album_with_songs.show_all() self.box.add(album_with_songs) def on_song_selected(self, album_component): for child in self.box.get_children(): if album_component != child: child.deselect_all()