mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Finish porting of web store dialog to QtWebEngine
This commit is contained in:
parent
c8e78749eb
commit
85fcea41fb
@ -6,22 +6,76 @@ from __future__ import absolute_import, division, print_function, unicode_litera
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
from base64 import standard_b64decode, standard_b64encode
|
from base64 import standard_b64decode, standard_b64encode
|
||||||
|
|
||||||
from PyQt5.Qt import (
|
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 import random_user_agent, url_slash_cleaner
|
||||||
from calibre.constants import STORE_DIALOG_APP_UID, islinux, iswindows
|
from calibre.constants import STORE_DIALOG_APP_UID, cache_dir, islinux, iswindows
|
||||||
from calibre.gui2 import Application, set_app_uid
|
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.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):
|
class DownloadItem(QWidget):
|
||||||
pass
|
|
||||||
|
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):
|
class Central(QWidget):
|
||||||
@ -31,13 +85,16 @@ class Central(QWidget):
|
|||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QWidget.__init__(self, parent)
|
QWidget.__init__(self, parent)
|
||||||
self.l = l = QVBoxLayout(self)
|
self.l = l = QVBoxLayout(self)
|
||||||
self.view = v = View(self)
|
self.view = v = QWebEngineView(self)
|
||||||
v.loadStarted.connect(self.load_started)
|
v.loadStarted.connect(self.load_started)
|
||||||
v.loadProgress.connect(self.load_progress)
|
v.loadProgress.connect(self.load_progress)
|
||||||
v.loadFinished.connect(self.load_finished)
|
v.loadFinished.connect(self.load_finished)
|
||||||
l.addWidget(v)
|
l.addWidget(v)
|
||||||
self.h = h = QHBoxLayout()
|
self.h = h = QHBoxLayout()
|
||||||
l.addLayout(h)
|
l.addLayout(h)
|
||||||
|
self.download_progress = d = DownloadProgress(self)
|
||||||
|
h.addWidget(d)
|
||||||
|
|
||||||
self.home_button = b = QPushButton(_('Home'))
|
self.home_button = b = QPushButton(_('Home'))
|
||||||
b.clicked.connect(self.home)
|
b.clicked.connect(self.home)
|
||||||
h.addWidget(b)
|
h.addWidget(b)
|
||||||
@ -51,6 +108,10 @@ class Central(QWidget):
|
|||||||
self.progress_bar = b = QProgressBar(self)
|
self.progress_bar = b = QProgressBar(self)
|
||||||
h.addWidget(b)
|
h.addWidget(b)
|
||||||
|
|
||||||
|
self.reload_button = b = QPushButton(_('Reload'))
|
||||||
|
b.clicked.connect(v.reload)
|
||||||
|
h.addWidget(b)
|
||||||
|
|
||||||
def load_started(self):
|
def load_started(self):
|
||||||
self.progress_bar.setValue(0)
|
self.progress_bar.setValue(0)
|
||||||
|
|
||||||
@ -65,10 +126,29 @@ class Main(MainWindow):
|
|||||||
|
|
||||||
def __init__(self, data):
|
def __init__(self, data):
|
||||||
MainWindow.__init__(self, None)
|
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.data = data
|
||||||
self.central = c = Central(self)
|
self.central = c = Central(self)
|
||||||
c.home.connect(self.go_home)
|
c.home.connect(self.go_home)
|
||||||
self.setCentralWidget(c)
|
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
|
@property
|
||||||
def view(self):
|
def view(self):
|
||||||
@ -80,7 +160,74 @@ class Main(MainWindow):
|
|||||||
def go_to(self, url=None):
|
def go_to(self, url=None):
|
||||||
url = url or self.data['base_url']
|
url = url or self.data['base_url']
|
||||||
url = url_slash_cleaner(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):
|
def main(args):
|
||||||
@ -95,18 +242,23 @@ def main(args):
|
|||||||
|
|
||||||
data = args[-1]
|
data = args[-1]
|
||||||
data = json.loads(standard_b64decode(data))
|
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)
|
app = Application(args, override_program_name=override)
|
||||||
|
m = Main(data)
|
||||||
|
m.show(), m.raise_()
|
||||||
app.exec_()
|
app.exec_()
|
||||||
|
del m
|
||||||
|
del app
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
sample_data = standard_b64encode(
|
sample_data = standard_b64encode(
|
||||||
json.dumps({
|
json.dumps({
|
||||||
u'window_title': u'MobileRead',
|
'window_title': 'MobileRead',
|
||||||
u'base_url': u'https://www.mobileread.com/',
|
'base_url': 'https://www.mobileread.com/',
|
||||||
u'detail_url': u'http://www.mobileread.com/forums/showthread.php?t=54477',
|
'detail_url': 'http://www.mobileread.com/forums/showthread.php?t=54477',
|
||||||
u'tags': u''
|
'id':1,
|
||||||
|
'tags': '',
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
main([sample_data])
|
main(['store-dialog', sample_data])
|
||||||
|
@ -6,15 +6,20 @@ from __future__ import absolute_import, division, print_function, unicode_litera
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
from base64 import standard_b64encode
|
from base64 import standard_b64encode
|
||||||
|
from itertools import count
|
||||||
|
|
||||||
|
counter = count()
|
||||||
|
|
||||||
|
|
||||||
class WebStoreDialog(object):
|
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.gui = gui
|
||||||
self.base_url = base_url
|
self.base_url = base_url
|
||||||
self.detail_url = detail_url
|
self.detail_url = detail_url
|
||||||
self.create_browser = create_browser
|
|
||||||
self.window_title = None
|
self.window_title = None
|
||||||
self.tags = None
|
self.tags = None
|
||||||
|
|
||||||
@ -25,7 +30,13 @@ class WebStoreDialog(object):
|
|||||||
self.tags = tags
|
self.tags = tags
|
||||||
|
|
||||||
def exec_(self):
|
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)
|
data = json.dumps(data)
|
||||||
if not isinstance(data, bytes):
|
if not isinstance(data, bytes):
|
||||||
data = data.encode('utf-8')
|
data = data.encode('utf-8')
|
||||||
@ -33,4 +44,4 @@ class WebStoreDialog(object):
|
|||||||
if isinstance(data, bytes):
|
if isinstance(data, bytes):
|
||||||
data = data.decode('ascii')
|
data = data.decode('ascii')
|
||||||
args = ['store-dialog', data]
|
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})
|
||||||
|
@ -666,8 +666,25 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
except Exception:
|
except Exception:
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
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:
|
else:
|
||||||
print(msg)
|
prints(u'Ignoring unknown message from other instance: %r' % msg[:20])
|
||||||
|
|
||||||
def current_view(self):
|
def current_view(self):
|
||||||
'''Convenience method that returns the currently visible view '''
|
'''Convenience method that returns the currently visible view '''
|
||||||
|
Loading…
x
Reference in New Issue
Block a user