Adding more offline mode load errors

This commit is contained in:
Sumner Evans
2020-05-25 18:50:38 -06:00
parent b79ce9e205
commit eee58f97e1
17 changed files with 396 additions and 127 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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}")

View File

@@ -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):

View File

@@ -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",

View File

@@ -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):

View File

@@ -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;

View File

@@ -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

View File

@@ -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()

View File

@@ -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",
)

View File

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

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

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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(