1# Copyright 2020 The GNOME Music developers 2# 3# GNOME Music is free software; you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published by 5# the Free Software Foundation; either version 2 of the License, or 6# (at your option) any later version. 7# 8# GNOME Music is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License along 14# with GNOME Music; if not, write to the Free Software Foundation, Inc., 15# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 16# 17# The GNOME Music authors hereby grant permission for non-GPL compatible 18# GStreamer plugins to be used and distributed together with GStreamer 19# and GNOME Music. This permission is above and beyond the permissions 20# granted by the GPL license by which GNOME Music is covered. If you 21# modify this code, you may extend this exception to your version of the 22# code, but you are not obligated to do so. If you do not wish to do so, 23# delete this exception statement from your version. 24 25from __future__ import annotations 26from gettext import ngettext 27from typing import Optional, Union 28import typing 29 30from gi.repository import Gfm, Gio, GLib, GObject, Gtk, Handy 31 32from gnomemusic.corealbum import CoreAlbum 33from gnomemusic.defaulticon import DefaultIcon 34from gnomemusic.utils import ArtSize 35from gnomemusic.widgets.disclistboxwidget import DiscBox 36from gnomemusic.widgets.disclistboxwidget import DiscListBox # noqa: F401 37from gnomemusic.widgets.playlistdialog import PlaylistDialog 38if typing.TYPE_CHECKING: 39 from gnomemusic.application import Application 40 from gnomemusic.coreartist import CoreArtist 41 from gnomemusic.coredisc import CoreDisc 42 from gnomemusic.coresong import CoreSong 43 from gnomemusic.widgets.songwidget import SongWidget 44 45 46@Gtk.Template(resource_path='/org/gnome/Music/ui/AlbumWidget.ui') 47class AlbumWidget(Handy.Clamp): 48 """Album widget. 49 50 The album widget consists of an image with the album art 51 on the left and a list of songs on the right. 52 """ 53 54 __gtype_name__ = 'AlbumWidget' 55 56 _artist_label = Gtk.Template.Child() 57 _composer_label = Gtk.Template.Child() 58 _art_stack = Gtk.Template.Child() 59 _disc_list_box = Gtk.Template.Child() 60 _menu_button = Gtk.Template.Child() 61 _play_button = Gtk.Template.Child() 62 _released_label = Gtk.Template.Child() 63 _title_label = Gtk.Template.Child() 64 65 selection_mode = GObject.Property(type=bool, default=False) 66 show_artist_label = GObject.Property(type=bool, default=True) 67 68 def __init__(self, application: Application) -> None: 69 """Initialize the AlbumWidget. 70 71 :param GtkApplication application: The application object 72 """ 73 super().__init__() 74 75 self._application = application 76 self._corealbum: CoreAlbum 77 self._active_coreobject: Union[CoreAlbum, CoreArtist] 78 self._coremodel = self._application.props.coremodel 79 self._duration_signal_id = 0 80 self._year_signal_id = 0 81 self._model_signal_id = 0 82 83 self._art_stack.props.size = ArtSize.LARGE 84 self._art_stack.props.art_type = DefaultIcon.Type.ALBUM 85 self._player = self._application.props.player 86 87 self.bind_property( 88 "selection-mode", self._disc_list_box, "selection-mode", 89 GObject.BindingFlags.BIDIRECTIONAL 90 | GObject.BindingFlags.SYNC_CREATE) 91 92 self.bind_property( 93 "show-artist-label", self._artist_label, "visible", 94 GObject.BindingFlags.SYNC_CREATE) 95 96 self.connect("notify::selection-mode", self._on_selection_mode_changed) 97 98 action_group = Gio.SimpleActionGroup() 99 actions = ( 100 ("play", self._on_play_action), 101 ("add_favorites", self._on_add_favorites_action), 102 ("add_playlist", self._on_add_playlist_action) 103 ) 104 for (name, callback) in actions: 105 action = Gio.SimpleAction.new(name, None) 106 action.connect("activate", callback) 107 action_group.add_action(action) 108 109 self.insert_action_group("album", action_group) 110 111 @GObject.Property( 112 type=CoreAlbum, default=None, flags=GObject.ParamFlags.READWRITE) 113 def corealbum(self) -> Optional[CoreAlbum]: 114 """Get the current CoreAlbum. 115 116 :returns: The current CoreAlbum 117 :rtype: CoreAlbum or None 118 """ 119 try: 120 return self._corealbum 121 except AttributeError: 122 return None 123 124 @corealbum.setter # type:ignore 125 def corealbum(self, corealbum: CoreAlbum) -> None: 126 """Update CoreAlbum used for AlbumWidget. 127 128 :param CoreAlbum corealbum: The CoreAlbum object 129 """ 130 if (self._duration_signal_id != 0 131 or self._year_signal_id != 0 132 or self._model_signal_id != 0): 133 self._corealbum.disconnect(self._duration_signal_id) 134 self._corealbum.disconnect(self._year_signal_id) 135 self._corealbum.props.model.disconnect(self._model_signal_id) 136 137 self._corealbum = corealbum 138 139 self._art_stack.props.coreobject = self._corealbum 140 141 album_name = self._corealbum.props.title 142 artist = self._corealbum.props.artist 143 144 self._title_label.props.label = album_name 145 self._title_label.props.tooltip_text = album_name 146 147 self._artist_label.props.label = artist 148 self._artist_label.props.tooltip_text = artist 149 150 self._duration_signal_id = self._corealbum.connect( 151 "notify::duration", self._on_release_info_changed) 152 self._year_signal_id = self._corealbum.connect( 153 "notify::year", self._on_release_info_changed) 154 self._set_composer_label() 155 156 self._album_model = self._corealbum.props.model 157 self._model_signal_id = self._album_model.connect_after( 158 "items-changed", self._on_model_items_changed) 159 self._disc_list_box.bind_model(self._album_model, self._create_widget) 160 161 self._album_model.items_changed(0, 0, 0) 162 163 @GObject.Property( 164 type=object, default=None, flags=GObject.ParamFlags.READWRITE) 165 def active_coreobject(self) -> Optional[Union[CoreAlbum, CoreArtist]]: 166 """Get the current CoreObject. 167 168 active_coreobject is used to set the Player playlist 169 AlbumWidget can be used to display an Album in AlbumsView or 170 ArtistsView. In the former case, there is no need to set it: it's 171 already the corealbum. In the later case, the active_coreobject is 172 an artist. It needs to be set. 173 174 :returns: The current CoreAlbum 175 :rtype: CoreAlbum or None 176 """ 177 try: 178 return self._active_coreobject 179 except AttributeError: 180 return self.props.corealbum 181 182 @active_coreobject.setter # type:ignore 183 def active_coreobject( 184 self, coreobject: Union[CoreAlbum, CoreArtist]) -> None: 185 """Update CoreOject used for AlbumWidget. 186 187 :param CoreAlbum corealbum: The CoreAlbum object 188 """ 189 self._active_coreobject = coreobject 190 191 def _create_widget(self, disc: CoreDisc) -> DiscBox: 192 disc_box = DiscBox(self._application, self._corealbum, disc) 193 disc_box.connect('song-activated', self._song_activated) 194 195 self._disc_list_box.bind_property( 196 "selection-mode", disc_box, "selection-mode", 197 GObject.BindingFlags.BIDIRECTIONAL 198 | GObject.BindingFlags.SYNC_CREATE) 199 200 return disc_box 201 202 def _on_model_items_changed( 203 self, model: Gfm.SortListModel, position: int, removed: int, 204 added: int) -> None: 205 n_items: int = model.get_n_items() 206 if n_items == 1: 207 discbox = self._disc_list_box.get_row_at_index(0) 208 discbox.props.show_disc_label = False 209 else: 210 for i in range(n_items): 211 discbox = self._disc_list_box.get_row_at_index(i) 212 discbox.props.show_disc_label = True 213 214 empty_album = (n_items == 0) 215 self._play_button.props.sensitive = not empty_album 216 self._menu_button.props.sensitive = not empty_album 217 218 def _set_composer_label(self) -> None: 219 composer = self._corealbum.props.composer 220 show = False 221 222 if composer: 223 self._composer_label.props.label = composer 224 self._composer_label.props.tooltip_text = composer 225 show = True 226 227 self._composer_label.props.visible = show 228 self._composer_label.props.visible = show 229 230 def _on_release_info_changed( 231 self, klass: CoreAlbum, 232 value: Optional[GObject.ParamSpecString]) -> None: 233 if not self._corealbum: 234 return 235 236 mins = (self._corealbum.props.duration // 60) + 1 237 mins_text = ngettext("{} minute", "{} minutes", mins).format(mins) 238 year = self._corealbum.props.year 239 240 if year == "----": 241 label = mins_text 242 else: 243 label = f"{year}, {mins_text}" 244 245 self._released_label.props.label = label 246 247 def _play(self, coresong: Optional[CoreSong] = None) -> None: 248 signal_id = 0 249 250 def _on_playlist_loaded(klass, playlist_type): 251 self._player.play(coresong) 252 self._coremodel.disconnect(signal_id) 253 254 signal_id = self._coremodel.connect( 255 "playlist-loaded", _on_playlist_loaded) 256 self._coremodel.props.active_core_object = self.props.active_coreobject 257 258 def _song_activated( 259 self, widget: Gtk.Widget, song_widget: SongWidget) -> None: 260 if self.props.selection_mode: 261 song_widget.props.selected = not song_widget.props.selected 262 return 263 264 self._play(song_widget.props.coresong) 265 266 def select_all(self) -> None: 267 self._disc_list_box.select_all() 268 269 def deselect_all(self) -> None: 270 self._disc_list_box.deselect_all() 271 272 def _on_selection_mode_changed( 273 self, widget: Gtk.Widget, value: GObject.ParamSpecBoolean) -> None: 274 if not self.props.selection_mode: 275 self.deselect_all() 276 277 def _on_add_favorites_action( 278 self, action: Gio.SimpleAction, 279 data: Optional[GLib.Variant]) -> None: 280 if self._corealbum: 281 for coredisc in self._corealbum.props.model: 282 for coresong in coredisc.props.model: 283 if not coresong.props.favorite: 284 coresong.props.favorite = True 285 286 def _on_add_playlist_action( 287 self, action: Gio.SimpleAction, 288 data: Optional[GLib.Variant]) -> None: 289 if not self._corealbum: 290 return 291 292 playlist_dialog = PlaylistDialog(self._application) 293 active_window = self._application.props.active_window 294 playlist_dialog.props.transient_for = active_window 295 if playlist_dialog.run() == Gtk.ResponseType.ACCEPT: 296 playlist = playlist_dialog.props.selected_playlist 297 coresongs = [ 298 song 299 for disc in self._corealbum.props.model 300 for song in disc.props.model] 301 playlist.add_songs(coresongs) 302 303 playlist_dialog.destroy() 304 305 def _on_play_action( 306 self, action: Gio.SimpleAction, 307 data: Optional[GLib.Variant]) -> None: 308 self._play() 309 310 @Gtk.Template.Callback() 311 def _on_play_button_clicked(self, button: Gtk.Button) -> None: 312 self._play() 313