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'
|
||||
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):
|
||||
name = 'Next Match'
|
||||
actual_plugin = 'calibre.gui2.actions.next_match:NextMatchAction'
|
||||
@ -957,7 +962,7 @@ plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
|
||||
ActionShowBookDetails,ActionRestart, ActionOpenFolder, ActionConnectShare,
|
||||
ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks,
|
||||
ActionAddToLibrary, ActionEditCollections, ActionMatchBooks, ActionChooseLibrary,
|
||||
ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch, ActionStore,
|
||||
ActionCopyToLibrary, ActionTweakEpub, ActionUnpackBook, ActionNextMatch, ActionStore,
|
||||
ActionPluginUpdater, ActionPickRandom, ActionEditToC, ActionSortBy,
|
||||
ActionMarkBooks]
|
||||
|
||||
|
@ -671,8 +671,8 @@ class Cache(object):
|
||||
'''
|
||||
Return absolute path to the ebook file of format `format`
|
||||
|
||||
Currently used only in calibredb list, the viewer and the catalogs (via
|
||||
get_data_as_dict()).
|
||||
Currently used only in calibredb list, the viewer, tweak book and the
|
||||
catalogs (via get_data_as_dict()).
|
||||
|
||||
Apart from the viewer, I don't believe any of the others do any file
|
||||
I/O with the results of this call.
|
||||
|
@ -5,290 +5,56 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os, weakref, shutil
|
||||
import time
|
||||
from functools import partial
|
||||
|
||||
from PyQt4.Qt import (QDialog, QVBoxLayout, QHBoxLayout, QRadioButton, QFrame,
|
||||
QPushButton, QLabel, QGroupBox, QGridLayout, QIcon, QSize, QTimer)
|
||||
from PyQt4.Qt import QTimer, QDialog, QDialogButtonBox, QCheckBox, QVBoxLayout, QLabel, Qt
|
||||
|
||||
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 import error_dialog
|
||||
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)
|
||||
self.setWindowIcon(QIcon(I('tweak.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.l = l = QVBoxLayout(self)
|
||||
self.setLayout(l)
|
||||
self.setWindowTitle(_('Choose format to tweak'))
|
||||
|
||||
self.setup_ui()
|
||||
self.setWindowTitle(_('Tweak Book') + ' - ' + db.title(book_id,
|
||||
index_is_id=True))
|
||||
self.la = la = QLabel(_(
|
||||
'This book mas multiple formats that can be tweaked. Choose the format you want to tweak.'))
|
||||
l.addWidget(la)
|
||||
|
||||
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.rem = QCheckBox(_('Always ask when more than one format is available'))
|
||||
self.rem.setChecked(True)
|
||||
l.addWidget(self.rem)
|
||||
|
||||
self.init_state()
|
||||
for button in self.fmt_choice_buttons:
|
||||
button.toggled.connect(self.init_state)
|
||||
self.bb = bb = QDialogButtonBox(self)
|
||||
l.addWidget(bb)
|
||||
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._exploded = None
|
||||
self.preview_button.setEnabled(False)
|
||||
self.rebuild_button.setEnabled(False)
|
||||
self.explode_button.setEnabled(True)
|
||||
self.fmt = None
|
||||
self.resize(self.sizeHint())
|
||||
|
||||
def setup_ui(self): # {{{
|
||||
self._g = g = QHBoxLayout(self)
|
||||
self.setLayout(g)
|
||||
self._l = l = QVBoxLayout()
|
||||
g.addLayout(l)
|
||||
def chosen(self, fmt):
|
||||
self.fmt = fmt
|
||||
|
||||
fmts = sorted(x.upper() for x in self.fmts)
|
||||
self.fmt_choice_box = QGroupBox(_('Choose the format to tweak:'), 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)
|
||||
def accept(self):
|
||||
from calibre.gui2.tweak_book import tprefs
|
||||
tprefs['choose_tweak_fmt'] = self.rem.isChecked()
|
||||
QDialog.accept(self)
|
||||
|
||||
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):
|
||||
|
||||
name = 'Tweak ePub'
|
||||
action_spec = (_('Tweak Book'), 'tweak.png',
|
||||
_('Make small changes to ePub, HTMLZ or AZW3 format books'),
|
||||
_('T'))
|
||||
action_spec = (_('Tweak Book'), 'tweak.png', _('Edit eBooks'), _('T'))
|
||||
dont_add_to = frozenset(['context-menu-device'])
|
||||
action_type = 'current'
|
||||
|
||||
@ -324,25 +90,46 @@ class TweakEpubAction(InterfaceAction):
|
||||
def tweak_book(self):
|
||||
row = self.gui.library_view.currentIndex()
|
||||
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)
|
||||
|
||||
book_id = self.gui.library_view.model().id(row)
|
||||
self.do_tweak(book_id)
|
||||
|
||||
def do_tweak(self, book_id):
|
||||
from calibre.ebooks.oeb.polish.main import SUPPORTED
|
||||
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'})
|
||||
fmts = [x.upper().strip() for x in fmts.split(',')]
|
||||
tweakable_fmts = set(fmts).intersection(SUPPORTED)
|
||||
if not tweakable_fmts:
|
||||
return error_dialog(self.gui, _('Cannot Tweak Book'),
|
||||
_('The book must be in ePub, HTMLZ or AZW3 formats to tweak.'
|
||||
'\n\nFirst convert the book to one of these formats.'),
|
||||
_('The book must be in the %s formats to tweak.'
|
||||
'\n\nFirst convert the book to one of these formats.') % (_(' or '.join(SUPPORTED))),
|
||||
show=True)
|
||||
dlg = TweakBook(self.gui, book_id, tweakable_fmts, db)
|
||||
dlg.exec_()
|
||||
dlg.cleanup()
|
||||
|
||||
if len(tweakable_fmts) > 1:
|
||||
from calibre.gui2.tweak_book import tprefs
|
||||
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_line_wrap'] = True
|
||||
tprefs.defaults['preview_refresh_time'] = 2
|
||||
tprefs.defaults['choose_tweak_fmt'] = True
|
||||
tprefs.defaults['tweak_fmt_order'] = ['EPUB', 'AZW3']
|
||||
|
||||
_current_container = None
|
||||
|
||||
|
@ -25,6 +25,9 @@ PARALLEL_FUNCS = {
|
||||
'ebook-viewer' :
|
||||
('calibre.gui2.viewer.main', 'main', None),
|
||||
|
||||
'ebook-tweak' :
|
||||
('calibre.gui2.tweak_book.main', 'main', None),
|
||||
|
||||
'render_pages' :
|
||||
('calibre.ebooks.comic.input', 'render_pages', 'notification'),
|
||||
|
||||
@ -197,6 +200,5 @@ def main():
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
|
Loading…
x
Reference in New Issue
Block a user