From 95aa3a50c27483d617921068b8454c7f949b4482 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 14 Aug 2019 15:17:05 +0530 Subject: [PATCH] Implement opening of local ebook files --- src/calibre/gui2/viewer/main.py | 2 +- src/calibre/gui2/viewer/ui.py | 17 +++++++- src/calibre/gui2/viewer/web_view.py | 21 +++++----- src/pyj/read_book/open_book.pyj | 62 +++++++++++++++++++++++++++++ src/pyj/read_book/overlay.pyj | 39 +++++++++++++++++- src/pyj/read_book/view.pyj | 3 ++ src/pyj/session.pyj | 2 + src/pyj/viewer-main.pyj | 10 +++-- 8 files changed, 139 insertions(+), 17 deletions(-) create mode 100644 src/pyj/read_book/open_book.pyj diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index 2b2323275e..4c4d9db47e 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -156,7 +156,7 @@ def main(args=sys.argv): app.setWindowIcon(QIcon(I('viewer.png'))) main = EbookViewer() main.set_exception_handler() - if args: + if len(args) > 1: acc.events.append(args[-1]) acc.got_file.connect(main.handle_commandline_arg) main.show() diff --git a/src/calibre/gui2/viewer/ui.py b/src/calibre/gui2/viewer/ui.py index a0a850ba0f..178f2274d0 100644 --- a/src/calibre/gui2/viewer/ui.py +++ b/src/calibre/gui2/viewer/ui.py @@ -17,7 +17,8 @@ from PyQt5.Qt import ( from calibre import prints from calibre.constants import config_dir -from calibre.gui2 import error_dialog +from calibre.customize.ui import available_input_formats +from calibre.gui2 import choose_files, error_dialog from calibre.gui2.main_window import MainWindow from calibre.gui2.viewer.annotations import ( merge_annotations, parse_annotations, save_annots_to_epub, serialize_annotations @@ -92,6 +93,7 @@ class EbookViewer(MainWindow): self.web_view.toggle_bookmarks.connect(self.toggle_bookmarks) self.web_view.update_current_toc_nodes.connect(self.toc.update_current_toc_nodes) self.web_view.toggle_full_screen.connect(self.toggle_full_screen) + self.web_view.ask_for_open.connect(self.ask_for_open, type=Qt.QueuedConnection) self.setCentralWidget(self.web_view) self.restore_state() @@ -161,6 +163,17 @@ class EbookViewer(MainWindow): # Load book {{{ + def ask_for_open(self, path=None): + if path is None: + files = choose_files( + self, 'ebook viewer open dialog', + _('Choose e-book'), [(_('E-books'), available_input_formats())], + all_files=False, select_only_single_file=True) + if not files: + return + path = files[0] + self.load_ebook(path) + def load_ebook(self, pathtoebook, open_at=None, reload_book=False): # TODO: Implement open_at self.setWindowTitle(_('Loading book … — {}').format(self.base_window_title)) @@ -193,7 +206,7 @@ class EbookViewer(MainWindow): 'Failed to open the book at {0}. Click "Show details" for more info.').format(data['pathtoebook']), det_msg=data['tb'], show=True) return - set_book_path(data['base']) + set_book_path(data['base'], data['pathtoebook']) self.current_book_data = data self.current_book_data['annotations_map'] = defaultdict(list) self.current_book_data['annotations_path_key'] = path_key(data['pathtoebook']) + '.json' diff --git a/src/calibre/gui2/viewer/web_view.py b/src/calibre/gui2/viewer/web_view.py index 565221162f..83851b60c6 100644 --- a/src/calibre/gui2/viewer/web_view.py +++ b/src/calibre/gui2/viewer/web_view.py @@ -45,14 +45,14 @@ vprefs.defaults['main_window_geometry'] = None # Override network access to load data from the book {{{ -def set_book_path(path=None): - if path is not None: - set_book_path.path = os.path.abspath(path) - set_book_path.metadata = get_data('calibre-book-metadata.json')[0] - set_book_path.manifest, set_book_path.manifest_mime = get_data('calibre-book-manifest.json') - set_book_path.metadata = get_data('calibre-book-metadata.json')[0] - set_book_path.parsed_metadata = json_loads(set_book_path.metadata) - set_book_path.parsed_manifest = json_loads(set_book_path.manifest) +def set_book_path(path, pathtoebook): + set_book_path.pathtoebook = pathtoebook + set_book_path.path = os.path.abspath(path) + set_book_path.metadata = get_data('calibre-book-metadata.json')[0] + set_book_path.manifest, set_book_path.manifest_mime = get_data('calibre-book-manifest.json') + set_book_path.metadata = get_data('calibre-book-metadata.json')[0] + set_book_path.parsed_metadata = json_loads(set_book_path.metadata) + set_book_path.parsed_manifest = json_loads(set_book_path.manifest) def get_data(name): @@ -189,6 +189,7 @@ class ViewerBridge(Bridge): update_current_toc_nodes = from_js(object, object) toggle_full_screen = from_js() report_cfi = from_js(object, object) + ask_for_open = from_js(object) create_view = to_js() show_preparing_message = to_js() @@ -305,6 +306,7 @@ class WebView(RestartingWebEngineView): toggle_bookmarks = pyqtSignal() update_current_toc_nodes = pyqtSignal(object, object) toggle_full_screen = pyqtSignal() + ask_for_open = pyqtSignal(object) def __init__(self, parent=None): self._host_widget = None @@ -324,6 +326,7 @@ class WebView(RestartingWebEngineView): self.bridge.toggle_bookmarks.connect(self.toggle_bookmarks) self.bridge.update_current_toc_nodes.connect(self.update_current_toc_nodes) self.bridge.toggle_full_screen.connect(self.toggle_full_screen) + self.bridge.ask_for_open.connect(self.ask_for_open) self.bridge.report_cfi.connect(self.call_callback) self.pending_bridge_ready_actions = {} self.setPage(self._page) @@ -392,7 +395,7 @@ class WebView(RestartingWebEngineView): def start_book_load(self, initial_cfi=None): key = (set_book_path.path,) - self.execute_when_ready('start_book_load', key, initial_cfi) + self.execute_when_ready('start_book_load', key, initial_cfi, set_book_path.pathtoebook) def execute_when_ready(self, action, *args): if self.bridge.ready: diff --git a/src/pyj/read_book/open_book.pyj b/src/pyj/read_book/open_book.pyj new file mode 100644 index 0000000000..f6faa5d1a7 --- /dev/null +++ b/src/pyj/read_book/open_book.pyj @@ -0,0 +1,62 @@ +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2019, Kovid Goyal +from __python__ import bound_methods, hash_literals + +from elementmaker import E +from gettext import gettext as _ + +from book_list.globals import get_session_data +from book_list.item_list import create_item, create_item_list +from dom import unique_id +from read_book.globals import ui_operations +from widgets import create_button + + +def create_open_book(container, book): + container.appendChild(E.div(style='margin: 1rem')) + container = container.lastChild + container.appendChild(create_button(_('Open a book from your computer'), action=ui_operations.ask_for_open.bind(None, None))) + sd = get_session_data() + rl = sd.get('standalone_recently_opened') + if rl.length: + container.appendChild(E.div(id=unique_id())) + c = container.lastChild + items = [] + c.appendChild(E.div(style='margin-top: 1rem', _('Recently viewed books'))) + c.appendChild(E.div()) + for entry in rl: + if book and book.manifest.pathtoebook is entry.pathtoebook: + continue + fname = str.replace(entry.pathtoebook, '\\', '/') + if '/' in fname: + fname = fname.rpartition('/')[-1] + items.push(create_item(entry.title, ui_operations.ask_for_open.bind(None, entry.pathtoebook), fname)) + create_item_list(c.lastChild, items) + + c.appendChild(E.div(style='margin: 1rem')) + c.lastChild.appendChild( + create_button(_('Clear recent list'), action=clear_recent_list.bind(None, c.id))) + + +def clear_recent_list(container_id): + sd = get_session_data() + sd.set('standalone_recently_opened', v'[]') + document.getElementById(container_id).style.display = 'none' + + +def add_book_to_recently_viewed(book): + sd = get_session_data() + rl = sd.get('standalone_recently_opened') + key = book.manifest.pathtoebook + new_entry = { + 'key': key, 'pathtoebook': book.manifest.pathtoebook, + 'title': book.metadata.title, 'authors': book.metadata.authors, + 'timestamp': Date().toISOString(), + } + ans = v'[]' + ans.push(new_entry) + for entry in rl: + if entry.key is not key: + ans.push(entry) + sd.set('standalone_recently_opened', ans.slice(0, 25)) + return ans diff --git a/src/pyj/read_book/overlay.pyj b/src/pyj/read_book/overlay.pyj index b9e4f34efb..90a0f20a43 100644 --- a/src/pyj/read_book/overlay.pyj +++ b/src/pyj/read_book/overlay.pyj @@ -10,8 +10,9 @@ from book_list.router import home from book_list.theme import get_color from dom import add_extra_css, build_rule, clear, set_css, svgicon, unique_id from modals import error_dialog -from read_book.globals import ui_operations, runtime +from read_book.globals import runtime, ui_operations from read_book.goto import create_goto_panel +from read_book.open_book import create_open_book from read_book.prefs.font_size import create_font_size_panel from read_book.prefs.main import create_prefs_panel from read_book.toc import create_toc_panel @@ -220,7 +221,10 @@ class MainOverlay: back_action = ac(_('Back'), None, self.back, 'arrow-left') forward_action = ac(_('Forward'), None, self.forward, 'arrow-right') if runtime.is_standalone_viewer: - reload_actions = E.ul(reload_action) + reload_actions = E.ul( + ac(_('Open book'), _('Open a new book'), self.overlay.open_book, 'book'), + reload_action + ) nav_actions = E.ul(back_action, forward_action) else: reload_actions = E.ul(sync_action, delete_action, reload_action) @@ -384,6 +388,30 @@ class FontSizeOverlay: # {{{ create_font_size_panel(container, self.overlay.hide_current_panel) # }}} +class OpenBook: # {{{ + + def __init__(self, overlay, closeable): + self.overlay = overlay + self.closeable = closeable + + def on_container_click(self, evt): + pass # Dont allow panel to be closed by a click + + def show(self, container): + container.style.backgroundColor = get_color('window-background') + close_button_style = '' if self.closeable else 'display: none' + container.appendChild(E.div( + style='padding: 1ex 1em; border-bottom: solid 1px currentColor; display:flex; justify-content: space-between', + E.h2(_('Open a new book')), + E.div( + svgicon('close'), style=f'cursor:pointer; {close_button_style}', + onclick=def(event):event.preventDefault(), event.stopPropagation(), self.overlay.hide_current_panel(event);, + class_='simple-link'), + )) + create_open_book(container, self.overlay.view?.book) +# }}} + + class Overlay: @@ -458,6 +486,13 @@ class Overlay: self.panels = [DeleteBook(self, _('Are you sure you want to reload this book?'), 'refresh', _('Reload book'), True)] self.show_current_panel() + def open_book(self, closeable): + self.hide_current_panel() + if jstype(closeable) is not 'boolean': + closeable = True + self.panels = [OpenBook(self, closeable)] + self.show_current_panel() + def sync_book(self): self.hide_current_panel() self.panels = [SyncBook(self)] diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index ab86389e4e..3271e3ca99 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -18,6 +18,7 @@ from read_book.globals import ( current_book, runtime, set_current_spine_item, ui_operations ) from read_book.goto import get_next_section +from read_book.open_book import add_book_to_recently_viewed from read_book.overlay import Overlay from read_book.prefs.colors import resolve_color_scheme from read_book.prefs.font_size import change_font_size_by @@ -382,6 +383,8 @@ class View: self.content_popup_overlay.loaded_resources = {} self.timers.start_book(book) self.book = current_book.book = book + if runtime.is_standalone_viewer: + add_book_to_recently_viewed(book) if ui_operations.update_last_read_time: ui_operations.update_last_read_time(book) pos = {'replace_history':True} diff --git a/src/pyj/session.pyj b/src/pyj/session.pyj index df4c11c83a..fffa7d28f7 100644 --- a/src/pyj/session.pyj +++ b/src/pyj/session.pyj @@ -42,6 +42,7 @@ defaults = { 'word_actions': v'[]', 'standalone_font_settings': {}, 'standalone_misc_settings': {}, + 'standalone_recently_opened': v'[]', } is_local_setting = { @@ -59,6 +60,7 @@ is_local_setting = { 'controls_help_shown_count': True, 'standalone_font_settings': True, 'standalone_misc_settings': True, + 'standalone_recently_opened': True, } diff --git a/src/pyj/viewer-main.pyj b/src/pyj/viewer-main.pyj index 1eb69219ed..846720ebae 100644 --- a/src/pyj/viewer-main.pyj +++ b/src/pyj/viewer-main.pyj @@ -124,13 +124,14 @@ def show_error(title, msg, details): error_dialog(title, msg, details) -def manifest_received(key, initial_cfi, end_type, xhr, ev): +def manifest_received(key, initial_cfi, pathtoebook, end_type, xhr, ev): nonlocal book if end_type is 'load': book = new_book(key, {}) data = xhr.response book.manifest = data[0] book.metadata = book.manifest.metadata = data[1] + book.manifest.pathtoebook = pathtoebook book.stored_files = {} book.is_complete = True v'delete book.manifest["metadata"]' @@ -181,6 +182,7 @@ def create_view(prefs, all_font_families): if view is None: create_session_data(prefs) view = View(document.getElementById('view')) + view.overlay.open_book(False) @from_python @@ -189,8 +191,8 @@ def show_preparing_message(msg): @from_python -def start_book_load(key, initial_cfi): - xhr = ajax('manifest', manifest_received.bind(None, key, initial_cfi), ok_code=0) +def start_book_load(key, initial_cfi, pathtoebook): + xhr = ajax('manifest', manifest_received.bind(None, key, initial_cfi, pathtoebook), ok_code=0) xhr.responseType = 'json' xhr.send() @@ -257,6 +259,8 @@ if window is window.top: to_python.toggle_full_screen() ui_operations.report_cfi = def(request_id, data): to_python.report_cfi(request_id, data) + ui_operations.ask_for_open = def(path): + to_python.ask_for_open(path) document.body.appendChild(E.div(id='view')) window.onerror = onerror create_modal_container()