From c58675f899a8ecac218232ed38c732598815b770 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Wed, 13 Mar 2024 07:43:16 +0100 Subject: [PATCH] add playlist control options - easily switch songs by selecting one from the playlist content - enable shuffle mode, single-song-repeat or repeat-all (if shuffle is disabled) - navigate forwards/backwards (if shuffle is disabled) - skip songs (if shuffle is enabled) --- melon/browse/playlist.py | 5 +- melon/player/__init__.py | 26 ++-- melon/player/playlist.py | 192 ++++++++++++++++++++---- melon/servers/invidious/__init__.py | 12 +- melon/widgets/feeditem.py | 21 ++- po/POTFILES.in | 2 + po/de.po | 224 ++++++++++++++++++++-------- po/fa.po | 222 +++++++++++++++++++-------- po/melon.pot | 224 ++++++++++++++++++++-------- 9 files changed, 687 insertions(+), 241 deletions(-) diff --git a/melon/browse/playlist.py b/melon/browse/playlist.py index 758a4c9..0033ffa 100644 --- a/melon/browse/playlist.py +++ b/melon/browse/playlist.py @@ -34,13 +34,12 @@ class BrowsePlaylistScreen(Adw.NavigationPage): box = Gtk.Box() self.external_btn = IconButton("","modem-symbolic") self.external_btn.connect("clicked", self.on_open_in_browser) - box.append(self.external_btn) + self.header_bar.pack_end(self.external_btn) self.startplay_btn = IconButton("","media-playback-start-symbolic", tooltip=_("Start playing")) self.startplay_btn.set_action_name("win.playlistplayer-external") self.startplay_btn.set_action_target_value( GLib.Variant("as", [self.playlist.server, self.playlist.id])) - box.append(self.startplay_btn) - self.header_bar.pack_end(box) + self.header_bar.pack_end(self.startplay_btn) # base layout self.box = Adw.PreferencesPage() diff --git a/melon/player/__init__.py b/melon/player/__init__.py index e402da9..c4283d4 100644 --- a/melon/player/__init__.py +++ b/melon/player/__init__.py @@ -19,6 +19,9 @@ from melon.utils import pass_me class PlayerScreen(Adw.NavigationPage): duration = None position = None + + external_btn = None + def on_open_in_browser(self, arg): Gtk.UriLauncher.new(uri=self.video.url).launch() @@ -89,16 +92,6 @@ class PlayerScreen(Adw.NavigationPage): subs = get_subscribed_channels() status.set_description(_("Video could not be loaded")) status.set_icon_name("weather-few-clouds-night-symbolic") - icon_button = IconButton(_("Exit Playlist mode"), "window-close-symbolic") - # TODO fix action - icon_button.set_action_name("win.browse") - icon_button.set_margin_bottom(6) - box = Gtk.CenterBox() - 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.scrollview.set_child(status) return False @@ -121,6 +114,7 @@ class PlayerScreen(Adw.NavigationPage): self.channel = self.instance.get_channel_info(self.video.channel[1]) GLib.idle_add(self.display_channel) + view = None def __init__(self, server_id, video_id, *args, **kwargs): super().__init__(*args, **kwargs) if server_id is None or video_id is None: @@ -147,6 +141,9 @@ class PlayerScreen(Adw.NavigationPage): self.position = None self.duration = None + # disconnect old webview + self.stop_webview() + # show fallback title # in case init_video is called twice, # this resets the title @@ -170,6 +167,8 @@ class PlayerScreen(Adw.NavigationPage): server = get_servers_list()[server_id] self.instance = get_server_instance(server) + if not self.external_btn is None: + self.header_bar.remove(self.external_btn) self.external_btn = IconButton("","modem-symbolic") self.external_btn.connect("clicked", self.on_open_in_browser) self.header_bar.pack_end(self.external_btn) @@ -193,6 +192,7 @@ class PlayerScreen(Adw.NavigationPage): def on_hide(self): self.visible = False + self.stop_webview() # save playback position & duration when closing the video # NOTE: this does not include closing the app self.store_position() @@ -242,6 +242,8 @@ class PlayerScreen(Adw.NavigationPage): self.instance.connect_video_ended(self.view, self.on_end) def on_end(self, state): + if not state: + return # we've reached the end of the video # might as well manually save the position if not self.duration is None: @@ -276,3 +278,7 @@ class PlayerScreen(Adw.NavigationPage): if self.position is None or self.duration is None: return set_video_playback_position(self.video, self.position, self.duration) + + def stop_webview(self): + if not self.view is None: + self.view.terminate_web_process() diff --git a/melon/player/playlist.py b/melon/player/playlist.py index 6c9ae78..e245a59 100644 --- a/melon/player/playlist.py +++ b/melon/player/playlist.py @@ -6,6 +6,7 @@ from gi.repository import Gtk, Adw, WebKit, GLib from unidecode import unidecode from gettext import gettext as _ import threading +import random from melon.servers.utils import get_server_instance, get_servers_list from melon.servers import SearchMode @@ -26,6 +27,10 @@ class PlaylistPlayerScreen(PlayerScreen): playlist_instance = None playlist_content = [] + playlist_shuffle = False + playlist_repeat = False + playlist_repeat_single = False + def __init__(self, ref: (tuple[str, str] | int), index=0, *args, **kwargs): # make sure to initalize widget # PlayerScreen.__init__ will break after initializing parents @@ -37,6 +42,9 @@ class PlaylistPlayerScreen(PlayerScreen): self.video_duration = None self.video_position = None self.playlist_instance = None + self.playlist_shuffle = False + self.playlist_repeat = False + self.playlist_repeat_single = False # prepare base layout self.header_bar = Adw.HeaderBar() @@ -100,9 +108,37 @@ class PlaylistPlayerScreen(PlayerScreen): if self.playlist_index > len(self.playlist_content): GLib.idle_add(self.playlist_error) return - self.prepare(0) + + ind = self.get_next_index(0) + if not ind is None: + self.prepare(ind) + + def get_next_index(self,fallback=None): + index = self.playlist_index + 1 + if not fallback is None: + index = fallback + + if self.playlist_repeat_single: + # repeat single song is enabled + return self.playlist_index + elif self.playlist_shuffle: + # select a random title + # NOTE: may just repeat the same song again + index = random.randrange(0,len(self.playlist_content), 1) + return index + + if self.playlist_index+1 >= len(self.playlist_content): + # playlist ended + if self.playlist_repeat: + # playlist is set to repeat so we continue picking songs + return self.get_next_index(0) + else: + # no more songs to play + return None + return index def prepare(self, index): + self.playlist_index = index dt = self.playlist_content[index] server_id = dt.server video_id = dt.id @@ -111,47 +147,145 @@ class PlaylistPlayerScreen(PlayerScreen): def on_end(self, state): super().on_end(state) - # if there are more videos in the playlist - # load the next one - # do nothing if there are no more videos - if self.playlist_index+1 < len(self.playlist_content): - self.playlist_index += 1 - self.prepare(self.playlist_index) + if not state: + return + ind = self.get_next_index() + if not ind is None: + self.prepare(ind) def empty_playlist(self): # display playlist is empty banner # acts as a drop in replacement for the player screen status = Adw.StatusPage() status.set_title(_("*crickets chirping*")) - subs = get_subscribed_channels() status.set_description(_("This playlist is empty")) status.set_icon_name("weather-few-clouds-night-symbolic") - icon_button = IconButton(_("Exit Playlist mode"), "window-close-symbolic") - # TODO fix action - icon_button.set_action_name("win.browse") - icon_button.set_margin_bottom(6) - box = Gtk.CenterBox() - 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.scrollview.set_child(status) def playlist_error(self): status = Adw.StatusPage() status.set_title(_("*crickets chirping*")) - subs = get_subscribed_channels() status.set_description(_("There was an error loading the playlist")) status.set_icon_name("weather-few-clouds-night-symbolic") - icon_button = IconButton(_("Close"), "window-close-symbolic") - # TODO fix action - icon_button.set_action_name("win.browse") - icon_button.set_margin_bottom(6) - box = Gtk.CenterBox() - 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.scrollview.set_child(status) + + def background(self, video_id): + super().background(video_id) + GLib.idle_add(self.display_control) + + def ctr_previous_video(self): + index = self.playlist_index - 1 + if index >= 0: + self.prepare(index) + + def ctr_next_video(self): + index = self.playlist_index + 1 + if index < len(self.playlist_content): + self.prepare(index) + + def ctr_skip_video(self): + ind = self.get_next_index() + if not ind is None: + self.prepare(ind) + + def ctr_toggle_shuffle(self, state): + self.playlist_shuffle = state + # force rerender the playlist control box + # updates the prev/next and repeat_all button + self.about.remove(self.ctr_group) + self.display_control() + + def ctr_toggle_playlist_repeat(self, state): + self.playlist_repeat = state + def ctr_toggle_playlist_repeat_single(self, state): + self.playlist_repeat_single = state + + def display_control(self): + self.ctr_group = Adw.PreferencesGroup() + self.ctr_group.set_title(self.playlist.inner.title) + self.ctr_group.set_description(self.playlist.inner.description) + + # if the video isn't the first + # show a previous button + # not shown when shuffle is enbaled + if self.playlist_index > 0 and not self.playlist_shuffle: + prev_btn = Adw.ActionRow() + prev_btn.set_title(_("Previous")) + prev_btn.set_subtitle(_("Play video that comes before this one in the playlist")) + prev_btn.add_suffix(Gtk.Image.new_from_icon_name("media-skip-backward-symbolic")) + prev_btn.connect("activated", lambda _: self.ctr_previous_video()) + prev_btn.set_activatable(True) + self.ctr_group.add(prev_btn) + + # if the video isn't the last + # show a next button + # not shown when shuffle is enbaled + if self.playlist_index+1 < len(self.playlist_content) and not self.playlist_shuffle: + next_btn = Adw.ActionRow() + next_btn.set_title(_("Next")) + next_btn.set_subtitle(_("Play video that comes after this one in the playlist")) + next_btn.add_suffix(Gtk.Image.new_from_icon_name("media-skip-forward-symbolic")) + next_btn.connect("activated", lambda _: self.ctr_next_video()) + next_btn.set_activatable(True) + self.ctr_group.add(next_btn) + elif self.playlist_shuffle: + # show alternative next button + # which picks a new song + skip_btn = Adw.ActionRow() + skip_btn.set_title(_("Skip")) + skip_btn.set_subtitle(_("Skip this video and pick a new one at random")) + skip_btn.add_suffix(Gtk.Image.new_from_icon_name("media-skip-forward-symbolic")) + skip_btn.connect("activated", lambda _: self.ctr_skip_video()) + skip_btn.set_activatable(True) + self.ctr_group.add(skip_btn) + + # control the playlist shuffle mode + pref_shuffle = Preference( + "playlist-shuffle", + _("Shuffle"), + _("Chooses the next video at random"), + PreferenceType.TOGGLE, + self.playlist_shuffle, + self.playlist_shuffle) + widg_shuffle = PreferenceRow(pref_shuffle) + widg_shuffle.set_callback(self.ctr_toggle_shuffle) + self.ctr_group.add(widg_shuffle.get_widget()) + # control single video repeat mode + pref_repeat_single = Preference( + "playlist-repeat-single", + _("Repeat current video"), + _("Puts this video on loop"), + PreferenceType.TOGGLE, + self.playlist_repeat_single, + self.playlist_repeat_single) + widg_repeat_single = PreferenceRow(pref_repeat_single) + widg_repeat_single.set_callback(self.ctr_toggle_playlist_repeat_single) + self.ctr_group.add(widg_repeat_single.get_widget()) + + # control playlist repeat mode + # cannot be changed if shuffle mode is enabled + # because the playlist never ends + if not self.playlist_shuffle: + pref_repeat = Preference( + "playlist-repeat", + _("Repeat playlist"), + _("Start playling the playlist from the beginning after reaching the end"), + PreferenceType.TOGGLE, + self.playlist_repeat, + self.playlist_repeat) + widg_repeat = PreferenceRow(pref_repeat) + widg_repeat.set_callback(self.ctr_toggle_playlist_repeat) + self.ctr_group.add(widg_repeat.get_widget()) + + # expander with playlist content + expander = Adw.ExpanderRow() + expander.set_title(_("Playlist Content")) + expander.set_subtitle(_("Click on videos to continue playing the playlist from there")) + for i in range(len(self.playlist_content)): + item = self.playlist_content[i] + # onClick: starts playling the playlist from + row = AdaptiveFeedItem(item, onClick=pass_me(lambda _, i: self.prepare(i), i)) + expander.add_row(row) + self.ctr_group.add(expander) + + self.about.add(self.ctr_group) diff --git a/melon/servers/invidious/__init__.py b/melon/servers/invidious/__init__.py index 640f1cc..79748f6 100644 --- a/melon/servers/invidious/__init__.py +++ b/melon/servers/invidious/__init__.py @@ -422,7 +422,11 @@ class Invidious(Server): def on_js_bool(self, obj, async_res, webview, on_bool): if on_bool is None: return - val = webview.call_async_javascript_function_finish(async_res) + try: + val = webview.call_async_javascript_function_finish(async_res) + except Exception as e: + on_bool(False) + return if val.is_boolean(): on_bool(val.to_boolean()) else: @@ -430,7 +434,11 @@ class Invidious(Server): def on_js_double(self, obj, async_res, webview, on_double): if on_double is None: return - val = webview.call_async_javascript_function_finish(async_res) + try: + val = webview.call_async_javascript_function_finish(async_res) + except Exception as e: + on_double(None) + return if val.is_number(): on_double(val.to_double()) else: diff --git a/melon/widgets/feeditem.py b/melon/widgets/feeditem.py index 819f126..589bad3 100644 --- a/melon/widgets/feeditem.py +++ b/melon/widgets/feeditem.py @@ -15,11 +15,12 @@ class AdaptiveFeedItem(Adw.ActionRow): """ FeedItem with automatic adjustment to resource type """ - def __init__(self, resource:Resource, show_preview=True, clickable=True, *args, **kwargs): + def __init__(self, resource:Resource, show_preview=True, clickable=True, onClick=None, *args, **kwargs): super().__init__(*args, **kwargs) self.res = resource self.show_preview = show_preview self.clickable = clickable + self.onClick = onClick self.update() def set_resource(self, resource: Resource): self.res = resource @@ -31,14 +32,16 @@ class AdaptiveFeedItem(Adw.ActionRow): show_preview = self.show_preview clickable = self.clickable self.set_activatable(clickable) - self.set_action_target_value( - GLib.Variant("as", [resource.server, resource.id])) + if self.onClick is None: + self.set_action_target_value( + GLib.Variant("as", [resource.server, resource.id])) thumb_url = None if isinstance(resource, Video): thumb_url = resource.thumbnail self.set_title(unidecode(resource.title).replace("&","&")) self.set_subtitle(unidecode(resource.channel[0]).replace("&","&")) - self.set_action_name("win.player") + if self.onClick is None: + self.set_action_name("win.player") elif isinstance(resource, Playlist): thumb_url = resource.thumbnail self.set_title(unidecode(resource.title).replace("&","&")) @@ -49,7 +52,8 @@ class AdaptiveFeedItem(Adw.ActionRow): # because it could possibly cut the unicode in half? I think? sub = unidecode(resource.description)[:80] + pad self.set_subtitle(sub.replace("&","&")) - self.set_action_name("win.browse_playlist") + if self.onClick is None: + self.set_action_name("win.browse_playlist") elif isinstance(resource, Channel): thumb_url = resource.avatar self.set_title(unidecode(resource.name).replace("&","&")) @@ -60,7 +64,12 @@ class AdaptiveFeedItem(Adw.ActionRow): # because it could possibly cut the unicode in half? I think? sub = unidecode(resource.bio)[:80] + pad self.set_subtitle(sub.replace("&","&")) - self.set_action_name("win.browse_channel") + if self.onClick is None: + self.set_action_name("win.browse_channel") + + if not self.onClick is None: + self.connect("activated", self.onClick) + self.preview = Adw.Avatar() self.preview.set_size(48) self.preview.set_show_initials(True) diff --git a/po/POTFILES.in b/po/POTFILES.in index 35566fe..7cf1af5 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -5,6 +5,7 @@ melon/browse/channel.py melon/browse/playlist.py melon/browse/search.py melon/browse/server.py +melon/home/__init__.py melon/home/history.py melon/home/new.py melon/home/playlists.py @@ -12,6 +13,7 @@ melon/home/subs.py melon/import_providers/newpipe.py melon/importer.py melon/player/__init__.py +melon/player/playlist.py melon/playlist/__init__.py melon/playlist/create.py melon/playlist/pick.py diff --git a/po/de.po b/po/de.po index 114e0b2..8b5c5a4 100644 --- a/po/de.po +++ b/po/de.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Melon 0.1.2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-03-09 08:25+0100\n" +"POT-Creation-Date: 2024-03-13 07:44+0100\n" "PO-Revision-Date: 2024-03-01 11:23+0000\n" "Last-Translator: Anonymous \n" "Language-Team: German \n" "Language-Team: Persian \n" "Language-Team: LANGUAGE \n" @@ -74,9 +74,11 @@ msgstr "" #: ../melon/browse/__init__.py:18 ../melon/browse/search.py:58 #: ../melon/browse/server.py:101 ../melon/browse/server.py:127 -#: ../melon/home/history.py:23 ../melon/home/new.py:28 +#: ../melon/home/history.py:23 ../melon/home/new.py:29 #: ../melon/home/playlists.py:21 ../melon/home/subs.py:20 -#: ../melon/importer.py:61 ../melon/playlist/__init__.py:44 +#: ../melon/importer.py:61 ../melon/player/__init__.py:91 +#: ../melon/player/playlist.py:160 ../melon/player/playlist.py:167 +#: ../melon/playlist/__init__.py:52 msgid "*crickets chirping*" msgstr "" @@ -125,15 +127,19 @@ msgstr "" msgid "Channel" msgstr "" -#: ../melon/browse/playlist.py:47 ../melon/player/__init__.py:43 +#: ../melon/browse/playlist.py:38 ../melon/playlist/__init__.py:43 +msgid "Start playing" +msgstr "" + +#: ../melon/browse/playlist.py:57 ../melon/player/__init__.py:50 msgid "Bookmark" msgstr "" -#: ../melon/browse/playlist.py:48 +#: ../melon/browse/playlist.py:58 msgid "Add Playlist to your local playlist collection" msgstr "" -#: ../melon/browse/playlist.py:102 +#: ../melon/browse/playlist.py:112 msgid "Playlist" msgstr "" @@ -190,14 +196,31 @@ msgstr "" msgid "This feed is empty" msgstr "" -#: ../melon/home/history.py:24 -msgid "You haven't watched any videos yet" +#: ../melon/home/__init__.py:18 +msgid "Home" msgstr "" -#: ../melon/home/history.py:26 ../melon/home/new.py:35 ../melon/home/subs.py:23 +#: ../melon/home/__init__.py:39 ../melon/home/history.py:26 +#: ../melon/home/new.py:36 ../melon/home/subs.py:23 msgid "Browse Servers" msgstr "" +#: ../melon/home/__init__.py:46 +msgid "Preferences" +msgstr "" + +#: ../melon/home/__init__.py:47 +msgid "Import Data" +msgstr "" + +#: ../melon/home/__init__.py:48 +msgid "About Melon" +msgstr "" + +#: ../melon/home/history.py:24 +msgid "You haven't watched any videos yet" +msgstr "" + #: ../melon/home/history.py:36 ../melon/home/history.py:117 msgid "History" msgstr "" @@ -214,31 +237,39 @@ msgstr "" msgid "Load older videos" msgstr "" -#: ../melon/home/new.py:22 +#: ../melon/home/new.py:23 msgid "Refresh" msgstr "" -#: ../melon/home/new.py:31 +#: ../melon/home/new.py:32 msgid "Subscribe to a channel first, to view new uploads" msgstr "" -#: ../melon/home/new.py:33 +#: ../melon/home/new.py:34 msgid "The channels you are subscribed to haven't uploaded anything yet" msgstr "" -#: ../melon/home/new.py:54 +#: ../melon/home/new.py:55 #, python-brace-format msgid "(Last refresh: {last_refresh})" msgstr "" -#: ../melon/home/new.py:56 ../melon/home/new.py:113 +#: ../melon/home/new.py:57 ../melon/home/new.py:144 msgid "What's new" msgstr "" -#: ../melon/home/new.py:57 +#: ../melon/home/new.py:58 msgid "These are the latest videos of channels you follow" msgstr "" +#: ../melon/home/new.py:135 +msgid "Pick up where you left off" +msgstr "" + +#: ../melon/home/new.py:136 +msgid "Watch" +msgstr "" + #: ../melon/home/playlists.py:22 msgid "You don't have any playlists yet" msgstr "" @@ -297,110 +328,175 @@ msgstr "" msgid "There are no available importer methods" msgstr "" -#: ../melon/player/__init__.py:33 +#: ../melon/player/__init__.py:40 msgid "Description" msgstr "" -#: ../melon/player/__init__.py:44 +#: ../melon/player/__init__.py:51 msgid "Add this video to a playlist" msgstr "" -#: ../melon/player/__init__.py:62 +#: ../melon/player/__init__.py:71 msgid "Quality" msgstr "" -#: ../melon/player/__init__.py:63 +#: ../melon/player/__init__.py:72 msgid "Video quality" msgstr "" -#: ../melon/player/__init__.py:78 +#: ../melon/player/__init__.py:93 msgid "Video could not be loaded" msgstr "" -#: ../melon/player/__init__.py:110 -msgid "Player" +#: ../melon/player/__init__.py:124 ../melon/player/__init__.py:150 +#: ../melon/player/playlist.py:51 +msgid "Loading..." +msgstr "" + +#: ../melon/player/playlist.py:161 +msgid "This playlist is empty" +msgstr "" + +#: ../melon/player/playlist.py:168 +msgid "There was an error loading the playlist" +msgstr "" + +#: ../melon/player/playlist.py:213 +msgid "Previous" +msgstr "" + +#: ../melon/player/playlist.py:214 +msgid "Play video that comes before this one in the playlist" +msgstr "" + +#: ../melon/player/playlist.py:225 +msgid "Next" +msgstr "" + +#: ../melon/player/playlist.py:226 +msgid "Play video that comes after this one in the playlist" +msgstr "" + +#: ../melon/player/playlist.py:235 +msgid "Skip" +msgstr "" + +#: ../melon/player/playlist.py:236 +msgid "Skip this video and pick a new one at random" +msgstr "" + +#: ../melon/player/playlist.py:245 +msgid "Shuffle" +msgstr "" + +#: ../melon/player/playlist.py:246 +msgid "Chooses the next video at random" +msgstr "" + +#: ../melon/player/playlist.py:256 +msgid "Repeat current video" +msgstr "" + +#: ../melon/player/playlist.py:257 +msgid "Puts this video on loop" +msgstr "" + +#: ../melon/player/playlist.py:271 +msgid "Repeat playlist" +msgstr "" + +#: ../melon/player/playlist.py:272 +msgid "Start playling the playlist from the beginning after reaching the end" +msgstr "" + +#: ../melon/player/playlist.py:282 +msgid "Playlist Content" +msgstr "" + +#: ../melon/player/playlist.py:283 +msgid "Click on videos to continue playing the playlist from there" msgstr "" #: ../melon/playlist/__init__.py:39 msgid "Edit" msgstr "" -#: ../melon/playlist/__init__.py:45 +#: ../melon/playlist/__init__.py:53 msgid "You haven't added any videos to this playlist yet" msgstr "" -#: ../melon/playlist/__init__.py:47 +#: ../melon/playlist/__init__.py:55 msgid "Start watching" msgstr "" -#: ../melon/playlist/__init__.py:77 +#: ../melon/playlist/__init__.py:85 msgid "Edit Playlist" msgstr "" -#: ../melon/playlist/__init__.py:83 +#: ../melon/playlist/__init__.py:91 msgid "Playlist details" msgstr "" -#: ../melon/playlist/__init__.py:84 +#: ../melon/playlist/__init__.py:92 msgid "Change playlist information" msgstr "" -#: ../melon/playlist/__init__.py:86 ../melon/playlist/create.py:37 +#: ../melon/playlist/__init__.py:94 ../melon/playlist/create.py:37 msgid "Playlist name" msgstr "" -#: ../melon/playlist/__init__.py:89 ../melon/playlist/create.py:40 +#: ../melon/playlist/__init__.py:97 ../melon/playlist/create.py:40 msgid "Playlist description" msgstr "" -#: ../melon/playlist/__init__.py:94 +#: ../melon/playlist/__init__.py:102 msgid "Save details" msgstr "" -#: ../melon/playlist/__init__.py:95 +#: ../melon/playlist/__init__.py:103 msgid "Change playlist title and description. (Closes the dialog)" msgstr "" -#: ../melon/playlist/__init__.py:105 ../melon/playlist/__init__.py:147 +#: ../melon/playlist/__init__.py:113 ../melon/playlist/__init__.py:155 msgid "Delete Playlist" msgstr "" -#: ../melon/playlist/__init__.py:106 +#: ../melon/playlist/__init__.py:114 msgid "Delete this playlist and it's content. This can NOT be undone." msgstr "" -#: ../melon/playlist/__init__.py:117 +#: ../melon/playlist/__init__.py:125 msgid "Close" msgstr "" -#: ../melon/playlist/__init__.py:117 +#: ../melon/playlist/__init__.py:125 msgid "Close without changing anything" msgstr "" -#: ../melon/playlist/__init__.py:150 +#: ../melon/playlist/__init__.py:158 msgid "Do you really want to delete this playlist?" msgstr "" -#: ../melon/playlist/__init__.py:151 +#: ../melon/playlist/__init__.py:159 msgid "This cannot be undone" msgstr "" -#: ../melon/playlist/__init__.py:155 ../melon/playlist/create.py:47 -#: ../melon/playlist/pick.py:70 ../melon/widgets/preferencerow.py:174 -#: ../melon/widgets/preferencerow.py:217 +#: ../melon/playlist/__init__.py:163 ../melon/playlist/create.py:47 +#: ../melon/playlist/pick.py:70 ../melon/widgets/preferencerow.py:175 +#: ../melon/widgets/preferencerow.py:218 msgid "Cancel" msgstr "" -#: ../melon/playlist/__init__.py:155 +#: ../melon/playlist/__init__.py:163 msgid "Do not delete the playlist" msgstr "" -#: ../melon/playlist/__init__.py:156 ../melon/widgets/preferencerow.py:206 -#: ../melon/widgets/preferencerow.py:218 +#: ../melon/playlist/__init__.py:164 ../melon/widgets/preferencerow.py:207 +#: ../melon/widgets/preferencerow.py:219 msgid "Delete" msgstr "" -#: ../melon/playlist/__init__.py:156 +#: ../melon/playlist/__init__.py:164 msgid "Delete this playlist" msgstr "" @@ -428,7 +524,7 @@ msgstr "" msgid "Do not create playlist" msgstr "" -#: ../melon/playlist/create.py:48 ../melon/widgets/preferencerow.py:175 +#: ../melon/playlist/create.py:48 ../melon/widgets/preferencerow.py:176 msgid "Create" msgstr "" @@ -462,24 +558,24 @@ msgstr "" msgid "Add to Playlist" msgstr "" -#: ../melon/servers/invidious/__init__.py:27 +#: ../melon/servers/invidious/__init__.py:32 msgid "Open source alternative front-end to YouTube" msgstr "" -#: ../melon/servers/invidious/__init__.py:32 +#: ../melon/servers/invidious/__init__.py:37 msgid "Instance" msgstr "" -#: ../melon/servers/invidious/__init__.py:33 +#: ../melon/servers/invidious/__init__.py:38 msgid "" "See https://docs.invidious.io/instances/ for a list of available instances" msgstr "" -#: ../melon/servers/invidious/__init__.py:44 +#: ../melon/servers/invidious/__init__.py:49 msgid "Trending" msgstr "" -#: ../melon/servers/invidious/__init__.py:45 +#: ../melon/servers/invidious/__init__.py:50 msgid "Popular" msgstr "" @@ -619,54 +715,54 @@ msgid "" "Disabled servers won't show up in the browser or on the local/home screen" msgstr "" -#: ../melon/widgets/preferencerow.py:115 +#: ../melon/widgets/preferencerow.py:116 msgid "Add" msgstr "" -#: ../melon/widgets/preferencerow.py:129 +#: ../melon/widgets/preferencerow.py:130 msgid "Move up" msgstr "" -#: ../melon/widgets/preferencerow.py:140 +#: ../melon/widgets/preferencerow.py:141 msgid "Move down" msgstr "" -#: ../melon/widgets/preferencerow.py:149 +#: ../melon/widgets/preferencerow.py:150 msgid "Remove from list" msgstr "" -#: ../melon/widgets/preferencerow.py:162 +#: ../melon/widgets/preferencerow.py:163 msgid "Add Item" msgstr "" -#: ../melon/widgets/preferencerow.py:165 +#: ../melon/widgets/preferencerow.py:166 msgid "Create a new list entry" msgstr "" -#: ../melon/widgets/preferencerow.py:166 +#: ../melon/widgets/preferencerow.py:167 msgid "Enter the new value here" msgstr "" -#: ../melon/widgets/preferencerow.py:169 +#: ../melon/widgets/preferencerow.py:170 msgid "Value" msgstr "" -#: ../melon/widgets/preferencerow.py:209 +#: ../melon/widgets/preferencerow.py:210 msgid "Do you really want to delete this item?" msgstr "" -#: ../melon/widgets/preferencerow.py:210 +#: ../melon/widgets/preferencerow.py:211 msgid "You won't be able to restore it afterwards" msgstr "" -#: ../melon/widgets/preferencerow.py:217 +#: ../melon/widgets/preferencerow.py:218 msgid "Do not remove item" msgstr "" -#: ../melon/widgets/preferencerow.py:218 +#: ../melon/widgets/preferencerow.py:219 msgid "Remove item from list" msgstr "" -#: ../melon/window.py:76 +#: ../melon/window.py:84 msgid "Stream videos on the go" msgstr "" -- 2.38.5