~comcloudway/melon

92384b47590b372e51e440e8711816346c7466dd — Jakob Meier 7 months ago 41976b2
Added basic file data import screen

currently only single file picking is implemented
M melon/home/__init__.py => melon/home/__init__.py +1 -0
@@ 43,6 43,7 @@ class HomeScreen(Adw.NavigationPage):
        self.menu_button.set_icon_name("open-menu-symbolic")
        model = Gio.Menu()
        model.append("Preferences", "win.prefs")
        model.append("Import Data", "win.import")
        model.append("About Melon", "win.about")
        self.menu_popover = Gtk.PopoverMenu()
        self.menu_popover.set_menu_model(model)

A melon/import_providers/__init__.py => melon/import_providers/__init__.py +25 -0
@@ 0,0 1,25 @@
from enum import Enum, auto
from abc import ABC,abstractmethod

class PickerMode(Enum):
    FILE = auto()
    FOLDER = auto()
class ImportProvider(ABC):
    id: str
    title: str
    description: str
    picker_title: str
    is_multi: bool
    mode: PickerMode
    # dictionary/map with filter name as key
    # and mime types in an array as the value
    # only used for PickerMode.FILE
    filters: dict[str, list[str]] = {}
    @abstractmethod
    def load(self, selection:list[str]):
        """
        Called with a list of file/folder paths
        after the selection dialog was shown
        should take care of importing by accessing the internal model
        """
        pass

A melon/import_providers/utils.py => melon/import_providers/utils.py +62 -0
@@ 0,0 1,62 @@
import sys
import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw, GLib, Gio
from melon.import_providers import ImportProvider, PickerMode

