Merge branch '27-playlist-artist-collapsed-view'
This commit is contained in:
@@ -164,6 +164,11 @@ class ConfigParamDescriptor:
|
||||
UI.
|
||||
* The literal string ``"option"``: corresponds to dropdown in the UI.
|
||||
|
||||
The :class:`hidden_behind` is an optional string representing the name of the
|
||||
expander that the component should be displayed underneath. For example, one common
|
||||
value for this is "Advanced" which will make the component only visible when the
|
||||
user expands the "Advanced" settings.
|
||||
|
||||
The :class:`numeric_bounds` parameter only has an effect if the :class:`type` is
|
||||
`int`. It specifies the min and max values that the UI control can have.
|
||||
|
||||
@@ -179,6 +184,7 @@ class ConfigParamDescriptor:
|
||||
type: Union[Type, str]
|
||||
description: str
|
||||
required: bool = True
|
||||
hidden_behind: Optional[str] = None
|
||||
default: Any = None
|
||||
numeric_bounds: Optional[Tuple[int, int]] = None
|
||||
numeric_step: Optional[int] = None
|
||||
|
@@ -139,11 +139,15 @@ class FilesystemAdapter(CachingAdapter):
|
||||
cache_key: CachingAdapter.CachedDataKey,
|
||||
ignore_cache_miss: bool = False,
|
||||
where_clauses: Tuple[Any, ...] = None,
|
||||
order_by: Any = None,
|
||||
) -> Sequence:
|
||||
result = model.select()
|
||||
if where_clauses is not None:
|
||||
result = result.where(*where_clauses)
|
||||
|
||||
if order_by:
|
||||
result = result.order_by(order_by)
|
||||
|
||||
if self.is_cache and not ignore_cache_miss:
|
||||
# Determine if the adapter has ingested data for this key before, and if
|
||||
# not, cache miss.
|
||||
@@ -231,6 +235,7 @@ class FilesystemAdapter(CachingAdapter):
|
||||
models.Playlist,
|
||||
CachingAdapter.CachedDataKey.PLAYLISTS,
|
||||
ignore_cache_miss=ignore_cache_miss,
|
||||
order_by=fn.LOWER(models.Playlist.name),
|
||||
)
|
||||
return self._playlists
|
||||
|
||||
@@ -291,7 +296,10 @@ class FilesystemAdapter(CachingAdapter):
|
||||
)
|
||||
|
||||
def get_albums(
|
||||
self, query: AlbumSearchQuery, sort_direction: str = "ascending"
|
||||
self,
|
||||
query: AlbumSearchQuery,
|
||||
sort_direction: str = "ascending"
|
||||
# TODO deal with sort dir here?
|
||||
) -> Sequence[API.Album]:
|
||||
strhash = query.strhash()
|
||||
query_result = models.AlbumQueryResult.get_or_none(
|
||||
@@ -507,7 +515,7 @@ class FilesystemAdapter(CachingAdapter):
|
||||
"year": getattr(api_album, "year", None),
|
||||
"genre": ingest_genre_data(g) if (g := api_album.genre) else None,
|
||||
"artist": ingest_artist_data(ar) if (ar := api_album.artist) else None,
|
||||
"songs": [
|
||||
"_songs": [
|
||||
ingest_song_data(s, fill_album=False) for s in api_album.songs or []
|
||||
],
|
||||
"_cover_art": self._do_ingest_new_data(
|
||||
|
@@ -108,6 +108,15 @@ class Album(BaseModel):
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@property
|
||||
def songs(self) -> List["Song"]:
|
||||
albums = Album.select()
|
||||
artists = Album.select()
|
||||
return sorted(
|
||||
prefetch(self._songs, albums, artists),
|
||||
key=lambda s: (s.disc_number or 1, s.track),
|
||||
)
|
||||
|
||||
|
||||
class AlbumQueryResult(BaseModel):
|
||||
query_hash = TextField(primary_key=True)
|
||||
@@ -144,7 +153,7 @@ class Song(BaseModel):
|
||||
duration = DurationField(null=True)
|
||||
|
||||
parent_id = TextField(null=True)
|
||||
album = ForeignKeyField(Album, null=True, backref="songs")
|
||||
album = ForeignKeyField(Album, null=True, backref="_songs")
|
||||
artist = ForeignKeyField(Artist, null=True)
|
||||
genre = ForeignKeyField(Genre, null=True, backref="songs")
|
||||
|
||||
|
@@ -125,7 +125,7 @@ class SubsonicAdapter(Adapter):
|
||||
self.ping_process = multiprocessing.Process(target=self._check_ping_thread)
|
||||
self.ping_process.start()
|
||||
|
||||
# TODO (#191): support XML?
|
||||
# TODO (#112): support XML?
|
||||
|
||||
def initial_sync(self):
|
||||
# Wait for the ping to happen.
|
||||
|
@@ -61,14 +61,11 @@
|
||||
}
|
||||
|
||||
#playlist-album-artwork {
|
||||
min-height: 200px;
|
||||
min-width: 200px;
|
||||
margin: 10px;
|
||||
margin: 10px 15px 0 10px;
|
||||
}
|
||||
|
||||
#playlist-album-artwork.collapsed {
|
||||
min-height: 40px;
|
||||
min-width: 40px;
|
||||
#artist-info-panel {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#playlist-name, #artist-detail-panel #artist-name {
|
||||
@@ -76,6 +73,11 @@
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#playlist-name.collapsed,
|
||||
#artist-detail-panel #artist-name.collapsed {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
#playlist-comment, #playlist-stats, #artist-bio, #artist-stats, #similar-artists {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
@@ -85,10 +87,6 @@
|
||||
margin: -10px 0 0 10px;
|
||||
}
|
||||
|
||||
#playlist-play-shuffle-buttons {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* ********** Playback Controls ********** */
|
||||
#player-controls-album-artwork {
|
||||
min-height: 70px;
|
||||
@@ -211,9 +209,7 @@
|
||||
}
|
||||
|
||||
#artist-album-artwork {
|
||||
margin: 10px;
|
||||
min-width: 300px;
|
||||
min-height: 300px;
|
||||
margin: 10px 15px 0 10px;
|
||||
}
|
||||
|
||||
#artist-album-list-artwork {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
from datetime import timedelta
|
||||
from random import randint
|
||||
from typing import Any, List, Sequence
|
||||
from typing import List, Sequence
|
||||
|
||||
from gi.repository import Gio, GLib, GObject, Gtk, Pango
|
||||
|
||||
@@ -36,6 +36,9 @@ class ArtistsPanel(Gtk.Paned):
|
||||
self.artist_detail_panel.connect(
|
||||
"song-clicked", lambda _, *args: self.emit("song-clicked", *args),
|
||||
)
|
||||
self.artist_detail_panel.connect(
|
||||
"refresh-window", lambda _, *args: self.emit("refresh-window", *args),
|
||||
)
|
||||
self.pack2(self.artist_detail_panel, True, False)
|
||||
|
||||
def update(self, app_config: AppConfiguration, force: bool = False):
|
||||
@@ -145,7 +148,7 @@ class ArtistList(Gtk.Box):
|
||||
self.loading_indicator.hide()
|
||||
|
||||
|
||||
class ArtistDetailPanel(Gtk.ScrolledWindow):
|
||||
class ArtistDetailPanel(Gtk.Box):
|
||||
"""Defines the artists list."""
|
||||
|
||||
__gsignals__ = {
|
||||
@@ -154,19 +157,30 @@ class ArtistDetailPanel(Gtk.ScrolledWindow):
|
||||
GObject.TYPE_NONE,
|
||||
(int, object, object),
|
||||
),
|
||||
"refresh-window": (
|
||||
GObject.SignalFlags.RUN_FIRST,
|
||||
GObject.TYPE_NONE,
|
||||
(object, bool),
|
||||
),
|
||||
}
|
||||
|
||||
update_order_token = 0
|
||||
artist_details_expanded = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, name="artist-detail-panel", **kwargs)
|
||||
super().__init__(
|
||||
*args,
|
||||
name="artist-detail-panel",
|
||||
orientation=Gtk.Orientation.VERTICAL,
|
||||
**kwargs,
|
||||
)
|
||||
self.albums: Sequence[API.Album] = []
|
||||
self.artist_id = None
|
||||
|
||||
artist_info_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
|
||||
# Artist info panel
|
||||
self.big_info_panel = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
self.big_info_panel = Gtk.Box(
|
||||
orientation=Gtk.Orientation.HORIZONTAL, name="artist-info-panel"
|
||||
)
|
||||
|
||||
self.artist_artwork = SpinnerImage(
|
||||
loading=False,
|
||||
@@ -179,22 +193,6 @@ class ArtistDetailPanel(Gtk.ScrolledWindow):
|
||||
# Action buttons, name, comment, number of songs, etc.
|
||||
artist_details_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
|
||||
# Action buttons (note we are packing end here, so we have to put them
|
||||
# in right-to-left).
|
||||
self.artist_action_buttons = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
|
||||
view_refresh_button = IconButton("view-refresh-symbolic", "Refresh artist info")
|
||||
view_refresh_button.connect("clicked", self.on_view_refresh_click)
|
||||
self.artist_action_buttons.pack_end(view_refresh_button, False, False, 5)
|
||||
|
||||
download_all_btn = IconButton(
|
||||
"folder-download-symbolic", "Download all songs by this artist"
|
||||
)
|
||||
download_all_btn.connect("clicked", self.on_download_all_click)
|
||||
self.artist_action_buttons.pack_end(download_all_btn, False, False, 5)
|
||||
|
||||
artist_details_box.pack_start(self.artist_action_buttons, False, False, 5)
|
||||
|
||||
artist_details_box.pack_start(Gtk.Box(), True, False, 0)
|
||||
|
||||
self.artist_indicator = self.make_label(name="artist-indicator")
|
||||
@@ -246,35 +244,55 @@ class ArtistDetailPanel(Gtk.ScrolledWindow):
|
||||
|
||||
self.big_info_panel.pack_start(artist_details_box, True, True, 10)
|
||||
|
||||
artist_info_box.pack_start(self.big_info_panel, False, True, 0)
|
||||
# Action buttons
|
||||
action_buttons_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
self.artist_action_buttons = Gtk.Box(
|
||||
orientation=Gtk.Orientation.HORIZONTAL, spacing=10
|
||||
)
|
||||
|
||||
download_all_btn = IconButton(
|
||||
"folder-download-symbolic", "Download all songs by this artist"
|
||||
)
|
||||
download_all_btn.connect("clicked", self.on_download_all_click)
|
||||
self.artist_action_buttons.add(download_all_btn)
|
||||
|
||||
view_refresh_button = IconButton("view-refresh-symbolic", "Refresh artist info")
|
||||
view_refresh_button.connect("clicked", self.on_view_refresh_click)
|
||||
self.artist_action_buttons.add(view_refresh_button)
|
||||
|
||||
action_buttons_container.pack_start(
|
||||
self.artist_action_buttons, False, False, 10
|
||||
)
|
||||
|
||||
action_buttons_container.pack_start(Gtk.Box(), True, True, 0)
|
||||
|
||||
expand_button_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
self.expand_collapse_button = IconButton(
|
||||
"pan-up-symbolic", "Expand playlist details"
|
||||
)
|
||||
self.expand_collapse_button.connect("clicked", self.on_expand_collapse_click)
|
||||
expand_button_container.pack_end(self.expand_collapse_button, False, False, 0)
|
||||
action_buttons_container.add(expand_button_container)
|
||||
|
||||
self.big_info_panel.pack_start(action_buttons_container, False, False, 5)
|
||||
|
||||
self.pack_start(self.big_info_panel, False, True, 0)
|
||||
|
||||
self.album_list_scrolledwindow = Gtk.ScrolledWindow()
|
||||
self.albums_list = AlbumsListWithSongs()
|
||||
self.albums_list.connect(
|
||||
"song-clicked", lambda _, *args: self.emit("song-clicked", *args),
|
||||
)
|
||||
artist_info_box.pack_start(self.albums_list, True, True, 0)
|
||||
|
||||
self.add(artist_info_box)
|
||||
self.album_list_scrolledwindow.add(self.albums_list)
|
||||
self.pack_start(self.album_list_scrolledwindow, True, True, 0)
|
||||
|
||||
def update(self, app_config: AppConfiguration):
|
||||
self.artist_id = app_config.state.selected_artist_id
|
||||
if app_config.state.selected_artist_id is None:
|
||||
self.artist_action_buttons.hide()
|
||||
self.artist_indicator.set_text("")
|
||||
self.artist_name.set_markup("")
|
||||
self.artist_stats.set_markup("")
|
||||
|
||||
self.artist_bio.set_markup("")
|
||||
self.similar_artists_scrolledwindow.hide()
|
||||
self.play_shuffle_buttons.hide()
|
||||
|
||||
self.artist_artwork.set_from_file(None)
|
||||
|
||||
self.albums = []
|
||||
self.albums_list.update(None)
|
||||
self.hide()
|
||||
else:
|
||||
self.update_order_token += 1
|
||||
self.artist_action_buttons.show()
|
||||
self.show()
|
||||
self.update_artist_view(
|
||||
app_config.state.selected_artist_id,
|
||||
app_config=app_config,
|
||||
@@ -296,28 +314,50 @@ class ArtistDetailPanel(Gtk.ScrolledWindow):
|
||||
if order_token != self.update_order_token:
|
||||
return
|
||||
|
||||
self.artist_indicator.set_text("ARTIST")
|
||||
if app_config:
|
||||
self.artist_details_expanded = app_config.state.artist_details_expanded
|
||||
|
||||
up_down = "up" if self.artist_details_expanded else "down"
|
||||
self.expand_collapse_button.set_icon(f"pan-{up_down}-symbolic")
|
||||
self.expand_collapse_button.set_tooltip_text(
|
||||
"Collapse" if self.artist_details_expanded else "Expand"
|
||||
)
|
||||
|
||||
self.artist_name.set_markup(util.esc(f"<b>{artist.name}</b>"))
|
||||
self.artist_stats.set_markup(self.format_stats(artist))
|
||||
self.artist_name.set_tooltip_text(artist.name)
|
||||
|
||||
self.artist_bio.set_markup(util.esc(artist.biography))
|
||||
if self.artist_details_expanded:
|
||||
self.show_all()
|
||||
self.artist_artwork.set_image_size(300)
|
||||
self.artist_indicator.set_text("ARTIST")
|
||||
self.artist_stats.set_markup(self.format_stats(artist))
|
||||
|
||||
if len(artist.similar_artists or []) > 0:
|
||||
self.similar_artists_label.set_markup("<b>Similar Artists:</b> ")
|
||||
for c in self.similar_artists_button_box.get_children():
|
||||
self.similar_artists_button_box.remove(c)
|
||||
self.artist_bio.set_markup(util.esc(artist.biography))
|
||||
|
||||
for artist in (artist.similar_artists or [])[:5]:
|
||||
self.similar_artists_button_box.add(
|
||||
Gtk.LinkButton(
|
||||
label=artist.name,
|
||||
name="similar-artist-button",
|
||||
action_name="app.go-to-artist",
|
||||
action_target=GLib.Variant("s", artist.id),
|
||||
if len(artist.similar_artists or []) > 0:
|
||||
self.similar_artists_label.set_markup("<b>Similar Artists:</b> ")
|
||||
for c in self.similar_artists_button_box.get_children():
|
||||
self.similar_artists_button_box.remove(c)
|
||||
|
||||
for artist in (artist.similar_artists or [])[:5]:
|
||||
self.similar_artists_button_box.add(
|
||||
Gtk.LinkButton(
|
||||
label=artist.name,
|
||||
name="similar-artist-button",
|
||||
action_name="app.go-to-artist",
|
||||
action_target=GLib.Variant("s", artist.id),
|
||||
)
|
||||
)
|
||||
)
|
||||
self.similar_artists_scrolledwindow.show_all()
|
||||
self.similar_artists_scrolledwindow.show_all()
|
||||
else:
|
||||
self.similar_artists_scrolledwindow.hide()
|
||||
else:
|
||||
self.artist_name.get_style_context().add_class("collapsed")
|
||||
self.show_all()
|
||||
self.artist_artwork.set_image_size(70)
|
||||
self.artist_indicator.hide()
|
||||
self.artist_stats.hide()
|
||||
self.artist_bio.hide()
|
||||
self.similar_artists_scrolledwindow.hide()
|
||||
|
||||
self.play_shuffle_buttons.show_all()
|
||||
@@ -353,7 +393,7 @@ class ArtistDetailPanel(Gtk.ScrolledWindow):
|
||||
self.artist_id, force=True, order_token=self.update_order_token,
|
||||
)
|
||||
|
||||
def on_download_all_click(self, btn: Any):
|
||||
def on_download_all_click(self, _):
|
||||
AdapterManager.batch_download_songs(
|
||||
self.get_artist_song_ids(),
|
||||
before_download=lambda _: self.update_artist_view(
|
||||
@@ -364,13 +404,13 @@ class ArtistDetailPanel(Gtk.ScrolledWindow):
|
||||
),
|
||||
)
|
||||
|
||||
def on_play_all_clicked(self, btn: Any):
|
||||
def on_play_all_clicked(self, _):
|
||||
songs = self.get_artist_song_ids()
|
||||
self.emit(
|
||||
"song-clicked", 0, songs, {"force_shuffle_state": False},
|
||||
)
|
||||
|
||||
def on_shuffle_all_button(self, btn: Any):
|
||||
def on_shuffle_all_button(self, _):
|
||||
songs = self.get_artist_song_ids()
|
||||
self.emit(
|
||||
"song-clicked",
|
||||
@@ -379,6 +419,13 @@ class ArtistDetailPanel(Gtk.ScrolledWindow):
|
||||
{"force_shuffle_state": True},
|
||||
)
|
||||
|
||||
def on_expand_collapse_click(self, _):
|
||||
self.emit(
|
||||
"refresh-window",
|
||||
{"artist_details_expanded": not self.artist_details_expanded},
|
||||
False,
|
||||
)
|
||||
|
||||
# Helper Methods
|
||||
# =========================================================================
|
||||
def set_all_loading(self, loading_state: bool):
|
||||
|
@@ -14,6 +14,7 @@ class SpinnerImage(Gtk.Overlay):
|
||||
):
|
||||
Gtk.Overlay.__init__(self)
|
||||
self.image_size = image_size
|
||||
self.filename: Optional[str] = None
|
||||
|
||||
self.image = Gtk.Image(name=image_name, **kwargs)
|
||||
self.add(self.image)
|
||||
@@ -29,6 +30,7 @@ class SpinnerImage(Gtk.Overlay):
|
||||
def set_from_file(self, filename: Optional[str]):
|
||||
if filename == "":
|
||||
filename = None
|
||||
self.filename = filename
|
||||
if self.image_size is not None and filename:
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
|
||||
filename, self.image_size, self.image_size, True,
|
||||
@@ -44,3 +46,7 @@ class SpinnerImage(Gtk.Overlay):
|
||||
else:
|
||||
self.spinner.stop()
|
||||
self.spinner.hide()
|
||||
|
||||
def set_image_size(self, size: int):
|
||||
self.image_size = size
|
||||
self.set_from_file(self.filename)
|
||||
|
@@ -153,7 +153,6 @@ class PlaylistList(Gtk.Box):
|
||||
margin=10,
|
||||
halign=Gtk.Align.START,
|
||||
ellipsize=Pango.EllipsizeMode.END,
|
||||
max_width_chars=30,
|
||||
)
|
||||
)
|
||||
row.show_all()
|
||||
@@ -204,25 +203,25 @@ class PlaylistList(Gtk.Box):
|
||||
|
||||
# Event Handlers
|
||||
# =========================================================================
|
||||
def on_new_playlist_clicked(self, _: Any):
|
||||
def on_new_playlist_clicked(self, _):
|
||||
self.new_playlist_entry.set_text("Untitled Playlist")
|
||||
self.new_playlist_entry.grab_focus()
|
||||
self.new_playlist_row.show()
|
||||
|
||||
def on_list_refresh_click(self, _: Any):
|
||||
def on_list_refresh_click(self, _):
|
||||
self.update(force=True)
|
||||
|
||||
def new_entry_activate(self, entry: Gtk.Entry):
|
||||
self.create_playlist(entry.get_text())
|
||||
|
||||
def cancel_button_clicked(self, _: Any):
|
||||
def cancel_button_clicked(self, _):
|
||||
self.new_playlist_row.hide()
|
||||
|
||||
def confirm_button_clicked(self, _: Any):
|
||||
def confirm_button_clicked(self, _):
|
||||
self.create_playlist(self.new_playlist_entry.get_text())
|
||||
|
||||
def create_playlist(self, playlist_name: str):
|
||||
def on_playlist_created(_: Any):
|
||||
def on_playlist_created(_):
|
||||
self.update(force=True)
|
||||
|
||||
self.loading_indicator.show()
|
||||
@@ -247,54 +246,27 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
}
|
||||
|
||||
playlist_id = None
|
||||
playlist_details_expanded = False
|
||||
|
||||
editing_playlist_song_list: bool = False
|
||||
reordering_playlist_song_list: bool = False
|
||||
|
||||
def __init__(self):
|
||||
Gtk.Overlay.__init__(self, name="playlist-view-overlay")
|
||||
playlist_view_scroll_window = Gtk.ScrolledWindow()
|
||||
playlist_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
self.playlist_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
|
||||
# Playlist info panel
|
||||
self.big_info_panel = Gtk.Box(
|
||||
name="playlist-info-panel", orientation=Gtk.Orientation.HORIZONTAL,
|
||||
)
|
||||
playlist_info_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
|
||||
self.playlist_artwork = SpinnerImage(
|
||||
image_name="playlist-album-artwork",
|
||||
spinner_name="playlist-artwork-spinner",
|
||||
image_size=200,
|
||||
)
|
||||
self.big_info_panel.pack_start(self.playlist_artwork, False, False, 0)
|
||||
playlist_info_box.add(self.playlist_artwork)
|
||||
|
||||
# Action buttons, name, comment, number of songs, etc.
|
||||
# Name, comment, number of songs, etc.
|
||||
playlist_details_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
|
||||
# Action buttons (note we are packing end here, so we have to put them
|
||||
# in right-to-left).
|
||||
self.playlist_action_buttons = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
|
||||
view_refresh_button = IconButton(
|
||||
"view-refresh-symbolic", "Refresh playlist info"
|
||||
)
|
||||
view_refresh_button.connect("clicked", self.on_view_refresh_click)
|
||||
self.playlist_action_buttons.pack_end(view_refresh_button, False, False, 5)
|
||||
|
||||
playlist_edit_button = IconButton("document-edit-symbolic", "Edit paylist")
|
||||
playlist_edit_button.connect("clicked", self.on_playlist_edit_button_click)
|
||||
self.playlist_action_buttons.pack_end(playlist_edit_button, False, False, 5)
|
||||
|
||||
download_all_button = IconButton(
|
||||
"folder-download-symbolic", "Download all songs in the playlist"
|
||||
)
|
||||
download_all_button.connect(
|
||||
"clicked", self.on_playlist_list_download_all_button_click
|
||||
)
|
||||
self.playlist_action_buttons.pack_end(download_all_button, False, False, 5)
|
||||
|
||||
playlist_details_box.pack_start(self.playlist_action_buttons, False, False, 5)
|
||||
|
||||
playlist_details_box.pack_start(Gtk.Box(), True, False, 0)
|
||||
|
||||
self.playlist_indicator = self.make_label(name="playlist-indicator")
|
||||
@@ -328,11 +300,53 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
|
||||
playlist_details_box.add(self.play_shuffle_buttons)
|
||||
|
||||
self.big_info_panel.pack_start(playlist_details_box, True, True, 10)
|
||||
playlist_info_box.pack_start(playlist_details_box, True, True, 0)
|
||||
|
||||
playlist_box.pack_start(self.big_info_panel, False, True, 0)
|
||||
# Action buttons & expand/collapse button
|
||||
action_buttons_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
self.playlist_action_buttons = Gtk.Box(
|
||||
orientation=Gtk.Orientation.HORIZONTAL, spacing=10
|
||||
)
|
||||
|
||||
download_all_button = IconButton(
|
||||
"folder-download-symbolic", "Download all songs in the playlist"
|
||||
)
|
||||
download_all_button.connect(
|
||||
"clicked", self.on_playlist_list_download_all_button_click
|
||||
)
|
||||
self.playlist_action_buttons.add(download_all_button)
|
||||
|
||||
playlist_edit_button = IconButton("document-edit-symbolic", "Edit paylist")
|
||||
playlist_edit_button.connect("clicked", self.on_playlist_edit_button_click)
|
||||
self.playlist_action_buttons.add(playlist_edit_button)
|
||||
|
||||
view_refresh_button = IconButton(
|
||||
"view-refresh-symbolic", "Refresh playlist info"
|
||||
)
|
||||
view_refresh_button.connect("clicked", self.on_view_refresh_click)
|
||||
self.playlist_action_buttons.add(view_refresh_button)
|
||||
|
||||
action_buttons_container.pack_start(
|
||||
self.playlist_action_buttons, False, False, 10
|
||||
)
|
||||
|
||||
action_buttons_container.pack_start(Gtk.Box(), True, True, 0)
|
||||
|
||||
expand_button_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
self.expand_collapse_button = IconButton(
|
||||
"pan-up-symbolic", "Expand playlist details"
|
||||
)
|
||||
self.expand_collapse_button.connect("clicked", self.on_expand_collapse_click)
|
||||
expand_button_container.pack_end(self.expand_collapse_button, False, False, 0)
|
||||
action_buttons_container.add(expand_button_container)
|
||||
|
||||
playlist_info_box.pack_end(action_buttons_container, False, False, 5)
|
||||
|
||||
self.playlist_box.add(playlist_info_box)
|
||||
|
||||
# Playlist songs list
|
||||
playlist_view_scroll_window = Gtk.ScrolledWindow()
|
||||
|
||||
self.playlist_song_store = Gtk.ListStore(
|
||||
str, # cache status
|
||||
str, # title
|
||||
@@ -399,10 +413,10 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
)
|
||||
self.playlist_song_store.connect("row-deleted", self.on_playlist_model_row_move)
|
||||
|
||||
playlist_box.add(self.playlist_songs)
|
||||
playlist_view_scroll_window.add(self.playlist_songs)
|
||||
|
||||
playlist_view_scroll_window.add(playlist_box)
|
||||
self.add(playlist_view_scroll_window)
|
||||
self.playlist_box.pack_start(playlist_view_scroll_window, True, True, 0)
|
||||
self.add(self.playlist_box)
|
||||
|
||||
playlist_view_spinner = Gtk.Spinner(active=True)
|
||||
playlist_view_spinner.start()
|
||||
@@ -417,17 +431,11 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
|
||||
def update(self, app_config: AppConfiguration, force: bool = False):
|
||||
if app_config.state.selected_playlist_id is None:
|
||||
self.playlist_artwork.set_from_file(None)
|
||||
self.playlist_indicator.set_markup("")
|
||||
self.playlist_name.set_markup("")
|
||||
self.playlist_comment.hide()
|
||||
self.playlist_stats.set_markup("")
|
||||
self.playlist_action_buttons.hide()
|
||||
self.play_shuffle_buttons.hide()
|
||||
self.playlist_box.hide()
|
||||
self.playlist_view_loading_box.hide()
|
||||
self.playlist_artwork.set_loading(False)
|
||||
else:
|
||||
self.update_playlist_view_order_token += 1
|
||||
self.playlist_box.show()
|
||||
self.update_playlist_view(
|
||||
app_config.state.selected_playlist_id,
|
||||
app_config=app_config,
|
||||
@@ -459,15 +467,40 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
|
||||
self.playlist_id = playlist.id
|
||||
|
||||
if app_config:
|
||||
self.playlist_details_expanded = app_config.state.playlist_details_expanded
|
||||
|
||||
up_down = "up" if self.playlist_details_expanded else "down"
|
||||
self.expand_collapse_button.set_icon(f"pan-{up_down}-symbolic")
|
||||
self.expand_collapse_button.set_tooltip_text(
|
||||
"Collapse" if self.playlist_details_expanded else "Expand"
|
||||
)
|
||||
|
||||
# Update the info display.
|
||||
self.playlist_indicator.set_markup("PLAYLIST")
|
||||
self.playlist_name.set_markup(f"<b>{playlist.name}</b>")
|
||||
if playlist.comment:
|
||||
self.playlist_comment.set_text(playlist.comment)
|
||||
self.playlist_comment.show()
|
||||
self.playlist_name.set_tooltip_text(playlist.name)
|
||||
|
||||
if self.playlist_details_expanded:
|
||||
self.playlist_name.get_style_context().remove_class("collapsed")
|
||||
self.playlist_box.show_all()
|
||||
self.playlist_artwork.set_image_size(200)
|
||||
self.playlist_indicator.set_markup("PLAYLIST")
|
||||
|
||||
if playlist.comment:
|
||||
self.playlist_comment.set_text(playlist.comment)
|
||||
self.playlist_comment.set_tooltip_text(playlist.comment)
|
||||
self.playlist_comment.show()
|
||||
else:
|
||||
self.playlist_comment.hide()
|
||||
|
||||
self.playlist_stats.set_markup(self._format_stats(playlist))
|
||||
else:
|
||||
self.playlist_name.get_style_context().add_class("collapsed")
|
||||
self.playlist_box.show_all()
|
||||
self.playlist_artwork.set_image_size(70)
|
||||
self.playlist_indicator.hide()
|
||||
self.playlist_comment.hide()
|
||||
self.playlist_stats.set_markup(self._format_stats(playlist))
|
||||
self.playlist_stats.hide()
|
||||
|
||||
# Update the artwork.
|
||||
self.update_playlist_artwork(playlist.cover_art, order_token=order_token)
|
||||
@@ -522,7 +555,6 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
|
||||
self.playlist_view_loading_box.hide()
|
||||
self.playlist_action_buttons.show_all()
|
||||
self.play_shuffle_buttons.show_all()
|
||||
|
||||
@util.async_callback(
|
||||
AdapterManager.get_cover_art_filename,
|
||||
@@ -544,14 +576,14 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
|
||||
# Event Handlers
|
||||
# =========================================================================
|
||||
def on_view_refresh_click(self, _: Any):
|
||||
def on_view_refresh_click(self, _):
|
||||
self.update_playlist_view(
|
||||
self.playlist_id,
|
||||
force=True,
|
||||
order_token=self.update_playlist_view_order_token,
|
||||
)
|
||||
|
||||
def on_playlist_edit_button_click(self, _: Any):
|
||||
def on_playlist_edit_button_click(self, _):
|
||||
assert self.playlist_id
|
||||
playlist = AdapterManager.get_playlist_details(self.playlist_id).result()
|
||||
dialog = EditPlaylistDialog(self.get_toplevel(), playlist)
|
||||
@@ -609,7 +641,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
|
||||
dialog.destroy()
|
||||
|
||||
def on_playlist_list_download_all_button_click(self, _: Any):
|
||||
def on_playlist_list_download_all_button_click(self, _):
|
||||
def download_state_change(song_id: str):
|
||||
GLib.idle_add(
|
||||
lambda: self.update_playlist_view(
|
||||
@@ -624,7 +656,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
on_song_download_complete=download_state_change,
|
||||
)
|
||||
|
||||
def on_play_all_clicked(self, _: Any):
|
||||
def on_play_all_clicked(self, _):
|
||||
self.emit(
|
||||
"song-clicked",
|
||||
0,
|
||||
@@ -632,7 +664,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
{"force_shuffle_state": False, "active_playlist_id": self.playlist_id},
|
||||
)
|
||||
|
||||
def on_shuffle_all_button(self, _: Any):
|
||||
def on_shuffle_all_button(self, _):
|
||||
self.emit(
|
||||
"song-clicked",
|
||||
randint(0, len(self.playlist_song_store) - 1),
|
||||
@@ -640,7 +672,14 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
{"force_shuffle_state": True, "active_playlist_id": self.playlist_id},
|
||||
)
|
||||
|
||||
def on_song_activated(self, _: Any, idx: Gtk.TreePath, col: Any):
|
||||
def on_expand_collapse_click(self, _):
|
||||
self.emit(
|
||||
"refresh-window",
|
||||
{"playlist_details_expanded": not self.playlist_details_expanded},
|
||||
False,
|
||||
)
|
||||
|
||||
def on_song_activated(self, _, idx: Gtk.TreePath, col: Any):
|
||||
# The song ID is in the last column of the model.
|
||||
self.emit(
|
||||
"song-clicked",
|
||||
@@ -678,7 +717,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
bin_coords = tree.convert_tree_to_bin_window_coords(event.x, event.y)
|
||||
widget_coords = tree.convert_tree_to_widget_coords(event.x, event.y)
|
||||
|
||||
def on_remove_songs_click(_: Any):
|
||||
def on_remove_songs_click(_):
|
||||
assert self.playlist_id
|
||||
delete_idxs = {p.get_indices()[0] for p in paths}
|
||||
new_song_ids = [
|
||||
@@ -737,7 +776,13 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
self.playlist_view_loading_box.show_all()
|
||||
|
||||
def make_label(self, text: str = None, name: str = None, **params,) -> Gtk.Label:
|
||||
return Gtk.Label(label=text, name=name, halign=Gtk.Align.START, **params,)
|
||||
return Gtk.Label(
|
||||
label=text,
|
||||
name=name,
|
||||
halign=Gtk.Align.START,
|
||||
ellipsize=Pango.EllipsizeMode.END,
|
||||
**params,
|
||||
)
|
||||
|
||||
@util.async_callback(AdapterManager.get_playlist_details)
|
||||
def _update_playlist_order(
|
||||
|
@@ -43,6 +43,8 @@ class UIState:
|
||||
)
|
||||
|
||||
version: int = 1
|
||||
|
||||
# Play state
|
||||
playing: bool = False
|
||||
current_song_index: int = -1
|
||||
play_queue: Tuple[str, ...] = field(default_factory=tuple)
|
||||
@@ -54,6 +56,8 @@ class UIState:
|
||||
song_progress: timedelta = timedelta()
|
||||
song_stream_cache_progress: Optional[timedelta] = timedelta()
|
||||
current_device: str = "this device"
|
||||
|
||||
# UI state
|
||||
current_tab: str = "albums"
|
||||
selected_album_id: Optional[str] = None
|
||||
selected_artist_id: Optional[str] = None
|
||||
@@ -63,6 +67,8 @@ class UIState:
|
||||
album_page_size: int = 30
|
||||
album_page: int = 0
|
||||
current_notification: Optional[UINotification] = None
|
||||
playlist_details_expanded: bool = True
|
||||
artist_details_expanded: bool = True
|
||||
|
||||
def __getstate__(self):
|
||||
state = self.__dict__.copy()
|
||||
|
Reference in New Issue
Block a user