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