class ImportPicker(Gtk.FileDialog):
    cb = None
    def __init__(self, provider:ImportProvider, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.set_title(provider.picker_title)
        self.set_modal(True)
        self.provider = provider
    def set_onselect(self, cb):
        self.cb = cb
    def done(self, paths: list[str]):
        if self.cb is None:
            return
        self.cb(self.provider, paths)
    def open_single_file(self, dialog, result):
        try:
            file = dialog.open_finish(result)
            if file is not None:
                self.done([file.get_path()])
        except GLib.Error as error:
            print(f"Error opening file: {error.message}")
    def open_multi_files(self, dialog, result):
        try:
            data = dialog.open_multiple_finish(result)
            files = []
            for i in range(data.get_n_items()):
                file = data.get_item(i)
                if file is not None:
                    files.append(file.get_path())
            self.done(files)
        except GLib.Error as error:
            print(f"Error opening file: {error.message}")
    def show(self, win):
        if self.provider.mode == PickerMode.FILE:
            # generate filter
            it = self.provider.filters.items()
            if len(it) > 0:
                filters = Gio.ListStore.new(Gtk.FileFilter)
                first = True
                for name, mimes in it:
                    f = Gtk.FileFilter()
                    f.set_name(name)
                    for mime in mimes:
                        f.add_mime_type(mime)
                    filters.append(f)
                    if first:
                        first = False
                        self.set_default_filter(f)
                self.set_filters(filters)
            # construct dialog
            if self.provider.is_multi:
                self.open_multiple(win, None, self.open_multi_files)
            else:
                self.open(win, None, self.open_single_file)
        else:
            pass

A melon/importer.py => melon/importer.py +98 -0
@@ 0,0 1,98 @@
import sys
import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw, Gio

from melon.import_providers.utils import ImportPicker
from melon.import_providers import ImportProvider, PickerMode
from melon.models.callbacks import freeze, unfreeze
from melon.servers.utils import get_allowed_servers_list, get_server_instance
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.header_bar = Adw.HeaderBar()

        self.toolbar_view = Adw.ToolbarView()
        self.toolbar_view.add_top_bar(self.header_bar)

        self.wrapper = Adw.Clamp()
        self.scrollview = Gtk.ScrolledWindow()
        self.wrapper.set_child(self.scrollview)
        self.toolbar_view.set_content(self.wrapper)
        self.set_child(self.toolbar_view)

        self.reset()

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

        count = 0
        for server in servers:
            inst = get_server_instance(server)
            providers = inst.get_import_providers()
            if len(providers) == 0:
                continue
            group = Adw.PreferencesGroup()
            group.set_title(server["name"])
            group.set_description(server["description"])
            for provider in providers:
                count += 1
                row = Adw.ActionRow()
                row.set_title(provider.title)
                row.set_subtitle(provider.description)
                row.add_suffix(Gtk.Image.new_from_icon_name("go-next-symbolic"))
                row.set_activatable(True)
                row.connect("activated", pass_me(self.do_import, provider))
                group.add(row)
            self.box.add(group)

        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_icon_name("weather-few-clouds-night-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)
            status.set_child(box)
            self.scrollview.set_child(status)
        else:
            self.scrollview.set_child(self.box)

    def do_import(self, _, provider):
        picker = ImportPicker(provider)
        picker.show(None)
        picker.set_onselect(self.on_select)

    def on_select(self, provider:ImportProvider, files):
        # show spinner
        spinner = Gtk.Spinner()
        spinner.set_size_request(50, 50)
        spinner.start()
        cb = Gtk.CenterBox()
        cb.set_center_widget(spinner)
        self.scrollview.set_child(cb)
        # freeze model callbacks
        # otherwise this would result in a lot of reloads and requests
        # which may lead to ddos-like server requests
        freeze()
        # long task
        provider.load(files)
        # reset once done
        self.reset()
        unfreeze()
        # automatically go back home
        self.activate_action("win.home", None)

def pass_me(func, *args):
    return lambda x: func(x, *args)

M melon/models/__init__.py => melon/models/__init__.py +147 -82
@@ 67,7 67,8 @@ def execute_sql(conn:sqlite3.Connection, sql, *sqlargs, many=False) -> bool:
            c.execute(sql, *sqlargs)
        conn.commit()
        c.close()
    except:
    except Exception as e:
        print('SQLite-error:', e)
        return False
    else:
        return True


@@ 101,8 102,6 @@ server_settings_template = {
}

def die():
    # TODO remove
    print("ERROR")
    raise "DB ERROR"

def init_db():


@@ 203,8 202,9 @@ def init_db():
        id,
        video_id,
        video_server,
        position,
        FOREIGN KEY(video_id, video_server) REFERENCES videos(id, server),
        PRIMARY KEY(id, video_id, video_server)
        PRIMARY KEY(id, video_id, video_server, position)
        )
        """) or die()
        # history


@@ 234,6 234,14 @@ def init_db():
        FOREIGN KEY(server, playlist_id) REFERENCES playlists(server, id)
        )
        """)
        execute_sql(conn, """
        CREATE TABLE news(
        timestamp,
        video_id,
        video_server,
        FOREIGN KEY(video_id, video_server) REFERENCES videos(id, server)
        )
        """)

        # the initial setup should always initialize the newest version
        # and not depend on migrations


@@ 261,7 269,7 @@ def init_server_settings(sid:str) -> bool:
        "SELECT * FROM servers WHERE server = ?", (sid,)).fetchall()
    if len(results) == 0:
        # initialize enabled config
        execute_sql(conn, "INSERT INTO servers VALUES (?, ?)", (sid, server_settings_template["enabled"]))
        execute_sql(conn, "INSERT OR REPLACE INTO servers VALUES (?, ?)", (sid, server_settings_template["enabled"]))
        notify("settings_changed")
        return True
    return False


@@ 278,7 286,7 @@ def set_server_setting(sid:str, pref:str, value):
    init_server_settings(sid)
    conn = connect_to_db()
    execute_sql(conn,
                "INSERT INTO server_settings VALUE (?,?,?)",
                "INSERT OR REPLACE INTO server_settings VALUE (?,?,?)",
                (sid, pref, value))
    notify("settings_changed")
def get_server_settings(sid:str):


