Implement opening of local ebook files

This commit is contained in:
Kovid Goyal 2019-08-14 15:17:05 +05:30
parent 48a2990600
commit 95aa3a50c2
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
8 changed files with 139 additions and 17 deletions

View File

@ -156,7 +156,7 @@ def main(args=sys.argv):
app.setWindowIcon(QIcon(I('viewer.png'))) app.setWindowIcon(QIcon(I('viewer.png')))
main = EbookViewer() main = EbookViewer()
main.set_exception_handler() main.set_exception_handler()
if args: if len(args) > 1:
acc.events.append(args[-1]) acc.events.append(args[-1])
acc.got_file.connect(main.handle_commandline_arg) acc.got_file.connect(main.handle_commandline_arg)
main.show() main.show()

View File

@ -17,7 +17,8 @@ from PyQt5.Qt import (
from calibre import prints from calibre import prints
from calibre.constants import config_dir 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.main_window import MainWindow
from calibre.gui2.viewer.annotations import ( from calibre.gui2.viewer.annotations import (
merge_annotations, parse_annotations, save_annots_to_epub, serialize_annotations 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.toggle_bookmarks.connect(self.toggle_bookmarks)
self.web_view.update_current_toc_nodes.connect(self.toc.update_current_toc_nodes) 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.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.setCentralWidget(self.web_view)
self.restore_state() self.restore_state()
@ -161,6 +163,17 @@ class EbookViewer(MainWindow):
# Load book {{{ # 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): def load_ebook(self, pathtoebook, open_at=None, reload_book=False):
# TODO: Implement open_at # TODO: Implement open_at
self.setWindowTitle(_('Loading book … — {}').format(self.base_window_title)) 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']), 'Failed to open the book at {0}. Click "Show details" for more info.').format(data['pathtoebook']),
det_msg=data['tb'], show=True) det_msg=data['tb'], show=True)
return return
set_book_path(data['base']) set_book_path(data['base'], data['pathtoebook'])
self.current_book_data = data self.current_book_data = data
self.current_book_data['annotations_map'] = defaultdict(list) self.current_book_data['annotations_map'] = defaultdict(list)
self.current_book_data['annotations_path_key'] = path_key(data['pathtoebook']) + '.json' self.current_book_data['annotations_path_key'] = path_key(data['pathtoebook']) + '.json'

View File

@ -45,14 +45,14 @@ vprefs.defaults['main_window_geometry'] = None
# Override network access to load data from the book {{{ # Override network access to load data from the book {{{
def set_book_path(path=None): def set_book_path(path, pathtoebook):
if path is not None: set_book_path.pathtoebook = pathtoebook
set_book_path.path = os.path.abspath(path) set_book_path.path = os.path.abspath(path)
set_book_path.metadata = get_data('calibre-book-metadata.json')[0] 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.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.metadata = get_data('calibre-book-metadata.json')[0]
set_book_path.parsed_metadata = json_loads(set_book_path.metadata) set_book_path.parsed_metadata = json_loads(set_book_path.metadata)
set_book_path.parsed_manifest = json_loads(set_book_path.manifest) set_book_path.parsed_manifest = json_loads(set_book_path.manifest)
def get_data(name): def get_data(name):
@ -189,6 +189,7 @@ class ViewerBridge(Bridge):
update_current_toc_nodes = from_js(object, object) update_current_toc_nodes = from_js(object, object)
toggle_full_screen = from_js() toggle_full_screen = from_js()
report_cfi = from_js(object, object) report_cfi = from_js(object, object)
ask_for_open = from_js(object)
create_view = to_js() create_view = to_js()
show_preparing_message = to_js() show_preparing_message = to_js()
@ -305,6 +306,7 @@ class WebView(RestartingWebEngineView):
toggle_bookmarks = pyqtSignal() toggle_bookmarks = pyqtSignal()
update_current_toc_nodes = pyqtSignal(object, object) update_current_toc_nodes = pyqtSignal(object, object)
toggle_full_screen = pyqtSignal() toggle_full_screen = pyqtSignal()
ask_for_open = pyqtSignal(object)
def __init__(self, parent=None): def __init__(self, parent=None):
self._host_widget = None self._host_widget = None
@ -324,6 +326,7 @@ class WebView(RestartingWebEngineView):
self.bridge.toggle_bookmarks.connect(self.toggle_bookmarks) self.bridge.toggle_bookmarks.connect(self.toggle_bookmarks)
self.bridge.update_current_toc_nodes.connect(self.update_current_toc_nodes) self.bridge.update_current_toc_nodes.connect(self.update_current_toc_nodes)
self.bridge.toggle_full_screen.connect(self.toggle_full_screen) 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.bridge.report_cfi.connect(self.call_callback)
self.pending_bridge_ready_actions = {} self.pending_bridge_ready_actions = {}
self.setPage(self._page) self.setPage(self._page)
@ -392,7 +395,7 @@ class WebView(RestartingWebEngineView):
def start_book_load(self, initial_cfi=None): def start_book_load(self, initial_cfi=None):
key = (set_book_path.path,) 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): def execute_when_ready(self, action, *args):
if self.bridge.ready: if self.bridge.ready:

