From 41976b2744924e2270ae5e054c400b616af6038d Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Tue, 20 Feb 2024 09:50:48 +0100 Subject: [PATCH] Load home tab contents in separate threads to improve application startup time NOTE: widget rendering is now earliest-possible / earliest-reasonable (rendering headlines when the first child is added) to reduce the time spent on a loading animation and make newer content available faster --- melon/home/history.py | 59 +++++++++++++++++++++++------ melon/home/new.py | 84 +++++++++++++++++++++++++++++++++++------ melon/home/playlists.py | 30 +++++++++++++-- melon/home/subs.py | 34 +++++++++++++++-- 4 files changed, 178 insertions(+), 29 deletions(-) diff --git a/melon/home/history.py b/melon/home/history.py index 21b06b8..40bf808 100644 --- a/melon/home/history.py +++ b/melon/home/history.py @@ -3,6 +3,7 @@ import gi gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') from gi.repository import Gtk, Adw +import threading from melon.widgets.viewstackpage import ViewStackPage from melon.widgets.iconbutton import IconButton @@ -11,11 +12,15 @@ from melon.models import get_history, register_callback, get_app_settings from melon.servers.utils import group_by_date, filter_resources class History(ViewStackPage): + # datecount displayed + page_size = 7 + def update(self): app_settings = get_app_settings() # make sure only videos from available servers are shown hist = filter_resources(get_history(), app_settings, access=lambda x:x[0]) hist.reverse() + self.data = list(group_by_date(hist).items()) if len(hist) == 0: status = Adw.StatusPage() status.set_title("*crickets chirping*") @@ -28,19 +33,49 @@ class History(ViewStackPage): status.set_child(box) self.inner.set_child(status) else: - grouped = group_by_date(hist) - results = Adw.PreferencesPage() + self.results = Adw.PreferencesPage() title = Adw.PreferencesGroup() title.set_title("History") title.set_description("These are the videos you opened in the past") - results.add(title) - for date, content in grouped.items(): - group = Adw.PreferencesGroup() - group.set_title(date) - for resource in content: - group.add(AdaptiveFeedItem(resource, app_settings["show_images_in_feed"])) - results.add(group) - self.inner.set_child(results) + self.results.add(title) + self.inner.set_child(self.results) + self.load_page(0) + + def load_page(self, page=0): + app_settings = get_app_settings() + for date, content in self.data[page*self.page_size:(page+1)*self.page_size]: + # faster thread breakout + if self.stop_update: + break + group = Adw.PreferencesGroup() + group.set_title(date) + self.results.add(group) + for resource in content: + # faster thread breakout + if self.stop_update: + break + group.add(AdaptiveFeedItem(resource, app_settings["show_images_in_feed"])) + # TODO somehow add "load more" button + + stop_update = False + thread = None + def do_update(self): + if not self.thread is None: + self.stop_update = True + # wait for old thread to finish + self.thread.join() + self.stop_update = False + # show spinner + spinner = Gtk.Spinner() + spinner.set_size_request(50, 50) + spinner.start() + cb = Gtk.CenterBox() + cb.set_center_widget(spinner) + self.inner.set_child(cb) + # start thread + self.thread = threading.Thread(target=self.update) + self.thread.daemon = True + self.thread.start() def __init__(self): super().__init__("history", "History", "media-playlist-repeat-symbolic") @@ -48,6 +83,6 @@ class History(ViewStackPage): self.inner = Gtk.ScrolledWindow() self.widget.set_child(self.inner) # register update listener - register_callback("history_changed", "home-history", self.update) + register_callback("history_changed", "home-history", self.do_update) # manually run render - self.update() + self.do_update() diff --git a/melon/home/new.py b/melon/home/new.py index e4b253e..2e5d520 100644 --- a/melon/home/new.py +++ b/melon/home/new.py @@ -2,21 +2,32 @@ import sys import gi gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') -from gi.repository import Gtk, Adw +from gi.repository import Gtk, Adw, GLib +import threading +from datetime import datetime from melon.widgets.viewstackpage import ViewStackPage from melon.widgets.iconbutton import IconButton from melon.widgets.feeditem import AdaptiveFeedItem from melon.servers.utils import fetch_home_feed, group_by_date -from melon.models import register_callback, get_subscribed_channels, get_app_settings +from melon.models import register_callback +from melon.models import get_subscribed_channels, get_app_settings +from melon.models import get_cached_feed, clear_cached_feed, update_cached_feed, get_last_feed_refresh class NewFeed(ViewStackPage): def update(self): - # NOTE: fetch_home_feed automatically dismisses channes - # if the server isn't available (i.e because it has been disabled) - news = fetch_home_feed() + news = get_cached_feed() + if len(news) == 0: + # long task: + # NOTE: fetch_home_feed automatically dismisses channels + # if the server isn't available (i.e because it has been disabled) + news = fetch_home_feed() + update_cached_feed(news) # reverse the feed, because the latest (highest uts) should be on top news.reverse() + refresh_btn = IconButton("Refresh", "view-refresh-symbolic") + refresh_btn.connect("clicked", lambda a: self.reload()) + # render data if len(news) == 0: status = Adw.StatusPage() status.set_title("*crickets chirping*") @@ -28,24 +39,75 @@ class NewFeed(ViewStackPage): status.set_icon_name("weather-few-clouds-night-symbolic") icon_button = IconButton("Browse Servers", "list-add-symbolic") icon_button.set_action_name("win.browse") + icon_button.set_margin_bottom(6) box = Gtk.CenterBox() - box.set_center_widget(icon_button) + ls = Gtk.Box(orientation = Gtk.Orientation.VERTICAL) + ls.append(icon_button) + ls.append(refresh_btn) + box.set_center_widget(ls) status.set_child(box) self.inner.set_child(status) else: app_settings = get_app_settings() results = Adw.PreferencesPage() title = Adw.PreferencesGroup() - title.set_title("What's new") + last_refresh = get_last_feed_refresh() + if last_refresh is None: + last_refresh = "Never" + else: + last_refresh = datetime.fromtimestamp(last_refresh).strftime("%c") + refresh_info = f" (Last refresh: {last_refresh})" + title.set_title("What's new" + refresh_info) title.set_description("These are the latest videos of channels you follow") + title.set_header_suffix(refresh_btn) + # append box and then add results + # this keeps the loading animation shorter + # and newer videos will be visible fairly quickly results.add(title) + self.inner.set_child(results) for date, content in group_by_date(news).items(): + # because we have to join the thread before reset + # we need a way to skip the old drawing process + # as it would take way longer otherwise + if self.stop_update: + break group = Adw.PreferencesGroup() group.set_title(date) + added = False for resource in content: + # faster thread breakout + if self.stop_update: + break group.add(AdaptiveFeedItem(resource, app_settings["show_images_in_feed"])) - results.add(group) - self.inner.set_child(results) + # add preference group to results when first item is added to + if not added: + added = True + results.add(group) + # return false, because otherwise ide_add would run multiple times + return False + stop_update = False + thread = None + def do_update(self): + if not self.thread is None: + self.stop_update = True + # wait for old thread to finish + self.thread.join() + self.stop_update = False + # show spinner + spinner = Gtk.Spinner() + spinner.set_size_request(50, 50) + spinner.start() + cb = Gtk.CenterBox() + cb.set_center_widget(spinner) + self.inner.set_child(cb) + # start thread + self.thread = threading.Thread(target=self.update) + self.thread.daemon = True + self.thread.start() + + def reload(self): + clear_cached_feed() + self.do_update() def __init__(self): super().__init__("feed-new", "What's new", "user-home-symbolic") @@ -53,6 +115,6 @@ class NewFeed(ViewStackPage): self.inner = Gtk.ScrolledWindow() self.widget.set_child(self.inner) # register update listener - register_callback("channels_changed", "home-new", self.update) + register_callback("channels_changed", "home-new", self.do_update) # manually run render - self.update() + self.do_update() diff --git a/melon/home/playlists.py b/melon/home/playlists.py index 16e7b50..2138526 100644 --- a/melon/home/playlists.py +++ b/melon/home/playlists.py @@ -3,6 +3,7 @@ import gi gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') from gi.repository import Gtk, Adw +import threading from melon.widgets.viewstackpage import ViewStackPage from melon.widgets.iconbutton import IconButton @@ -36,9 +37,32 @@ class Playlists(ViewStackPage): icon_button = IconButton("New", "list-add-symbolic", tooltip="Create a new playlist") icon_button.set_action_name("win.new_playlist") results.set_header_suffix(icon_button) + self.inner.set_child(results) for playlist in playlists: + # faster thread breakout + if self.stop_update: + break results.add(AdaptivePlaylistFeedItem(playlist, app_conf["show_images_in_feed"])) - self.inner.set_child(results) + + stop_update = False + thread = None + def do_update(self): + if not self.thread is None: + self.stop_update = True + # wait for old thread to finish + self.thread.join() + self.stop_update = False + # show spinner + spinner = Gtk.Spinner() + spinner.set_size_request(50, 50) + spinner.start() + cb = Gtk.CenterBox() + cb.set_center_widget(spinner) + self.inner.set_child(cb) + # start thread + self.thread = threading.Thread(target=self.update) + self.thread.daemon = True + self.thread.start() def __init__(self): super().__init__("playlists", "Playlists", "user-bookmarks-symbolic") @@ -46,6 +70,6 @@ class Playlists(ViewStackPage): self.inner = Gtk.ScrolledWindow() self.widget.set_child(self.inner) # register update listener - register_callback("playlists_changed", "home-playlist", self.update) + register_callback("playlists_changed", "home-playlist", self.do_update) # manually run render - self.update() + self.do_update() diff --git a/melon/home/subs.py b/melon/home/subs.py index 7d805aa..5004dd1 100644 --- a/melon/home/subs.py +++ b/melon/home/subs.py @@ -3,6 +3,7 @@ import gi gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') from gi.repository import Gtk, Adw +import threading from melon.widgets.viewstackpage import ViewStackPage from melon.widgets.iconbutton import IconButton @@ -31,9 +32,36 @@ class Subscriptions(ViewStackPage): results.set_title("Subscriptions") results.set_description("You are subscribed to the following channels") subs.sort(key=lambda c:c.name) + # append first, so subscriptions are added as we go + # not all at once + # reduces time spent on loading animation + # NOTE: might seem confusing to unknowing users + self.inner.set_child(results) for channel in subs: + # faster thread breakout + if self.stop_update: + break results.add(AdaptiveFeedItem(channel, app_settings["show_images_in_feed"])) - self.inner.set_child(results) + + stop_update = False + thread = None + def do_update(self): + if not self.thread is None: + self.stop_update = True + # wait for old thread to finish + self.thread.join() + self.stop_update = False + # show spinner + spinner = Gtk.Spinner() + spinner.set_size_request(50, 50) + spinner.start() + cb = Gtk.CenterBox() + cb.set_center_widget(spinner) + self.inner.set_child(cb) + # start thread + self.thread = threading.Thread(target=self.update) + self.thread.daemon = True + self.thread.start() def __init__(self): super().__init__("subs", "Subscriptions", "x-office-address-book-symbolic") @@ -41,6 +69,6 @@ class Subscriptions(ViewStackPage): self.inner = Gtk.ScrolledWindow() self.widget.set_child(self.inner) # register update listener - register_callback("channels_changed", "home-subs", self.update) + register_callback("channels_changed", "home-subs", self.do_update) # manually run render - self.update() + self.do_update() -- 2.38.5