~comcloudway/melon

d88e6e476b70ee26b1a14a7f0a4b3a1349f1d60d — Jakob Meier 6 months ago d0273c5
use gettext to translate the project

doesn't include any other languages,
but this commit allows the application to support multiple languages
M README.md => README.md +8 -0
@@ 58,9 58,17 @@ Of course, you are welcome to send issues and pull requests via email as well:
- `py3-requests`
- `py3-unidecode`
- `gtk4.0`
- `gettext`
- `libadwaita`
- `webkit2gtk-6.0`

## Translations
Melon supports multiple languages.
If you want to help translate Melon into your language,
head over to [Weblate](https://translate.codeberg.org/engage/melon/)

[![Translation status](https://translate.codeberg.org/widget/melon/multi-auto.svg)](https://translate.codeberg.org/engage/melon/)

## ⚠️ Disclaimer

- The developers of this application does not have any affiliation with the content providers available.

M data/meson.build => data/meson.build +20 -1
@@ 5,6 5,7 @@ install_data (
)

gnome = import('gnome')
i18n = import('i18n')

#
# .desktop file


@@ 19,12 20,30 @@ desktop_conf.set('projectname', meson.project_name())

desktop_file = configure_file(
    input: base_id + '.desktop.in',
    output: app_id + '.desktop',
    output: app_id + '.desktop.i18n.in',
    configuration: desktop_conf,
)

# Merges translations
i18n.merge_file(
    input: desktop_file,
    output: app_id + '.desktop',
    po_dir: '../po',
    type: 'desktop',
    install: true,
    install_dir: join_paths(datadir, 'applications')
)

# Validating the .desktop file
desktop_file_validate = find_program('desktop-file-validate', required:false)
if desktop_file_validate.found()
test (
    'Validate desktop file',
    desktop_file_validate,
    args: join_paths(meson.current_build_dir (), app_id + '.desktop')
)
endif

#
# Dependencies
#

M flatpak/icu.ccw.Melon.yml => flatpak/icu.ccw.Melon.yml +5 -5
@@ 32,8 32,8 @@ modules:
    buildsystem: meson
    sources:
      # uncomment to build local version instead
      # - type: dir
      #   path: ".."
      - type: dir
        path: ".."
      #
      # uncomment to build a specific version
      # - type: git


@@ 41,6 41,6 @@ modules:
      #   tag: "0.1.2"
      #
      # build latest main branch
      - type: git
        url: https://codeberg.org/comcloudway/melon
        branch: main
      # - type: git
      #   url: https://codeberg.org/comcloudway/melon
      #   branch: main

M melon/browse/__init__.py => melon/browse/__init__.py +8 -7
@@ 3,6 3,7 @@ import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw, Gio, Gdk, GLib
from gettext import gettext as _

from melon.widgets.iconbutton import IconButton
from melon.servers.utils import get_allowed_servers_list


@@ 14,10 15,10 @@ class BrowseScreen(Adw.NavigationPage):
        if len(servers) == 0:
            # if no servers are allowed, show the nothing here box
            status = Adw.StatusPage()
            status.set_title("*crickets chirping*")
            status.set_description("There are no available servers")
            status.set_title(_("*crickets chirping*"))
            status.set_description(_("There are no available servers"))
            status.set_icon_name("weather-few-clouds-night-symbolic")
            icon_button = IconButton("Enable servers in the settings menu", "list-add-symbolic")
            icon_button = IconButton(_("Enable servers in the settings menu"), "list-add-symbolic")
            icon_button.set_action_name("win.prefs")
            box = Gtk.CenterBox()
            box.set_center_widget(icon_button)


@@ 25,8 26,8 @@ class BrowseScreen(Adw.NavigationPage):
            self.scrollview.set_child(status)
        else:
            results = Adw.PreferencesGroup()
            results.set_title("Available Servers")
            results.set_description("You can enable/disable and filter servers in the settings menu")
            results.set_title(_("Available Servers"))
            results.set_description(_("You can enable/disable and filter servers in the settings menu"))
            for server in servers:
                row = Adw.ActionRow()
                row.set_title(server["name"])


@@ 44,7 45,7 @@ class BrowseScreen(Adw.NavigationPage):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.set_title("Servers")
        self.set_title(_("Servers"))

        self.header_bar = Adw.HeaderBar()



@@ 52,7 53,7 @@ class BrowseScreen(Adw.NavigationPage):
        self.toolbar_view.add_top_bar(self.header_bar)
        # open global search screen
        search_button = IconButton("", "system-search-symbolic")
        search_button.set_tooltip_text("Global Search")
        search_button.set_tooltip_text(_("Global Search"))
        search_button.set_action_name("win.global_search")
        self.header_bar.pack_end(search_button)


M melon/browse/channel.py => melon/browse/channel.py +6 -5
@@ 3,6 3,8 @@ import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw, Gio, Gdk, GLib
from unidecode import unidecode
from gettext import gettext as _

from melon.servers.utils import get_server_instance, get_servers_list
from melon.servers.utils import pixbuf_from_url


@@ 12,7 14,6 @@ from melon.widgets.preferencerow import PreferenceRow, PreferenceType, Preferenc
from melon.widgets.iconbutton import IconButton
from melon.models import get_app_settings
from melon.models import is_subscribed_to_channel, ensure_subscribed_to_channel, ensure_unsubscribed_from_channel
from unidecode import unidecode

class BrowseChannelScreen(Adw.NavigationPage):
    def fetch_page(self, page=1):


@@ 95,8 96,8 @@ class BrowseChannelScreen(Adw.NavigationPage):
        # add (un)subscribe button
        sub_pref = Preference(
            "subscribe",
            "Subscribe to channel",
            "Add latest uploads to home feed",
            _("Subscribe to channel"),
            _("Add latest uploads to home feed"),
            PreferenceType.TOGGLE,
            False,
            is_subscribed_to_channel(self.channel.server, self.channel_id)


@@ 123,8 124,8 @@ class BrowseChannelScreen(Adw.NavigationPage):
                    break
            pref = Preference(
                "channel-feed",
                "Channel feed",
                "This channel provides multiple feeds, choose one to view",
                _("Channel feed"),
                _("This channel provides multiple feeds, choose which one to view"),
                PreferenceType.DROPDOWN,
                [ feed.name for feed in feeds ],
                default_name

M melon/browse/playlist.py => melon/browse/playlist.py +3 -2
@@ 4,6 4,7 @@ gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw, Gio, Gdk, GLib
from unidecode import unidecode
from gettext import gettext as _

from melon.servers.utils import get_server_instance, get_servers_list
from melon.servers.utils import pixbuf_from_url


@@ 57,8 58,8 @@ class BrowsePlaylistScreen(Adw.NavigationPage):

        bookmark_pref = Preference(
            "bookmark-playlist",
            "Bookmark",
            "Add Playlist to your local playlist collection",
            _("Bookmark"),
            _("Add Playlist to your local playlist collection"),
            PreferenceType.TOGGLE,
            False,
            has_bookmarked_external_playlist(server_id, playlist_id))

M melon/browse/search.py => melon/browse/search.py +12 -13
@@ 3,6 3,8 @@ import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw, Gio, Gdk, GLib
from gettext import gettext as _
from gettext import ngettext

from melon.widgets.iconbutton import IconButton
from melon.widgets.feeditem import AdaptiveFeedItem


@@ 39,12 41,9 @@ class GlobalSearchScreen(Adw.NavigationPage):
            results = instance.search(self.text, self.search_mode)
            box = Adw.ExpanderRow()
            box.set_title(instance.name)
            box.set_subtitle("No results")
            box.set_subtitle(_("No results"))
            count = len(results)
            if count == 1:
                box.set_subtitle("1 result")
            elif count > 1:
                box.set_subtitle(f"{count} results")
            box.set_subtitle(ngettext("{count} result","{count} results", count).format(count = count))
            self.results.add(box)
            for entry in results:
                row = AdaptiveFeedItem(entry, app_conf["show_images_in_browse"])


@@ 56,10 55,10 @@ class GlobalSearchScreen(Adw.NavigationPage):
        if len(servers) == 0:
            # if no servers are allowed, show the nothing here box
            status = Adw.StatusPage()
            status.set_title("*crickets chirping*")
            status.set_description("There are no available servers, a search would yield no results")
            status.set_title(_("*crickets chirping*"))
            status.set_description(_("There are no available servers, a search would yield no results"))
            status.set_icon_name("weather-few-clouds-night-symbolic")
            icon_button = IconButton("Enable servers in the settings menu", "list-add-symbolic")
            icon_button = IconButton(_("Enable servers in the settings menu"), "list-add-symbolic")
            icon_button.set_action_name("win.prefs")
            box = Gtk.CenterBox()
            box.set_center_widget(icon_button)


@@ 81,15 80,15 @@ class GlobalSearchScreen(Adw.NavigationPage):
            filter_popover = Gtk.Popover()
            filter_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
            # the filter options
            filter_any = FilterButton("Any", SearchMode.ANY, self.on_filter, self.search_mode)
            filter_any = FilterButton(_("Any"), SearchMode.ANY, self.on_filter, self.search_mode)
            filter_box.append(filter_any)
            filter_channels = FilterButton("Channels", SearchMode.CHANNELS, self.on_filter, self.search_mode)
            filter_channels = FilterButton(_("Channels"), SearchMode.CHANNELS, self.on_filter, self.search_mode)
            filter_channels.set_group(filter_any)
            filter_box.append(filter_channels)
            filter_playlists = FilterButton("Playlists", SearchMode.PLAYLISTS, self.on_filter, self.search_mode)
            filter_playlists = FilterButton(_("Playlists"), SearchMode.PLAYLISTS, self.on_filter, self.search_mode)
            filter_playlists.set_group(filter_any)
            filter_box.append(filter_playlists)
            filter_videos = FilterButton("Videos", SearchMode.VIDEOS, self.on_filter, self.search_mode)
            filter_videos = FilterButton(_("Videos"), SearchMode.VIDEOS, self.on_filter, self.search_mode)
            filter_videos.set_group(filter_any)
            filter_box.append(filter_videos)



@@ 104,7 103,7 @@ class GlobalSearchScreen(Adw.NavigationPage):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.set_title("Global Search")
        self.set_title(_("Global Search"))

        self.header_bar = Adw.HeaderBar()


M melon/browse/server.py => melon/browse/server.py +11 -10
@@ 4,6 4,7 @@ gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw, Gio, Gdk, GLib
import threading
from gettext import gettext as _

from melon.servers.utils import get_servers_list, get_server_instance
from melon.servers import SearchMode


@@ 19,7 20,7 @@ class Search(ViewStackPage):
    search_mode = SearchMode.ANY
    results = None
    def __init__(self, plugin):
        super().__init__("search", "Search", "x-office-address-book-symbolic")
        super().__init__("search", _("Search"), "x-office-address-book-symbolic")
        self.instance = plugin
        self.widget = Adw.Clamp()
        self.scroll = Gtk.ScrolledWindow()


@@ 39,15 40,15 @@ class Search(ViewStackPage):
        filter_popover = Gtk.Popover()
        filter_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        # the filter options
        filter_any = FilterButton("Any", SearchMode.ANY, self.on_filter, self.search_mode)
        filter_any = FilterButton(_("Any"), SearchMode.ANY, self.on_filter, self.search_mode)
        filter_box.append(filter_any)
        filter_channels = FilterButton("Channels", SearchMode.CHANNELS, self.on_filter, self.search_mode)
        filter_channels = FilterButton(_("Channels"), SearchMode.CHANNELS, self.on_filter, self.search_mode)
        filter_channels.set_group(filter_any)
        filter_box.append(filter_channels)
        filter_playlists = FilterButton("Playlists", SearchMode.PLAYLISTS, self.on_filter, self.search_mode)
        filter_playlists = FilterButton(_("Playlists"), SearchMode.PLAYLISTS, self.on_filter, self.search_mode)
        filter_playlists.set_group(filter_any)
        filter_box.append(filter_playlists)
        filter_videos = FilterButton("Videos", SearchMode.VIDEOS, self.on_filter, self.search_mode)
        filter_videos = FilterButton(_("Videos"), SearchMode.VIDEOS, self.on_filter, self.search_mode)
        filter_videos.set_group(filter_any)
        filter_box.append(filter_videos)



@@ 97,10 98,10 @@ class Search(ViewStackPage):
        if not self.results is None:
            self.inner.remove(self.results)
        self.results = Adw.StatusPage()
        self.results.set_title("*crickets chirping*")
        desc = "Try searching for a term"
        self.results.set_title(_("*crickets chirping*"))
        desc = _("Try searching for a term")
        if self.query != "":
            desc = "Try using a different query"
            desc = _("Try using a different query")
        self.query = ""
        self.results.set_description(desc)
        self.results.set_icon_name("weather-few-clouds-night-symbolic")


@@ 121,8 122,8 @@ class Feed(ViewStackPage):
        if len(results) == 0:
            # show empty feed page
            status = Adw.StatusPage()
            status.set_title("*crickets chirping*")
            status.set_description("This feed is empty")
            status.set_title(_("*crickets chirping*"))
            status.set_description(_("This feed is empty"))
            status.set_icon_name("weather-few-clouds-night-symbolic")
            self.inner.set_child(status)
        else:

M melon/home/history.py => melon/home/history.py +7 -6
@@ 4,6 4,7 @@ gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw
import threading
from gettext import gettext as _

from melon.widgets.viewstackpage import ViewStackPage
from melon.widgets.iconbutton import IconButton


@@ 23,10 24,10 @@ class History(ViewStackPage):
        self.data = list(group_by_date(hist).items())
        if len(hist) == 0:
            status = Adw.StatusPage()
            status.set_title("*crickets chirping*")
            status.set_description("You haven't watched any videos yet")
            status.set_title(_("*crickets chirping*"))
            status.set_description(_("You haven't watched any videos yet"))
            status.set_icon_name("weather-few-clouds-night-symbolic")
            icon_button = IconButton("Browse Servers", "list-add-symbolic")
            icon_button = IconButton(_("Browse Servers"), "list-add-symbolic")
            icon_button.set_action_name("win.browse")
            box = Gtk.CenterBox()
            box.set_center_widget(icon_button)


@@ 35,8 36,8 @@ class History(ViewStackPage):
        else:
            self.results = Adw.PreferencesPage()
            title = Adw.PreferencesGroup()
            title.set_title("History")
            title.set_description("These are the videos you opened in the past")
            title.set_title(_("History"))
            title.set_description(_("These are the videos you opened in the past"))
            self.results.add(title)
            self.inner.set_child(self.results)
            self.load_page(0)


@@ 78,7 79,7 @@ class History(ViewStackPage):
        self.thread.start()

    def __init__(self):
        super().__init__("history", "History", "media-playlist-repeat-symbolic")
        super().__init__("history", _("History"), "media-playlist-repeat-symbolic")
        self.widget = Adw.Clamp()
        self.inner = Gtk.ScrolledWindow()
        self.widget.set_child(self.inner)

M melon/home/new.py => melon/home/new.py +10 -9
@@ 5,6 5,7 @@ gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw, GLib, Gio, GObject
import threading
from datetime import datetime
from gettext import gettext as _

from melon.widgets.viewstackpage import ViewStackPage
from melon.widgets.iconbutton import IconButton


@@ 17,20 18,20 @@ from melon.models import get_cached_feed, clear_cached_feed, update_cached_feed,
class NewFeed(ViewStackPage):
    def update(self, news):
        # both subscreens need the refresh button
        refresh_btn = IconButton("Refresh", "view-refresh-symbolic")
        refresh_btn = IconButton(_("Refresh"), "view-refresh-symbolic")
        refresh_btn.connect("clicked", lambda a: self.reload())

        if not news:
            # empty home feed
            status = Adw.StatusPage()
            status.set_title("*crickets chirping*")
            status.set_title(_("*crickets chirping*"))
            subs = get_subscribed_channels()
            if len(subs) == 0:
                status.set_description("Subscribe to a channel first, to view new uploads")
                status.set_description(_("Subscribe to a channel first, to view new uploads"))
            else:
                status.set_description("The channels you are subscribed to haven't uploaded anything yet")
                status.set_description(_("The channels you are subscribed to haven't uploaded anything yet"))
            status.set_icon_name("weather-few-clouds-night-symbolic")
            icon_button = IconButton("Browse Servers", "list-add-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()


@@ 49,10 50,10 @@ class NewFeed(ViewStackPage):
                last_refresh = "Never"
            else:
                last_refresh = datetime.fromtimestamp(last_refresh).strftime("%c")
            refresh_info = f" (Last refresh: {last_refresh})"
            refresh_info = _("(Last refresh: {last_refresh})").format(last_refresh = last_refresh)
            title = Adw.PreferencesGroup()
            title.set_title("What's new" + refresh_info)
            title.set_description("These are the latest videos of channels you follow")
            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)
            results.add(title)
            self.inner.set_child(results)


@@ 104,7 105,7 @@ class NewFeed(ViewStackPage):
        self.do_update()

    def __init__(self):
        super().__init__("feed-new", "What's new", "user-home-symbolic")
        super().__init__("feed-new", _("What's new"), "user-home-symbolic")
        self.widget = Adw.Clamp()
        self.inner = Gtk.ScrolledWindow()
        self.widget.set_child(self.inner)

M melon/home/playlists.py => melon/home/playlists.py +8 -7
@@ 4,6 4,7 @@ gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw
import threading
from gettext import gettext as _

from melon.widgets.viewstackpage import ViewStackPage
from melon.widgets.iconbutton import IconButton


@@ 21,10 22,10 @@ class Playlists(ViewStackPage):
        playlists.sort(key=lambda x:x.inner.title)
        if len(playlists) == 0:
             status = Adw.StatusPage()
             status.set_title("*crickets chirping*")
             status.set_description("You don't have any playlists yet")
             status.set_title(_("*crickets chirping*"))
             status.set_description(_("You don't have any playlists yet"))
             status.set_icon_name("weather-few-clouds-night-symbolic")
             icon_button = IconButton("Create a new playlist", "list-add-symbolic")
             icon_button = IconButton(_("Create a new playlist"), "list-add-symbolic")
             icon_button.set_action_name("win.new_playlist")
             box = Gtk.CenterBox()
             box.set_center_widget(icon_button)


@@ 32,9 33,9 @@ class Playlists(ViewStackPage):
             self.inner.set_child(status)
        else:
            results = Adw.PreferencesGroup()
            results.set_title("Playlists")
            results.set_description("Here are playlists you've bookmarked or created yourself")
            icon_button = IconButton("New", "list-add-symbolic", tooltip="Create a new playlist")
            results.set_title(_("Playlists"))
            results.set_description(_("Here are playlists you've bookmarked or created yourself"))
            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)


@@ 65,7 66,7 @@ class Playlists(ViewStackPage):
        self.thread.start()

    def __init__(self):
        super().__init__("playlists", "Playlists", "user-bookmarks-symbolic")
        super().__init__("playlists", _("Playlists"), "user-bookmarks-symbolic")
        self.widget = Adw.Clamp()
        self.inner = Gtk.ScrolledWindow()
        self.widget.set_child(self.inner)

M melon/home/subs.py => melon/home/subs.py +7 -6
@@ 4,6 4,7 @@ gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw
import threading
from gettext import gettext as _

from melon.widgets.viewstackpage import ViewStackPage
from melon.widgets.iconbutton import IconButton


@@ 18,10 19,10 @@ class Subscriptions(ViewStackPage):
        subs = filter_resources(get_subscribed_channels(), app_settings)
        if len(subs) == 0:
            status = Adw.StatusPage()
            status.set_title("*crickets chirping*")
            status.set_description("You aren't yet subscribed to channels")
            status.set_title(_("*crickets chirping*"))
            status.set_description(_("You aren't yet subscribed to channels"))
            status.set_icon_name("weather-few-clouds-night-symbolic")
            icon_button = IconButton("Browse Servers", "list-add-symbolic")
            icon_button = IconButton(_("Browse Servers"), "list-add-symbolic")
            icon_button.set_action_name("win.browse")
            box = Gtk.CenterBox()
            box.set_center_widget(icon_button)


@@ 29,8 30,8 @@ class Subscriptions(ViewStackPage):
            self.inner.set_child(status)
        else:
            results = Adw.PreferencesGroup()
            results.set_title("Subscriptions")
            results.set_description("You are subscribed to the following channels")
            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


@@ 64,7 65,7 @@ class Subscriptions(ViewStackPage):
        self.thread.start()

    def __init__(self):
        super().__init__("subs", "Subscriptions", "x-office-address-book-symbolic")
        super().__init__("subs", _("Subscriptions"), "x-office-address-book-symbolic")
        self.widget = Adw.Clamp()
        self.inner = Gtk.ScrolledWindow()
        self.widget.set_child(self.inner)

M melon/import_providers/newpipe.py => melon/import_providers/newpipe.py +5 -4
@@ 1,6 1,7 @@
from abc import ABC
import sqlite3
import json
from gettext import gettext as _

from melon.import_providers import ImportProvider, PickerMode
from melon.servers import Channel, Playlist, Video


@@ 24,9 25,9 @@ sqlite3.register_converter('json', convert_json)
class NewpipeImporter(ImportProvider, ABC):
    id = "newpipedb"

    title = "Newpipe Database importer"
    description = "Import the .db file from inside the newpipe .zip export (as invidious content)"
    picker_title = "Select .db file"
    title = _("Newpipe Database importer")
    description = _("Import the .db file from inside the newpipe .zip export (as invidious content)")
    picker_title = _("Select .db file")

    # the server id for resources
    server_id:str


@@ 41,7 42,7 @@ class NewpipeImporter(ImportProvider, ABC):
    is_multi = False

    filters = {
        "Newpipe Database": [ "application/x-sqlite3" ]
        _("Newpipe Database"): [ "application/x-sqlite3" ]
    }

    db:sqlite3.Connection = None

M melon/importer.py => melon/importer.py +7 -6
@@ 3,6 3,7 @@ import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw, Gio
from gettext import gettext as _

from melon.import_providers.utils import ImportPicker
from melon.import_providers import ImportProvider, PickerMode


@@ 13,7 14,7 @@ from melon.models import get_app_settings
class ImporterScreen(Adw.NavigationPage):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.set_title("Import")
        self.set_title(_("Import"))

        self.header_bar = Adw.HeaderBar()



@@ 31,8 32,8 @@ class ImporterScreen(Adw.NavigationPage):
    def reset(self):
        servers = get_allowed_servers_list(get_app_settings())
        self.box = Adw.PreferencesPage()
        self.box.set_title("Import")
        self.box.set_description("The following import methods have been found")
        self.box.set_title(_("Import"))
        self.box.set_description(_("The following import methods have been found"))

        count = 0
        for server in servers:


@@ 57,10 58,10 @@ class ImporterScreen(Adw.NavigationPage):
        if count == 0:
            # if no servers are allowed, show the nothing here box
            status = Adw.StatusPage()
            status.set_title("*crickets chirping*")
            status.set_description("There are no available importers")
            status.set_title(_("*crickets chirping*"))
            status.set_description(_("There are no available importer methods"))
            status.set_icon_name("weather-few-clouds-night-symbolic")
            icon_button = IconButton("Enable servers in the settings menu", "list-add-symbolic")
            icon_button = IconButton(_("Enable servers in the settings menu"), "list-add-symbolic")
            icon_button.set_action_name("win.prefs")
            box = Gtk.CenterBox()
            box.set_center_widget(icon_button)

M melon/player/__init__.py => melon/player/__init__.py +7 -5
@@ 3,6 3,8 @@ gi.require_version("WebKit", "6.0")
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw, WebKit, GLib
from unidecode import unidecode
from gettext import gettext as _

from melon.servers.utils import get_server_instance, get_servers_list
from melon.servers import SearchMode


@@ 10,7 12,6 @@ from melon.widgets.feeditem import AdaptiveFeedItem
from melon.widgets.preferencerow import PreferenceRow, PreferenceType, Preference
from melon.widgets.iconbutton import IconButton
from melon.models import get_app_settings, notify, add_to_history
from unidecode import unidecode

class PlayerScreen(Adw.NavigationPage):
    def on_open_in_browser(self, arg):


@@ 61,7 62,8 @@ class PlayerScreen(Adw.NavigationPage):
            # stream selector
            pref = Preference(
                "quality",
                "Quality", "Video quality",
                _("Quality"),
                _("Video quality"),
                PreferenceType.DROPDOWN,
                [ unidecode(stream.quality) for stream in self.streams ],
                default_stream)


@@ 71,7 73,7 @@ class PlayerScreen(Adw.NavigationPage):

        # expandable description field
        desc_field = Adw.ExpanderRow()
        desc_field.set_title("Description")
        desc_field.set_title(_("Description"))
        desc_field.set_subtitle(unidecode(self.video.description[:40]).replace("&","&")+"...")
        desc = Adw.ActionRow()
        desc.set_subtitle(unidecode(self.video.description).replace("&","&"))


@@ 86,8 88,8 @@ class PlayerScreen(Adw.NavigationPage):

        # add to playlist button
        btn_bookmark = Adw.ActionRow()
        btn_bookmark.set_title("Bookmark")
        btn_bookmark.set_subtitle("Add this video to a playlist")
        btn_bookmark.set_title(_("Bookmark"))
        btn_bookmark.set_subtitle(_("Add this video to a playlist"))
        btn_bookmark.add_suffix(Gtk.Image.new_from_icon_name("user-bookmarks-symbolic"))
        btn_bookmark.set_activatable(True)
        btn_bookmark.set_action_name("win.add_to_playlist")

M melon/playlist/__init__.py => melon/playlist/__init__.py +6 -4
@@ 3,13 3,15 @@ import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw
from unidecode import unidecode
from gettext import gettext as _

from melon.servers.utils import get_servers_list, get_server_instance
from melon.servers import Preference, PreferenceType
from melon.widgets.iconbutton import IconButton
from melon.widgets.feeditem import AdaptiveFeedItem
from melon.models import get_app_settings, get_local_playlist
from melon.models import is_server_enabled, ensure_server_disabled, ensure_server_enabled
from unidecode import unidecode

class LocalPlaylistScreen(Adw.NavigationPage):
    def __init__(self, playlist_id, *args, **kwargs):


@@ 30,10 32,10 @@ class LocalPlaylistScreen(Adw.NavigationPage):

        if len(playlist.content) == 0:
            status = Adw.StatusPage()
            status.set_title("*crickets chirping*")
            status.set_description("You haven't added any videos to this playlist yet")
            status.set_title(_("*crickets chirping*"))
            status.set_description(_("You haven't added any videos to this playlist yet"))
            status.set_icon_name("weather-few-clouds-night-symbolic")
            icon_button = IconButton("Start watching", "video-display-symbolic")
            icon_button = IconButton(_("Start watching"), "video-display-symbolic")
            icon_button.set_action_name("win.home")
            box = Gtk.CenterBox()
            box.set_center_widget(icon_button)

M melon/playlist/create.py => melon/playlist/create.py +11 -10
@@ 3,6 3,7 @@ import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw, Gio, GLib
from gettext import gettext as _

from melon.widgets.iconbutton import IconButton
from melon.widgets.simpledialog import SimpleDialog


@@ 13,15 14,15 @@ from melon.servers.utils import get_app_settings
class PlaylistCreatorDialog(SimpleDialog):
    def __init__(self, video=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.set_title("New Playlist")
        self.set_title(_("New Playlist"))
        page = Adw.PreferencesPage()
        self.content = []
        if not video is None:
            self.content = [ video ]
            # use preference group as preview for video element
            preview_group = Adw.PreferencesGroup()
            preview_group.set_title("Video")
            preview_group.set_description("The following video will be added to the new playlist")
            preview_group.set_title(_("Video"))
            preview_group.set_description(_("The following video will be added to the new playlist"))
            preview_group.add(AdaptiveFeedItem(
                video,
                clickable=False,


@@ 30,22 31,22 @@ class PlaylistCreatorDialog(SimpleDialog):
            page.add(preview_group)
        # use preference group for input
        input_group = Adw.PreferencesGroup()
        input_group.set_title("New Playlist")
        input_group.set_description("Enter more playlist information")
        input_group.set_title(_("New Playlist"))
        input_group.set_description(_("Enter more playlist information"))

        self.input_title = Adw.EntryRow()
        self.input_title.set_title("Playlist Name")
        self.input_title.set_text("Unnamed Playlist")
        self.input_title.set_title(_("Playlist Name"))
        self.input_title.set_text(_("Unnamed Playlist"))
        self.input_desc = Adw.EntryRow()
        self.input_desc.set_title("Playlist description")
        self.input_desc.set_title(_("Playlist description"))
        input_group.add(self.input_title)
        input_group.add(self.input_desc)

        page.add(input_group)

        bottom_bar = Gtk.Box()
        btn_cancel = IconButton("Cancel", "process-stop-symbolic", tooltip="Do not create playlist")
        btn_confirm = IconButton("Create", "list-add-symbolic", tooltip="Create playlist")
        btn_cancel = IconButton(_("Cancel"), "process-stop-symbolic", tooltip=_("Do not create playlist"))
        btn_confirm = IconButton(_("Create"), "list-add-symbolic", tooltip=_("Create playlist"))
        btn_confirm.connect(
            "clicked",
            self.create_playlist

M melon/playlist/pick.py => melon/playlist/pick.py +9 -8
@@ 4,6 4,7 @@ gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw, Gio, GLib, Gdk
from unidecode import unidecode
from gettext import gettext as _

from melon.widgets.feeditem import AdaptiveFeedItem
from melon.widgets.iconbutton import IconButton


@@ 19,13 20,13 @@ class PlaylistPickerDialog(SimpleDialog):
        diag.show()
    def __init__(self, video, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.set_title("Add to Playlist")
        self.set_title(_("Add to Playlist"))
        page = Adw.PreferencesPage()

        # use preference group as preview for video element
        preview_group = Adw.PreferencesGroup()
        preview_group.set_title("Video")
        preview_group.set_description("The following video will be added to the playlist")
        preview_group.set_title(_("Video"))
        preview_group.set_description(_("The following video will be added to the playlist"))
        preview_group.add(AdaptiveFeedItem(
            video,
            clickable=False,


@@ 35,11 36,11 @@ class PlaylistPickerDialog(SimpleDialog):

        group = Adw.PreferencesGroup()
        page.add(group)
        group.set_title("Add to playlist")
        group.set_description("Choose a playlist to add the video to. Note that you can only add videos to local playlists, not external bookmarked ones")
        group.set_title(_("Add to playlist"))
        group.set_description(_("Choose a playlist to add the video to. Note that you can only add videos to local playlists, not external bookmarked ones"))
        make_new = Adw.ActionRow()
        make_new.set_title("Create new playlist")
        make_new.set_subtitle("Create a new playlist and add the video to it")
        make_new.set_title(_("Create new playlist"))
        make_new.set_subtitle(_("Create a new playlist and add the video to it"))
        make_new.add_suffix(Gtk.Image.new_from_icon_name("go-next-symbolic"))
        # I can't make this work ):
        # make_new.set_action_name("app.add_to_new_playlist")


@@ 82,7 83,7 @@ class PlaylistPickerDialog(SimpleDialog):
            group.add(row)

        bottom_bar = Gtk.Box()
        btn_cancel = IconButton("Cancel", "process-stop-symbolic", tooltip="Do not create playlist")
        btn_cancel = IconButton(_("Cancel"), "process-stop-symbolic", tooltip=_("Do not create playlist"))
        padding = 12
        btn_cancel.set_vexpand(True)
        btn_cancel.set_hexpand(True)

M melon/servers/invidious/__init__.py => melon/servers/invidious/__init__.py +10 -8
@@ 1,11 1,13 @@
from melon.servers import Server, Preference, PreferenceType
from melon.servers import Feed, Channel, Video, Playlist, Stream, SearchMode
from melon.servers import USER_AGENT
from bs4 import BeautifulSoup
import requests
from urllib.parse import urlparse,parse_qs
from datetime import datetime
from gettext import gettext as _

from melon.import_providers.newpipe import NewpipeImporter
from melon.servers import Server, Preference, PreferenceType
from melon.servers import Feed, Channel, Video, Playlist, Stream, SearchMode
from melon.servers import USER_AGENT

class NewpipeInvidiousImporter(NewpipeImporter):
    server_id = "invidious"


@@ 22,13 24,13 @@ class Invidious(Server):

    id = "invidious"
    name = "Invidious"
    description = "Open source alternative front-end to YouTube"
    description = _("Open source alternative front-end to YouTube")

    settings = {
        "instance": Preference(
            "instance",
            "Instance",
            "See https://docs.invidious.io/instances/ for a list of available instances",
            _("Instance"),
            _("See https://docs.invidious.io/instances/ for a list of available instances"),
            PreferenceType.TEXT,
            "https://inv.tux.pizza",
            "https://inv.tux.pizza"),


@@ 39,8 41,8 @@ class Invidious(Server):
    is_nsfw = True,

    known_public_feeds = {
        "/feed/trending": Feed("trending", "Trending", "starred-symbolic"),
        "/feed/popular": Feed("popular", "Popular", "emblem-favorite-symbolic")
        "/feed/trending": Feed("trending", _("Trending"), "starred-symbolic"),
        "/feed/popular": Feed("popular", _("Popular"), "emblem-favorite-symbolic")
    }

    def get_external_url(self):

M melon/servers/nebula/__init__.py => melon/servers/nebula/__init__.py +18 -16
@@ 1,11 1,13 @@
from melon.servers import Server, Preference, PreferenceType
from melon.servers import Feed, Channel, Video, Playlist, Stream, SearchMode
from melon.servers import USER_AGENT
from bs4 import BeautifulSoup
import requests
from urllib.parse import urlparse,parse_qs
from urllib.parse import urlparse, parse_qs
from datetime import datetime
import json
from gettext import gettext as _

from melon.servers import Server, Preference, PreferenceType
from melon.servers import Feed, Channel, Video, Playlist, Stream, SearchMode
from melon.servers import USER_AGENT

# NOTE: uses beautifulsoup instead of the invidious api
# because not all invidious servers provide the api


@@ 14,26 16,26 @@ class Nebula(Server):

    id = "nebula"
    name = "Nebula"
    description = "Home of smart, thoughtful videos, podcasts, and classes from your favorite creators"
    description = _("Home of smart, thoughtful videos, podcasts, and classes from your favorite creators")

    settings = {
        "email": Preference(
            "email",
            "Email Address",
            "Email Address to login to your account",
            _("Email Address"),
            _("Email Address to login to your account"),
            PreferenceType.TEXT,
            "",""
        ),
        "password": Preference(
            "password",
            "Password",
            "Password associated with your account",
            _("Password"),
            _("Password associated with your account"),
            PreferenceType.PASSWORD,
            "",""),
        "m3u8-player": Preference(
            "m3u8-player",
            ".m3u8 Player",
            "valid .m3u8 web-pased player url. %s will be replaced with playlist url",
            _(".m3u8 Player"),
            _("valid .m3u8 web-pased player url. %s will be replaced with playlist url"),
            PreferenceType.TEXT,
            "https://www.hlsplayer.org/play?url=%s",
            "https://www.hlsplayer.org/play?url=%s")


@@ 65,19 67,19 @@ class Nebula(Server):
                if listing["title"] == "Latest Videos":
                    feeds.append(Feed(
                        listing["id"],
                        listing["title"],
                        _(listing["title"]),
                        icon="dialog-information-symbolic")
                    .with_url(listing["view_all_url"]))
                elif listing["title"] == "Nebula Originals":
                    feeds.append(Feed(
                        listing["id"],
                        listing["title"],
                        _(listing["title"]),
                        icon="starred-symbolic"
                    ).with_url(listing["view_all_url"]))
                elif listing["title"] == "Nebula Plus":
                    feeds.append(Feed(
                        listing["id"],
                        listing["title"],
                        _(listing["title"]),
                        icon="list-add-symbolic"
                    ).with_url(listing["view_all_url"]))
            return feeds


@@ 189,13 191,13 @@ class Nebula(Server):
    def get_channel_feeds(self, channel_id:str):
        url = f"https://content.api.nebula.app/video_channels/{channel_id}/"
        r = requests.get(url, headers={ "User-Agent": USER_AGENT })
        video_feed = Feed("videos", "Videos")
        video_feed = Feed("videos", _("Videos"))
        if r.ok:
            data = json.loads(r.text)
            if (not "playlist" in data) or len(data["playlist"]) == 0:
                return [ video_feed ]
            else:
                return [ video_feed, Feed("playlists", "Playlists") ]
                return [ video_feed, Feed("playlists", _("Playlists")) ]

    def get_default_channel_feed(self, cid:str):
        return "videos"

M melon/servers/peertube/__init__.py => melon/servers/peertube/__init__.py +16 -14
@@ 1,35 1,37 @@
from melon.servers import Server, Preference, PreferenceType
from melon.servers import Feed, Channel, Video, Playlist, Stream, SearchMode
from melon.servers import USER_AGENT
import requests
from urllib.parse import urlparse,parse_qs
from datetime import datetime
from gettext import gettext as _

from melon.servers import Server, Preference, PreferenceType
from melon.servers import Feed, Channel, Video, Playlist, Stream, SearchMode
from melon.servers import USER_AGENT
from melon.import_providers.newpipe import NewpipeImporter

class Peertube(Server):

    id = "peertube"
    name = "Peertube"
    description = "Decentralized video hosting network, based on free/libre software"
    description = _("Decentralized video hosting network, based on free/libre software")

    settings = {
        "instances": Preference(
            "instances",
            "Instances",
            "List of peertube instances, from which to fetch content. See https://joinpeertube.org/instances",
            _("Instances"),
            _("List of peertube instances, from which to fetch content. See https://joinpeertube.org/instances"),
            PreferenceType.MULTI,
            ["https://tilvids.com/"],
            ["https://tilvids.com/"]),
        "nsfw": Preference(
            "nsfw",
            "Show NSFW content",
            "Passes the nsfw filter to the peertube search API",
            _("Show NSFW content"),
            _("Passes the nsfw filter to the peertube search API"),
            PreferenceType.TOGGLE,
            False, False),
        "federate": Preference(
            "federate",
            "Enable Federation",
            "Returns content from federated instances instead of only local content",
            _("Enable Federation"),
            _("Returns content from federated instances instead of only local content"),
            PreferenceType.TOGGLE,
            True, True)
       }


@@ 53,7 55,7 @@ class Peertube(Server):
        # nor is it possible to show trending videos,
        # because they cannot be merged
        return [
            Feed("latest", "Latest", "dialog-information-symbolic")
            Feed("latest", _("Latest"), "dialog-information-symbolic")
        ]

    def get_query_config(self) -> str:


@@ 199,11 201,11 @@ class Peertube(Server):
        res = self.get_channel_feed_content(cid, "playlists")
        if len(res) != 0:
            return [
                Feed("videos", "Videos"),
                Feed("playlists", "Playlists")
                Feed("videos", _("Videos")),
                Feed("playlists", _("Playlists"))
            ]
        return [
            Feed("videos", "Videos")
            Feed("videos", _("Videos"))
        ]

    def get_default_channel_feed(self, cid:str):

M melon/servers/utils.py => melon/servers/utils.py +0 -2
@@ 213,8 213,6 @@ def get_servers_list(include_disabled=False, order_by='name'):

                modules.append(importlib.import_module(relname, package='melon.servers'))
                count += 1

            logger.info('Load {0} servers from external folder: {1}'.format(count, servers_path))
    else:
        # fallback to local exploration
        import melon.servers

M melon/settings/__init__.py => melon/settings/__init__.py +17 -15
@@ 3,6 3,8 @@ import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw
from gettext import gettext as _

from melon.servers.utils import get_servers_list, get_server_instance
from melon.servers import Preference, PreferenceType
from melon.widgets.preferencerow import PreferenceRow


@@ 13,35 15,35 @@ global_prefs = {
        # True if images should be shown when searching or browsing public feeds
        "show_images_in_browse": Preference(
            "show_images_in_browse",
            "Show Previews when browsing public feeds (incl. search)",
            "Set to true to show previews when viewing channel contents, public feeds and searching",
            _("Show Previews when browsing public feeds (incl. search)"),
            _("Set to true to show previews when viewing channel contents, public feeds and searching"),
            PreferenceType.TOGGLE,
            True, True),
        # True if images should be shown when browsing channels/playlists/home feed
        "show_images_in_feed": Preference(
            "show_images_in_feed",
            "Show Previews in local feeds",
            "Set to true to show previews in the new feed, for subscribed channels, and saved playlists",
            _("Show Previews in local feeds"),
            _("Set to true to show previews in the new feed, for subscribed channels, and saved playlists"),
            PreferenceType.TOGGLE,
            True, True),

        "nsfw_content": Preference(
            "nsfw_content",
            "Show servers that may contain nsfw content",
            "Lists/Delists servers in the browse servers list, if they contain some nsfw content",
            _("Show servers that may contain nsfw content"),
            _("Lists/Delists servers in the browse servers list, if they contain some nsfw content"),
            PreferenceType.TOGGLE,
            True, True),
        "nsfw_only_content": Preference(
            "nsfw_only_content",
            "Show servers that only contain nsfw content",
            "Lists/Delists servers in the browse servers list, if they contain only/mostly nsfw content",
            _("Show servers that only contain nsfw content"),
            _("Lists/Delists servers in the browse servers list, if they contain only/mostly nsfw content"),
            PreferenceType.TOGGLE,
            True, True
        ),
        "login_required": Preference(
            "login_required",
            "Show servers that require login",
            "Lists/Delists servers in the browse servers list, if they require login to function",
            _("Show servers that require login"),
            _("Lists/Delists servers in the browse servers list, if they require login to function"),
            PreferenceType.TOGGLE,
            True, True
        )


@@ 50,7 52,7 @@ global_prefs = {
class SettingsScreen(Adw.NavigationPage):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.set_title("Settings")
        self.set_title(_("Settings"))

        self.header_bar = Adw.HeaderBar()



@@ 63,8 65,8 @@ class SettingsScreen(Adw.NavigationPage):

        # global app settings
        self.general = Adw.PreferencesGroup()
        self.general.set_title("General")
        self.general.set_description("Global app settings")
        self.general.set_title(_("General"))
        self.general.set_description(_("Global app settings"))
        app_conf = get_app_settings()
        for pref_id, pref in global_prefs.items():
            # manually fetch current values


@@ 88,8 90,8 @@ class SettingsScreen(Adw.NavigationPage):
            instance = get_server_instance(server)
            epref = Preference(
                "server-enabled",
                "Enable Server",
                "Disabled servers won't show up in the browser or on the local/home screen",
                _("Enable Server"),
                _("Disabled servers won't show up in the browser or on the local/home screen"),
                PreferenceType.TOGGLE,
                True, is_server_enabled(server_id))
            er = PreferenceRow(epref)

M melon/widgets/preferencerow.py => melon/widgets/preferencerow.py +19 -16
@@ 3,11 3,14 @@ import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw, GLib, Gdk

from gettext import gettext as _
from copy import deepcopy as copy

from melon.servers import Resource,Video,Playlist,Channel,Preference,PreferenceType
from melon.servers.utils import pixbuf_from_url
from melon.widgets.iconbutton import IconButton
from melon.widgets.simpledialog import SimpleDialog
from copy import deepcopy as copy

class PreferenceRow():
    def __init__(self, pref: Preference, *args, **kwargs):


@@ 109,7 112,7 @@ class MultiRow(Adw.PreferencesRow):
        self.set_child(self.inner)
        self.inner.set_title(self.pref.name)
        self.inner.set_description(self.pref.description)
        self.adder = IconButton("Add", "list-add-symbolic")
        self.adder = IconButton(_("Add"), "list-add-symbolic")
        self.adder.connect("clicked", lambda x: self.open_add())
        self.inner.set_header_suffix(self.adder)
        counter = 0


@@ 123,7 126,7 @@ class MultiRow(Adw.PreferencesRow):
            # not shown for first element
            if counter > 0:
                move_up = IconButton("","go-up-symbolic")
                move_up.set_tooltip_text("Move up")
                move_up.set_tooltip_text(_("Move up"))
                move_up.add_css_class("circular")
                move_up.connect(
                    "clicked",


@@ 134,7 137,7 @@ class MultiRow(Adw.PreferencesRow):
            # not shown for last element
            if counter < length-1:
                move_down = IconButton("", "go-down-symbolic")
                move_down.set_tooltip_text("Move down")
                move_down.set_tooltip_text(_("Move down"))
                move_down.add_css_class("circular")
                move_down.connect(
                    "clicked",


@@ 143,7 146,7 @@ class MultiRow(Adw.PreferencesRow):
                actions.append(move_down)
            # remove button
            remove = IconButton("", "list-remove-symbolic")
            remove.set_tooltip_text("Remove from list")
            remove.set_tooltip_text(_("Remove from list"))
            remove.add_css_class("circular")
            remove.connect("clicked", pass_me(self.confirm_delete, item, counter))
            actions.append(remove)


@@ 156,20 159,20 @@ class MultiRow(Adw.PreferencesRow):
        self.update()
    def open_add(self):
        diag = SimpleDialog()
        diag.set_title("Add Item")
        diag.set_title(_("Add Item"))
        # preview prferences group
        box = Adw.PreferencesGroup();
        box.set_title("Create a new list entry")
        box.set_description("Enter the new value here")
        box.set_title(_("Create a new list entry"))
        box.set_description(_("Enter the new value here"))
        # text input
        inp = Adw.EntryRow()
        inp.set_title("Value")
        inp.set_title(_("Value"))
        box.add(inp)
        diag.set_widget(box)
        # place button bar in toolbar
        bottom_bar = Gtk.Box()
        btn_cancel = IconButton("Cancel", "process-stop-symbolic", tooltip="Do not create a new item")
        btn_confirm = IconButton("Create", "list-add-symbolic", tooltip="Add entry to list")
        btn_cancel = IconButton(_("Cancel"), "process-stop-symbolic", tooltip="Do not create a new item")
        btn_confirm = IconButton(_("Create"), "list-add-symbolic", tooltip="Add entry to list")
        btn_confirm.connect(
            "clicked",
            lambda _: many(


@@ 200,19 203,19 @@ class MultiRow(Adw.PreferencesRow):
        diag.show()
    def confirm_delete(self, _, item:str, counter:int):
        diag = SimpleDialog()
        diag.set_title("Delete")
        diag.set_title(_("Delete"))
        # preview prferences group
        preview = Adw.PreferencesGroup();
        preview.set_title("Do you really want to delete this item?")
        preview.set_description("You won't be able to restore it afterwards")
        preview.set_title(_("Do you really want to delete this item?"))
        preview.set_description(_("You won't be able to restore it afterwards"))
        row = Adw.ActionRow()
        row.set_title(item)
        preview.add(row)
        diag.set_widget(preview)
        # place button bar in toolbar
        bottom_bar = Gtk.Box()
        btn_cancel = IconButton("Cancel", "process-stop-symbolic", tooltip="Do not remove item")
        btn_confirm = IconButton("Delete", "list-add-symbolic", tooltip="Remove item from list")
        btn_cancel = IconButton(_("Cancel"), "process-stop-symbolic", tooltip=_("Do not remove item"))
        btn_confirm = IconButton(_("Delete"), "list-add-symbolic", tooltip=_("Remove item from list"))
        btn_confirm.connect(
            "clicked",
            lambda _: many(

M melon/window.py => melon/window.py +3 -2
@@ 3,6 3,7 @@ import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw, Gio, GLib
from gettext import gettext as _

from melon.home import HomeScreen
from melon.browse import BrowseScreen


@@ 77,7 78,7 @@ class MainWindow(Adw.ApplicationWindow):
        dialog.set_version("0.1.2")
        dialog.set_developer_name("Jakob Meier (@comcloudway)")
        dialog.set_license_type(Gtk.License(Gtk.License.GPL_3_0))
        dialog.set_comments("Stream videos on the go")
        dialog.set_comments(_("Stream videos on the go"))
        dialog.set_website("https://codeberg.org/comcloudway/melon")
        dialog.set_issue_url("https://codeberg.org/comcloudway/melon/issues")
        #dialog.add_credit_section("Contributors", ["Name1 url"])


@@ 85,7 86,7 @@ class MainWindow(Adw.ApplicationWindow):
        dialog.set_copyright("© 2024 Jakob Meier (@comcloudway)")
        dialog.set_developers(["Jakob Meier (@comcloudway)"])
        # TODO: icon must be uploaded in ~/.local/share/icons or /usr/share/icons
        dialog.set_application_icon("icu.ccw.melon")
        dialog.set_application_icon("icu.ccw.Melon")
        dialog.set_visible(True)
    # opens the setting panel
    def open_settings(self,action,prefs):

M meson.build => meson.build +2 -0
@@ 37,8 37,10 @@ app_id_aspath = '/'.join([domainext, domainname, prettyname])
app_id = base_id

install_subdir(meson.project_name(), install_dir: pythondir)

subdir('data')
subdir('bin')
subdir('po')

# Run required post-install steps
gnome.post_install(

A po/LINGUAS => po/LINGUAS +1 -0
@@ 0,0 1,1 @@
# Please keep this list alphabetically sorted

A po/POTFILES.in => po/POTFILES.in +22 -0
@@ 0,0 1,22 @@
data/icu.ccw.Melon.desktop.in
melon/home/history.py
melon/home/new.py
melon/home/playlists.py
melon/home/subs.py
melon/settings/__init__.py
melon/servers/invidious/__init__.py
melon/servers/nebula/__init__.py
melon/servers/peertube/__init__.py
melon/widgets/preferencerow.py
melon/browse/channel.py
melon/browse/playlist.py
melon/browse/__init__.py
melon/browse/search.py
melon/browse/server.py
melon/player/__init__.py
melon/playlist/create.py
melon/playlist/__init__.py
melon/playlist/pick.py
melon/import_providers/newpipe.py
melon/importer.py
melon/window.py

A po/melon.pot => po/melon.pot +536 -0
@@ 0,0 1,536 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the Melon package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Melon 0.1.2\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-01 12:20+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"

#: ../melon/home/history.py:27 ../melon/home/new.py:27
#: ../melon/home/playlists.py:25 ../melon/home/subs.py:22
#: ../melon/browse/__init__.py:18 ../melon/browse/search.py:58
#: ../melon/browse/server.py:101 ../melon/browse/server.py:125
#: ../melon/playlist/__init__.py:35 ../melon/importer.py:61
msgid "*crickets chirping*"
msgstr ""

#: ../melon/home/history.py:28
msgid "You haven't watched any videos yet"
msgstr ""

#: ../melon/home/history.py:30 ../melon/home/new.py:34 ../melon/home/subs.py:25
msgid "Browse Servers"
msgstr ""

#: ../melon/home/history.py:39 ../melon/home/history.py:82
msgid "History"
msgstr ""

#: ../melon/home/history.py:40
msgid "These are the videos you opened in the past"
msgstr ""

#: ../melon/home/new.py:21
msgid "Refresh"
msgstr ""

#: ../melon/home/new.py:30
msgid "Subscribe to a channel first, to view new uploads"
msgstr ""

#: ../melon/home/new.py:32
msgid "The channels you are subscribed to haven't uploaded anything yet"
msgstr ""

#: ../melon/home/new.py:53
#, python-brace-format
msgid "(Last refresh: {last_refresh})"
msgstr ""

#: ../melon/home/new.py:55 ../melon/home/new.py:108
msgid "What's new"
msgstr ""

#: ../melon/home/new.py:56
msgid "These are the latest videos of channels you follow"
msgstr ""

#: ../melon/home/playlists.py:26
msgid "You don't have any playlists yet"
msgstr ""

#: ../melon/home/playlists.py:28 ../melon/home/playlists.py:38
msgid "Create a new playlist"
msgstr ""

#: ../melon/home/playlists.py:36 ../melon/home/playlists.py:69
#: ../melon/servers/nebula/__init__.py:200
#: ../melon/servers/peertube/__init__.py:205 ../melon/browse/search.py:88
#: ../melon/browse/server.py:48
msgid "Playlists"
msgstr ""

#: ../melon/home/playlists.py:37
msgid "Here are playlists you've bookmarked or created yourself"
msgstr ""

#: ../melon/home/playlists.py:38
msgid "New"
msgstr ""

#: ../melon/home/subs.py:23
msgid "You aren't yet subscribed to channels"
msgstr ""

#: ../melon/home/subs.py:33 ../melon/home/subs.py:68
msgid "Subscriptions"
msgstr ""

#: ../melon/home/subs.py:34
msgid "You are subscribed to the following channels"
msgstr ""

#: ../melon/settings/__init__.py:18
msgid "Show Previews when browsing public feeds (incl. search)"
msgstr ""

#: ../melon/settings/__init__.py:19
msgid ""
"Set to true to show previews when viewing channel contents, public feeds and "
"searching"
msgstr ""

#: ../melon/settings/__init__.py:25
msgid "Show Previews in local feeds"
msgstr ""

#: ../melon/settings/__init__.py:26
msgid ""
"Set to true to show previews in the new feed, for subscribed channels, and "
"saved playlists"
msgstr ""

#: ../melon/settings/__init__.py:32
msgid "Show servers that may contain nsfw content"
msgstr ""

#: ../melon/settings/__init__.py:33
msgid ""
"Lists/Delists servers in the browse servers list, if they contain some nsfw "
"content"
msgstr ""

#: ../melon/settings/__init__.py:38
msgid "Show servers that only contain nsfw content"
msgstr ""

#: ../melon/settings/__init__.py:39
msgid ""
"Lists/Delists servers in the browse servers list, if they contain only/"
"mostly nsfw content"
msgstr ""

#: ../melon/settings/__init__.py:45
msgid "Show servers that require login"
msgstr ""

#: ../melon/settings/__init__.py:46
msgid ""
"Lists/Delists servers in the browse servers list, if they require login to "
"function"
msgstr ""

#: ../melon/settings/__init__.py:55
msgid "Settings"
msgstr ""

#: ../melon/settings/__init__.py:68
msgid "General"
msgstr ""

#: ../melon/settings/__init__.py:69
msgid "Global app settings"
msgstr ""

#: ../melon/settings/__init__.py:93
msgid "Enable Server"
msgstr ""

#: ../melon/settings/__init__.py:94
msgid ""
"Disabled servers won't show up in the browser or on the local/home screen"
msgstr ""

#: ../melon/servers/invidious/__init__.py:27
msgid "Open source alternative front-end to YouTube"
msgstr ""

#: ../melon/servers/invidious/__init__.py:32
msgid "Instance"
msgstr ""

#: ../melon/servers/invidious/__init__.py:33
msgid ""
"See https://docs.invidious.io/instances/ for a list of available instances"
msgstr ""

#: ../melon/servers/invidious/__init__.py:44
msgid "Trending"
msgstr ""

#: ../melon/servers/invidious/__init__.py:45
msgid "Popular"
msgstr ""

#: ../melon/servers/nebula/__init__.py:19
msgid ""
"Home of smart, thoughtful videos, podcasts, and classes from your favorite "
"creators"
msgstr ""

#: ../melon/servers/nebula/__init__.py:24
msgid "Email Address"
msgstr ""

#: ../melon/servers/nebula/__init__.py:25
msgid "Email Address to login to your account"
msgstr ""

#: ../melon/servers/nebula/__init__.py:31
msgid "Password"
msgstr ""

#: ../melon/servers/nebula/__init__.py:32
msgid "Password associated with your account"
msgstr ""

#: ../melon/servers/nebula/__init__.py:37
msgid ".m3u8 Player"
msgstr ""

#: ../melon/servers/nebula/__init__.py:38
#, python-format
msgid "valid .m3u8 web-pased player url. %s will be replaced with playlist url"
msgstr ""

#: ../melon/servers/nebula/__init__.py:194
#: ../melon/servers/peertube/__init__.py:204
#: ../melon/servers/peertube/__init__.py:208 ../melon/browse/search.py:91
#: ../melon/browse/server.py:51
msgid "Videos"
msgstr ""

#: ../melon/servers/peertube/__init__.py:15
msgid "Decentralized video hosting network, based on free/libre software"
msgstr ""

#: ../melon/servers/peertube/__init__.py:20
msgid "Instances"
msgstr ""

#: ../melon/servers/peertube/__init__.py:21
msgid ""
"List of peertube instances, from which to fetch content. See https://"
"joinpeertube.org/instances"
msgstr ""

#: ../melon/servers/peertube/__init__.py:27
msgid "Show NSFW content"
msgstr ""

#: ../melon/servers/peertube/__init__.py:28
msgid "Passes the nsfw filter to the peertube search API"
msgstr ""

#: ../melon/servers/peertube/__init__.py:33
msgid "Enable Federation"
msgstr ""

#: ../melon/servers/peertube/__init__.py:34
msgid "Returns content from federated instances instead of only local content"
msgstr ""

#: ../melon/servers/peertube/__init__.py:58
msgid "Latest"
msgstr ""

#: ../melon/widgets/preferencerow.py:115
msgid "Add"
msgstr ""

#: ../melon/widgets/preferencerow.py:129
msgid "Move up"
msgstr ""

#: ../melon/widgets/preferencerow.py:140
msgid "Move down"
msgstr ""

#: ../melon/widgets/preferencerow.py:149
msgid "Remove from list"
msgstr ""

#: ../melon/widgets/preferencerow.py:162
msgid "Add Item"
msgstr ""

#: ../melon/widgets/preferencerow.py:165
msgid "Create a new list entry"
msgstr ""

#: ../melon/widgets/preferencerow.py:166
msgid "Enter the new value here"
msgstr ""

#: ../melon/widgets/preferencerow.py:169
msgid "Value"
msgstr ""

#: ../melon/widgets/preferencerow.py:174 ../melon/widgets/preferencerow.py:217
#: ../melon/playlist/create.py:48 ../melon/playlist/pick.py:86
msgid "Cancel"
msgstr ""

#: ../melon/widgets/preferencerow.py:175 ../melon/playlist/create.py:49
msgid "Create"
msgstr ""

#: ../melon/widgets/preferencerow.py:206 ../melon/widgets/preferencerow.py:218
msgid "Delete"
msgstr ""

#: ../melon/widgets/preferencerow.py:209
msgid "Do you really want to delete this item?"
msgstr ""

#: ../melon/widgets/preferencerow.py:210
msgid "You won't be able to restore it afterwards"
msgstr ""

#: ../melon/widgets/preferencerow.py:217
msgid "Do not remove item"
msgstr ""

#: ../melon/widgets/preferencerow.py:218
msgid "Remove item from list"
msgstr ""

#: ../melon/browse/channel.py:99
msgid "Subscribe to channel"
msgstr ""

#: ../melon/browse/channel.py:100
msgid "Add latest uploads to home feed"
msgstr ""

#: ../melon/browse/channel.py:127
msgid "Channel feed"
msgstr ""

#: ../melon/browse/channel.py:128
msgid "This channel provides multiple feeds, choose which one to view"
msgstr ""

#: ../melon/browse/playlist.py:61 ../melon/player/__init__.py:91
msgid "Bookmark"
msgstr ""

#: ../melon/browse/playlist.py:62
msgid "Add Playlist to your local playlist collection"
msgstr ""

#: ../melon/browse/__init__.py:19
msgid "There are no available servers"
msgstr ""

#: ../melon/browse/__init__.py:21 ../melon/browse/search.py:61
#: ../melon/importer.py:64
msgid "Enable servers in the settings menu"
msgstr ""

#: ../melon/browse/__init__.py:29
msgid "Available Servers"
msgstr ""

#: ../melon/browse/__init__.py:30
msgid "You can enable/disable and filter servers in the settings menu"
msgstr ""

#: ../melon/browse/__init__.py:48
msgid "Servers"
msgstr ""

#: ../melon/browse/__init__.py:56 ../melon/browse/search.py:106
msgid "Global Search"
msgstr ""

#: ../melon/browse/search.py:44
msgid "No results"
msgstr ""

#: ../melon/browse/search.py:46
#, python-brace-format
msgid "{count} result"
msgid_plural "{count} results"
msgstr[0] ""
msgstr[1] ""

#: ../melon/browse/search.py:59
msgid "There are no available servers, a search would yield no results"
msgstr ""

#: ../melon/browse/search.py:83 ../melon/browse/server.py:43
msgid "Any"
msgstr ""

#: ../melon/browse/search.py:85 ../melon/browse/server.py:45
msgid "Channels"
msgstr ""

#: ../melon/browse/server.py:23
msgid "Search"
msgstr ""

#: ../melon/browse/server.py:102
msgid "Try searching for a term"
msgstr ""

#: ../melon/browse/server.py:104
msgid "Try using a different query"
msgstr ""

#: ../melon/browse/server.py:126
msgid "This feed is empty"
msgstr ""

#: ../melon/player/__init__.py:65
msgid "Quality"
msgstr ""

#: ../melon/player/__init__.py:66
msgid "Video quality"
msgstr ""

#: ../melon/player/__init__.py:76
msgid "Description"
msgstr ""

#: ../melon/player/__init__.py:92
msgid "Add this video to a playlist"
msgstr ""

#: ../melon/playlist/create.py:17 ../melon/playlist/create.py:34
msgid "New Playlist"
msgstr ""

#: ../melon/playlist/create.py:24 ../melon/playlist/pick.py:28
msgid "Video"
msgstr ""

#: ../melon/playlist/create.py:25
msgid "The following video will be added to the new playlist"
msgstr ""

#: ../melon/playlist/create.py:35
msgid "Enter more playlist information"
msgstr ""

#: ../melon/playlist/create.py:38
msgid "Playlist Name"
msgstr ""

#: ../melon/playlist/create.py:39
msgid "Unnamed Playlist"
msgstr ""

#: ../melon/playlist/create.py:41
msgid "Playlist description"
msgstr ""

#: ../melon/playlist/create.py:48 ../melon/playlist/pick.py:86
msgid "Do not create playlist"
msgstr ""

#: ../melon/playlist/create.py:49
msgid "Create playlist"
msgstr ""

#: ../melon/playlist/__init__.py:36
msgid "You haven't added any videos to this playlist yet"
msgstr ""

#: ../melon/playlist/__init__.py:38
msgid "Start watching"
msgstr ""

#: ../melon/playlist/pick.py:23
msgid "Add to Playlist"
msgstr ""

#: ../melon/playlist/pick.py:29
msgid "The following video will be added to the playlist"
msgstr ""

#: ../melon/playlist/pick.py:39
msgid "Add to playlist"
msgstr ""

#: ../melon/playlist/pick.py:40
msgid ""
"Choose a playlist to add the video to. Note that you can only add videos to "
"local playlists, not external bookmarked ones"
msgstr ""

#: ../melon/playlist/pick.py:42
msgid "Create new playlist"
msgstr ""

#: ../melon/playlist/pick.py:43
msgid "Create a new playlist and add the video to it"
msgstr ""

#: ../melon/import_providers/newpipe.py:28
msgid "Newpipe Database importer"
msgstr ""

#: ../melon/import_providers/newpipe.py:29
msgid ""
"Import the .db file from inside the newpipe .zip export (as invidious "
"content)"
msgstr ""

#: ../melon/import_providers/newpipe.py:30
msgid "Select .db file"
msgstr ""

#: ../melon/import_providers/newpipe.py:45
msgid "Newpipe Database"
msgstr ""

#: ../melon/importer.py:17 ../melon/importer.py:35
msgid "Import"
msgstr ""

#: ../melon/importer.py:36
msgid "The following import methods have been found"
msgstr ""

#: ../melon/importer.py:62
msgid "There are no available importer methods"
msgstr ""

#: ../melon/window.py:81
msgid "Stream videos on the go"
msgstr ""

A po/meson.build => po/meson.build +4 -0
@@ 0,0 1,4 @@
i18n = import('i18n')

message('Update translations')
i18n.gettext(meson.project_name(), preset: 'glib')

A po/update_potfiles.sh => po/update_potfiles.sh +36 -0
@@ 0,0 1,36 @@
#!/bin/sh
# adapted from: https://codeberg.org/valos/Komikku/src/branch/main/po/update_potfiles.sh

version=$(fgrep "version: " ../meson.build | grep -v "meson" | grep -o "'.*'" | sed "s/'//g")

find ../melon -iname "*.py" | \
    xargs xgettext \
    --package-name=Melon \
    --package-version=$version \
    --from-code=UTF-8 \
    --output=melon.pot

echo data/icu.ccw.Melon.desktop.in > POTFILES.in
find ../melon -iname "*.py" | \
    xargs -n 1 grep -H "import gettext" | \
    cut -d':' -f1 | \
    cut -d'/' -f2- >> POTFILES.in

echo "# Please keep this list alphabetically sorted" > LINGUAS
for l in $(find -iname '*.po'); do basename $l .po >> LINGUAS; done

for lang in $(sed "s/^#.*$//g" LINGUAS); do
    mv "${lang}.po" "${lang}.po.old"
    msginit --no-translator --locale=$lang --input melon.pot
    mv "${lang}.po" "${lang}.po.new"
    msgmerge -N "${lang}.po.old" "${lang}.po.new" > ${lang}.po
    rm "${lang}.po.old" "${lang}.po.new"
done

# To create language file use this command
# msginit --locale=LOCALE --input melon.pot
# where LOCALE is something like `de`, `it`, `es`...

# To compile a .po file
# msgfmt --output-file=xx.mo xx.po
# where xx is something like `de`, `it`, `es`...