1# Copyright (c) 2014-2021 Cedric Bellegarde <cedric.bellegarde@adishatz.org>
2# This program is free software: you can redistribute it and/or modify
3# it under the terms of the GNU General Public License as published by
4# the Free Software Foundation, either version 3 of the License, or
5# (at your option) any later version.
6# This program is distributed in the hope that it will be useful,
7# but WITHOUT ANY WARRANTY; without even the implied warranty of
8# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9# GNU General Public License for more details.
10# You should have received a copy of the GNU General Public License
11# along with this program. If not, see <http://www.gnu.org/licenses/>.
12
13from gi.repository import Gtk, Gdk, GLib, Gio, GdkPixbuf, GObject
14
15from gettext import gettext as _
16
17from lollypop.logger import Logger
18from lollypop.utils import emit_signal
19from lollypop.define import App, ArtSize, ArtBehaviour
20
21
22class ArtworkSearchChild(Gtk.FlowBoxChild):
23    """
24        Child for ArtworkSearch
25    """
26
27    __gsignals__ = {
28        "hidden": (GObject.SignalFlags.RUN_FIRST, None, (bool,)),
29    }
30
31    def __init__(self, api, view_type):
32        """
33            Init child
34            @param api as str
35            @param view_type as ViewType
36        """
37        Gtk.FlowBoxChild.__init__(self)
38        self.__bytes = None
39        self.__view_type = view_type
40        self.__api = api
41        self.__image = Gtk.Image()
42        self.__image.show()
43        self.__label = Gtk.Label()
44        self.__label.show()
45        grid = Gtk.Grid()
46        grid.set_orientation(Gtk.Orientation.VERTICAL)
47        grid.show()
48        grid.add(self.__image)
49        grid.add(self.__label)
50        grid.set_row_spacing(5)
51        self.__image.get_style_context().add_class("cover-frame")
52        self.__image.set_property("halign", Gtk.Align.CENTER)
53        self.__image.set_property("valign", Gtk.Align.CENTER)
54        self.add(grid)
55
56    def populate(self, bytes, art_manager, art_size):
57        """
58            Populate images with bytes
59            @param bytes as bytes
60            @param art_manager as ArtworkManager
61            @param art_size as int
62            @return bool if success
63        """
64        try:
65            scale_factor = self.get_scale_factor()
66            gbytes = GLib.Bytes.new(bytes)
67            stream = Gio.MemoryInputStream.new_from_bytes(gbytes)
68            if stream is not None:
69                pixbuf = GdkPixbuf.Pixbuf.new_from_stream(stream, None)
70                if self.__api is None:
71                    text = "%sx%s" % (pixbuf.get_width(),
72                                      pixbuf.get_height())
73                else:
74                    text = "%s: %sx%s" % (self.__api,
75                                          pixbuf.get_width(),
76                                          pixbuf.get_height())
77                self.__label.set_text(text)
78                pixbuf = art_manager.load_behaviour(
79                                                pixbuf,
80                                                art_size * scale_factor,
81                                                art_size * scale_factor,
82                                                ArtBehaviour.CROP)
83                stream.close()
84                self.__bytes = bytes
85                surface = Gdk.cairo_surface_create_from_pixbuf(
86                                                       pixbuf,
87                                                       scale_factor,
88                                                       None)
89                self.__image.set_from_surface(surface)
90                return True
91        except Exception as e:
92            Logger.error("ArtworkSearchChild::__get_image: %s" % e)
93        return False
94
95    @property
96    def bytes(self):
97        """
98            Get bytes associated to widget
99            @return bytes
100        """
101        return self.__bytes
102
103
104class ArtworkSearchWidget(Gtk.Grid):
105    """
106        Search for artwork
107    """
108
109    def __init__(self, view_type):
110        """
111            Init widget
112            @param view_type as ViewType
113        """
114        Gtk.Grid.__init__(self)
115        self.__view_type = view_type
116        self.__timeout_id = None
117        self.__uri_artwork_id = None
118        self._loaders = 0
119        self._cancellable = Gio.Cancellable()
120        builder = Gtk.Builder()
121        builder.add_from_resource("/org/gnome/Lollypop/ArtworkSearch.ui")
122        builder.connect_signals(self)
123        widget = builder.get_object("widget")
124        self.__stack = builder.get_object("stack")
125        self.__entry = builder.get_object("entry")
126        self.__spinner = builder.get_object("spinner")
127
128        self._flowbox = Gtk.FlowBox()
129        self._flowbox.set_selection_mode(Gtk.SelectionMode.SINGLE)
130        self._flowbox.connect("child-activated", self._on_activate)
131        self._flowbox.set_max_children_per_line(100)
132        self._flowbox.set_property("row-spacing", 10)
133        self._flowbox.set_property("valign", Gtk.Align.START)
134        self._flowbox.set_vexpand(True)
135        self._flowbox.set_homogeneous(True)
136        self._flowbox.show()
137
138        self.__label = builder.get_object("label")
139        self.__label.set_text(_("Select artwork"))
140
141        if App().window.folded:
142            self._art_size = ArtSize.MEDIUM
143            widget.add(self._flowbox)
144        else:
145            self._art_size = ArtSize.BANNER
146            scrolled = Gtk.ScrolledWindow.new()
147            scrolled.show()
148            scrolled.set_size_request(700, 400)
149            scrolled.set_vexpand(True)
150            viewport = Gtk.Viewport.new()
151            viewport.show()
152            viewport.add(self._flowbox)
153            scrolled.add(viewport)
154            widget.add(scrolled)
155        self.add(widget)
156        self.connect("unmap", self.__on_unmap)
157
158    def populate(self):
159        """
160            Populate view
161        """
162        try:
163            grid = Gtk.Grid()
164            grid.set_orientation(Gtk.Orientation.VERTICAL)
165            grid.show()
166            grid.set_row_spacing(5)
167            image = Gtk.Image.new_from_icon_name("edit-clear-all-symbolic",
168                                                 Gtk.IconSize.INVALID)
169            image.set_pixel_size(self._art_size)
170            context = image.get_style_context()
171            context.add_class("cover-frame")
172            padding = context.get_padding(Gtk.StateFlags.NORMAL)
173            border = context.get_border(Gtk.StateFlags.NORMAL)
174            image.set_size_request(self._art_size + padding.left +
175                                   padding.right + border.left + border.right,
176                                   self._art_size + padding.top +
177                                   padding.bottom + border.top + border.bottom)
178            image.show()
179            label = Gtk.Label.new(_("Remove"))
180            label.show()
181            grid.add(image)
182            grid.add(label)
183            grid.set_property("valign", Gtk.Align.CENTER)
184            grid.set_property("halign", Gtk.Align.CENTER)
185            self._flowbox.add(grid)
186            self._search_for_artwork()
187        except Exception as e:
188            Logger.error("ArtworkSearchWidget::populate(): %s", e)
189
190    def stop(self):
191        """
192            Stop loading
193        """
194        self._cancellable.cancel()
195
196    @property
197    def view_type(self):
198        """
199            Get view type
200            @return ViewType
201        """
202        return self.__view_type
203
204#######################
205# PROTECTED           #
206#######################
207    def _search_for_artwork(self):
208        """
209            Search artwork on the web
210        """
211        self._loaders = 0
212        self._cancellable = Gio.Cancellable()
213        self.__spinner.start()
214
215    def _save_from_filename(self, filename):
216        pass
217
218    def _on_button_clicked(self, button):
219        """
220            Show file chooser
221            @param button as Gtk.button
222        """
223        dialog = Gtk.FileChooserDialog()
224        dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
225        dialog.add_buttons(Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
226        dialog.set_transient_for(App().window)
227        file_filter = Gtk.FileFilter.new()
228        file_filter.add_pixbuf_formats()
229        dialog.set_filter(file_filter)
230        emit_signal(self, "hidden", True)
231        response = dialog.run()
232        if response == Gtk.ResponseType.OK:
233            self._save_from_filename(dialog.get_filename())
234        dialog.destroy()
235
236    def _get_current_search(self):
237        """
238            Return current searches
239            @return str
240        """
241        return self.__entry.get_text()
242
243    def _on_search_changed(self, entry):
244        """
245            Launch search based on current text
246            @param entry as Gtk.Entry
247        """
248        if self.__timeout_id is not None:
249            GLib.source_remove(self.__timeout_id)
250        self.__timeout_id = GLib.timeout_add(1000,
251                                             self.__on_search_timeout)
252
253    def _on_activate(self, flowbox, child):
254        pass
255
256    def _on_uri_artwork_found(self, art, uris):
257        """
258            Load content in view
259            @param art as Art
260            @param uris as (str, str)/None
261        """
262        if uris:
263            (uri, api) = uris.pop(0)
264            App().task_helper.load_uri_content(uri,
265                                               self._cancellable,
266                                               self.__on_load_uri_content,
267                                               api,
268                                               uris,
269                                               self._cancellable)
270        else:
271            self._loaders -= 1
272            if self._loaders == 0:
273                self.__spinner.stop()
274
275#######################
276# PRIVATE             #
277#######################
278    def __add_pixbuf(self, content, api):
279        """
280            Add content to view
281            @param content as bytes
282            @param api as str
283        """
284        child = ArtworkSearchChild(api, self.__view_type)
285        child.show()
286        status = child.populate(content, self.art, self._art_size)
287        if status:
288            child.set_name("web")
289            self._flowbox.add(child)
290        else:
291            child.destroy()
292
293    def __on_unmap(self, widget):
294        """
295            Cancel loading
296            @param widget as Gtk.Widget
297        """
298        self._cancellable.cancel()
299
300    def __on_load_uri_content(self, uri, loaded, content,
301                              api, uris, cancellable):
302        """
303            Add loaded pixbuf
304            @param uri as str
305            @param loaded as bool
306            @param content as bytes
307            @param api as str
308            @param uris as [str]
309            @param cancellable as Gio.Cancellable
310        """
311        try:
312            if loaded:
313                self.__add_pixbuf(content, api)
314            if uris and not cancellable.is_cancelled():
315                (uri, api) = uris.pop(0)
316                App().task_helper.load_uri_content(uri,
317                                                   cancellable,
318                                                   self.__on_load_uri_content,
319                                                   api,
320                                                   uris,
321                                                   cancellable)
322            else:
323                self._loaders -= 1
324        except Exception as e:
325            self._loaders -= 1
326            Logger.warning(
327                "ArtworkSearchWidget::__on_load_uri_content(): %s", e)
328        if self._loaders == 0:
329            self.__spinner.stop()
330
331    def __on_search_timeout(self):
332        """
333            Populate widget
334        """
335        self.__timeout_id = None
336        self._cancellable.cancel()
337        self._cancellable = Gio.Cancellable()
338        for child in self._flowbox.get_children():
339            if child.get_name() == "web":
340                child.destroy()
341        GLib.timeout_add(500, self._search_for_artwork)
342