This commit is contained in:
Benjamin Schaaf
2022-01-02 20:40:07 +11:00
parent 87d32ddc7f
commit 5c4a29e6ae
16 changed files with 604 additions and 545 deletions

View File

@@ -81,9 +81,9 @@ class AlbumSearchQuery:
:class:`AlbumSearchQuery.Type.GENRE`) return albums of the given genre
"""
class _Genre(Genre):
def __init__(self, name: str):
self.name = name
@dataclass
class Genre:
name: str
class Type(Enum):
"""
@@ -116,7 +116,7 @@ class AlbumSearchQuery:
type: Type
year_range: Tuple[int, int] = this_decade()
genre: Genre = _Genre("Rock")
genre: Genre = Genre("Rock")
_strhash: Optional[str] = None

View File

@@ -19,7 +19,12 @@ except Exception:
tap_imported = False
import gi
from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk
from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk, GIRepository
# Temporary for development
repo = GIRepository.Repository.get_default()
repo.prepend_library_path('../libhandy/_build/src')
repo.prepend_search_path('../libhandy/_build/src')
gi.require_version('Handy', '1')
from gi.repository import Handy
@@ -52,6 +57,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 .util import resolve_path
@@ -83,38 +89,43 @@ class SublimeMusicApp(Gtk.Application):
action.connect("activate", fn)
self.add_action(action)
# Add action for menu items.
add_action("add-new-music-provider", self.on_add_new_music_provider)
add_action("edit-current-music-provider", self.on_edit_current_music_provider)
add_action(
"switch-music-provider", self.on_switch_music_provider, parameter_type="s"
)
add_action(
"remove-music-provider", self.on_remove_music_provider, parameter_type="s"
)
register_action(self, self.change_tab)
# Music provider actions
register_action(self, self.add_new_music_provider)
register_action(self, self.edit_current_music_provider)
register_action(self, self.switch_music_provider)
register_action(self, self.remove_music_provider)
# Connect after we know there's a server configured.
# self.window.connect("notification-closed", self.on_notification_closed)
# self.window.connect("key-press-event", self.on_window_key_press)
# Add actions for player controls
add_action("play-pause", self.on_play_pause)
add_action("next-track", self.on_next_track)
add_action("prev-track", self.on_prev_track)
add_action("repeat-press", self.on_repeat_press)
add_action("shuffle-press", self.on_shuffle_press)
register_action(self, self.seek)
register_action(self, self.play_pause)
register_action(self, self.next_track)
register_action(self, self.prev_track)
register_action(self, self.repeat)
register_action(self, self.shuffle)
register_action(self, self.play_song_action, name='play-song')
# self.window.connect("songs-removed", self.on_songs_removed)
register_action(self, self.select_device)
register_action(self, self.toggle_mute)
register_action(self, self.set_volume)
# Navigation actions.
add_action("play-next", self.on_play_next, parameter_type="as")
add_action("add-to-queue", self.on_add_to_queue, parameter_type="as")
add_action("go-to-album", self.on_go_to_album, parameter_type="s")
add_action("go-to-artist", self.on_go_to_artist, parameter_type="s")
add_action("browse-to", self.browse_to, parameter_type="s")
add_action("go-to-playlist", self.on_go_to_playlist, parameter_type="s")
register_action(self, self.queue_next_songs)
register_action(self, self.queue_songs)
register_action(self, self.go_to_album)
register_action(self, self.go_to_artist)
register_action(self, self.browse_to)
register_action(self, self.go_to_playlist)
add_action("go-online", self.on_go_online)
add_action("refresh-devices", self.on_refresh_devices)
add_action(
"refresh-window",
lambda *a: self.on_refresh_window(None, {}, True),
)
add_action("mute-toggle", self.on_mute_toggle)
register_action(self, self.force_refresh)
add_action(
"update-play-queue-from-server",
lambda a, p: self.update_play_state_from_server(),
@@ -122,11 +133,16 @@ class SublimeMusicApp(Gtk.Application):
if tap_imported:
self.tap = osxmmkeys.Tap()
self.tap.on("play_pause", self.on_play_pause)
self.tap.on("next_track", self.on_next_track)
self.tap.on("prev_track", self.on_prev_track)
self.tap.on("play_pause", self.play_pause)
self.tap.on("next_track", self.next_track)
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')
def do_activate(self):
# We only allow a single window and raise any existing ones
if self.window:
@@ -147,6 +163,12 @@ class SublimeMusicApp(Gtk.Application):
# closed the application shuts down.
self.window = MainWindow(application=self, title="Sublime Music")
albums = Gio.SimpleActionGroup()
register_action(albums, self.albums_set_search_query, 'set-search-query')
register_action(albums, self.albums_set_page, 'set-page')
register_action(albums, self.albums_select_album, 'select-album')
self.window.insert_action_group('albums', albums)
# Configure the CSS provider so that we can style elements on the
# window.
css_provider = Gtk.CssProvider()
@@ -177,18 +199,6 @@ class SublimeMusicApp(Gtk.Application):
AdapterManager.reset(self.app_config, self.on_song_download_progress)
# Connect after we know there's a server configured.
self.window.stack.connect("notify::visible-child", self.on_stack_change)
self.window.connect("song-clicked", self.on_song_clicked)
self.window.connect("songs-removed", self.on_songs_removed)
self.window.connect("refresh-window", self.on_refresh_window)
self.window.connect("notification-closed", self.on_notification_closed)
self.window.connect("go-to", self.on_window_go_to)
self.window.connect("key-press-event", self.on_window_key_press)
self.window.player_manager.connect("seek", self.on_seek)
self.window.player_manager.connect("device-update", self.on_device_update)
# self.window.player_manager.volume.connect("value-changed", self.on_volume_change)
# Configure the players
self.last_play_queue_update = timedelta(0)
self.loading_state = False
@@ -237,7 +247,7 @@ class SublimeMusicApp(Gtk.Application):
self.update_window()
return
GLib.idle_add(self.on_next_track)
GLib.idle_add(self.next_track)
def on_player_event(event: PlayerEvent):
if event.type == PlayerEvent.EventType.PLAY_STATE_CHANGE:
@@ -384,7 +394,7 @@ class SublimeMusicApp(Gtk.Application):
def set_pos_fn(track_id: str, position: float = 0):
if self.app_config.state.playing:
self.on_play_pause()
self.play_pause()
pos_seconds = timedelta(microseconds=position)
self.app_config.state.song_progress = pos_seconds
track_id, occurrence = track_id.split("/")[-2:]
@@ -448,7 +458,7 @@ class SublimeMusicApp(Gtk.Application):
if self.app_config.state.shuffle_on:
song_idx = random.randint(0, len(playlist.songs) - 1)
self.on_song_clicked(
self.play_song(
None,
song_idx,
tuple(s.id for s in playlist.songs),
@@ -499,11 +509,11 @@ class SublimeMusicApp(Gtk.Application):
def play():
if not self.app_config.state.playing:
self.on_play_pause()
self.play_pause()
def pause():
if self.app_config.state.playing:
self.on_play_pause()
self.play_pause()
method_call_map: Dict[str, Dict[str, Any]] = {
"org.mpris.MediaPlayer2": {
@@ -511,10 +521,10 @@ class SublimeMusicApp(Gtk.Application):
"Quit": self.window and self.window.destroy,
},
"org.mpris.MediaPlayer2.Player": {
"Next": self.on_next_track,
"Previous": self.on_prev_track,
"Next": self.next_track,
"Previous": self.prev_track,
"Pause": pause,
"PlayPause": self.on_play_pause,
"PlayPause": self.play_pause,
"Stop": pause,
"Play": play,
"Seek": seek_fn,
@@ -552,10 +562,10 @@ class SublimeMusicApp(Gtk.Application):
def set_shuffle(new_val: GLib.Variant):
if new_val.get_boolean() != self.app_config.state.shuffle_on:
self.on_shuffle_press(None, None)
self.shuffle()
def set_volume(new_val: GLib.Variant):
self.on_volume_change(None, new_val.get_double() * 100)
self.set_volume(new_val.get_double() * 100)
setter_map: Dict[str, Dict[str, Any]] = {
"org.mpris.MediaPlayer2.Player": {
@@ -574,49 +584,52 @@ class SublimeMusicApp(Gtk.Application):
setter(value)
# ########## ACTION HANDLERS ########## #
# @dbus_propagate()
# def on_refresh_window(self, _, state_updates: Dict[str, Any], force: bool = False):
# if settings := state_updates.get("__settings__"):
# for k, v in settings.items():
# setattr(self.app_config, k, v)
# if (offline_mode := settings.get("offline_mode")) is not None:
# AdapterManager.on_offline_mode_change(offline_mode)
# del state_updates["__settings__"]
# self.app_config.save()
# if player_setting := state_updates.get("__player_setting__"):
# player_name, option_name, value = player_setting
# self.app_config.player_config[player_name][option_name] = value
# del state_updates["__player_setting__"]
# if pm := self.player_manager:
# pm.change_settings(self.app_config.player_config)
# self.app_config.save()
# for k, v in state_updates.items():
# setattr(self.app_config.state, k, v)
# self.update_window(force=force)
@dbus_propagate()
def on_refresh_window(self, _, state_updates: Dict[str, Any], force: bool = False):
if settings := state_updates.get("__settings__"):
for k, v in settings.items():
setattr(self.app_config, k, v)
if (offline_mode := settings.get("offline_mode")) is not None:
AdapterManager.on_offline_mode_change(offline_mode)
del state_updates["__settings__"]
self.app_config.save()
if player_setting := state_updates.get("__player_setting__"):
player_name, option_name, value = player_setting
self.app_config.player_config[player_name][option_name] = value
del state_updates["__player_setting__"]
if pm := self.player_manager:
pm.change_settings(self.app_config.player_config)
self.app_config.save()
for k, v in state_updates.items():
setattr(self.app_config.state, k, v)
self.update_window(force=force)
def force_refresh(self):
self.update_window(force=True)
def on_notification_closed(self, _):
self.app_config.state.current_notification = None
self.update_window()
def on_add_new_music_provider(self, *args):
def add_new_music_provider(self):
self.show_configure_servers_dialog()
def on_edit_current_music_provider(self, *args):
def edit_current_music_provider(self):
self.show_configure_servers_dialog(self.app_config.provider.clone())
def on_switch_music_provider(self, _, provider_id: GLib.Variant):
def switch_music_provider(self, provider_id: str):
if self.app_config.state.playing:
self.on_play_pause()
self.play_pause()
self.app_config.save()
self.app_config.current_provider_id = provider_id.get_string()
self.app_config.current_provider_id = provider_id
self.reset_state()
self.app_config.save()
def on_remove_music_provider(self, _, provider_id: GLib.Variant):
provider = self.app_config.providers[provider_id.get_string()]
def remove_music_provider(self, provider_id: str):
provider = self.app_config.providers[provider_id]
confirm_dialog = Gtk.MessageDialog(
transient_for=self.window,
message_type=Gtk.MessageType.WARNING,
@@ -640,17 +653,10 @@ class SublimeMusicApp(Gtk.Application):
confirm_dialog.destroy()
def on_window_go_to(self, win: Any, action: str, value: str):
{
"album": self.on_go_to_album,
"artist": self.on_go_to_artist,
"playlist": self.on_go_to_playlist,
}[action](None, GLib.Variant("s", value))
_inhibit_cookie = None
@dbus_propagate()
def on_play_pause(self, *args):
def play_pause(self):
if self.app_config.state.current_song_index < 0:
return
@@ -675,7 +681,7 @@ class SublimeMusicApp(Gtk.Application):
self.update_window()
def on_next_track(self, *args):
def next_track(self):
if self.app_config.state.current_song is None:
# This may happen due to DBUS, ignore.
return
@@ -701,7 +707,7 @@ class SublimeMusicApp(Gtk.Application):
else:
self.update_window()
def on_prev_track(self, *args):
def prev_track(self):
if self.app_config.state.current_song is None:
# This may happen due to DBUS, ignore.
return
@@ -734,14 +740,14 @@ class SublimeMusicApp(Gtk.Application):
self.update_window()
@dbus_propagate()
def on_repeat_press(self, *args):
def repeat(self):
# Cycle through the repeat types.
new_repeat_type = RepeatType((self.app_config.state.repeat_type.value + 1) % 3)
self.app_config.state.repeat_type = new_repeat_type
self.update_window()
@dbus_propagate()
def on_shuffle_press(self, *args):
def shuffle(self):
if self.app_config.state.shuffle_on:
# Revert to the old play queue.
old_play_queue_copy = self.app_config.state.old_play_queue
@@ -765,8 +771,7 @@ class SublimeMusicApp(Gtk.Application):
self.update_window()
@dbus_propagate()
def on_play_next(self, action: Any, song_ids: GLib.Variant):
song_ids = tuple(song_ids)
def queue_next_songs(self, song_ids: List[str]):
if self.app_config.state.current_song is None:
insert_at = 0
else:
@@ -781,13 +786,13 @@ class SublimeMusicApp(Gtk.Application):
self.update_window()
@dbus_propagate()
def on_add_to_queue(self, action: Any, song_ids: GLib.Variant):
def queue_songs(self, song_ids: List[str]):
song_ids = tuple(song_ids)
self.app_config.state.play_queue += tuple(song_ids)
self.app_config.state.old_play_queue += tuple(song_ids)
self.update_window()
def on_go_to_album(self, action: Any, album_id: GLib.Variant):
def go_to_album(self, album_id: str):
# Switch to the Alphabetical by Name view to guarantee that the album is there.
self.app_config.state.current_album_search_query = AlbumSearchQuery(
AlbumSearchQuery.Type.ALPHABETICAL_BY_NAME,
@@ -796,22 +801,22 @@ class SublimeMusicApp(Gtk.Application):
)
self.app_config.state.current_tab = "albums"
self.app_config.state.selected_album_id = album_id.get_string()
self.app_config.state.selected_album_id = album_id
self.update_window()
def on_go_to_artist(self, action: Any, artist_id: GLib.Variant):
def go_to_artist(self, artist_id: str):
self.app_config.state.current_tab = "artists"
self.app_config.state.selected_artist_id = artist_id.get_string()
self.app_config.state.selected_artist_id = artist_id
self.update_window()
def browse_to(self, action: Any, item_id: GLib.Variant):
def browse_to(self, item_id: str):
self.app_config.state.current_tab = "browse"
self.app_config.state.selected_browse_element_id = item_id.get_string()
self.app_config.state.selected_browse_element_id = item_id
self.update_window()
def on_go_to_playlist(self, action: Any, playlist_id: GLib.Variant):
def go_to_playlist(self, playlist_id: str):
self.app_config.state.current_tab = "playlists"
self.app_config.state.selected_playlist_id = playlist_id.get_string()
self.app_config.state.selected_playlist_id = playlist_id
self.update_window()
def on_go_online(self, *args):
@@ -822,7 +827,7 @@ class SublimeMusicApp(Gtk.Application):
def reset_state(self):
if self.app_config.state.playing:
self.on_play_pause()
self.play_pause()
self.loading_state = True
self.player_manager.reset()
AdapterManager.reset(self.app_config, self.on_song_download_progress)
@@ -831,17 +836,14 @@ class SublimeMusicApp(Gtk.Application):
# Update the window according to the new server configuration.
self.update_window()
def on_stack_change(self, stack: Gtk.Stack, _):
self.app_config.state.current_tab = stack.get_visible_child_name()
def change_tab(self, tab_id: str):
self.app_config.state.current_tab = tab_id
self.update_window()
def on_song_clicked(
self,
win: Any,
song_index: int,
song_queue: Tuple[str, ...],
metadata: Dict[str, Any],
):
def play_song_action(self, song_index: int, song_queue: List[str], metadata: Dict[str, bool]):
if not song_queue:
song_queue = self.app_config.state.play_queue
song_queue = tuple(song_queue)
# Reset the play queue so that we don't ever revert back to the
# previous one.
@@ -886,7 +888,7 @@ class SublimeMusicApp(Gtk.Application):
if self.app_config.state.current_song_index in song_indexes_to_remove:
if len(self.app_config.state.play_queue) == 0:
self.on_play_pause()
self.play_pause()
self.app_config.state.current_song_index = -1
self.update_window()
return
@@ -899,7 +901,7 @@ class SublimeMusicApp(Gtk.Application):
self.save_play_queue()
@dbus_propagate()
def on_seek(self, _, value: float):
def seek(self, value: float):
if not self.app_config.state.current_song or not self.window:
return
@@ -917,14 +919,14 @@ class SublimeMusicApp(Gtk.Application):
self.save_play_queue()
def on_device_update(self, _, device_id: str):
def select_device(self, device_id: str):
assert self.player_manager
if device_id == self.app_config.state.current_device:
return
self.app_config.state.current_device = device_id
if was_playing := self.app_config.state.playing:
self.on_play_pause()
self.play_pause()
self.player_manager.set_current_device_id(self.app_config.state.current_device)
@@ -934,51 +936,22 @@ class SublimeMusicApp(Gtk.Application):
self.update_window()
if was_playing:
self.on_play_pause()
self.play_pause()
if self.dbus_manager:
self.dbus_manager.property_diff()
@dbus_propagate()
def on_mute_toggle(self, *args):
def toggle_mute(self):
self.app_config.state.is_muted = not self.app_config.state.is_muted
self.player_manager.set_muted(self.app_config.state.is_muted)
self.update_window()
@dbus_propagate()
def on_volume_change(self, _, value: float):
assert self.player_manager
def set_volume(self, value: float):
self.app_config.state.volume = value
self.player_manager.set_volume(self.app_config.state.volume)
self.update_window()
def on_window_key_press(self, window: Gtk.Window, event: Gdk.EventKey) -> bool:
# Need to use bitwise & here to see if CTRL is pressed.
if event.keyval == 102 and event.state & Gdk.ModifierType.CONTROL_MASK:
# Ctrl + F
window.search_entry.grab_focus()
return False
# Allow spaces to work in the text entry boxes.
if (
window.search_entry.has_focus()
or window.playlists_panel.playlist_list.new_playlist_entry.has_focus()
):
return False
# Spacebar, home/prev
keymap = {
32: self.on_play_pause,
65360: self.on_prev_track,
65367: self.on_next_track,
}
action = keymap.get(event.keyval)
if action:
action()
return True
return False
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)
@@ -1005,6 +978,23 @@ class SublimeMusicApp(Gtk.Application):
self.dbus_manager.shutdown()
AdapterManager.shutdown()
def albums_set_search_query(self, query: AlbumSearchQuery, sort_direction: str):
self.app_config.state.current_album_search_query = query
self.app_config.state.album_sort_direction = sort_direction
self.app_config.state.album_page = 0
self.app_config.state.selected_album_id = None
self.update_window()
def albums_set_page(self, page: int):
self.app_config.state.album_page = page
self.app_config.state.selected_album_id = None
self.update_window()
def albums_select_album(self, album_id: str):
self.app_config.state.selected_album_id = album_id
self.update_window()
# ########## HELPER METHODS ########## #
def show_configure_servers_dialog(
self,
@@ -1026,7 +1016,7 @@ class SublimeMusicApp(Gtk.Application):
else:
# Switch to the new provider.
if self.app_config.state.playing:
self.on_play_pause()
self.play_pause()
self.app_config.current_provider_id = provider_id
self.app_config.save()
self.update_window(force=True)
@@ -1066,7 +1056,7 @@ class SublimeMusicApp(Gtk.Application):
def do_resume(clear_notification: bool):
assert self.player_manager
if was_playing := self.app_config.state.playing:
self.on_play_pause()
self.play_pause()
self.app_config.state.play_queue = new_play_queue
self.app_config.state.song_progress = play_queue.position
@@ -1078,7 +1068,7 @@ class SublimeMusicApp(Gtk.Application):
self.update_window()
if was_playing:
self.on_play_pause()
self.play_pause()
if prompt_confirm:
# If there's not a significant enough difference in the song state,
@@ -1385,13 +1375,13 @@ class SublimeMusicApp(Gtk.Application):
# There are no songs that can be played. Show a notification that you
# have to go online to play anything and then don't go further.
if was_playing := self.app_config.state.playing:
self.on_play_pause()
self.play_pause()
def go_online_clicked():
self.app_config.state.current_notification = None
self.on_go_online()
if was_playing:
self.on_play_pause()
self.play_pause()
if all(s == SongCacheStatus.NOT_CACHED for s in statuses):
markup = (

256
sublime_music/ui/actions.py Normal file
View File

@@ -0,0 +1,256 @@
import inspect
import enum
import dataclasses
from typing import Callable, Optional, Tuple, Any, Union
from gi.repository import Gio, GLib
NoneType = type(None)
def run_action(widget, name, *args):
group, action = name.split('.')
action_group = widget.get_action_group(group)
if args:
type_str = action_group.get_action_parameter_type(action)
assert type_str
if len(args) > 1:
param = _create_variant(type_str.dup_string(), tuple(args))
else:
param = _create_variant(type_str.dup_string(), args[0])
else:
param = None
action_group.activate_action(action, param)
def register_action(group, fn: Callable, name: Optional[str] = None):
if name is None:
name = fn.__name__.replace('_', '-')
# Determine the type from the signature
signature = inspect.signature(fn)
if signature.parameters:
arg_types = tuple(p.annotation for p in signature.parameters.values())
if inspect.Parameter.empty in arg_types:
raise ValueError('Missing parameter annotation for action ' + name)
has_multiple = len(arg_types) > 1
if has_multiple:
param_type = Tuple.__getitem__(arg_types)
else:
param_type = arg_types[0]
type_str = variant_type_from_python(param_type)
var_type = GLib.VariantType(type_str)
build = generate_build_function(param_type)
if not build:
build = lambda a: a
else:
var_type = None
action = Gio.SimpleAction.new(name, var_type)
def activate(action, param):
if param:
if has_multiple:
fn(*build(param.unpack()))
else:
fn(build(param.unpack()))
else:
fn()
action.connect('activate', activate)
if hasattr(group, 'add_action'):
group.add_action(action)
else:
group.insert(action)
def variant_type_from_python(py_type: type) -> str:
if py_type is bool:
return 'b'
elif py_type is int:
return 'x'
elif py_type is float:
return 'd'
elif py_type is str:
return 's'
elif py_type is Any:
return 'v'
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):
types = (f.type for f in dataclasses.fields(py_type))
return '(' + ''.join(map(variant_type_from_python, types)) + ')'
else:
origin = py_type.__origin__
if origin is list:
assert len(py_type.__args__) == 1
return 'a' + variant_type_from_python(py_type.__args__[0])
elif origin is tuple:
return '(' + ''.join(map(variant_type_from_python, py_type.__args__)) + ')'
elif origin is dict:
assert len(py_type.__args__) == 2
key = variant_type_from_python(py_type.__args__[0])
value = variant_type_from_python(py_type.__args__[1])
return 'a{' + key + value + '}'
elif origin is Union:
non_maybe = [t for t in py_type.__args__ if t is not NoneType]
has_maybe = len(non_maybe) != len(py_type.__args__)
if has_maybe and len(non_maybe) == 1:
return 'm' + variant_type_from_python(non_maybe[0])
return ('m[' if has_maybe else '[') + ''.join(''.join(map(variant_type_from_python, non_maybe))) + ']'
else:
raise ValueError('{} does not have an equivalent'.format(py_type))
def unbuilt_type(py_type: type) -> type:
if 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
return py_type
def generate_build_function(py_type: type) -> Optional[Callable]:
"""
Return a function for reconstructing dataclasses and enumerations after
unpacking a GVariant. When no reconstruction is needed None is returned.
"""
if 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))
tuple_build = generate_build_function(Tuple.__getitem__(types))
if not tuple_build:
return lambda values: py_type(*values)
return lambda values: py_type(*tuple_build(values))
elif hasattr(py_type, '__origin__'):
origin = py_type.__origin__
if origin is list:
assert len(py_type.__args__) == 1
build = generate_build_function(py_type.__args__[0])
if build:
return lambda values: [build(v) for v in values]
elif origin is tuple:
builds = list(map(generate_build_function, py_type.__args__))
if not any(builds):
return None
return lambda values: tuple((build(value) if build else value) for build, value in zip(builds, values))
elif origin is dict:
assert len(py_type.__args__) == 2
build_key = generate_build_function(py_type.__args__[0])
build_value = generate_build_function(py_type.__args__[1])
if not build_key and not build_value:
return None
return lambda values: {
(build_key(key) if build_key else key): (build_value(value) if build_value else value)
for key, value in values.items()}
elif origin is Union:
builds = list(map(generate_build_function, py_type.__args__))
if not any(builds):
return None
unbuilt_types = list(map(unbuilt_type, py_type.__args__))
def build(value):
for bld, type_ in zip(builds, unbuilt_types):
if isinstance(value, type_):
if bld:
return bld(value)
else:
return value
return value
return build
return None
_VARIANT_CONSTRUCTORS = {
'b': GLib.Variant.new_boolean,
'y': GLib.Variant.new_byte,
'n': GLib.Variant.new_int16,
'q': GLib.Variant.new_uint16,
'i': GLib.Variant.new_int32,
'u': GLib.Variant.new_uint32,
'x': GLib.Variant.new_int64,
't': GLib.Variant.new_uint64,
'h': GLib.Variant.new_handle,
'd': GLib.Variant.new_double,
's': GLib.Variant.new_string,
'o': GLib.Variant.new_object_path,
'g': GLib.Variant.new_signature,
'v': GLib.Variant.new_variant,
}
from gi._gi import variant_type_from_string
def _create_variant(type_str, value):
if isinstance(value, enum.Enum):
value = value.value
elif dataclasses.is_dataclass(type(value)):
fields = dataclasses.fields(type(value))
value = tuple(getattr(value, field.name) for field in fields)
vtype = GLib.VariantType(type_str)
if vtype.is_basic():
return _VARIANT_CONSTRUCTORS[type_str](value)
builder = GLib.VariantBuilder.new(vtype)
if value is None:
return builder.end()
if vtype.is_maybe():
builder.add_value(_create_variant(vtype.element().dup_string(), value))
return builder.end()
try:
iter(value)
except TypeError:
raise TypeError("Could not create array, tuple or dictionary entry from non iterable value %s %s" %
(type_str, value))
if vtype.is_tuple() and vtype.n_items() != len(value):
raise TypeError("Tuple mismatches value's number of elements %s %s" % (type_str, value))
if vtype.is_dict_entry() and len(value) != 2:
raise TypeError("Dictionary entries must have two elements %s %s" % (type_str, value))
if vtype.is_array():
element_type = vtype.element().dup_string()
if isinstance(value, dict):
value = value.items()
for i in value:
builder.add_value(_create_variant(element_type, i))
else:
remainer_format = type_str[1:]
for i in value:
dup = variant_type_from_string(remainer_format).dup_string()
builder.add_value(_create_variant(dup, i))
remainer_format = remainer_format[len(dup):]
return builder.end()

View File

@@ -14,8 +14,9 @@ from ..adapters import (
Result,
)
from ..config import AppConfiguration
from ..ui import util
from ..ui.common import AlbumWithSongs, IconButton, LoadError, SpinnerImage, Sizer
from . import util
from .common import AlbumWithSongs, IconButton, LoadError, SpinnerImage, Sizer
from .actions import run_action
COVER_ART_WIDTH = 150
@@ -50,28 +51,12 @@ def _from_str(type_str: str) -> AlbumSearchQuery.Type:
class AlbumsPanel(Handy.Leaflet):
__gsignals__ = {
"song-clicked": (
GObject.SignalFlags.RUN_FIRST,
GObject.TYPE_NONE,
(int, object, object),
),
"refresh-window": (
GObject.SignalFlags.RUN_FIRST,
GObject.TYPE_NONE,
(object, bool),
),
}
current_query: AlbumSearchQuery = AlbumSearchQuery(AlbumSearchQuery.Type.RANDOM)
offline_mode = False
provider_id: Optional[str] = None
populating_genre_combo = False
album_sort_direction: str = "ascending"
# album_page_size: int = 30
album_page: int = 0
grid_pages_count: int = 0
current_albums_result: Result = None
@@ -146,14 +131,6 @@ class AlbumsPanel(Handy.Leaflet):
self.refresh_button.connect("clicked", self.on_refresh_clicked)
actionbar.pack_end(self.refresh_button)
# actionbar.pack_end(Gtk.Label(label="albums per page"))
# self.show_count_dropdown, _ = self.make_combobox(
# ((x, x, True) for x in ("20", "30", "40", "50")),
# self.on_show_count_dropdown_change,
# )
# actionbar.pack_end(self.show_count_dropdown)
# actionbar.pack_end(Gtk.Label(label="Show"))
self.grid_box.pack_start(actionbar, False, False, 0)
# 700 shows ~3 albums
@@ -176,47 +153,8 @@ class AlbumsPanel(Handy.Leaflet):
selection_mode=Gtk.SelectionMode.SINGLE)
self.grid.connect("child-activated", self.on_album_clicked)
# self.grid.connect(
# "song-clicked",
# lambda _, *args: self.emit("song-clicked", *args),
# )
# self.grid.connect(
# "refresh-window",
# lambda _, *args: self.emit("refresh-window", *args),
# )
# self.grid.connect("cover-clicked", self.on_album_clicked)
# self.grid.connect("num-pages-changed", self.on_grid_num_pages_changed)
box.add(self.grid)
bottom_actionbar = Gtk.ActionBar()
# Add the page widget.
page_widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
self.prev_page = IconButton(
"go-previous-symbolic", "Go to the previous page", sensitive=False
)
self.prev_page.connect("clicked", self.on_prev_page_clicked)
page_widget.add(self.prev_page)
page_widget.add(Gtk.Label(label="Page"))
self.page_entry = Gtk.Entry()
self.page_entry.set_width_chars(1)
self.page_entry.set_max_width_chars(1)
self.page_entry.connect("changed", self.on_page_entry_changed)
self.page_entry.connect("insert-text", self.on_page_entry_insert_text)
page_widget.add(self.page_entry)
page_widget.add(Gtk.Label(label="of"))
self.page_count_label = Gtk.Label(label="-")
page_widget.add(self.page_count_label)
self.next_page = IconButton(
"go-next-symbolic", "Go to the next page", sensitive=False
)
self.next_page.connect("clicked", self.on_next_page_clicked)
page_widget.add(self.next_page)
bottom_actionbar.set_center_widget(page_widget)
box.add(bottom_actionbar)
scrolled_window.add(box)
grid_sizer.add(scrolled_window)
self.grid_box.pack_start(grid_sizer, True, True, 0)
@@ -228,8 +166,6 @@ class AlbumsPanel(Handy.Leaflet):
self.album_with_songs = AlbumWithSongs(scroll_contents=True)
self.album_with_songs.get_style_context().add_class("details-panel")
self.album_with_songs.connect("song-clicked", lambda _, *args: self.emit("song-clicked", *args))
def back_clicked(_):
self.set_visible_child(self.grid_box)
self.album_with_songs.connect("back-clicked", back_clicked)
@@ -336,11 +272,8 @@ class AlbumsPanel(Handy.Leaflet):
lambda f: GLib.idle_add(self._albums_loaded, f)
)
if (self.album_sort_direction != app_config.state.album_sort_direction
or self.album_page != app_config.state.album_page):
if self.album_sort_direction != app_config.state.album_sort_direction:
self.album_sort_direction = app_config.state.album_sort_direction
self.album_page = app_config.state.album_page
self._update_albums()
# self.current_query = app_config.state.current_album_search_query
@@ -361,12 +294,8 @@ class AlbumsPanel(Handy.Leaflet):
# Update the page display
if app_config:
self.album_page = app_config.state.album_page
self.refresh_button.set_sensitive(not app_config.offline_mode)
self.prev_page.set_sensitive(self.album_page > 0)
self.page_entry.set_text(str(self.album_page + 1))
# Show/hide the combo boxes.
def show_if(sort_type: Iterable[AlbumSearchQuery.Type], *elements):
for element in elements:
@@ -508,85 +437,50 @@ class AlbumsPanel(Handy.Leaflet):
return None
def on_sort_toggle_clicked(self, _):
self.emit(
"refresh-window",
{
"album_sort_direction": self._get_opposite_sort_dir(
self.album_sort_direction
),
"album_page": 0,
"selected_album_id": None,
},
False,
)
run_action(self, 'albums.set-search-query', self.current_query, self._get_opposite_sort_dir(self.album_sort_direction))
def on_refresh_clicked(self, _):
self.emit("refresh-window", {}, True)
class _Genre(API.Genre):
def __init__(self, name: str):
self.name = name
def on_grid_num_pages_changed(self, grid: Any, pages: int):
self.grid_pages_count = pages
pages_str = str(self.grid_pages_count)
self.page_count_label.set_text(pages_str)
self.next_page.set_sensitive(self.album_page < self.grid_pages_count - 1)
num_digits = len(pages_str)
self.page_entry.set_width_chars(num_digits)
self.page_entry.set_max_width_chars(num_digits)
run_action(self, "app.force-refresh")
def on_type_combo_changed(self, combo: Gtk.ComboBox):
id = self.get_id(combo)
assert id
if id == "alphabetical":
id += "_" + cast(str, self.get_id(self.alphabetical_type_combo))
self.emit_if_not_updating(
"refresh-window",
{
"current_album_search_query": AlbumSearchQuery(
_from_str(id),
self.current_query.year_range,
self.current_query.genre,
),
"album_page": 0,
"selected_album_id": None,
},
False,
)
run_action(
self,
'albums.set-search-query',
AlbumSearchQuery(
_from_str(id),
self.current_query.year_range,
self.current_query.genre,
),
self.album_sort_direction)
def on_alphabetical_type_change(self, combo: Gtk.ComboBox):
id = "alphabetical_" + cast(str, self.get_id(combo))
self.emit_if_not_updating(
"refresh-window",
{
"current_album_search_query": AlbumSearchQuery(
_from_str(id),
self.current_query.year_range,
self.current_query.genre,
),
"album_page": 0,
"selected_album_id": None,
},
False,
)
run_action(
self,
'albums.set-search-query',
AlbumSearchQuery(
_from_str(id),
self.current_query.year_range,
self.current_query.genre,
),
self.album_sort_direction)
def on_genre_change(self, combo: Gtk.ComboBox):
genre = self.get_id(combo)
assert genre
self.emit_if_not_updating(
"refresh-window",
{
"current_album_search_query": AlbumSearchQuery(
self.current_query.type,
self.current_query.year_range,
AlbumsPanel._Genre(genre),
),
"album_page": 0,
"selected_album_id": None,
},
False,
)
run_action(
self,
'albums.set-search-query',
AlbumSearchQuery(
self.current_query.type,
self.current_query.year_range,
AlbumSearchQuery.Genre(genre),
),
self.album_sort_direction)
def on_year_changed(self, entry: Gtk.SpinButton) -> bool:
year = int(entry.get_value())
@@ -596,27 +490,19 @@ class AlbumsPanel(Handy.Leaflet):
else:
new_year_tuple = (year, self.current_query.year_range[1])
self.emit_if_not_updating(
"refresh-window",
{
"current_album_search_query": AlbumSearchQuery(
self.current_query.type, new_year_tuple, self.current_query.genre
),
"album_page": 0,
"selected_album_id": None,
},
False,
)
run_action(
self,
'albums.set-search-query',
AlbumSearchQuery(
self.current_query.type, new_year_tuple, self.current_query.genre
),
self.album_sort_direction)
return False
def on_page_entry_changed(self, entry: Gtk.Entry) -> bool:
if len(text := entry.get_text()) > 0:
self.emit_if_not_updating(
"refresh-window",
{"album_page": int(text) - 1, "selected_album_id": None},
False,
)
run_action(self, 'albums.set-page', int(text) - 1)
return False
def on_page_entry_insert_text(
@@ -633,50 +519,18 @@ class AlbumsPanel(Handy.Leaflet):
return True
return False
def on_prev_page_clicked(self, _):
self.emit_if_not_updating(
"refresh-window",
{"album_page": self.album_page - 1, "selected_album_id": None},
False,
)
def on_next_page_clicked(self, _):
self.emit_if_not_updating(
"refresh-window",
{"album_page": self.album_page + 1, "selected_album_id": None},
False,
)
def on_album_clicked(self, _:Any, child: Gtk.FlowBoxChild):
album = self.albums[child.get_index()]
if self.get_folded() and self.get_visible_child() == self.grid_box:
self.set_visible_child(self.album_container)
self.emit(
"refresh-window",
{"selected_album_id": album.id},
False,
)
def on_show_count_dropdown_change(self, combo: Gtk.ComboBox):
show_count = int(self.get_id(combo) or 30)
self.emit(
"refresh-window",
{"album_page_size": show_count, "album_page": 0},
False,
)
def emit_if_not_updating(self, *args):
if self.updating_query:
return
self.emit(*args)
run_action(self, 'albums.select-album', album.id)
"""
# TODO: REMOVE
class AlbumsGrid(Gtk.Overlay):
"""Defines the albums panel."""
__gsignals__ = {
"cover-clicked": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (object,)),
@@ -1155,3 +1009,4 @@ class AlbumsGrid(Gtk.Overlay):
self.grid_bottom.unselect_all()
self.currently_selected_index = selected_index
"""

View File

@@ -236,11 +236,6 @@ entry.invalid {
min-width: 35px;
}
/* ********** General ********** */
.menu-button {
padding: 5px;
}
/* ********** Search ********** */
#search-results {
min-width: 400px;

View File

@@ -636,10 +636,6 @@ class AlbumsListWithSongs(Gtk.Overlay):
for album in self.albums:
album_with_songs = AlbumWithSongs(show_artist_name=False)
album_with_songs.update(album, app_config, force=force)
album_with_songs.connect(
"song-clicked",
lambda _, *args: self.emit("song-clicked", *args),
)
# album_with_songs.connect("song-selected", self.on_song_selected)
album_with_songs.show_all()
self.box.add(album_with_songs)

View File

@@ -11,16 +11,11 @@ from .icon_button import IconButton
from .load_error import LoadError
from .song_list_column import SongListColumn
from .spinner_image import SpinnerImage
from ..actions import run_action
class AlbumWithSongs(Gtk.Box):
__gsignals__ = {
# "song-selected": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, ()),
"song-clicked": (
GObject.SignalFlags.RUN_FIRST,
GObject.TYPE_NONE,
(int, object, object),
),
"back-clicked": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, ()),
}
@@ -166,8 +161,7 @@ class AlbumWithSongs(Gtk.Box):
margin_bottom=10,
)
selection = self.album_songs.get_selection()
selection.set_mode(Gtk.SelectionMode.MULTIPLE)
selection.set_select_function(lambda _, model, path, current: model[path[0]][0])
selection.set_mode(Gtk.SelectionMode.SINGLE)
# Song status column.
renderer = Gtk.CellRendererPixbuf()
@@ -188,20 +182,19 @@ class AlbumWithSongs(Gtk.Box):
# Event Handlers
# =========================================================================
def on_song_selection_change(self, event: Any):
if not self.album_songs.has_focus():
self.emit("song-selected")
def on_song_selection_change(self, selection: Gtk.TreeSelection):
paths = selection.get_selected_rows()[1]
if not paths:
return
assert len(paths) == 1
self.play_song(paths[0].get_indices()[0], {})
def on_song_activated(self, treeview: Any, idx: Gtk.TreePath, column: Any):
if not self.album_song_store[idx[0]][0]:
return
# The song ID is in the last column of the model.
self.emit(
"song-clicked",
idx.get_indices()[0],
[m[-1] for m in self.album_song_store],
{},
)
self.play_song(idx.get_indices()[0], {})
def on_song_button_press(self, tree: Any, event: Gdk.EventButton) -> bool:
if event.button == 3: # Right click
@@ -250,22 +243,14 @@ class AlbumWithSongs(Gtk.Box):
)
def play_btn_clicked(self, btn: Any):
song_ids = [x[-1] for x in self.album_song_store]
self.emit(
"song-clicked",
0,
song_ids,
{"force_shuffle_state": False},
)
self.play_song(0, {"force_shuffle_state": False})
def shuffle_btn_clicked(self, btn: Any):
song_ids = [x[-1] for x in self.album_song_store]
self.emit(
"song-clicked",
randint(0, len(self.album_song_store) - 1),
song_ids,
{"force_shuffle_state": True},
)
self.play_song(randint(0, len(self.album_song_store) - 1),
{"force_shuffle_state": True})
def play_song(self, index: int, metadata: Any):
run_action(self, 'app.play-song', index, [m[-1] for m in self.album_song_store], metadata)
# Helper Methods
# =========================================================================
@@ -403,8 +388,8 @@ class AlbumWithSongs(Gtk.Box):
if any_song_playable:
self.play_next_btn.set_action_target_value(GLib.Variant("as", song_ids))
self.add_to_queue_btn.set_action_target_value(GLib.Variant("as", song_ids))
self.play_next_btn.set_action_name("app.play-next")
self.add_to_queue_btn.set_action_name("app.add-to-queue")
self.play_next_btn.set_action_name("app.queue-next-songs")
self.add_to_queue_btn.set_action_name("app.queue-songs")
else:
self.play_next_btn.set_action_name("")
self.add_to_queue_btn.set_action_name("")

View File

@@ -16,27 +16,11 @@ from ..config import AppConfiguration, ProviderConfiguration
from ..players import PlayerManager
from ..ui import albums, artists, browse, player_controls, playlists, util
from ..ui.common import IconButton, IconToggleButton, IconMenuButton, SpinnerImage
from .actions import run_action
class MainWindow(Handy.ApplicationWindow):
"""Defines the main window for Sublime Music."""
__gsignals__ = {
"song-clicked": (
GObject.SignalFlags.RUN_FIRST,
GObject.TYPE_NONE,
(int, object, object),
),
"songs-removed": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (object,)),
"refresh-window": (
GObject.SignalFlags.RUN_FIRST,
GObject.TYPE_NONE,
(object, bool),
),
"notification-closed": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, ()),
"go-to": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (str, str)),
}
_updating_settings: bool = False
_pending_downloads: Set[str] = set()
_failed_downloads: Set[str] = set()
@@ -81,14 +65,19 @@ class MainWindow(Handy.ApplicationWindow):
# Browse=self.browse_panel,
Playlists=self.playlists_panel,
)
self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT)
self.stack.set_transition_type(Gtk.StackTransitionType.NONE)
self.sidebar_flap = Handy.Flap(
orientation=Gtk.Orientation.HORIZONTAL,
fold_policy=Handy.FlapFoldPolicy.ALWAYS,
transition_type=Handy.FlapTransitionType.OVER,
modal=True)
self.stack.connect("notify::visible-child", lambda *_: self.sidebar_flap.set_reveal_flap(False))
def stack_changed(*_):
self.sidebar_flap.set_reveal_flap(False)
run_action(self, 'app.change-tab', self.stack.get_visible_child_name())
self.stack.connect("notify::visible-child", stack_changed)
self.titlebar = self._create_headerbar(self.stack)
box.add(self.titlebar)
@@ -128,17 +117,7 @@ class MainWindow(Handy.ApplicationWindow):
drawer.set_content(notification_container)
# Player state
self.player_manager = player_controls.Manager()
self.player_manager.connect(
"song-clicked", lambda _, *a: self.emit("song-clicked", *a)
)
self.player_manager.connect(
"songs-removed", lambda _, *a: self.emit("songs-removed", *a)
)
self.player_manager.connect(
"refresh-window",
lambda _, *args: self.emit("refresh-window", *args),
)
self.player_manager = player_controls.Manager(self)
# Player Controls
drawer.set_separator(Gtk.Separator())
@@ -595,14 +574,6 @@ class MainWindow(Handy.ApplicationWindow):
def _create_stack(self, **kwargs: Gtk.Widget) -> Gtk.Stack:
stack = Gtk.Stack(homogeneous=True, transition_type=Gtk.StackTransitionType.NONE)
for name, child in kwargs.items():
child.connect(
"song-clicked",
lambda _, *args: self.emit("song-clicked", *args),
)
child.connect(
"refresh-window",
lambda _, *args: self.emit("refresh-window", *args),
)
stack.add_titled(child, name.lower(), name)
return stack