@@ 363,23 371,14 @@ def has_channel(channel_server, channel_id):
    """, (channel_server, channel_id)).fetchall()
    return len(results) != 0
def ensure_channel(channel:Channel):
    if has_channel(channel.server, channel.id):
        # Update channel data
        conn = connect_to_db()
        execute_sql(conn, """
        UPDATE channels
        SET url = ?, name = ?, bio = ?, avatar = ?
        WHERE server = ?, id = ?
        """,
        (channel.url, channel.name, channel.bio, channel.avatar, channel.server, channel.id))
    else:
        # insert channel data
        conn = connect_to_db()
        execute_sql(conn, """
        INSERT INTO channels
        VALUES (?,?,?,?,?,?)
        """,
        (channel.server, channel.id, channel.url, channel.name, channel.bio, channel.avatar))
    # insert channel data
    conn = connect_to_db()
    execute_sql(conn, """
    INSERT OR REPLACE INTO channels
    VALUES (?,?,?,?,?,?)
    """,
    (channel.server, channel.id, channel.url, channel.name, channel.bio, channel.avatar))
    conn.close()
    notify("channels_changed")
def ensure_subscribed_to_channel(channel: Channel):
    if is_subscribed_to_channel(channel.server, channel.id):


@@ 387,9 386,10 @@ def ensure_subscribed_to_channel(channel: Channel):
    ensure_channel(channel)
    conn = connect_to_db()
    execute_sql(conn, """
    INSERT INTO subscriptions
    INSERT OR REPLACE INTO subscriptions
    VALUES (?,?)
    """, (channel.id, channel.server))
    conn.close()
    notify("channels_changed")
def ensure_unsubscribed_from_channel(server_id: str, channel_id:str):
    conn = connect_to_db()


@@ 397,6 397,7 @@ def ensure_unsubscribed_from_channel(server_id: str, channel_id:str):
    DELETE FROM subscriptions
    WHERE server = ?, id = ?
    """, (server_id, channel_id))
    conn.close()
    notify("channels_changed")
def get_subscribed_channels() -> list[Channel]:
    conn = connect_to_db()


@@ 405,6 406,7 @@ def get_subscribed_channels() -> list[Channel]:
    FROM channels, subscriptions
    WHERE channels.id = subscriptions.video_id AND channels.server == subscriptions.server
    """).fetchall()
    conn.close()
    return [ Channel(d[0], d[2], d[1], d[3], d[4], d[5]) for d in results ]

def has_video(server_id, video_id):


@@ 416,24 418,25 @@ def has_video(server_id, video_id):
    """, (server_id, video_id)).fetchall()
    return len(results) != 0
def ensure_video(vid):
    if has_video(vid.server, vid.id):
        # update video data
        conn = connect_to_db()
        execute_sql(conn, """
        UPDATE videos
        SET url = ?, title = ?, description = ?, thumbnail = ?, channel_id = ?, channel_name = ?
        WHERE server = ?, id = ?
        """,
        (vid.url, vid.title, vid.description, vid.thumbnail, vid.channel[1], vid.channel[0], vid.server, vid.id))
    else:
        # create video
        conn = connect_to_db()
        execute_sql(conn, """
        INSERT INTO videos
    # create video
    conn = connect_to_db()
    execute_sql(conn, """
    INSERT OR REPLACE INTO videos
    VALUES (?,?,?,?,?,?,?,?)
    """,
    (vid.server, vid.id, vid.url, vid.title, vid.description, vid.thumbnail, vid.channel[1], vid.channel[0]))
def add_videos(vids:list[Video]):
    # TODO deduplicate entries
    conn = connect_to_db()
    execute_sql(
        conn,
        """
        INSERT OR REPLACE INTO videos
        VALUES (?,?,?,?,?,?,?,?)
        """,
        (vid.server, vid.id, vid.url, vid.title, vid.description, vid.thumbnail, vid.channel[1], vid.channel[0])
        )
        [ (vid.server, vid.id, vid.url, vid.title, vid.description, vid.thumbnail, vid.channel[1], vid.channel[0]) for vid in vids],
        many=True)
    notify("history_changed")

def has_bookmarked_external_playlist(server_id: str, playlist_id:str) -> bool:
    conn = connect_to_db()


@@ 460,44 463,23 @@ def has_playlist(p: PlaylistWrapper) -> bool:
        WHERE server = ? AND playlist_id = ?
        """, (p.inner.server, p.inner.id)).fetchall()
        return len(results) != 0
    return False
