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