View File

@@ -4,6 +4,7 @@ from .. import util
from ..common import IconButton, IconToggleButton, SpinnerImage, SpinnerPicture
from .manager import Manager
from ...util import resolve_path
from ..actions import run_action
from gi.repository import Gdk, GdkPixbuf, GLib, GObject, Gtk, Pango, Handy
@@ -28,7 +29,7 @@ def create_play_button(manager: Manager, large=True, **kwargs):
def create_repeat_button(manager: Manager, **kwargs):
button = IconToggleButton("media-playlist-repeat", "Switch between repeat modes", **kwargs)
button.set_action_name("app.repeat-press")
button.set_action_name("app.repeat")
manager.bind_property("repeat-button-icon", button, "icon-name")
def on_active_change(*_):
@@ -36,21 +37,21 @@ def create_repeat_button(manager: Manager, **kwargs):
# Don't run the action, just update visual state
button.set_action_name(None)
button.set_active(manager.repeat_button_active)
button.set_action_name("app.repeat-press")
button.set_action_name("app.repeat")
manager.connect("notify::repeat-button-active", on_active_change)
return button
def create_shuffle_button(manager: Manager, **kwargs):
button = IconToggleButton("media-playlist-shuffle-symbolic", "Toggle playlist shuffling", **kwargs)
button.set_action_name("app.shuffle-press")
button.set_action_name("app.shuffle")
def on_active_change(*_):
if manager.shuffle_button_active != button.get_active():
# Don't run the action, just update visual state
button.set_action_name(None)
button.set_active(manager.shuffle_button_active)
button.set_action_name("app.shuffle-press")
button.set_action_name("app.shuffle")
manager.connect("notify::shuffle-button-active", on_active_change)
return button
@@ -132,3 +133,59 @@ def filename_to_pixbuf(manager: Manager):
cell.set_property("pixbuf", pixbuf)
return f2p
def create_play_queue_list(manager: Manager):
view = Gtk.TreeView(
model=manager.play_queue_store,
reorderable=True,
headers_visible=False,
)
view.get_style_context().add_class("background")
renderer = Gtk.CellRendererPixbuf()
renderer.set_fixed_size(55, 60)
column = Gtk.TreeViewColumn("", renderer)
column.set_cell_data_func(renderer, filename_to_pixbuf(manager))
column.set_resizable(True)
view.append_column(column)
renderer = Gtk.CellRendererText(markup=True, ellipsize=Pango.EllipsizeMode.END)
column = Gtk.TreeViewColumn("", renderer, markup=2, sensitive=0)
column.set_expand(True)
view.append_column(column)
renderer = Gtk.CellRendererPixbuf(icon_name="view-more-symbolic")
renderer.set_fixed_size(45, 60)
view_more_column = Gtk.TreeViewColumn("", renderer)
view_more_column.set_resizable(True)
view.append_column(view_more_column)
renderer = Gtk.CellRendererPixbuf(icon_name="window-close-symbolic")
renderer.set_fixed_size(45, 60)
close_column = Gtk.TreeViewColumn("", renderer)
close_column.set_resizable(True)
view.append_column(close_column)
def on_play_queue_button_press(tree: Any, event: Gdk.EventButton) -> bool:
if event.button == 1:
path, column, cell_x, cell_y = tree.get_path_at_pos(event.x, event.y)
song_idx = path.get_indices()[0]
tree_width = tree.get_allocation().width
if column == close_column:
# manager.emit("songs-removed", [song_idx])
pass
elif column == view_more_column:
area = tree.get_cell_area(path, view_more_column)
x = area.x + area.width / 2
y = area.y + area.height / 2
show_play_queue_popover(manager, view, [path], x, y)
else:
run_action(view, 'app.play-song', song_idx, [], {"no_reshuffle": None})
return True
return False
view.connect("button-press-event", on_play_queue_button_press)
return view