def ensure_playlist(playlist: PlaylistWrapper):
    if playlist.type == PlaylistWrapperType.LOCAL:
        if has_playlist(playlist):
            # update playlist data
            conn = connect_to_db()
            execute_sql(conn, """
            UPDATE local_playlists
            SET title = ?, description = ?, thumbnail = ?
            WHERE id = ?
            """,
            (playlist.inner.title, playlist.inner.description, playlist.inner.thumbnail, playlist.inner.id))
        else:
            # insert new playlist
            conn = connect_to_db()
            execute_sql(conn, """
            INSERT INTO local_playlists
            VALUES (?,?,?,?)
            """,
            (playlist.inner.id, playlist.inner.title, playlist.inner.description, playlist.inner.thumbnail))
        # insert new playlist
        conn = connect_to_db()
        execute_sql(conn, """
        INSERT OR REPLACE INTO local_playlists
        VALUES (?,?,?,?)
        """,
        (playlist.inner.id, playlist.inner.title, playlist.inner.description, playlist.inner.thumbnail))
    else:
        if has_playlist(playlist):
            # update playlist data
            conn = connect_to_db()
            execute_sql(conn, """
            UPDATE playlists
            SET url = ?, title = ?, description = ?, channel_id = ?, channel_name = ?, thumbnail = ?
            WHERE server = ?, id = ?
            """,
            (playlist.inner.url, playlist.inner.title, playlist.inner.description, playlist.inner.channel[1], playlist.inner.channel[0], playlist.inner.thumbnail, playlist.inner.server, playlist.inner.id))
        else:
            # insert new playlist
            conn = connect_to_db()
            execute_sql(conn, """
            INSERT INTO playlists
            VALUES (?,?,?,?,?,?,?,?)
            """,
            (playlist.inner.server, playlist.inner.id, playlist.inner.url, playlist.inner.title, playlist.inner.description,playlist.inner.channel[1], playlist.inner.channel[0], playlist.inner.thumbnail))
        # insert new playlist
        conn = connect_to_db()
        execute_sql(conn, """
        INSERT OR REPLACE INTO playlists
        VALUES (?,?,?,?,?,?,?,?)
        """,
        (playlist.inner.server, playlist.inner.id, playlist.inner.url, playlist.inner.title, playlist.inner.description,playlist.inner.channel[1], playlist.inner.channel[0], playlist.inner.thumbnail))
    notify("playlists_changed")
def ensure_bookmark_external_playlist(playlist: Playlist):
    wrapped = PlaylistWrapper.from_external(playlist)


@@ 505,7 487,7 @@ def ensure_bookmark_external_playlist(playlist: Playlist):
    ensure_playlist(wrapped)
    conn = connect_to_db()
    execute_sql(conn, """
    INSERT INTO bookmarked_playlists
    INSERT OR REPLACE INTO bookmarked_playlists
    VALUES (?,?)
    """,
    (playlist.server, playlist.id))


@@ 529,6 511,10 @@ def new_local_playlist(name: str, description: str, videos=[]) -> int:
    VALUES (?,?,?,?)
    """,
    (id, name, description, None))
    index = 0
    for vid in videos:
        add_to_local_playlist(id, vid, index)
        index += 1
    notify("playlists_changed")
    return id
def ensure_delete_local_playlist(playlist_id:int):


@@ 551,7 537,8 @@ def get_playlists() -> list[PlaylistWrapper]:
    FROM local_playlists
    """).fetchall()
    for collection in local:
        p = LocalPlaylist(collection[0], collection[2], collection[1])
        p = LocalPlaylist(collection[0], collection[1], collection[2])
        p.thumbnail = collection[3]
        results.append(PlaylistWrapper.from_local(p))
    # get all bookmark playlists
    glob = conn.execute("""


@@ 563,13 550,20 @@ def get_playlists() -> list[PlaylistWrapper]:
        p = Playlist(collection[0], collection[1], collection[2], collection[3], (collection[5], collection[4]), collection[6])
        results.append(PlaylistWrapper.from_external(p))
    return results
def add_to_local_playlist(playlist_id:int, vid):
def add_to_local_playlist(playlist_id:int, vid, pos=None):
    ensure_video(vid)
    conn = connect_to_db()
    if pos is None:
        vids = conn.execute("""
        SELECT server, videos.id, url, title, description, thumbnail, channel_id, channel_name
        FROM local_playlist_content, videos
        WHERE local_playlist_content.id = ? AND local_playlist_content.video_id = videos.id AND local_playlist_content.video_server = videos.server
        """, (playlist_id,)).fetchall()
        pos = len(vids)
    execute_sql(conn, """
    INSERT INTO local_playlist_content
    VALUES (?, ?, ?)
    """, (playlist_id, vid.id, vid.server))
    VALUES (?, ?, ?, ?)
    """, (playlist_id, vid.id, vid.server, pos))
    notify("playlists_changed")
