From 85fcea41fb1251cddd5ce13775f6600e3d2edae8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 29 Mar 2019 19:56:08 +0530 Subject: [PATCH] Finish porting of web store dialog to QtWebEngine --- src/calibre/gui2/store/web_store.py | 184 +++++++++++++++++++-- src/calibre/gui2/store/web_store_dialog.py | 19 ++- src/calibre/gui2/ui.py | 19 ++- 3 files changed, 201 insertions(+), 21 deletions(-) diff --git a/src/calibre/gui2/store/web_store.py b/src/calibre/gui2/store/web_store.py index 8c5ee5ddcd..327c1cc863 100644 --- a/src/calibre/gui2/store/web_store.py +++ b/src/calibre/gui2/store/web_store.py @@ -6,22 +6,76 @@ from __future__ import absolute_import, division, print_function, unicode_litera import json import os +import shutil from base64 import standard_b64decode, standard_b64encode from PyQt5.Qt import ( - QHBoxLayout, QProgressBar, QPushButton, QVBoxLayout, QWidget, pyqtSignal + QHBoxLayout, QIcon, QLabel, QProgressBar, QPushButton, QSize, QUrl, QVBoxLayout, + QWidget, pyqtSignal ) -from PyQt5.QtWebEngineWidgets import QWebEngineView +from PyQt5.QtWebEngineWidgets import QWebEngineProfile, QWebEngineView -from calibre import url_slash_cleaner -from calibre.constants import STORE_DIALOG_APP_UID, islinux, iswindows -from calibre.gui2 import Application, set_app_uid +from calibre import random_user_agent, url_slash_cleaner +from calibre.constants import STORE_DIALOG_APP_UID, cache_dir, islinux, iswindows +from calibre.ebooks import BOOK_EXTENSIONS +from calibre.gui2 import ( + Application, choose_save_file, error_dialog, gprefs, info_dialog, set_app_uid +) +from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.main_window import MainWindow -from calibre.ptempfile import reset_base_dir +from calibre.ptempfile import PersistentTemporaryDirectory, reset_base_dir +from calibre.utils.ipc import RC +from polyglot.builtins import string_or_bytes -class View(QWebEngineView): - pass +class DownloadItem(QWidget): + + def __init__(self, download_id, filename, parent=None): + QWidget.__init__(self, parent) + self.l = l = QHBoxLayout(self) + self.la = la = QLabel('{}:\xa0'.format(filename)) + la.setMaximumWidth(400) + l.addWidget(la) + + self.pb = pb = QProgressBar(self) + pb.setRange(0, 0) + l.addWidget(pb) + + self.download_id = download_id + + def __call__(self, done, total): + self.pb.setRange(0, total) + self.pb.setValue(done) + + +class DownloadProgress(QWidget): + + def __init__(self, parent=None): + QWidget.__init__(self, parent) + self.setVisible(False) + self.l = QVBoxLayout(self) + self.items = {} + + def add_item(self, download_id, filename): + self.setVisible(True) + item = DownloadItem(download_id, filename, self) + self.l.addWidget(item) + self.items[download_id] = item + + def update_item(self, download_id, done, total): + item = self.items.get(download_id) + if item is not None: + item(done, total) + + def remove_item(self, download_id): + item = self.items.pop(download_id, None) + if item is not None: + self.l.removeWidget(item) + item.setVisible(False) + item.setParent(None) + item.deleteLater() + if not self.items: + self.setVisible(False) class Central(QWidget): @@ -31,13 +85,16 @@ class Central(QWidget): def __init__(self, parent=None): QWidget.__init__(self, parent) self.l = l = QVBoxLayout(self) - self.view = v = View(self) + self.view = v = QWebEngineView(self) v.loadStarted.connect(self.load_started) v.loadProgress.connect(self.load_progress) v.loadFinished.connect(self.load_finished) l.addWidget(v) self.h = h = QHBoxLayout() l.addLayout(h) + self.download_progress = d = DownloadProgress(self) + h.addWidget(d) + self.home_button = b = QPushButton(_('Home')) b.clicked.connect(self.home) h.addWidget(b) @@ -51,6 +108,10 @@ class Central(QWidget): self.progress_bar = b = QProgressBar(self) h.addWidget(b) + self.reload_button = b = QPushButton(_('Reload')) + b.clicked.connect(v.reload) + h.addWidget(b) + def load_started(self): self.progress_bar.setValue(0) @@ -65,10 +126,29 @@ class Main(MainWindow): def __init__(self, data): MainWindow.__init__(self, None) + self.setWindowIcon(QIcon(I('store.png'))) + self.setWindowTitle(data['window_title']) + self.download_data = {} + profile = QWebEngineProfile.defaultProfile() + profile.setCachePath(os.path.join(cache_dir(), 'web_store', 'hc')) + profile.setPersistentStoragePath(os.path.join(cache_dir(), 'web_store', 'ps')) + profile.setHttpUserAgent(random_user_agent(allow_ie=False)) + profile.downloadRequested.connect(self.download_requested) self.data = data self.central = c = Central(self) c.home.connect(self.go_home) self.setCentralWidget(c) + geometry = gprefs.get('store_dialog_main_window_geometry') + if geometry is not None: + self.restoreGeometry(geometry) + self.go_to(data['detail_url'] or None) + + def sizeHint(self): + return QSize(1024, 740) + + def closeEvent(self, e): + gprefs.set('store_dialog_main_window_geometry', bytearray(self.saveGeometry())) + MainWindow.closeEvent(self, e) @property def view(self): @@ -80,7 +160,74 @@ class Main(MainWindow): def go_to(self, url=None): url = url or self.data['base_url'] url = url_slash_cleaner(url) - self.view.load(url) + self.view.load(QUrl(url)) + + def download_requested(self, download_item): + path = download_item.path() + fname = os.path.basename(path) + download_id = download_item.id() + tdir = PersistentTemporaryDirectory() + self.download_data[download_id] = download_item + path = os.path.join(tdir, fname) + download_item.setPath(path) + connect_lambda(download_item.downloadProgress, self, lambda self, done, total: self.download_progress(download_id, done, total)) + connect_lambda(download_item.finished, self, lambda self: self.download_finished(download_id)) + download_item.accept() + self.central.download_progress.add_item(download_id, fname) + + def download_progress(self, download_id, done, total): + self.central.download_progress.update_item(download_id, done, total) + + def download_finished(self, download_id): + self.central.download_progress.remove_item(download_id) + download_item = self.download_data.pop(download_id) + path = download_item.path() + fname = os.path.basename(path) + if download_item.state() == download_item.DownloadInterrupted: + error_dialog(self, _('Download failed'), _( + 'Download of {0} failed with error: {1}').format(fname, download_item.interruptReasonString()), show=True) + return + ext = fname.rpartition('.')[-1].lower() + if ext not in BOOK_EXTENSIONS: + if ext == 'acsm': + if not confirm('

' + _( + 'This e-book is a DRMed EPUB file. ' + 'You will be prompted to save this file to your ' + 'computer. Once it is saved, open it with ' + '' + 'Adobe Digital Editions (ADE).

ADE, in turn ' + 'will download the actual e-book, which will be a ' + '.epub file. You can add this book to calibre ' + 'using "Add Books" and selecting the file from ' + 'the ADE library folder.'), + 'acsm_download', self): + return + name = choose_save_file(self, 'web-store-download-unknown', _( + 'File is not a supported e-book type. Save to disk?'), initial_filename=fname) + if name: + shutil.copyfile(path, name) + os.remove(path) + return + t = RC(print_error=False) + t.start() + t.join(3.0) + if t.conn is None: + error_dialog(self, _('No running calibre'), _( + 'No running calibre instance found. Please start calibre before trying to' + ' download books.'), show=True) + return + tags = self.data['tags'] + if isinstance(tags, string_or_bytes): + tags = list(filter(None, [x.strip() for x in tags.split(',')])) + data = json.dumps({'path': path, 'tags': tags}) + if not isinstance(data, bytes): + data = data.encode('utf-8') + t.conn.send(b'web-store:' + data) + t.conn.close() + + info_dialog(self, _('Download completed'), _( + 'Download of {0} has been completed, the book was added to' + ' your calibre library').format(fname), show=True) def main(args): @@ -95,18 +242,23 @@ def main(args): data = args[-1] data = json.loads(standard_b64decode(data)) - override = 'calibre-ebook-viewer' if islinux else None + override = 'calibre-gui' if islinux else None app = Application(args, override_program_name=override) + m = Main(data) + m.show(), m.raise_() app.exec_() + del m + del app if __name__ == '__main__': sample_data = standard_b64encode( json.dumps({ - u'window_title': u'MobileRead', - u'base_url': u'https://www.mobileread.com/', - u'detail_url': u'http://www.mobileread.com/forums/showthread.php?t=54477', - u'tags': u'' + 'window_title': 'MobileRead', + 'base_url': 'https://www.mobileread.com/', + 'detail_url': 'http://www.mobileread.com/forums/showthread.php?t=54477', + 'id':1, + 'tags': '', }) ) - main([sample_data]) + main(['store-dialog', sample_data]) diff --git a/src/calibre/gui2/store/web_store_dialog.py b/src/calibre/gui2/store/web_store_dialog.py index f15c4ec62c..bb0934a97f 100644 --- a/src/calibre/gui2/store/web_store_dialog.py +++ b/src/calibre/gui2/store/web_store_dialog.py @@ -6,15 +6,20 @@ from __future__ import absolute_import, division, print_function, unicode_litera import json from base64 import standard_b64encode +from itertools import count + +counter = count() class WebStoreDialog(object): - def __init__(self, gui, base_url, parent=None, detail_url=None, create_browser=None): + def __init__( + self, gui, base_url, parent=None, detail_url=None, create_browser=None + ): + self.id = next(counter) self.gui = gui self.base_url = base_url self.detail_url = detail_url - self.create_browser = create_browser self.window_title = None self.tags = None @@ -25,7 +30,13 @@ class WebStoreDialog(object): self.tags = tags def exec_(self): - data = {'base_url': self.base_url, 'detail_url': self.detail_url, 'window_title': self.window_title, 'tags': self.tags} + data = { + 'base_url': self.base_url, + 'detail_url': self.detail_url, + 'window_title': self.window_title, + 'tags': self.tags, + 'id': self.id + } data = json.dumps(data) if not isinstance(data, bytes): data = data.encode('utf-8') @@ -33,4 +44,4 @@ class WebStoreDialog(object): if isinstance(data, bytes): data = data.decode('ascii') args = ['store-dialog', data] - self.gui.job_manager.launch_gui_app(args[0], kwargs={'args':args}) + self.gui.job_manager.launch_gui_app(args[0], kwargs={'args': args}) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index deee2c7da2..0fa0d73219 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -666,8 +666,25 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ except Exception: import traceback traceback.print_exc() + elif msg.startswith('web-store:'): + import json + try: + data = json.loads(msg[len('web-store:'):]) + except ValueError: + prints('Failed to decode message from other instance: %r' % msg) + path = data['path'] + if data['tags']: + before = self.current_db.new_api.all_book_ids() + self.iactions['Add Books'].add_filesystem_book([path], allow_device=False) + if data['tags']: + db = self.current_db.new_api + after = self.current_db.new_api.all_book_ids() + for book_id in after - before: + tags = list(db.field_for('tags', book_id)) + tags += list(data['tags']) + self.current_db.new_api.set_field('tags', {book_id: tags}) else: - print(msg) + prints(u'Ignoring unknown message from other instance: %r' % msg[:20]) def current_view(self): '''Convenience method that returns the currently visible view '''