Adding more offline mode load errors
This commit is contained in:
@@ -312,6 +312,8 @@ class Adapter(abc.ABC):
|
||||
# These properties determine if what things the adapter can be used to do
|
||||
# at the current moment.
|
||||
# ==================================================================================
|
||||
# TODO: change these to just be "if it has the attribute, then you can try the
|
||||
# action" and use "ping_status" to determine online/offline status.
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def can_service_requests(self) -> bool:
|
||||
@@ -331,7 +333,7 @@ class Adapter(abc.ABC):
|
||||
@property
|
||||
def can_get_playlists(self) -> bool:
|
||||
"""
|
||||
Whether :class:`get_playlist` can be called on the adapter right now.
|
||||
Whether the adapter supports :class:`get_playlist`.
|
||||
"""
|
||||
return False
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 6.3 KiB |
@@ -7,14 +7,17 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
fill="#000000"
|
||||
viewBox="0 0 24 24"
|
||||
width="384px"
|
||||
height="384px"
|
||||
version="1.1"
|
||||
inkscape:export-ydpi="103.984"
|
||||
inkscape:export-xdpi="103.984"
|
||||
inkscape:export-filename="/home/sumner/projects/sublime-music/sublime/adapters/images/default-album-art.png"
|
||||
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
|
||||
sodipodi:docname="default-album-art.svg"
|
||||
id="svg6"
|
||||
sodipodi:docname="icons8-music.svg"
|
||||
inkscape:version="0.92.4 5da689c313, 2019-01-14">
|
||||
version="1.1"
|
||||
height="384px"
|
||||
width="384px"
|
||||
viewBox="0 0 24 24"
|
||||
fill="#000000">
|
||||
<metadata
|
||||
id="metadata12">
|
||||
<rdf:RDF>
|
||||
@@ -29,71 +32,72 @@
|
||||
<defs
|
||||
id="defs10" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1366"
|
||||
inkscape:window-height="729"
|
||||
id="namedview8"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.61458333"
|
||||
inkscape:cx="192"
|
||||
inkscape:cy="192"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:document-rotation="0"
|
||||
inkscape:current-layer="g26"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg6">
|
||||
inkscape:window-y="64"
|
||||
inkscape:window-x="0"
|
||||
inkscape:cy="255.37351"
|
||||
inkscape:cx="249.24086"
|
||||
inkscape:zoom="1.7383042"
|
||||
showgrid="false"
|
||||
id="namedview8"
|
||||
inkscape:window-height="1374"
|
||||
inkscape:window-width="2556"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0"
|
||||
guidetolerance="10"
|
||||
gridtolerance="10"
|
||||
objecttolerance="10"
|
||||
borderopacity="1"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff">
|
||||
<sodipodi:guide
|
||||
position="0,0"
|
||||
orientation="0,384"
|
||||
inkscape:locked="false"
|
||||
id="guide14"
|
||||
inkscape:locked="false" />
|
||||
orientation="0,384"
|
||||
position="0,0" />
|
||||
<sodipodi:guide
|
||||
position="24,0"
|
||||
orientation="-384,0"
|
||||
inkscape:locked="false"
|
||||
id="guide16"
|
||||
inkscape:locked="false" />
|
||||
orientation="-384,0"
|
||||
position="24,0" />
|
||||
<sodipodi:guide
|
||||
position="24,24"
|
||||
orientation="0,-384"
|
||||
inkscape:locked="false"
|
||||
id="guide18"
|
||||
inkscape:locked="false" />
|
||||
orientation="0,-384"
|
||||
position="24,24" />
|
||||
<sodipodi:guide
|
||||
position="0,24"
|
||||
orientation="384,0"
|
||||
inkscape:locked="false"
|
||||
id="guide20"
|
||||
inkscape:locked="false" />
|
||||
orientation="384,0"
|
||||
position="0,24" />
|
||||
</sodipodi:namedview>
|
||||
<rect
|
||||
id="rect22"
|
||||
width="24"
|
||||
height="24"
|
||||
x="0"
|
||||
style="stroke-width:0.0625;fill:#1d1c1c;fill-opacity:1"
|
||||
y="0"
|
||||
style="stroke-width:0.0625;fill:#e6e6e6" />
|
||||
x="0"
|
||||
height="24"
|
||||
width="24"
|
||||
id="rect22" />
|
||||
<g
|
||||
id="g26"
|
||||
style="stroke:#b3b3b3">
|
||||
style="stroke:#b3b3b3"
|
||||
id="g26">
|
||||
<path
|
||||
id="path2"
|
||||
style="stroke:#b3b3b3"
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
stroke-miterlimit="10"
|
||||
stroke-width="2"
|
||||
d="M9 15A3 3 0 1 0 9 21A3 3 0 1 0 9 15Z"
|
||||
stroke-width="2"
|
||||
stroke-miterlimit="10"
|
||||
stroke="#000000"
|
||||
fill="none"
|
||||
style="stroke:#b3b3b3" />
|
||||
id="path2" />
|
||||
<path
|
||||
id="path4"
|
||||
d="M12 18L12 4 18 4 18 8 13 8"
|
||||
stroke-width="2"
|
||||
stroke-miterlimit="10"
|
||||
stroke="#000000"
|
||||
style="stroke:#b3b3b3"
|
||||
fill="none"
|
||||
style="stroke:#b3b3b3" />
|
||||
stroke="#000000"
|
||||
stroke-miterlimit="10"
|
||||
stroke-width="2"
|
||||
d="M12 18L12 4 18 4 18 8 13 8"
|
||||
id="path4" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.8 KiB |
@@ -318,6 +318,7 @@ class AdapterManager:
|
||||
function_name: str,
|
||||
*params: Any,
|
||||
before_download: Callable[[], None] = None,
|
||||
partial_data: Any = None,
|
||||
**kwargs,
|
||||
) -> Result:
|
||||
"""
|
||||
@@ -337,7 +338,10 @@ class AdapterManager:
|
||||
if before_download:
|
||||
before_download()
|
||||
fn = getattr(AdapterManager._instance.ground_truth_adapter, function_name)
|
||||
return fn(*params, **kwargs)
|
||||
try:
|
||||
return fn(*params, **kwargs)
|
||||
except Exception:
|
||||
raise CacheMissError(partial_data=partial_data)
|
||||
|
||||
return Result(future_fn)
|
||||
|
||||
@@ -431,7 +435,7 @@ class AdapterManager:
|
||||
assert AdapterManager._instance
|
||||
assert AdapterManager._instance.caching_adapter
|
||||
AdapterManager._instance.caching_adapter.ingest_new_data(
|
||||
cache_key, param, f.result(),
|
||||
cache_key, param, f.result()
|
||||
)
|
||||
|
||||
return future_finished
|
||||
@@ -517,24 +521,33 @@ class AdapterManager:
|
||||
cache_key, param_str
|
||||
)
|
||||
|
||||
# TODO
|
||||
# TODO (#122) If any of the following fails, do we want to return what the
|
||||
# caching adapter has?
|
||||
|
||||
# TODO (#188): don't short circuit if not allow_download because it could be the
|
||||
# filesystem adapter.
|
||||
if not allow_download or not AdapterManager._ground_truth_can_do(function_name):
|
||||
if (
|
||||
not allow_download
|
||||
and AdapterManager._instance.ground_truth_adapter.is_networked
|
||||
) or not AdapterManager._ground_truth_can_do(function_name):
|
||||
logging.info(f"END: NO DOWNLOAD: {function_name}")
|
||||
if partial_data:
|
||||
# TODO (#122) indicate that this is partial data. Probably just re-throw
|
||||
# here?
|
||||
logging.debug("partial_data exists, returning", partial_data)
|
||||
return Result(cast(AdapterManager.R, partial_data))
|
||||
raise Exception(f"No adapters can service {function_name} at the moment.")
|
||||
|
||||
def cache_miss_result():
|
||||
raise CacheMissError(partial_data=partial_data)
|
||||
|
||||
return Result(cache_miss_result)
|
||||
# TODO
|
||||
# raise CacheMissError(partial_data=partial_data)
|
||||
# # TODO (#122) indicate that this is partial data. Probably just re-throw
|
||||
# # here?
|
||||
# logging.debug("partial_data exists, returning", partial_data)
|
||||
# return Result(cast(AdapterManager.R, partial_data))
|
||||
# raise Exception(f"No adapters can service {function_name} at the moment.")
|
||||
|
||||
result: Result[AdapterManager.R] = AdapterManager._create_ground_truth_result(
|
||||
function_name,
|
||||
*((param,) if param is not None else ()),
|
||||
before_download=before_download,
|
||||
partial_data=partial_data,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -545,7 +558,6 @@ class AdapterManager:
|
||||
)
|
||||
|
||||
if on_result_finished:
|
||||
# TODO (#122): figure out a way to pass partial data here
|
||||
result.add_done_callback(on_result_finished)
|
||||
|
||||
logging.info(f"END: {function_name}")
|
||||
|
@@ -50,6 +50,10 @@ if delay_str := os.environ.get("REQUEST_DELAY"):
|
||||
else:
|
||||
REQUEST_DELAY = (float(delay_str), float(delay_str))
|
||||
|
||||
NETWORK_ALWAYS_ERROR: bool = False
|
||||
if always_error := os.environ.get("NETWORK_ALWAYS_ERROR"):
|
||||
NETWORK_ALWAYS_ERROR = True
|
||||
|
||||
|
||||
class SubsonicAdapter(Adapter):
|
||||
"""
|
||||
@@ -248,12 +252,15 @@ class SubsonicAdapter(Adapter):
|
||||
sleep(delay)
|
||||
if timeout:
|
||||
if type(timeout) == tuple:
|
||||
if cast(Tuple[float, float], timeout)[0] > delay:
|
||||
if delay > cast(Tuple[float, float], timeout)[0]:
|
||||
raise TimeoutError("DUMMY TIMEOUT ERROR")
|
||||
else:
|
||||
if cast(float, timeout) > delay:
|
||||
if delay > cast(float, timeout):
|
||||
raise TimeoutError("DUMMY TIMEOUT ERROR")
|
||||
|
||||
if NETWORK_ALWAYS_ERROR:
|
||||
raise Exception("NETWORK_ALWAYS_ERROR enabled")
|
||||
|
||||
# Deal with datetime parameters (convert to milliseconds since 1970)
|
||||
for k, v in params.items():
|
||||
if isinstance(v, datetime):
|
||||
|
@@ -86,6 +86,15 @@ class SublimeMusicApp(Gtk.Application):
|
||||
add_action("browse-to", self.browse_to, parameter_type="s")
|
||||
add_action("go-to-playlist", self.on_go_to_playlist, parameter_type="s")
|
||||
|
||||
add_action(
|
||||
"go-online",
|
||||
lambda *a: self.on_refresh_window(
|
||||
None, {"__settings__": {"offline_mode": False}}
|
||||
),
|
||||
)
|
||||
add_action(
|
||||
"refresh-window", lambda *a: self.on_refresh_window(None, {}, True),
|
||||
)
|
||||
add_action("mute-toggle", self.on_mute_toggle)
|
||||
add_action(
|
||||
"update-play-queue-from-server",
|
||||
|
@@ -10,11 +10,12 @@ from sublime.adapters import (
|
||||
AdapterManager,
|
||||
AlbumSearchQuery,
|
||||
api_objects as API,
|
||||
CacheMissError,
|
||||
Result,
|
||||
)
|
||||
from sublime.config import AppConfiguration
|
||||
from sublime.ui import util
|
||||
from sublime.ui.common import AlbumWithSongs, IconButton, SpinnerImage
|
||||
from sublime.ui.common import AlbumWithSongs, IconButton, LoadError, SpinnerImage
|
||||
|
||||
|
||||
def _to_type(query_type: AlbumSearchQuery.Type) -> str:
|
||||
@@ -59,6 +60,7 @@ class AlbumsPanel(Gtk.Box):
|
||||
),
|
||||
}
|
||||
|
||||
offline_mode = False
|
||||
populating_genre_combo = False
|
||||
grid_order_token: int = 0
|
||||
album_sort_direction: str = "ascending"
|
||||
@@ -233,6 +235,7 @@ class AlbumsPanel(Gtk.Box):
|
||||
|
||||
if app_config:
|
||||
self.current_query = app_config.state.current_album_search_query
|
||||
self.offline_mode = app_config.offline_mode
|
||||
|
||||
self.alphabetical_type_combo.set_active_id(
|
||||
{
|
||||
@@ -301,7 +304,9 @@ class AlbumsPanel(Gtk.Box):
|
||||
self.populate_genre_combo(app_config, force=force)
|
||||
|
||||
# At this point, the current query should be totally updated.
|
||||
self.grid_order_token = self.grid.update_params(self.current_query)
|
||||
self.grid_order_token = self.grid.update_params(
|
||||
self.current_query, self.offline_mode
|
||||
)
|
||||
self.grid.update(self.grid_order_token, app_config, force=force)
|
||||
|
||||
def _get_opposite_sort_dir(self, sort_dir: str) -> str:
|
||||
@@ -400,7 +405,7 @@ class AlbumsPanel(Gtk.Box):
|
||||
if self.to_year_spin_button == entry:
|
||||
new_year_tuple = (self.current_query.year_range[0], year)
|
||||
else:
|
||||
new_year_tuple = (year, self.current_query.year_range[0])
|
||||
new_year_tuple = (year, self.current_query.year_range[1])
|
||||
|
||||
self.emit_if_not_updating(
|
||||
"refresh-window",
|
||||
@@ -505,6 +510,7 @@ class AlbumsGrid(Gtk.Overlay):
|
||||
current_models: List[_AlbumModel] = []
|
||||
latest_applied_order_ratchet: int = 0
|
||||
order_ratchet: int = 0
|
||||
offline_mode: bool = False
|
||||
|
||||
currently_selected_index: Optional[int] = None
|
||||
currently_selected_id: Optional[str] = None
|
||||
@@ -515,11 +521,14 @@ class AlbumsGrid(Gtk.Overlay):
|
||||
next_page_fn = None
|
||||
server_hash: Optional[str] = None
|
||||
|
||||
def update_params(self, query: AlbumSearchQuery) -> int:
|
||||
def update_params(self, query: AlbumSearchQuery, offline_mode: bool) -> int:
|
||||
# If there's a diff, increase the ratchet.
|
||||
if self.current_query.strhash() != query.strhash():
|
||||
self.order_ratchet += 1
|
||||
self.current_query = query
|
||||
if offline_mode != self.offline_mode:
|
||||
self.order_ratchet += 1
|
||||
self.offline_mode = offline_mode
|
||||
return self.order_ratchet
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -530,6 +539,9 @@ class AlbumsGrid(Gtk.Overlay):
|
||||
scrolled_window = Gtk.ScrolledWindow()
|
||||
grid_detail_grid_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
|
||||
self.error_container = Gtk.Box()
|
||||
grid_detail_grid_box.add(self.error_container)
|
||||
|
||||
def create_flowbox(**kwargs) -> Gtk.FlowBox:
|
||||
flowbox = Gtk.FlowBox(
|
||||
**kwargs,
|
||||
@@ -661,8 +673,12 @@ class AlbumsGrid(Gtk.Overlay):
|
||||
return
|
||||
self.latest_applied_order_ratchet = self.order_ratchet
|
||||
|
||||
is_partial = False
|
||||
try:
|
||||
albums = f.result()
|
||||
albums = list(f.result())
|
||||
except CacheMissError as e:
|
||||
albums = cast(Optional[List[API.Album]], e.partial_data) or []
|
||||
is_partial = True
|
||||
except Exception as e:
|
||||
if self.error_dialog:
|
||||
self.spinner.hide()
|
||||
@@ -685,6 +701,20 @@ class AlbumsGrid(Gtk.Overlay):
|
||||
self.spinner.hide()
|
||||
return
|
||||
|
||||
for c in self.error_container.get_children():
|
||||
self.error_container.remove(c)
|
||||
if is_partial and self.current_query.type != AlbumSearchQuery.Type.RANDOM:
|
||||
load_error = LoadError(
|
||||
"Album list",
|
||||
"load albums",
|
||||
has_data=albums is not None and len(albums) > 0,
|
||||
offline_mode=self.offline_mode,
|
||||
)
|
||||
self.error_container.pack_start(load_error, True, True, 0)
|
||||
self.error_container.show_all()
|
||||
else:
|
||||
self.error_container.hide()
|
||||
|
||||
selected_index = None
|
||||
self.current_models = []
|
||||
for i, album in enumerate(albums):
|
||||
|
@@ -214,6 +214,16 @@
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
/* ********** Error Indicator ********** */
|
||||
#load-error-box {
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
#load-error-image,
|
||||
#load-error-label {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* ********** Artists & Albums ********** */
|
||||
#grid-artwork-spinner, #album-list-song-list-spinner {
|
||||
min-height: 35px;
|
||||
|
@@ -7,7 +7,7 @@ from gi.repository import Gio, GLib, GObject, Gtk, Pango
|
||||
from sublime.adapters import AdapterManager, api_objects as API
|
||||
from sublime.config import AppConfiguration
|
||||
from sublime.ui import util
|
||||
from sublime.ui.common import AlbumWithSongs, IconButton, SpinnerImage
|
||||
from sublime.ui.common import AlbumWithSongs, IconButton, LoadError, SpinnerImage
|
||||
|
||||
|
||||
class ArtistsPanel(Gtk.Paned):
|
||||
@@ -72,6 +72,9 @@ class ArtistList(Gtk.Box):
|
||||
|
||||
self.add(list_actions)
|
||||
|
||||
self.error_container = Gtk.Box()
|
||||
self.add(self.error_container)
|
||||
|
||||
self.loading_indicator = Gtk.ListBox()
|
||||
spinner_row = Gtk.ListBoxRow(activatable=False, selectable=False)
|
||||
spinner = Gtk.Spinner(name="artist-list-spinner", active=True)
|
||||
@@ -124,12 +127,27 @@ class ArtistList(Gtk.Box):
|
||||
self,
|
||||
artists: Sequence[API.Artist],
|
||||
app_config: AppConfiguration = None,
|
||||
is_partial: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
if app_config:
|
||||
self._app_config = app_config
|
||||
self.refresh_button.set_sensitive(not app_config.offline_mode)
|
||||
|
||||
for c in self.error_container.get_children():
|
||||
self.error_container.remove(c)
|
||||
if is_partial:
|
||||
load_error = LoadError(
|
||||
"Artist list",
|
||||
"load artists",
|
||||
has_data=len(artists) > 0,
|
||||
offline_mode=self._app_config.offline_mode,
|
||||
)
|
||||
self.error_container.pack_start(load_error, True, True, 0)
|
||||
self.error_container.show_all()
|
||||
else:
|
||||
self.error_container.hide()
|
||||
|
||||
new_store = []
|
||||
selected_idx = None
|
||||
for i, artist in enumerate(artists):
|
||||
@@ -318,6 +336,7 @@ class ArtistDetailPanel(Gtk.Box):
|
||||
app_config: AppConfiguration,
|
||||
force: bool = False,
|
||||
order_token: int = None,
|
||||
is_partial: bool = False,
|
||||
):
|
||||
if order_token != self.update_order_token:
|
||||
return
|
||||
@@ -393,6 +412,7 @@ class ArtistDetailPanel(Gtk.Box):
|
||||
app_config: AppConfiguration,
|
||||
force: bool = False,
|
||||
order_token: int = None,
|
||||
is_partial: bool = False,
|
||||
):
|
||||
if order_token != self.update_order_token:
|
||||
return
|
||||
|
@@ -3,10 +3,10 @@ from typing import Any, cast, List, Optional, Tuple
|
||||
|
||||
from gi.repository import Gdk, Gio, GLib, GObject, Gtk, Pango
|
||||
|
||||
from sublime.adapters import AdapterManager, api_objects as API, Result
|
||||
from sublime.adapters import AdapterManager, api_objects as API, CacheMissError, Result
|
||||
from sublime.config import AppConfiguration
|
||||
from sublime.ui import util
|
||||
from sublime.ui.common import IconButton, SongListColumn
|
||||
from sublime.ui.common import IconButton, LoadError, SongListColumn
|
||||
|
||||
|
||||
class BrowsePanel(Gtk.Overlay):
|
||||
@@ -30,6 +30,10 @@ class BrowsePanel(Gtk.Overlay):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
scrolled_window = Gtk.ScrolledWindow()
|
||||
window_box = Gtk.Box()
|
||||
|
||||
self.error_container = Gtk.Box()
|
||||
window_box.pack_start(self.error_container, True, True, 0)
|
||||
|
||||
self.root_directory_listing = ListAndDrilldown()
|
||||
self.root_directory_listing.connect(
|
||||
@@ -38,8 +42,9 @@ class BrowsePanel(Gtk.Overlay):
|
||||
self.root_directory_listing.connect(
|
||||
"refresh-window", lambda _, *args: self.emit("refresh-window", *args),
|
||||
)
|
||||
scrolled_window.add(self.root_directory_listing)
|
||||
window_box.add(self.root_directory_listing)
|
||||
|
||||
scrolled_window.add(window_box)
|
||||
self.add(scrolled_window)
|
||||
|
||||
self.spinner = Gtk.Spinner(
|
||||
@@ -60,7 +65,22 @@ class BrowsePanel(Gtk.Overlay):
|
||||
if self.update_order_token != update_order_token:
|
||||
return
|
||||
|
||||
self.root_directory_listing.update(id_stack, app_config, force)
|
||||
if len(id_stack) == 0:
|
||||
self.root_directory_listing.hide()
|
||||
if len(self.error_container.get_children()) == 0:
|
||||
load_error = LoadError(
|
||||
"Directory list",
|
||||
"browse to song",
|
||||
has_data=False,
|
||||
offline_mode=app_config.offline_mode,
|
||||
)
|
||||
self.error_container.pack_start(load_error, True, True, 0)
|
||||
self.error_container.show_all()
|
||||
else:
|
||||
for c in self.error_container.get_children():
|
||||
self.error_container.remove(c)
|
||||
self.error_container.hide()
|
||||
self.root_directory_listing.update(id_stack, app_config, force)
|
||||
self.spinner.hide()
|
||||
|
||||
def calculate_path() -> Tuple[str, ...]:
|
||||
@@ -68,13 +88,19 @@ class BrowsePanel(Gtk.Overlay):
|
||||
return ("root",)
|
||||
|
||||
id_stack = []
|
||||
while current_dir_id and (
|
||||
directory := AdapterManager.get_directory(
|
||||
current_dir_id, before_download=self.spinner.show,
|
||||
).result()
|
||||
):
|
||||
id_stack.append(directory.id)
|
||||
current_dir_id = directory.parent_id
|
||||
while current_dir_id:
|
||||
try:
|
||||
directory = AdapterManager.get_directory(
|
||||
current_dir_id, before_download=self.spinner.show,
|
||||
).result()
|
||||
except CacheMissError as e:
|
||||
directory = cast(API.Directory, e.partial_data)
|
||||
|
||||
if not directory:
|
||||
break
|
||||
else:
|
||||
id_stack.append(directory.id)
|
||||
current_dir_id = directory.parent_id
|
||||
|
||||
return tuple(id_stack)
|
||||
|
||||
@@ -123,6 +149,7 @@ class ListAndDrilldown(Gtk.Paned):
|
||||
):
|
||||
*child_id_stack, dir_id = id_stack
|
||||
selected_id = child_id_stack[-1] if len(child_id_stack) > 0 else None
|
||||
self.show()
|
||||
|
||||
self.list.update(
|
||||
directory_id=dir_id,
|
||||
@@ -199,6 +226,9 @@ class MusicDirectoryList(Gtk.Box):
|
||||
self.loading_indicator.add(spinner_row)
|
||||
self.pack_start(self.loading_indicator, False, False, 0)
|
||||
|
||||
self.error_container = Gtk.Box()
|
||||
self.add(self.error_container)
|
||||
|
||||
self.scroll_window = Gtk.ScrolledWindow(min_content_width=250)
|
||||
scrollbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
|
||||
@@ -257,6 +287,8 @@ class MusicDirectoryList(Gtk.Box):
|
||||
# Deselect everything if switching online to offline.
|
||||
if self.offline_mode != app_config.offline_mode:
|
||||
self.directory_song_list.get_selection().unselect_all()
|
||||
for c in self.error_container.get_children():
|
||||
self.error_container.remove(c)
|
||||
|
||||
self.offline_mode = app_config.offline_mode
|
||||
|
||||
@@ -275,10 +307,26 @@ class MusicDirectoryList(Gtk.Box):
|
||||
app_config: AppConfiguration = None,
|
||||
force: bool = False,
|
||||
order_token: int = None,
|
||||
is_partial: bool = False,
|
||||
):
|
||||
if order_token != self.update_order_token:
|
||||
return
|
||||
|
||||
dir_children = directory.children or []
|
||||
for c in self.error_container.get_children():
|
||||
self.error_container.remove(c)
|
||||
if is_partial:
|
||||
load_error = LoadError(
|
||||
"Directory listing",
|
||||
"load directory",
|
||||
has_data=len(dir_children) > 0,
|
||||
offline_mode=self.offline_mode,
|
||||
)
|
||||
self.error_container.pack_start(load_error, True, True, 0)
|
||||
self.error_container.show_all()
|
||||
else:
|
||||
self.error_container.hide()
|
||||
|
||||
# This doesn't look efficient, since it's doing a ton of passses over the data,
|
||||
# but there is some annoying memory overhead for generating the stores to diff,
|
||||
# so we are short-circuiting by checking to see if any of the the IDs have
|
||||
@@ -289,7 +337,10 @@ class MusicDirectoryList(Gtk.Box):
|
||||
# changed.
|
||||
children_ids, children, song_ids = [], [], []
|
||||
selected_dir_idx = None
|
||||
for i, c in enumerate(directory.children):
|
||||
if len(self._current_child_ids) != len(dir_children):
|
||||
force = True
|
||||
|
||||
for i, c in enumerate(dir_children):
|
||||
if i >= len(self._current_child_ids) or c.id != self._current_child_ids[i]:
|
||||
force = True
|
||||
|
||||
@@ -352,6 +403,7 @@ class MusicDirectoryList(Gtk.Box):
|
||||
]
|
||||
|
||||
util.diff_song_store(self.directory_song_store, new_songs_store)
|
||||
self.directory_song_list.show()
|
||||
|
||||
if len(self.drilldown_directories_store) == 0:
|
||||
self.list.hide()
|
||||
|
@@ -1,6 +1,7 @@
|
||||
from .album_with_songs import AlbumWithSongs
|
||||
from .edit_form_dialog import EditFormDialog
|
||||
from .icon_button import IconButton, IconMenuButton, IconToggleButton
|
||||
from .load_error import LoadError
|
||||
from .song_list_column import SongListColumn
|
||||
from .spinner_image import SpinnerImage
|
||||
|
||||
@@ -10,6 +11,7 @@ __all__ = (
|
||||
"IconButton",
|
||||
"IconMenuButton",
|
||||
"IconToggleButton",
|
||||
"LoadError",
|
||||
"SongListColumn",
|
||||
"SpinnerImage",
|
||||
)
|
||||
|
@@ -6,9 +6,11 @@ from gi.repository import Gdk, GLib, GObject, Gtk, Pango
|
||||
from sublime.adapters import AdapterManager, api_objects as API, Result
|
||||
from sublime.config import AppConfiguration
|
||||
from sublime.ui import util
|
||||
from sublime.ui.common.icon_button import IconButton
|
||||
from sublime.ui.common.song_list_column import SongListColumn
|
||||
from sublime.ui.common.spinner_image import SpinnerImage
|
||||
|
||||
from .icon_button import IconButton
|
||||
from .load_error import LoadError
|
||||
from .song_list_column import SongListColumn
|
||||
from .spinner_image import SpinnerImage
|
||||
|
||||
|
||||
class AlbumWithSongs(Gtk.Box):
|
||||
@@ -125,6 +127,9 @@ class AlbumWithSongs(Gtk.Box):
|
||||
self.loading_indicator_container = Gtk.Box()
|
||||
album_details.add(self.loading_indicator_container)
|
||||
|
||||
self.error_container = Gtk.Box()
|
||||
album_details.add(self.error_container)
|
||||
|
||||
# clickable, cache status, title, duration, song ID
|
||||
self.album_song_store = Gtk.ListStore(bool, str, str, str, str)
|
||||
|
||||
@@ -247,9 +252,13 @@ class AlbumWithSongs(Gtk.Box):
|
||||
|
||||
def update(self, app_config: AppConfiguration = None, force: bool = False):
|
||||
if app_config:
|
||||
# Deselect everything if switching online to offline.
|
||||
# Deselect everything and reset the error container if switching between
|
||||
# online and offline.
|
||||
if self.offline_mode != app_config.offline_mode:
|
||||
self.album_songs.get_selection().unselect_all()
|
||||
for c in self.error_container.get_children():
|
||||
self.error_container.remove(c)
|
||||
|
||||
self.offline_mode = app_config.offline_mode
|
||||
|
||||
self.update_album_songs(self.album.id, app_config=app_config, force=force)
|
||||
@@ -278,29 +287,48 @@ class AlbumWithSongs(Gtk.Box):
|
||||
app_config: AppConfiguration,
|
||||
force: bool = False,
|
||||
order_token: int = None,
|
||||
is_partial: bool = False,
|
||||
):
|
||||
song_ids = [s.id for s in album.songs or []]
|
||||
songs = album.songs or []
|
||||
if is_partial:
|
||||
if len(self.error_container.get_children()) == 0:
|
||||
load_error = LoadError(
|
||||
"Song list",
|
||||
"retrieve songs",
|
||||
has_data=len(songs) > 0,
|
||||
offline_mode=self.offline_mode,
|
||||
)
|
||||
self.error_container.pack_start(load_error, True, True, 0)
|
||||
self.error_container.show_all()
|
||||
else:
|
||||
self.error_container.hide()
|
||||
|
||||
song_ids = [s.id for s in songs]
|
||||
new_store = []
|
||||
any_song_playable = False
|
||||
for cached_status, song in zip(
|
||||
util.get_cached_status_icons(song_ids), album.songs or []
|
||||
):
|
||||
playable = not self.offline_mode or cached_status in (
|
||||
"folder-download-symbolic",
|
||||
"view-pin-symbolic",
|
||||
)
|
||||
new_store.append(
|
||||
[
|
||||
playable,
|
||||
cached_status,
|
||||
util.esc(song.title),
|
||||
util.format_song_duration(song.duration),
|
||||
song.id,
|
||||
]
|
||||
)
|
||||
any_song_playable |= playable
|
||||
|
||||
song_ids = [cast(str, song[-1]) for song in new_store]
|
||||
if len(songs) == 0:
|
||||
self.album_songs.hide()
|
||||
else:
|
||||
self.album_songs.show()
|
||||
for status, song in zip(util.get_cached_status_icons(song_ids), songs):
|
||||
playable = not self.offline_mode or status in (
|
||||
"folder-download-symbolic",
|
||||
"view-pin-symbolic",
|
||||
)
|
||||
new_store.append(
|
||||
[
|
||||
playable,
|
||||
status,
|
||||
util.esc(song.title),
|
||||
util.format_song_duration(song.duration),
|
||||
song.id,
|
||||
]
|
||||
)
|
||||
any_song_playable |= playable
|
||||
|
||||
song_ids = [cast(str, song[-1]) for song in new_store]
|
||||
util.diff_song_store(self.album_song_store, new_store)
|
||||
|
||||
self.play_btn.set_sensitive(any_song_playable)
|
||||
self.shuffle_btn.set_sensitive(any_song_playable)
|
||||
@@ -317,7 +345,5 @@ class AlbumWithSongs(Gtk.Box):
|
||||
self.play_next_btn.set_action_name("")
|
||||
self.add_to_queue_btn.set_action_name("")
|
||||
|
||||
util.diff_song_store(self.album_song_store, new_store)
|
||||
|
||||
# Have to idle_add here so that his happens after the component is rendered.
|
||||
self.set_loading(False)
|
||||
|
59
sublime/ui/common/load_error.py
Normal file
59
sublime/ui/common/load_error.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
class LoadError(Gtk.Box):
|
||||
def __init__(
|
||||
self, entity_name: str, action: str, has_data: bool, offline_mode: bool
|
||||
):
|
||||
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL)
|
||||
|
||||
self.pack_start(Gtk.Box(), True, True, 0)
|
||||
|
||||
inner_box = Gtk.Box(
|
||||
orientation=Gtk.Orientation.HORIZONTAL, name="load-error-box"
|
||||
)
|
||||
|
||||
inner_box.pack_start(Gtk.Box(), True, True, 0)
|
||||
|
||||
error_description_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
|
||||
if offline_mode:
|
||||
icon_name = "weather-severe-alert-symbolic"
|
||||
label = f"{entity_name} may be incomplete.\n" if has_data else ""
|
||||
label += f"Go online to {action}."
|
||||
else:
|
||||
icon_name = "dialog-error"
|
||||
label = f"Error attempting to {action}."
|
||||
|
||||
image = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.DIALOG)
|
||||
image.set_name("load-error-image")
|
||||
error_description_box.add(image)
|
||||
|
||||
error_description_box.add(
|
||||
Gtk.Label(
|
||||
label=label, justify=Gtk.Justification.CENTER, name="load-error-label"
|
||||
)
|
||||
)
|
||||
|
||||
box = Gtk.Box()
|
||||
box.pack_start(Gtk.Box(), True, True, 0)
|
||||
|
||||
if offline_mode:
|
||||
go_online_button = Gtk.Button(label="Go Online")
|
||||
go_online_button.set_action_name("app.go-online")
|
||||
box.add(go_online_button)
|
||||
else:
|
||||
retry_button = Gtk.Button(label="Retry")
|
||||
retry_button.set_action_name("app.refresh-window")
|
||||
box.add(retry_button)
|
||||
|
||||
box.pack_start(Gtk.Box(), True, True, 0)
|
||||
error_description_box.add(box)
|
||||
|
||||
inner_box.add(error_description_box)
|
||||
|
||||
inner_box.pack_start(Gtk.Box(), True, True, 0)
|
||||
|
||||
self.add(inner_box)
|
||||
|
||||
self.pack_start(Gtk.Box(), True, True, 0)
|
@@ -1,5 +1,5 @@
|
||||
from functools import partial
|
||||
from typing import Any, Callable, Optional, Set, Tuple
|
||||
from typing import Any, Callable, Dict, Optional, Set, Tuple
|
||||
|
||||
from gi.repository import Gdk, GLib, GObject, Gtk, Pango
|
||||
|
||||
@@ -127,13 +127,14 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
elif AdapterManager.get_ping_status():
|
||||
status_label = "Connected"
|
||||
else:
|
||||
status_label = "Error"
|
||||
status_label = "Error Connecting to Server"
|
||||
|
||||
self.server_connection_menu_button.set_icon(
|
||||
f"server-subsonic-{status_label.lower()}-symbolic"
|
||||
f"server-subsonic-{status_label.split()[0].lower()}-symbolic"
|
||||
)
|
||||
self.connection_status_icon.set_from_icon_name(
|
||||
f"server-{status_label.lower()}-symbolic", Gtk.IconSize.BUTTON
|
||||
f"server-{status_label.split()[0].lower()}-symbolic",
|
||||
Gtk.IconSize.BUTTON,
|
||||
)
|
||||
self.connection_status_label.set_text(status_label)
|
||||
self.connected_status_box.show_all()
|
||||
@@ -271,7 +272,7 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
self, label: str, settings_name: str
|
||||
) -> Tuple[Gtk.Box, Gtk.Switch]:
|
||||
def on_active_change(toggle: Gtk.Switch, _):
|
||||
self._emit_settings_change(**{settings_name: toggle.get_active()})
|
||||
self._emit_settings_change({settings_name: toggle.get_active()})
|
||||
|
||||
box = Gtk.Box()
|
||||
box.add(gtk_label := Gtk.Label(label=label))
|
||||
@@ -295,7 +296,7 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
self, label: str, low: int, high: int, step: int, settings_name: str
|
||||
) -> Tuple[Gtk.Box, Gtk.Entry]:
|
||||
def on_change(entry: Gtk.SpinButton) -> bool:
|
||||
self._emit_settings_change(**{settings_name: int(entry.get_value())})
|
||||
self._emit_settings_change({settings_name: int(entry.get_value())})
|
||||
return False
|
||||
|
||||
box = Gtk.Box()
|
||||
@@ -636,7 +637,7 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
|
||||
def _on_replay_gain_change(self, combo: Gtk.ComboBox):
|
||||
self._emit_settings_change(
|
||||
replay_gain=ReplayGainType.from_string(combo.get_active_id())
|
||||
{"replay_gain": ReplayGainType.from_string(combo.get_active_id())}
|
||||
)
|
||||
|
||||
def _on_search_entry_focus(self, *args):
|
||||
@@ -690,10 +691,10 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
|
||||
# Helper Functions
|
||||
# =========================================================================
|
||||
def _emit_settings_change(self, **kwargs):
|
||||
def _emit_settings_change(self, changed_settings: Dict[str, Any]):
|
||||
if self._updating_settings:
|
||||
return
|
||||
self.emit("refresh-window", {"__settings__": kwargs}, False)
|
||||
self.emit("refresh-window", {"__settings__": changed_settings}, False)
|
||||
|
||||
def _show_search(self):
|
||||
self.search_entry.set_size_request(300, -1)
|
||||
|
@@ -325,6 +325,7 @@ class PlayerControls(Gtk.ActionBar):
|
||||
app_config: AppConfiguration,
|
||||
force: bool = False,
|
||||
order_token: int = None,
|
||||
is_partial: bool = False,
|
||||
):
|
||||
if order_token != self.cover_art_update_order_token:
|
||||
return
|
||||
|
@@ -5,12 +5,13 @@ from typing import Any, cast, Iterable, List, Tuple
|
||||
from fuzzywuzzy import process
|
||||
from gi.repository import Gdk, Gio, GLib, GObject, Gtk, Pango
|
||||
|
||||
from sublime.adapters import AdapterManager, api_objects as API
|
||||
from sublime.adapters import AdapterManager, api_objects as API, CacheMissError
|
||||
from sublime.config import AppConfiguration
|
||||
from sublime.ui import util
|
||||
from sublime.ui.common import (
|
||||
EditFormDialog,
|
||||
IconButton,
|
||||
LoadError,
|
||||
SongListColumn,
|
||||
SpinnerImage,
|
||||
)
|
||||
@@ -73,6 +74,8 @@ class PlaylistList(Gtk.Box):
|
||||
),
|
||||
}
|
||||
|
||||
offline_mode = False
|
||||
|
||||
class PlaylistModel(GObject.GObject):
|
||||
playlist_id = GObject.Property(type=str)
|
||||
name = GObject.Property(type=str)
|
||||
@@ -99,6 +102,9 @@ class PlaylistList(Gtk.Box):
|
||||
|
||||
self.add(playlist_list_actions)
|
||||
|
||||
self.error_container = Gtk.Box()
|
||||
self.add(self.error_container)
|
||||
|
||||
loading_new_playlist = Gtk.ListBox()
|
||||
|
||||
self.loading_indicator = Gtk.ListBoxRow(activatable=False, selectable=False,)
|
||||
@@ -166,6 +172,7 @@ class PlaylistList(Gtk.Box):
|
||||
|
||||
def update(self, app_config: AppConfiguration = None, force: bool = False):
|
||||
if app_config:
|
||||
self.offline_mode = app_config.offline_mode
|
||||
self.new_playlist_button.set_sensitive(not app_config.offline_mode)
|
||||
self.list_refresh_button.set_sensitive(not app_config.offline_mode)
|
||||
self.new_playlist_row.hide()
|
||||
@@ -182,7 +189,22 @@ class PlaylistList(Gtk.Box):
|
||||
app_config: AppConfiguration = None,
|
||||
force: bool = False,
|
||||
order_token: int = None,
|
||||
is_partial: bool = False,
|
||||
):
|
||||
for c in self.error_container.get_children():
|
||||
self.error_container.remove(c)
|
||||
if is_partial:
|
||||
load_error = LoadError(
|
||||
"Playlist list",
|
||||
"load playlists",
|
||||
has_data=len(playlists) > 0,
|
||||
offline_mode=self.offline_mode,
|
||||
)
|
||||
self.error_container.pack_start(load_error, True, True, 0)
|
||||
self.error_container.show_all()
|
||||
else:
|
||||
self.error_container.hide()
|
||||
|
||||
new_store = []
|
||||
selected_idx = None
|
||||
for i, playlist in enumerate(playlists or []):
|
||||
@@ -471,6 +493,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
app_config: AppConfiguration = None,
|
||||
force: bool = False,
|
||||
order_token: int = None,
|
||||
is_partial: bool = False,
|
||||
):
|
||||
if self.update_playlist_view_order_token != order_token:
|
||||
return
|
||||
@@ -596,6 +619,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
app_config: AppConfiguration,
|
||||
force: bool = False,
|
||||
order_token: int = None,
|
||||
is_partial: bool = False,
|
||||
):
|
||||
if self.update_playlist_view_order_token != order_token:
|
||||
return
|
||||
|
@@ -16,7 +16,7 @@ from typing import (
|
||||
from deepdiff import DeepDiff
|
||||
from gi.repository import Gdk, GLib, Gtk
|
||||
|
||||
from sublime.adapters import AdapterManager, Result, SongCacheStatus
|
||||
from sublime.adapters import AdapterManager, CacheMissError, Result, SongCacheStatus
|
||||
from sublime.adapters.api_objects import Playlist, Song
|
||||
from sublime.config import AppConfiguration
|
||||
|
||||
@@ -395,6 +395,15 @@ def async_callback(
|
||||
def future_callback(is_immediate: bool, f: Result):
|
||||
try:
|
||||
result = f.result()
|
||||
is_partial = False
|
||||
except CacheMissError as e:
|
||||
result = e.partial_data
|
||||
if result is None:
|
||||
if on_failure:
|
||||
GLib.idle_add(on_failure, self, e)
|
||||
return
|
||||
|
||||
is_partial = True
|
||||
except Exception as e:
|
||||
if on_failure:
|
||||
GLib.idle_add(on_failure, self, e)
|
||||
@@ -407,6 +416,7 @@ def async_callback(
|
||||
app_config=app_config,
|
||||
force=force,
|
||||
order_token=order_token,
|
||||
is_partial=is_partial,
|
||||
)
|
||||
|
||||
if is_immediate:
|
||||
@@ -415,8 +425,8 @@ def async_callback(
|
||||
# event queue.
|
||||
fn()
|
||||
else:
|
||||
# We don'h have the data, and we have to idle add so that we don't
|
||||
# seg fault GTK.
|
||||
# We don't have the data yet, meaning that it is a future, and we
|
||||
# have to idle add so that we don't seg fault GTK.
|
||||
GLib.idle_add(fn)
|
||||
|
||||
result: Result = future_fn(
|
||||
|
Reference in New Issue
Block a user