Go to artist working

This commit is contained in:
Sumner Evans
2019-09-01 13:50:44 -06:00
parent c64fa37bb3
commit 32abac8034
8 changed files with 150 additions and 101 deletions

View File

@@ -67,9 +67,11 @@ class LibremsonicApp(Gtk.Application):
def do_startup(self):
Gtk.Application.do_startup(self)
def add_action(name: str, fn):
def add_action(name: str, fn, parameter_type=None):
"""Registers an action with the application."""
action = Gio.SimpleAction.new(name, None)
if type(parameter_type) == str:
parameter_type = GLib.VariantType(parameter_type)
action = Gio.SimpleAction.new(name, parameter_type)
action.connect('activate', fn)
self.add_action(action)
@@ -84,6 +86,12 @@ class LibremsonicApp(Gtk.Application):
add_action('repeat-press', self.on_repeat_press)
add_action('shuffle-press', self.on_shuffle_press)
# 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('mute-toggle', self.on_mute_toggle)
add_action(
'update-play-queue-from-server',
@@ -208,6 +216,9 @@ class LibremsonicApp(Gtk.Application):
dialog.destroy()
def on_play_pause(self, *args):
if self.state.current_song is None:
return
if self.player.song_loaded:
self.player.toggle_play()
self.save_play_queue()
@@ -270,6 +281,24 @@ class LibremsonicApp(Gtk.Application):
self.state.shuffle_on = not self.state.shuffle_on
self.update_window()
def on_play_next(self, action, song_ids):
# TODO
print(song_ids)
def on_add_to_queue(self, action, song_ids):
# TODO
print(song_ids)
def on_go_to_album(self, action, album_id):
# TODO
self.state.current_tab = 'albums'
self.update_window()
def on_go_to_artist(self, action, artist_id):
self.state.current_tab = 'artists'
self.state.selected_artist_id = artist_id.get_string()
self.update_window()
def on_server_list_changed(self, action, servers):
self.state.config.servers = servers
self.state.save()
@@ -292,6 +321,7 @@ class LibremsonicApp(Gtk.Application):
self.update_window()
def on_stack_change(self, stack, child):
self.state.current_tab = stack.get_visible_child_name()
self.update_window()
def on_song_clicked(self, win, song_id, song_queue, metadata):
@@ -514,6 +544,9 @@ class LibremsonicApp(Gtk.Application):
lambda f: GLib.idle_add(do_play_song, f.result()), )
def save_play_queue(self):
if len(self.state.play_queue) == 0:
return
position = self.state.song_progress
self.last_play_queue_update = position

View File

