Major GTK refactoring to use GTK Application and actions
This commit is contained in:
@@ -1,6 +1,12 @@
|
|||||||
libremsonic
|
libremsonic
|
||||||
===========
|
===========
|
||||||
|
|
||||||
A \*sonic client for the Librem 5.
|
A \*sonic client for the Linux Desktop.
|
||||||
|
|
||||||
Built using Python and GTK+.
|
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.
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
#! /usr/bin/env python3
|
#! /usr/bin/env python3
|
||||||
import asyncio
|
|
||||||
import threading
|
import threading
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
@@ -8,17 +7,9 @@ import gi
|
|||||||
gi.require_version('Gtk', '3.0')
|
gi.require_version('Gtk', '3.0')
|
||||||
from gi.repository import GLib, Gio, Gtk
|
from gi.repository import GLib, Gio, Gtk
|
||||||
|
|
||||||
from .ui import MainWindow
|
from .ui import LibremsonicApp
|
||||||
from .server import Server
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
server = Server(name='ohea',
|
app = LibremsonicApp()
|
||||||
hostname='https://airsonic.the-evans.family',
|
app.run(sys.argv)
|
||||||
username=sys.argv[1],
|
|
||||||
password=sys.argv[2])
|
|
||||||
|
|
||||||
win = MainWindow(server)
|
|
||||||
win.connect("destroy", Gtk.main_quit)
|
|
||||||
win.show_all()
|
|
||||||
Gtk.main()
|
|
||||||
|
@@ -1 +1,4 @@
|
|||||||
|
"""
|
||||||
|
This module defines a stateless server which interops with the Subsonic API.
|
||||||
|
"""
|
||||||
from .server import Server
|
from .server import Server
|
||||||
|
@@ -6,11 +6,18 @@ from dateutil import parser
|
|||||||
|
|
||||||
def _from_json(cls, data):
|
def _from_json(cls, data):
|
||||||
"""
|
"""
|
||||||
Approach for deserialization here:
|
Converts data from a JSON parse into Python data structures.
|
||||||
https://stackoverflow.com/a/40639688/2319844
|
|
||||||
|
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
|
# 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):
|
if isinstance(cls, typing.ForwardRef):
|
||||||
cls = cls._evaluate(globals(), locals())
|
cls = cls._evaluate(globals(), locals())
|
||||||
|
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import requests
|
import requests
|
||||||
from asyncio import sleep
|
from typing import List, Optional, Dict
|
||||||
from typing import List, Optional, Dict, Awaitable
|
|
||||||
|
|
||||||
from .api_objects import (SubsonicResponse, License, MusicFolder, Indexes,
|
from .api_objects import (SubsonicResponse, License, MusicFolder, Indexes,
|
||||||
AlbumInfo, ArtistInfo, VideoInfo, File, Album,
|
AlbumInfo, ArtistInfo, VideoInfo, File, Album,
|
||||||
@@ -34,7 +33,7 @@ class Server:
|
|||||||
def _make_url(self, endpoint: str) -> str:
|
def _make_url(self, endpoint: str) -> str:
|
||||||
return f'{self.hostname}/rest/{endpoint}.view'
|
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
|
Make a post to a *Sonic REST API. Handle all types of errors including
|
||||||
*Sonic ``<error>`` responses.
|
*Sonic ``<error>`` responses.
|
||||||
@@ -55,6 +54,8 @@ class Server:
|
|||||||
if not subsonic_response:
|
if not subsonic_response:
|
||||||
raise Exception('Fail!')
|
raise Exception('Fail!')
|
||||||
|
|
||||||
|
# Debug
|
||||||
|
# TODO: logging
|
||||||
print(subsonic_response)
|
print(subsonic_response)
|
||||||
|
|
||||||
response = SubsonicResponse.from_json(subsonic_response)
|
response = SubsonicResponse.from_json(subsonic_response)
|
||||||
@@ -65,11 +66,11 @@ class Server:
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
async def ping(self) -> SubsonicResponse:
|
def ping(self) -> SubsonicResponse:
|
||||||
"""
|
"""
|
||||||
Used to test connectivity with the server.
|
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:
|
def get_license(self) -> License:
|
||||||
"""
|
"""
|
||||||
|
@@ -1 +1 @@
|
|||||||
from .main import MainWindow
|
from .app import LibremsonicApp
|
||||||
|
@@ -1,44 +1,16 @@
|
|||||||
import gi
|
import gi
|
||||||
|
import sys
|
||||||
|
|
||||||
gi.require_version('Gtk', '3.0')
|
gi.require_version('Gtk', '3.0')
|
||||||
from gi.repository import Gio, Gtk
|
from gi.repository import Gio, Gtk
|
||||||
|
|
||||||
|
|
||||||
class AlbumsPanel(Gtk.Box):
|
class AlbumsPanel(Gtk.Box):
|
||||||
"""Defines the albums panel."""
|
"""Defines the albums panel."""
|
||||||
def __init__(self, server):
|
|
||||||
|
def __init__(self):
|
||||||
Gtk.Container.__init__(self)
|
Gtk.Container.__init__(self)
|
||||||
|
|
||||||
albums = Gtk.Label('Albums')
|
albums = Gtk.Label('Albums')
|
||||||
|
|
||||||
self.add(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
|
|
||||||
|
47
libremsonic/ui/app.py
Normal file
47
libremsonic/ui/app.py
Normal file
@@ -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()
|
16
libremsonic/ui/configure_servers.py
Normal file
16
libremsonic/ui/configure_servers.py
Normal file
@@ -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()
|
@@ -5,11 +5,11 @@ from gi.repository import Gio, Gtk
|
|||||||
from .albums import AlbumsPanel
|
from .albums import AlbumsPanel
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(Gtk.Window):
|
class MainWindow(Gtk.ApplicationWindow):
|
||||||
"""Defines the main window for LibremSonic."""
|
"""Defines the main window for LibremSonic."""
|
||||||
|
|
||||||
def __init__(self, server):
|
def __init__(self, *args, **kwargs):
|
||||||
Gtk.Window.__init__(self, title="LibremSonic")
|
super().__init__(*args, **kwargs)
|
||||||
self.set_default_size(400, 200)
|
self.set_default_size(400, 200)
|
||||||
|
|
||||||
artists = Gtk.Label('Artists')
|
artists = Gtk.Label('Artists')
|
||||||
@@ -18,14 +18,15 @@ class MainWindow(Gtk.Window):
|
|||||||
|
|
||||||
# Create the stack
|
# Create the stack
|
||||||
stack = self.create_stack(
|
stack = self.create_stack(
|
||||||
Albums=AlbumsPanel(server),
|
Albums=AlbumsPanel(),
|
||||||
Artists=artists,
|
Artists=artists,
|
||||||
Playlists=playlists,
|
Playlists=playlists,
|
||||||
More=more,
|
More=more,
|
||||||
)
|
)
|
||||||
stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT)
|
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)
|
self.add(stack)
|
||||||
|
|
||||||
def create_stack(self, **kwargs):
|
def create_stack(self, **kwargs):
|
||||||
@@ -67,8 +68,14 @@ class MainWindow(Gtk.Window):
|
|||||||
def create_menu(self):
|
def create_menu(self):
|
||||||
self.menu = Gtk.PopoverMenu()
|
self.menu = Gtk.PopoverMenu()
|
||||||
|
|
||||||
|
menu_items = [
|
||||||
|
('app.configure_servers', Gtk.ModelButton('Configure Servers')),
|
||||||
|
]
|
||||||
|
|
||||||
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
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)
|
self.menu.add(vbox)
|
||||||
|
|
||||||
return self.menu
|
return self.menu
|
||||||
|
Reference in New Issue
Block a user