mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Start work on porting the Tag Browser panel
This commit is contained in:
parent
3942b56390
commit
ca8fed1bd4
@ -4,15 +4,27 @@ from __python__ import hash_literals
|
|||||||
|
|
||||||
from ajax import ajax
|
from ajax import ajax
|
||||||
from complete import create_search_bar
|
from complete import create_search_bar
|
||||||
from dom import clear, set_css, build_rule, svgicon, add_extra_css
|
from dom import clear, set_css, build_rule, svgicon, add_extra_css, ensure_id
|
||||||
from elementmaker import E
|
from elementmaker import E
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
from widgets import create_button, create_spinner, Breadcrumbs
|
from widgets import create_button, create_spinner, Breadcrumbs
|
||||||
from modals import show_modal
|
from modals import show_modal
|
||||||
from utils import rating_to_stars
|
from utils import rating_to_stars
|
||||||
from book_list.globals import get_boss, get_session_data
|
from session import get_interface_data
|
||||||
|
|
||||||
|
from book_list.library_data import library_data, current_library_id
|
||||||
|
from book_list.ui import show_panel
|
||||||
|
from book_list.router import back
|
||||||
|
from book_list.top_bar import create_top_bar
|
||||||
|
from book_list.globals import get_session_data
|
||||||
from book_list.theme import get_color, get_font_size
|
from book_list.theme import get_color, get_font_size
|
||||||
|
|
||||||
|
apply_search = None
|
||||||
|
|
||||||
|
def set_apply_search(func):
|
||||||
|
nonlocal apply_search
|
||||||
|
apply_search = func
|
||||||
|
|
||||||
sp_counter = 0
|
sp_counter = 0
|
||||||
CLASS_NAME = 'book-search-panel'
|
CLASS_NAME = 'book-search-panel'
|
||||||
add_extra_css(def():
|
add_extra_css(def():
|
||||||
@ -36,195 +48,39 @@ add_extra_css(def():
|
|||||||
return style
|
return style
|
||||||
)
|
)
|
||||||
|
|
||||||
class SearchPanel:
|
state = {}
|
||||||
|
|
||||||
def __init__(self, interface_data, book_list_container):
|
def component(container, name):
|
||||||
nonlocal sp_counter
|
return container.querySelector(f'[data-component="{name}"]')
|
||||||
sp_counter += 1
|
|
||||||
self.container_id = 'search-panel-' + sp_counter
|
|
||||||
self.interface_data = interface_data
|
|
||||||
self.tag_path = []
|
|
||||||
|
|
||||||
div = E.div(
|
|
||||||
id=self.container_id, style='display:none', class_=CLASS_NAME,
|
|
||||||
E.div(style="text-align:center; padding:1ex 1em; border-bottom: solid 1px currentColor; margin-bottom: 0.5ex"), # search input container
|
|
||||||
E.div(
|
|
||||||
E.div(),
|
|
||||||
E.ol(style="display:none"), # breadcrumbs container
|
|
||||||
E.div(style="display:none") # tag browser container
|
|
||||||
)
|
|
||||||
)
|
|
||||||
book_list_container.appendChild(div)
|
|
||||||
|
|
||||||
# Build search input
|
def icon_for_node(node):
|
||||||
search_container = div.firstChild
|
interface_data = get_interface_data()
|
||||||
search_button = create_button(_('Search'), icon='search', tooltip=_('Do the search'))
|
ans = interface_data.icon_map[node.data.category] or 'column.png'
|
||||||
search_bar = create_search_bar(self.execute_search.bind(self), 'search-books', tooltip=_('Search for books'), placeholder=_('Enter the search query'), button=search_button)
|
return interface_data.icon_path + '/' + ans
|
||||||
set_css(search_bar, flex_grow='10', margin_right='0.5em')
|
|
||||||
search_container.appendChild(E.div(style="display: flex; width: 100%;", search_bar, search_button))
|
|
||||||
search_container.appendChild(E.ul(class_='search-items'))
|
|
||||||
|
|
||||||
# Build loading panel
|
|
||||||
loading_panel = div.lastChild.firstChild
|
|
||||||
loading_panel.appendChild(E.div(
|
|
||||||
create_spinner(), '\xa0' + _('Fetching data for the tag browser, please wait') + '…',
|
|
||||||
style='margin-left:auto; margin-right:auto; font-size: 1.5rem; font-weight; bold; text-align:center; margin-top:30vh')
|
|
||||||
)
|
|
||||||
self.breadcrumbs = Breadcrumbs(self.breadcrumbs_container)
|
|
||||||
self.initial_load_started = False
|
|
||||||
self.currently_loading = None
|
|
||||||
self.tag_browser_data = None
|
|
||||||
self.node_id_map = {}
|
|
||||||
self.active_nodes = {}
|
|
||||||
|
|
||||||
def init(self):
|
def node_for_path(path):
|
||||||
tb = self.search_control
|
path = path or state.tag_path
|
||||||
# We dont focus the search box because on mobile that will cause the
|
ans = state.tag_browser_data
|
||||||
# keyboard to popup and obscure the rest of the page
|
|
||||||
# tb.focus()
|
|
||||||
tb.value = ''
|
|
||||||
self.tag_path = []
|
|
||||||
self.active_nodes = {}
|
|
||||||
if not self.initial_load_started:
|
|
||||||
self.initial_load_started = True
|
|
||||||
self.refresh()
|
|
||||||
else:
|
|
||||||
self.render_tag_browser()
|
|
||||||
|
|
||||||
def refresh(self):
|
|
||||||
if self.currently_loading is not None:
|
|
||||||
self.currently_loading.abort()
|
|
||||||
self.currently_loading = None
|
|
||||||
sd = get_session_data()
|
|
||||||
query = {'library_id': self.interface_data.library_id}
|
|
||||||
for k in 'sort_tags_by partition_method collapse_at dont_collapse hide_empty_categories'.split(' '):
|
|
||||||
query[k] = sd.get(k) + ''
|
|
||||||
xhr = ajax('interface-data/tag-browser', self.on_data_fetched.bind(self), query=query, bypass_cache=False)
|
|
||||||
xhr.send()
|
|
||||||
self.currently_loading = xhr
|
|
||||||
|
|
||||||
def on_data_fetched(self, end_type, xhr, ev):
|
|
||||||
self.currently_loading = None
|
|
||||||
if end_type is 'abort':
|
|
||||||
return
|
|
||||||
|
|
||||||
parent = self.container.lastChild
|
|
||||||
if parent.lastChild.style.display is 'none':
|
|
||||||
parent.firstChild.style.display = 'none'
|
|
||||||
parent.lastChild.style.display = 'block'
|
|
||||||
container = self.tb_container
|
|
||||||
clear(container)
|
|
||||||
|
|
||||||
def show_error(error_html):
|
|
||||||
ediv = E.div()
|
|
||||||
container.appendChild(ediv)
|
|
||||||
ediv.innerHTML = '<h3>' + _('Failed to load tag browser data') + '</h3>' + error_html
|
|
||||||
|
|
||||||
def process_node(node):
|
|
||||||
self.node_id_map[node.id] = node
|
|
||||||
node.data = item_map[node.id]
|
|
||||||
for child in node.children:
|
|
||||||
child.parent = node
|
|
||||||
process_node(child)
|
|
||||||
|
|
||||||
if end_type is 'load':
|
|
||||||
try:
|
|
||||||
tag_browser_data = JSON.parse(xhr.responseText)
|
|
||||||
except Exception as err:
|
|
||||||
show_error(err + '')
|
|
||||||
return
|
|
||||||
item_map = tag_browser_data.item_map
|
|
||||||
self.tag_browser_data = tag_browser_data.root
|
|
||||||
self.node_id_map = {}
|
|
||||||
self.active_nodes = {}
|
|
||||||
process_node(self.tag_browser_data)
|
|
||||||
self.render_tag_browser(container)
|
|
||||||
else:
|
|
||||||
show_error(xhr.error_html)
|
|
||||||
|
|
||||||
def node_for_path(self, path):
|
|
||||||
path = path or self.tag_path
|
|
||||||
ans = self.tag_browser_data
|
|
||||||
for child_index in path:
|
for child_index in path:
|
||||||
ans = ans.children[child_index]
|
ans = ans.children[child_index]
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
def render_tag_browser(self, container=None):
|
|
||||||
container = container or self.tb_container
|
|
||||||
clear(container)
|
|
||||||
set_css(container, padding='1ex 1em', display='flex', flex_wrap='wrap', margin_left='-0.5rem')
|
|
||||||
self.render_children(container, self.node_for_path().children)
|
|
||||||
self.render_breadcrumbs()
|
|
||||||
|
|
||||||
def icon_for_node(self, node):
|
def execute_search(text):
|
||||||
ans = self.interface_data.icon_map[node.data.category] or 'column.png'
|
container = document.getElementById(state.container_id)
|
||||||
return self.interface_data.icon_path + '/' + ans
|
search_control = container.querySelector('input[name="search-books"]')
|
||||||
|
text = text or search_control.value or ''
|
||||||
|
apply_search(text)
|
||||||
|
|
||||||
def render_children(self, container, children):
|
|
||||||
|
|
||||||
def click_handler(func, i):
|
def search_expression_for_item(node, node_state):
|
||||||
return def():
|
|
||||||
func.call(self, i)
|
|
||||||
|
|
||||||
for i, node in enumerate(children):
|
|
||||||
data = node.data
|
|
||||||
tooltip = ''
|
|
||||||
if data.count is not undefined:
|
|
||||||
tooltip += '\n' + _('Number of books in this category: {}').format(data.count)
|
|
||||||
if data.avg_rating is not undefined:
|
|
||||||
tooltip += '\n' + _('Average rating for books in this category: {:.1f}').format(data.avg_rating)
|
|
||||||
div = E.div(
|
|
||||||
title=tooltip.lstrip(),
|
|
||||||
style="display:flex; align-items: stretch",
|
|
||||||
E.div(class_='tag-name',
|
|
||||||
style='border-right:solid 1px currentColor; padding: 1ex; display:flex; align-items: center',
|
|
||||||
E.img(src=self.icon_for_node(node), style='display:inline-block; max-height:2.5ex'),
|
|
||||||
'\xa0' + data.name
|
|
||||||
),
|
|
||||||
E.div(class_='tag-menu',
|
|
||||||
style='padding: 1ex; display:flex; align-items:center',
|
|
||||||
E.div(svgicon('angle-down'))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
set_css(div, max_width='45vw', border='solid 1px currentColor', border_radius='20px', margin='0.5rem', cursor='pointer', overflow='hidden', user_select='none')
|
|
||||||
div.firstChild.addEventListener('click', click_handler(self.node_clicked, i))
|
|
||||||
div.lastChild.addEventListener('click', click_handler(self.menu_clicked, i))
|
|
||||||
container.appendChild(div)
|
|
||||||
|
|
||||||
def render_breadcrumbs(self):
|
|
||||||
container = self.breadcrumbs_container
|
|
||||||
if not self.tag_path.length:
|
|
||||||
container.style.display = 'none'
|
|
||||||
return
|
|
||||||
container.style.display = 'inline-block'
|
|
||||||
self.breadcrumbs.reset()
|
|
||||||
|
|
||||||
def onclick(i):
|
|
||||||
return def(ev):
|
|
||||||
self.tag_path = self.tag_path[:i+1]
|
|
||||||
self.render_tag_browser()
|
|
||||||
ev.preventDefault()
|
|
||||||
return True
|
|
||||||
|
|
||||||
def create_breadcrumb(index=-1, item=None):
|
|
||||||
li = self.breadcrumbs.add_crumb(onclick(index))
|
|
||||||
if item:
|
|
||||||
li.appendChild(E.span(item.name))
|
|
||||||
else:
|
|
||||||
li.appendChild(svgicon('home', '2.2ex', '2.2ex'))
|
|
||||||
|
|
||||||
create_breadcrumb()
|
|
||||||
parent = self.tag_browser_data
|
|
||||||
for i, index in enumerate(self.tag_path):
|
|
||||||
parent = parent.children[index]
|
|
||||||
create_breadcrumb(i, parent.data)
|
|
||||||
|
|
||||||
def search_expression_for_item(self, node, state):
|
|
||||||
item = node.data
|
item = node.data
|
||||||
if item.is_searchable is False or not state or state is 'clear':
|
if item.is_searchable is False or not node_state or node_state is 'clear':
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
search_state = {'plus':'true', 'plusplus':'.true', 'minus':'false', 'minusminus':'.false'}[state]
|
search_state = {'plus':'true', 'plusplus':'.true', 'minus':'false', 'minusminus':'.false'}[node_state]
|
||||||
stars = rating_to_stars(3, True)
|
stars = rating_to_stars(3, True)
|
||||||
|
|
||||||
if item.is_category:
|
if item.is_category:
|
||||||
@ -265,7 +121,8 @@ class SearchPanel:
|
|||||||
rnum = '{}.5'.format(rnum - 1)
|
rnum = '{}.5'.format(rnum - 1)
|
||||||
expr = '{}:{}'.format(category, rnum)
|
expr = '{}:{}'.format(category, rnum)
|
||||||
else:
|
else:
|
||||||
fm = self.interface_data.field_metadata[item.category]
|
interface_data = get_interface_data()
|
||||||
|
fm = interface_data.field_metadata[item.category]
|
||||||
suffix = ':' if fm and fm.is_csp else ''
|
suffix = ':' if fm and fm.is_csp else ''
|
||||||
name = item.original_name or item.name or item.sort
|
name = item.original_name or item.name or item.sort
|
||||||
if not name:
|
if not name:
|
||||||
@ -281,23 +138,56 @@ class SearchPanel:
|
|||||||
expr = '(not ' + expr + ')'
|
expr = '(not ' + expr + ')'
|
||||||
return expr
|
return expr
|
||||||
|
|
||||||
def node_clicked(self, i):
|
def node_clicked(i):
|
||||||
node = self.node_for_path().children[i]
|
node = node_for_path().children[i]
|
||||||
if node.children and node.children.length:
|
if node.children and node.children.length:
|
||||||
self.tag_path.append(i)
|
state.tag_path.append(i)
|
||||||
self.render_tag_browser()
|
render_tag_browser()
|
||||||
else:
|
else:
|
||||||
expr = self.search_expression_for_item(node, 'plus')
|
expr = search_expression_for_item(node, 'plus')
|
||||||
self.execute_search(expr)
|
execute_search(expr)
|
||||||
|
|
||||||
def menu_clicked(self, i):
|
|
||||||
|
def add_to_search(node, search_type, anded):
|
||||||
|
if anded is undefined or anded is None:
|
||||||
|
anded = get_session_data().get('and_search_terms')
|
||||||
|
state.active_nodes[node.id] = [search_type, anded]
|
||||||
|
render_search_expression()
|
||||||
|
|
||||||
|
|
||||||
|
def render_search_expression():
|
||||||
|
def remove_expression(node_id):
|
||||||
|
return def():
|
||||||
|
v'delete state.active_nodes[node_id]'
|
||||||
|
render_search_expression()
|
||||||
|
parts = []
|
||||||
|
container = document.getElementById(state.container_id)
|
||||||
|
sic = component(container, 'tag_browser')
|
||||||
|
clear(sic)
|
||||||
|
for node_id in Object.keys(state.active_nodes):
|
||||||
|
search_type, anded = state.active_nodes[node_id]
|
||||||
|
node = state.node_id_map[node_id]
|
||||||
|
expr = search_expression_for_item(node, search_type)
|
||||||
|
name = node.data.original_name or node.data.name or node.data.sort or ''
|
||||||
|
if expr:
|
||||||
|
c = E.li(svgicon('remove'), '\xa0' + name)
|
||||||
|
sic.appendChild(c)
|
||||||
|
c.addEventListener('click', remove_expression(node_id))
|
||||||
|
if parts.length:
|
||||||
|
expr = ('and' if anded else 'or') + ' ' + expr
|
||||||
|
parts.push(expr)
|
||||||
|
search_control = container.querySelector('input[name="search-books"]')
|
||||||
|
search_control.value = parts.join(' ')
|
||||||
|
|
||||||
|
|
||||||
|
def menu_clicked(i):
|
||||||
|
|
||||||
def add_to_search(node, search_type):
|
def add_to_search(node, search_type):
|
||||||
return def():
|
return def():
|
||||||
self.add_to_search(node, search_type)
|
add_to_search(node, search_type)
|
||||||
|
|
||||||
def create_details(container, hide_modal):
|
def create_details(container, hide_modal):
|
||||||
node = self.node_for_path().children[i]
|
node = node_for_path().children[i]
|
||||||
data = node.data
|
data = node.data
|
||||||
name = data.original_name or data.name or data.sort
|
name = data.original_name or data.name or data.sort
|
||||||
items = []
|
items = []
|
||||||
@ -311,7 +201,7 @@ class SearchPanel:
|
|||||||
|
|
||||||
title = E.h2(
|
title = E.h2(
|
||||||
style='display:flex; align-items: center; border-bottom: solid 1px currentColor; font-weight:bold; font-size:' + get_font_size('title'),
|
style='display:flex; align-items: center; border-bottom: solid 1px currentColor; font-weight:bold; font-size:' + get_font_size('title'),
|
||||||
E.img(src=self.icon_for_node(node), style='height:2ex'),
|
E.img(src=icon_for_node(node), style='height:2ex'),
|
||||||
E.span('\xa0' + name + suffix)
|
E.span('\xa0' + name + suffix)
|
||||||
)
|
)
|
||||||
container.appendChild(title)
|
container.appendChild(title)
|
||||||
@ -331,10 +221,11 @@ class SearchPanel:
|
|||||||
(_('Books that match this category and all sub-categories'), 'plusplus'),
|
(_('Books that match this category and all sub-categories'), 'plusplus'),
|
||||||
(_('Books that do not match this category or any of its sub-categories'), 'minusminus'),
|
(_('Books that do not match this category or any of its sub-categories'), 'minusminus'),
|
||||||
])
|
])
|
||||||
|
interface_data = get_interface_data()
|
||||||
for text, search_type in items:
|
for text, search_type in items:
|
||||||
li = E.li(
|
li = E.li(
|
||||||
style='display:flex; align-items: center; margin-bottom:0.5ex; padding: 0.5ex; cursor:pointer',
|
style='display:flex; align-items: center; margin-bottom:0.5ex; padding: 0.5ex; cursor:pointer',
|
||||||
E.img(src='{}/{}.png'.format(self.interface_data.icon_path, search_type), style='max-height: 2.5ex'),
|
E.img(src='{}/{}.png'.format(interface_data.icon_path, search_type), style='max-height: 2.5ex'),
|
||||||
E.span('\xa0' + text)
|
E.span('\xa0' + text)
|
||||||
)
|
)
|
||||||
li.addEventListener('click', add_to_search(node, search_type))
|
li.addEventListener('click', add_to_search(node, search_type))
|
||||||
@ -359,35 +250,181 @@ class SearchPanel:
|
|||||||
)
|
)
|
||||||
show_modal(create_details)
|
show_modal(create_details)
|
||||||
|
|
||||||
def add_to_search(self, node, search_type, anded):
|
|
||||||
if anded is undefined or anded is None:
|
|
||||||
anded = get_session_data().get('and_search_terms')
|
|
||||||
self.active_nodes[node.id] = [search_type, anded]
|
|
||||||
self.render_search_expression()
|
|
||||||
|
|
||||||
def render_search_expression(self):
|
def render_children(container, children):
|
||||||
def remove_expression(node_id):
|
for i, node in enumerate(children):
|
||||||
return def():
|
data = node.data
|
||||||
v'delete self.active_nodes[node_id]'
|
tooltip = ''
|
||||||
self.render_search_expression()
|
if data.count is not undefined:
|
||||||
parts = []
|
tooltip += '\n' + _('Number of books in this category: {}').format(data.count)
|
||||||
container = self.search_items_container
|
if data.avg_rating is not undefined:
|
||||||
|
tooltip += '\n' + _('Average rating for books in this category: {:.1f}').format(data.avg_rating)
|
||||||
|
div = E.div(
|
||||||
|
title=tooltip.lstrip(),
|
||||||
|
style="display:flex; align-items: stretch",
|
||||||
|
E.div(class_='tag-name',
|
||||||
|
style='border-right:solid 1px currentColor; padding: 1ex; display:flex; align-items: center',
|
||||||
|
E.img(src=icon_for_node(node), style='display:inline-block; max-height:2.5ex'),
|
||||||
|
'\xa0' + data.name
|
||||||
|
),
|
||||||
|
E.div(class_='tag-menu',
|
||||||
|
style='padding: 1ex; display:flex; align-items:center',
|
||||||
|
E.div(svgicon('angle-down'))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
set_css(div, max_width='45vw', border='solid 1px currentColor', border_radius='20px', margin='0.5rem', cursor='pointer', overflow='hidden', user_select='none')
|
||||||
|
div.firstChild.addEventListener('click', node_clicked.bind(i))
|
||||||
|
div.lastChild.addEventListener('click', menu_clicked.bind(i))
|
||||||
|
container.appendChild(div)
|
||||||
|
|
||||||
|
def render_breadcrumbs():
|
||||||
|
container = state.breadcrumbs.container
|
||||||
|
if not state.tag_path.length:
|
||||||
|
container.style.display = 'none'
|
||||||
|
return
|
||||||
|
container.style.display = 'inline-block'
|
||||||
|
state.breadcrumbs.reset()
|
||||||
|
|
||||||
|
def onclick(i):
|
||||||
|
return def(ev):
|
||||||
|
state.tag_path = state.tag_path[:i+1]
|
||||||
|
render_tag_browser()
|
||||||
|
ev.preventDefault()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def create_breadcrumb(index=-1, item=None):
|
||||||
|
li = state.breadcrumbs.add_crumb(onclick(index))
|
||||||
|
if item:
|
||||||
|
li.appendChild(E.span(item.name))
|
||||||
|
else:
|
||||||
|
li.appendChild(svgicon('home', '2.2ex', '2.2ex'))
|
||||||
|
|
||||||
|
create_breadcrumb()
|
||||||
|
parent = state.tag_browser_data
|
||||||
|
for i, index in enumerate(state.tag_path):
|
||||||
|
parent = parent.children[index]
|
||||||
|
create_breadcrumb(i, parent.data)
|
||||||
|
|
||||||
|
def render_tag_browser():
|
||||||
|
container = document.getElementById(state.container_id).lastChild.lastChild
|
||||||
clear(container)
|
clear(container)
|
||||||
for node_id in Object.keys(self.active_nodes):
|
set_css(container, padding='1ex 1em', display='flex', flex_wrap='wrap', margin_left='-0.5rem')
|
||||||
search_type, anded = self.active_nodes[node_id]
|
render_children(container, node_for_path().children)
|
||||||
node = self.node_id_map[node_id]
|
render_breadcrumbs()
|
||||||
expr = self.search_expression_for_item(node, search_type)
|
|
||||||
name = node.data.original_name or node.data.name or node.data.sort or ''
|
|
||||||
if expr:
|
|
||||||
c = E.li(svgicon('remove'), '\xa0' + name)
|
|
||||||
container.appendChild(c)
|
|
||||||
c.addEventListener('click', remove_expression(node_id))
|
|
||||||
if parts.length:
|
|
||||||
expr = ('and' if anded else 'or') + ' ' + expr
|
|
||||||
parts.push(expr)
|
|
||||||
self.search_control.value = parts.join(' ')
|
|
||||||
|
|
||||||
def get_prefs(self):
|
|
||||||
|
def on_data_fetched(end_type, xhr, ev):
|
||||||
|
state.currently_loading = None
|
||||||
|
if end_type is 'abort':
|
||||||
|
return
|
||||||
|
container = document.getElementById(state.container_id)
|
||||||
|
if not container:
|
||||||
|
return
|
||||||
|
|
||||||
|
loading_panel = component(container, 'loading')
|
||||||
|
loading_panel.style.display = 'none'
|
||||||
|
container = component(container, 'tag_browser')
|
||||||
|
container.style.display = 'block'
|
||||||
|
clear(container)
|
||||||
|
|
||||||
|
def show_error(error_html):
|
||||||
|
ediv = E.div()
|
||||||
|
container.appendChild(ediv)
|
||||||
|
ediv.innerHTML = '<h3>' + _('Failed to load tag browser data') + '</h3>' + error_html
|
||||||
|
|
||||||
|
def process_node(node):
|
||||||
|
state.node_id_map[node.id] = node
|
||||||
|
node.data = item_map[node.id]
|
||||||
|
for child in node.children:
|
||||||
|
child.parent = node
|
||||||
|
process_node(child)
|
||||||
|
|
||||||
|
if end_type is 'load':
|
||||||
|
try:
|
||||||
|
tag_browser_data = JSON.parse(xhr.responseText)
|
||||||
|
except Exception as err:
|
||||||
|
show_error(err + '')
|
||||||
|
return
|
||||||
|
item_map = tag_browser_data.item_map
|
||||||
|
state.tag_browser_data = tag_browser_data.root
|
||||||
|
state.node_id_map = {}
|
||||||
|
state.active_nodes = {}
|
||||||
|
process_node(state.tag_browser_data)
|
||||||
|
render_tag_browser()
|
||||||
|
else:
|
||||||
|
show_error(xhr.error_html)
|
||||||
|
|
||||||
|
|
||||||
|
def refresh():
|
||||||
|
if state.currently_loading is not None:
|
||||||
|
state.currently_loading.abort()
|
||||||
|
state.currently_loading = None
|
||||||
|
sd = get_session_data()
|
||||||
|
query = {'library_id': current_library_id()}
|
||||||
|
for k in 'sort_tags_by partition_method collapse_at dont_collapse hide_empty_categories'.split(' '):
|
||||||
|
query[k] = sd.get(k) + ''
|
||||||
|
xhr = ajax('interface-data/tag-browser', on_data_fetched, query=query, bypass_cache=False)
|
||||||
|
xhr.send()
|
||||||
|
state.currently_loading = xhr
|
||||||
|
|
||||||
|
|
||||||
|
def create_search_panel(container):
|
||||||
|
nonlocal state
|
||||||
|
state = {}
|
||||||
|
container.classList.add(CLASS_NAME)
|
||||||
|
# search input container
|
||||||
|
container.appendChild(E.div(
|
||||||
|
data_component='search',
|
||||||
|
style="text-align:center; padding:1ex 1em; border-bottom: solid 1px currentColor; margin-bottom: 0.5ex"
|
||||||
|
))
|
||||||
|
container.appendChild(E.div(
|
||||||
|
E.div(data_component='loading'),
|
||||||
|
E.ol(style="display:none", data_component='breadcrumbs'),
|
||||||
|
E.div(style="display:none", data_component='tag_browser')
|
||||||
|
))
|
||||||
|
|
||||||
|
# Build search input
|
||||||
|
# We dont focus the search box because on mobile that will cause the
|
||||||
|
# keyboard to popup and obscure the rest of the page
|
||||||
|
search_container = component(container, 'search')
|
||||||
|
search_button = create_button(_('Search'), icon='search', tooltip=_('Do the search'))
|
||||||
|
search_bar = create_search_bar(execute_search, 'search-books', tooltip=_('Search for books'), placeholder=_('Enter the search query'), button=search_button)
|
||||||
|
set_css(search_bar, flex_grow='10', margin_right='0.5em')
|
||||||
|
search_container.appendChild(E.div(style="display: flex; width: 100%;", search_bar, search_button))
|
||||||
|
search_container.appendChild(E.ul(class_='search-items'))
|
||||||
|
|
||||||
|
# Build loading panel
|
||||||
|
loading_panel = component(container, 'loading')
|
||||||
|
loading_panel.appendChild(E.div(
|
||||||
|
create_spinner(), '\xa0' + _('Fetching data for the tag browser, please wait') + '…',
|
||||||
|
style='margin-left:auto; margin-right:auto; font-size: 1.5rem; font-weight; bold; text-align:center; margin-top:30vh')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build breadcrumbs
|
||||||
|
state.breadcrumbs = Breadcrumbs(component(container, 'breadcrumbs'))
|
||||||
|
|
||||||
|
# Init state
|
||||||
|
state.currently_loading = None
|
||||||
|
state.tag_browser_data = None
|
||||||
|
state.node_id_map = {}
|
||||||
|
state.active_nodes = {}
|
||||||
|
state.tag_path = []
|
||||||
|
state.container_id = ensure_id(container)
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def init(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=_('Search for books'), action=back, icon='close')
|
||||||
|
container.appendChild(E.div(class_=CLASS_NAME))
|
||||||
|
create_search_panel(container.lastChild)
|
||||||
|
|
||||||
|
|
||||||
|
def get_prefs():
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'name': 'sort_tags_by',
|
'name': 'sort_tags_by',
|
||||||
@ -439,44 +476,3 @@ class SearchPanel:
|
|||||||
},
|
},
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def apply_prefs(self):
|
|
||||||
container = self.tb_container
|
|
||||||
clear(container)
|
|
||||||
container.appendChild(E.div(
|
|
||||||
style='margin: 1ex 1em',
|
|
||||||
_('Reloading tag browser with updated settings, please wait...'))
|
|
||||||
)
|
|
||||||
self.refresh()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def container(self):
|
|
||||||
return document.getElementById(self.container_id)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def breadcrumbs_container(self):
|
|
||||||
return self.tb_container.previousSibling
|
|
||||||
|
|
||||||
@property
|
|
||||||
def tb_container(self):
|
|
||||||
return self.container.lastChild.lastChild
|
|
||||||
|
|
||||||
@property
|
|
||||||
def search_control(self):
|
|
||||||
return self.container.querySelector('input[name="search-books"]')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def search_items_container(self):
|
|
||||||
return self.container.firstChild.lastChild
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_visible(self):
|
|
||||||
self.container.style.display is 'block'
|
|
||||||
|
|
||||||
@is_visible.setter
|
|
||||||
def is_visible(self, val):
|
|
||||||
self.container.style.display = 'block' if val else 'none'
|
|
||||||
|
|
||||||
def execute_search(self, text=''):
|
|
||||||
text = text or self.search_control.value or ''
|
|
||||||
get_boss().ui.books_view.change_search(text)
|
|
||||||
|
@ -20,6 +20,7 @@ from book_list.router import back
|
|||||||
from book_list.ui import set_panel_handler, show_panel
|
from book_list.ui import set_panel_handler, show_panel
|
||||||
from book_list.library_data import load_status, ensure_current_library_data, library_data, current_sorted_field, loaded_books_query, url_books_query, current_library_id
|
from book_list.library_data import load_status, ensure_current_library_data, library_data, current_sorted_field, loaded_books_query, url_books_query, current_library_id
|
||||||
from book_list.item_list import create_item_list, create_item
|
from book_list.item_list import create_item_list, create_item
|
||||||
|
from book_list.search import init as init_search_panel, set_apply_search
|
||||||
|
|
||||||
ALLOWED_MODES = {'cover_grid'}
|
ALLOWED_MODES = {'cover_grid'}
|
||||||
DEFAULT_MODE = 'cover_grid'
|
DEFAULT_MODE = 'cover_grid'
|
||||||
@ -157,7 +158,8 @@ def create_books_list(container):
|
|||||||
container.appendChild(E.div()), container.appendChild(E.div())
|
container.appendChild(E.div()), container.appendChild(E.div())
|
||||||
apply_view_mode(get_session_data().get('view_mode'))
|
apply_view_mode(get_session_data().get('view_mode'))
|
||||||
create_more_button(container.lastChild)
|
create_more_button(container.lastChild)
|
||||||
add_button(container.parentNode, icon='sort-amount-desc', action=show_panel.bind(None, 'book_list^sort'), tooltip=_('Sort Books'))
|
add_button(container.parentNode, icon='sort-amount-desc', action=show_panel.bind(None, 'book_list^sort'), tooltip=_('Sort books'))
|
||||||
|
add_button(container.parentNode, icon='search', action=show_panel.bind(None, 'book_list^search'), tooltip=_('Search for books'))
|
||||||
add_button(container.parentNode, icon='ellipsis-v', action=show_panel.bind(None, 'book_list^more_actions'), tooltip=_('More actions'))
|
add_button(container.parentNode, icon='ellipsis-v', action=show_panel.bind(None, 'book_list^more_actions'), tooltip=_('More actions'))
|
||||||
|
|
||||||
|
|
||||||
@ -241,6 +243,8 @@ def create_sort_panel(container_id):
|
|||||||
def search(query, replace=False):
|
def search(query, replace=False):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
set_apply_search(def(query): search(query, True);)
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
# More actions {{{
|
# More actions {{{
|
||||||
@ -262,4 +266,5 @@ def create_more_actions_panel(container_id):
|
|||||||
|
|
||||||
set_panel_handler('book_list', init)
|
set_panel_handler('book_list', init)
|
||||||
set_panel_handler('book_list^sort', create_sort_panel)
|
set_panel_handler('book_list^sort', create_sort_panel)
|
||||||
|
set_panel_handler('book_list^search', init_search_panel)
|
||||||
set_panel_handler('book_list^more_actions', create_more_actions_panel)
|
set_panel_handler('book_list^more_actions', create_more_actions_panel)
|
||||||
|
@ -145,6 +145,8 @@ default_interface_data = {
|
|||||||
'allow_console_print':False,
|
'allow_console_print':False,
|
||||||
'default_library_id': None,
|
'default_library_id': None,
|
||||||
'library_map': None,
|
'library_map': None,
|
||||||
|
'icon_map': {},
|
||||||
|
'icon_path': '',
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_interface_data():
|
def get_interface_data():
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
# License: GPL v3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPL v3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
from __python__ import hash_literals
|
from __python__ import hash_literals
|
||||||
|
|
||||||
from dom import build_rule, clear, svgicon, create_keyframes, set_css, change_icon_image, add_extra_css
|
from dom import build_rule, clear, svgicon, create_keyframes, set_css, change_icon_image, add_extra_css, ensure_id
|
||||||
from elementmaker import E
|
from elementmaker import E
|
||||||
|
|
||||||
from book_list.theme import get_color
|
from book_list.theme import get_color
|
||||||
@ -44,8 +44,6 @@ create_spinner.style += create_keyframes('spin', 'from { transform: rotate(0deg)
|
|||||||
|
|
||||||
# Breadcrumbs {{{
|
# Breadcrumbs {{{
|
||||||
|
|
||||||
id_counter = 0
|
|
||||||
|
|
||||||
class Breadcrumbs:
|
class Breadcrumbs:
|
||||||
|
|
||||||
STYLE_RULES = build_rule(
|
STYLE_RULES = build_rule(
|
||||||
@ -81,11 +79,9 @@ class Breadcrumbs:
|
|||||||
|
|
||||||
|
|
||||||
def __init__(self, container):
|
def __init__(self, container):
|
||||||
nonlocal id_counter id_counter += 1
|
self.container_id = ensure_id(container, 'calibre-breadcrumbs-')
|
||||||
container.setAttribute('id', container.getAttribute('id') or ('calibre-breadcrumbs-' + id_counter))
|
|
||||||
container.classList.add('calibre-breadcrumbs')
|
container.classList.add('calibre-breadcrumbs')
|
||||||
clear(container)
|
clear(container)
|
||||||
self.container_id = container.getAttribute('id')
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def container(self):
|
def container(self):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user