diff --git a/sublime_music/ui/albums.py b/sublime_music/ui/albums.py index a6f094e..273a6af 100644 --- a/sublime_music/ui/albums.py +++ b/sublime_music/ui/albums.py @@ -99,30 +99,37 @@ class AlbumsPanel(Handy.Leaflet): ) actionbar.pack_start(self.sort_type_combo) + self.filter_stack = Gtk.Stack(no_show_all=True) + self.alphabetical_type_combo, _ = self.make_combobox( (("by_name", "by album name", True), ("by_artist", "by artist name", True)), self.on_alphabetical_type_change, ) - actionbar.pack_start(self.alphabetical_type_combo) + self.filter_stack.add(self.alphabetical_type_combo) self.genre_combo, self.genre_combo_store = self.make_combobox( (), self.on_genre_change ) - actionbar.pack_start(self.genre_combo) + self.filter_stack.add(self.genre_combo) + filter_time_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) next_decade = (datetime.datetime.now().year // 10) * 10 + 10 self.from_year_label = Gtk.Label(label="from") - actionbar.pack_start(self.from_year_label) + filter_time_box.add(self.from_year_label) self.from_year_spin_button = Gtk.SpinButton.new_with_range(0, next_decade, 1) self.from_year_spin_button.connect("value-changed", self.on_year_changed) - actionbar.pack_start(self.from_year_spin_button) + filter_time_box.add(self.from_year_spin_button) self.to_year_label = Gtk.Label(label="to") - actionbar.pack_start(self.to_year_label) + filter_time_box.add(self.to_year_label) self.to_year_spin_button = Gtk.SpinButton.new_with_range(0, next_decade, 1) self.to_year_spin_button.connect("value-changed", self.on_year_changed) - actionbar.pack_start(self.to_year_spin_button) + filter_time_box.add(self.to_year_spin_button) + + self.filter_stack.add(filter_time_box) + + actionbar.pack_start(self.filter_stack) self.sort_toggle = IconButton( "view-sort-descending-symbolic", "Sort descending", relief=True @@ -213,34 +220,25 @@ class AlbumsPanel(Handy.Leaflet): self.add(self.grid_box) - self.details_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - self.details_box.get_style_context().add_class("details-panel") + self.album_container = Sizer(natural_width=500) - details_top_bar_revealer = Gtk.Revealer(reveal_child=False) - details_top_bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + self.album_with_songs = AlbumWithSongs(scroll_contents=True) + self.album_with_songs.get_style_context().add_class("details-panel") - back_button = IconButton("go-previous-symbolic") + self.album_with_songs.connect("song-clicked", lambda _, *args: self.emit("song-clicked", *args)) - def back_clicked(*_): + def back_clicked(_): self.set_visible_child(self.grid_box) - back_button.connect("clicked", back_clicked) + self.album_with_songs.connect("back-clicked", back_clicked) - details_top_bar.pack_start(back_button, False, False, 0) - - details_top_bar_revealer.add(details_top_bar) - - self.details_box.pack_start(details_top_bar_revealer, False, False, 0) - - self.album_with_songs = AlbumWithSongs(cover_art_size=100) - self.details_box.pack_start(self.album_with_songs, True, True, 0) - - self.add(self.details_box) + self.album_container.add(self.album_with_songs) + self.add(self.album_container) def folded_changed(*_): if not self.get_folded(): self.set_visible_child(self.grid_box) - details_top_bar_revealer.set_reveal_child(self.get_folded()) + self.album_with_songs.show_back_button = self.get_folded() self.connect("notify::folded", folded_changed) @@ -643,7 +641,7 @@ class AlbumsPanel(Handy.Leaflet): album = self.albums[child.get_index()] if self.get_folded() and self.get_visible_child() == self.grid_box: - self.set_visible_child(self.details_box) + self.set_visible_child(self.album_container) self.emit( "refresh-window", diff --git a/sublime_music/ui/artists.py b/sublime_music/ui/artists.py index 32a54de..ba7e6c6 100644 --- a/sublime_music/ui/artists.py +++ b/sublime_music/ui/artists.py @@ -5,7 +5,7 @@ from typing import cast, List, Sequence import bleach -from gi.repository import Gio, GLib, GObject, Gtk, Pango +from gi.repository import Gio, GLib, GObject, Gtk, Pango, Handy from ..adapters import ( AdapterManager, @@ -15,10 +15,10 @@ from ..adapters import ( ) from ..config import AppConfiguration from ..ui import util -from ..ui.common import AlbumWithSongs, IconButton, LoadError, SpinnerImage +from ..ui.common import AlbumWithSongs, IconButton, IconToggleButton, LoadError, SpinnerImage, Sizer -class ArtistsPanel(Gtk.Paned): +class ArtistsPanel(Handy.Leaflet): """Defines the arist panel.""" __gsignals__ = { @@ -35,11 +35,14 @@ class ArtistsPanel(Gtk.Paned): } def __init__(self, *args, **kwargs): - Gtk.Paned.__init__(self, orientation=Gtk.Orientation.HORIZONTAL) + super().__init__(transition_type=Handy.LeafletTransitionType.SLIDE, can_swipe_forward=False, interpolate_size=False) + list_sizer = Sizer(natural_width=400) self.artist_list = ArtistList() - self.pack1(self.artist_list, False, False) + list_sizer.add(self.artist_list) + self.add(list_sizer) + details_sizer = Sizer(hexpand=True, natural_width=800) self.artist_detail_panel = ArtistDetailPanel() self.artist_detail_panel.connect( "song-clicked", @@ -49,7 +52,24 @@ class ArtistsPanel(Gtk.Paned): "refresh-window", lambda _, *args: self.emit("refresh-window", *args), ) - self.pack2(self.artist_detail_panel, True, False) + details_sizer.add(self.artist_detail_panel) + self.add(details_sizer) + + def artist_clicked(_): + if self.get_folded(): + self.set_visible_child(details_sizer) + self.artist_list.connect("artist-clicked", artist_clicked) + + def back_clicked(_): + self.set_visible_child(list_sizer) + self.artist_detail_panel.connect("back-clicked", back_clicked) + + def folded_changed(*_): + if not self.get_folded(): + self.set_visible_child(list_sizer) + + self.artist_detail_panel.show_mobile = self.get_folded() + self.connect("notify::folded", folded_changed) def update(self, app_config: AppConfiguration, force: bool = False): self.artist_list.update(app_config=app_config) @@ -69,6 +89,10 @@ class _ArtistModel(GObject.GObject): class ArtistList(Gtk.Box): + __gsignals__ = { + "artist-clicked": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, ()), + } + def __init__(self): Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) @@ -119,8 +143,9 @@ class ArtistList(Gtk.Box): return row self.artists_store = Gio.ListStore() - self.list = Gtk.ListBox(name="artist-list") + self.list = Gtk.ListBox(name="artist-list", selection_mode=Gtk.SelectionMode.BROWSE) self.list.bind_model(self.artists_store, create_artist_row) + self.list.connect("row-selected", lambda *_: self.emit("artist-clicked")) list_scroll_window.add(self.list) self.pack_start(list_scroll_window, True, True, 0) @@ -184,6 +209,7 @@ class ArtistDetailPanel(Gtk.Box): """Defines the artists list.""" __gsignals__ = { + "back-clicked": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, ()), "song-clicked": ( GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, @@ -196,8 +222,9 @@ class ArtistDetailPanel(Gtk.Box): ), } + show_mobile = GObject.Property(type=bool, default=False) + update_order_token = 0 - artist_details_expanded = False def __init__(self, *args, **kwargs): super().__init__( @@ -206,134 +233,136 @@ class ArtistDetailPanel(Gtk.Box): orientation=Gtk.Orientation.VERTICAL, **kwargs, ) + + self.connect("notify::show-mobile", self.on_show_mobile_changed) + self.albums: Sequence[API.Album] = [] self.artist_id = None - # Artist info panel - self.big_info_panel = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, name="artist-info-panel" - ) + action_bar = Gtk.ActionBar() - self.artist_artwork = SpinnerImage( - loading=False, - image_name="artist-album-artwork", - spinner_name="artist-artwork-spinner", - image_size=300, - ) - self.big_info_panel.pack_start(self.artist_artwork, False, False, 0) + back_button_revealer = Gtk.Revealer(transition_type=Gtk.RevealerTransitionType.CROSSFADE) + self.bind_property("show-mobile", back_button_revealer, "reveal-child", GObject.BindingFlags.SYNC_CREATE) - # Action buttons, name, comment, number of songs, etc. - artist_details_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + back_button = IconButton("go-previous-symbolic") + back_button.connect("clicked", lambda *_: self.emit("back-clicked")) + back_button_revealer.add(back_button) - artist_details_box.pack_start(Gtk.Box(), True, False, 0) + action_bar.pack_start(back_button_revealer) - self.artist_indicator = self.make_label(name="artist-indicator") - artist_details_box.add(self.artist_indicator) - - self.artist_name = self.make_label( - name="artist-name", ellipsize=Pango.EllipsizeMode.END - ) - artist_details_box.add(self.artist_name) - - self.artist_bio = self.make_label( - name="artist-bio", justify=Gtk.Justification.LEFT - ) - self.artist_bio.set_line_wrap(True) - artist_details_box.add(self.artist_bio) - - self.similar_artists_scrolledwindow = Gtk.ScrolledWindow() - similar_artists_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - - self.similar_artists_label = self.make_label(name="similar-artists") - similar_artists_box.add(self.similar_artists_label) - - self.similar_artists_button_box = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL - ) - similar_artists_box.add(self.similar_artists_button_box) - self.similar_artists_scrolledwindow.add(similar_artists_box) - - artist_details_box.add(self.similar_artists_scrolledwindow) - - self.artist_stats = self.make_label(name="artist-stats") - artist_details_box.add(self.artist_stats) - - self.play_shuffle_buttons = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - name="playlist-play-shuffle-buttons", - ) - - self.play_button = IconButton( - "media-playback-start-symbolic", label="Play All", relief=True - ) - self.play_button.connect("clicked", self.on_play_all_clicked) - self.play_shuffle_buttons.pack_start(self.play_button, False, False, 0) - - self.shuffle_button = IconButton( - "media-playlist-shuffle-symbolic", label="Shuffle All", relief=True - ) - self.shuffle_button.connect("clicked", self.on_shuffle_all_button) - self.play_shuffle_buttons.pack_start(self.shuffle_button, False, False, 5) - artist_details_box.add(self.play_shuffle_buttons) - - self.big_info_panel.pack_start(artist_details_box, True, 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 - ) + self.refresh_button = IconButton("view-refresh-symbolic", "Refresh artist info") + self.refresh_button.connect("clicked", self.on_view_refresh_click) + action_bar.pack_end(self.refresh_button) self.download_all_button = IconButton( "folder-download-symbolic", "Download all songs by this artist" ) self.download_all_button.connect("clicked", self.on_download_all_click) - self.artist_action_buttons.add(self.download_all_button) + action_bar.pack_end(self.download_all_button) - self.refresh_button = IconButton("view-refresh-symbolic", "Refresh artist info") - self.refresh_button.connect("clicked", self.on_view_refresh_click) - self.artist_action_buttons.add(self.refresh_button) + self.shuffle_button = IconButton("media-playlist-shuffle-symbolic") + self.shuffle_button.connect("clicked", self.on_shuffle_all_button) + action_bar.pack_end(self.shuffle_button) - action_buttons_container.pack_start( - self.artist_action_buttons, False, False, 10 + self.play_button = IconButton("media-playback-start-symbolic") + self.play_button.connect("clicked", self.on_play_all_clicked) + action_bar.pack_end(self.play_button) + + self.pack_start(action_bar, False, False, 0) + self.pack_start(Gtk.Separator(), False, False, 0) + + self.scrolled_window = Gtk.ScrolledWindow(hscrollbar_policy=Gtk.PolicyType.NEVER) + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + + # Artist info panel + info_panel = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, name="artist-info-panel" ) - 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.artist_artwork = SpinnerImage( + loading=False, + image_size=120, + valign=Gtk.Align.START, ) - 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) + info_panel.pack_start(self.artist_artwork, False, False, 10) - self.big_info_panel.pack_start(action_buttons_container, False, False, 5) + # Action buttons, name, comment, number of songs, etc. + details_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - self.pack_start(self.big_info_panel, False, True, 0) + self.artist_name = self.make_label( + name="artist-name", ellipsize=Pango.EllipsizeMode.END + ) + details_box.add(self.artist_name) + + self.artist_bio_revealer = Gtk.Revealer(transition_type=Gtk.RevealerTransitionType.SLIDE_DOWN, reveal_child=True) + self.artist_bio = self.make_label( + name="artist-bio", justify=Gtk.Justification.LEFT + ) + self.artist_bio.set_line_wrap(True) + self.artist_bio_revealer.add(self.artist_bio) + details_box.add(self.artist_bio_revealer) + + details_bottom_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + + artist_stats_squeezer = Handy.Squeezer(homogeneous=False) + + self.artist_stats_long = self.make_label(name="artist-stats") + artist_stats_squeezer.add(self.artist_stats_long) + + self.artist_stats_short = self.make_label(name="artist-stats") + artist_stats_squeezer.add(self.artist_stats_short) + + details_bottom_box.pack_start(artist_stats_squeezer, False, False, 0) + + self.expand_button_revealer = Gtk.Revealer(transition_type=Gtk.RevealerTransitionType.CROSSFADE, margin_left=10) + self.expand_button = IconToggleButton( + "pan-down-symbolic", "Expand" + ) + self.expand_button.bind_property("active", self.artist_bio_revealer, "reveal-child") + self.expand_button.connect("clicked", self.on_expand_button_clicked) + self.expand_button_revealer.add(self.expand_button) + details_bottom_box.pack_end(self.expand_button_revealer, False, False, 0) + + details_box.pack_end(details_bottom_box, False, False, 0) + + info_panel.pack_start(details_box, True, True, 0) + + box.pack_start(info_panel, False, False, 0) self.error_container = Gtk.Box() - self.add(self.error_container) + # self.add(self.error_container) - self.album_list_scrolledwindow = Gtk.ScrolledWindow() self.albums_list = AlbumsListWithSongs() self.albums_list.connect( "song-clicked", lambda _, *args: self.emit("song-clicked", *args), ) - self.album_list_scrolledwindow.add(self.albums_list) - self.pack_start(self.album_list_scrolledwindow, True, True, 0) + box.pack_start(self.albums_list, True, True, 0) + + self.scrolled_window.add(box) + + self.pack_start(self.scrolled_window, True, True, 0) + + def on_show_mobile_changed(self, *_): + self.expand_button.set_active(not self.show_mobile) + self.artist_bio_revealer.set_reveal_child(not self.show_mobile) + self.expand_button_revealer.set_reveal_child(self.show_mobile) + + def on_expand_button_clicked(self, *_): + up_down = "up" if self.expand_button.get_active() else "down" + self.expand_button.set_icon(f"pan-{up_down}-symbolic") + self.expand_button.set_tooltip_text( + "Collapse" if self.expand_button.get_active() else "Expand" + ) def update(self, app_config: AppConfiguration): self.artist_id = app_config.state.selected_artist_id self.offline_mode = app_config.offline_mode if app_config.state.selected_artist_id is None: - self.big_info_panel.hide() - self.album_list_scrolledwindow.hide() - self.play_shuffle_buttons.hide() + self.shuffle_button.set_sensitive(False) + self.play_button.set_sensitive(False) else: self.update_order_token += 1 - self.album_list_scrolledwindow.show() self.update_artist_view( app_config.state.selected_artist_id, app_config=app_config, @@ -358,58 +387,27 @@ class ArtistDetailPanel(Gtk.Box): if order_token != self.update_order_token: return - self.big_info_panel.show_all() - - 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" - ) + # Scroll to top + self.scrolled_window.get_vadjustment().set_value(0) self.artist_name.set_markup(bleach.clean(f"{artist.name}")) self.artist_name.set_tooltip_text(artist.name) - if self.artist_details_expanded: - self.artist_artwork.get_style_context().remove_class("collapsed") - self.artist_name.get_style_context().remove_class("collapsed") - self.artist_indicator.set_text("ARTIST") - self.artist_stats.set_markup(self.format_stats(artist)) + self.artist_stats_long.set_markup(self.format_stats(artist, short=False)) + self.artist_stats_short.set_markup(self.format_stats(artist, short=True)) - if artist.biography: - self.artist_bio.set_markup(bleach.clean(artist.biography)) - self.artist_bio.show() - else: - self.artist_bio.hide() + biography = "" + if artist.biography: + biography += util.esc(artist.biography) - if len(artist.similar_artists or []) > 0: - self.similar_artists_label.set_markup("Similar Artists: ") - for c in self.similar_artists_button_box.get_children(): - self.similar_artists_button_box.remove(c) + if artist.similar_artists: + biography += "\n\nSimilar Artists: " - for similar_artist in (artist.similar_artists or [])[:5]: - self.similar_artists_button_box.add( - Gtk.LinkButton( - label=similar_artist.name, - name="similar-artist-button", - action_name="app.go-to-artist", - action_target=GLib.Variant("s", similar_artist.id), - ) - ) - self.similar_artists_scrolledwindow.show_all() - else: - self.similar_artists_scrolledwindow.hide() - else: - self.artist_artwork.get_style_context().add_class("collapsed") - self.artist_name.get_style_context().add_class("collapsed") - self.artist_indicator.hide() - self.artist_stats.hide() - self.artist_bio.hide() - self.similar_artists_scrolledwindow.hide() + # TODO: Make links work + biography += ", ".join(f"{bleach.clean(a.name)}" for a in artist.similar_artists[:6]) - self.play_shuffle_buttons.show_all() + self.artist_bio.set_markup(biography) + self.expand_button.set_sensitive(bool(biography)) self.update_artist_artwork( artist.artist_image_url, @@ -429,11 +427,8 @@ class ArtistDetailPanel(Gtk.Box): ) self.error_container.pack_start(load_error, True, True, 0) self.error_container.show_all() - if not has_data: - self.album_list_scrolledwindow.hide() else: self.error_container.hide() - self.album_list_scrolledwindow.show() self.albums = artist.albums or [] @@ -486,10 +481,10 @@ class ArtistDetailPanel(Gtk.Box): self.artist_artwork.set_from_file(cover_art_filename) self.artist_artwork.set_loading(False) - if self.artist_details_expanded: - self.artist_artwork.set_image_size(300) - else: - self.artist_artwork.set_image_size(70) + # if self.artist_details_expanded: + # self.artist_artwork.set_image_size(300) + # else: + # self.artist_artwork.set_image_size(70) # Event Handlers # ========================================================================= @@ -531,13 +526,6 @@ class ArtistDetailPanel(Gtk.Box): {"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): @@ -554,7 +542,7 @@ class ArtistDetailPanel(Gtk.Box): label=text, name=name, halign=Gtk.Align.START, xalign=0, **params ) - def format_stats(self, artist: API.Artist) -> str: + def format_stats(self, artist: API.Artist, short=False) -> str: album_count = artist.album_count or len(artist.albums or []) song_count, duration = 0, timedelta(0) for album in artist.albums or []: @@ -564,7 +552,8 @@ class ArtistDetailPanel(Gtk.Box): return util.dot_join( "{} {}".format(album_count, util.pluralize("album", album_count)), "{} {}".format(song_count, util.pluralize("song", song_count)), - util.format_sequence_duration(duration), + util.format_song_duration(duration) + if short else util.format_sequence_duration(duration), ) def get_artist_song_ids(self) -> List[str]: @@ -633,8 +622,8 @@ class AlbumsListWithSongs(Gtk.Overlay): if self.albums == new_albums: # Just go through all of the colidren and update them. - for c in self.box.get_children(): - c.update(app_config=app_config, force=force) + for c, album in zip(self.box.get_children(), self.albums): + c.update(album, app_config=app_config, force=force) self.spinner.hide() return @@ -644,19 +633,20 @@ class AlbumsListWithSongs(Gtk.Overlay): remove_all() for album in self.albums: - album_with_songs = AlbumWithSongs(album, show_artist_name=False) + 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.connect("song-selected", self.on_song_selected) album_with_songs.show_all() self.box.add(album_with_songs) # Update everything (no force to ensure that if we are online, then everything # is clickable) - for c in self.box.get_children(): - c.update(app_config=app_config) + # for c in self.box.get_children(): + # c.update(app_config=app_config) self.spinner.hide() diff --git a/sublime_music/ui/common/album_with_songs.py b/sublime_music/ui/common/album_with_songs.py index 092f8d2..c067646 100644 --- a/sublime_music/ui/common/album_with_songs.py +++ b/sublime_music/ui/common/album_with_songs.py @@ -1,7 +1,7 @@ from random import randint from typing import Any, cast, List -from gi.repository import Gdk, GLib, GObject, Gtk, Pango +from gi.repository import Gdk, GLib, GObject, Gtk, Pango, Handy from sublime_music.adapters import AdapterManager, api_objects as API, Result from sublime_music.config import AppConfiguration @@ -15,81 +15,37 @@ from .spinner_image import SpinnerImage class AlbumWithSongs(Gtk.Box): __gsignals__ = { - "song-selected": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, ()), + # "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, ()), } + show_back_button = GObject.Property(type=bool, default=False) + album = None offline_mode = True cover_art_result = None - def __init__( - self, - cover_art_size: int = 200, - show_artist_name: bool = True, - ): - Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL) + def __init__(self, show_artist_name: bool = True, scroll_contents: bool = False, **kwargs): + Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) self.show_artist_name = show_artist_name - box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - self.artist_artwork = SpinnerImage( - loading=False, - image_name="artist-album-list-artwork", - spinner_name="artist-artwork-spinner", - image_size=cover_art_size, - ) - # Account for 10px margin on all sides with "+ 20". - self.artist_artwork.set_size_request(cover_art_size + 20, cover_art_size + 20) - box.pack_start(self.artist_artwork, False, False, 0) - box.pack_start(Gtk.Box(), True, True, 0) - self.pack_start(box, False, False, 0) + action_bar = Gtk.ActionBar() - album_details = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - album_title_and_buttons = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + back_button_revealer = Gtk.Revealer(transition_type=Gtk.RevealerTransitionType.CROSSFADE) + self.bind_property("show-back-button", back_button_revealer, "reveal-child", GObject.BindingFlags.SYNC_CREATE) - # TODO (#43): deal with super long-ass titles - self.title = Gtk.Label( - name="artist-album-list-album-name", - halign=Gtk.Align.START, - ellipsize=Pango.EllipsizeMode.END, - ) - album_title_and_buttons.add(self.title) + back_button = IconButton("go-previous-symbolic") + back_button.connect("clicked", lambda *_: self.emit("back-clicked")) + back_button_revealer.add(back_button) - self.play_btn = IconButton( - "media-playback-start-symbolic", - "Play all songs in this album", - sensitive=False, - ) - self.play_btn.connect("clicked", self.play_btn_clicked) - album_title_and_buttons.pack_start(self.play_btn, False, False, 5) - - self.shuffle_btn = IconButton( - "media-playlist-shuffle-symbolic", - "Shuffle all songs in this album", - sensitive=False, - ) - self.shuffle_btn.connect("clicked", self.shuffle_btn_clicked) - album_title_and_buttons.pack_start(self.shuffle_btn, False, False, 5) - - self.play_next_btn = IconButton( - "queue-front-symbolic", - "Play all of the songs in this album next", - sensitive=False, - ) - album_title_and_buttons.pack_start(self.play_next_btn, False, False, 5) - - self.add_to_queue_btn = IconButton( - "queue-back-symbolic", - "Add all the songs in this album to the end of the play queue", - sensitive=False, - ) - album_title_and_buttons.pack_start(self.add_to_queue_btn, False, False, 5) + action_bar.pack_start(back_button_revealer) self.download_all_btn = IconButton( "folder-download-symbolic", @@ -97,21 +53,105 @@ class AlbumWithSongs(Gtk.Box): sensitive=False, ) self.download_all_btn.connect("clicked", self.on_download_all_click) - album_title_and_buttons.pack_end(self.download_all_btn, False, False, 5) + action_bar.pack_end(self.download_all_btn) - album_details.add(album_title_and_buttons) + self.add_to_queue_btn = IconButton( + "queue-back-symbolic", + "Add all the songs in this album to the end of the play queue", + sensitive=False, + ) + action_bar.pack_end(self.add_to_queue_btn) - self.stats = Gtk.Label( + self.play_next_btn = IconButton( + "queue-front-symbolic", + "Play all of the songs in this album next", + sensitive=False, + ) + action_bar.pack_end(self.play_next_btn) + + self.shuffle_btn = IconButton( + "media-playlist-shuffle-symbolic", + "Shuffle all songs in this album", + sensitive=False, + ) + self.shuffle_btn.connect("clicked", self.shuffle_btn_clicked) + action_bar.pack_end(self.shuffle_btn) + + self.play_btn = IconButton( + "media-playback-start-symbolic", + "Play all songs in this album", + sensitive=False, + ) + self.play_btn.connect("clicked", self.play_btn_clicked) + action_bar.pack_end(self.play_btn) + + self.pack_start(action_bar, False, False, 0) + + if scroll_contents: + self.pack_start(Gtk.Separator(), False, False, 0) + scrolled_window = Gtk.ScrolledWindow(hscrollbar_policy=Gtk.PolicyType.NEVER) + self.pack_start(scrolled_window, True, True, 0) + + contents_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + scrolled_window.add(contents_box) + else: + contents_box = self + + box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + + self.artist_artwork = SpinnerImage( + loading=False, + image_name="artist-album-list-artwork", + spinner_name="artist-artwork-spinner", + image_size=80, + ) + # Account for 10px margin on all sides with "+ 20". + # self.artist_artwork.set_size_request(cover_art_size + 20, cover_art_size + 20) + box.pack_start(self.artist_artwork, False, False, 0) + + album_details = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + + # TODO (#43): deal with super long-ass titles + self.title = Gtk.Label( + name="artist-album-list-album-name", + halign=Gtk.Align.START, + ellipsize=Pango.EllipsizeMode.END, + ) + album_details.pack_start(self.title, False, False, 0) + + self.artist_and_year = Gtk.Label( halign=Gtk.Align.START, margin_left=10, + margin_right=10, + ellipsize=Pango.EllipsizeMode.END, ) - album_details.add(self.stats) + album_details.pack_start(self.artist_and_year, False, False, 0) + + details_bottom_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + + self.genre_and_song_count = Gtk.Label() + details_bottom_box.pack_start(self.genre_and_song_count, False, False, 10) + + squeezer = Handy.Squeezer(homogeneous=False) + + self.song_duration_long = Gtk.Label(halign=Gtk.Align.END) + squeezer.add(self.song_duration_long) + + self.song_duration_short = Gtk.Label(halign=Gtk.Align.END) + squeezer.add(self.song_duration_short) + + details_bottom_box.pack_end(squeezer, False, False, 20) + album_details.pack_start(details_bottom_box, False, False, 0) self.loading_indicator_container = Gtk.Box() - album_details.add(self.loading_indicator_container) + album_details.pack_start(self.loading_indicator_container, False, False, 0) self.error_container = Gtk.Box() - album_details.add(self.error_container) + album_details.pack_start(self.error_container, False, False, 0) + + box.pack_start(album_details, True, True, 0) + + contents_box.pack_start(box, False, False, 0) # clickable, cache status, title, duration, song ID self.album_song_store = Gtk.ListStore(bool, str, str, str, str) @@ -144,11 +184,7 @@ class AlbumWithSongs(Gtk.Box): self.album_songs.get_selection().connect( "changed", self.on_song_selection_change ) - album_details.add(self.album_songs) - - self.pack_end(album_details, True, True, 0) - - # self.update_album_songs(album.id) + contents_box.pack_start(self.album_songs, True, True, 0) # Event Handlers # ========================================================================= @@ -256,12 +292,18 @@ class AlbumWithSongs(Gtk.Box): self.title.set_label(album.name) - self.stats.set_label(util.dot_join( - album.artist.name if self.show_artist_name and album.artist else None, - album.year, - album.genre.name if album.genre else None, - util.format_sequence_duration(album.duration) if album.duration else None, - )) + artist = album.artist.name if self.show_artist_name and album.artist else None + self.artist_and_year.set_label(util.dot_join(artist, album.year)) + + self.genre_and_song_count.set_label(util.dot_join( + f"{album.song_count} " + util.pluralize("song", album.song_count), + album.genre.name if album.genre else None)) + + self.song_duration_long.set_label( + util.format_sequence_duration(album.duration) if album.duration else "") + + self.song_duration_short.set_label( + util.format_song_duration(album.duration) if album.duration else "") if self.cover_art_result is not None: self.cover_art_result.cancel() diff --git a/sublime_music/ui/main.py b/sublime_music/ui/main.py index 9430f92..c62dcf8 100644 --- a/sublime_music/ui/main.py +++ b/sublime_music/ui/main.py @@ -74,8 +74,8 @@ class MainWindow(Gtk.ApplicationWindow): self.stack = self._create_stack( Albums=self.albums_panel, Artists=self.artists_panel, - Browse=self.browse_panel, - Playlists=self.playlists_panel, + # Browse=self.browse_panel, + # Playlists=self.playlists_panel, ) self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT) @@ -84,6 +84,7 @@ class MainWindow(Gtk.ApplicationWindow): 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)) self.titlebar = self._create_headerbar(self.stack) self.set_titlebar(self.titlebar) diff --git a/sublime_music/ui/player_controls/manager.py b/sublime_music/ui/player_controls/manager.py index 27cdfdb..b1ddf88 100644 --- a/sublime_music/ui/player_controls/manager.py +++ b/sublime_music/ui/player_controls/manager.py @@ -3,7 +3,7 @@ from datetime import timedelta from typing import Any, Optional, Callable, Dict, Set, Tuple from functools import partial -from gi.repository import GObject, Gtk +from gi.repository import GObject, Gtk, GLib from .. import util from ...adapters import AdapterManager, Result