WIP
This commit is contained in:
@@ -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
|
||||
|
||||
|
@@ -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
256
sublime_music/ui/actions.py
Normal 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()
|
@@ -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
|
||||
"""
|
||||
|
@@ -236,11 +236,6 @@ entry.invalid {
|
||||
min-width: 35px;
|
||||
}
|
||||
|
||||
/* ********** General ********** */
|
||||
.menu-button {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
/* ********** Search ********** */
|
||||
#search-results {
|
||||
min-width: 400px;
|
||||
|
@@ -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)
|
||||
|
@@ -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("")
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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:
|
||||
|
@@ -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(*_):
|
||||
|
@@ -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(),
|
||||
)
|
||||
|
||||
|
@@ -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:
|
||||
|
Reference in New Issue
Block a user