mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Refactor panel architecture to use browser history
This means that the panels work with the browsers back and forward actions
This commit is contained in:
parent
debb1a3968
commit
ea9e907298
@ -9,13 +9,14 @@ from gettext import gettext as _
|
|||||||
from widgets import get_widget_css
|
from widgets import get_widget_css
|
||||||
from utils import parse_url_params
|
from utils import parse_url_params
|
||||||
|
|
||||||
from book_list.globals import get_session_data
|
from book_list.globals import get_session_data, set_boss
|
||||||
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
|
||||||
|
|
||||||
class Boss:
|
class Boss:
|
||||||
|
|
||||||
def __init__(self, interface_data):
|
def __init__(self, interface_data):
|
||||||
|
set_boss(self)
|
||||||
document.head.appendChild(E.style(get_widget_css()))
|
document.head.appendChild(E.style(get_widget_css()))
|
||||||
set_css(document.body, background_color=get_color('window-background'), color=get_color('window-foreground'))
|
set_css(document.body, background_color=get_color('window-background'), color=get_color('window-foreground'))
|
||||||
create_modal_container()
|
create_modal_container()
|
||||||
@ -28,10 +29,20 @@ 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)
|
||||||
|
self.history_count = 0
|
||||||
|
self.ui.apply_state() # Render the book list
|
||||||
|
data = parse_url_params()
|
||||||
|
if not data.mode or data.mode == 'book_list':
|
||||||
|
if data.panel != self.ui.current_panel:
|
||||||
|
self.ui.show_panel(data.panel, push_state=False)
|
||||||
setTimeout(def():
|
setTimeout(def():
|
||||||
window.onpopstate = self.onpopstate.bind(self)
|
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
|
, 0) # We do this after event loop ticks over to avoid catching popstate events that some browsers send on page load
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_history(self):
|
||||||
|
return self.history_count > 0
|
||||||
|
|
||||||
def update_window_title(self):
|
def update_window_title(self):
|
||||||
document.title = 'calibre :: ' + self.current_library_name
|
document.title = 'calibre :: ' + self.current_library_name
|
||||||
|
|
||||||
@ -50,10 +61,13 @@ class Boss:
|
|||||||
def onpopstate(self, ev):
|
def onpopstate(self, ev):
|
||||||
data = parse_url_params()
|
data = parse_url_params()
|
||||||
mode = data.mode or 'book_list'
|
mode = data.mode or 'book_list'
|
||||||
|
self.history_count -= 1
|
||||||
if mode == 'book_list':
|
if mode == 'book_list':
|
||||||
search = data.search or ''
|
search = data.search or ''
|
||||||
|
if data.panel != self.ui.current_panel:
|
||||||
|
self.ui.show_panel(data.panel, push_state=False)
|
||||||
if search != self.ui.books_view.interface_data.search_result.query:
|
if search != self.ui.books_view.interface_data.search_result.query:
|
||||||
self.ui.books_view.change_search(search, push_state=False)
|
self.ui.books_view.change_search(search, push_state=False, panel_to_show=data.panel)
|
||||||
|
|
||||||
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(',')
|
||||||
@ -66,9 +80,12 @@ class Boss:
|
|||||||
self.interface_data.search_result = data.search_result
|
self.interface_data.search_result = data.search_result
|
||||||
self.ui.refresh_books_view()
|
self.ui.refresh_books_view()
|
||||||
|
|
||||||
def push_state(self):
|
def push_state(self, replace=False):
|
||||||
query = {}
|
query = {}
|
||||||
if self.current_mode != 'book_list':
|
if self.current_mode == 'book_list':
|
||||||
|
if self.ui.current_panel != self.ui.ROOT_PANEL:
|
||||||
|
query.panel = self.ui.current_panel
|
||||||
|
else:
|
||||||
query.current_mode = self.current_mode
|
query.current_mode = self.current_mode
|
||||||
idata = self.interface_data
|
idata = self.interface_data
|
||||||
if idata.library_id != idata.default_library:
|
if idata.library_id != idata.default_library:
|
||||||
@ -77,4 +94,8 @@ class Boss:
|
|||||||
if sq:
|
if sq:
|
||||||
query.search = sq
|
query.search = sq
|
||||||
query = encode_query(query) or '?'
|
query = encode_query(query) or '?'
|
||||||
window.history.pushState(None, '', query)
|
if replace:
|
||||||
|
window.history.replaceState(None, '', query)
|
||||||
|
else:
|
||||||
|
window.history.pushState(None, '', query)
|
||||||
|
self.history_count += 1
|
||||||
|
@ -26,118 +26,103 @@ class ClosePanelBar(BarState):
|
|||||||
|
|
||||||
class UIState:
|
class UIState:
|
||||||
|
|
||||||
def __init__(self, top_bar_state=None, main_panel=None, panel_data=None, is_cacheable=True):
|
def __init__(self, top_bar_state=None, main_panel=None, panel_data=None):
|
||||||
self.top_bar_state = top_bar_state
|
self.top_bar_state = top_bar_state
|
||||||
self.main_panel = main_panel or get_boss().ui.items_view
|
self.main_panel = main_panel
|
||||||
if type(self.main_panel) == 'string':
|
|
||||||
self.main_panel = getattr(get_boss().ui, self.main_panel)
|
|
||||||
self.panel_data = panel_data
|
self.panel_data = panel_data
|
||||||
self.is_cacheable = is_cacheable
|
|
||||||
|
|
||||||
def add_button(self, **kw):
|
def add_button(self, **kw):
|
||||||
self.top_bar_state.add_button(**kw)
|
self.top_bar_state.add_button(**kw)
|
||||||
|
|
||||||
panels = {}
|
|
||||||
|
|
||||||
def panel(key):
|
|
||||||
ans = panels[key]
|
|
||||||
if not ans:
|
|
||||||
ans = create_panel[key]()
|
|
||||||
if ans.is_cacheable:
|
|
||||||
panels[key] = ans
|
|
||||||
return ans
|
|
||||||
|
|
||||||
def close_panel():
|
def close_panel():
|
||||||
get_boss().ui.close_panel()
|
get_boss().ui.close_panel()
|
||||||
|
|
||||||
def replace_panel_action(replacement):
|
def replace_panel_action(replacement):
|
||||||
return def():
|
return def():
|
||||||
get_boss().ui.replace_panel(panel(replacement))
|
get_boss().ui.replace_panel(replacement)
|
||||||
|
|
||||||
def show_panel_action(key):
|
def show_panel_action(key):
|
||||||
return def():
|
return def():
|
||||||
get_boss().ui.show_panel(panel(key))
|
get_boss().ui.show_panel(key)
|
||||||
|
|
||||||
create_panel = {
|
|
||||||
'more-actions-menu': def more_actions_menu():
|
|
||||||
return UIState(ClosePanelBar(_('More actions')), panel_data=[
|
|
||||||
create_item(_('Book List Mode'), replace_panel_action('booklist-mode-menu'), _('Change how the list of books is displayed')),
|
|
||||||
])
|
|
||||||
,
|
|
||||||
|
|
||||||
'booklist-mode-menu': def booklist_mode_menu():
|
|
||||||
return UIState(ClosePanelBar(_('Book List Mode')), panel_data=[])
|
|
||||||
,
|
|
||||||
|
|
||||||
'booklist-sort-menu': def change_booklist_sort():
|
|
||||||
data = get_boss().ui.books_view.sort_panel_data(create_item)
|
|
||||||
return UIState(ClosePanelBar(_('Sort books')), panel_data=data, is_cacheable=False)
|
|
||||||
,
|
|
||||||
|
|
||||||
'booklist-search': def search_panel():
|
|
||||||
return UIState(ClosePanelBar(_('Search for books')), main_panel='search_panel')
|
|
||||||
,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
def create_book_view_top_bar_state(books_view):
|
||||||
|
ibs = BarState(run_animation=True, title=def():
|
||||||
|
q = books_view.interface_data['search_result']['query']
|
||||||
|
if q:
|
||||||
|
return {'title': _('Books matching') + ':: ' + q, 'title_tooltip':_('Click to clear this search'),
|
||||||
|
'title_action':def():
|
||||||
|
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='search', tooltip=_('Search for books'), action=show_panel_action('booklist-search'))
|
||||||
|
ibs.add_button(icon_name='ellipsis-v', tooltip=_('More actions'), action=show_panel_action('more-actions-menu'))
|
||||||
|
return ibs
|
||||||
|
|
||||||
class UI:
|
class UI:
|
||||||
|
|
||||||
|
ROOT_PANEL = 'books'
|
||||||
|
|
||||||
def __init__(self, interface_data, book_list_container):
|
def __init__(self, interface_data, book_list_container):
|
||||||
self.states, self.panels = [], []
|
|
||||||
self.top_bar = TopBar(book_list_container)
|
self.top_bar = TopBar(book_list_container)
|
||||||
self.books_view = BooksView(interface_data, book_list_container)
|
self.books_view = BooksView(interface_data, book_list_container)
|
||||||
self.items_view = ItemsView(interface_data, book_list_container)
|
self.items_view = ItemsView(interface_data, book_list_container)
|
||||||
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():
|
|
||||||
q = self.books_view.interface_data['search_result']['query']
|
|
||||||
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='search', tooltip=_('Search for books'), action=show_panel_action('booklist-search'))
|
|
||||||
ibs.add_button(icon_name='ellipsis-v', tooltip=_('More actions'), action=show_panel_action('more-actions-menu'))
|
|
||||||
self.states.append(UIState(ibs, self.books_view))
|
|
||||||
self.apply_state(self.states[0])
|
|
||||||
ibs.left_state.run_animation = False
|
|
||||||
window.addEventListener('resize', debounce(self.on_resize.bind(self), 250))
|
|
||||||
self.panels = [self.books_view, self.items_view, self.search_panel]
|
self.panels = [self.books_view, self.items_view, self.search_panel]
|
||||||
|
self.panel_map = {self.ROOT_PANEL: UIState(create_book_view_top_bar_state(self.books_view), main_panel=self.books_view)}
|
||||||
|
self.current_panel = self.ROOT_PANEL
|
||||||
|
window.addEventListener('resize', debounce(self.on_resize.bind(self), 250))
|
||||||
|
|
||||||
|
self.panel_map['more-actions-menu'] = UIState(ClosePanelBar(_('More actions')), panel_data=[
|
||||||
|
create_item(_('Book List Mode'), replace_panel_action('booklist-mode-menu'), _('Change how the list of books is displayed')),
|
||||||
|
])
|
||||||
|
|
||||||
|
self.panel_map['booklist-mode-menu'] = UIState(ClosePanelBar(_('Book List Mode')), panel_data=[])
|
||||||
|
|
||||||
|
self.panel_map['booklist-sort-menu'] = UIState(ClosePanelBar(_('Sort books')), panel_data=def():
|
||||||
|
return self.books_view.sort_panel_data(create_item)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.panel_map['booklist-search'] = UIState(ClosePanelBar(_('Search for books')), main_panel=self.search_panel)
|
||||||
|
|
||||||
def on_resize(self):
|
def on_resize(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def apply_state(self, state):
|
def apply_state(self):
|
||||||
|
state = self.panel_map[self.current_panel]
|
||||||
self.top_bar.apply_state(state.top_bar_state.left_state, state.top_bar_state.buttons)
|
self.top_bar.apply_state(state.top_bar_state.left_state, state.top_bar_state.buttons)
|
||||||
|
if self.current_panel == self.ROOT_PANEL:
|
||||||
|
# only run the beating heart animation once
|
||||||
|
state.top_bar_state.left_state.run_animation = False
|
||||||
|
main_panel = state.main_panel or self.items_view
|
||||||
for panel in self.panels:
|
for panel in self.panels:
|
||||||
panel.is_visible = panel is state.main_panel
|
panel.is_visible = panel is main_panel
|
||||||
if callable(state.main_panel.init):
|
if callable(main_panel.init):
|
||||||
state.main_panel.init(state.panel_data)
|
panel_data = state.panel_data() if callable(state.panel_data) else state.panel_data
|
||||||
|
main_panel.init(panel_data)
|
||||||
|
|
||||||
def close_panel(self):
|
def close_panel(self):
|
||||||
if len(self.states) > 1:
|
if get_boss().has_history:
|
||||||
self.states.pop()
|
window.history.back()
|
||||||
self.apply_state(self.states[-1])
|
else:
|
||||||
|
self.show_panel(self.ROOT_PANEL)
|
||||||
|
|
||||||
def close_all_panels(self):
|
def replace_panel(self, panel_name, force=False):
|
||||||
if len(self.states) > 1:
|
if force or panel_name != self.current_panel:
|
||||||
while len(self.states) > 1:
|
self.current_panel = panel_name or self.ROOT_PANEL
|
||||||
self.states.pop()
|
self.apply_state()
|
||||||
self.apply_state(self.states[-1])
|
get_boss().push_state(replace=True)
|
||||||
|
|
||||||
def replace_panel(self, state):
|
def show_panel(self, panel_name, push_state=True, force=False):
|
||||||
if len(self.states) > 1:
|
if force or panel_name != self.current_panel:
|
||||||
self.states.pop()
|
self.current_panel = panel_name or self.ROOT_PANEL
|
||||||
self.states.append(state)
|
self.apply_state()
|
||||||
self.apply_state(self.states[-1])
|
if push_state:
|
||||||
|
get_boss().push_state()
|
||||||
def show_panel(self, state):
|
|
||||||
self.states.append(state)
|
|
||||||
self.apply_state(self.states[-1])
|
|
||||||
|
|
||||||
def refresh_books_view(self):
|
def refresh_books_view(self):
|
||||||
self.books_view.refresh()
|
self.books_view.refresh()
|
||||||
if len(self.states) == 1:
|
if self.current_panel == self.ROOT_PANEL:
|
||||||
self.top_bar.refresh_left()
|
self.top_bar.refresh_left()
|
||||||
|
@ -227,18 +227,18 @@ class BooksView:
|
|||||||
boss.change_books(data)
|
boss.change_books(data)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
return error_dialog(_('Could not change sort order'), err + '', details=err.stack)
|
return error_dialog(_('Could not change sort order'), err + '', details=err.stack)
|
||||||
boss.ui.close_panel()
|
boss.ui.show_panel(boss.ui.ROOT_PANEL)
|
||||||
window.scrollTo(0, 0)
|
window.scrollTo(0, 0)
|
||||||
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, push_state=True):
|
def change_search(self, query, push_state=True, panel_to_show=None):
|
||||||
self.abort_get_more_books()
|
self.abort_get_more_books()
|
||||||
query = query or ''
|
query = query or ''
|
||||||
sd = get_session_data()
|
sd = get_session_data()
|
||||||
data = {'search':query, 'sort':sd.get('sort'), 'library_id':self.interface_data.library_id}
|
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), _(
|
ajax_progress_dialog('interface-data/get-books', self.search_change_completed.bind(self), _(
|
||||||
'Fetching data from server, please wait') + '…', query=data, extra_data_for_callback={'push_state':push_state})
|
'Fetching data from server, please wait') + '…', query=data, extra_data_for_callback={'push_state':push_state, 'panel_to_show': panel_to_show})
|
||||||
|
|
||||||
def search_change_completed(self, end_type, xhr, ev):
|
def search_change_completed(self, end_type, xhr, ev):
|
||||||
if end_type == 'load':
|
if end_type == 'load':
|
||||||
@ -248,10 +248,9 @@ class BooksView:
|
|||||||
boss.change_books(data)
|
boss.change_books(data)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
return error_dialog(_('Could not change search query'), err + '', details=err.stack)
|
return error_dialog(_('Could not change search query'), err + '', details=err.stack)
|
||||||
boss.ui.close_all_panels()
|
|
||||||
if xhr.extra_data_for_callback.push_state:
|
|
||||||
boss.push_state()
|
|
||||||
self.update_fetching_status()
|
self.update_fetching_status()
|
||||||
|
ed = xhr.extra_data_for_callback
|
||||||
|
boss.ui.show_panel(ed.panel_to_show or boss.ui.ROOT_PANEL, push_state=ed.push_state)
|
||||||
window.scrollTo(0, 0)
|
window.scrollTo(0, 0)
|
||||||
elif end_type != 'abort':
|
elif end_type != 'abort':
|
||||||
msg = xhr.error_html
|
msg = xhr.error_html
|
||||||
|
@ -8,7 +8,7 @@ from session import UserSessionData
|
|||||||
from utils import parse_url_params
|
from utils import parse_url_params
|
||||||
|
|
||||||
from book_list.boss import Boss
|
from book_list.boss import Boss
|
||||||
from book_list.globals import set_boss, set_session_data
|
from book_list.globals import set_session_data
|
||||||
|
|
||||||
def on_library_loaded(end_type, xhr, ev):
|
def on_library_loaded(end_type, xhr, ev):
|
||||||
p = document.getElementById('page_load_progress')
|
p = document.getElementById('page_load_progress')
|
||||||
@ -17,8 +17,7 @@ def on_library_loaded(end_type, xhr, ev):
|
|||||||
interface_data = JSON.parse(xhr.responseText)
|
interface_data = JSON.parse(xhr.responseText)
|
||||||
sd = UserSessionData(interface_data['username'], interface_data['user_session_data'])
|
sd = UserSessionData(interface_data['username'], interface_data['user_session_data'])
|
||||||
set_session_data(sd)
|
set_session_data(sd)
|
||||||
boss = Boss(interface_data)
|
Boss(interface_data)
|
||||||
set_boss(boss)
|
|
||||||
else:
|
else:
|
||||||
p = E.p(style='color:red; font-weight: bold; font-size:1.5em')
|
p = E.p(style='color:red; font-weight: bold; font-size:1.5em')
|
||||||
if xhr.status == 401:
|
if xhr.status == 401:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user