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])