~comcloudway/melon

41976b2744924e2270ae5e054c400b616af6038d — Jakob Meier 7 months ago e83700d
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
4 files changed, 178 insertions(+), 29 deletions(-)

M melon/home/history.py
M melon/home/new.py
M melon/home/playlists.py
M melon/home/subs.py
M melon/home/history.py => melon/home/history.py +47 -12
@@ 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()

M melon/home/new.py => melon/home/new.py +73 -11
@@ 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()

M melon/home/playlists.py => melon/home/playlists.py +27 -3
@@ 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()

M melon/home/subs.py => melon/home/subs.py +31 -3
@@ 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()