From 4a61a71ed801ecf69c5dd998b696d5a2f5c64c9d Mon Sep 17 00:00:00 2001 From: Sumner Evans Date: Tue, 4 Jun 2019 20:38:31 -0600 Subject: [PATCH] Major GTK refactoring to use GTK Application and actions --- README.rst | 8 ++++- libremsonic/__main__.py | 15 ++------- libremsonic/server/__init__.py | 3 ++ libremsonic/server/api_objects.py | 13 ++++++-- libremsonic/server/server.py | 11 ++++--- libremsonic/ui/__init__.py | 2 +- libremsonic/ui/albums.py | 36 +++------------------- libremsonic/ui/app.py | 47 +++++++++++++++++++++++++++++ libremsonic/ui/configure_servers.py | 16 ++++++++++ libremsonic/ui/main.py | 19 ++++++++---- 10 files changed, 110 insertions(+), 60 deletions(-) create mode 100644 libremsonic/ui/app.py create mode 100644 libremsonic/ui/configure_servers.py diff --git a/README.rst b/README.rst index 6a33b32..cb8de22 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,12 @@ libremsonic =========== -A \*sonic client for the Librem 5. +A \*sonic client for the Linux Desktop. Built using Python and GTK+. + +Design Decisions +================ + +- The ``server`` module is stateless. The only thing that it does is allow the + module's user to query the Airsonic server via the API. diff --git a/libremsonic/__main__.py b/libremsonic/__main__.py index 452e0f3..9ea60b1 100644 --- a/libremsonic/__main__.py +++ b/libremsonic/__main__.py @@ -1,5 +1,4 @@ #! /usr/bin/env python3 -import asyncio import threading import argparse import sys @@ -8,17 +7,9 @@ import gi gi.require_version('Gtk', '3.0') from gi.repository import GLib, Gio, Gtk -from .ui import MainWindow -from .server import Server +from .ui import LibremsonicApp def main(): - server = Server(name='ohea', - hostname='https://airsonic.the-evans.family', - username=sys.argv[1], - password=sys.argv[2]) - - win = MainWindow(server) - win.connect("destroy", Gtk.main_quit) - win.show_all() - Gtk.main() + app = LibremsonicApp() + app.run(sys.argv) diff --git a/libremsonic/server/__init__.py b/libremsonic/server/__init__.py index b7f2cf5..3502b79 100644 --- a/libremsonic/server/__init__.py +++ b/libremsonic/server/__init__.py @@ -1 +1,4 @@ +""" +This module defines a stateless server which interops with the Subsonic API. +""" from .server import Server diff --git a/libremsonic/server/api_objects.py b/libremsonic/server/api_objects.py index 4f12672..4873019 100644 --- a/libremsonic/server/api_objects.py +++ b/libremsonic/server/api_objects.py @@ -6,11 +6,18 @@ from dateutil import parser def _from_json(cls, data): """ - Approach for deserialization here: - https://stackoverflow.com/a/40639688/2319844 + Converts data from a JSON parse into Python data structures. + + Arguments: + + cls: the template class to deserialize into + data: the data to deserialize to the class """ + # Approach for deserialization here: + # https://stackoverflow.com/a/40639688/2319844 + # If it's a forward reference, evaluate it to figure out the actual - # type. + # type. This allows for types that have to be put into a string. if isinstance(cls, typing.ForwardRef): cls = cls._evaluate(globals(), locals()) diff --git a/libremsonic/server/server.py b/libremsonic/server/server.py index c07a512..daacad3 100644 --- a/libremsonic/server/server.py +++ b/libremsonic/server/server.py @@ -1,6 +1,5 @@ import requests -from asyncio import sleep -from typing import List, Optional, Dict, Awaitable +from typing import List, Optional, Dict from .api_objects import (SubsonicResponse, License, MusicFolder, Indexes, AlbumInfo, ArtistInfo, VideoInfo, File, Album, @@ -34,7 +33,7 @@ class Server: def _make_url(self, endpoint: str) -> str: return f'{self.hostname}/rest/{endpoint}.view' - async def _post(self, url, **params) -> SubsonicResponse: + def _post(self, url, **params) -> SubsonicResponse: """ Make a post to a *Sonic REST API. Handle all types of errors including *Sonic ```` responses. @@ -55,6 +54,8 @@ class Server: if not subsonic_response: raise Exception('Fail!') + # Debug + # TODO: logging print(subsonic_response) response = SubsonicResponse.from_json(subsonic_response) @@ -65,11 +66,11 @@ class Server: return response - async def ping(self) -> SubsonicResponse: + def ping(self) -> SubsonicResponse: """ Used to test connectivity with the server. """ - return await self._post(self._make_url('ping')) + return self._post(self._make_url('ping')) def get_license(self) -> License: """ diff --git a/libremsonic/ui/__init__.py b/libremsonic/ui/__init__.py index bb513f1..b09d954 100644 --- a/libremsonic/ui/__init__.py +++ b/libremsonic/ui/__init__.py @@ -1 +1 @@ -from .main import MainWindow +from .app import LibremsonicApp diff --git a/libremsonic/ui/albums.py b/libremsonic/ui/albums.py index 41671b1..6eb8676 100644 --- a/libremsonic/ui/albums.py +++ b/libremsonic/ui/albums.py @@ -1,44 +1,16 @@ import gi +import sys + gi.require_version('Gtk', '3.0') from gi.repository import Gio, Gtk class AlbumsPanel(Gtk.Box): """Defines the albums panel.""" - def __init__(self, server): + + def __init__(self): Gtk.Container.__init__(self) albums = Gtk.Label('Albums') self.add(albums) - - def create_stack(self, **kwargs): - stack = Gtk.Stack() - for name, child in kwargs.items(): - stack.add_titled(child, name.lower(), name) - return stack - - def create_headerbar(self, stack): - """ - Configure the header bar for the window. - """ - header = Gtk.HeaderBar() - header.set_show_close_button(True) - header.props.title = 'LibremSonic' - - # Search - search = Gtk.SearchEntry() - header.pack_start(search) - - # Stack switcher - switcher = Gtk.StackSwitcher(stack=stack) - header.set_custom_title(switcher) - - # Menu button - button = Gtk.MenuButton() - icon = Gio.ThemedIcon(name="open-menu-symbolic") - image = Gtk.Image.new_from_gicon(icon, Gtk.IconSize.BUTTON) - button.add(image) - header.pack_end(button) - - return header diff --git a/libremsonic/ui/app.py b/libremsonic/ui/app.py new file mode 100644 index 0000000..bcb0806 --- /dev/null +++ b/libremsonic/ui/app.py @@ -0,0 +1,47 @@ +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gio, Gtk + +from .main import MainWindow +from .configure_servers import ConfigureServersDialog + + +class LibremsonicApp(Gtk.Application): + def __init__(self, *args, **kwargs): + super().__init__( + *args, + application_id="com.sumnerevans.libremsonic", + **kwargs, + ) + self.window = None + + # TODO load this from the config file + self.current_server = None + + def do_startup(self): + Gtk.Application.do_startup(self) + + action = Gio.SimpleAction.new('configure_servers', None) + action.connect('activate', self.on_configure_servers) + self.add_action(action) + + def do_activate(self): + # We only allow a single window and raise any existing ones + if not self.window: + # Windows are associated with the application + # when the last one is closed the application shuts down + self.window = MainWindow(application=self, title="LibremSonic") + + self.window.show_all() + self.window.present() + + if not self.current_server: + self.show_configure_servers_dialog() + + def on_configure_servers(self, action, param): + self.show_configure_servers_dialog() + + def show_configure_servers_dialog(self): + dialog = ConfigureServersDialog(self.window) + dialog.run() + dialog.destroy() diff --git a/libremsonic/ui/configure_servers.py b/libremsonic/ui/configure_servers.py new file mode 100644 index 0000000..fa83747 --- /dev/null +++ b/libremsonic/ui/configure_servers.py @@ -0,0 +1,16 @@ +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + + +class ConfigureServersDialog(Gtk.Dialog): + def __init__(self, parent): + Gtk.Dialog.__init__(self, 'Configure Servers', parent, 0, + ('Done', Gtk.ResponseType.NONE)) + + self.set_default_size(400, 400) + label = Gtk.Label('ohea') + + box = self.get_content_area() + box.add(label) + self.show_all() diff --git a/libremsonic/ui/main.py b/libremsonic/ui/main.py index 7fc6c39..21e06ff 100644 --- a/libremsonic/ui/main.py +++ b/libremsonic/ui/main.py @@ -5,11 +5,11 @@ from gi.repository import Gio, Gtk from .albums import AlbumsPanel -class MainWindow(Gtk.Window): +class MainWindow(Gtk.ApplicationWindow): """Defines the main window for LibremSonic.""" - def __init__(self, server): - Gtk.Window.__init__(self, title="LibremSonic") + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.set_default_size(400, 200) artists = Gtk.Label('Artists') @@ -18,14 +18,15 @@ class MainWindow(Gtk.Window): # Create the stack stack = self.create_stack( - Albums=AlbumsPanel(server), + Albums=AlbumsPanel(), Artists=artists, Playlists=playlists, More=more, ) stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT) - self.set_titlebar(self.create_headerbar(stack)) + titlebar = self.create_headerbar(stack) + self.set_titlebar(titlebar) self.add(stack) def create_stack(self, **kwargs): @@ -67,8 +68,14 @@ class MainWindow(Gtk.Window): def create_menu(self): self.menu = Gtk.PopoverMenu() + menu_items = [ + ('app.configure_servers', Gtk.ModelButton('Configure Servers')), + ] + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - vbox.pack_start(Gtk.Label('Foo'), False, True, 10) + for name, item in menu_items: + item.set_action_name(name) + vbox.pack_start(item, False, True, 10) self.menu.add(vbox) return self.menu