@@ -39,17 +39,19 @@ class ApplicationState:
loads.
"""
config: AppConfiguration = AppConfiguration()
current_song: Child
config_file: str
current_song: Child = None
config_file: str = None
playing: bool = False
play_queue: List[str]
old_play_queue: List[str]
play_queue: List[str] = []
old_play_queue: List[str] = []
volume: int = 100
old_volume: int = 100
repeat_type: RepeatType = RepeatType.NO_REPEAT
shuffle_on: bool = False
song_progress: float = 0
current_device: str = 'this device'
current_tab: str = 'albums'
selected_artist_id: str = None
def to_json(self):
current_song = (self.current_song.id if
@@ -65,6 +67,8 @@ class ApplicationState:
'shuffle_on': getattr(self, 'shuffle_on', None),
'song_progress': getattr(self, 'song_progress', None),
'current_device': getattr(self, 'current_device', 'this device'),
'current_tab': getattr(self, 'current_tab', 'albums'),
'selected_artist_id': getattr(self, 'selected_artist_id', None),
}
def load_from_json(self, json_object):
@@ -83,6 +87,8 @@ class ApplicationState:
self.shuffle_on = json_object.get('shuffle_on', False)
self.song_progress = json_object.get('song_progress', 0.0)
self.current_device = json_object.get('current_device', 'this device')
self.current_tab = json_object.get('current_tab', 'albums')
self.selected_artist_id = json_object.get('selected_artist_id', None)
def load(self):
self.config = self.get_config(self.config_file)

View File

@@ -3,7 +3,7 @@ from typing import List, Union, Optional
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject, Pango
from gi.repository import Gtk, GObject, Pango, GLib
from libremsonic.state_manager import ApplicationState
from libremsonic.cache_manager import CacheManager
@@ -33,13 +33,7 @@ class ArtistsPanel(Gtk.Paned):
def __init__(self, *args, **kwargs):
Gtk.Paned.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
self.selected_artist = None
self.artist_list = ArtistList()
self.artist_list.connect(
'selection-changed',
self.on_list_selection_changed,
)
self.pack1(self.artist_list, False, False)
self.artist_detail_panel = ArtistDetailPanel()
@@ -52,28 +46,14 @@ class ArtistsPanel(Gtk.Paned):
def update(self, state: ApplicationState):
self.artist_list.update(state)
if self.artist_id:
self.artist_detail_panel.update(self.artist_id)
def on_list_selection_changed(self, artist_list, artist):
self.artist_id = artist.id
self.artist_detail_panel.update(self.artist_id)
if state.selected_artist_id:
self.artist_detail_panel.update(state.selected_artist_id)
class ArtistList(Gtk.Box):
__gsignals__ = {
'selection-changed': (
GObject.SignalFlags.RUN_FIRST,
GObject.TYPE_NONE,
(object, ),
),
}
def __init__(self):
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL)
self.artist_map = {}
list_actions = Gtk.ActionBar()
refresh = IconButton('view-refresh')
@@ -93,24 +73,19 @@ class ArtistList(Gtk.Box):
self.loading_indicator.add(loading_spinner)
self.list.add(self.loading_indicator)
self.list.connect('row-activated', self.on_row_activated)
list_scroll_window.add(self.list)
self.pack_start(list_scroll_window, True, True, 0)
def update(self, state=None, force=False):
self.update_list(force=force)
self.update_list(force=force, state=state)
@util.async_callback(
lambda *a, **k: CacheManager.get_artists(*a, **k),
before_download=lambda self: self.loading_indicator.show(),
on_failure=lambda self, e: self.loading_indicator.hide(),
)
def update_list(self, artists):
selected_row = self.list.get_selected_row()
selected_artist = None
if selected_row:
selected_artist = self.artist_map.get(selected_row.get_index())
def update_list(self, artists, state: ApplicationState):
# TODO use a diff here
# Remove everything
for row in self.list.get_children()[1:]:
self.list.remove(row)
@@ -119,9 +94,9 @@ class ArtistList(Gtk.Box):
for i, artist in enumerate(artists):
# Use i + 1 because of the loading indicator in index 0.
if selected_artist and artist.id == selected_artist.id:
if (state.selected_artist_id
and artist.id == (state.selected_artist_id or -1)):
selected_idx = i + 1
self.artist_map[i + 1] = artist
label_text = [f'<b>{util.esc(artist.name)}</b>']
@@ -130,7 +105,11 @@ class ArtistList(Gtk.Box):
label_text.append('{} {}'.format(
album_count, util.pluralize('album', album_count)))
self.list.add(
row = Gtk.ListBoxRow(
action_name='app.go-to-artist',
action_target=GLib.Variant('s', artist.id),
)
row.add(
Gtk.Label(
label='\n'.join(label_text),
use_markup=True,
@@ -139,16 +118,16 @@ class ArtistList(Gtk.Box):
ellipsize=Pango.EllipsizeMode.END,
max_width_chars=30,
))
self.list.add(row)
if selected_idx:
row = self.list.get_row_at_index(selected_idx)
# TODO scroll to the row
self.list.select_row(row)
self.list.show_all()
self.loading_indicator.hide()
def on_row_activated(self, listbox, row):
self.emit('selection-changed', self.artist_map[row.get_index()])
class ArtistDetailPanel(Gtk.Box):
"""Defines the artists list."""
@@ -238,15 +217,15 @@ class ArtistDetailPanel(Gtk.Box):
self.albums_list = AlbumsListWithSongs()
self.albums_list.connect(
'song-clicked',
lambda _, song, queue, metadata: self.emit(
'song-clicked', song, queue, metadata),
lambda _, song, queue, metadata: self.emit('song-clicked', song,
queue, metadata),
)
artist_info_box.pack_start(self.albums_list, True, True, 0)
self.add(artist_info_box)
def update(self, album_id):
self.update_artist_view(album_id)
def update(self, artist_id):
self.update_artist_view(artist_id)
def get_model_list_future(self, before_download):
def do_get_model_list() -> List[Child]:
@@ -261,7 +240,11 @@ class ArtistDetailPanel(Gtk.Box):
before_download=lambda self: self.artist_artwork.set_loading(True),
on_failure=lambda self, e: print('fail a', e),
)
def update_artist_view(self, artist: ArtistWithAlbumsID3):
def update_artist_view(
self,
artist: ArtistWithAlbumsID3,
state: ApplicationState,
):
self.artist_id = artist.id
self.artist_indicator.set_text('ARTIST')
self.artist_name.set_markup(util.esc(f'<b>{artist.name}</b>'))
@@ -276,7 +259,11 @@ class ArtistDetailPanel(Gtk.Box):
@util.async_callback(
lambda *a, **k: CacheManager.get_artist_info(*a, **k),
)
def update_artist_info(self, artist_info: ArtistInfo2):
def update_artist_info(
self,
artist_info: ArtistInfo2,
state: ApplicationState,
):
self.artist_bio.set_markup(util.esc(''.join(artist_info.biography)))
if len(artist_info.similarArtist or []) > 0:
@@ -287,9 +274,10 @@ class ArtistDetailPanel(Gtk.Box):
for artist in artist_info.similarArtist[:5]:
self.similar_artists_button_box.add(
Gtk.LinkButton(
uri=f'artist://{artist.id}',
label=artist.name,
name='similar-artist-button',
action_name='app.go-to-artist',
action_target=GLib.Variant('s', artist.id),
))
self.similar_artists_box.show_all()
else:
@@ -300,7 +288,11 @@ class ArtistDetailPanel(Gtk.Box):
before_download=lambda self: self.artist_artwork.set_loading(True),
on_failure=lambda self, e: self.artist_artwork.set_loading(False),
)
def update_artist_artwork(self, cover_art_filename):
def update_artist_artwork(
self,
cover_art_filename,
state: ApplicationState,
):
self.artist_artwork.set_from_file(cover_art_filename)
self.artist_artwork.set_loading(False)

View File

@@ -6,6 +6,7 @@ import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject, Pango, GLib
from libremsonic.state_manager import ApplicationState
from libremsonic.cache_manager import CacheManager
from libremsonic.ui import util
from .icon_button import IconButton
@@ -65,6 +66,7 @@ class AlbumWithSongs(Gtk.Box):
album_title_and_buttons = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL)
# TODO: deal with super long-ass titles
album_title_and_buttons.add(
Gtk.Label(
label=album.get('name', album.get('title')),
@@ -239,7 +241,7 @@ class AlbumWithSongs(Gtk.Box):
)
def shuffle_btn_clicked(self, btn):
rand_idx = randint(0, len(self.album_song_store))
rand_idx = randint(0, len(self.album_song_store) - 1)
song_ids = [x[-1] for x in self.album_song_store]
self.emit(
'song-clicked',
@@ -272,6 +274,7 @@ class AlbumWithSongs(Gtk.Box):
def update_album_songs(
self,
album: Union[AlbumWithSongsID3, Child, Directory],
state: ApplicationState,
):
new_store = [[
util.get_cached_status_icon(CacheManager.get_cached_status(song)),

View File

@@ -50,6 +50,8 @@ class MainWindow(Gtk.ApplicationWindow):
self.connected_to_label.set_markup(
f'<span style="italic">Not Connected to a Server</span>')
self.stack.set_visible_child_name(state.current_tab)
active_panel = self.stack.get_visible_child()
if hasattr(active_panel, 'update'):
active_panel.update(state)

View File

@@ -151,7 +151,7 @@ class PlayerControls(Gtk.ActionBar):
before_download=lambda self: self.album_art.set_loading(True),
on_failure=lambda self, e: self.album_art.set_loading(False),
)
def update_cover_art(self, cover_art_filename):
def update_cover_art(self, cover_art_filename, state):
self.album_art.set_from_file(cover_art_filename)
self.album_art.set_loading(False)

View File

@@ -59,7 +59,6 @@ class PlaylistsPanel(Gtk.Paned):
playlist_list_actions = Gtk.ActionBar()
self.new_playlist = IconButton(
relief=Gtk.ReliefStyle.NONE,
icon_name='list-add',
label='New Playlist',
)
@@ -380,7 +379,7 @@ class PlaylistsPanel(Gtk.Paned):
{'force_shuffle_state': False})
def on_shuffle_all_button(self, btn):
rand_idx = randint(0, len(self.playlist_song_store))
rand_idx = randint(0, len(self.playlist_song_store) - 1)
self.emit(
'song-clicked',
self.playlist_song_store[rand_idx][-1],
@@ -531,7 +530,11 @@ class PlaylistsPanel(Gtk.Paned):
before_download=lambda self: self.set_playlist_list_loading(True),
on_failure=lambda self, e: self.set_playlist_list_loading(False),
)
def update_playlist_list(self, playlists: List[PlaylistWithSongs]):
def update_playlist_list(
self,
playlists: List[PlaylistWithSongs],
state: ApplicationState,
):
selected_row = self.playlist_list.get_selected_row()
selected_playlist = None
if selected_row:
@@ -564,7 +567,7 @@ class PlaylistsPanel(Gtk.Paned):
on_failure=lambda self, e: (self.set_playlist_view_loading(False) or
self.playlist_artwork.set_loading(False)),
)
def update_playlist_view(self, playlist):
def update_playlist_view(self, playlist, state: ApplicationState):
# Update the Playlist Info panel
self.update_playlist_artwork(playlist.coverArt)
self.playlist_indicator.set_markup('PLAYLIST')
@@ -583,7 +586,7 @@ class PlaylistsPanel(Gtk.Paned):
@util.async_callback(
lambda *a, **k: CacheManager.get_playlist(*a, **k),
)
def update_playlist_song_list(self, playlist):
def update_playlist_song_list(self, playlist, state: ApplicationState):
# Update the song list model. This requires some fancy diffing to
# update the list.
self.editing_playlist_song_list = True
@@ -607,7 +610,11 @@ class PlaylistsPanel(Gtk.Paned):
before_download=lambda self: self.playlist_artwork.set_loading(True),
on_failure=lambda self, e: self.playlist_artwork.set_loading(False),
)
def update_playlist_artwork(self, cover_art_filename):
def update_playlist_artwork(
self,
cover_art_filename,
state: ApplicationState,
):
self.playlist_artwork.set_from_file(cover_art_filename)
self.playlist_artwork.set_loading(False)
@@ -615,7 +622,7 @@ class PlaylistsPanel(Gtk.Paned):
lambda *a, **k: CacheManager.get_playlist(*a, **k),
# TODO make loading here
)
def update_playlist_order(self, playlist):
def update_playlist_order(self, playlist, state: ApplicationState):
CacheManager.update_playlist(
playlist_id=playlist.id,
song_index_to_remove=list(range(playlist.songCount)),

View File

@@ -103,22 +103,6 @@ def show_song_popover(
show_remove_from_playlist_button: bool = False,
extra_menu_items: List[Tuple[Gtk.ModelButton, Any]] = [],
):
def on_play_next_click(button):
# TODO
print('play next click')
def on_add_to_queue_click(button):
# TODO
print('add to queue click')
def on_go_to_album_click(button):
# TODO
print('go to album click')
def on_go_to_artist_click(button):
# TODO
print('go to artist click')
def on_download_songs_click(button):
CacheManager.batch_download_songs(
song_ids,
@@ -147,9 +131,13 @@ def show_song_popover(
# Determine if we should enable the download button.
download_sensitive, remove_download_sensitive = False, False
albums, artists = set(), set()
for song_id in song_ids:
details = CacheManager.get_song_details(song_id).result()
status = CacheManager.get_cached_status(details)
albums.add(details.albumId)
artists.add(details.artistId)
if download_sensitive or status == SongCacheStatus.NOT_CACHED:
download_sensitive = True
@@ -159,18 +147,32 @@ def show_song_popover(
remove_download_sensitive = True
menu_items = [
(Gtk.ModelButton(text='Play next'), on_play_next_click),
(Gtk.ModelButton(text='Add to queue'), on_add_to_queue_click),
(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), None),
(
Gtk.ModelButton(text='Go to album', sensitive=len(song_ids) == 1),
on_go_to_album_click,
Gtk.ModelButton(
text='Play next',
action_name='app.play-next',
action_target=GLib.Variant('as', song_ids),
),
(
Gtk.ModelButton(text='Go to artist', sensitive=len(song_ids) == 1),
on_go_to_artist_click,
Gtk.ModelButton(
text='Add to queue',
action_name='app.add-to-queue',
action_target=GLib.Variant('as', song_ids),
),
(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), None),
Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL),
Gtk.ModelButton(
text='Go to album',
action_name='app.go-to-album',
action_target=GLib.Variant('s',
list(albums)[0]),
sensitive=len(albums) == 1,
),
Gtk.ModelButton(
text='Go to artist',
action_name='app.go-to-artist',
action_target=GLib.Variant('s',
list(artists)[0]),
sensitive=len(artists) == 1,
),
Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL),
(
Gtk.ModelButton(
text=f"Download {pluralize('song', song_count)}",
@@ -185,23 +187,23 @@ def show_song_popover(
),
on_remove_downloads_click,
),
(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), None),
(
Gtk.ModelButton(
text=f"Add {pluralize('song', song_count)} to playlist",
menu_name='add-to-playlist',
),
None,
Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL),
Gtk.ModelButton(
text=f"Add {pluralize('song', song_count)} to playlist",
menu_name='add-to-playlist',
),
*extra_menu_items,
]
for item, action in menu_items:
if action:
item.connect('clicked', action)
if type(item) == Gtk.ModelButton:
for item in menu_items:
if type(item) == tuple:
el, fn = item
el.connect('clicked', fn)
el.get_style_context().add_class('menu-button')
vbox.pack_start(item[0], False, True, 0)
else:
item.get_style_context().add_class('menu-button')
vbox.pack_start(item, False, True, 0)
vbox.pack_start(item, False, True, 0)
popover.add(vbox)
@@ -237,7 +239,11 @@ def show_song_popover(
popover.show_all()
def async_callback(future_fn, before_download=None, on_failure=None):
def async_callback(
future_fn,
before_download=None,
on_failure=None,
):
"""
Defines the ``async_callback`` decorator.
@@ -251,7 +257,7 @@ def async_callback(future_fn, before_download=None, on_failure=None):
"""
def decorator(callback_fn):
@functools.wraps(callback_fn)
def wrapper(self, *args, **kwargs):
def wrapper(self, *args, state=None, **kwargs):
if before_download:
on_before_download = (
lambda: GLib.idle_add(before_download, self))
@@ -266,7 +272,7 @@ def async_callback(future_fn, before_download=None, on_failure=None):
on_failure(self, e)
return
return GLib.idle_add(callback_fn, self, result)
return GLib.idle_add(callback_fn, self, result, state)
future: Future = future_fn(
*args,