mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Allow clicking on title bar to clear current search
This commit is contained in:
parent
e6a98f5478
commit
3869c4764b
@ -3,6 +3,21 @@
|
|||||||
|
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
|
|
||||||
|
def encode_query(query):
|
||||||
|
if not query:
|
||||||
|
return ''
|
||||||
|
keys = Object.keys(query)
|
||||||
|
has_query = False
|
||||||
|
path = ''
|
||||||
|
if keys.length:
|
||||||
|
for k in keys:
|
||||||
|
val = query[k]
|
||||||
|
if val is undefined or val is None:
|
||||||
|
continue
|
||||||
|
path += ('&' if has_query else '?') + encodeURIComponent(k) + '=' + encodeURIComponent(val.toString())
|
||||||
|
has_query = True
|
||||||
|
return path
|
||||||
|
|
||||||
def ajax(path, on_complete, on_progress=None, bypass_cache=True, method='GET', query=None, timeout=30*1000, ok_code=200):
|
def ajax(path, on_complete, on_progress=None, bypass_cache=True, method='GET', query=None, timeout=30*1000, ok_code=200):
|
||||||
# Run an AJAX request. on_complete must be a function that accepts three
|
# Run an AJAX request. on_complete must be a function that accepts three
|
||||||
# arguments: end_type, xhr, ev where end_type is one of 'abort', 'error',
|
# arguments: end_type, xhr, ev where end_type is one of 'abort', 'error',
|
||||||
@ -14,15 +29,9 @@ def ajax(path, on_complete, on_progress=None, bypass_cache=True, method='GET', q
|
|||||||
# Returns the xhr object, call xhr.send() to start the request.
|
# Returns the xhr object, call xhr.send() to start the request.
|
||||||
query = query or {}
|
query = query or {}
|
||||||
xhr = XMLHttpRequest()
|
xhr = XMLHttpRequest()
|
||||||
keys = Object.keys(query)
|
eq = encode_query(query)
|
||||||
has_query = False
|
has_query = eq.length > 0
|
||||||
if keys.length:
|
path += eq
|
||||||
for k in keys:
|
|
||||||
val = query[k]
|
|
||||||
if val is undefined or val is None:
|
|
||||||
continue
|
|
||||||
path += ('&' if has_query else '?') + encodeURIComponent(k) + '=' + encodeURIComponent(val.toString())
|
|
||||||
has_query = True
|
|
||||||
if bypass_cache:
|
if bypass_cache:
|
||||||
path += ('&' if has_query else '?') + Date().getTime()
|
path += ('&' if has_query else '?') + Date().getTime()
|
||||||
|
|
||||||
|
@ -6,8 +6,9 @@ from elementmaker import E
|
|||||||
from modals import error_dialog, create_modal_container
|
from modals import error_dialog, create_modal_container
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
from widgets import get_widget_css
|
from widgets import get_widget_css
|
||||||
from book_list.globals import get_session_data
|
from utils import parse_url_params
|
||||||
|
|
||||||
|
from book_list.globals import get_session_data
|
||||||
from book_list.theme import get_color
|
from book_list.theme import get_color
|
||||||
from book_list.ui import UI
|
from book_list.ui import UI
|
||||||
|
|
||||||
@ -25,6 +26,9 @@ class Boss:
|
|||||||
document.body.appendChild(div)
|
document.body.appendChild(div)
|
||||||
self.ui = UI(interface_data, div)
|
self.ui = UI(interface_data, div)
|
||||||
window.onerror = self.onerror.bind(self)
|
window.onerror = self.onerror.bind(self)
|
||||||
|
setTimeout(def():
|
||||||
|
window.onpopstate = self.onpopstate.bind(self)
|
||||||
|
, 0) # We do this after event loop ticks over to avoid catching popstate events that some browsers send on page load
|
||||||
|
|
||||||
def update_window_title(self):
|
def update_window_title(self):
|
||||||
document.title = 'calibre :: ' + self.current_library_name
|
document.title = 'calibre :: ' + self.current_library_name
|
||||||
@ -41,6 +45,13 @@ class Boss:
|
|||||||
except:
|
except:
|
||||||
console.error('There was an error in the unhandled exception handler')
|
console.error('There was an error in the unhandled exception handler')
|
||||||
|
|
||||||
|
def onpopstate(self, ev):
|
||||||
|
data = parse_url_params()
|
||||||
|
if not data.mode or data.mode == 'book_list':
|
||||||
|
search = data.search or ''
|
||||||
|
if search != self.ui.books_view.interface_data.search_result.query:
|
||||||
|
self.ui.books_view.change_search(search)
|
||||||
|
|
||||||
def change_books(self, data):
|
def change_books(self, data):
|
||||||
data.search_result.sort = str.split(data.search_result.sort, ',')[:2].join(',')
|
data.search_result.sort = str.split(data.search_result.sort, ',')[:2].join(',')
|
||||||
data.search_result.sort_order = str.split(data.search_result.sort_order, ',')[:2].join(',')
|
data.search_result.sort_order = str.split(data.search_result.sort_order, ',')[:2].join(',')
|
||||||
@ -50,4 +61,4 @@ class Boss:
|
|||||||
get_session_data().set('sort', str.rstrip(sval, ','))
|
get_session_data().set('sort', str.rstrip(sval, ','))
|
||||||
self.interface_data.metadata = data.metadata
|
self.interface_data.metadata = data.metadata
|
||||||
self.interface_data.search_result = data.search_result
|
self.interface_data.search_result = data.search_result
|
||||||
self.ui.books_view.refresh()
|
self.ui.refresh_books_view()
|
||||||
|
@ -14,6 +14,7 @@ def get_color(name):
|
|||||||
# Top bar specific colors
|
# Top bar specific colors
|
||||||
'bar-background': DARK,
|
'bar-background': DARK,
|
||||||
'bar-foreground': LIGHT,
|
'bar-foreground': LIGHT,
|
||||||
|
'bar-highlight': 'yellow',
|
||||||
'heart': '#B92111',
|
'heart': '#B92111',
|
||||||
|
|
||||||
# Item list colors
|
# Item list colors
|
||||||
|
@ -16,6 +16,7 @@ class TopBar:
|
|||||||
def __init__(self, book_list_container):
|
def __init__(self, book_list_container):
|
||||||
nonlocal bar_counter
|
nonlocal bar_counter
|
||||||
bar_counter += 1
|
bar_counter += 1
|
||||||
|
self.current_left_data = {}
|
||||||
self.bar_id, self.dummy_bar_id = 'top-bar-' + bar_counter, 'dummy-top-bar-' + bar_counter
|
self.bar_id, self.dummy_bar_id = 'top-bar-' + bar_counter, 'dummy-top-bar-' + bar_counter
|
||||||
self.throbber_name = self.bar_id + '-throbber'
|
self.throbber_name = self.bar_id + '-throbber'
|
||||||
style = create_keyframes(self.throbber_name, 'from { transform: scale(1); } 50% { transform: scale(0.5); } to { transform: scale(1); }')
|
style = create_keyframes(self.throbber_name, 'from { transform: scale(1); } 50% { transform: scale(0.5); } to { transform: scale(1); }')
|
||||||
@ -28,6 +29,8 @@ class TopBar:
|
|||||||
style += build_rule(sel + ':hover', transform='scale(1.5)')
|
style += build_rule(sel + ':hover', transform='scale(1.5)')
|
||||||
style += build_rule(sel + ':active', transform='scale(2)')
|
style += build_rule(sel + ':active', transform='scale(2)')
|
||||||
style += build_rule(sel + ':focus', outline='none')
|
style += build_rule(sel + ':focus', outline='none')
|
||||||
|
style += build_rule(sel + '.top-bar-title:hover', transform='scale(1)', color=get_color('bar-highlight'), font_style='italic')
|
||||||
|
style += build_rule(sel + '.top-bar-title:active', transform='scale(1)', color=get_color('bar-highlight'), font_style='italic')
|
||||||
for bid in self.dummy_bar_id, self.bar_id:
|
for bid in self.dummy_bar_id, self.bar_id:
|
||||||
bar = E.div(
|
bar = E.div(
|
||||||
id=bid,
|
id=bid,
|
||||||
@ -53,18 +56,23 @@ class TopBar:
|
|||||||
return document.getElementById(self.dummy_bar_id)
|
return document.getElementById(self.dummy_bar_id)
|
||||||
|
|
||||||
def set_left(self, title='calibre', icon_name='heart', action=None, tooltip='', run_animation=False):
|
def set_left(self, title='calibre', icon_name='heart', action=None, tooltip='', run_animation=False):
|
||||||
|
self.current_left_data = {'title':title, 'icon_name':icon_name, 'action':action, 'tooltip':tooltip}
|
||||||
if icon_name == 'heart':
|
if icon_name == 'heart':
|
||||||
if not tooltip:
|
if not tooltip:
|
||||||
tooltip = _('Donate to support calibre development')
|
tooltip = _('Donate to support calibre development')
|
||||||
|
|
||||||
|
title_action = title_tooltip = None
|
||||||
if callable(title):
|
if callable(title):
|
||||||
title = title()
|
data = title()
|
||||||
|
title, title_action, title_tooltip = data.title, data.title_action, data.title_tooltip
|
||||||
|
|
||||||
for bar in self.bar, self.dummy_bar:
|
for bar in self.bar, self.dummy_bar:
|
||||||
left = bar.firstChild
|
left = bar.firstChild
|
||||||
clear(left)
|
clear(left)
|
||||||
|
title_elem = 'a' if callable(title_action) else 'span'
|
||||||
left.appendChild(E.a(title=tooltip, E.i(class_='fa fa-' + icon_name + ' fa-fw')))
|
left.appendChild(E.a(title=tooltip, E.i(class_='fa fa-' + icon_name + ' fa-fw')))
|
||||||
left.appendChild(E.span(title, style=str.format('margin-left: {0}; font-weight: bold; padding-top: {1}; padding-bottom: {1}; vertical-align: middle', self.SPACING, self.VSPACING)))
|
left.appendChild(E(title_elem, title, title=title_tooltip, class_='top-bar-title', style=str.format(
|
||||||
|
'margin-left: {0}; font-weight: bold; padding-top: {1}; padding-bottom: {1}; vertical-align: middle', self.SPACING, self.VSPACING)))
|
||||||
if bar is self.bar:
|
if bar is self.bar:
|
||||||
a = left.firstChild
|
a = left.firstChild
|
||||||
if icon_name == 'heart':
|
if icon_name == 'heart':
|
||||||
@ -78,6 +86,14 @@ class TopBar:
|
|||||||
if action is not None:
|
if action is not None:
|
||||||
a.addEventListener('click', def(event): event.preventDefault(), action();)
|
a.addEventListener('click', def(event): event.preventDefault(), action();)
|
||||||
|
|
||||||
|
if callable(title_action):
|
||||||
|
a = a.nextSibling
|
||||||
|
a.addEventListener('click', def(event): event.preventDefault(), title_action();)
|
||||||
|
|
||||||
|
def refresh_left(self):
|
||||||
|
kw = self.current_left_data
|
||||||
|
self.set_left(**kw)
|
||||||
|
|
||||||
def add_button(self, icon_name=None, action=None, tooltip=''):
|
def add_button(self, icon_name=None, action=None, tooltip=''):
|
||||||
if not icon_name:
|
if not icon_name:
|
||||||
return
|
return
|
||||||
|
@ -90,7 +90,12 @@ class UI:
|
|||||||
self.search_panel = SearchPanel(interface_data, book_list_container)
|
self.search_panel = SearchPanel(interface_data, book_list_container)
|
||||||
ibs = BarState(run_animation=True, title=def():
|
ibs = BarState(run_animation=True, title=def():
|
||||||
q = self.books_view.interface_data['search_result']['query']
|
q = self.books_view.interface_data['search_result']['query']
|
||||||
return (_('Books matching:') + ' ' + q) if q else 'calibre'
|
if q:
|
||||||
|
return {'title': _('Books matching') + ':: ' + q, 'title_tooltip':_('Click to clear this search'),
|
||||||
|
'title_action':def():
|
||||||
|
self.books_view.change_search('')
|
||||||
|
}
|
||||||
|
return {'title': 'calibre'}
|
||||||
)
|
)
|
||||||
ibs.add_button(icon_name='sort-amount-desc', tooltip=_('Sort books'), action=show_panel_action('booklist-sort-menu'))
|
ibs.add_button(icon_name='sort-amount-desc', tooltip=_('Sort books'), action=show_panel_action('booklist-sort-menu'))
|
||||||
ibs.add_button(icon_name='search', tooltip=_('Search for books'), action=show_panel_action('booklist-search'))
|
ibs.add_button(icon_name='search', tooltip=_('Search for books'), action=show_panel_action('booklist-search'))
|
||||||
@ -125,3 +130,8 @@ class UI:
|
|||||||
def show_panel(self, state):
|
def show_panel(self, state):
|
||||||
self.states.append(state)
|
self.states.append(state)
|
||||||
self.apply_state(self.states[-1])
|
self.apply_state(self.states[-1])
|
||||||
|
|
||||||
|
def refresh_books_view(self):
|
||||||
|
self.books_view.refresh()
|
||||||
|
if len(self.states) == 1:
|
||||||
|
self.top_bar.refresh_left()
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# vim:fileencoding=utf-8
|
# vim:fileencoding=utf-8
|
||||||
# License: GPL v3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPL v3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
from ajax import ajax_send
|
from ajax import ajax_send, encode_query
|
||||||
from dom import set_css, build_rule, clear
|
from dom import set_css, build_rule, clear
|
||||||
from elementmaker import E
|
from elementmaker import E
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
@ -231,6 +231,28 @@ class BooksView:
|
|||||||
elif end_type != 'abort':
|
elif end_type != 'abort':
|
||||||
error_dialog(_('Could not change sort order'), xhr.error_html)
|
error_dialog(_('Could not change sort order'), xhr.error_html)
|
||||||
|
|
||||||
|
def change_search(self, query):
|
||||||
|
query = query or ''
|
||||||
|
sd = get_session_data()
|
||||||
|
data = {'search':query, 'sort':sd.get('sort'), 'library_id':self.interface_data.library_id}
|
||||||
|
ajax_progress_dialog('interface-data/get-books', self.search_change_completed.bind(self), _(
|
||||||
|
'Fetching data from server, please wait') + '…', query=data)
|
||||||
|
|
||||||
|
def search_change_completed(self, end_type, xhr, ev):
|
||||||
|
if end_type == 'load':
|
||||||
|
boss = get_boss()
|
||||||
|
try:
|
||||||
|
data = JSON.parse(xhr.responseText)
|
||||||
|
boss.change_books(data)
|
||||||
|
except Exception as err:
|
||||||
|
return error_dialog(_('Could not change search query'), err + '', details=err.stack)
|
||||||
|
query = {'search':self.interface_data.search_result.query, 'library_id':self.interface_data.library_id}
|
||||||
|
window.history.pushState(None, '', encode_query(query))
|
||||||
|
boss.ui.close_panel()
|
||||||
|
window.scrollTo(0, 0)
|
||||||
|
elif end_type != 'abort':
|
||||||
|
error_dialog(_('Could not change search query'), xhr.error_html)
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
self.clear()
|
self.clear()
|
||||||
self.render_ids()
|
self.render_ids()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user