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