DeDRM_tools/Obok_plugin/dialogs.py
2022-07-13 15:34:47 +02:00

456 lines
19 KiB
Python

# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__docformat__ = 'restructuredtext en'
TEXT_DRM_FREE = ' (*: drm - free)'
LAB_DRM_FREE = '* : drm - free'
try:
from PyQt5.Qt import (Qt, QVBoxLayout, QLabel, QApplication, QGroupBox,
QDialogButtonBox, QHBoxLayout, QTextBrowser, QProgressDialog,
QTimer, QSize, QDialog, QIcon, QTableWidget, QTableWidgetItem)
except ImportError:
from PyQt4.Qt import (Qt, QVBoxLayout, QLabel, QApplication, QGroupBox,
QDialogButtonBox, QHBoxLayout, QTextBrowser, QProgressDialog,
QTimer, QSize, QDialog, QIcon, QTableWidget, QTableWidgetItem)
try:
from PyQt5.QtWidgets import (QListWidget, QAbstractItemView)
except ImportError:
from PyQt4.QtGui import (QListWidget, QAbstractItemView)
from calibre.gui2 import gprefs, warning_dialog, error_dialog
from calibre.gui2.dialogs.message_box import MessageBox
#from calibre.ptempfile import remove_dir
from calibre_plugins.obok_dedrm.utilities import (SizePersistedDialog, ImageTitleLayout,
showErrorDlg, get_icon, convert_qvariant, debug_print
)
from calibre_plugins.obok_dedrm.__init__ import (PLUGIN_NAME,
PLUGIN_SAFE_NAME, PLUGIN_VERSION, PLUGIN_DESCRIPTION)
try:
debug_print("obok::dialogs.py - loading translations")
load_translations()
except NameError:
debug_print("obok::dialogs.py - exception when loading translations")
pass # load_translations() added in calibre 1.9
class SelectionDialog(SizePersistedDialog):
'''
Dialog to select the kobo books to decrypt
'''
def __init__(self, gui, interface_action, books):
'''
:param gui: Parent gui
:param interface_action: InterfaceActionObject (InterfacePluginAction class from action.py)
:param books: list of Kobo book
'''
self.books = books
self.gui = gui
self.interface_action = interface_action
self.books = books
SizePersistedDialog.__init__(self, gui, PLUGIN_NAME + 'plugin:selections dialog')
self.setWindowTitle(_(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
self.setMinimumWidth(300)
self.setMinimumHeight(300)
layout = QVBoxLayout(self)
self.setLayout(layout)
title_layout = ImageTitleLayout(self, 'images/obok.png', _('Obok DeDRM'))
layout.addLayout(title_layout)
help_label = QLabel(_('<a href="http://www.foo.com/">Help</a>'), self)
help_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
help_label.setAlignment(Qt.AlignRight)
help_label.linkActivated.connect(self._help_link_activated)
title_layout.addWidget(help_label)
title_layout.setAlignment(Qt.AlignTop)
layout.addSpacing(5)
main_layout = QHBoxLayout()
layout.addLayout(main_layout)
# self.listy = QListWidget()
# self.listy.setSelectionMode(QAbstractItemView.ExtendedSelection)
# main_layout.addWidget(self.listy)
# self.listy.addItems(books)
self.books_table = BookListTableWidget(self)
main_layout.addWidget(self.books_table)
layout.addSpacing(10)
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
button_box.accepted.connect(self._ok_clicked)
button_box.rejected.connect(self.reject)
self.select_all_button = button_box.addButton(_("Select All"), QDialogButtonBox.ResetRole)
self.select_all_button.setToolTip(_("Select all books to add them to the calibre library."))
self.select_all_button.clicked.connect(self._select_all_clicked)
self.select_drm_button = button_box.addButton(_("All with DRM"), QDialogButtonBox.ResetRole)
self.select_drm_button.setToolTip(_("Select all books with DRM."))
self.select_drm_button.clicked.connect(self._select_drm_clicked)
self.select_free_button = button_box.addButton(_("All DRM free"), QDialogButtonBox.ResetRole)
self.select_free_button.setToolTip(_("Select all books without DRM."))
self.select_free_button.clicked.connect(self._select_free_clicked)
layout.addWidget(button_box)
# Cause our dialog size to be restored from prefs or created on first usage
self.resize_dialog()
self.books_table.populate_table(self.books)
def _select_all_clicked(self):
self.books_table.select_all()
def _select_drm_clicked(self):
self.books_table.select_drm(True)
def _select_free_clicked(self):
self.books_table.select_drm(False)
def _help_link_activated(self, url):
'''
:param url: Dummy url to pass to the show_help method of the InterfacePluginAction class
'''
self.interface_action.show_help()
def _ok_clicked(self):
'''
Build an index of the selected titles
'''
if len(self.books_table.selectedItems()):
self.accept()
else:
msg = 'You must make a selection!'
showErrorDlg(msg, self)
def getBooks(self):
'''
Method to return the selected books
'''
return self.books_table.get_books()
class BookListTableWidget(QTableWidget):
def __init__(self, parent):
QTableWidget.__init__(self, parent)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
def populate_table(self, books):
self.clear()
self.setAlternatingRowColors(True)
self.setRowCount(len(books))
header_labels = ['DRM', _('Title'), _('Author'), _('Series'), 'book_id']
self.setColumnCount(len(header_labels))
self.setHorizontalHeaderLabels(header_labels)
self.verticalHeader().setDefaultSectionSize(24)
self.horizontalHeader().setStretchLastSection(True)
self.books = {}
for row, book in enumerate(books):
self.populate_table_row(row, book)
self.books[row] = book
self.setSortingEnabled(False)
self.resizeColumnsToContents()
self.setMinimumColumnWidth(1, 100)
self.setMinimumColumnWidth(2, 100)
self.setMinimumSize(300, 0)
if len(books) > 0:
self.selectRow(0)
self.hideColumn(4)
self.setSortingEnabled(True)
def setMinimumColumnWidth(self, col, minimum):
if self.columnWidth(col) < minimum:
self.setColumnWidth(col, minimum)
def populate_table_row(self, row, book):
if book.has_drm:
icon = get_icon('drm-locked.png')
val = 1
else:
icon = get_icon('drm-unlocked.png')
val = 0
status_cell = IconWidgetItem(None, icon, val)
status_cell.setData(Qt.UserRole, val)
self.setItem(row, 0, status_cell)
self.setItem(row, 1, ReadOnlyTableWidgetItem(book.title))
self.setItem(row, 2, AuthorTableWidgetItem(book.author, book.author))
self.setItem(row, 3, SeriesTableWidgetItem(book.series, book.series_index))
self.setItem(row, 4, NumericTableWidgetItem(row))
def get_books(self):
# debug_print("BookListTableWidget:get_books - self.books:", self.books)
books = []
if len(self.selectedItems()):
for row in range(self.rowCount()):
# debug_print("BookListTableWidget:get_books - row:", row)
if self.item(row, 0).isSelected():
book_num = convert_qvariant(self.item(row, 4).data(Qt.DisplayRole))
debug_print("BookListTableWidget:get_books - book_num:", book_num)
book = self.books[book_num]
debug_print("BookListTableWidget:get_books - book:", book.title)
books.append(book)
return books
def select_all(self):
self .selectAll()
def select_drm(self, has_drm):
self.clearSelection()
current_selection_mode = self.selectionMode()
self.setSelectionMode(QAbstractItemView.MultiSelection)
for row in range(self.rowCount()):
# debug_print("BookListTableWidget:select_drm - row:", row)
if convert_qvariant(self.item(row, 0).data(Qt.UserRole)) == 1:
# debug_print("BookListTableWidget:select_drm - has DRM:", row)
if has_drm:
self.selectRow(row)
else:
# debug_print("BookListTableWidget:select_drm - DRM free:", row)
if not has_drm:
self.selectRow(row)
self.setSelectionMode(current_selection_mode)
class DecryptAddProgressDialog(QProgressDialog):
'''
Use the QTimer singleShot method to dole out books one at
a time to the indicated callback function from action.py
'''
def __init__(self, gui, indices, callback_fn, db, db_type='calibre', status_msg_type='books', action_type=('Decrypting','Decryption')):
'''
:param gui: Parent gui
:param indices: List of Kobo books or list calibre book maps (indicated by param db_type)
:param callback_fn: the function from action.py that will do the heavy lifting (get_decrypted_kobo_books or add_new_books)
:param db: kobo database object or calibre database cache (indicated by param db_type)
:param db_type: string indicating what kind of database param db is
:param status_msg_type: string to indicate what the ProgressDialog is operating on (cosmetic only)
:param action_type: 2-Tuple of strings indicating what the ProgressDialog is doing to param status_msg_type (cosmetic only)
'''
self.total_count = len(indices)
QProgressDialog.__init__(self, '', 'Cancel', 0, self.total_count, gui)
self.setMinimumWidth(500)
self.indices, self.callback_fn, self.db, self.db_type = indices, callback_fn, db, db_type
self.action_type, self.status_msg_type = action_type, status_msg_type
self.gui = gui
self.setWindowTitle('{0} {1} {2}...'.format(self.action_type[0], self.total_count, self.status_msg_type))
self.i, self.successes, self.failures = 0, [], []
QTimer.singleShot(0, self.do_book_action)
self.exec_()
def do_book_action(self):
if self.wasCanceled():
return self.do_close()
if self.i >= self.total_count:
return self.do_close()
book = self.indices[self.i]
self.i += 1
# Get the title and build the caption and label text from the string parameters provided
if self.db_type == 'calibre':
dtitle = book[0].title
elif self.db_type == 'kobo':
dtitle = book.title
self.setWindowTitle('{0} {1} {2} ({3} {4} failures)...'.format(self.action_type[0], self.total_count,
self.status_msg_type, len(self.failures), self.action_type[1]))
self.setLabelText('{0}: {1}'.format(self.action_type[0], dtitle))
# If a calibre db, feed the calibre bookmap to action.py's add_new_books method
if self.db_type == 'calibre':
if self.callback_fn([book]):
self.successes.append(book)
else:
self.failures.append(book)
# If a kobo db, feed the index to the kobo book to action.py's get_decrypted_kobo_books method
elif self.db_type == 'kobo':
if self.callback_fn(book):
debug_print("DecryptAddProgressDialog::do_book_action - decrypted book: '%s'" % dtitle)
self.successes.append(book)
else:
debug_print("DecryptAddProgressDialog::do_book_action - book decryption failed: '%s'" % dtitle)
self.failures.append(book)
self.setValue(self.i)
# Lather, rinse, repeat.
QTimer.singleShot(0, self.do_book_action)
def do_close(self):
self.hide()
self.gui = None
class AddEpubFormatsProgressDialog(QProgressDialog):
'''
Use the QTimer singleShot method to dole out epub formats one at
a time to the indicated callback function from action.py
'''
def __init__(self, gui, entries, callback_fn, status_msg_type='formats', action_type=('Adding','Added')):
'''
:param gui: Parent gui
:param entries: List of 3-tuples [(target calibre id, calibre metadata object, path to epub file)]
:param callback_fn: the function from action.py that will do the heavy lifting (process_epub_formats)
:param status_msg_type: string to indicate what the ProgressDialog is operating on (cosmetic only)
:param action_type: 2-tuple of strings indicating what the ProgressDialog is doing to param status_msg_type (cosmetic only)
'''
self.total_count = len(entries)
QProgressDialog.__init__(self, '', 'Cancel', 0, self.total_count, gui)
self.setMinimumWidth(500)
self.entries, self.callback_fn = entries, callback_fn
self.action_type, self.status_msg_type = action_type, status_msg_type
self.gui = gui
self.setWindowTitle('{0} {1} {2}...'.format(self.action_type[0], self.total_count, self.status_msg_type))
self.i, self.successes, self.failures = 0, [], []
QTimer.singleShot(0, self.do_book_action)
self.exec_()
def do_book_action(self):
if self.wasCanceled():
return self.do_close()
if self.i >= self.total_count:
return self.do_close()
epub_format = self.entries[self.i]
self.i += 1
# assign the elements of the 3-tuple details to legible variables
book_id, mi, path = epub_format[0], epub_format[1], epub_format[2]
# Get the title and build the caption and label text from the string parameters provided
dtitle = mi.title
self.setWindowTitle('{0} {1} {2} ({3} {4} failures)...'.format(self.action_type[0], self.total_count,
self.status_msg_type, len(self.failures), self.action_type[1]))
self.setLabelText('{0}: {1}'.format(self.action_type[0], dtitle))
# Send the necessary elements to the process_epub_formats callback function (action.py)
# and record the results
if self.callback_fn(book_id, mi, path):
self.successes.append((book_id, mi, path))
else:
self.failures.append((book_id, mi, path))
self.setValue(self.i)
# Lather, rinse, repeat
QTimer.singleShot(0, self.do_book_action)
def do_close(self):
self.hide()
self.gui = None
class ViewLog(QDialog):
'''
Show a detailed summary of results as html.
'''
def __init__(self, title, html, parent=None):
'''
:param title: Caption for window title
:param html: HTML string log/report
'''
QDialog.__init__(self, parent)
self.l = l = QVBoxLayout()
self.setLayout(l)
self.tb = QTextBrowser(self)
QApplication.setOverrideCursor(Qt.WaitCursor)
# Rather than formatting the text in <pre> blocks like the calibre
# ViewLog does, instead just format it inside divs to keep style formatting
html = html.replace('\t','&nbsp;&nbsp;&nbsp;&nbsp;')#.replace('\n', '<br/>')
html = html.replace('> ','>&nbsp;')
self.tb.setHtml('<div>{0}</div>'.format(html))
QApplication.restoreOverrideCursor()
l.addWidget(self.tb)
self.bb = QDialogButtonBox(QDialogButtonBox.Ok)
self.bb.accepted.connect(self.accept)
self.bb.rejected.connect(self.reject)
self.copy_button = self.bb.addButton(_('Copy to clipboard'),
self.bb.ActionRole)
self.copy_button.setIcon(QIcon(I('edit-copy.png')))
self.copy_button.clicked.connect(self.copy_to_clipboard)
l.addWidget(self.bb)
self.setModal(False)
self.resize(QSize(700, 500))
self.setWindowTitle(title)
self.setWindowIcon(QIcon(I('dialog_information.png')))
self.show()
def copy_to_clipboard(self):
txt = self.tb.toPlainText()
QApplication.clipboard().setText(txt)
class ResultsSummaryDialog(MessageBox):
def __init__(self, parent, title, msg, log='', det_msg=''):
'''
:param log: An HTML log
:param title: The title for this popup
:param msg: The msg to display
:param det_msg: Detailed message
'''
MessageBox.__init__(self, MessageBox.INFO, title, msg,
det_msg=det_msg, show_copy_button=False,
parent=parent)
self.log = log
self.vlb = self.bb.addButton(_('View Report'), self.bb.ActionRole)
self.vlb.setIcon(QIcon(I('dialog_information.png')))
self.vlb.clicked.connect(self.show_log)
self.det_msg_toggle.setVisible(bool(det_msg))
self.vlb.setVisible(bool(log))
def show_log(self):
self.log_viewer = ViewLog(PLUGIN_NAME + ' v' + PLUGIN_VERSION, self.log,
parent=self)
class ReadOnlyTableWidgetItem(QTableWidgetItem):
def __init__(self, text):
if text is None:
text = ''
QTableWidgetItem.__init__(self, text, QTableWidgetItem.ItemType.UserType)
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
class AuthorTableWidgetItem(ReadOnlyTableWidgetItem):
def __init__(self, text, sort_key):
ReadOnlyTableWidgetItem.__init__(self, text)
self.sort_key = sort_key
#Qt uses a simple < check for sorting items, override this to use the sortKey
def __lt__(self, other):
return self.sort_key < other.sort_key
class SeriesTableWidgetItem(ReadOnlyTableWidgetItem):
def __init__(self, series, series_index=None):
display = ''
if series:
if series_index:
from calibre.ebooks.metadata import fmt_sidx
display = '%s [%s]' % (series, fmt_sidx(series_index))
self.sortKey = '%s%04d' % (series, series_index)
else:
display = series
self.sortKey = series
ReadOnlyTableWidgetItem.__init__(self, display)
class IconWidgetItem(ReadOnlyTableWidgetItem):
def __init__(self, text, icon, sort_key):
ReadOnlyTableWidgetItem.__init__(self, text)
if icon:
self.setIcon(icon)
self.sort_key = sort_key
#Qt uses a simple < check for sorting items, override this to use the sortKey
def __lt__(self, other):
return self.sort_key < other.sort_key
class NumericTableWidgetItem(QTableWidgetItem):
def __init__(self, number, is_read_only=False):
QTableWidgetItem.__init__(self, '', QTableWidgetItem.ItemType.UserType)
self.setData(Qt.DisplayRole, number)
if is_read_only:
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)