diff --git a/src/pyj/book_list/cover_grid.pyj b/src/pyj/book_list/cover_grid.pyj index 7064b81072..71a45c952c 100644 --- a/src/pyj/book_list/cover_grid.pyj +++ b/src/pyj/book_list/cover_grid.pyj @@ -6,7 +6,7 @@ from dom import clear, set_css, build_rule from elementmaker import E from gettext import gettext as _ -from book_list.library_data import cover_url, book_metadata +from book_list.library_data import thumbnail_cache, book_metadata COVER_GRID_CLASS = 'book-list-cover-grid' THUMBNAIL_MAX_WIDTH = 300 @@ -41,26 +41,26 @@ def init(container): for i in range(12): container.lastChild.appendChild(E.div(class_='cover-grid-filler')) -def on_img_load_error(err): - img = err.target +def on_img_load(img, load_type): div = img.parentNode if not div: return - clear(div) - div.appendChild(E.div( - E.h2(img.getAttribute('data-title'), style='text-align:center; font-size:larger; font-weight: bold'), - E.div(_('by'), style='text-align: center'), - E.h2(img.getAttribute('data-authors'), style='text-align:center; font-size:larger; font-weight: bold') - )) - set_css(div, border='dashed 1px currentColor', border_radius=BORDER_RADIUS+'px') + if load_type is not 'load': + metadata = book_metadata(int(img.dataset.bookId)) + if metadata: + clear(div) + div.appendChild(E.div( + E.h2(metadata.title, style='text-align:center; font-size:larger; font-weight: bold'), + E.div(_('by'), style='text-align: center'), + E.h2(metadata.authors.join(' & '), style='text-align:center; font-size:larger; font-weight: bold') + )) + set_css(div, border='dashed 1px currentColor', border_radius=BORDER_RADIUS+'px') def create_item(book_id, show_book_details): - curl = cover_url(book_id, THUMBNAIL_MAX_WIDTH, THUMBNAIL_MAX_HEIGHT) metadata = book_metadata(book_id) - alt = _('{} by {}').format(metadata.title, metadata.authors.join(' & ')) - img = E.img(src=curl, alt=alt, title=alt, data_title=metadata.title, data_authors=metadata.authors.join(' & ')) - img.onerror = on_img_load_error - + authors = metadata.authors.join(' & ') + img = thumbnail_cache.get(book_id, THUMBNAIL_MAX_WIDTH, THUMBNAIL_MAX_HEIGHT, on_img_load) + img.setAttribute('alt', _('{} by {}').format(metadata.title, authors)) ans = E.div(img, data_book_id=str(book_id), onclick=show_book_details) return ans diff --git a/src/pyj/book_list/library_data.pyj b/src/pyj/book_list/library_data.pyj index 330670dc75..1bdefc2b50 100644 --- a/src/pyj/book_list/library_data.pyj +++ b/src/pyj/book_list/library_data.pyj @@ -3,6 +3,7 @@ from __python__ import hash_literals, bound_methods from ajax import ajax +from lru_cache import LRUCache from session import get_interface_data from utils import parse_url_params @@ -77,7 +78,7 @@ def fetch_init_data(): load_status.current_fetch.send() -def cover_url(book_id, width, height): +def thumbnail_url(book_id, width, height): return 'get/thumb/{}/{}?sz={}x{}'.format(book_id, loaded_books_query().library_id, Math.ceil(width * window.devicePixelRatio), Math.ceil(height * window.devicePixelRatio)) @@ -95,3 +96,41 @@ def ensure_current_library_data(): break if not matches: fetch_init_data() + + +class ThumbnailCache: + + # Cache to prevent browser from issuing HTTP requests when thumbnails pages + # are destroyed/rebuilt. + + def __init__(self, size=250): + self.cache = LRUCache(size) + + def get(self, book_id, width, height, callback): + url = thumbnail_url(book_id, width, height) + item = self.cache.get(url) + if item is None: + img = new Image() + item = {'img':img, 'load_type':None, 'callbacks':v'[callback]'} + img.onerror = self.load_finished.bind(None, item, 'error') + img.onload = self.load_finished.bind(None, item, 'load') + img.onabort = self.load_finished.bind(None, item, 'abort') + img.dataset.bookId = str(book_id) + img.src = url + self.cache.set(url, item) + return img + if item.load_type is None: + if item.callbacks.indexOf(callback) < 0: + item.callbacks.push(callback) + else: + callback(item.img, item.load_type) + return item.img + + def load_finished(self, item, load_type): + item.load_type = load_type + img = item.img + img.onload = img.onerror = img.onabort = None + for callback in item.callbacks: + callback(img, load_type) + +thumbnail_cache = ThumbnailCache() diff --git a/src/pyj/lru_cache.pyj b/src/pyj/lru_cache.pyj new file mode 100644 index 0000000000..e35125d940 --- /dev/null +++ b/src/pyj/lru_cache.pyj @@ -0,0 +1,70 @@ +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2017, Kovid Goyal +from __python__ import hash_literals, bound_methods + + +def lru_node(key, val): + return {'key': key, 'val':val, 'prev':None, 'next':None} + + +class LRUCache: + + def __init__(self, size): + self.limit = 200 + self.clear(size) + + def set_head(self, node): + node.next = self.head + node.prev = None + if self.head is not None: + self.head.prev = node + self.head = node + if self.tail is None: + self.tail = node + self.size += 1 + self.map[node.key] = node + + def pop(self, key): + node = self.map[key] + if not node: + return + if node.prev is not None: + node.prev.next = node.next + else: + self.head = node.next + if node.next is not None: + node.next.prev = node.prev + else: + self.tail = node.prev + v'delete self.map[key]' + self.size -= 1 + + def set(self, key, val): + node = lru_node(key, val) + existing = self.map[key] + if existing: + existing.value = node.value + self.pop(node.key) + elif self.size > self.limit: + v'delete self.map[self.tail.key]' + self.size -= 1 + self.tail = self.tail.prev + self.tail.next = None + self.set_head(node) + + def get(self, key, defval): + existing = self.map[key] + if not existing: + return None if defval is undefined else defval + val = existing.value + node = lru_node(key, val) + self.pop(key) + self.set_head(node) + return val + + def clear(self, size): + self.size = 0 + self.map = {} + self.head = self.tail = None + if jstype(size) is 'number': + self.limit = size