View File

@@ -199,13 +199,12 @@ class Desktop(Gtk.Box):
def on_song_activated(self, t: Any, idx: Gtk.TreePath, c: Any):
if not self.play_queue_store[idx[0]][0]:
return
# The song ID is in the last column of the model.
self.state.emit(
"song-clicked",
self.get_action_group('app').activate_action(
'play-song',
idx.get_indices()[0],
[m[-1] for m in self.play_queue_store],
{"no_reshuffle": True},
)
None,
{"no_reshuffle": True})
def on_play_queue_button_press(self, tree: Any, event: Gdk.EventButton) -> bool:
if event.button == 3: # Right click
@@ -372,25 +371,7 @@ class Desktop(Gtk.Box):
propagate_natural_height=True,
)
self.play_queue_list = Gtk.TreeView(
model=self.state.play_queue_store,
reorderable=True,
headers_visible=False,
)
selection = self.play_queue_list.get_selection()
selection.set_mode(Gtk.SelectionMode.MULTIPLE)
selection.set_select_function(lambda _, model, path, current: model[path[0]][0])
renderer = Gtk.CellRendererPixbuf()
renderer.set_fixed_size(55, 60)
column = Gtk.TreeViewColumn("", renderer)
column.set_cell_data_func(renderer, common.filename_to_pixbuf(self.state))
column.set_resizable(True)
self.play_queue_list.append_column(column)
renderer = Gtk.CellRendererText(markup=True, ellipsize=Pango.EllipsizeMode.END)
column = Gtk.TreeViewColumn("", renderer, markup=2, sensitive=0)
self.play_queue_list.append_column(column)
self.play_queue_list = common.create_play_queue_list(self.state)
self.play_queue_list.connect("row-activated", self.on_song_activated)
self.play_queue_list.connect(
@@ -419,12 +400,11 @@ class Desktop(Gtk.Box):
box.pack_start(self.volume_mute_toggle, False, True, 0)
# Volume slider
self.volume_slider = Gtk.Scale.new_with_range(
orientation=Gtk.Orientation.HORIZONTAL, min=0, max=100, step=5
)
self.volume_slider = Gtk.Scale.new(
orientation=Gtk.Orientation.HORIZONTAL, adjustment=self.state.volume)
self.volume_slider.set_name("volume-slider")
self.volume_slider.set_draw_value(False)
self.volume_slider.connect("value-changed", self.on_volume_change)
box.pack_start(self.volume_slider, True, True, 0)
vbox.pack_start(box, False, True, 0)

View File

@@ -13,24 +13,13 @@ from ...adapters import AdapterManager, Result
from ...adapters.api_objects import Song
from ...config import AppConfiguration
from ..state import RepeatType
from ..actions import run_action
from ..common import IconButton, IconToggleButton, SpinnerImage, SpinnerPicture
class Manager(GObject.GObject):
__gsignals__ = {
"device-update": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (str,)),
"song-clicked": (
GObject.SignalFlags.RUN_FIRST,
GObject.TYPE_NONE,
(int, object, object),
),
"songs-removed": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (object,)),
"refresh-window": (
GObject.SignalFlags.RUN_FIRST,
GObject.TYPE_NONE,
(object, bool),
),
"seek": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (float,)),
}
"""
Common state for player controls.
"""
volume = GObject.Property(type=Gtk.Adjustment)
scrubber = GObject.Property(type=Gtk.Adjustment)
@@ -53,14 +42,19 @@ class Manager(GObject.GObject):
cover_art_update_order_token = 0
play_queue_update_order_token = 0
def __init__(self):
current_song_index = None
def __init__(self, widget):
super().__init__()
self.volume = Gtk.Adjustment.new(1, 0, 1, 0.01, 0, 0)
self.widget = widget
self.volume = Gtk.Adjustment.new(100, 0, 100, 1, 0, 0)
self.volume.connect('value-changed', self.on_volume_changed)
self.scrubber = Gtk.Adjustment()
self.scrubber.set_step_increment(1)
self.scrubber.connect("value-changed", self.on_scrubber_changed)
self.scrubber.connect("value-changed", self.update_progress_label)
self.scrubber.connect("changed", self.update_duration_label)
self.play_queue_store = Gtk.ListStore(
@@ -115,7 +109,10 @@ class Manager(GObject.GObject):
self._controls.append(control)
def update(self, app_config: AppConfiguration, force: bool = False):
self.volume.set_value(app_config.state.volume)
self.has_song = app_config.state.current_song is not None
self.current_song_index = app_config.state.current_song_index
self.update_song_progress(
app_config.state.song_progress,
@@ -406,26 +403,28 @@ class Manager(GObject.GObject):
button.get_style_context().add_class("menu-button")
button.connect(
"clicked",
lambda _, player_id: self.state.emit("device-update", player_id),
lambda _, player_id: run_action(button, "app.select-device", player_id),
player_id,
)
self.device_list.add(button)
self.device_list.show_all()
def on_volume_changed(self, _: Any):
run_action(self.widget, 'app.set-volume', self.volume.get_value())
def on_scrubber_changed(self, _: Any):
if self.updating_scrubber:
return
self.emit("seek", self.scrubber.get_value())
def update_progress_label(self, _: Any):
if self.scrubber.get_upper() == 0:
self.progress_label = "-:--"
else:
self.progress_label = util.format_song_duration(
int(self.scrubber.get_value()))
if self.updating_scrubber:
return
run_action(self.widget, 'app.seek', self.scrubber.get_value())
def update_duration_label(self, _: Any):
upper = self.scrubber.get_upper()
if upper == 0:

View File

@@ -47,6 +47,17 @@ class MobileHandle(Gtk.ActionBar):
"Menu",
relief=True,
icon_size=Gtk.IconSize.LARGE_TOOLBAR)
def on_menu(*_):
x = self.menu_button.get_allocated_width() / 2
y = self.menu_button.get_allocated_height()
util.show_song_popover(
[self.state.play_queue_store[self.state.current_song_index][-1]],
x, y,
self.menu_button,
self.state.offline_mode)
self.menu_button.connect('clicked', on_menu)
open_buttons.pack_start(self.menu_button, False, False, 5)
buttons.add(open_buttons)
@@ -249,56 +260,7 @@ class MobileFlap(Gtk.Stack):
play_queue_scrollbox = Gtk.ScrolledWindow(vexpand=True)
play_queue_list = Gtk.TreeView(
model=self.state.play_queue_store,
reorderable=True,
headers_visible=False,
)
play_queue_list.get_style_context().add_class("background")
renderer = Gtk.CellRendererPixbuf()
renderer.set_fixed_size(55, 60)
column = Gtk.TreeViewColumn("", renderer)
column.set_cell_data_func(renderer, common.filename_to_pixbuf(self.state))
column.set_resizable(True)
play_queue_list.append_column(column)
renderer = Gtk.CellRendererText(markup=True, ellipsize=Pango.EllipsizeMode.END)
column = Gtk.TreeViewColumn("", renderer, markup=2, sensitive=0)
column.set_expand(True)
play_queue_list.append_column(column)
renderer = Gtk.CellRendererPixbuf(icon_name="view-more-symbolic")
renderer.set_fixed_size(45, 60)
view_more_column = Gtk.TreeViewColumn("", renderer)
view_more_column.set_resizable(True)
play_queue_list.append_column(view_more_column)
renderer = Gtk.CellRendererPixbuf(icon_name="window-close-symbolic")
renderer.set_fixed_size(45, 60)
close_column = Gtk.TreeViewColumn("", renderer)
close_column.set_resizable(True)
play_queue_list.append_column(close_column)
def on_play_queue_button_press(tree: Any, event: Gdk.EventButton) -> bool:
if event.button == 1:
clicked_path = tree.get_path_at_pos(event.x, event.y)[0]
song_idx = clicked_path.get_indices()[0]
tree_width = tree.get_allocation().width
if event.x > tree_width - close_column.get_width():
self.state.emit("songs-removed", [song_idx])
elif event.x > tree_width - close_column.get_width() - view_more_column.get_width():
common.show_play_queue_popover(self.state, play_queue_list, [clicked_path], event.x, event.y)
else:
self.state.emit("song-clicked", song_idx, [m[-1] for m in self.state.play_queue_store], {"no_reshuffle": True})
return True
return False
play_queue_list.connect("button-press-event", on_play_queue_button_press)
play_queue_scrollbox.add(play_queue_list)
play_queue_scrollbox.add(common.create_play_queue_list(self.state))
self.add(play_queue_scrollbox)
def on_play_queue_open(*_):

View File

@@ -82,14 +82,8 @@ class UIState:
artist_details_expanded: bool = True
loading_play_queue: bool = False
# State for Album sort.
class _DefaultGenre(Genre):
def __init__(self):
self.name = "Rock"
current_album_search_query: AlbumSearchQuery = AlbumSearchQuery(
AlbumSearchQuery.Type.RANDOM,
genre=_DefaultGenre(),
year_range=this_decade(),
)

View File

@@ -213,9 +213,9 @@ def show_song_popover(
play_next_button = Gtk.ModelButton(text="Play next", sensitive=False)
add_to_queue_button = Gtk.ModelButton(text="Add to queue", sensitive=False)
if not offline_mode:
play_next_button.set_action_name("app.play-next")
play_next_button.set_action_name("app.queue-next-songs")
play_next_button.set_action_target_value(GLib.Variant("as", song_ids))
add_to_queue_button.set_action_name("app.add-to-queue")
add_to_queue_button.set_action_name("app.queue_songs")
add_to_queue_button.set_action_target_value(GLib.Variant("as", song_ids))
go_to_album_button = Gtk.ModelButton(text="Go to album", sensitive=False)
@@ -248,9 +248,9 @@ def show_song_popover(
):
remove_download_button.set_sensitive(True)
play_next_button.set_action_target_value(GLib.Variant("as", song_ids))
play_next_button.set_action_name("app.play-next")
play_next_button.set_action_name("app.queue-next-songs")
add_to_queue_button.set_action_target_value(GLib.Variant("as", song_ids))
add_to_queue_button.set_action_name("app.add-to-queue")
add_to_queue_button.set_action_name("app.queue-songs")
albums, artists, parents = set(), set(), set()
for song in songs: