from typing import Union from random import randint import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk, GObject, Pango, GLib from sublime.state_manager import ApplicationState from sublime.cache_manager import CacheManager from sublime.ui import util from .icon_button import IconButton from .spinner_image import SpinnerImage from sublime.server.api_objects import ( AlbumWithSongsID3, Child, Directory, ) class AlbumWithSongs(Gtk.Box): __gsignals__ = { 'song-selected': ( GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (), ), 'song-clicked': ( GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (int, object, object), ), } def __init__(self, album, cover_art_size=200, show_artist_name=True): 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', image_size=cover_art_size, ) # Account for 10px margin on all sides with "+ 20". artist_artwork.set_size_request( cover_art_size + 20, cover_art_size + 20) 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=cover_art_size, ) 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_title_and_buttons = Gtk.Box( orientation=Gtk.Orientation.HORIZONTAL) # TODO: deal with super long-ass titles album_title_and_buttons.add( Gtk.Label( label=album.get('name', album.get('title')), name='artist-album-list-album-name', halign=Gtk.Align.START, ellipsize=Pango.EllipsizeMode.END, )) self.play_btn = IconButton( 'media-playback-start-symbolic', sensitive=False) self.play_btn.connect('clicked', self.play_btn_clicked) album_title_and_buttons.pack_start(self.play_btn, False, False, 5) self.shuffle_btn = IconButton( 'media-playlist-shuffle-symbolic', sensitive=False) self.shuffle_btn.connect('clicked', self.shuffle_btn_clicked) album_title_and_buttons.pack_start(self.shuffle_btn, False, False, 5) self.play_next_btn = IconButton( 'go-top-symbolic', action_name='app.play-next') album_title_and_buttons.pack_start(self.play_next_btn, False, False, 5) self.add_to_queue_btn = IconButton( 'go-jump-symbolic', action_name='app.add-to-queue') album_title_and_buttons.pack_start( self.add_to_queue_btn, False, False, 5) self.download_all_btn = IconButton( 'folder-download-symbolic', sensitive=False) self.download_all_btn.connect('clicked', self.on_download_all_click) album_title_and_buttons.pack_end( self.download_all_btn, False, False, 5) album_details.add(album_title_and_buttons) stats = [ album.artist if show_artist_name else None, album.year, album.genre, # TODO when not available (not browse by tags), calculate after the # list loads and update the stats label. util.format_sequence_duration(album.duration) if album.get('duration') else None, ] 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') album_details.add(self.loading_indicator) self.album_songs = Gtk.TreeView( model=self.album_song_store, name='album-songs-list', headers_visible=False, 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) # Event Handlers # ========================================================================= 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. self.emit( 'song-clicked', idx.get_indices()[0], [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 on_download_all_click(self, btn): CacheManager.batch_download_songs( [x[-1] for x in self.album_song_store], before_download=self.update, on_song_download_complete=lambda x: self.update(), ) def play_btn_clicked(self, btn): song_ids = [x[-1] for x in self.album_song_store] self.emit( 'song-clicked', 0, song_ids, {'force_shuffle_state': False}, ) def shuffle_btn_clicked(self, btn): song_ids = [x[-1] for x in self.album_song_store] self.emit( 'song-clicked', randint(0, len(self.album_song_store) - 1), song_ids, {'force_shuffle_state': True}, ) # Helper Methods # ========================================================================= def deselect_all(self): self.album_songs.get_selection().unselect_all() def update(self, force=False): self.update_album_songs(self.album.id) def set_loading(self, loading): if loading: self.loading_indicator.start() self.loading_indicator.show() else: self.loading_indicator.stop() self.loading_indicator.hide() @util.async_callback( lambda *a, **k: CacheManager.get_album(*a, **k), before_download=lambda self: self.set_loading(True), on_failure=lambda self, e: self.set_loading(False), ) def update_album_songs( self, album: Union[AlbumWithSongsID3, Child, Directory], state: ApplicationState, ): 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') or album.get('song') or []) ] song_ids = [song[-1] for song in new_store] self.play_btn.set_sensitive(True) self.shuffle_btn.set_sensitive(True) self.play_next_btn.set_action_target_value( GLib.Variant('as', song_ids)) self.add_to_queue_btn.set_action_target_value( GLib.Variant('as', song_ids)) self.download_all_btn.set_sensitive(True) util.diff_song_store(self.album_song_store, new_store) self.loading_indicator.hide()