Edit book: Allow creation of new, empty books via File->Create new book

This commit is contained in:
Kovid Goyal 2013-12-19 17:33:53 +05:30
parent b4abb37e74
commit c8a2ac65e0
5 changed files with 183 additions and 4 deletions

View File

@ -1492,7 +1492,7 @@ def metadata_to_opf(mi, as_string=True, default_lang=None):
root = etree.fromstring(textwrap.dedent( root = etree.fromstring(textwrap.dedent(
''' '''
<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="uuid_id"> <package xmlns="http://www.idpf.org/2007/opf" unique-identifier="uuid_id" version="2.0">
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf"> <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
<dc:identifier opf:scheme="%(a)s" id="%(a)s_id">%(id)s</dc:identifier> <dc:identifier opf:scheme="%(a)s" id="%(a)s_id">%(id)s</dc:identifier>
<dc:identifier opf:scheme="uuid" id="uuid_id">%(uuid)s</dc:identifier> <dc:identifier opf:scheme="uuid" id="uuid_id">%(uuid)s</dc:identifier>

View File

@ -0,0 +1,102 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import sys, os
from lxml import etree
from calibre import prepare_string_for_xml, CurrentDir
from calibre.ptempfile import TemporaryDirectory
from calibre.ebooks.metadata.opf2 import metadata_to_opf
from calibre.ebooks.oeb.polish.container import OPF_NAMESPACES, guess_type, opf_to_azw3
from calibre.ebooks.oeb.polish.pretty import pretty_xml_tree
from calibre.ebooks.oeb.polish.toc import TOC, create_ncx
from calibre.utils.localization import lang_as_iso639_1
from calibre.utils.logging import DevNull
from calibre.utils.zipfile import ZipFile, ZIP_STORED
def create_toc(mi, opf, html_name, lang):
uuid = ''
for u in opf.xpath('//*[@id="uuid_id"]'):
uuid = u.text
toc = TOC()
toc.add(_('Start'), html_name)
return create_ncx(toc, lambda x:x, mi.title, lang, uuid)
def create_book(mi, path, fmt='epub', opf_name='metadata.opf', html_name='start.xhtml', toc_name='toc.ncx'):
''' Create an empty book in the specified format at the specified location. '''
path = os.path.abspath(path)
lang = 'und'
opf = metadata_to_opf(mi, as_string=False)
for l in opf.xpath('//*[local-name()="language"]'):
if l.text:
lang = l.text
break
lang = lang_as_iso639_1(lang) or lang
opfns = OPF_NAMESPACES['opf']
m = opf.makeelement('{%s}manifest' % opfns)
opf.insert(1, m)
i = m.makeelement('{%s}item' % opfns, href=html_name, id='start')
i.set('media-type', guess_type('a.xhtml'))
m.append(i)
i = m.makeelement('{%s}item' % opfns, href=toc_name, id='ncx')
i.set('media-type', guess_type(toc_name))
m.append(i)
s = opf.makeelement('{%s}spine' % opfns, toc="ncx")
opf.insert(2, s)
i = s.makeelement('{%s}itemref' % opfns, idref='start')
s.append(i)
CONTAINER = '''\
<?xml version="1.0"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
<rootfiles>
<rootfile full-path="{0}" media-type="application/oebps-package+xml"/>
</rootfiles>
</container>
'''.format(prepare_string_for_xml(opf_name, True)).encode('utf-8')
HTML = '''\
<?xml version='1.0' encoding='utf-8'?>
<html lang="{1}" xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>{0}</title>
</head>
<body>
<h1>{0}</h1>
</body>
</html>
'''.format(prepare_string_for_xml(mi.title), lang).encode('utf-8')
ncx = etree.tostring(create_toc(mi, opf, html_name, lang), encoding='utf-8', xml_declaration=True, pretty_print=True)
pretty_xml_tree(opf)
opf = etree.tostring(opf, encoding='utf-8', xml_declaration=True, pretty_print=True)
if fmt == 'azw3':
with TemporaryDirectory('create-azw3') as tdir, CurrentDir(tdir):
for name, data in ((opf_name, opf), (html_name, HTML), (toc_name, ncx)):
with open(name, 'wb') as f:
f.write(data)
opf_to_azw3(opf_name, path, DevNull())
else:
with ZipFile(path, 'w', compression=ZIP_STORED) as zf:
zf.writestr('mimetype', b'application/epub+zip', compression=ZIP_STORED)
zf.writestr('META-INF/', b'', 0755)
zf.writestr('META-INF/container.xml', CONTAINER)
zf.writestr(opf_name, opf)
zf.writestr(html_name, HTML)
zf.writestr(toc_name, ncx)
if __name__ == '__main__':
from calibre.ebooks.metadata.book.base import Metadata
mi = Metadata('Test book', authors=('Kovid Goyal',))
path = sys.argv[-1]
ext = path.rpartition('.')[-1].lower()
if ext not in ('epub', 'azw3'):
print ('Unsupported format:', ext)
raise SystemExit(1)
create_book(mi, path, fmt=ext)

View File

@ -32,7 +32,7 @@ from calibre.gui2.tweak_book.save import SaveManager, save_container
from calibre.gui2.tweak_book.preview import parse_worker, font_cache from calibre.gui2.tweak_book.preview import parse_worker, font_cache
from calibre.gui2.tweak_book.toc import TOCEditor from calibre.gui2.tweak_book.toc import TOCEditor
from calibre.gui2.tweak_book.editor import editor_from_syntax, syntax_from_mime from calibre.gui2.tweak_book.editor import editor_from_syntax, syntax_from_mime
from calibre.gui2.tweak_book.editor.insert_resource import get_resource_data from calibre.gui2.tweak_book.editor.insert_resource import get_resource_data, NewBook
from calibre.gui2.tweak_book.preferences import Preferences from calibre.gui2.tweak_book.preferences import Preferences
def get_container(*args, **kwargs): def get_container(*args, **kwargs):
@ -135,7 +135,7 @@ class Boss(QObject):
self.container_count += 1 self.container_count += 1
return tempfile.mkdtemp(prefix='%s%05d-' % (prefix, self.container_count), dir=self.tdir) return tempfile.mkdtemp(prefix='%s%05d-' % (prefix, self.container_count), dir=self.tdir)
def open_book(self, path=None, edit_file=None, clear_notify_data=True): def _check_before_open(self):
if self.gui.action_save.isEnabled(): if self.gui.action_save.isEnabled():
if not question_dialog(self.gui, _('Unsaved changes'), _( if not question_dialog(self.gui, _('Unsaved changes'), _(
'The current book has unsaved changes. If you open a new book, they will be lost' 'The current book has unsaved changes. If you open a new book, they will be lost'
@ -145,7 +145,24 @@ class Boss(QObject):
return info_dialog(self.gui, _('Cannot open'), return info_dialog(self.gui, _('Cannot open'),
_('The current book is being saved, you cannot open a new book until' _('The current book is being saved, you cannot open a new book until'
' the saving is completed'), show=True) ' the saving is completed'), show=True)
return True
def new_book(self):
if not self._check_before_open():
return
d = NewBook(self.gui)
if d.exec_() == d.Accepted:
fmt = d.fmt
path = choose_save_file(self.gui, 'edit-book-new-book', _('Choose file location'),
filters=[(fmt.upper(), (fmt,))], all_files=False)
if path is not None:
from calibre.ebooks.oeb.polish.create import create_book
create_book(d.mi, path, fmt=fmt)
self.open_book(path=path)
def open_book(self, path=None, edit_file=None, clear_notify_data=True):
if not self._check_before_open():
return
if not hasattr(path, 'rpartition'): if not hasattr(path, 'rpartition'):
path = choose_files(self.gui, 'open-book-for-tweaking', _('Choose book'), path = choose_files(self.gui, 'open-book-for-tweaking', _('Choose book'),
[(_('Books'), [x.lower() for x in SUPPORTED])], all_files=False, select_only_single_file=True) [(_('Books'), [x.lower() for x in SUPPORTED])], all_files=False, select_only_single_file=True)

View File

@ -7,6 +7,7 @@ __license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import sys, os import sys, os
from functools import partial
from PyQt4.Qt import ( from PyQt4.Qt import (
QDialog, QGridLayout, QDialogButtonBox, QSize, QListView, QStyledItemDelegate, QDialog, QGridLayout, QDialogButtonBox, QSize, QListView, QStyledItemDelegate,
@ -16,9 +17,13 @@ from PyQt4.Qt import (
from calibre import fit_image from calibre import fit_image
from calibre.constants import plugins from calibre.constants import plugins
from calibre.ebooks.metadata import string_to_authors
from calibre.ebooks.metadata.book.base import Metadata
from calibre.gui2 import NONE, choose_files, error_dialog from calibre.gui2 import NONE, choose_files, error_dialog
from calibre.gui2.languages import LanguagesEdit
from calibre.gui2.tweak_book import current_container, tprefs from calibre.gui2.tweak_book import current_container, tprefs
from calibre.gui2.tweak_book.file_list import name_is_ok from calibre.gui2.tweak_book.file_list import name_is_ok
from calibre.utils.localization import get_lang, canonicalize_lang
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
class Dialog(QDialog): class Dialog(QDialog):
@ -54,7 +59,9 @@ class Dialog(QDialog):
tprefs.set(self.name + '-splitter-state', bytearray(self.splitter.saveState())) tprefs.set(self.name + '-splitter-state', bytearray(self.splitter.saveState()))
QDialog.reject(self) QDialog.reject(self)
class ChooseName(Dialog): class ChooseName(Dialog): # {{{
''' Chooses the filename for a newly imported file, with error checking '''
def __init__(self, candidate, parent=None): def __init__(self, candidate, parent=None):
self.candidate = candidate self.candidate = candidate
@ -91,7 +98,9 @@ class ChooseName(Dialog):
name, ext = n.rpartition('.')[0::2] name, ext = n.rpartition('.')[0::2]
self.filename = name + '.' + ext.lower() self.filename = name + '.' + ext.lower()
super(ChooseName, self).accept() super(ChooseName, self).accept()
# }}}
# Images {{{
class ImageDelegate(QStyledItemDelegate): class ImageDelegate(QStyledItemDelegate):
MARGIN = 4 MARGIN = 4
@ -291,6 +300,7 @@ class InsertImage(Dialog):
def filter_changed(self, *args): def filter_changed(self, *args):
f = unicode(self.filter.text()) f = unicode(self.filter.text())
self.fm.setFilterFixedString(f) self.fm.setFilterFixedString(f)
# }}}
def get_resource_data(rtype, parent): def get_resource_data(rtype, parent):
if rtype == 'image': if rtype == 'image':
@ -298,6 +308,54 @@ def get_resource_data(rtype, parent):
if d.exec_() == d.Accepted: if d.exec_() == d.Accepted:
return d.chosen_image, d.chosen_image_is_external return d.chosen_image, d.chosen_image_is_external
class NewBook(Dialog): # {{{
def __init__(self, parent=None):
self.fmt = 'epub'
Dialog.__init__(self, _('Create new book'), 'create-new-book', parent=parent)
def setup_ui(self):
self.l = l = QFormLayout(self)
self.setLayout(l)
self.title = t = QLineEdit(self)
l.addRow(_('&Title:'), t)
t.setFocus(Qt.OtherFocusReason)
self.authors = a = QLineEdit(self)
l.addRow(_('&Authors:'), a)
a.setText(tprefs.get('previous_new_book_authors', ''))
self.languages = la = LanguagesEdit(self)
l.addRow(_('&Language:'), la)
la.lang_codes = (tprefs.get('previous_new_book_lang', canonicalize_lang(get_lang())),)
bb = self.bb
l.addRow(bb)
bb.clear()
bb.addButton(bb.Cancel)
b = bb.addButton('&EPUB', bb.AcceptRole)
b.clicked.connect(partial(self.set_fmt, 'epub'))
b = bb.addButton('&AZW3', bb.AcceptRole)
b.clicked.connect(partial(self.set_fmt, 'azw3'))
def set_fmt(self, fmt):
self.fmt = fmt
def accept(self):
tprefs.set('previous_new_book_authors', unicode(self.authors.text()))
tprefs.set('previous_new_book_lang', (self.languages.lang_codes or [get_lang()])[0])
super(NewBook, self).accept()
@property
def mi(self):
mi = Metadata(unicode(self.title.text()).strip() or _('Unknown'))
mi.authors = string_to_authors(unicode(self.authors.text()).strip()) or [_('Unknown')]
mi.languages = self.languages.lang_codes or [get_lang()]
return mi
# }}}
if __name__ == '__main__': if __name__ == '__main__':
app = QApplication([]) # noqa app = QApplication([]) # noqa
from calibre.gui2.tweak_book import set_current_container from calibre.gui2.tweak_book import set_current_container

View File

@ -277,6 +277,7 @@ class Main(MainWindow):
self.action_save_copy = reg('save.png', _('Save a &copy'), self.boss.save_copy, 'save-copy', 'Ctrl+Alt+S', _('Save a copy of the book')) self.action_save_copy = reg('save.png', _('Save a &copy'), self.boss.save_copy, 'save-copy', 'Ctrl+Alt+S', _('Save a copy of the book'))
self.action_quit = reg('quit.png', _('&Quit'), self.boss.quit, 'quit', 'Ctrl+Q', _('Quit')) self.action_quit = reg('quit.png', _('&Quit'), self.boss.quit, 'quit', 'Ctrl+Q', _('Quit'))
self.action_preferences = reg('config.png', _('&Preferences'), self.boss.preferences, 'preferences', 'Ctrl+P', _('Preferences')) self.action_preferences = reg('config.png', _('&Preferences'), self.boss.preferences, 'preferences', 'Ctrl+P', _('Preferences'))
self.action_new_book = reg('book.png', _('Create &new, empty book'), self.boss.new_book, 'new-book', (), _('Create a new, empty book'))
# Editor actions # Editor actions
group = _('Editor actions') group = _('Editor actions')
@ -391,6 +392,7 @@ class Main(MainWindow):
f = b.addMenu(_('&File')) f = b.addMenu(_('&File'))
f.addAction(self.action_new_file) f.addAction(self.action_new_file)
f.addAction(self.action_open_book) f.addAction(self.action_open_book)
f.addAction(self.action_new_book)
self.recent_books_menu = f.addMenu(_('&Recently opened books')) self.recent_books_menu = f.addMenu(_('&Recently opened books'))
self.update_recent_books() self.update_recent_books()
f.addSeparator() f.addSeparator()