This commit is contained in:
Benjamin Schaaf
2022-01-06 20:40:40 +11:00
parent 0f3e04007f
commit 4ced8e607b
3 changed files with 95 additions and 19 deletions

View File

@@ -65,3 +65,6 @@ def main():
app = SublimeMusicApp(Path(config_file))
app.run(unknown_args)
if __name__ == '__main__':
main()

View File

@@ -19,6 +19,7 @@ except Exception:
tap_imported = False
import gi
gi.require_version('GIRepository', '2.0')
from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk, GIRepository
# Temporary for development
@@ -57,7 +58,7 @@ from .players import PlayerDeviceEvent, PlayerEvent, PlayerManager
from .ui.configure_provider import ConfigureProviderDialog
from .ui.main import MainWindow
from .ui.state import RepeatType, UIState
from .ui.actions import register_action
from .ui.actions import register_action, register_dataclass_actions
from .util import resolve_path
@@ -75,6 +76,8 @@ class SublimeMusicApp(Gtk.Application):
self.connect("shutdown", self.on_app_shutdown)
self._download_progress = {}
player_manager: Optional[PlayerManager] = None
exiting: bool = False
@@ -125,6 +128,7 @@ class SublimeMusicApp(Gtk.Application):
add_action("go-online", self.on_go_online)
add_action("refresh-devices", self.on_refresh_devices)
register_action(self, self.refresh)
register_action(self, self.force_refresh)
add_action(
"update-play-queue-from-server",
@@ -138,10 +142,11 @@ class SublimeMusicApp(Gtk.Application):
self.tap.on("prev_track", self.prev_track)
self.tap.start()
# self.add_accelerator("<Ctrl>F", 'app.play-pause')
self.add_accelerator("space", 'app.play-pause')
self.add_accelerator("Home", 'app.prev-track')
self.add_accelerator("End", 'app.next-track')
# self.set_accels_for_action('app.play-pause', ["<Ctrl>F"])
self.set_accels_for_action('app.play-pause', ["space"])
self.set_accels_for_action('app.prev-track', ["Home"])
self.set_accels_for_action('app.next-track', ["End"])
# self.set_accels_for_action('app.quit', ["space"])
def do_activate(self):
# We only allow a single window and raise any existing ones
@@ -169,6 +174,16 @@ class SublimeMusicApp(Gtk.Application):
register_action(albums, self.albums_select_album, 'select-album')
self.window.insert_action_group('albums', albums)
settings = Gio.SimpleActionGroup()
register_dataclass_actions(settings, self.app_config, after=self._save_and_refresh)
self.window.insert_action_group('settings', settings)
players = Gio.SimpleActionGroup()
register_action(players, self.players_set_option, name='set-str-option', types=(str, str, str))
register_action(players, self.players_set_option, name='set-bool-option', types=(str, str, bool))
register_action(players, self.players_set_option, name='set-int-option', types=(str, str, int))
self.window.insert_action_group('players', players)
# Configure the CSS provider so that we can style elements on the
# window.
css_provider = Gtk.CssProvider()
@@ -606,10 +621,19 @@ class SublimeMusicApp(Gtk.Application):
# for k, v in state_updates.items():
# setattr(self.app_config.state, k, v)
# self.update_window(force=force)
@dbus_propagate()
def refresh(self):
self.update_window(force=False)
@dbus_propagate()
def force_refresh(self):
self.update_window(force=True)
def _save_and_refresh(self):
self.app_config.save()
self.refresh()
def on_notification_closed(self, _):
self.app_config.state.current_notification = None
self.update_window()
@@ -953,8 +977,20 @@ class SublimeMusicApp(Gtk.Application):
self.update_window()
def on_song_download_progress(self, song_id: str, progress: DownloadProgress):
assert self.window
GLib.idle_add(self.window.update_song_download_progress, song_id, progress)
if len(self._download_progress) == 0:
GLib.timeout_add(100, self._propagate_download_progress)
# Amortize progress updates
self._download_progress[song_id] = progress
def _propagate_download_progress(self):
items = list(self._download_progress.items())
self._download_progress = {}
for song_id, progress in items:
self.window.update_song_download_progress(song_id, progress)
return False
def on_app_shutdown(self, app: "SublimeMusicApp"):
self.exiting = True
@@ -994,6 +1030,15 @@ class SublimeMusicApp(Gtk.Application):
self.app_config.state.selected_album_id = album_id
self.update_window()
def players_set_option(self, player: str, option: str, value: Any):
self.app_config.player_config[player][option] = value
if pm := self.player_manager:
pm.change_settings(self.app_config.player_config)
self.app_config.save()
self.update_window()
# ########## HELPER METHODS ########## #
def show_configure_servers_dialog(

View File

@@ -1,7 +1,8 @@
import inspect
import enum
import dataclasses
from typing import Callable, Optional, Tuple, Any, Union
import pathlib
from typing import Callable, Optional, Tuple, Any, Union, List, Type
from gi.repository import Gio, GLib
@@ -10,7 +11,7 @@ NoneType = type(None)
def run_action(widget, name, *args):
# print('run action', name, args)
print('run action', name, args)
group, action = name.split('.')
action_group = widget.get_action_group(group)
@@ -29,24 +30,43 @@ def run_action(widget, name, *args):
action_group.activate_action(action, param)
def register_action(group, fn: Callable, name: Optional[str] = None):
def register_dataclass_actions(group, data, after=None):
fields = dataclasses.fields(type(data))
for field in fields:
if field.name[0] == '_':
continue
def set_field(value, name=field.name):
setattr(data, name, value)
if after:
after()
name = field.name.replace('_', '-')
try:
register_action(group, set_field, name=f'set-{name}', types=(field.type,))
except ValueError:
continue
def register_action(group, fn: Callable, name: Optional[str] = None, types: Tuple[Type] = None):
if name is None:
name = fn.__name__.replace('_', '-')
# Determine the type from the signature
signature = inspect.signature(fn)
if types is None:
types = tuple(p.annotation for p in signature.parameters.values())
if signature.parameters:
arg_types = tuple(p.annotation for p in signature.parameters.values())
if inspect.Parameter.empty in arg_types:
if inspect.Parameter.empty in types:
raise ValueError('Missing parameter annotation for action ' + name)
has_multiple = len(arg_types) > 1
has_multiple = len(types) > 1
if has_multiple:
param_type = Tuple.__getitem__(arg_types)
param_type = Tuple.__getitem__(types)
else:
param_type = arg_types[0]
param_type = types[0]
type_str = variant_type_from_python(param_type)
var_type = GLib.VariantType(type_str)
@@ -59,7 +79,7 @@ def register_action(group, fn: Callable, name: Optional[str] = None):
action = Gio.SimpleAction.new(name, var_type)
def activate(action, param):
if param:
if param is not None:
if has_multiple:
fn(*build(param.unpack()))
else:
@@ -85,6 +105,8 @@ def variant_type_from_python(py_type: type) -> str:
return 's'
elif py_type is Any:
return 'v'
elif isinstance(py_type, type) and issubclass(py_type, pathlib.PurePath):
return 's'
elif isinstance(py_type, type) and issubclass(py_type, enum.Enum):
return variant_type_from_python(type(list(py_type)[0].value))
elif dataclasses.is_dataclass(py_type):
@@ -119,7 +141,9 @@ def variant_type_from_python(py_type: type) -> str:
def unbuilt_type(py_type: type) -> type:
if isinstance(py_type, type) and issubclass(py_type, enum.Enum):
if isinstance(py_type, type) and issubclass(py_type, pathlib.PurePath):
return str
elif isinstance(py_type, type) and issubclass(py_type, enum.Enum):
return type(list(py_type)[0].value)
elif dataclasses.is_dataclass(py_type):
return tuple
@@ -132,7 +156,9 @@ def generate_build_function(py_type: type) -> Optional[Callable]:
unpacking a GVariant. When no reconstruction is needed None is returned.
"""
if isinstance(py_type, type) and issubclass(py_type, enum.Enum):
if isinstance(py_type, type) and issubclass(py_type, pathlib.PurePath):
return py_type
elif isinstance(py_type, type) and issubclass(py_type, enum.Enum):
return py_type
elif dataclasses.is_dataclass(py_type):
types = tuple(f.type for f in dataclasses.fields(py_type))
@@ -214,6 +240,8 @@ from gi._gi import variant_type_from_string
def _create_variant(type_str, value):
if isinstance(value, enum.Enum):
value = value.value
elif isinstance(value, pathlib.PurePath):
value = str(value)
elif dataclasses.is_dataclass(type(value)):
fields = dataclasses.fields(type(value))
value = tuple(getattr(value, field.name) for field in fields)