diff --git a/sublime/app.py b/sublime/app.py index 0d8b746..1ac9645 100644 --- a/sublime/app.py +++ b/sublime/app.py @@ -96,8 +96,7 @@ class SublimeMusicApp(Gtk.Application): add_action('add-to-queue', self.on_add_to_queue, parameter_type='as') add_action('go-to-album', self.on_go_to_album, parameter_type='s') add_action('go-to-artist', self.on_go_to_artist, parameter_type='s') - add_action( - 'browse-to-song', self.on_browse_to_song, parameter_type='s') + add_action('browse-to', self.browse_to, parameter_type='s') add_action( 'go-to-playlist', self.on_go_to_playlist, parameter_type='s') @@ -583,9 +582,9 @@ class SublimeMusicApp(Gtk.Application): self.state.selected_artist_id = artist_id.get_string() self.update_window() - def on_browse_to_song(self, action, song_id): - # Really, we want to browse to the song's parent. + def browse_to(self, action, item_id): self.state.current_tab = 'browse' + self.state.selected_browse_element_id = item_id.get_string() self.update_window() def on_go_to_playlist(self, action, playlist_id): diff --git a/sublime/cache_manager.py b/sublime/cache_manager.py index b15d980..eacbeac 100644 --- a/sublime/cache_manager.py +++ b/sublime/cache_manager.py @@ -42,7 +42,6 @@ from .server.api_objects import ( # Non-ID3 versions Artist, - ArtistInfo, Directory, # ID3 versions @@ -314,7 +313,7 @@ class CacheManager(metaclass=Singleton): ('song_details', Child, dict), # Non-ID3 caches - ('music_directories', Child, 'dict-list'), + ('music_directories', Directory, dict), ('indexes', Artist, list), # ID3 caches @@ -331,6 +330,7 @@ class CacheManager(metaclass=Singleton): for x in meta_json.get(name, []) ] elif default == dict: + print('dict', name) self.cache[name] = { id: type_name.from_json(x) for id, x in meta_json.get(name, {}).items() @@ -605,16 +605,16 @@ class CacheManager(metaclass=Singleton): id, before_download: Callable[[], None] = lambda: None, force: bool = False, - ) -> 'CacheManager.Result[Child]': + ) -> 'CacheManager.Result[Directory]': cache_name = 'music_directories' if id in self.cache.get(cache_name, {}) and not force: return CacheManager.Result.from_data( self.cache[cache_name][id]) - def after_download(artist): + def after_download(album): with self.cache_lock: - self.cache[cache_name][id] = artist + self.cache[cache_name][id] = album self.save_cache_info() return CacheManager.Result.from_server( @@ -957,8 +957,7 @@ class CacheManager(metaclass=Singleton): # Local Results search_result = SearchResult(query) search_result.add_results( - 'album', - itertools.chain(*self.cache['albums'].values())) + 'album', itertools.chain(*self.cache['albums'].values())) search_result.add_results('artist', self.cache['artists']) search_result.add_results( 'song', self.cache['song_details'].values()) diff --git a/sublime/state_manager.py b/sublime/state_manager.py index 66eb03c..5738b5d 100644 --- a/sublime/state_manager.py +++ b/sublime/state_manager.py @@ -68,6 +68,7 @@ class ApplicationState: current_tab: str = 'albums' selected_album_id: str = None selected_artist_id: str = None + selected_browse_element_id: str = None selected_playlist_id: str = None # State for Album sort. @@ -111,6 +112,8 @@ class ApplicationState: self.current_tab = json_object.get('current_tab', 'albums') self.selected_album_id = json_object.get('selected_album_id', None) self.selected_artist_id = json_object.get('selected_artist_id', None) + self.selected_browse_element_id = json_object.get( + 'selected_browse_element_id', None) self.selected_playlist_id = json_object.get( 'selected_playlist_id', None) self.current_album_sort = json_object.get( diff --git a/sublime/ui/app_styles.css b/sublime/ui/app_styles.css index dc582a8..bfa1ac0 100644 --- a/sublime/ui/app_styles.css +++ b/sublime/ui/app_styles.css @@ -22,7 +22,7 @@ #playlist-list-spinner:checked, #artist-list-spinner:checked, -#directory-list-spinner:checked { +#drilldown-list-spinner:checked { margin: 10px; padding: 0px; } diff --git a/sublime/ui/artists.py b/sublime/ui/artists.py index 70ca372..7e7e16e 100644 --- a/sublime/ui/artists.py +++ b/sublime/ui/artists.py @@ -77,7 +77,10 @@ class ArtistList(Gtk.Box): self.add(list_actions) self.loading_indicator = Gtk.ListBox() - spinner_row = Gtk.ListBoxRow() + spinner_row = Gtk.ListBoxRow( + activatable=False, + selectable=False, + ) spinner = Gtk.Spinner( name='artist-list-spinner', active=True, @@ -216,8 +219,7 @@ class ArtistDetailPanel(Gtk.Box): artist_details_box.add(self.artist_bio) self.similar_artists_scrolledwindow = Gtk.ScrolledWindow() - similar_artists_box = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL) + similar_artists_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.similar_artists_label = self.make_label(name='similar-artists') similar_artists_box.add(self.similar_artists_label) diff --git a/sublime/ui/browse.py b/sublime/ui/browse.py index 90a701c..aaadf3e 100644 --- a/sublime/ui/browse.py +++ b/sublime/ui/browse.py @@ -1,3 +1,5 @@ +from typing import Union + import gi gi.require_version('Gtk', '3.0') @@ -8,8 +10,10 @@ from sublime.cache_manager import CacheManager from sublime.ui import util from sublime.ui.common import IconButton +from sublime.server.api_objects import Child, Artist -class BrowsePanel(Gtk.ScrolledWindow): + +class BrowsePanel(Gtk.Overlay): """Defines the arist panel.""" __gsignals__ = { @@ -25,18 +29,39 @@ class BrowsePanel(Gtk.ScrolledWindow): ), } + id_stack = None + def __init__(self): super().__init__() + scrolled_window = Gtk.ScrolledWindow() + self.root_directory_listing = ListAndDrilldown(IndexList) + scrolled_window.add(self.root_directory_listing) + self.add(scrolled_window) - self.root_directory_listing = DirectoryListAndDrilldown(is_root=True) - - self.add(self.root_directory_listing) + self.spinner = Gtk.Spinner( + active=True, + halign=Gtk.Align.CENTER, + valign=Gtk.Align.CENTER, + ) + self.add_overlay(self.spinner) def update(self, state: ApplicationState, force=False): - self.root_directory_listing.update(state=state, force=force) + id_stack = [] + # TODO make async + if CacheManager.ready: + directory = None + current_dir_id = state.selected_browse_element_id + while directory is None or directory.parent is not None: + directory = CacheManager.get_music_directory( + current_dir_id).result() + id_stack.append(directory.id) + current_dir_id = directory.parent + + self.root_directory_listing.update(id_stack, state=state, force=force) + self.spinner.hide() -class DirectoryListAndDrilldown(Gtk.Paned): +class ListAndDrilldown(Gtk.Paned): __gsignals__ = { 'song-clicked': ( GObject.SignalFlags.RUN_FIRST, @@ -50,33 +75,60 @@ class DirectoryListAndDrilldown(Gtk.Paned): ), } - def __init__(self, is_root=False): + id_stack = None + + def __init__(self, list_type): Gtk.Paned.__init__(self, orientation=Gtk.Orientation.HORIZONTAL) - self.is_root = is_root - self.directory_listing = DirectoryList() - self.pack1(self.directory_listing, False, False) + self.list = list_type() + self.pack1(self.list, False, False) - self.listing_drilldown_panel = Gtk.Box() - self.pack2(self.listing_drilldown_panel, True, False) + self.drilldown = Gtk.Box() + self.pack2(self.drilldown, True, False) - def update(self, state: ApplicationState, force=False): - self.directory_listing.update( + def update( + self, + id_stack, + state: ApplicationState, + force=False, + directory_id=None, + ): + if self.id_stack == id_stack: + return + self.id_stack = id_stack + + if len(id_stack) > 0: + self.remove(self.drilldown) + self.drilldown = ListAndDrilldown(MusicDirectoryList) + self.drilldown.update( + id_stack[:-1], + state, + force=force, + directory_id=id_stack[-1], + ) + self.drilldown.show_all() + self.pack2(self.drilldown, True, False) + + self.list.update( + None if len(id_stack) == 0 else id_stack[-1], state=state, force=force, - is_root=self.is_root, + directory_id=directory_id, ) -class DirectoryList(Gtk.Box): - class SubelementModel(GObject.GObject): +class DrilldownList(Gtk.Box): + class DrilldownElement(GObject.GObject): id = GObject.Property(type=str) name = GObject.Property(type=str) + is_dir = GObject.Property(type=bool, default=True) - def __init__(self, id, name): + def __init__(self, element: Union[Child, Artist]): GObject.GObject.__init__(self) - self.id = id - self.name = name + self.id = element.id + self.name = ( + element.name if isinstance(element, Artist) else element.title) + self.is_dir = isinstance(element, Artist) or element.isDir def __init__(self): Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) @@ -84,15 +136,18 @@ class DirectoryList(Gtk.Box): list_actions = Gtk.ActionBar() refresh = IconButton('view-refresh-symbolic') - refresh.connect('clicked', lambda *a: self.update(force=True)) + refresh.connect('clicked', self.on_refresh_clicked) list_actions.pack_end(refresh) self.add(list_actions) self.loading_indicator = Gtk.ListBox() - spinner_row = Gtk.ListBoxRow() + spinner_row = Gtk.ListBoxRow( + activatable=False, + selectable=False, + ) spinner = Gtk.Spinner( - name='directory-list-spinner', + name='drilldown-list-spinner', active=True, ) spinner_row.add(spinner) @@ -101,45 +156,22 @@ class DirectoryList(Gtk.Box): list_scroll_window = Gtk.ScrolledWindow(min_content_width=250) - def create_row(model: DirectoryList.SubelementModel): - return Gtk.Label( - label=f'{util.esc(model.name)}', - use_markup=True, - margin=10, - halign=Gtk.Align.START, - ellipsize=Pango.EllipsizeMode.END, - max_width_chars=30, - ) - - self.directory_list_store = Gio.ListStore() - self.list = Gtk.ListBox(name='directory-list') - self.list.bind_model(self.directory_list_store, create_row) + self.drilldown_list_store = Gio.ListStore() + self.list = Gtk.ListBox() + self.list.bind_model(self.drilldown_list_store, self.create_row) list_scroll_window.add(self.list) self.pack_start(list_scroll_window, True, True, 0) - def update( - self, - state: ApplicationState = None, - force=False, - is_root=False, - ): - self.is_root = is_root - if self.is_root: - self.update_root(state=state, force=force) - else: - self.update_not_root(state=state, force=force) - - def update_store(self, elements): + def do_update_store(self, elements): new_store = [] selected_idx = None - for i, el in enumerate(elements): - # if state and state.selected_artist_id == el.id: - # selected_idx = i + for idx, el in enumerate(elements): + if el.id == self.selected_id: + selected_idx = idx + new_store.append(DrilldownList.DrilldownElement(el)) - new_store.append(DirectoryList.SubelementModel(el.id, el.name)) - - util.diff_model_store(self.directory_list_store, new_store) + util.diff_model_store(self.drilldown_list_store, new_store) # Preserve selection if selected_idx is not None: @@ -148,28 +180,95 @@ class DirectoryList(Gtk.Box): self.loading_indicator.hide() + +class IndexList(DrilldownList): + def update( + self, + selected_id, + state: ApplicationState = None, + force=False, + **kwargs, + ): + self.selected_id = selected_id + self.update_store(force=force, state=state) + + def on_refresh_clicked(self, _): + self.update(self.selected_id, force=True) + @util.async_callback( lambda *a, **k: CacheManager.get_indexes(*a, **k), before_download=lambda self: self.loading_indicator.show_all(), on_failure=lambda self, e: self.loading_indicator.hide(), ) - def update_root( + def update_store( self, artists, state: ApplicationState = None, force=False, ): - self.update_store(artists) + self.do_update_store(artists) + + def create_row(self, model: DrilldownList.DrilldownElement): + row = Gtk.ListBoxRow( + action_name='app.browse-to', + action_target=GLib.Variant('s', model.id), + ) + row.add( + Gtk.Label( + label=f'{util.esc(model.name)}', + use_markup=True, + margin=10, + halign=Gtk.Align.START, + ellipsize=Pango.EllipsizeMode.END, + max_width_chars=30, + )) + row.show_all() + return row + + +class MusicDirectoryList(DrilldownList): + def update( + self, + selected_id, + state: ApplicationState = None, + force=False, + directory_id=None, + ): + self.directory_id = directory_id + self.selected_id = selected_id + self.update_store(directory_id, force=force, state=state) + + def on_refresh_clicked(self, _): + self.update( + self.selected_id, force=True, directory_id=self.directory_id) @util.async_callback( lambda *a, **k: CacheManager.get_music_directory(*a, **k), before_download=lambda self: self.loading_indicator.show_all(), on_failure=lambda self, e: self.loading_indicator.hide(), ) - def update_not_root( + def update_store( self, directory, state: ApplicationState = None, force=False, ): - self.update_store(directory.child) + self.do_update_store(directory.child) + + def create_row(self, model: DrilldownList.DrilldownElement): + row = Gtk.ListBoxRow() + if model.is_dir: + row.set_action_name('app.browse-to') + row.set_action_target_value(GLib.Variant('s', model.id)) + + row.add( + Gtk.Label( + label=f'{util.esc(model.name)}', + use_markup=True, + margin=10, + halign=Gtk.Align.START, + ellipsize=Pango.EllipsizeMode.END, + max_width_chars=30, + )) + row.show_all() + return row diff --git a/sublime/ui/util.py b/sublime/ui/util.py index 9aefda8..a76f184 100644 --- a/sublime/ui/util.py +++ b/sublime/ui/util.py @@ -194,7 +194,7 @@ def show_song_popover( browse_to_song = Gtk.ModelButton( text=f"Browse to {pluralize('song', song_count)}", - action_name='app.browse-to-song', + action_name='app.browse-to', ) if len(parents) == 1 and list(parents)[0] is not None: parent_value = GLib.Variant('s', list(parents)[0])