mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-07 10:14:46 -04:00
Integrate the new Tweak Book tool into the main calibre gui
The old Tweak Book tool has become "Unpack Book"
This commit is contained in:
parent
3447684c5e
commit
df1c8b7e56
BIN
resources/images/unpack-book.png
Normal file
BIN
resources/images/unpack-book.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.6 KiB |
@ -907,6 +907,11 @@ class ActionTweakEpub(InterfaceActionBase):
|
|||||||
actual_plugin = 'calibre.gui2.actions.tweak_epub:TweakEpubAction'
|
actual_plugin = 'calibre.gui2.actions.tweak_epub:TweakEpubAction'
|
||||||
description = _('Make small tweaks to epub or htmlz files in your calibre library')
|
description = _('Make small tweaks to epub or htmlz files in your calibre library')
|
||||||
|
|
||||||
|
class ActionUnpackBook(InterfaceActionBase):
|
||||||
|
name = 'Unpack Book'
|
||||||
|
actual_plugin = 'calibre.gui2.actions.unpack_book:UnpackBookAction'
|
||||||
|
description = _('Make small changes to epub or htmlz files in your calibre library')
|
||||||
|
|
||||||
class ActionNextMatch(InterfaceActionBase):
|
class ActionNextMatch(InterfaceActionBase):
|
||||||
name = 'Next Match'
|
name = 'Next Match'
|
||||||
actual_plugin = 'calibre.gui2.actions.next_match:NextMatchAction'
|
actual_plugin = 'calibre.gui2.actions.next_match:NextMatchAction'
|
||||||
@ -957,7 +962,7 @@ plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
|
|||||||
ActionShowBookDetails,ActionRestart, ActionOpenFolder, ActionConnectShare,
|
ActionShowBookDetails,ActionRestart, ActionOpenFolder, ActionConnectShare,
|
||||||
ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks,
|
ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks,
|
||||||
ActionAddToLibrary, ActionEditCollections, ActionMatchBooks, ActionChooseLibrary,
|
ActionAddToLibrary, ActionEditCollections, ActionMatchBooks, ActionChooseLibrary,
|
||||||
ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch, ActionStore,
|
ActionCopyToLibrary, ActionTweakEpub, ActionUnpackBook, ActionNextMatch, ActionStore,
|
||||||
ActionPluginUpdater, ActionPickRandom, ActionEditToC, ActionSortBy,
|
ActionPluginUpdater, ActionPickRandom, ActionEditToC, ActionSortBy,
|
||||||
ActionMarkBooks]
|
ActionMarkBooks]
|
||||||
|
|
||||||
|
@ -671,8 +671,8 @@ class Cache(object):
|
|||||||
'''
|
'''
|
||||||
Return absolute path to the ebook file of format `format`
|
Return absolute path to the ebook file of format `format`
|
||||||
|
|
||||||
Currently used only in calibredb list, the viewer and the catalogs (via
|
Currently used only in calibredb list, the viewer, tweak book and the
|
||||||
get_data_as_dict()).
|
catalogs (via get_data_as_dict()).
|
||||||
|
|
||||||
Apart from the viewer, I don't believe any of the others do any file
|
Apart from the viewer, I don't believe any of the others do any file
|
||||||
I/O with the results of this call.
|
I/O with the results of this call.
|
||||||
|
@ -5,290 +5,56 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import os, weakref, shutil
|
import time
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
from PyQt4.Qt import (QDialog, QVBoxLayout, QHBoxLayout, QRadioButton, QFrame,
|
from PyQt4.Qt import QTimer, QDialog, QDialogButtonBox, QCheckBox, QVBoxLayout, QLabel, Qt
|
||||||
QPushButton, QLabel, QGroupBox, QGridLayout, QIcon, QSize, QTimer)
|
|
||||||
|
|
||||||
from calibre import as_unicode
|
from calibre.gui2 import error_dialog
|
||||||
from calibre.constants import isosx
|
|
||||||
from calibre.gui2 import error_dialog, question_dialog, open_local_file, gprefs
|
|
||||||
from calibre.gui2.actions import InterfaceAction
|
from calibre.gui2.actions import InterfaceAction
|
||||||
from calibre.ptempfile import (PersistentTemporaryDirectory,
|
|
||||||
PersistentTemporaryFile)
|
|
||||||
from calibre.utils.config import prefs, tweaks
|
|
||||||
|
|
||||||
class TweakBook(QDialog):
|
class Choose(QDialog):
|
||||||
|
|
||||||
def __init__(self, parent, book_id, fmts, db):
|
def __init__(self, fmts, parent=None):
|
||||||
QDialog.__init__(self, parent)
|
QDialog.__init__(self, parent)
|
||||||
self.setWindowIcon(QIcon(I('tweak.png')))
|
self.l = l = QVBoxLayout(self)
|
||||||
self.book_id, self.fmts, self.db_ref = book_id, fmts, weakref.ref(db)
|
self.setLayout(l)
|
||||||
self._exploded = None
|
self.setWindowTitle(_('Choose format to tweak'))
|
||||||
self._cleanup_dirs = []
|
|
||||||
self._cleanup_files = []
|
|
||||||
|
|
||||||
self.setup_ui()
|
self.la = la = QLabel(_(
|
||||||
self.setWindowTitle(_('Tweak Book') + ' - ' + db.title(book_id,
|
'This book mas multiple formats that can be tweaked. Choose the format you want to tweak.'))
|
||||||
index_is_id=True))
|
l.addWidget(la)
|
||||||
|
|
||||||
button = self.fmt_choice_buttons[0]
|
self.rem = QCheckBox(_('Always ask when more than one format is available'))
|
||||||
button_map = {unicode(x.text()):x for x in self.fmt_choice_buttons}
|
self.rem.setChecked(True)
|
||||||
of = prefs['output_format'].upper()
|
l.addWidget(self.rem)
|
||||||
df = tweaks.get('default_tweak_format', None)
|
|
||||||
lf = gprefs.get('last_tweak_format', None)
|
|
||||||
if df and df.lower() == 'remember' and lf in button_map:
|
|
||||||
button = button_map[lf]
|
|
||||||
elif df and df.upper() in button_map:
|
|
||||||
button = button_map[df.upper()]
|
|
||||||
elif of in button_map:
|
|
||||||
button = button_map[of]
|
|
||||||
button.setChecked(True)
|
|
||||||
|
|
||||||
self.init_state()
|
self.bb = bb = QDialogButtonBox(self)
|
||||||
for button in self.fmt_choice_buttons:
|
l.addWidget(bb)
|
||||||
button.toggled.connect(self.init_state)
|
bb.accepted.connect(self.accept)
|
||||||
|
bb.rejected.connect(self.reject)
|
||||||
|
self.buts = buts = []
|
||||||
|
for fmt in fmts:
|
||||||
|
b = bb.addButton(fmt.upper(), bb.AcceptRole)
|
||||||
|
b.clicked.connect(partial(self.chosen, fmt))
|
||||||
|
buts.append(b)
|
||||||
|
|
||||||
def init_state(self, *args):
|
self.fmt = None
|
||||||
self._exploded = None
|
self.resize(self.sizeHint())
|
||||||
self.preview_button.setEnabled(False)
|
|
||||||
self.rebuild_button.setEnabled(False)
|
|
||||||
self.explode_button.setEnabled(True)
|
|
||||||
|
|
||||||
def setup_ui(self): # {{{
|
def chosen(self, fmt):
|
||||||
self._g = g = QHBoxLayout(self)
|
self.fmt = fmt
|
||||||
self.setLayout(g)
|
|
||||||
self._l = l = QVBoxLayout()
|
|
||||||
g.addLayout(l)
|
|
||||||
|
|
||||||
fmts = sorted(x.upper() for x in self.fmts)
|
def accept(self):
|
||||||
self.fmt_choice_box = QGroupBox(_('Choose the format to tweak:'), self)
|
from calibre.gui2.tweak_book import tprefs
|
||||||
self._fl = fl = QHBoxLayout()
|
tprefs['choose_tweak_fmt'] = self.rem.isChecked()
|
||||||
self.fmt_choice_box.setLayout(self._fl)
|
QDialog.accept(self)
|
||||||
self.fmt_choice_buttons = [QRadioButton(y, self) for y in fmts]
|
|
||||||
for x in self.fmt_choice_buttons:
|
|
||||||
fl.addWidget(x, stretch=10 if x is self.fmt_choice_buttons[-1] else
|
|
||||||
0)
|
|
||||||
l.addWidget(self.fmt_choice_box)
|
|
||||||
self.fmt_choice_box.setVisible(len(fmts) > 1)
|
|
||||||
|
|
||||||
self.help_label = QLabel(_('''\
|
|
||||||
<h2>About Tweak Book</h2>
|
|
||||||
<p>Tweak Book allows you to fine tune the appearance of an ebook by
|
|
||||||
making small changes to its internals. In order to use Tweak Book,
|
|
||||||
you need to know a little bit about HTML and CSS, technologies that
|
|
||||||
are used in ebooks. Follow the steps:</p>
|
|
||||||
<br>
|
|
||||||
<ol>
|
|
||||||
<li>Click "Explode Book": This will "explode" the book into its
|
|
||||||
individual internal components.<br></li>
|
|
||||||
<li>Right click on any individual file and select "Open with..." to
|
|
||||||
edit it in your favorite text editor.<br></li>
|
|
||||||
<li>When you are done Tweaking: <b>close the file browser window
|
|
||||||
and the editor windows you used to make your tweaks</b>. Then click
|
|
||||||
the "Rebuild Book" button, to update the book in your calibre
|
|
||||||
library.</li>
|
|
||||||
</ol>'''))
|
|
||||||
self.help_label.setWordWrap(True)
|
|
||||||
self._fr = QFrame()
|
|
||||||
self._fr.setFrameShape(QFrame.VLine)
|
|
||||||
g.addWidget(self._fr)
|
|
||||||
g.addWidget(self.help_label)
|
|
||||||
|
|
||||||
self._b = b = QGridLayout()
|
|
||||||
left, top, right, bottom = b.getContentsMargins()
|
|
||||||
top += top
|
|
||||||
b.setContentsMargins(left, top, right, bottom)
|
|
||||||
l.addLayout(b, stretch=10)
|
|
||||||
|
|
||||||
self.explode_button = QPushButton(QIcon(I('wizard.png')), _('&Explode Book'))
|
|
||||||
self.preview_button = QPushButton(QIcon(I('view.png')), _('&Preview Book'))
|
|
||||||
self.cancel_button = QPushButton(QIcon(I('window-close.png')), _('&Cancel'))
|
|
||||||
self.rebuild_button = QPushButton(QIcon(I('exec.png')), _('&Rebuild Book'))
|
|
||||||
|
|
||||||
self.explode_button.setToolTip(
|
|
||||||
_('Explode the book to edit its components'))
|
|
||||||
self.preview_button.setToolTip(
|
|
||||||
_('Preview the result of your tweaks'))
|
|
||||||
self.cancel_button.setToolTip(
|
|
||||||
_('Abort without saving any changes'))
|
|
||||||
self.rebuild_button.setToolTip(
|
|
||||||
_('Save your changes and update the book in the calibre library'))
|
|
||||||
|
|
||||||
a = b.addWidget
|
|
||||||
a(self.explode_button, 0, 0, 1, 1)
|
|
||||||
a(self.preview_button, 0, 1, 1, 1)
|
|
||||||
a(self.cancel_button, 1, 0, 1, 1)
|
|
||||||
a(self.rebuild_button, 1, 1, 1, 1)
|
|
||||||
|
|
||||||
for x in ('explode', 'preview', 'cancel', 'rebuild'):
|
|
||||||
getattr(self, x+'_button').clicked.connect(getattr(self, x))
|
|
||||||
|
|
||||||
self.msg = QLabel('dummy', self)
|
|
||||||
self.msg.setVisible(False)
|
|
||||||
self.msg.setStyleSheet('''
|
|
||||||
QLabel {
|
|
||||||
text-align: center;
|
|
||||||
background-color: white;
|
|
||||||
color: black;
|
|
||||||
border-width: 1px;
|
|
||||||
border-style: solid;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: x-large;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
''')
|
|
||||||
|
|
||||||
self.resize(self.sizeHint() + QSize(40, 10))
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
def show_msg(self, msg):
|
|
||||||
self.msg.setText(msg)
|
|
||||||
self.msg.resize(self.size() - QSize(50, 25))
|
|
||||||
self.msg.move((self.width() - self.msg.width())//2,
|
|
||||||
(self.height() - self.msg.height())//2)
|
|
||||||
self.msg.setVisible(True)
|
|
||||||
|
|
||||||
def hide_msg(self):
|
|
||||||
self.msg.setVisible(False)
|
|
||||||
|
|
||||||
def explode(self):
|
|
||||||
self.show_msg(_('Exploding, please wait...'))
|
|
||||||
if len(self.fmt_choice_buttons) > 1:
|
|
||||||
gprefs.set('last_tweak_format', self.current_format.upper())
|
|
||||||
QTimer.singleShot(5, self.do_explode)
|
|
||||||
|
|
||||||
def ask_question(self, msg):
|
|
||||||
return question_dialog(self, _('Are you sure?'), msg)
|
|
||||||
|
|
||||||
def do_explode(self):
|
|
||||||
from calibre.ebooks.tweak import get_tools, Error, WorkerError
|
|
||||||
tdir = PersistentTemporaryDirectory('_tweak_explode')
|
|
||||||
self._cleanup_dirs.append(tdir)
|
|
||||||
det_msg = None
|
|
||||||
try:
|
|
||||||
src = self.db.format(self.book_id, self.current_format,
|
|
||||||
index_is_id=True, as_path=True)
|
|
||||||
self._cleanup_files.append(src)
|
|
||||||
exploder = get_tools(self.current_format)[0]
|
|
||||||
opf = exploder(src, tdir, question=self.ask_question)
|
|
||||||
except WorkerError as e:
|
|
||||||
det_msg = e.orig_tb
|
|
||||||
except Error as e:
|
|
||||||
return error_dialog(self, _('Failed to unpack'),
|
|
||||||
(_('Could not explode the %s file.')%self.current_format) + ' '
|
|
||||||
+ as_unicode(e), show=True)
|
|
||||||
except:
|
|
||||||
import traceback
|
|
||||||
det_msg = traceback.format_exc()
|
|
||||||
finally:
|
|
||||||
self.hide_msg()
|
|
||||||
|
|
||||||
if det_msg is not None:
|
|
||||||
return error_dialog(self, _('Failed to unpack'),
|
|
||||||
_('Could not explode the %s file. Click "Show Details" for '
|
|
||||||
'more information.')%self.current_format, det_msg=det_msg,
|
|
||||||
show=True)
|
|
||||||
|
|
||||||
if opf is None:
|
|
||||||
# The question was answered with No
|
|
||||||
return
|
|
||||||
|
|
||||||
self._exploded = tdir
|
|
||||||
self.explode_button.setEnabled(False)
|
|
||||||
self.preview_button.setEnabled(True)
|
|
||||||
self.rebuild_button.setEnabled(True)
|
|
||||||
open_local_file(tdir)
|
|
||||||
|
|
||||||
def rebuild_it(self):
|
|
||||||
from calibre.ebooks.tweak import get_tools, WorkerError
|
|
||||||
src_dir = self._exploded
|
|
||||||
det_msg = None
|
|
||||||
of = PersistentTemporaryFile('_tweak_rebuild.'+self.current_format.lower())
|
|
||||||
of.close()
|
|
||||||
of = of.name
|
|
||||||
self._cleanup_files.append(of)
|
|
||||||
try:
|
|
||||||
rebuilder = get_tools(self.current_format)[1]
|
|
||||||
rebuilder(src_dir, of)
|
|
||||||
except WorkerError as e:
|
|
||||||
det_msg = e.orig_tb
|
|
||||||
except:
|
|
||||||
import traceback
|
|
||||||
det_msg = traceback.format_exc()
|
|
||||||
finally:
|
|
||||||
self.hide_msg()
|
|
||||||
|
|
||||||
if det_msg is not None:
|
|
||||||
error_dialog(self, _('Failed to rebuild file'),
|
|
||||||
_('Failed to rebuild %s. For more information, click '
|
|
||||||
'"Show details".')%self.current_format,
|
|
||||||
det_msg=det_msg, show=True)
|
|
||||||
return None
|
|
||||||
|
|
||||||
return of
|
|
||||||
|
|
||||||
def preview(self):
|
|
||||||
self.show_msg(_('Rebuilding, please wait...'))
|
|
||||||
QTimer.singleShot(5, self.do_preview)
|
|
||||||
|
|
||||||
def do_preview(self):
|
|
||||||
rebuilt = self.rebuild_it()
|
|
||||||
if rebuilt is not None:
|
|
||||||
self.parent().iactions['View']._view_file(rebuilt)
|
|
||||||
|
|
||||||
def rebuild(self):
|
|
||||||
self.show_msg(_('Rebuilding, please wait...'))
|
|
||||||
QTimer.singleShot(5, self.do_rebuild)
|
|
||||||
|
|
||||||
def do_rebuild(self):
|
|
||||||
rebuilt = self.rebuild_it()
|
|
||||||
if rebuilt is not None:
|
|
||||||
fmt = os.path.splitext(rebuilt)[1][1:].upper()
|
|
||||||
with open(rebuilt, 'rb') as f:
|
|
||||||
self.db.add_format(self.book_id, fmt, f, index_is_id=True)
|
|
||||||
self.accept()
|
|
||||||
|
|
||||||
def cancel(self):
|
|
||||||
self.reject()
|
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
if isosx and self._exploded:
|
|
||||||
try:
|
|
||||||
import appscript
|
|
||||||
self.finder = appscript.app('Finder')
|
|
||||||
self.finder.Finder_windows[os.path.basename(self._exploded)].close()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
for f in self._cleanup_files:
|
|
||||||
try:
|
|
||||||
os.remove(f)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
for d in self._cleanup_dirs:
|
|
||||||
try:
|
|
||||||
shutil.rmtree(d)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
|
||||||
def db(self):
|
|
||||||
return self.db_ref()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_format(self):
|
|
||||||
for b in self.fmt_choice_buttons:
|
|
||||||
if b.isChecked():
|
|
||||||
return unicode(b.text())
|
|
||||||
|
|
||||||
class TweakEpubAction(InterfaceAction):
|
class TweakEpubAction(InterfaceAction):
|
||||||
|
|
||||||
name = 'Tweak ePub'
|
name = 'Tweak ePub'
|
||||||
action_spec = (_('Tweak Book'), 'tweak.png',
|
action_spec = (_('Tweak Book'), 'tweak.png', _('Edit eBooks'), _('T'))
|
||||||
_('Make small changes to ePub, HTMLZ or AZW3 format books'),
|
|
||||||
_('T'))
|
|
||||||
dont_add_to = frozenset(['context-menu-device'])
|
dont_add_to = frozenset(['context-menu-device'])
|
||||||
action_type = 'current'
|
action_type = 'current'
|
||||||
|
|
||||||
@ -324,25 +90,46 @@ class TweakEpubAction(InterfaceAction):
|
|||||||
def tweak_book(self):
|
def tweak_book(self):
|
||||||
row = self.gui.library_view.currentIndex()
|
row = self.gui.library_view.currentIndex()
|
||||||
if not row.isValid():
|
if not row.isValid():
|
||||||
return error_dialog(self.gui, _('Cannot tweak Book'),
|
return error_dialog(self.gui, _('Cannot Tweak Book'),
|
||||||
_('No book selected'), show=True)
|
_('No book selected'), show=True)
|
||||||
|
|
||||||
book_id = self.gui.library_view.model().id(row)
|
book_id = self.gui.library_view.model().id(row)
|
||||||
self.do_tweak(book_id)
|
self.do_tweak(book_id)
|
||||||
|
|
||||||
def do_tweak(self, book_id):
|
def do_tweak(self, book_id):
|
||||||
|
from calibre.ebooks.oeb.polish.main import SUPPORTED
|
||||||
db = self.gui.library_view.model().db
|
db = self.gui.library_view.model().db
|
||||||
fmts = db.formats(book_id, index_is_id=True) or ''
|
fmts = db.formats(book_id, index_is_id=True) or ''
|
||||||
fmts = [x.lower().strip() for x in fmts.split(',')]
|
fmts = [x.upper().strip() for x in fmts.split(',')]
|
||||||
tweakable_fmts = set(fmts).intersection({'epub', 'htmlz', 'azw3',
|
tweakable_fmts = set(fmts).intersection(SUPPORTED)
|
||||||
'mobi', 'azw'})
|
|
||||||
if not tweakable_fmts:
|
if not tweakable_fmts:
|
||||||
return error_dialog(self.gui, _('Cannot Tweak Book'),
|
return error_dialog(self.gui, _('Cannot Tweak Book'),
|
||||||
_('The book must be in ePub, HTMLZ or AZW3 formats to tweak.'
|
_('The book must be in the %s formats to tweak.'
|
||||||
'\n\nFirst convert the book to one of these formats.'),
|
'\n\nFirst convert the book to one of these formats.') % (_(' or '.join(SUPPORTED))),
|
||||||
show=True)
|
show=True)
|
||||||
dlg = TweakBook(self.gui, book_id, tweakable_fmts, db)
|
if len(tweakable_fmts) > 1:
|
||||||
dlg.exec_()
|
from calibre.gui2.tweak_book import tprefs
|
||||||
dlg.cleanup()
|
if tprefs['choose_tweak_fmt']:
|
||||||
|
d = Choose(sorted(tweakable_fmts, key=tprefs.defaults['tweak_fmt_order'].index), self.gui)
|
||||||
|
if d.exec_() != d.Accepted:
|
||||||
|
return
|
||||||
|
tweakable_fmts = {d.fmt}
|
||||||
|
else:
|
||||||
|
fmts = [f for f in tprefs['tweak_fmt_order'] if f in tweakable_fmts]
|
||||||
|
if not fmts:
|
||||||
|
fmts = [f for f in tprefs.defaults['tweak_fmt_order'] if f in tweakable_fmts]
|
||||||
|
tweakable_fmts = {fmts[0]}
|
||||||
|
|
||||||
|
fmt = tuple(tweakable_fmts)[0]
|
||||||
|
path = db.new_api.format_abspath(book_id, fmt)
|
||||||
|
if path is None:
|
||||||
|
return error_dialog(self.gui, _('File missing'), _(
|
||||||
|
'The %s format is missing from the calibre library. You should run'
|
||||||
|
' library maintenance.') % fmt, show=True)
|
||||||
|
tweak = 'ebook-tweak'
|
||||||
|
self.gui.setCursor(Qt.BusyCursor)
|
||||||
|
try:
|
||||||
|
self.gui.job_manager.launch_gui_app(tweak, kwargs=dict(args=[tweak, path]))
|
||||||
|
time.sleep(2)
|
||||||
|
finally:
|
||||||
|
self.gui.unsetCursor()
|
||||||
|
348
src/calibre/gui2/actions/unpack_book.py
Normal file
348
src/calibre/gui2/actions/unpack_book.py
Normal file
@ -0,0 +1,348 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import os, weakref, shutil
|
||||||
|
|
||||||
|
from PyQt4.Qt import (QDialog, QVBoxLayout, QHBoxLayout, QRadioButton, QFrame,
|
||||||
|
QPushButton, QLabel, QGroupBox, QGridLayout, QIcon, QSize, QTimer)
|
||||||
|
|
||||||
|
from calibre import as_unicode
|
||||||
|
from calibre.constants import isosx
|
||||||
|
from calibre.gui2 import error_dialog, question_dialog, open_local_file, gprefs
|
||||||
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
from calibre.ptempfile import (PersistentTemporaryDirectory,
|
||||||
|
PersistentTemporaryFile)
|
||||||
|
from calibre.utils.config import prefs, tweaks
|
||||||
|
|
||||||
|
class UnpackBook(QDialog):
|
||||||
|
|
||||||
|
def __init__(self, parent, book_id, fmts, db):
|
||||||
|
QDialog.__init__(self, parent)
|
||||||
|
self.setWindowIcon(QIcon(I('unpack-book.png')))
|
||||||
|
self.book_id, self.fmts, self.db_ref = book_id, fmts, weakref.ref(db)
|
||||||
|
self._exploded = None
|
||||||
|
self._cleanup_dirs = []
|
||||||
|
self._cleanup_files = []
|
||||||
|
|
||||||
|
self.setup_ui()
|
||||||
|
self.setWindowTitle(_('Unpack Book') + ' - ' + db.title(book_id,
|
||||||
|
index_is_id=True))
|
||||||
|
|
||||||
|
button = self.fmt_choice_buttons[0]
|
||||||
|
button_map = {unicode(x.text()):x for x in self.fmt_choice_buttons}
|
||||||
|
of = prefs['output_format'].upper()
|
||||||
|
df = tweaks.get('default_tweak_format', None)
|
||||||
|
lf = gprefs.get('last_tweak_format', None)
|
||||||
|
if df and df.lower() == 'remember' and lf in button_map:
|
||||||
|
button = button_map[lf]
|
||||||
|
elif df and df.upper() in button_map:
|
||||||
|
button = button_map[df.upper()]
|
||||||
|
elif of in button_map:
|
||||||
|
button = button_map[of]
|
||||||
|
button.setChecked(True)
|
||||||
|
|
||||||
|
self.init_state()
|
||||||
|
for button in self.fmt_choice_buttons:
|
||||||
|
button.toggled.connect(self.init_state)
|
||||||
|
|
||||||
|
def init_state(self, *args):
|
||||||
|
self._exploded = None
|
||||||
|
self.preview_button.setEnabled(False)
|
||||||
|
self.rebuild_button.setEnabled(False)
|
||||||
|
self.explode_button.setEnabled(True)
|
||||||
|
|
||||||
|
def setup_ui(self): # {{{
|
||||||
|
self._g = g = QHBoxLayout(self)
|
||||||
|
self.setLayout(g)
|
||||||
|
self._l = l = QVBoxLayout()
|
||||||
|
g.addLayout(l)
|
||||||
|
|
||||||
|
fmts = sorted(x.upper() for x in self.fmts)
|
||||||
|
self.fmt_choice_box = QGroupBox(_('Choose the format to unpack:'), self)
|
||||||
|
self._fl = fl = QHBoxLayout()
|
||||||
|
self.fmt_choice_box.setLayout(self._fl)
|
||||||
|
self.fmt_choice_buttons = [QRadioButton(y, self) for y in fmts]
|
||||||
|
for x in self.fmt_choice_buttons:
|
||||||
|
fl.addWidget(x, stretch=10 if x is self.fmt_choice_buttons[-1] else
|
||||||
|
0)
|
||||||
|
l.addWidget(self.fmt_choice_box)
|
||||||
|
self.fmt_choice_box.setVisible(len(fmts) > 1)
|
||||||
|
|
||||||
|
self.help_label = QLabel(_('''\
|
||||||
|
<h2>About Unpack Book</h2>
|
||||||
|
<p>Unpack Book allows you to fine tune the appearance of an ebook by
|
||||||
|
making small changes to its internals. In order to use Unpack Book,
|
||||||
|
you need to know a little bit about HTML and CSS, technologies that
|
||||||
|
are used in ebooks. Follow the steps:</p>
|
||||||
|
<br>
|
||||||
|
<ol>
|
||||||
|
<li>Click "Explode Book": This will "explode" the book into its
|
||||||
|
individual internal components.<br></li>
|
||||||
|
<li>Right click on any individual file and select "Open with..." to
|
||||||
|
edit it in your favorite text editor.<br></li>
|
||||||
|
<li>When you are done: <b>close the file browser window
|
||||||
|
and the editor windows you used to make your tweaks</b>. Then click
|
||||||
|
the "Rebuild Book" button, to update the book in your calibre
|
||||||
|
library.</li>
|
||||||
|
</ol>'''))
|
||||||
|
self.help_label.setWordWrap(True)
|
||||||
|
self._fr = QFrame()
|
||||||
|
self._fr.setFrameShape(QFrame.VLine)
|
||||||
|
g.addWidget(self._fr)
|
||||||
|
g.addWidget(self.help_label)
|
||||||
|
|
||||||
|
self._b = b = QGridLayout()
|
||||||
|
left, top, right, bottom = b.getContentsMargins()
|
||||||
|
top += top
|
||||||
|
b.setContentsMargins(left, top, right, bottom)
|
||||||
|
l.addLayout(b, stretch=10)
|
||||||
|
|
||||||
|
self.explode_button = QPushButton(QIcon(I('wizard.png')), _('&Explode Book'))
|
||||||
|
self.preview_button = QPushButton(QIcon(I('view.png')), _('&Preview Book'))
|
||||||
|
self.cancel_button = QPushButton(QIcon(I('window-close.png')), _('&Cancel'))
|
||||||
|
self.rebuild_button = QPushButton(QIcon(I('exec.png')), _('&Rebuild Book'))
|
||||||
|
|
||||||
|
self.explode_button.setToolTip(
|
||||||
|
_('Explode the book to edit its components'))
|
||||||
|
self.preview_button.setToolTip(
|
||||||
|
_('Preview the result of your changes'))
|
||||||
|
self.cancel_button.setToolTip(
|
||||||
|
_('Abort without saving any changes'))
|
||||||
|
self.rebuild_button.setToolTip(
|
||||||
|
_('Save your changes and update the book in the calibre library'))
|
||||||
|
|
||||||
|
a = b.addWidget
|
||||||
|
a(self.explode_button, 0, 0, 1, 1)
|
||||||
|
a(self.preview_button, 0, 1, 1, 1)
|
||||||
|
a(self.cancel_button, 1, 0, 1, 1)
|
||||||
|
a(self.rebuild_button, 1, 1, 1, 1)
|
||||||
|
|
||||||
|
for x in ('explode', 'preview', 'cancel', 'rebuild'):
|
||||||
|
getattr(self, x+'_button').clicked.connect(getattr(self, x))
|
||||||
|
|
||||||
|
self.msg = QLabel('dummy', self)
|
||||||
|
self.msg.setVisible(False)
|
||||||
|
self.msg.setStyleSheet('''
|
||||||
|
QLabel {
|
||||||
|
text-align: center;
|
||||||
|
background-color: white;
|
||||||
|
color: black;
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: x-large;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
|
||||||
|
self.resize(self.sizeHint() + QSize(40, 10))
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def show_msg(self, msg):
|
||||||
|
self.msg.setText(msg)
|
||||||
|
self.msg.resize(self.size() - QSize(50, 25))
|
||||||
|
self.msg.move((self.width() - self.msg.width())//2,
|
||||||
|
(self.height() - self.msg.height())//2)
|
||||||
|
self.msg.setVisible(True)
|
||||||
|
|
||||||
|
def hide_msg(self):
|
||||||
|
self.msg.setVisible(False)
|
||||||
|
|
||||||
|
def explode(self):
|
||||||
|
self.show_msg(_('Exploding, please wait...'))
|
||||||
|
if len(self.fmt_choice_buttons) > 1:
|
||||||
|
gprefs.set('last_tweak_format', self.current_format.upper())
|
||||||
|
QTimer.singleShot(5, self.do_explode)
|
||||||
|
|
||||||
|
def ask_question(self, msg):
|
||||||
|
return question_dialog(self, _('Are you sure?'), msg)
|
||||||
|
|
||||||
|
def do_explode(self):
|
||||||
|
from calibre.ebooks.tweak import get_tools, Error, WorkerError
|
||||||
|
tdir = PersistentTemporaryDirectory('_tweak_explode')
|
||||||
|
self._cleanup_dirs.append(tdir)
|
||||||
|
det_msg = None
|
||||||
|
try:
|
||||||
|
src = self.db.format(self.book_id, self.current_format,
|
||||||
|
index_is_id=True, as_path=True)
|
||||||
|
self._cleanup_files.append(src)
|
||||||
|
exploder = get_tools(self.current_format)[0]
|
||||||
|
opf = exploder(src, tdir, question=self.ask_question)
|
||||||
|
except WorkerError as e:
|
||||||
|
det_msg = e.orig_tb
|
||||||
|
except Error as e:
|
||||||
|
return error_dialog(self, _('Failed to unpack'),
|
||||||
|
(_('Could not explode the %s file.')%self.current_format) + ' '
|
||||||
|
+ as_unicode(e), show=True)
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
det_msg = traceback.format_exc()
|
||||||
|
finally:
|
||||||
|
self.hide_msg()
|
||||||
|
|
||||||
|
if det_msg is not None:
|
||||||
|
return error_dialog(self, _('Failed to unpack'),
|
||||||
|
_('Could not explode the %s file. Click "Show Details" for '
|
||||||
|
'more information.')%self.current_format, det_msg=det_msg,
|
||||||
|
show=True)
|
||||||
|
|
||||||
|
if opf is None:
|
||||||
|
# The question was answered with No
|
||||||
|
return
|
||||||
|
|
||||||
|
self._exploded = tdir
|
||||||
|
self.explode_button.setEnabled(False)
|
||||||
|
self.preview_button.setEnabled(True)
|
||||||
|
self.rebuild_button.setEnabled(True)
|
||||||
|
open_local_file(tdir)
|
||||||
|
|
||||||
|
def rebuild_it(self):
|
||||||
|
from calibre.ebooks.tweak import get_tools, WorkerError
|
||||||
|
src_dir = self._exploded
|
||||||
|
det_msg = None
|
||||||
|
of = PersistentTemporaryFile('_tweak_rebuild.'+self.current_format.lower())
|
||||||
|
of.close()
|
||||||
|
of = of.name
|
||||||
|
self._cleanup_files.append(of)
|
||||||
|
try:
|
||||||
|
rebuilder = get_tools(self.current_format)[1]
|
||||||
|
rebuilder(src_dir, of)
|
||||||
|
except WorkerError as e:
|
||||||
|
det_msg = e.orig_tb
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
det_msg = traceback.format_exc()
|
||||||
|
finally:
|
||||||
|
self.hide_msg()
|
||||||
|
|
||||||
|
if det_msg is not None:
|
||||||
|
error_dialog(self, _('Failed to rebuild file'),
|
||||||
|
_('Failed to rebuild %s. For more information, click '
|
||||||
|
'"Show details".')%self.current_format,
|
||||||
|
det_msg=det_msg, show=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return of
|
||||||
|
|
||||||
|
def preview(self):
|
||||||
|
self.show_msg(_('Rebuilding, please wait...'))
|
||||||
|
QTimer.singleShot(5, self.do_preview)
|
||||||
|
|
||||||
|
def do_preview(self):
|
||||||
|
rebuilt = self.rebuild_it()
|
||||||
|
if rebuilt is not None:
|
||||||
|
self.parent().iactions['View']._view_file(rebuilt)
|
||||||
|
|
||||||
|
def rebuild(self):
|
||||||
|
self.show_msg(_('Rebuilding, please wait...'))
|
||||||
|
QTimer.singleShot(5, self.do_rebuild)
|
||||||
|
|
||||||
|
def do_rebuild(self):
|
||||||
|
rebuilt = self.rebuild_it()
|
||||||
|
if rebuilt is not None:
|
||||||
|
fmt = os.path.splitext(rebuilt)[1][1:].upper()
|
||||||
|
with open(rebuilt, 'rb') as f:
|
||||||
|
self.db.add_format(self.book_id, fmt, f, index_is_id=True)
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
self.reject()
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
if isosx and self._exploded:
|
||||||
|
try:
|
||||||
|
import appscript
|
||||||
|
self.finder = appscript.app('Finder')
|
||||||
|
self.finder.Finder_windows[os.path.basename(self._exploded)].close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for f in self._cleanup_files:
|
||||||
|
try:
|
||||||
|
os.remove(f)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for d in self._cleanup_dirs:
|
||||||
|
try:
|
||||||
|
shutil.rmtree(d)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def db(self):
|
||||||
|
return self.db_ref()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_format(self):
|
||||||
|
for b in self.fmt_choice_buttons:
|
||||||
|
if b.isChecked():
|
||||||
|
return unicode(b.text())
|
||||||
|
|
||||||
|
class UnpackBookAction(InterfaceAction):
|
||||||
|
|
||||||
|
name = 'Unpack Book'
|
||||||
|
action_spec = (_('Unpack Book'), 'unpack-book.png',
|
||||||
|
_('Unpack books in the EPUB, AZW3, HTMLZ formats into their individual components'), None)
|
||||||
|
dont_add_to = frozenset(['context-menu-device'])
|
||||||
|
action_type = 'current'
|
||||||
|
|
||||||
|
accepts_drops = True
|
||||||
|
|
||||||
|
def accept_enter_event(self, event, mime_data):
|
||||||
|
if mime_data.hasFormat("application/calibre+from_library"):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def accept_drag_move_event(self, event, mime_data):
|
||||||
|
if mime_data.hasFormat("application/calibre+from_library"):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def drop_event(self, event, mime_data):
|
||||||
|
mime = 'application/calibre+from_library'
|
||||||
|
if mime_data.hasFormat(mime):
|
||||||
|
self.dropped_ids = tuple(map(int, str(mime_data.data(mime)).split()))
|
||||||
|
QTimer.singleShot(1, self.do_drop)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def do_drop(self):
|
||||||
|
book_ids = self.dropped_ids
|
||||||
|
del self.dropped_ids
|
||||||
|
if book_ids:
|
||||||
|
self.do_tweak(book_ids[0])
|
||||||
|
|
||||||
|
def genesis(self):
|
||||||
|
self.qaction.triggered.connect(self.tweak_book)
|
||||||
|
|
||||||
|
def tweak_book(self):
|
||||||
|
row = self.gui.library_view.currentIndex()
|
||||||
|
if not row.isValid():
|
||||||
|
return error_dialog(self.gui, _('Cannot unpack Book'),
|
||||||
|
_('No book selected'), show=True)
|
||||||
|
|
||||||
|
book_id = self.gui.library_view.model().id(row)
|
||||||
|
self.do_tweak(book_id)
|
||||||
|
|
||||||
|
def do_tweak(self, book_id):
|
||||||
|
db = self.gui.library_view.model().db
|
||||||
|
fmts = db.formats(book_id, index_is_id=True) or ''
|
||||||
|
fmts = [x.lower().strip() for x in fmts.split(',')]
|
||||||
|
tweakable_fmts = set(fmts).intersection({'epub', 'htmlz', 'azw3',
|
||||||
|
'mobi', 'azw'})
|
||||||
|
if not tweakable_fmts:
|
||||||
|
return error_dialog(self.gui, _('Cannot unpack Book'),
|
||||||
|
_('The book must be in ePub, HTMLZ or AZW3 formats to unpack.'
|
||||||
|
'\n\nFirst convert the book to one of these formats.'),
|
||||||
|
show=True)
|
||||||
|
dlg = UnpackBook(self.gui, book_id, tweakable_fmts, db)
|
||||||
|
dlg.exec_()
|
||||||
|
dlg.cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -14,6 +14,8 @@ tprefs.defaults['editor_font_family'] = None
|
|||||||
tprefs.defaults['editor_font_size'] = 12
|
tprefs.defaults['editor_font_size'] = 12
|
||||||
tprefs.defaults['editor_line_wrap'] = True
|
tprefs.defaults['editor_line_wrap'] = True
|
||||||
tprefs.defaults['preview_refresh_time'] = 2
|
tprefs.defaults['preview_refresh_time'] = 2
|
||||||
|
tprefs.defaults['choose_tweak_fmt'] = True
|
||||||
|
tprefs.defaults['tweak_fmt_order'] = ['EPUB', 'AZW3']
|
||||||
|
|
||||||
_current_container = None
|
_current_container = None
|
||||||
|
|
||||||
|
@ -25,6 +25,9 @@ PARALLEL_FUNCS = {
|
|||||||
'ebook-viewer' :
|
'ebook-viewer' :
|
||||||
('calibre.gui2.viewer.main', 'main', None),
|
('calibre.gui2.viewer.main', 'main', None),
|
||||||
|
|
||||||
|
'ebook-tweak' :
|
||||||
|
('calibre.gui2.tweak_book.main', 'main', None),
|
||||||
|
|
||||||
'render_pages' :
|
'render_pages' :
|
||||||
('calibre.ebooks.comic.input', 'render_pages', 'notification'),
|
('calibre.ebooks.comic.input', 'render_pages', 'notification'),
|
||||||
|
|
||||||
@ -197,6 +200,5 @@ def main():
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
Loading…
x
Reference in New Issue
Block a user