View File

@ -0,0 +1,62 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net>
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

View File

@ -10,8 +10,9 @@ from book_list.router import home
from book_list.theme import get_color from book_list.theme import get_color
from dom import add_extra_css, build_rule, clear, set_css, svgicon, unique_id from dom import add_extra_css, build_rule, clear, set_css, svgicon, unique_id
from modals import error_dialog 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.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.font_size import create_font_size_panel
from read_book.prefs.main import create_prefs_panel from read_book.prefs.main import create_prefs_panel
from read_book.toc import create_toc_panel from read_book.toc import create_toc_panel
@ -220,7 +221,10 @@ class MainOverlay:
back_action = ac(_('Back'), None, self.back, 'arrow-left') back_action = ac(_('Back'), None, self.back, 'arrow-left')
forward_action = ac(_('Forward'), None, self.forward, 'arrow-right') forward_action = ac(_('Forward'), None, self.forward, 'arrow-right')
if runtime.is_standalone_viewer: 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) nav_actions = E.ul(back_action, forward_action)
else: else:
reload_actions = E.ul(sync_action, delete_action, reload_action) 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) 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: 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.panels = [DeleteBook(self, _('Are you sure you want to reload this book?'), 'refresh', _('Reload book'), True)]
self.show_current_panel() 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): def sync_book(self):
self.hide_current_panel() self.hide_current_panel()
self.panels = [SyncBook(self)] self.panels = [SyncBook(self)]

View File

@ -18,6 +18,7 @@ from read_book.globals import (
current_book, runtime, set_current_spine_item, ui_operations current_book, runtime, set_current_spine_item, ui_operations
) )
from read_book.goto import get_next_section 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.overlay import Overlay
from read_book.prefs.colors import resolve_color_scheme from read_book.prefs.colors import resolve_color_scheme
from read_book.prefs.font_size import change_font_size_by from read_book.prefs.font_size import change_font_size_by
@ -382,6 +383,8 @@ class View:
self.content_popup_overlay.loaded_resources = {} self.content_popup_overlay.loaded_resources = {}
self.timers.start_book(book) self.timers.start_book(book)
self.book = current_book.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: if ui_operations.update_last_read_time:
ui_operations.update_last_read_time(book) ui_operations.update_last_read_time(book)
pos = {'replace_history':True} pos = {'replace_history':True}

View File

@ -42,6 +42,7 @@ defaults = {
'word_actions': v'[]', 'word_actions': v'[]',
'standalone_font_settings': {}, 'standalone_font_settings': {},
'standalone_misc_settings': {}, 'standalone_misc_settings': {},
'standalone_recently_opened': v'[]',
} }
is_local_setting = { is_local_setting = {
@ -59,6 +60,7 @@ is_local_setting = {
'controls_help_shown_count': True, 'controls_help_shown_count': True,
'standalone_font_settings': True, 'standalone_font_settings': True,
'standalone_misc_settings': True, 'standalone_misc_settings': True,
'standalone_recently_opened': True,
} }

View File

@ -124,13 +124,14 @@ def show_error(title, msg, details):
error_dialog(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 nonlocal book
if end_type is 'load': if end_type is 'load':
book = new_book(key, {}) book = new_book(key, {})
data = xhr.response data = xhr.response
book.manifest = data[0] book.manifest = data[0]
book.metadata = book.manifest.metadata = data[1] book.metadata = book.manifest.metadata = data[1]
book.manifest.pathtoebook = pathtoebook
book.stored_files = {} book.stored_files = {}
book.is_complete = True book.is_complete = True
v'delete book.manifest["metadata"]' v'delete book.manifest["metadata"]'
@ -181,6 +182,7 @@ def create_view(prefs, all_font_families):
if view is None: if view is None:
create_session_data(prefs) create_session_data(prefs)
view = View(document.getElementById('view')) view = View(document.getElementById('view'))
view.overlay.open_book(False)
@from_python @from_python
@ -189,8 +191,8 @@ def show_preparing_message(msg):
@from_python @from_python
def start_book_load(key, initial_cfi): def start_book_load(key, initial_cfi, pathtoebook):
xhr = ajax('manifest', manifest_received.bind(None, key, initial_cfi), ok_code=0) xhr = ajax('manifest', manifest_received.bind(None, key, initial_cfi, pathtoebook), ok_code=0)
xhr.responseType = 'json' xhr.responseType = 'json'
xhr.send() xhr.send()
@ -257,6 +259,8 @@ if window is window.top:
to_python.toggle_full_screen() to_python.toggle_full_screen()
ui_operations.report_cfi = def(request_id, data): ui_operations.report_cfi = def(request_id, data):
to_python.report_cfi(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')) document.body.appendChild(E.div(id='view'))
window.onerror = onerror window.onerror = onerror
create_modal_container() create_modal_container()