diff --git a/src/calibre/srv/ajax.py b/src/calibre/srv/ajax.py index c2bbaa1ede..4000d4f6b4 100644 --- a/src/calibre/srv/ajax.py +++ b/src/calibre/srv/ajax.py @@ -573,6 +573,7 @@ def interface_data(ctx, rd, library_id): session = rd.session ans = {'session_data': {k:session[k] for k in defaults.iterkeys()}} ans['library_map'], ans['default_library'] = ctx.library_map + ans['library_id'] = library_id or ans['default_library'] sorts, orders = [], [] for x in ans['session_data']['sort'].split(','): s, o = x.partition(':')[::2] diff --git a/src/pyj/book_list/ui.pyj b/src/pyj/book_list/ui.pyj index 7606cfd8a5..5b47460d99 100644 --- a/src/pyj/book_list/ui.pyj +++ b/src/pyj/book_list/ui.pyj @@ -3,8 +3,10 @@ from book_list.theme import get_color from book_list.top_bar import TopBar +from book_list.views import BooksView from dom import set_css from gettext import gettext as _ +from utils import debounce class BarState: @@ -27,4 +29,9 @@ class BookList: self.states.append(ibs) self.top_bar = TopBar() self.top_bar.apply_state(ibs.left_state, ibs.buttons) + self.books_view = BooksView(interface_data) ibs.left_state.run_animation = False + window.addEventListener('resize', debounce(bind(self.on_resize, self), 250)) + + def on_resize(self): + self.books_view.on_resize() diff --git a/src/pyj/book_list/views.pyj b/src/pyj/book_list/views.pyj new file mode 100644 index 0000000000..2f053a5a88 --- /dev/null +++ b/src/pyj/book_list/views.pyj @@ -0,0 +1,102 @@ +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2015, Kovid Goyal + +from dom import set_css +from elementmaker import E + +bv_counter = 0 +THUMBNAIL_MAX_WIDTH = 300 +THUMBNAIL_MAX_HEIGHT = 400 + +class BooksView: + + def __init__(self, interface_data): + nonlocal bv_counter + bv_counter += 1 + self.interface_data = interface_data + self.search_result = interface_data['search_result'] + self.metadata_map = interface_data['metadata'] + self.container_id = 'books-view-' + bv_counter + div = E.div( + id=self.container_id, style='display:none', + E.style(), + E.div(), + E.div(id='get-more-books') + ) + document.body.appendChild(div) + self.render_book = bind(self.cover_grid_item, self) + self.init_grid = bind(self.init_cover_grid, self) + self.resize_grid = bind(self.resize_cover_grid, self) + self.mode = 'cover_grid' + self.init_grid() + self.render_ids() + self.resize_grid() + self.is_visible = True + + @property + def container(self): + return document.getElementById(self.container_id) + + @property + def grid(self): + return self.container.lastChild.previousSibling + + @property + def is_visible(self): + self.container.style.display == 'block' + + @is_visible.setter + def is_visible(self, val): + self.container.style.display = 'block' if val else 'none' + + def clear(self): + # We replace the div entirely so that any styles associated with it are + # also removed + c = self.container + c.removeChild(self.grid) + c.insertBefore(E.div(), c.lastChild) + self.init_grid() + + def init_cover_grid(self): + div = self.grid + set_css(div, display='flex', flex_wrap='wrap', justify_content='flex-start', align_items='flex-end', align_content='flex-start') + self.cover_grid_width = self.cover_grid_height = -1 + + def resize_cover_grid(self): + w, h = window.innerWidth, window.innerHeight + if w <= h: + # Portrait + MAX_WIDTH, MIN_WIDTH = THUMBNAIL_MAX_WIDTH, THUMBNAIL_MAX_WIDTH // 2 + no_wider_than = (w - 50) // 2 + width = min(no_wider_than, MAX_WIDTH) + width = max(width, MIN_WIDTH) + height = int((4 / 3) * width) + else: + # Landscape + MAX_HEIGHT, MIN_HEIGHT = THUMBNAIL_MAX_HEIGHT, THUMBNAIL_MAX_HEIGHT // 2 + no_taller_than = (h - 75) // 2 + height = min(no_taller_than, MAX_HEIGHT) + height = max(height, MIN_HEIGHT) + width = int((3 / 4) * height) + width, height = width + 'px', height + 'px' + for child in self.grid.childNodes: + set_css(child, width=width, height=height) + + def cover_grid_item(self, book_id): + cover_url = str.format('get/thumb/{}/{}?sz={}x{}', book_id, self.interface_data['library_id'], THUMBNAIL_MAX_WIDTH, THUMBNAIL_MAX_HEIGHT) + return E.div( + style='margin: 10px; display: flex; align-content: flex-end; align-items: flex-end; justify-content: space-around', + data_book_id=str(book_id), + E.img(src=cover_url, style='max-width: 100%; max-height: 100%; display: block; cursor: pointer; width:auto; height:auto') + ) + + def render_ids(self, book_ids=None): + if book_ids is None: + book_ids = self.search_result['book_ids'] + div = self.grid + for book_id in book_ids: + div.appendChild(self.render_book(book_id)) + + def on_resize(self): + if self.resize_grid: + self.resize_grid() diff --git a/src/pyj/utils.pyj b/src/pyj/utils.pyj new file mode 100644 index 0000000000..b31517f145 --- /dev/null +++ b/src/pyj/utils.pyj @@ -0,0 +1,22 @@ +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2015, Kovid Goyal + +def debounce(func, wait, immediate=False): + # Returns a function, that, as long as it continues to be invoked, will not + # be triggered. The function will be called after it stops being called for + # N milliseconds. If `immediate` is True, trigger the function on the + # leading edge, instead of the trailing. + timeout = None + return def debounce_inner(): # noqa: unused-local + nonlocal timeout + context, args = this, arguments + def later(): + nonlocal timeout + timeout = None + if not immediate: + func.apply(context, args) + call_now = immediate and not timeout + window.clearTimeout(timeout) + timeout = window.setTimeout(later, wait) + if call_now: + func.apply(context, args)