WIP
This commit is contained in:
@@ -65,3 +65,6 @@ def main():
|
||||
|
||||
app = SublimeMusicApp(Path(config_file))
|
||||
app.run(unknown_args)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
@@ -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(
|
||||
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user