Working on getting the Albums view showing the album details
This commit is contained in:
@@ -33,8 +33,8 @@ def from_json(cls, data):
|
|||||||
# Having to use this because things changed in Python 3.7.
|
# Having to use this because things changed in Python 3.7.
|
||||||
class_name = cls._name
|
class_name = cls._name
|
||||||
|
|
||||||
# TODO: this is not very elegant since it doesn't allow things which
|
# This is not very elegant since it doesn't allow things which sublass
|
||||||
# sublass from List or Dict.
|
# from List or Dict. For my purposes, this doesn't matter.
|
||||||
if class_name == 'List':
|
if class_name == 'List':
|
||||||
list_type = cls.__args__[0]
|
list_type = cls.__args__[0]
|
||||||
instance: List[list_type] = list()
|
instance: List[list_type] = list()
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import gi
|
import gi
|
||||||
from typing import Optional
|
from typing import Optional, Union
|
||||||
|
|
||||||
gi.require_version('Gtk', '3.0')
|
gi.require_version('Gtk', '3.0')
|
||||||
from gi.repository import Gio, Gtk, GObject, Pango
|
from gi.repository import Gio, Gtk, GObject, Pango
|
||||||
@@ -7,9 +7,11 @@ from gi.repository import Gio, Gtk, GObject, Pango
|
|||||||
from libremsonic.state_manager import ApplicationState
|
from libremsonic.state_manager import ApplicationState
|
||||||
from libremsonic.cache_manager import CacheManager
|
from libremsonic.cache_manager import CacheManager
|
||||||
from libremsonic.ui import util
|
from libremsonic.ui import util
|
||||||
from libremsonic.ui.common import CoverArtGrid
|
from libremsonic.ui.common import AlbumWithSongs, CoverArtGrid
|
||||||
|
|
||||||
from libremsonic.server.api_objects import Child
|
from libremsonic.server.api_objects import Child, AlbumWithSongsID3
|
||||||
|
|
||||||
|
Album = Union[Child, AlbumWithSongsID3]
|
||||||
|
|
||||||
|
|
||||||
class AlbumsPanel(Gtk.ScrolledWindow):
|
class AlbumsPanel(Gtk.ScrolledWindow):
|
||||||
@@ -36,15 +38,12 @@ class AlbumsPanel(Gtk.ScrolledWindow):
|
|||||||
|
|
||||||
|
|
||||||
class AlbumModel(GObject.Object):
|
class AlbumModel(GObject.Object):
|
||||||
def __init__(self, title, cover_art, artist, year):
|
def __init__(self, album: Album):
|
||||||
self.title = title
|
self.album: Album = album
|
||||||
self.cover_art = cover_art
|
|
||||||
self.artist = artist
|
|
||||||
self.year = year
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<AlbumModel title={self.title} cover_art={self.cover_art} artist={self.artist} year={self.year}>'
|
return f'<AlbumModel {self.album}>'
|
||||||
|
|
||||||
|
|
||||||
class AlbumsGrid(CoverArtGrid):
|
class AlbumsGrid(CoverArtGrid):
|
||||||
@@ -53,10 +52,11 @@ class AlbumsGrid(CoverArtGrid):
|
|||||||
# Override Methods
|
# Override Methods
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
def get_header_text(self, item: AlbumModel) -> str:
|
def get_header_text(self, item: AlbumModel) -> str:
|
||||||
return item.title
|
return (item.album.title
|
||||||
|
if type(item.album) == Child else item.album.name)
|
||||||
|
|
||||||
def get_info_text(self, item: AlbumModel) -> Optional[str]:
|
def get_info_text(self, item: AlbumModel) -> Optional[str]:
|
||||||
return util.dot_join(item.artist, item.year)
|
return util.dot_join(item.album.artist, item.album.year)
|
||||||
|
|
||||||
def get_model_list_future(self, before_download):
|
def get_model_list_future(self, before_download):
|
||||||
return CacheManager.get_albums(
|
return CacheManager.get_albums(
|
||||||
@@ -65,15 +65,13 @@ class AlbumsGrid(CoverArtGrid):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def create_model_from_element(self, album):
|
def create_model_from_element(self, album):
|
||||||
return AlbumModel(
|
return AlbumModel(album)
|
||||||
album.title if type(album) == Child else album.name,
|
|
||||||
album.coverArt,
|
def create_detail_element_from_model(self, album: AlbumModel):
|
||||||
album.artist,
|
return AlbumWithSongs(album.album)
|
||||||
album.year,
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_cover_art_filename_future(self, item, before_download):
|
def get_cover_art_filename_future(self, item, before_download):
|
||||||
return CacheManager.get_cover_art_filename(
|
return CacheManager.get_cover_art_filename(
|
||||||
item.cover_art,
|
item.album.coverArt,
|
||||||
before_download=before_download,
|
before_download=before_download,
|
||||||
)
|
)
|
||||||
|
@@ -8,16 +8,13 @@ from gi.repository import Gtk, GObject, Pango, GLib
|
|||||||
from libremsonic.state_manager import ApplicationState
|
from libremsonic.state_manager import ApplicationState
|
||||||
from libremsonic.cache_manager import CacheManager
|
from libremsonic.cache_manager import CacheManager
|
||||||
from libremsonic.ui import util
|
from libremsonic.ui import util
|
||||||
from libremsonic.ui.common import SpinnerImage
|
from libremsonic.ui.common import AlbumWithSongs, SpinnerImage
|
||||||
|
|
||||||
from libremsonic.server.api_objects import (
|
from libremsonic.server.api_objects import (
|
||||||
AlbumID3,
|
AlbumID3,
|
||||||
ArtistID3,
|
|
||||||
ArtistInfo2,
|
ArtistInfo2,
|
||||||
ArtistWithAlbumsID3,
|
ArtistWithAlbumsID3,
|
||||||
AlbumWithSongsID3,
|
|
||||||
Child,
|
Child,
|
||||||
Directory,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -392,192 +389,3 @@ class AlbumsListWithSongs(Gtk.Overlay):
|
|||||||
for child in self.box.get_children():
|
for child in self.box.get_children():
|
||||||
if album_component != child:
|
if album_component != child:
|
||||||
child.deselect_all()
|
child.deselect_all()
|
||||||
|
|
||||||
|
|
||||||
class AlbumWithSongs(Gtk.Box):
|
|
||||||
__gsignals__ = {
|
|
||||||
'song-selected': (
|
|
||||||
GObject.SignalFlags.RUN_FIRST,
|
|
||||||
GObject.TYPE_NONE,
|
|
||||||
(),
|
|
||||||
),
|
|
||||||
'song-clicked': (
|
|
||||||
GObject.SignalFlags.RUN_FIRST,
|
|
||||||
GObject.TYPE_NONE,
|
|
||||||
(str, object),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, album):
|
|
||||||
Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
|
|
||||||
self.album = album
|
|
||||||
|
|
||||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
||||||
artist_artwork = SpinnerImage(
|
|
||||||
loading=False,
|
|
||||||
image_name='artist-album-list-artwork',
|
|
||||||
spinner_name='artist-artwork-spinner',
|
|
||||||
)
|
|
||||||
box.pack_start(artist_artwork, False, False, 0)
|
|
||||||
box.pack_start(Gtk.Box(), True, True, 0)
|
|
||||||
self.pack_start(box, False, False, 0)
|
|
||||||
|
|
||||||
def cover_art_future_done(f):
|
|
||||||
artist_artwork.set_from_file(f.result())
|
|
||||||
artist_artwork.set_loading(False)
|
|
||||||
|
|
||||||
cover_art_filename_future = CacheManager.get_cover_art_filename(
|
|
||||||
album.coverArt,
|
|
||||||
before_download=lambda: artist_artwork.set_loading(True),
|
|
||||||
size=200,
|
|
||||||
)
|
|
||||||
cover_art_filename_future.add_done_callback(
|
|
||||||
lambda f: GLib.idle_add(cover_art_future_done, f))
|
|
||||||
|
|
||||||
album_details = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
||||||
album_details.add(
|
|
||||||
Gtk.Label(
|
|
||||||
label=album.get('name', album.get('title')),
|
|
||||||
name='artist-album-list-album-name',
|
|
||||||
halign=Gtk.Align.START,
|
|
||||||
))
|
|
||||||
|
|
||||||
stats = [album.year, album.genre]
|
|
||||||
if album.get('duration'):
|
|
||||||
stats.append(util.format_sequence_duration(album.duration))
|
|
||||||
|
|
||||||
album_details.add(
|
|
||||||
Gtk.Label(
|
|
||||||
label=util.dot_join(*stats),
|
|
||||||
halign=Gtk.Align.START,
|
|
||||||
margin_left=10,
|
|
||||||
))
|
|
||||||
|
|
||||||
self.album_song_store = Gtk.ListStore(
|
|
||||||
str, # cache status
|
|
||||||
str, # title
|
|
||||||
str, # duration
|
|
||||||
str, # song ID
|
|
||||||
)
|
|
||||||
|
|
||||||
def create_column(header, text_idx, bold=False, align=0, width=None):
|
|
||||||
renderer = Gtk.CellRendererText(
|
|
||||||
xalign=align,
|
|
||||||
weight=Pango.Weight.BOLD if bold else Pango.Weight.NORMAL,
|
|
||||||
ellipsize=Pango.EllipsizeMode.END,
|
|
||||||
)
|
|
||||||
renderer.set_fixed_size(width or -1, 35)
|
|
||||||
|
|
||||||
column = Gtk.TreeViewColumn(header, renderer, text=text_idx)
|
|
||||||
column.set_resizable(True)
|
|
||||||
column.set_expand(not width)
|
|
||||||
return column
|
|
||||||
|
|
||||||
self.loading_indicator = Gtk.Spinner(
|
|
||||||
name='album-list-song-list-spinner',
|
|
||||||
active=True,
|
|
||||||
)
|
|
||||||
album_details.add(self.loading_indicator)
|
|
||||||
|
|
||||||
self.album_songs = Gtk.TreeView(
|
|
||||||
model=self.album_song_store,
|
|
||||||
name='album-songs-list',
|
|
||||||
headers_visible=False, # TODO use the config value for this
|
|
||||||
margin_top=15,
|
|
||||||
margin_left=10,
|
|
||||||
margin_right=10,
|
|
||||||
margin_bottom=10,
|
|
||||||
)
|
|
||||||
self.album_songs.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
|
|
||||||
|
|
||||||
# Song status column.
|
|
||||||
renderer = Gtk.CellRendererPixbuf()
|
|
||||||
renderer.set_fixed_size(30, 35)
|
|
||||||
column = Gtk.TreeViewColumn('', renderer, icon_name=0)
|
|
||||||
column.set_resizable(True)
|
|
||||||
self.album_songs.append_column(column)
|
|
||||||
|
|
||||||
self.album_songs.append_column(create_column('TITLE', 1, bold=True))
|
|
||||||
self.album_songs.append_column(
|
|
||||||
create_column('DURATION', 2, align=1, width=40))
|
|
||||||
|
|
||||||
self.album_songs.connect('row-activated', self.on_song_activated)
|
|
||||||
self.album_songs.connect('button-press-event',
|
|
||||||
self.on_song_button_press)
|
|
||||||
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)
|
|
||||||
|
|
||||||
def on_song_selection_change(self, event):
|
|
||||||
if not self.album_songs.has_focus():
|
|
||||||
self.emit('song-selected')
|
|
||||||
|
|
||||||
def on_song_activated(self, treeview, idx, column):
|
|
||||||
# The song ID is in the last column of the model.
|
|
||||||
song_id = self.album_song_store[idx][-1]
|
|
||||||
self.emit('song-clicked', song_id,
|
|
||||||
[m[-1] for m in self.album_song_store])
|
|
||||||
|
|
||||||
def on_song_button_press(self, tree, event):
|
|
||||||
if event.button == 3: # Right click
|
|
||||||
clicked_path = tree.get_path_at_pos(event.x, event.y)
|
|
||||||
if not clicked_path:
|
|
||||||
return False
|
|
||||||
|
|
||||||
store, paths = tree.get_selection().get_selected_rows()
|
|
||||||
allow_deselect = False
|
|
||||||
|
|
||||||
def on_download_state_change(song_id=None):
|
|
||||||
self.update_album_songs(self.album.id)
|
|
||||||
|
|
||||||
# Use the new selection instead of the old one for calculating what
|
|
||||||
# to do the right click on.
|
|
||||||
if clicked_path[0] not in paths:
|
|
||||||
paths = [clicked_path[0]]
|
|
||||||
allow_deselect = True
|
|
||||||
|
|
||||||
song_ids = [self.album_song_store[p][-1] for p in paths]
|
|
||||||
|
|
||||||
# Used to adjust for the header row.
|
|
||||||
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)
|
|
||||||
|
|
||||||
util.show_song_popover(
|
|
||||||
song_ids,
|
|
||||||
event.x,
|
|
||||||
event.y + abs(bin_coords.by - widget_coords.wy),
|
|
||||||
tree,
|
|
||||||
on_download_state_change=on_download_state_change,
|
|
||||||
)
|
|
||||||
|
|
||||||
# If the click was on a selected row, don't deselect anything.
|
|
||||||
if not allow_deselect:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def deselect_all(self):
|
|
||||||
self.album_songs.get_selection().unselect_all()
|
|
||||||
|
|
||||||
@util.async_callback(
|
|
||||||
lambda *a, **k: CacheManager.get_album(*a, **k),
|
|
||||||
before_download=lambda self: self.loading_indicator.show(),
|
|
||||||
on_failure=lambda self, e: self.loading_indicator.hide(),
|
|
||||||
)
|
|
||||||
def update_album_songs(
|
|
||||||
self,
|
|
||||||
album: Union[AlbumWithSongsID3, Child, Directory],
|
|
||||||
):
|
|
||||||
new_store = [[
|
|
||||||
util.get_cached_status_icon(CacheManager.get_cached_status(song)),
|
|
||||||
util.esc(song.title),
|
|
||||||
util.format_song_duration(song.duration),
|
|
||||||
song.id,
|
|
||||||
] for song in album.get('child', album.get('song', []))]
|
|
||||||
|
|
||||||
util.diff_store(self.album_song_store, new_store)
|
|
||||||
self.loading_indicator.hide()
|
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
from .edit_form_dialog import EditFormDialog
|
from .album_with_songs import AlbumWithSongs
|
||||||
from .cover_art_grid import CoverArtGrid
|
from .cover_art_grid import CoverArtGrid
|
||||||
|
from .edit_form_dialog import EditFormDialog
|
||||||
from .spinner_image import SpinnerImage
|
from .spinner_image import SpinnerImage
|
||||||
|
|
||||||
__all__ = ('EditFormDialog', 'CoverArtGrid', 'SpinnerImage')
|
__all__ = ('AlbumWithSongs', 'CoverArtGrid', 'EditFormDialog', 'SpinnerImage')
|
||||||
|
@@ -34,7 +34,6 @@ class CoverArtGrid(Gtk.ScrolledWindow):
|
|||||||
grid_detail_grid_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
grid_detail_grid_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||||
|
|
||||||
self.grid_top = Gtk.FlowBox(
|
self.grid_top = Gtk.FlowBox(
|
||||||
vexpand=True,
|
|
||||||
hexpand=True,
|
hexpand=True,
|
||||||
row_spacing=5,
|
row_spacing=5,
|
||||||
column_spacing=5,
|
column_spacing=5,
|
||||||
@@ -54,8 +53,6 @@ class CoverArtGrid(Gtk.ScrolledWindow):
|
|||||||
grid_detail_grid_box.add(self.grid_top)
|
grid_detail_grid_box.add(self.grid_top)
|
||||||
|
|
||||||
self.detail_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
self.detail_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||||
self.detail_box.add(Gtk.Label('foo'))
|
|
||||||
self.detail_box.add(Gtk.Label('bar'))
|
|
||||||
grid_detail_grid_box.add(self.detail_box)
|
grid_detail_grid_box.add(self.detail_box)
|
||||||
|
|
||||||
self.grid_bottom = Gtk.FlowBox(
|
self.grid_bottom = Gtk.FlowBox(
|
||||||
@@ -239,6 +236,11 @@ class CoverArtGrid(Gtk.ScrolledWindow):
|
|||||||
'create_model_from_element must be implemented by the inheritor '
|
'create_model_from_element must be implemented by the inheritor '
|
||||||
'of CoverArtGrid.')
|
'of CoverArtGrid.')
|
||||||
|
|
||||||
|
def create_detail_element_from_model(self, model):
|
||||||
|
raise NotImplementedError(
|
||||||
|
'create_detail_element_from_model must be implemented by the '
|
||||||
|
'inheritor of CoverArtGrid.')
|
||||||
|
|
||||||
def get_cover_art_filename_future(self, item, before_download) -> Future:
|
def get_cover_art_filename_future(self, item, before_download) -> Future:
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
'get_cover_art_filename_future must be implemented by the '
|
'get_cover_art_filename_future must be implemented by the '
|
||||||
@@ -256,6 +258,12 @@ class CoverArtGrid(Gtk.ScrolledWindow):
|
|||||||
else:
|
else:
|
||||||
self.selected_list_store_index = selected
|
self.selected_list_store_index = selected
|
||||||
|
|
||||||
|
for c in self.detail_box.get_children():
|
||||||
|
self.detail_box.remove(c)
|
||||||
|
model = self.list_store[self.selected_list_store_index]
|
||||||
|
self.detail_box.pack_start(
|
||||||
|
self.create_detail_element_from_model(model), True, True, 5)
|
||||||
|
|
||||||
self.reflow_grids()
|
self.reflow_grids()
|
||||||
|
|
||||||
def on_grid_resize(self, flowbox, rect):
|
def on_grid_resize(self, flowbox, rect):
|
||||||
|
Reference in New Issue
Block a user