Finish porting of web store dialog to QtWebEngine

This commit is contained in:
Kovid Goyal 2019-03-29 19:56:08 +05:30
parent c8e78749eb
commit 85fcea41fb
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 201 additions and 21 deletions

View File

@ -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('<p>' + _(
'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 '
'<a href="https://www.adobe.com/solutions/ebook/digital-editions.html">'
'Adobe Digital Editions</a> (ADE).<p>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])

View File

@ -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})

View File

@ -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 '''