def get_local_playlist(playlist_id:int) -> LocalPlaylist:
    conn = connect_to_db()


@@ 584,16 578,26 @@ def get_local_playlist(playlist_id:int) -> LocalPlaylist:
    SELECT server, videos.id, url, title, description, thumbnail, channel_id, channel_name
    FROM local_playlist_content, videos
    WHERE local_playlist_content.id = ? AND local_playlist_content.video_id = videos.id AND local_playlist_content.video_server = videos.server
    ORDER BY position
    """, (playlist_id,)).fetchall()
    p.content = [ Video(d[0], d[2], d[1], d[3], (d[7], d[6]), d[4], d[5]) for d in vids ]
    return p
def set_local_playlist_thumbnail(playlist_id: int, thumb: str):
    conn = connect_to_db()
    execute_sql(conn, """
    UPDATE local_playlists
    SET thumbnail = ?
    WHERE id = ?
    """, (thumb, playlist_id))
    notify("playlists_changed")

def add_to_history(vid):
def add_to_history(vid, uts=None):
    """
    Takes :Video type and adds the video to the history
    automatically adds a uts to the video
    """
    uts = datetime.now().timestamp()
    if uts == None:
        uts = datetime.now().timestamp()
    ensure_video(vid)
    conn = connect_to_db()
    execute_sql(conn, """


@@ 601,6 605,20 @@ def add_to_history(vid):
    VALUES (?, ?, ?)
    """,
    (uts, vid.id, vid.server))
    notify("history_changed")
def add_history_items(items:list[(Video, int)]):
    # NOTE: you have to ensure the videos exist yourself
    conn = connect_to_db()
    execute_sql(
        conn,
        """
        INSERT INTO history
        VALUES (?, ?, ?)
        """,
        [(d[1], d[0].id, d[0].server) for d in items],
        many=True)
    notify("history_changed")

def get_history():
    """
    Returns a list of (Video, uts) tuples


