WIP
This commit is contained in:
@@ -65,3 +65,6 @@ def main():
|
|||||||
|
|
||||||
app = SublimeMusicApp(Path(config_file))
|
app = SublimeMusicApp(Path(config_file))
|
||||||
app.run(unknown_args)
|
app.run(unknown_args)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
@@ -19,6 +19,7 @@ except Exception:
|
|||||||
tap_imported = False
|
tap_imported = False
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
|
gi.require_version('GIRepository', '2.0')
|
||||||
from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk, GIRepository
|
from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk, GIRepository
|
||||||
|
|
||||||
# Temporary for development
|
# Temporary for development
|
||||||
@@ -57,7 +58,7 @@ from .players import PlayerDeviceEvent, PlayerEvent, PlayerManager
|
|||||||
from .ui.configure_provider import ConfigureProviderDialog
|
from .ui.configure_provider import ConfigureProviderDialog
|
||||||
from .ui.main import MainWindow
|
from .ui.main import MainWindow
|
||||||
from .ui.state import RepeatType, UIState
|
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
|
from .util import resolve_path
|
||||||
|
|
||||||
|
|
||||||
@@ -75,6 +76,8 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
|
|
||||||
self.connect("shutdown", self.on_app_shutdown)
|
self.connect("shutdown", self.on_app_shutdown)
|
||||||
|
|
||||||
|
self._download_progress = {}
|
||||||
|
|
||||||
player_manager: Optional[PlayerManager] = None
|
player_manager: Optional[PlayerManager] = None
|
||||||
exiting: bool = False
|
exiting: bool = False
|
||||||
|
|
||||||
@@ -125,6 +128,7 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
|
|
||||||
add_action("go-online", self.on_go_online)
|
add_action("go-online", self.on_go_online)
|
||||||
add_action("refresh-devices", self.on_refresh_devices)
|
add_action("refresh-devices", self.on_refresh_devices)
|
||||||
|
register_action(self, self.refresh)
|
||||||
register_action(self, self.force_refresh)
|
register_action(self, self.force_refresh)
|
||||||
add_action(
|
add_action(
|
||||||
"update-play-queue-from-server",
|
"update-play-queue-from-server",
|
||||||
@@ -138,10 +142,11 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
self.tap.on("prev_track", self.prev_track)
|
self.tap.on("prev_track", self.prev_track)
|
||||||
self.tap.start()
|
self.tap.start()
|
||||||
|
|
||||||
# self.add_accelerator("<Ctrl>F", 'app.play-pause')
|
# self.set_accels_for_action('app.play-pause', ["<Ctrl>F"])
|
||||||
self.add_accelerator("space", 'app.play-pause')
|
self.set_accels_for_action('app.play-pause', ["space"])
|
||||||
self.add_accelerator("Home", 'app.prev-track')
|
self.set_accels_for_action('app.prev-track', ["Home"])
|
||||||
self.add_accelerator("End", 'app.next-track')
|
self.set_accels_for_action('app.next-track', ["End"])
|
||||||
|
# self.set_accels_for_action('app.quit', ["space"])
|
||||||
|
|
||||||
def do_activate(self):
|
def do_activate(self):
|
||||||
# We only allow a single window and raise any existing ones
|
# 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')
|
register_action(albums, self.albums_select_album, 'select-album')
|
||||||
self.window.insert_action_group('albums', albums)
|
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
|
# Configure the CSS provider so that we can style elements on the
|
||||||
# window.
|
# window.
|
||||||
css_provider = Gtk.CssProvider()
|
css_provider = Gtk.CssProvider()
|
||||||
@@ -606,10 +621,19 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
# for k, v in state_updates.items():
|
# for k, v in state_updates.items():
|
||||||
# setattr(self.app_config.state, k, v)
|
# setattr(self.app_config.state, k, v)
|
||||||
# self.update_window(force=force)
|
# self.update_window(force=force)
|
||||||
|
|
||||||
|
@dbus_propagate()
|
||||||
|
def refresh(self):
|
||||||
|
self.update_window(force=False)
|
||||||
|
|
||||||
@dbus_propagate()
|
@dbus_propagate()
|
||||||
def force_refresh(self):
|
def force_refresh(self):
|
||||||
self.update_window(force=True)
|
self.update_window(force=True)
|
||||||
|
|
||||||
|
def _save_and_refresh(self):
|
||||||
|
self.app_config.save()
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
def on_notification_closed(self, _):
|
def on_notification_closed(self, _):
|
||||||
self.app_config.state.current_notification = None
|
self.app_config.state.current_notification = None
|
||||||
self.update_window()
|
self.update_window()
|
||||||
@@ -953,8 +977,20 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
self.update_window()
|
self.update_window()
|
||||||
|
|
||||||
def on_song_download_progress(self, song_id: str, progress: DownloadProgress):
|
def on_song_download_progress(self, song_id: str, progress: DownloadProgress):
|
||||||
assert self.window
|
if len(self._download_progress) == 0:
|
||||||
GLib.idle_add(self.window.update_song_download_progress, song_id, progress)
|
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"):
|
def on_app_shutdown(self, app: "SublimeMusicApp"):
|
||||||
self.exiting = True
|
self.exiting = True
|
||||||
@@ -994,6 +1030,15 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
self.app_config.state.selected_album_id = album_id
|
self.app_config.state.selected_album_id = album_id
|
||||||
self.update_window()
|
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 ########## #
|
# ########## HELPER METHODS ########## #
|
||||||
def show_configure_servers_dialog(
|
def show_configure_servers_dialog(
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
import inspect
|
import inspect
|
||||||
import enum
|
import enum
|
||||||
import dataclasses
|
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
|
from gi.repository import Gio, GLib
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ NoneType = type(None)
|
|||||||
|
|
||||||
|
|
||||||
def run_action(widget, name, *args):
|
def run_action(widget, name, *args):
|
||||||
# print('run action', name, args)
|
print('run action', name, args)
|
||||||
|
|
||||||
group, action = name.split('.')
|
group, action = name.split('.')
|
||||||
action_group = widget.get_action_group(group)
|
action_group = widget.get_action_group(group)
|
||||||
@@ -29,24 +30,43 @@ def run_action(widget, name, *args):
|
|||||||
action_group.activate_action(action, param)
|
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:
|
if name is None:
|
||||||
name = fn.__name__.replace('_', '-')
|
name = fn.__name__.replace('_', '-')
|
||||||
|
|
||||||
# Determine the type from the signature
|
# Determine the type from the signature
|
||||||
signature = inspect.signature(fn)
|
signature = inspect.signature(fn)
|
||||||
|
if types is None:
|
||||||
|
types = tuple(p.annotation for p in signature.parameters.values())
|
||||||
|
|
||||||
if signature.parameters:
|
if signature.parameters:
|
||||||
arg_types = tuple(p.annotation for p in signature.parameters.values())
|
if inspect.Parameter.empty in types:
|
||||||
|
|
||||||
if inspect.Parameter.empty in arg_types:
|
|
||||||
raise ValueError('Missing parameter annotation for action ' + name)
|
raise ValueError('Missing parameter annotation for action ' + name)
|
||||||
|
|
||||||
has_multiple = len(arg_types) > 1
|
has_multiple = len(types) > 1
|
||||||
if has_multiple:
|
if has_multiple:
|
||||||
param_type = Tuple.__getitem__(arg_types)
|
param_type = Tuple.__getitem__(types)
|
||||||
else:
|
else:
|
||||||
param_type = arg_types[0]
|
param_type = types[0]
|
||||||
|
|
||||||
type_str = variant_type_from_python(param_type)
|
type_str = variant_type_from_python(param_type)
|
||||||
var_type = GLib.VariantType(type_str)
|
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)
|
action = Gio.SimpleAction.new(name, var_type)
|
||||||
def activate(action, param):
|
def activate(action, param):
|
||||||
if param:
|
if param is not None:
|
||||||
if has_multiple:
|
if has_multiple:
|
||||||
fn(*build(param.unpack()))
|
fn(*build(param.unpack()))
|
||||||
else:
|
else:
|
||||||
@@ -85,6 +105,8 @@ def variant_type_from_python(py_type: type) -> str:
|
|||||||
return 's'
|
return 's'
|
||||||
elif py_type is Any:
|
elif py_type is Any:
|
||||||
return 'v'
|
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):
|
elif isinstance(py_type, type) and issubclass(py_type, enum.Enum):
|
||||||
return variant_type_from_python(type(list(py_type)[0].value))
|
return variant_type_from_python(type(list(py_type)[0].value))
|
||||||
elif dataclasses.is_dataclass(py_type):
|
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:
|
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)
|
return type(list(py_type)[0].value)
|
||||||
elif dataclasses.is_dataclass(py_type):
|
elif dataclasses.is_dataclass(py_type):
|
||||||
return tuple
|
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.
|
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
|
return py_type
|
||||||
elif dataclasses.is_dataclass(py_type):
|
elif dataclasses.is_dataclass(py_type):
|
||||||
types = tuple(f.type for f in dataclasses.fields(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):
|
def _create_variant(type_str, value):
|
||||||
if isinstance(value, enum.Enum):
|
if isinstance(value, enum.Enum):
|
||||||
value = value.value
|
value = value.value
|
||||||
|
elif isinstance(value, pathlib.PurePath):
|
||||||
|
value = str(value)
|
||||||
elif dataclasses.is_dataclass(type(value)):
|
elif dataclasses.is_dataclass(type(value)):
|
||||||
fields = dataclasses.fields(type(value))
|
fields = dataclasses.fields(type(value))
|
||||||
value = tuple(getattr(value, field.name) for field in fields)
|
value = tuple(getattr(value, field.name) for field in fields)
|
||||||
|
Reference in New Issue
Block a user