mirror of
https://github.com/kovidgoyal/calibre.git
synced 2026-03-17 15:09:17 -04:00
495 lines
18 KiB
Plaintext
495 lines
18 KiB
Plaintext
# vim:fileencoding=utf-8
|
|
# License: GPL v3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
|
|
from __python__ import hash_literals
|
|
|
|
import traceback
|
|
from elementmaker import E
|
|
from gettext import gettext as _
|
|
|
|
from ajax import ajax_send
|
|
from book_list.add import add_books_panel
|
|
from book_list.cover_grid import (
|
|
append_item as cover_grid_append_item, cover_grid_css,
|
|
create_item as create_cover_grid_item, description as COVER_GRID_DESCRIPTION,
|
|
init as init_cover_grid
|
|
)
|
|
from book_list.custom_list import (
|
|
append_item as custom_list_append_item, create_item as create_custom_list_item,
|
|
custom_list_css, description as CUSTOM_LIST_DESCRIPTION,
|
|
init as init_custom_list
|
|
)
|
|
from book_list.details_list import (
|
|
append_item as details_list_append_item, create_item as create_details_list_item,
|
|
description as DETAILS_LIST_DESCRIPTION, details_list_css,
|
|
init as init_details_list
|
|
)
|
|
from book_list.globals import get_session_data
|
|
from book_list.item_list import create_item, create_item_list
|
|
from book_list.library_data import (
|
|
add_more_books, all_virtual_libraries, book_metadata, current_book_ids,
|
|
current_sorted_field, ensure_current_library_data, library_data, load_status,
|
|
loaded_books_query, thumbnail_cache, url_books_query
|
|
)
|
|
from book_list.router import back, home, push_state, update_window_title
|
|
from book_list.search import (
|
|
init as init_search_panel, set_apply_search, tb_config_panel_handler
|
|
)
|
|
from book_list.top_bar import add_button, create_top_bar, set_title_tooltip
|
|
from book_list.ui import query_as_href, set_panel_handler, show_panel
|
|
from dom import add_extra_css, build_rule, clear, ensure_id, set_css
|
|
from modals import error_dialog
|
|
from session import get_interface_data
|
|
from utils import conditional_timeout, parse_url_params, safe_set_inner_html
|
|
from widgets import create_button, create_spinner, enable_escape_key
|
|
|
|
CLASS_NAME = 'book-list-container'
|
|
ITEM_CLASS_NAME = 'book-list-item'
|
|
ALLOWED_MODES = {'cover_grid', 'details_list', 'custom_list'}
|
|
DEFAULT_MODE = 'cover_grid'
|
|
|
|
add_extra_css(def():
|
|
sel = '.' + CLASS_NAME + ' '
|
|
ans = build_rule(sel + '[data-component="top_message"]', margin='1ex 1em')
|
|
ans += cover_grid_css()
|
|
ans += details_list_css()
|
|
ans += custom_list_css()
|
|
return ans
|
|
)
|
|
|
|
book_list_data = {
|
|
'container_id': None, 'shown_book_ids': set(), 'mode': None, 'fetching_more_books': None,
|
|
}
|
|
|
|
def component(name):
|
|
return document.getElementById(book_list_data.container_id).querySelector(f'[data-component="{name}"]')
|
|
|
|
|
|
def clear_grid():
|
|
container = document.getElementById(book_list_data.container_id)
|
|
# We replace the div entirely so that any styles associated with it are also removed
|
|
e = component('book_list')
|
|
bl = E.div(data_component='book_list')
|
|
container.insertBefore(bl, e)
|
|
container.removeChild(e)
|
|
book_list_data.init_grid(bl)
|
|
|
|
|
|
def has_parent_with_class(elem, cls):
|
|
while elem:
|
|
if elem.classList and elem.classList.contains(cls):
|
|
return elem
|
|
elem = elem.parentNode
|
|
|
|
|
|
def save_scroll_position():
|
|
container = document.getElementById(book_list_data.container_id)
|
|
if container:
|
|
left, top = container.offsetLeft, container.offsetTop
|
|
for y in range(5, 100, 5):
|
|
for x in range(25, 125, 5):
|
|
elem = document.elementFromPoint(left + x, top + y)
|
|
p = has_parent_with_class(elem, ITEM_CLASS_NAME)
|
|
if p:
|
|
q = parse_url_params()
|
|
q.book_id = p.dataset.bookId
|
|
push_state(q, replace=True, call_handler=False)
|
|
return
|
|
|
|
|
|
def show_subsequent_panel(name, query=None, replace=False):
|
|
save_scroll_position()
|
|
show_panel(name, query, replace)
|
|
|
|
|
|
def on_book_click(book_id, evt):
|
|
evt.preventDefault()
|
|
show_subsequent_panel('book_details', {'book_id':book_id})
|
|
|
|
|
|
def create_image(book_id, max_width, max_height, on_load):
|
|
return thumbnail_cache.get(book_id, max_width, max_height, on_load)
|
|
|
|
|
|
def render_id(book_id):
|
|
l = book_list_data.shown_book_ids.length
|
|
book_list_data.shown_book_ids.add(book_id)
|
|
if l < book_list_data.shown_book_ids.length:
|
|
metadata = book_metadata(book_id)
|
|
ans = book_list_data.render_book(book_id, metadata, create_image, on_book_click.bind(None, book_id), query_as_href({'book_id': book_id + ''}, 'book_details'))
|
|
ans.classList.add(ITEM_CLASS_NAME)
|
|
ans.dataset.bookId = str(book_id)
|
|
return ans
|
|
|
|
|
|
def render_ids(book_ids):
|
|
book_ids = book_ids or current_book_ids()
|
|
div = component('book_list')
|
|
if div:
|
|
for book_id in book_ids:
|
|
child = render_id(book_id)
|
|
if child:
|
|
book_list_data.append_item(div, child)
|
|
|
|
|
|
def get_view_mode():
|
|
idata = get_interface_data()
|
|
mode = get_session_data().get('view_mode', idata.default_book_list_mode)
|
|
if mode not in ('cover_grid', 'details_list', 'custom_list'):
|
|
mode = DEFAULT_MODE
|
|
return mode
|
|
|
|
|
|
def setup_view_mode(mode, book_list_data):
|
|
if mode not in ALLOWED_MODES:
|
|
mode = DEFAULT_MODE
|
|
book_list_data.mode = mode
|
|
if mode is 'cover_grid':
|
|
book_list_data.render_book = create_cover_grid_item
|
|
book_list_data.init_grid = init_cover_grid
|
|
book_list_data.append_item = cover_grid_append_item
|
|
elif mode is 'details_list':
|
|
book_list_data.render_book = create_details_list_item
|
|
book_list_data.init_grid = init_details_list
|
|
book_list_data.append_item = details_list_append_item
|
|
elif mode is 'custom_list':
|
|
book_list_data.render_book = create_custom_list_item
|
|
book_list_data.init_grid = init_custom_list
|
|
book_list_data.append_item = custom_list_append_item
|
|
return mode
|
|
|
|
|
|
def apply_view_mode(mode=DEFAULT_MODE):
|
|
if book_list_data.mode is mode:
|
|
return
|
|
setup_view_mode(mode, book_list_data)
|
|
clear_grid()
|
|
render_ids()
|
|
|
|
|
|
# More button {{{
|
|
|
|
def update_fetching_status():
|
|
more = component('more_button')
|
|
if not more:
|
|
return
|
|
if book_list_data.fetching_more_books:
|
|
more.firstChild.style.display = 'none'
|
|
more.lastChild.style.display = 'block'
|
|
elif library_data.search_result.total_num > book_list_data.shown_book_ids.length:
|
|
more.firstChild.style.display = 'block'
|
|
more.lastChild.style.display = 'none'
|
|
else:
|
|
more.firstChild.style.display = 'none'
|
|
more.lastChild.style.display = 'none'
|
|
|
|
|
|
def abort_get_more_books(no_update):
|
|
if book_list_data.fetching_more_books:
|
|
book_list_data.fetching_more_books.abort()
|
|
book_list_data.fetching_more_books = None
|
|
if not no_update:
|
|
update_fetching_status()
|
|
|
|
|
|
def got_more_books(end_type, xhr, event):
|
|
if book_list_data.fetching_more_books is not xhr:
|
|
return # Fetching was aborted
|
|
book_list_data.fetching_more_books = None
|
|
update_fetching_status()
|
|
if end_type is 'load':
|
|
try:
|
|
data = JSON.parse(xhr.responseText)
|
|
if not data.search_result.book_ids:
|
|
raise Exception('No books ids object in search result from server')
|
|
add_more_books(data)
|
|
render_ids(data.search_result.book_ids)
|
|
except Exception:
|
|
error_dialog(_('Could not get more books'), _('Server returned an invalid response'), traceback.format_exc())
|
|
elif end_type is not 'abort':
|
|
error_dialog(_('Could not get more books'), xhr.error_html)
|
|
|
|
|
|
def get_more_books():
|
|
data = {'offset':book_list_data.shown_book_ids.length}
|
|
for key in 'query', 'sort', 'sort_order', 'vl':
|
|
data[key] = library_data.search_result[key]
|
|
book_list_data.fetching_more_books = ajax_send(
|
|
'interface-data/more-books', data, got_more_books,
|
|
query={'library_id':loaded_books_query().library_id}
|
|
)
|
|
update_fetching_status()
|
|
|
|
|
|
def create_more_button(more):
|
|
more.appendChild(create_button(
|
|
_('Show more books'), 'cloud-download', get_more_books
|
|
))
|
|
more.lastChild.setAttribute('rel', 'next')
|
|
set_css(more.firstChild, display='block', margin_left='auto', margin_right='auto', margin_top='1rem')
|
|
set_css(more, font_size='1.5rem', padding_top='1.5rem', margin_bottom='1.5rem', text_align='center', display='flex')
|
|
more.appendChild(E.div(
|
|
create_spinner(), '\xa0' + _('Fetching metadata for more books, please wait') + '…',
|
|
style='margin-left:auto; margin-right:auto; display:none')
|
|
)
|
|
update_fetching_status()
|
|
|
|
|
|
# }}}
|
|
|
|
def show_top_message():
|
|
container = component('top_message')
|
|
q = loaded_books_query()
|
|
if not q.search and not q.vl:
|
|
container.style.display = 'none'
|
|
return
|
|
if q.vl:
|
|
container.appendChild(E.span(
|
|
E.span(_('Showing Virtual library: {} ').format(q.vl)),
|
|
create_button(_('Close'), 'close', def(): show_vl();, _('Show all books in library'))
|
|
))
|
|
if q.search:
|
|
container.appendChild(E.br())
|
|
if q.search:
|
|
text = _('Showing books matching: {} ')
|
|
if not library_data.search_result.total_num:
|
|
text = _('No books matching: {} ')
|
|
container.appendChild(E.span(
|
|
E.span(text.format(library_data.search_result.query)),
|
|
create_button(_('Clear'), 'close', def(): search();, _('Clear this search'))
|
|
))
|
|
|
|
def create_books_list(container):
|
|
book_list_data.container_id = ensure_id(container)
|
|
book_list_data.shown_book_ids = set()
|
|
book_list_data.mode = None
|
|
abort_get_more_books(True)
|
|
container.appendChild(E.div(data_component='top_message'))
|
|
container.appendChild(E.div(data_component='book_list'))
|
|
container.appendChild(E.div(data_component='more_button'))
|
|
show_top_message()
|
|
apply_view_mode(get_view_mode())
|
|
create_more_button(container.lastChild)
|
|
add_button(container.parentNode, icon='plus', action=show_subsequent_panel.bind(None, 'book_list^add'), tooltip=_('Add books'))
|
|
add_button(container.parentNode, icon='sort-amount-desc', action=show_subsequent_panel.bind(None, 'book_list^sort'), tooltip=_('Sort books'))
|
|
add_button(container.parentNode, icon='search', action=show_subsequent_panel.bind(None, 'book_list^search'), tooltip=_('Search for books'))
|
|
add_button(container.parentNode, icon='ellipsis-v', action=show_subsequent_panel.bind(None, 'book_list^more_actions'), tooltip=_('More actions'))
|
|
q = parse_url_params()
|
|
scrolled = False
|
|
if q.book_id:
|
|
e = container.querySelector(f'.{ITEM_CLASS_NAME}[data-book-id="{q.book_id}"]')
|
|
if e and e.scrollIntoView:
|
|
e.scrollIntoView(True)
|
|
# Now scroll extra corresponding to top bar size
|
|
window.scrollBy(0, -container.offsetTop)
|
|
scrolled = True
|
|
if not scrolled:
|
|
window.scrollTo(0, 0)
|
|
q = loaded_books_query()
|
|
if not q.search and library_data.search_result.total_num < 1:
|
|
div = component('book_list')
|
|
div.appendChild(E.div(_('No books found'), style='margin: 1ex 1em'))
|
|
|
|
|
|
def check_for_books_loaded():
|
|
container = this
|
|
container_id = container.id
|
|
if load_status.loading:
|
|
conditional_timeout(container_id, 5, check_for_books_loaded)
|
|
return
|
|
container = container.lastChild
|
|
clear(container)
|
|
if not load_status.ok:
|
|
if load_status.http_error_code is 401:
|
|
# Unauthorized, keep retrying until the user provides a
|
|
# username/password
|
|
container.appendChild(E.div(_('Username/password required, retrying...')))
|
|
ensure_current_library_data()
|
|
conditional_timeout(container_id, 100, check_for_books_loaded)
|
|
return
|
|
err = E.div()
|
|
safe_set_inner_html(err, load_status.error_html)
|
|
container.appendChild(E.div(
|
|
style='margin: 1ex 1em',
|
|
E.div(_('Failed to load books from calibre library, with error:')),
|
|
err,
|
|
E.div(
|
|
style='margin-top: 1em; border-top: solid 1px currentColor; padding-top: 1ex;',
|
|
E.a(onclick=def(): home(replace=True);, href='javascript: void(0)', class_='blue-link', _('Go back to the home page')))
|
|
),
|
|
)
|
|
return
|
|
create_books_list(container)
|
|
if library_data.search_result and jstype(library_data.search_result.num_books_without_search) is 'number':
|
|
set_title_tooltip(this, _('Total number of books: {}').format(library_data.search_result.num_books_without_search))
|
|
|
|
|
|
def init(container_id):
|
|
interface_data = get_interface_data()
|
|
ensure_current_library_data()
|
|
container = document.getElementById(container_id)
|
|
lid = container.dataset.library_id = url_books_query().library_id
|
|
title = interface_data.library_map[lid]
|
|
update_window_title(title)
|
|
create_top_bar(container, title=title, action=def(): save_scroll_position(), home();, icon='home')
|
|
container.appendChild(E.div(class_=CLASS_NAME))
|
|
container.lastChild.appendChild(E.div(_('Loading books from the {} calibre library, please wait...').format(title), style='margin: 1ex 1em'))
|
|
conditional_timeout(container_id, 5, check_for_books_loaded)
|
|
|
|
|
|
# Sorting {{{
|
|
|
|
def change_sort(field, order):
|
|
abort_get_more_books()
|
|
sd = get_session_data()
|
|
so = sd.get('last_sort_order')
|
|
order = order or so[field] or 'asc'
|
|
order = 'asc' if order is 'asc' else 'desc'
|
|
q = loaded_books_query()
|
|
q.sort = field + '.' + order
|
|
so[field] = order
|
|
sd.set('last_sort_order', so)
|
|
sd.set_library_option(q.library_id, 'sort', q.sort)
|
|
show_panel('book_list', query=q, replace=True)
|
|
|
|
|
|
def create_sort_panel(container_id):
|
|
if not library_data.sortable_fields:
|
|
show_panel('book_list', replace=True)
|
|
return
|
|
container = document.getElementById(container_id)
|
|
close_action, close_icon = back, 'close'
|
|
if parse_url_params().close_action is 'home':
|
|
close_action, close_icon = def(): home();, 'home'
|
|
create_top_bar(container, title=_('Sort books by…'), action=close_action, icon=close_icon)
|
|
items = []
|
|
csf, csf_order = current_sorted_field()
|
|
new_sort_order = 'desc' if csf_order is 'asc' else 'asc'
|
|
if csf is 'date':
|
|
csf = 'timestamp'
|
|
for field, name in library_data.sortable_fields:
|
|
subtitle = icon_name = None
|
|
if field is csf:
|
|
subtitle = _('Reverse current sort order')
|
|
icon_name = 'sort-amount-asc' if csf_order is 'asc' else 'sort-amount-desc'
|
|
action = change_sort.bind(None, field, new_sort_order)
|
|
else:
|
|
action = change_sort.bind(None, field, None)
|
|
items.push(create_item(name, subtitle=subtitle, icon=icon_name, action=action))
|
|
container.appendChild(E.div())
|
|
create_item_list(container.lastChild, items, _('Change how the list of books is sorted'))
|
|
enable_escape_key(container, close_action)
|
|
# }}}
|
|
|
|
# Searching {{{
|
|
|
|
|
|
def search_query_for(query):
|
|
q = loaded_books_query()
|
|
if query:
|
|
q.search = query
|
|
else:
|
|
v'delete q.search'
|
|
return q
|
|
|
|
|
|
def search(query, replace=False):
|
|
show_panel('book_list', query=search_query_for(query), replace=replace)
|
|
|
|
set_apply_search(def(query): search(query, True);)
|
|
|
|
# }}}
|
|
|
|
# Virtual libraries {{{
|
|
def create_vl_panel(container_id):
|
|
if not library_data.sortable_fields:
|
|
show_panel('book_list', replace=True)
|
|
return
|
|
container = document.getElementById(container_id)
|
|
create_top_bar(container, title=_('Choose Virtual library…'), action=back, icon='close')
|
|
items = []
|
|
vls = all_virtual_libraries()
|
|
vl_names = Object.keys(vls).sort(def (a, b): return a.toLowerCase().localeCompare(b.toLowerCase());)
|
|
for name in vl_names:
|
|
items.push(create_item(name, subtitle=vls[name], action=show_vl.bind(None, name, True)))
|
|
container.appendChild(E.div())
|
|
create_item_list(container.lastChild, items, _('Choose a Virtual library to browse from the list below'))
|
|
enable_escape_key(container, back)
|
|
|
|
|
|
def show_vl(vl_name, replace):
|
|
q = loaded_books_query()
|
|
q.vl = vl_name or None
|
|
show_panel('book_list', query=q, replace=replace or False)
|
|
|
|
# }}}
|
|
|
|
# Modes {{{
|
|
def create_mode_panel(container_id):
|
|
if not library_data.sortable_fields:
|
|
show_panel('book_list', replace=True)
|
|
return
|
|
container = document.getElementById(container_id)
|
|
create_top_bar(container, title=_('Choose book list mode…'), action=back, icon='close')
|
|
items = []
|
|
current_mode = get_view_mode()
|
|
|
|
def ci(title, desc, name):
|
|
ic = 'check' if name is current_mode else None
|
|
items.push(create_item(title, subtitle=desc, icon=ic, action=def():
|
|
if name is not current_mode:
|
|
get_session_data().set('view_mode', name)
|
|
back()
|
|
))
|
|
|
|
ci(_('Cover grid'), COVER_GRID_DESCRIPTION(), 'cover_grid')
|
|
ci(_('Detailed list'), DETAILS_LIST_DESCRIPTION(), 'details_list')
|
|
ci(_('Custom list'), CUSTOM_LIST_DESCRIPTION(), 'custom_list')
|
|
container.appendChild(E.div())
|
|
create_item_list(container.lastChild, items, _('Choose a display mode for the list of books from below'))
|
|
enable_escape_key(container, back)
|
|
|
|
# }}}
|
|
|
|
# More actions {{{
|
|
|
|
def create_more_actions_panel(container_id):
|
|
container = document.getElementById(container_id)
|
|
create_top_bar(container, title=_('More actions…'), action=back, icon='close')
|
|
items = [
|
|
create_item(_('Book list mode'), subtitle=_('Change how the list of books is displayed'), action=def():
|
|
show_panel('book_list^choose_mode', replace=True)
|
|
),
|
|
create_item(_('A random book'), subtitle=_('Choose a random book from the library'), action=def():
|
|
query = {'book_id':'0'}
|
|
show_panel('book_details', query=query, replace=True)
|
|
),
|
|
]
|
|
vls = all_virtual_libraries()
|
|
if vls and Object.keys(vls).length > 0:
|
|
items.insert(1, create_item(_('Choose Virtual library'), subtitle=_('Browse books in a Virtual library'), action=def():
|
|
show_panel('book_list^vl', replace=True)
|
|
))
|
|
container.appendChild(E.div())
|
|
create_item_list(container.lastChild, items)
|
|
enable_escape_key(container, back)
|
|
# }}}
|
|
|
|
# Adding books {{{
|
|
def create_add_panel(container_id):
|
|
if not library_data.sortable_fields:
|
|
show_panel('book_list', replace=True)
|
|
return
|
|
add_books_panel(container_id)
|
|
|
|
# }}}
|
|
|
|
|
|
set_panel_handler('book_list', init)
|
|
set_panel_handler('book_list^add', create_add_panel)
|
|
set_panel_handler('book_list^sort', create_sort_panel)
|
|
set_panel_handler('book_list^vl', create_vl_panel)
|
|
set_panel_handler('book_list^search', init_search_panel)
|
|
set_panel_handler('book_list^choose_mode', create_mode_panel)
|
|
set_panel_handler('book_list^search^prefs', tb_config_panel_handler())
|
|
set_panel_handler('book_list^more_actions', create_more_actions_panel)
|