@@ 612,3 630,50 @@ def get_history():
    WHERE history.video_id = id AND history.video_server = server
    """).fetchall()
    return [ (Video(d[1], d[3], d[2], d[4], (d[8], d[7]), d[5], d[6]), d[0]) for d in results ]

def get_cached_feed():
    """
    Returns a list of (Video, uts) tuples
    """
    conn = connect_to_db()
    results = conn.execute("""
    SELECT timestamp, server, id, url, title, description, thumbnail, channel_id, channel_name
    FROM news,videos
    WHERE news.video_id = id AND news.video_server = server
    """).fetchall()
    return [ (Video(d[1], d[3], d[2], d[4], (d[8], d[7]), d[5], d[6]), d[0]) for d in results ]
def clear_cached_feed():
    conn = connect_to_db()
    execute_sql(conn, """
    DELETE FROM news
    """)
def update_cached_feed(ls: list[(Video, int)]):
    uts = datetime.now().timestamp()
    conn = connect_to_db()
    # update last refresh time
    # stored in app settings
    # code same as set_app_setting
    # but the notify's in the normal function would cause an infinite loop
    app_conf = get_app_settings()
    execute_sql(
        conn,
        """
        INSERT OR REPLACE INTO appconf
        VALUES (?,?)
        """, ("news-feed-refresh", uts))
    # update cached items
    for entry in ls:
        vid = entry[0]
        uts = entry[1]
        ensure_video(vid)
        execute_sql(conn, """
        INSERT INTO news
        VALUES (?, ?, ?)
        """,
        (uts, vid.id, vid.server))
def get_last_feed_refresh() -> int:
    conn = connect_to_db()
    app_conf = get_app_settings()
    if "news-feed-refresh" in app_conf:
        return app_conf["news-feed-refresh"]
    return None

M melon/models/callbacks.py => melon/models/callbacks.py +12 -0
@@ 12,6 12,16 @@ callbacks = {
    "history_changed": {},
    "settings_changed": {}
}
frozen = False
def freeze():
    global frozen
    frozen = True
def unfreeze():
    global frozen
    frozen = False
    # notify everything - to refresh everything we might have missed
    for key in callbacks:
        notify(key)
def register_callback(target: str, callback_id:str, function):
    if not target in callbacks:
        return


@@ 22,6 32,8 @@ def unregister_callback(target: str, callback_id: str):
    if callback_id in callbacks[target]:
        callbacks[target].pop(callback_id)
def notify(target:str):
    if frozen:
        return
    if not target in callbacks:
        return
    for _, func in callbacks[target].items():

M melon/servers/__init__.py => melon/servers/__init__.py +3 -1
@@ 1,6 1,7 @@
from enum import Flag,Enum,auto
from abc import ABC,abstractmethod
from melon.servers.loader import server_finder
from melon.import_providers import ImportProvider

REQUESTS_TIMEOUT = 5



@@ 235,10 236,11 @@ class Server(ABC):
        """
        pass
    @abstractmethod
    @abstractmethod
    def get_video_streams(self, vid:str) -> list[Stream]:
        """
        Returns a list of available streams
        i.e hd, 720p
        """
        return []
    def get_import_providers(self) -> list[ImportProvider]:
        return []

M melon/widgets/feeditem.py => melon/widgets/feeditem.py +10 -2
@@ 25,13 25,21 @@ class AdaptiveFeedItem(Adw.ActionRow):
            if show_preview:
                pixbuf = pixbuf_from_url(resource.thumbnail)
            self.set_title(resource.title.replace("&","&"))
            self.set_subtitle(resource.description.replace("&","&"))
            pad = ""
            if len(resource.description) > 80:
                pad = "..."
            sub = resource.description[:80] + pad
            self.set_subtitle(sub.replace("&","&"))
            self.set_action_name("win.browse_playlist")
        elif isinstance(resource, Channel):
            if show_preview:
                pixbuf = pixbuf_from_url(resource.avatar)
            self.set_title(resource.name.replace("&","&"))
            self.set_subtitle(resource.bio.replace("&","&"))
            pad = ""
            if len(resource.bio) > 80:
                pad = "..."
            sub = resource.bio[:80] + pad
            self.set_subtitle(sub.replace("&","&"))
            self.set_action_name("win.browse_channel")
        self.preview = Adw.Avatar()
        self.preview.set_size(48)

M melon/window.py => melon/window.py +5 -0
@@ 12,6 12,7 @@ from melon.browse.playlist import BrowsePlaylistScreen
from melon.browse.search import GlobalSearchScreen
from melon.servers.utils import get_servers_list, get_server_instance
from melon.settings import SettingsScreen
from melon.importer import ImporterScreen
from melon.player import PlayerScreen
from melon.widgets.simpledialog import SimpleDialog
from melon.playlist import LocalPlaylistScreen


@@ 89,6 90,9 @@ class MainWindow(Adw.ApplicationWindow):
    # opens the setting panel
    def open_settings(self,action,prefs):
        self.view.push(SettingsScreen())
    # opens the data import panel
    def open_import(self,action,prefs):
        self.view.push(ImporterScreen())
    # navigate back to the home screen
    def go_home(self,action,prefs):
        self.view.pop_to_page(self.home)


@@ 114,6 118,7 @@ class MainWindow(Adw.ApplicationWindow):
        # Add actions
        self.reg_action("about", self.open_about)
        self.reg_action("prefs", self.open_settings)
        self.reg_action("import", self.open_import)
        self.reg_action("browse", self.open_browse)
        self.reg_action("home", self.go_home)
        self.reg_action("global_search", self.open_global_search)