diff --git a/libremsonic/app.py b/libremsonic/app.py index 766718e..ae5bb7a 100644 --- a/libremsonic/app.py +++ b/libremsonic/app.py @@ -134,7 +134,7 @@ class LibremsonicApp(Gtk.Application): self.on_stack_change, ) self.window.connect('song-clicked', self.on_song_clicked) - self.window.connect('force-refresh', self.on_force_refresh) + self.window.connect('refresh-window', self.on_refresh_window) self.window.player_controls.connect('song-scrub', self.on_song_scrub) self.window.player_controls.connect('device-update', self.on_device_update) @@ -152,6 +152,8 @@ class LibremsonicApp(Gtk.Application): if (self.state.config.current_server is None or self.state.config.current_server < 0): self.show_configure_servers_dialog() + if self.state.config.current_server is None: + self.window.close() self.update_window() @@ -171,6 +173,16 @@ class LibremsonicApp(Gtk.Application): self.save_play_queue() def on_track_end(): + current_idx = self.state.play_queue.index( + self.state.current_song.id) + + if (current_idx == len(self.state.play_queue) - 1 + and self.state.repeat_type == RepeatType.NO_REPEAT): + self.state.playing = False + self.state.current_song = None + GLib.idle_add(self.update_window) + return + GLib.idle_add(self.on_next_track) def on_player_event(event: PlayerEvent): @@ -210,10 +222,10 @@ class LibremsonicApp(Gtk.Application): self.update_play_state_from_server(prompt_confirm=True) # ########## ACTION HANDLERS ########## # - def on_force_refresh(self, _, state_updates): + def on_refresh_window(self, _, state_updates, force=False): for k, v in state_updates.items(): setattr(self.state, k, v) - self.update_window() + self.update_window(force=force) def on_configure_servers(self, action, param): self.show_configure_servers_dialog() @@ -480,8 +492,8 @@ class LibremsonicApp(Gtk.Application): dialog.run() dialog.destroy() - def update_window(self): - GLib.idle_add(self.window.update, self.state) + def update_window(self, force=False): + GLib.idle_add(lambda: self.window.update(self.state, force=force)) def update_play_state_from_server(self, prompt_confirm=False): # TODO need to make the up next list loading for the duration here @@ -499,6 +511,7 @@ class LibremsonicApp(Gtk.Application): if prompt_confirm: # If there's not a significant enough difference, don't prompt. if (self.state.play_queue == new_play_queue + and self.state.current_song and self.state.current_song.id == new_current_song_id and abs(self.state.song_progress - new_song_progress) < 15): diff --git a/libremsonic/state_manager.py b/libremsonic/state_manager.py index ea7355e..3913ab4 100644 --- a/libremsonic/state_manager.py +++ b/libremsonic/state_manager.py @@ -54,6 +54,7 @@ class ApplicationState: selected_album_id: str = None selected_artist_id: str = None selected_playlist_id: str = None + current_album_sort: str = 'random' def to_json(self): current_song = (self.current_song.id if @@ -74,6 +75,7 @@ class ApplicationState: 'selected_artist_id': getattr(self, 'selected_artist_id', None), 'selected_playlist_id': getattr(self, 'selected_playlist_id', None), + 'current_album_sort': getattr(self, 'current_album_sort', None), } def load_from_json(self, json_object): @@ -97,6 +99,8 @@ class ApplicationState: self.selected_artist_id = json_object.get('selected_artist_id', None) self.selected_playlist_id = json_object.get('selected_playlist_id', None) + self.current_album_sort = json_object.get('current_album_sort', + 'random') def load(self): self.config = self.get_config(self.config_file) diff --git a/libremsonic/ui/albums.py b/libremsonic/ui/albums.py index b1dc089..fa21d61 100644 --- a/libremsonic/ui/albums.py +++ b/libremsonic/ui/albums.py @@ -21,14 +21,13 @@ class AlbumsPanel(Gtk.Box): GObject.TYPE_NONE, (str, object, object), ), - 'force-refresh': ( + 'refresh-window': ( GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, - (object, ), + (object, bool), ), } - currently_active_sort: str = 'random' currently_active_alphabetical_sort: str = 'name' currently_active_genre: str = 'Rock' currently_active_from_year: int = 2010 @@ -136,8 +135,10 @@ class AlbumsPanel(Gtk.Box): lambda f: GLib.idle_add(get_genres_done, f)) def update(self, state: ApplicationState = None, force: bool = False): + if state: + self.sort_type_combo.set_active_id(state.current_album_sort) + # TODO store this in state - self.sort_type_combo.set_active_id(self.currently_active_sort) self.alphabetical_type_combo.set_active_id( self.currently_active_alphabetical_sort) self.from_year_entry.set_text(str(self.currently_active_from_year)) @@ -148,7 +149,7 @@ class AlbumsPanel(Gtk.Box): # Show/hide the combo boxes. def show_if(sort_type, *elements): for element in elements: - if self.currently_active_sort == sort_type: + if state.current_album_sort == sort_type: element.show() else: element.hide() @@ -166,10 +167,13 @@ class AlbumsPanel(Gtk.Box): return combo.get_model()[tree_iter][0] def on_type_combo_changed(self, combo): - self.currently_active_sort = self.get_id(combo) - if self.grid.type_ != self.currently_active_sort: - self.grid.update_params(type_=self.currently_active_sort) - self.update(force=True) + new_active_sort = self.get_id(combo) + self.grid.update_params(type_=new_active_sort) + self.emit( + 'refresh-window', + {'current_album_sort': new_active_sort}, + False, + ) def on_alphabetical_type_change(self, combo): self.currently_active_alphabetical_sort = self.get_id(combo) @@ -190,6 +194,7 @@ class AlbumsPanel(Gtk.Box): try: year = int(entry.get_text()) except: + # TODO print('failed, should do something to prevent non-numeric input') return @@ -216,7 +221,7 @@ class AlbumModel(GObject.Object): class AlbumsGrid(CoverArtGrid): """Defines the albums panel.""" - type_: str = 'random' + type_: str alphabetical_type: str = 'name' from_year: int = 2010 to_year: int = 2020 diff --git a/libremsonic/ui/artists.py b/libremsonic/ui/artists.py index 84c7312..d1c330e 100644 --- a/libremsonic/ui/artists.py +++ b/libremsonic/ui/artists.py @@ -27,10 +27,10 @@ class ArtistsPanel(Gtk.Paned): GObject.TYPE_NONE, (str, object, object), ), - 'force-refresh': ( + 'refresh-window': ( GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, - (object, ), + (object, bool), ), } @@ -47,7 +47,7 @@ class ArtistsPanel(Gtk.Paned): ) self.pack2(self.artist_detail_panel, True, False) - def update(self, state: ApplicationState): + def update(self, state: ApplicationState, force=False): self.artist_list.update(state=state) self.artist_detail_panel.update(state=state) @@ -313,6 +313,7 @@ class ArtistDetailPanel(Gtk.Box): cover_art_filename, state: ApplicationState, ): + print(cover_art_filename) self.artist_artwork.set_from_file(cover_art_filename) self.artist_artwork.set_loading(False) diff --git a/libremsonic/ui/common/album_with_songs.py b/libremsonic/ui/common/album_with_songs.py index fef5ac2..1e15355 100644 --- a/libremsonic/ui/common/album_with_songs.py +++ b/libremsonic/ui/common/album_with_songs.py @@ -72,6 +72,7 @@ class AlbumWithSongs(Gtk.Box): label=album.get('name', album.get('title')), name='artist-album-list-album-name', halign=Gtk.Align.START, + ellipsize=Pango.EllipsizeMode.END, )) self.play_btn = IconButton('media-playback-start-symbolic', diff --git a/libremsonic/ui/configure_servers.py b/libremsonic/ui/configure_servers.py index a97d151..9888769 100644 --- a/libremsonic/ui/configure_servers.py +++ b/libremsonic/ui/configure_servers.py @@ -103,9 +103,10 @@ class ConfigureServersDialog(Gtk.Dialog): flowbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) # Server List - self.server_list = Gtk.ListBox() + self.server_list = Gtk.ListBox(activate_on_single_click=False) self.server_list.connect('selected-rows-changed', self.server_list_on_selected_rows_changed) + self.server_list.connect('row-activated', self.on_server_list_activate) flowbox.pack_start(self.server_list, True, True, 10) button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) @@ -152,12 +153,21 @@ class ConfigureServersDialog(Gtk.Dialog): self.server_list.remove(el) # Add all of the rows for each of the servers. - for config in self.server_configs: - row = Gtk.ListBoxRow() + for i, config in enumerate(self.server_configs): + box = Gtk.Box() + image = Gtk.Image(margin=5) + if i == self.selected_server_index: + image.set_from_icon_name( + 'network-transmit-receive-symbolic', + Gtk.IconSize.SMALL_TOOLBAR, + ) + + box.add(image) + server_name_label = Gtk.Label(label=config.name) server_name_label.set_halign(Gtk.Align.START) - row.add(server_name_label) - self.server_list.add(row) + box.add(server_name_label) + self.server_list.add(box) # Show them, and select the current server. self.show_all() @@ -206,6 +216,9 @@ class ConfigureServersDialog(Gtk.Dialog): dialog.destroy() + def on_server_list_activate(self, *args): + self.on_connect_clicked(None) + def on_connect_clicked(self, event): selected_index = self.server_list.get_selected_row().get_index() self.emit('connected-server-changed', selected_index) diff --git a/libremsonic/ui/main.py b/libremsonic/ui/main.py index 7aac363..849bf6a 100644 --- a/libremsonic/ui/main.py +++ b/libremsonic/ui/main.py @@ -14,10 +14,10 @@ class MainWindow(Gtk.ApplicationWindow): GObject.TYPE_NONE, (str, object, object), ), - 'force-refresh': ( + 'refresh-window': ( GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, - (object, ), + (object, bool), ), } @@ -46,7 +46,7 @@ class MainWindow(Gtk.ApplicationWindow): flowbox.pack_start(self.player_controls, False, True, 0) self.add(flowbox) - def update(self, state: ApplicationState): + def update(self, state: ApplicationState, force=False): # Update the Connected to label on the popup menu. if state.config.current_server >= 0: server_name = state.config.servers[ @@ -61,7 +61,7 @@ class MainWindow(Gtk.ApplicationWindow): active_panel = self.stack.get_visible_child() if hasattr(active_panel, 'update'): - active_panel.update(state) + active_panel.update(state, force=force) self.player_controls.update(state) @@ -73,8 +73,8 @@ class MainWindow(Gtk.ApplicationWindow): lambda _, *args: self.emit('song-clicked', *args), ) child.connect( - 'force-refresh', - lambda _, *args: self.emit('force-refresh', *args), + 'refresh-window', + lambda _, *args: self.emit('refresh-window', *args), ) stack.add_titled(child, name.lower(), name) return stack diff --git a/libremsonic/ui/player_controls.py b/libremsonic/ui/player_controls.py index 3562843..589dbc2 100644 --- a/libremsonic/ui/player_controls.py +++ b/libremsonic/ui/player_controls.py @@ -59,9 +59,10 @@ class PlayerControls(Gtk.ActionBar): self.pack_end(play_queue_volume) def update(self, state: ApplicationState): - if hasattr(state, 'current_song') and state.current_song is not None: - self.update_scrubber(state.song_progress, - state.current_song.duration) + self.update_scrubber( + getattr(state, 'song_progress', None), + getattr(state.current_song, 'duration', None), + ) icon = 'pause' if state.playing else 'start' self.play_button.set_icon(f"media-playback-{icon}-symbolic") @@ -120,10 +121,13 @@ class PlayerControls(Gtk.ActionBar): self.song_title.set_text(util.esc(state.current_song.title)) self.album_name.set_text(util.esc(state.current_song.album)) artist_name = util.esc(state.current_song.artist) - if artist_name: - self.artist_name.set_text(artist_name) + self.artist_name.set_text(artist_name or '') else: - # TODO should probably clear out the cover art display if no song?? + # Clear out the cover art and song tite if no song + self.album_art.set_from_file(None) + self.song_title.set_text('') + self.album_name.set_text('') + self.artist_name.set_text('') self.album_art.set_loading(False) # Set the Play Queue button popup. @@ -137,7 +141,8 @@ class PlayerControls(Gtk.ActionBar): f'Play Queue: {play_queue_len} {song_label}') new_model = [ - PlayerControls.PlayQueueSong(s, s == state.current_song.id) + PlayerControls.PlayQueueSong( + s, has_current_song and s == state.current_song.id) for s in state.play_queue ] util.diff_model_store(self.play_queue_store, new_model) @@ -152,6 +157,12 @@ class PlayerControls(Gtk.ActionBar): self.album_art.set_loading(False) def update_scrubber(self, current, duration): + if current is None and duration is None: + self.song_duration_label.set_text('-:--') + self.song_progress_label.set_text('-:--') + self.song_scrubber.set_value(0) + return + current = current or 0 percent_complete = current / duration * 100 diff --git a/libremsonic/ui/playlists.py b/libremsonic/ui/playlists.py index ce953a2..de45610 100644 --- a/libremsonic/ui/playlists.py +++ b/libremsonic/ui/playlists.py @@ -35,10 +35,10 @@ class PlaylistsPanel(Gtk.Paned): GObject.TYPE_NONE, (str, object, object), ), - 'force-refresh': ( + 'refresh-window': ( GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, - (object, ), + (object, bool), ), } @@ -54,22 +54,22 @@ class PlaylistsPanel(Gtk.Paned): lambda _, *args: self.emit('song-clicked', *args), ) self.playlist_detail_panel.connect( - 'force-refresh', - lambda _, *args: self.emit('force-refresh', *args), + 'refresh-window', + lambda _, *args: self.emit('refresh-window', *args), ) self.pack2(self.playlist_detail_panel, True, False) - def update(self, state: ApplicationState = None): + def update(self, state: ApplicationState = None, force=False): self.playlist_list.update(state=state) self.playlist_detail_panel.update(state=state) class PlaylistList(Gtk.Box): __gsignals__ = { - 'force-refresh': ( + 'refresh-window': ( GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, - (object, ), + (object, bool), ), } @@ -238,10 +238,10 @@ class PlaylistDetailPanel(Gtk.Overlay): GObject.TYPE_NONE, (str, object, object), ), - 'force-refresh': ( + 'refresh-window': ( GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, - (object, ), + (object, bool), ), } @@ -540,11 +540,14 @@ class PlaylistDetailPanel(Gtk.Overlay): CacheManager.delete_cached_cover_art(self.playlist_id) CacheManager.invalidate_playlists_cache() self.emit( - 'force-refresh', { + 'refresh-window', + { 'selected_playlist_id': None if result == Gtk.ResponseType.DELETE_EVENT else self.playlist_id - }) + }, + True, + ) dialog.destroy()