From e602d726ab39c8c589db53c5400a2efe668259d1 Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Wed, 29 Sep 2010 15:01:39 +0800 Subject: [PATCH 01/75] [SNBOutput] Add stub for SNB format output support. SNB is the only format supported by the E Ink device Bambook manufactured by Shanda (NASDAQ: SNDA) --- resources/images/mimetypes/snb.png | Bin 0 -> 6245 bytes src/calibre/customize/builtins.py | 2 + src/calibre/ebooks/snb/__init__.py | 9 +++ src/calibre/ebooks/snb/output.py | 70 +++++++++++++++++++++++ src/calibre/gui2/convert/snb_output.py | 36 ++++++++++++ src/calibre/gui2/convert/snb_output.ui | 74 +++++++++++++++++++++++++ 6 files changed, 191 insertions(+) create mode 100644 resources/images/mimetypes/snb.png create mode 100644 src/calibre/ebooks/snb/__init__.py create mode 100644 src/calibre/ebooks/snb/output.py create mode 100644 src/calibre/gui2/convert/snb_output.py create mode 100644 src/calibre/gui2/convert/snb_output.ui diff --git a/resources/images/mimetypes/snb.png b/resources/images/mimetypes/snb.png new file mode 100644 index 0000000000000000000000000000000000000000..41b55f4343ae14d4bccaeede12193c0cee478a01 GIT binary patch literal 6245 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 + + Form + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From dd69e247476fa000e7f5b2d4edb60034d862dbdd Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Sat, 9 Oct 2010 22:30:38 +0800 Subject: [PATCH 02/75] [SNBOutput] Add basic output support for SNB file. --- src/calibre/ebooks/snb/output.py | 193 ++++++++++++++++--- src/calibre/ebooks/snb/snbfile.py | 300 ++++++++++++++++++++++++++++++ src/calibre/ebooks/snb/snbml.py | 160 ++++++++++++++++ 3 files changed, 629 insertions(+), 24 deletions(-) create mode 100644 src/calibre/ebooks/snb/snbfile.py create mode 100644 src/calibre/ebooks/snb/snbml.py diff --git a/src/calibre/ebooks/snb/output.py b/src/calibre/ebooks/snb/output.py index 4b94b65405..c302c17729 100644 --- a/src/calibre/ebooks/snb/output.py +++ b/src/calibre/ebooks/snb/output.py @@ -4,10 +4,29 @@ __license__ = 'GPL 3' __copyright__ = '2010, Li Fanxi ' __docformat__ = 'restructuredtext en' -import os +import os, string -from calibre.customize.conversion import OutputFormatPlugin, \ - OptionRecommendation +from lxml import etree +from calibre.customize.conversion import OutputFormatPlugin, OptionRecommendation +from calibre.ptempfile import TemporaryDirectory +from calibre.constants import __appname__, __version__ +from calibre.ebooks.oeb.base import XHTML, XHTML_NS, barename, namespace +from calibre.ebooks.snb.snbfile import SNBFile +from calibre.ebooks.snb.snbml import SNBMLizer + +def ProcessFileName(fileName): + # Flat the path + fileName = fileName.replace("/", "_").replace(os.sep, "_") + # Handle bookmark for HTML file + fileName = fileName.replace("#", "_") + # Make it lower case + fileName = fileName.lower() + # Change extension from jpeg to jpg + root, ext = os.path.splitext(fileName) + if ext in [ '.jpeg', '.jpg', '.gif', '.svg' ]: + fileName = root + '.png' + return fileName + class SNBOutput(OutputFormatPlugin): @@ -45,26 +64,152 @@ class SNBOutput(OutputFormatPlugin): ]) def convert(self, oeb_book, output_path, input_plugin, opts, log): + # Create temp dir + with TemporaryDirectory('_snb_output') as tdir: + # Create stub directories + snbfDir = os.path.join(tdir, 'snbf') + snbcDir = os.path.join(tdir, 'snbc') + snbiDir = os.path.join(tdir, 'snbc/images') + os.mkdir(snbfDir) + os.mkdir(snbcDir) + os.mkdir(snbiDir) + + # Process Meta data + meta = oeb_book.metadata + if meta.title: + title = unicode(meta.title[0]) + else: + title = '' + authors = [unicode(x) for x in meta.creator if x.role == 'aut'] + if meta.publisher: + publishers = unicode(meta.publisher[0]) + else: + publishers = '' + if meta.language: + lang = unicode(meta.language[0]).upper() + else: + lang = '' + if meta.description: + abstract = unicode(meta.description[0]) + else: + abstract = '' + + # Process Cover + from calibre.ebooks.oeb.base import urldefrag + g, m, s = oeb_book.guide, oeb_book.manifest, oeb_book.spine + href = None + if 'titlepage' not in g: + if 'cover' in g: + href = g['cover'].href + + # Output book info file + bookInfoTree = etree.Element("book-snbf", version="1.0") + headTree = etree.SubElement(bookInfoTree, "head") + etree.SubElement(headTree, "name").text = title + etree.SubElement(headTree, "author").text = ' '.join(authors) + etree.SubElement(headTree, "language").text = lang + etree.SubElement(headTree, "rights") + etree.SubElement(headTree, "publisher").text = publishers + etree.SubElement(headTree, "generator").text = __appname__ + ' ' + __version__ + etree.SubElement(headTree, "created") + etree.SubElement(headTree, "abstract").text = abstract + if href != None: + etree.SubElement(headTree, "cover").text = ProcessFileName(href) + else: + etree.SubElement(headTree, "cover") + bookInfoFile = open(os.path.join(snbfDir, 'book.snbf'), 'wb') + bookInfoFile.write(etree.tostring(bookInfoTree, pretty_print=True, encoding='utf-8')) + bookInfoFile.close() + + # Output TOC + tocInfoTree = etree.Element("toc-snbf") + tocHead = etree.SubElement(tocInfoTree, "head") + tocBody = etree.SubElement(tocInfoTree, "body") + outputFiles = { } + if oeb_book.toc.count() == 0: + log.warn('This SNB file has no Table of Contents. ' + 'Creating a default TOC') + first = iter(oeb_book.spine).next() + oeb_book.toc.add(_('Start'), first.href) + + for tocitem in oeb_book.toc: + ch = etree.SubElement(tocBody, "chapter") + ch.set("src", ProcessFileName(tocitem.href) + ".snbc") + ch.text = tocitem.title + if tocitem.href.find('#') != -1: + item = string.split(tocitem.href, '#') + if len(item) != 2: + log.error('Error in TOC item: %s' % tocitem) + else: + if item[0] in outputFiles: + outputFiles[item[0]].append((item[1], tocitem.title)) + else: + outputFiles[item[0]] = [] + outputFiles[item[0]].append((item[1], tocitem.title)) + else: + if tocitem.href in outputFiles: + outputFiles[tocitem.href].append(("", tocitem)) + else: + outputFiles[tocitem.href] = [] + outputFiles[tocitem.href].append(("", tocitem)) + + etree.SubElement(tocHead, "chapters").text = '%d' % len(tocBody) + + tocInfoFile = open(os.path.join(snbfDir, 'toc.snbf'), 'wb') + tocInfoFile.write(etree.tostring(tocInfoTree, pretty_print=True, encoding='utf-8')) + tocInfoFile.close() + + # Output Files + for item in s: + from calibre.ebooks.oeb.base import OEB_DOCS, OEB_IMAGES, PNG_MIME + if m.hrefs[item.href].media_type in OEB_DOCS: + if not item.href in outputFiles: + log.debug('Skipping %s because unused in TOC.' % item.href) + continue + log.debug('Converting %s to snbc...' % item.href) + snbwriter = SNBMLizer(log) + snbcTrees = snbwriter.extract_content(oeb_book, item, outputFiles[item.href], opts) + for subName in snbcTrees: + postfix = '' + if subName != '': + postfix = '_' + subName + outputFile = open(os.path.join(snbcDir, ProcessFileName(item.href + postfix + ".snbc")), 'wb') + outputFile.write(etree.tostring(snbcTrees[subName], pretty_print=True, encoding='utf-8')) + outputFile.close() + for item in m: + if m.hrefs[item.href].media_type in OEB_IMAGES: + log.debug('Converting image: %s ...' % item.href) + content = m.hrefs[item.href].data + if m.hrefs[item.href].media_type != PNG_MIME: + # Convert + from calibre.utils.magick import Image + img = Image() + img.load(content) + img.save(os.path.join(snbiDir, ProcessFileName(item.href))) + else: + outputFile = open(os.path.join(snbiDir, ProcessFileName(item.href)), 'wb') + outputFile.write(content) + outputFile.close() + + # Package as SNB File + snbFile = SNBFile() + snbFile.FromDir(tdir) + snbFile.Output(output_path) + +if __name__ == '__main__': + from calibre.ebooks.oeb.reader import OEBReader + from calibre.ebooks.oeb.base import OEBBook + from calibre.ebooks.conversion.preprocess import HTMLPreProcessor + from calibre.customize.profiles import HanlinV3Output + class OptionValues(object): pass - # writer = TXTMLizer(log) - # txt = writer.extract_content(oeb_book, opts) - - # log.debug('\tReplacing newlines with selected type...') - # txt = specified_newlines(TxtNewlines(opts.newline).newline, txt) - - # close = False - # if not hasattr(output_path, 'write'): - # close = True - # if not os.path.exists(os.path.dirname(output_path)) and os.path.dirname(output_path) != '': - # os.makedirs(os.path.dirname(output_path)) - # out_stream = open(output_path, 'wb') - # else: - # out_stream = output_path - - # out_stream.seek(0) - # out_stream.truncate() - # out_stream.write(txt.encode(opts.output_encoding, 'replace')) - - # if close: - # out_stream.close() + opts = OptionValues() + opts.output_profile = HanlinV3Output(None) + + html_preprocessor = HTMLPreProcessor(None, None, opts) + from calibre.utils.logging import default_log + oeb = OEBBook(default_log, html_preprocessor) + reader = OEBReader + reader()(oeb, '/tmp/bbb/processed/') + SNBOutput(None).convert(oeb, '/tmp/test.snb', None, None, default_log); diff --git a/src/calibre/ebooks/snb/snbfile.py b/src/calibre/ebooks/snb/snbfile.py new file mode 100644 index 0000000000..aa690fb92b --- /dev/null +++ b/src/calibre/ebooks/snb/snbfile.py @@ -0,0 +1,300 @@ +# -*- coding: utf-8 -*- + +__license__ = 'GPL 3' +__copyright__ = '2010, Li Fanxi ' +__docformat__ = 'restructuredtext en' + +import sys, struct, zlib, bz2, os, math + +class FileStream: + def IsBinary(self): + return self.attr & 0x41000000 != 0x41000000 + +def compareFileStream(file1, file2): + return cmp(file1.fileName, file2.fileName) + +class BlockData: + pass + +class SNBFile: + + files = [] + blocks = [] + + MAGIC = 'SNBP000B' + REV80 = 0x00008000 + REVA3 = 0x00A3A3A3 + REVZ1 = 0x00000000 + REVZ2 = 0x00000000 + + def __init__(self, inputFile = None): + if inputFile != None: + self.Parse(inputFile); + + def Parse(self, inputFile): + self.fileName = inputFile + + snbFile = open(self.fileName, "rb") + snbFile.seek(0) + + # Read header + vmbr = snbFile.read(44) + (self.magic, self.rev80, self.revA3, self.revZ1, + self.fileCount, self.vfatSize, self.vfatCompressed, + self.binStreamSize, self.plainStreamSizeUncompressed, + self.revZ2) = struct.unpack('>8siiiiiiiii', vmbr) + + # Read FAT + self.vfat = zlib.decompress(snbFile.read(self.vfatCompressed)) + self.ParseFile(self.vfat, self.fileCount) + + # Read tail + snbFile.seek(-16, os.SEEK_END) + #plainStreamEnd = snbFile.tell() + tailblock = snbFile.read(16) + (self.tailSize, self.tailOffset, self.tailMagic) = struct.unpack('>ii8s', tailblock) + snbFile.seek(self.tailOffset) + self.vTailUncompressed = zlib.decompress(snbFile.read(self.tailSize)) + self.tailSizeUncompressed = len(self.vTailUncompressed) + self.ParseTail(self.vTailUncompressed, self.fileCount) + + # Uncompress file data + # Read files + binPos = 0 + plainPos = 0 + uncompressedData = None + for f in self.files: + if f.attr & 0x41000000 == 0x41000000: + # Compressed Files + if uncompressedData == None: + uncompressedData = "" + for i in range(self.plainBlock): + bzdc = bz2.BZ2Decompressor() + if (i < self.plainBlock - 1): + bSize = self.blocks[self.binBlock + i + 1].Offset - self.blocks[self.binBlock + i].Offset; + else: + bSize = self.tailOffset - self.blocks[self.binBlock + i].Offset; + snbFile.seek(self.blocks[self.binBlock + i].Offset); + try: + data = snbFile.read(bSize) + uncompressedData += bzdc.decompress(data) + except EOFError, e: + print e + f.fileBody = uncompressedData[plainPos:plainPos+f.fileSize] + plainPos += f.fileSize + elif f.attr & 0x01000000 == 0x01000000: + # Binary Files + snbFile.seek(44 + self.vfatCompressed + binPos) + f.fileBody = snbFile.read(f.fileSize) + binPos += f.fileSize + else: + print f.attr, f.fileName + raise Exception("Invalid file") + snbFile.close() + + def ParseFile(self, vfat, fileCount): + fileNames = vfat[fileCount*12:].split('\0'); + for i in range(fileCount): + f = FileStream() + (f.attr, f.fileNameOffset, f.fileSize) = struct.unpack('>iii', vfat[i * 12 : (i+1)*12]) + f.fileName = fileNames[i] + self.files.append(f) + + def ParseTail(self, vtail, fileCount): + self.binBlock = (self.binStreamSize + 0x8000 - 1) / 0x8000; + self.plainBlock = (self.plainStreamSizeUncompressed + 0x8000 - 1) / 0x8000; + for i in range(self.binBlock + self.plainBlock): + block = BlockData() + (block.Offset,) = struct.unpack('>i', vtail[i * 4 : (i+1) * 4]) + self.blocks.append(block) + for i in range(fileCount): + (self.files[i].blockIndex, self.files[i].contentOffset) = struct.unpack('>ii', vtail[(self.binBlock + self.plainBlock) * 4 + i * 8 : (self.binBlock + self.plainBlock) * 4 + (i+1) * 8]) + + def IsValid(self): + if self.magic != SNBFile.MAGIC: + return False + if self.rev80 != SNBFile.REV80: + return False + if self.revA3 != SNBFile.REVA3: + return False + if self.revZ1 != SNBFile.REVZ1: + return False + if self.revZ2 != SNBFile.REVZ2: + return False + if self.vfatSize != len(self.vfat): + return False + if self.fileCount != len(self.files): + return False + if (self.binBlock + self.plainBlock) * 4 + self.fileCount * 8 != self.tailSizeUncompressed: + return False + if self.tailMagic != SNBFile.MAGIC: + print self.tailMagic + return False + return True + + def FromDir(self, tdir): + for root, dirs, files in os.walk(tdir): + for name in files: + print name + p, ext = os.path.splitext(name) + if ext in [ ".snbf", ".snbc" ]: + self.AppendPlain(os.path.relpath(os.path.join(root, name), tdir), tdir) + else: + self.AppendBinary(os.path.relpath(os.path.join(root, name), tdir), tdir) + + def AppendPlain(self, fileName, tdir): + f = FileStream() + f.attr = 0x41000000 + f.fileSize = os.path.getsize(os.path.join(tdir,fileName)) + f.fileBody = open(os.path.join(tdir,fileName), 'rb').read() + f.fileName = fileName + print f.fileSize + self.files.append(f) + + def AppendBinary(self, fileName, tdir): + f = FileStream() + f.attr = 0x01000000 + f.fileSize = os.path.getsize(os.path.join(tdir,fileName)) + f.fileBody = open(os.path.join(tdir,fileName), 'rb').read() + f.fileName = fileName + print f.fileSize + self.files.append(f) + + def Output(self, outputFile): + + # Sort the files in file buffer, + # requried by the SNB file format + self.files.sort(compareFileStream) + + outputFile = open(outputFile, 'wb') + # File header part 1 + vmbrp1 = struct.pack('>8siiii', SNBFile.MAGIC, SNBFile.REV80, SNBFile.REVA3, SNBFile.REVZ1, len(self.files)) + + # Create VFAT & file stream + vfat = '' + fileNameTable = '' + plainStream = '' + binStream = '' + for f in self.files: + vfat += struct.pack('>iii', f.attr, len(fileNameTable), f.fileSize); + fileNameTable += (f.fileName + '\0') + + if f.attr & 0x41000000 == 0x41000000: + # Plain Files + f.contentOffset = len(plainStream) + plainStream += f.fileBody + elif f.attr & 0x01000000 == 0x01000000: + # Binary Files + f.contentOffset = len(binStream) + binStream += f.fileBody + else: + print f.attr, f.fileName + raise Exception("Unknown file type") + vfatCompressed = zlib.compress(vfat+fileNameTable) + + # File header part 2 + vmbrp2 = struct.pack('>iiiii', len(vfat+fileNameTable), len(vfatCompressed), len(binStream), len(plainStream), SNBFile.REVZ2) + # Write header + outputFile.write(vmbrp1 + vmbrp2) + # Write vfat + outputFile.write(vfatCompressed) + + # Generate block information + binBlockOffset = 0x2C + len(vfatCompressed) + plainBlockOffset = binBlockOffset + len(binStream) + + binBlock = (len(binStream) + 0x8000 - 1) / 0x8000 + plainBlock = (len(plainStream) + 0x8000 - 1) / 0x8000 + + offset = 0 + tailBlock = '' + for i in range(binBlock): + tailBlock += struct.pack('>i', binBlockOffset + offset) + offset += 0x8000; + tailRec = '' + for f in self.files: + t = 0 + if f.IsBinary(): + t = 0 + else: + t = binBlock + tailRec += struct.pack('>ii', f.contentOffset / 0x8000 + t, f.contentOffset % 0x8000); + + # Write binary stream + outputFile.write(binStream) + + # Write plain stream + pos = 0 + offset = 0 + while pos < len(plainStream): + tailBlock += struct.pack('>i', plainBlockOffset + offset); + block = plainStream[pos:pos+0x8000]; + compressed = bz2.compress(block) + outputFile.write(compressed) + offset += len(compressed) + pos += 0x8000 + + # Write tail block + compressedTail = zlib.compress(tailBlock + tailRec) + outputFile.write(compressedTail) + + # Write tail pointer + veom = struct.pack('>ii', len(compressedTail), plainBlockOffset + offset) + outputFile.write(veom) + + # Write file end mark + outputFile.write(SNBFile.MAGIC); + + # Close + outputFile.close() + return + + def Dump(self): + print "File Name:\t", self.fileName + print "File Count:\t", self.fileCount + print "VFAT Size(Compressed):\t%d(%d)" % (self.vfatSize, self.vfatCompressed) + print "Binary Stream Size:\t", self.binStreamSize + print "Plain Stream Uncompressed Size:\t", self.plainStreamSizeUncompressed + print "Binary Block Count:\t", self.binBlock + print "Plain Block Count:\t", self.plainBlock + for i in range(self.fileCount): + print "File ", i + f = self.files[i] + print "File Name: ", f.fileName + print "File Attr: ", f.attr + print "File Size: ", f.fileSize + print "Block Index: ", f.blockIndex + print "Content Offset: ", f.contentOffset + tempFile = open("/tmp/" + f.fileName, 'wb') + tempFile.write(f.fileBody) + tempFile.close() + +def usage(): + print "This unit test is for INTERNAL usage only!" + print "This unit test accept two parameters." + print "python snbfile.py " + print "The input file will be extracted and write to dest file. " + print "Meta data of the file will be shown during this process." + +def main(): + if len(sys.argv) != 3: + usage() + sys.exit(0) + inputFile = sys.argv[1] + outputFile = sys.argv[2] + + print "Input file: ", inputFile + print "Output file: ", outputFile + + snbFile = SNBFile(inputFile) + if snbFile.IsValid(): + snbFile.Dump() + snbFile.Output(outputFile) + else: + print "The input file is invalid." + return 1 + return 0 + +if __name__ == "__main__": + """SNB file unit test""" + sys.exit(main()) diff --git a/src/calibre/ebooks/snb/snbml.py b/src/calibre/ebooks/snb/snbml.py new file mode 100644 index 0000000000..e1956b5937 --- /dev/null +++ b/src/calibre/ebooks/snb/snbml.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- + +__license__ = 'GPL 3' +__copyright__ = '2010, Li Fanxi ' +__docformat__ = 'restructuredtext en' + +''' +Transform OEB content into SNB format +''' + +import os +import re + +from lxml import etree + +from calibre.ebooks.oeb.base import XHTML, XHTML_NS, barename, namespace +from calibre.ebooks.oeb.stylizer import Stylizer + +def ProcessFileName(fileName): + # Flat the path + fileName = fileName.replace("/", "_").replace(os.sep, "_") + # Handle bookmark for HTML file + fileName = fileName.replace("#", "_") + # Make it lower case + fileName = fileName.lower() + # Change extension from jpeg to jpg + root, ext = os.path.splitext(fileName) + if ext in [ '.jpeg', '.jpg', '.gif', '.svg' ]: + fileName = root + '.png' + return fileName + + +BLOCK_TAGS = [ + 'div', + 'p', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'li', + 'tr', +] + +BLOCK_STYLES = [ + 'block', +] + +SPACE_TAGS = [ + 'td', +] + +CLIABRE_SNB_IMG_TAG = "" + +class SNBMLizer(object): + + curSubItem = "" + curText = [ ] + + def __init__(self, log): + self.log = log + + def extract_content(self, oeb_book, item, subitems, opts): + self.log.info('Converting XHTML to SNBC...') + self.oeb_book = oeb_book + self.opts = opts + self.item = item + self.subitems = subitems + return self.mlize(); + + + def mlize(self): + stylizer = Stylizer(self.item.data, self.item.href, self.oeb_book, self.opts, self.opts.output_profile) + content = unicode(etree.tostring(self.item.data.find(XHTML('body')), encoding=unicode)) + content = self.remove_newlines(content) + trees = { } + for subitem, subtitle in self.subitems: + snbcTree = etree.Element("snbc") + etree.SubElement(etree.SubElement(snbcTree, "head"), "title").text = subtitle + etree.SubElement(snbcTree, "body") + trees[subitem] = snbcTree + + self.dump_text(trees, self.subitems, etree.fromstring(content), stylizer) + self.Output(trees) + return trees + + def remove_newlines(self, text): + self.log.debug('\tRemove newlines for processing...') + text = text.replace('\r\n', ' ') + text = text.replace('\n', ' ') + text = text.replace('\r', ' ') + + return text + + def dump_text(self, trees, subitems, elem, stylizer, end=''): + ''' + @elem: The element in the etree that we are working on. + @stylizer: The style information attached to the element. + @end: The last two characters of the text from the previous element. + This is used to determine if a blank line is needed when starting + a new block element. + ''' + if not isinstance(elem.tag, basestring) \ + or namespace(elem.tag) != XHTML_NS: + return [''] + + if elem.attrib.get('id') != None and elem.attrib['id'] in [ href for href, title in subitems ]: + if self.curSubItem != None and self.curSubItem != elem.attrib['id']: + self.Output(trees) + self.curSubItem = elem.attrib['id'] + self.curText = [ ] + + style = stylizer.style(elem) + + if style['display'] in ('none', 'oeb-page-head', 'oeb-page-foot') \ + or style['visibility'] == 'hidden': + return [''] + + tag = barename(elem.tag) + in_block = False + + # Are we in a paragraph block? + if tag in BLOCK_TAGS or style['display'] in BLOCK_STYLES: + in_block = True + if not end.endswith(u'\n\n') and hasattr(elem, 'text') and elem.text: + self.curText.append(u'\n\n') + + if tag in SPACE_TAGS: + if not end.endswith('u ') and hasattr(elem, 'text') and elem.text: + self.curText.append(u' ') + + if tag == 'img': + self.curText.append(u'%s%s' % (CLIABRE_SNB_IMG_TAG, ProcessFileName(elem.attrib['src']))) + + # Process tags that contain text. + if hasattr(elem, 'text') and elem.text: + self.curText.append(elem.text) + + for item in elem: + en = u'' + if len(self.curText) >= 2: + en = self.curText[-1][-2:] + self.dump_text(trees, subitems, item, stylizer, en) + + if in_block: + self.curText.append(u'\n\n') + + if hasattr(elem, 'tail') and elem.tail: + self.curText.append(elem.tail) + + def Output(self, trees): + if self.curSubItem == None or not self.curSubItem in trees: + return + for t in self.curText: + if len(t.strip(' \t\n\r')) != 0: + if t.find(CLIABRE_SNB_IMG_TAG) == 0: + etree.SubElement(trees[self.curSubItem], "img").text = t[len(CLIABRE_SNB_IMG_TAG):] + else: + etree.SubElement(trees[self.curSubItem], "text").text = etree.CDATA(unicode('' + t)) From 603b8811893fc080b02b182e52b20cbf0aab8f33 Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Sat, 9 Oct 2010 23:04:03 +0800 Subject: [PATCH 03/75] [SNBOutputProfile] Add a Bambook Output Profile --- src/calibre/customize/profiles.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/calibre/customize/profiles.py b/src/calibre/customize/profiles.py index 0310f09242..937cb9c3b4 100644 --- a/src/calibre/customize/profiles.py +++ b/src/calibre/customize/profiles.py @@ -642,11 +642,24 @@ class NookOutput(OutputProfile): fbase = 16 fsizes = [12, 12, 14, 16, 18, 20, 22, 24] +class BambookOutput(OutputProfile): + + name = 'Sanda Bambook' + short_name = 'bambook' + description = _('This profile is intended for the Sanda Bambook.') + + # Screen size is a best guess + screen_size = (800, 600) + dpi = 168.451 + fbase = 12 + fsizes = [10, 12, 14, 16] + output_profiles = [OutputProfile, SonyReaderOutput, SonyReader300Output, SonyReader900Output, MSReaderOutput, MobipocketOutput, HanlinV3Output, HanlinV5Output, CybookG3Output, CybookOpusOutput, KindleOutput, iPadOutput, KoboReaderOutput, SonyReaderLandscapeOutput, KindleDXOutput, IlliadOutput, - IRexDR1000Output, IRexDR800Output, JetBook5Output, NookOutput,] + IRexDR1000Output, IRexDR800Output, JetBook5Output, NookOutput, + BambookOutput, ] output_profiles.sort(cmp=lambda x,y:cmp(x.name.lower(), y.name.lower())) From 6a301a1a9a97a7bb769b05f12b5574e9e83a5116 Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Sun, 10 Oct 2010 11:27:27 +0800 Subject: [PATCH 04/75] [SNBOutput] Add two spaces for each paragraph. --- src/calibre/ebooks/snb/snbml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/ebooks/snb/snbml.py b/src/calibre/ebooks/snb/snbml.py index e1956b5937..15f3413489 100644 --- a/src/calibre/ebooks/snb/snbml.py +++ b/src/calibre/ebooks/snb/snbml.py @@ -157,4 +157,4 @@ class SNBMLizer(object): if t.find(CLIABRE_SNB_IMG_TAG) == 0: etree.SubElement(trees[self.curSubItem], "img").text = t[len(CLIABRE_SNB_IMG_TAG):] else: - etree.SubElement(trees[self.curSubItem], "text").text = etree.CDATA(unicode('' + t)) + etree.SubElement(trees[self.curSubItem], "text").text = etree.CDATA(unicode(u'\u3000\u3000' + t)) From 8fd3f0ebaab8f9f104842e671e1e2312695780d4 Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Sun, 10 Oct 2010 11:28:43 +0800 Subject: [PATCH 05/75] [SNBOutput] Fix the path error on different OSes. --- src/calibre/ebooks/snb/snbfile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/snb/snbfile.py b/src/calibre/ebooks/snb/snbfile.py index aa690fb92b..6d2c627fbb 100644 --- a/src/calibre/ebooks/snb/snbfile.py +++ b/src/calibre/ebooks/snb/snbfile.py @@ -147,7 +147,7 @@ class SNBFile: f.attr = 0x41000000 f.fileSize = os.path.getsize(os.path.join(tdir,fileName)) f.fileBody = open(os.path.join(tdir,fileName), 'rb').read() - f.fileName = fileName + f.fileName = fileName.replace(os.sep, '/') print f.fileSize self.files.append(f) @@ -156,7 +156,7 @@ class SNBFile: f.attr = 0x01000000 f.fileSize = os.path.getsize(os.path.join(tdir,fileName)) f.fileBody = open(os.path.join(tdir,fileName), 'rb').read() - f.fileName = fileName + f.fileName = fileName.replace(os.sep, '/') print f.fileSize self.files.append(f) From 0c387834f43b108ee02f839d1122e44f6758a4d6 Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Sun, 10 Oct 2010 16:25:22 +0800 Subject: [PATCH 06/75] [SNBOutput] The conetent in html before the first bookmark should also be outputted. --- src/calibre/ebooks/snb/output.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/calibre/ebooks/snb/output.py b/src/calibre/ebooks/snb/output.py index a02d085b5e..65f06c7994 100644 --- a/src/calibre/ebooks/snb/output.py +++ b/src/calibre/ebooks/snb/output.py @@ -133,9 +133,6 @@ class SNBOutput(OutputFormatPlugin): oeb_book.toc.add(_('Start'), first.href) for tocitem in oeb_book.toc: - ch = etree.SubElement(tocBody, "chapter") - ch.set("src", ProcessFileName(tocitem.href) + ".snbc") - ch.text = tocitem.title if tocitem.href.find('#') != -1: item = string.split(tocitem.href, '#') if len(item) != 2: @@ -145,6 +142,11 @@ class SNBOutput(OutputFormatPlugin): outputFiles[item[0]].append((item[1], tocitem.title)) else: outputFiles[item[0]] = [] + if not "" in outputFiles[item[0]]: + outputFiles[item[0]].append(("", _("Start"))) + ch = etree.SubElement(tocBody, "chapter") + ch.set("src", ProcessFileName(item[0]) + ".snbc") + ch.text = _("Start") outputFiles[item[0]].append((item[1], tocitem.title)) else: if tocitem.href in outputFiles: @@ -152,6 +154,10 @@ class SNBOutput(OutputFormatPlugin): else: outputFiles[tocitem.href] = [] outputFiles[tocitem.href].append(("", tocitem.title)) + ch = etree.SubElement(tocBody, "chapter") + ch.set("src", ProcessFileName(tocitem.href) + ".snbc") + ch.text = tocitem.title + etree.SubElement(tocHead, "chapters").text = '%d' % len(tocBody) From b4c69ba6343cae44110ea28e364d82ba57c313ae Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Sun, 10 Oct 2010 17:47:23 +0800 Subject: [PATCH 07/75] [SNBOutput] Reduce the size of the images --- src/calibre/ebooks/snb/output.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/calibre/ebooks/snb/output.py b/src/calibre/ebooks/snb/output.py index 65f06c7994..08ede2ca37 100644 --- a/src/calibre/ebooks/snb/output.py +++ b/src/calibre/ebooks/snb/output.py @@ -21,7 +21,7 @@ def ProcessFileName(fileName): fileName = fileName.replace("#", "_") # Make it lower case fileName = fileName.lower() - # Change extension from jpeg to jpg + # Change extension for image files to png root, ext = os.path.splitext(fileName) if ext in [ '.jpeg', '.jpg', '.gif', '.svg' ]: fileName = root + '.png' @@ -187,11 +187,8 @@ class SNBOutput(OutputFormatPlugin): log.debug('Converting image: %s ...' % item.href) content = m.hrefs[item.href].data if m.hrefs[item.href].media_type != PNG_MIME: - # Convert - from calibre.utils.magick import Image - img = Image() - img.load(content) - img.save(os.path.join(snbiDir, ProcessFileName(item.href))) + # Convert & Resize image + self.HandleImage(content, os.path.join(snbiDir, ProcessFileName(item.href))) else: outputFile = open(os.path.join(snbiDir, ProcessFileName(item.href)), 'wb') outputFile.write(content) @@ -202,6 +199,29 @@ class SNBOutput(OutputFormatPlugin): snbFile.FromDir(tdir) snbFile.Output(output_path) + def HandleImage(self, imageData, imagePath): + from calibre.utils.magick import Image + img = Image() + img.load(imageData) + print img.size + (x,y) = img.size + # TODO use the data from device profile + SCREEN_X = 540 + SCREEN_Y = 700 + # Handle big image only + if x > SCREEN_X or y > SCREEN_Y: + SCREEN_RATIO = float(SCREEN_Y) / SCREEN_X + imgRatio = float(y) / x + xScale = float(x) / SCREEN_X + yScale = float(y) / SCREEN_Y + scale = max(xScale, yScale) + # TODO : intelligent image rotation + # img = img.rotate(90) + # x,y = y,x + img.size = (x / scale, y / scale) + print img.size + img.save(imagePath) + if __name__ == '__main__': from calibre.ebooks.oeb.reader import OEBReader from calibre.ebooks.oeb.base import OEBBook From e72c3ce0f83532688d26bda3d3fefb6e4e901729 Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Sun, 10 Oct 2010 19:40:55 +0800 Subject: [PATCH 08/75] [[SNBOutput] Reuse the original html->txt algorithm in txtml.py to get better output. Removed some unnecessary debug prints. --- src/calibre/ebooks/snb/output.py | 2 - src/calibre/ebooks/snb/snbfile.py | 3 - src/calibre/ebooks/snb/snbml.py | 134 ++++++++++++++++++++++-------- 3 files changed, 100 insertions(+), 39 deletions(-) diff --git a/src/calibre/ebooks/snb/output.py b/src/calibre/ebooks/snb/output.py index 08ede2ca37..a682062de2 100644 --- a/src/calibre/ebooks/snb/output.py +++ b/src/calibre/ebooks/snb/output.py @@ -203,7 +203,6 @@ class SNBOutput(OutputFormatPlugin): from calibre.utils.magick import Image img = Image() img.load(imageData) - print img.size (x,y) = img.size # TODO use the data from device profile SCREEN_X = 540 @@ -219,7 +218,6 @@ class SNBOutput(OutputFormatPlugin): # img = img.rotate(90) # x,y = y,x img.size = (x / scale, y / scale) - print img.size img.save(imagePath) if __name__ == '__main__': diff --git a/src/calibre/ebooks/snb/snbfile.py b/src/calibre/ebooks/snb/snbfile.py index 6d2c627fbb..ca10f800c7 100644 --- a/src/calibre/ebooks/snb/snbfile.py +++ b/src/calibre/ebooks/snb/snbfile.py @@ -135,7 +135,6 @@ class SNBFile: def FromDir(self, tdir): for root, dirs, files in os.walk(tdir): for name in files: - print name p, ext = os.path.splitext(name) if ext in [ ".snbf", ".snbc" ]: self.AppendPlain(os.path.relpath(os.path.join(root, name), tdir), tdir) @@ -148,7 +147,6 @@ class SNBFile: f.fileSize = os.path.getsize(os.path.join(tdir,fileName)) f.fileBody = open(os.path.join(tdir,fileName), 'rb').read() f.fileName = fileName.replace(os.sep, '/') - print f.fileSize self.files.append(f) def AppendBinary(self, fileName, tdir): @@ -157,7 +155,6 @@ class SNBFile: f.fileSize = os.path.getsize(os.path.join(tdir,fileName)) f.fileBody = open(os.path.join(tdir,fileName), 'rb').read() f.fileName = fileName.replace(os.sep, '/') - print f.fileSize self.files.append(f) def Output(self, outputFile): diff --git a/src/calibre/ebooks/snb/snbml.py b/src/calibre/ebooks/snb/snbml.py index 15f3413489..c357971b5e 100644 --- a/src/calibre/ebooks/snb/snbml.py +++ b/src/calibre/ebooks/snb/snbml.py @@ -51,12 +51,13 @@ SPACE_TAGS = [ 'td', ] -CLIABRE_SNB_IMG_TAG = "" +CALIBRE_SNB_IMG_TAG = "<$$calibre_snb_temp_img$$>" +CALIBRE_SNB_BM_TAG = "<$$calibre_snb_bm_tag$$>" class SNBMLizer(object): curSubItem = "" - curText = [ ] +# curText = [ ] def __init__(self, log): self.log = log @@ -71,6 +72,7 @@ class SNBMLizer(object): def mlize(self): + output = [ u'' ] stylizer = Stylizer(self.item.data, self.item.href, self.oeb_book, self.opts, self.opts.output_profile) content = unicode(etree.tostring(self.item.data.find(XHTML('body')), encoding=unicode)) content = self.remove_newlines(content) @@ -80,9 +82,20 @@ class SNBMLizer(object): etree.SubElement(etree.SubElement(snbcTree, "head"), "title").text = subtitle etree.SubElement(snbcTree, "body") trees[subitem] = snbcTree + output.append(u'%s%s\n\n' % (CALIBRE_SNB_BM_TAG, "")) + output += self.dump_text(self.subitems, etree.fromstring(content), stylizer) + output = self.cleanup_text(u''.join(output)) - self.dump_text(trees, self.subitems, etree.fromstring(content), stylizer) - self.Output(trees) + subitem = '' + for line in output.splitlines(): + line = line.strip(' \t\n\r') + if len(line) != 0: + if line.find(CALIBRE_SNB_IMG_TAG) == 0: + etree.SubElement(trees[subitem], "img").text = line[len(CALIBRE_SNB_IMG_TAG):] + elif line.find(CALIBRE_SNB_BM_TAG) == 0: + subitem = line[len(CALIBRE_SNB_BM_TAG):] + else: + etree.SubElement(trees[subitem], "text").text = etree.CDATA(unicode(u'\u3000\u3000' + line)) return trees def remove_newlines(self, text): @@ -93,25 +106,86 @@ class SNBMLizer(object): return text - def dump_text(self, trees, subitems, elem, stylizer, end=''): - ''' - @elem: The element in the etree that we are working on. - @stylizer: The style information attached to the element. - @end: The last two characters of the text from the previous element. - This is used to determine if a blank line is needed when starting - a new block element. - ''' + def cleanup_text(self, text): + self.log.debug('\tClean up text...') + # Replace bad characters. + text = text.replace(u'\xc2', '') + text = text.replace(u'\xa0', ' ') + text = text.replace(u'\xa9', '(C)') + + # Replace tabs, vertical tags and form feeds with single space. + text = text.replace('\t+', ' ') + text = text.replace('\v+', ' ') + text = text.replace('\f+', ' ') + + # Single line paragraph. + text = re.sub('(?<=.)%s(?=.)' % os.linesep, ' ', text) + + # Remove multiple spaces. + text = re.sub('[ ]{2,}', ' ', text) + + # Remove excessive newlines. + text = re.sub('\n[ ]+\n', '\n\n', text) + if self.opts.remove_paragraph_spacing: + text = re.sub('\n{2,}', '\n', text) + text = re.sub('(?imu)^(?=.)', '\t', text) + else: + text = re.sub('\n{3,}', '\n\n', text) + + # Replace spaces at the beginning and end of lines + text = re.sub('(?imu)^[ ]+', '', text) + text = re.sub('(?imu)[ ]+$', '', text) + + if self.opts.max_line_length: + max_length = self.opts.max_line_length + if self.opts.max_line_length < 25 and not self.opts.force_max_line_length: + max_length = 25 + short_lines = [] + lines = text.splitlines() + for line in lines: + while len(line) > max_length: + space = line.rfind(' ', 0, max_length) + if space != -1: + # Space was found. + short_lines.append(line[:space]) + line = line[space + 1:] + else: + # Space was not found. + if self.opts.force_max_line_length: + # Force breaking at max_lenght. + short_lines.append(line[:max_length]) + line = line[max_length:] + else: + # Look for the first space after max_length. + space = line.find(' ', max_length, len(line)) + if space != -1: + # Space was found. + short_lines.append(line[:space]) + line = line[space + 1:] + else: + # No space was found cannot break line. + short_lines.append(line) + line = '' + # Add the text that was less than max_lengh to the list + short_lines.append(line) + text = '\n'.join(short_lines) + + return text + + def dump_text(self, subitems, elem, stylizer, end=''): + if not isinstance(elem.tag, basestring) \ or namespace(elem.tag) != XHTML_NS: return [''] + + text = [''] + style = stylizer.style(elem) + if elem.attrib.get('id') != None and elem.attrib['id'] in [ href for href, title in subitems ]: if self.curSubItem != None and self.curSubItem != elem.attrib['id']: - self.Output(trees) self.curSubItem = elem.attrib['id'] - self.curText = [ ] - - style = stylizer.style(elem) + text.append(u'%s%s\n\n' % (CALIBRE_SNB_BM_TAG, self.curSubItem)) if style['display'] in ('none', 'oeb-page-head', 'oeb-page-foot') \ or style['visibility'] == 'hidden': @@ -124,37 +198,29 @@ class SNBMLizer(object): if tag in BLOCK_TAGS or style['display'] in BLOCK_STYLES: in_block = True if not end.endswith(u'\n\n') and hasattr(elem, 'text') and elem.text: - self.curText.append(u'\n\n') + text.append(u'\n\n') if tag in SPACE_TAGS: if not end.endswith('u ') and hasattr(elem, 'text') and elem.text: - self.curText.append(u' ') + text.append(u' ') if tag == 'img': - self.curText.append(u'%s%s' % (CLIABRE_SNB_IMG_TAG, ProcessFileName(elem.attrib['src']))) + text.append(u'%s%s' % (CALIBRE_SNB_IMG_TAG, ProcessFileName(elem.attrib['src']))) # Process tags that contain text. if hasattr(elem, 'text') and elem.text: - self.curText.append(elem.text) + text.append(elem.text) for item in elem: en = u'' - if len(self.curText) >= 2: - en = self.curText[-1][-2:] - self.dump_text(trees, subitems, item, stylizer, en) + if len(text) >= 2: + en = text[-1][-2:] + text += self.dump_text(subitems, item, stylizer, en) if in_block: - self.curText.append(u'\n\n') + text.append(u'\n\n') if hasattr(elem, 'tail') and elem.tail: - self.curText.append(elem.tail) + text.append(elem.tail) - def Output(self, trees): - if self.curSubItem == None or not self.curSubItem in trees: - return - for t in self.curText: - if len(t.strip(' \t\n\r')) != 0: - if t.find(CLIABRE_SNB_IMG_TAG) == 0: - etree.SubElement(trees[self.curSubItem], "img").text = t[len(CLIABRE_SNB_IMG_TAG):] - else: - etree.SubElement(trees[self.curSubItem], "text").text = etree.CDATA(unicode(u'\u3000\u3000' + t)) + return text From 081c5385f2862be4d0a7e73f76b9c8f9477156ae Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Sun, 10 Oct 2010 21:24:27 +0800 Subject: [PATCH 09/75] [SNBOutput] Improve TOC handling. If an spice is not referenced in TOC, it will be appended to the last TOC item. --- src/calibre/ebooks/snb/output.py | 51 +++++++++++++++++++++++++------- src/calibre/ebooks/snb/snbml.py | 13 ++++++-- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/src/calibre/ebooks/snb/output.py b/src/calibre/ebooks/snb/output.py index a682062de2..c8457347ec 100644 --- a/src/calibre/ebooks/snb/output.py +++ b/src/calibre/ebooks/snb/output.py @@ -131,6 +131,10 @@ class SNBOutput(OutputFormatPlugin): 'Creating a default TOC') first = iter(oeb_book.spine).next() oeb_book.toc.add(_('Start'), first.href) + else: + first = iter(oeb_book.spine).next() + if oeb_book.toc[0].href != first.href: + oeb_book.toc.add(_('Start'), first.href) for tocitem in oeb_book.toc: if tocitem.href.find('#') != -1: @@ -166,22 +170,49 @@ class SNBOutput(OutputFormatPlugin): tocInfoFile.close() # Output Files + oldTree = None + mergeLast = False + lastName = None for item in s: from calibre.ebooks.oeb.base import OEB_DOCS, OEB_IMAGES, PNG_MIME if m.hrefs[item.href].media_type in OEB_DOCS: if not item.href in outputFiles: - log.debug('Skipping %s because unused in TOC.' % item.href) - continue + log.debug('File %s is unused in TOC. Continue in last chapter' % item.href) + mergeLast = True + else: + log.debug('Output the modified chapter again: %s' % lastName) + if oldTree != None and mergeLast: + outputFile = open(os.path.join(snbcDir, lastName), 'wb') + outputFile.write(etree.tostring(oldTree, pretty_print=True, encoding='utf-8')) + outputFile.close() + mergeLast = False + log.debug('Converting %s to snbc...' % item.href) snbwriter = SNBMLizer(log) - snbcTrees = snbwriter.extract_content(oeb_book, item, outputFiles[item.href], opts) - for subName in snbcTrees: - postfix = '' - if subName != '': - postfix = '_' + subName - outputFile = open(os.path.join(snbcDir, ProcessFileName(item.href + postfix + ".snbc")), 'wb') - outputFile.write(etree.tostring(snbcTrees[subName], pretty_print=True, encoding='utf-8')) - outputFile.close() + snbcTrees = None + if not mergeLast: + snbcTrees = snbwriter.extract_content(oeb_book, item, outputFiles[item.href], opts) + for subName in snbcTrees: + postfix = '' + if subName != '': + postfix = '_' + subName + lastName = ProcessFileName(item.href + postfix + ".snbc") + oldTree = snbcTrees[subName] + outputFile = open(os.path.join(snbcDir, lastName), 'wb') + outputFile.write(etree.tostring(oldTree, pretty_print=True, encoding='utf-8')) + outputFile.close() + else: + log.debug('Merge %s with last TOC item...' % item.href) + snbwriter.merge_content(oldTree, oeb_book, item, [('', _("Start"))], opts) + + # Output the last one if needed + log.debug('Output the last modified chapter again: %s' % lastName) + if oldTree != None and mergeLast: + outputFile = open(os.path.join(snbcDir, lastName), 'wb') + outputFile.write(etree.tostring(oldTree, pretty_print=True, encoding='utf-8')) + outputFile.close() + mergeLast = False + for item in m: if m.hrefs[item.href].media_type in OEB_IMAGES: log.debug('Converting image: %s ...' % item.href) diff --git a/src/calibre/ebooks/snb/snbml.py b/src/calibre/ebooks/snb/snbml.py index c357971b5e..bfdaf53cae 100644 --- a/src/calibre/ebooks/snb/snbml.py +++ b/src/calibre/ebooks/snb/snbml.py @@ -70,6 +70,14 @@ class SNBMLizer(object): self.subitems = subitems return self.mlize(); + def merge_content(self, old_tree, oeb_book, item, subitems, opts): + newTrees = self.extract_content(oeb_book, item, subitems, opts) + body = old_tree.find(".//body") + if body != None: + for subName in newTrees: + newbody = newTrees[subName].find(".//body") + for entity in newbody: + body.append(entity) def mlize(self): output = [ u'' ] @@ -91,11 +99,12 @@ class SNBMLizer(object): line = line.strip(' \t\n\r') if len(line) != 0: if line.find(CALIBRE_SNB_IMG_TAG) == 0: - etree.SubElement(trees[subitem], "img").text = line[len(CALIBRE_SNB_IMG_TAG):] + etree.SubElement(trees[subitem].find(".//body"), "img").text = line[len(CALIBRE_SNB_IMG_TAG):] elif line.find(CALIBRE_SNB_BM_TAG) == 0: subitem = line[len(CALIBRE_SNB_BM_TAG):] else: - etree.SubElement(trees[subitem], "text").text = etree.CDATA(unicode(u'\u3000\u3000' + line)) + etree.SubElement(trees[subitem].find(".//body"), "text").text = \ + etree.CDATA(unicode(u'\u3000\u3000' + line)) return trees def remove_newlines(self, text): From 9be246500bfc1534d953753e56c338941886c9fc Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Mon, 11 Oct 2010 00:19:12 +0800 Subject: [PATCH 10/75] [SNBOutput] Removed a duplicated function. --- src/calibre/ebooks/snb/output.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/calibre/ebooks/snb/output.py b/src/calibre/ebooks/snb/output.py index c8457347ec..bd27a0614e 100644 --- a/src/calibre/ebooks/snb/output.py +++ b/src/calibre/ebooks/snb/output.py @@ -12,21 +12,7 @@ from calibre.ptempfile import TemporaryDirectory from calibre.constants import __appname__, __version__ from calibre.ebooks.oeb.base import XHTML, XHTML_NS, barename, namespace from calibre.ebooks.snb.snbfile import SNBFile -from calibre.ebooks.snb.snbml import SNBMLizer - -def ProcessFileName(fileName): - # Flat the path - fileName = fileName.replace("/", "_").replace(os.sep, "_") - # Handle bookmark for HTML file - fileName = fileName.replace("#", "_") - # Make it lower case - fileName = fileName.lower() - # Change extension for image files to png - root, ext = os.path.splitext(fileName) - if ext in [ '.jpeg', '.jpg', '.gif', '.svg' ]: - fileName = root + '.png' - return fileName - +from calibre.ebooks.snb.snbml import SNBMLizer, ProcessFileName class SNBOutput(OutputFormatPlugin): From 29e133c61a9f6d813b4137fd94f4c8305c4de4f5 Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Mon, 11 Oct 2010 00:41:24 +0800 Subject: [PATCH 11/75] [SNBOutput] Fixed a bug in image path handling. --- src/calibre/ebooks/snb/snbml.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/snb/snbml.py b/src/calibre/ebooks/snb/snbml.py index bfdaf53cae..72600fa4d2 100644 --- a/src/calibre/ebooks/snb/snbml.py +++ b/src/calibre/ebooks/snb/snbml.py @@ -99,7 +99,13 @@ class SNBMLizer(object): line = line.strip(' \t\n\r') if len(line) != 0: if line.find(CALIBRE_SNB_IMG_TAG) == 0: - etree.SubElement(trees[subitem].find(".//body"), "img").text = line[len(CALIBRE_SNB_IMG_TAG):] + prefix = ProcessFileName(os.path.dirname(self.item.href)) + if prefix != '': + etree.SubElement(trees[subitem].find(".//body"), "img").text = \ + prefix + '_' + line[len(CALIBRE_SNB_IMG_TAG):] + else: + etree.SubElement(trees[subitem].find(".//body"), "img").text = \ + line[len(CALIBRE_SNB_IMG_TAG):] elif line.find(CALIBRE_SNB_BM_TAG) == 0: subitem = line[len(CALIBRE_SNB_BM_TAG):] else: From 3ec09a3b40ac6eb637fb635d9ad2f81cff6d06c5 Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Mon, 11 Oct 2010 00:49:11 +0800 Subject: [PATCH 12/75] [SBNOutput] Use jpg instead of png. --- src/calibre/ebooks/snb/output.py | 13 ++++--------- src/calibre/ebooks/snb/snbml.py | 4 ++-- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/calibre/ebooks/snb/output.py b/src/calibre/ebooks/snb/output.py index bd27a0614e..7dd976ff25 100644 --- a/src/calibre/ebooks/snb/output.py +++ b/src/calibre/ebooks/snb/output.py @@ -160,7 +160,7 @@ class SNBOutput(OutputFormatPlugin): mergeLast = False lastName = None for item in s: - from calibre.ebooks.oeb.base import OEB_DOCS, OEB_IMAGES, PNG_MIME + from calibre.ebooks.oeb.base import OEB_DOCS, OEB_IMAGES if m.hrefs[item.href].media_type in OEB_DOCS: if not item.href in outputFiles: log.debug('File %s is unused in TOC. Continue in last chapter' % item.href) @@ -203,14 +203,9 @@ class SNBOutput(OutputFormatPlugin): if m.hrefs[item.href].media_type in OEB_IMAGES: log.debug('Converting image: %s ...' % item.href) content = m.hrefs[item.href].data - if m.hrefs[item.href].media_type != PNG_MIME: - # Convert & Resize image - self.HandleImage(content, os.path.join(snbiDir, ProcessFileName(item.href))) - else: - outputFile = open(os.path.join(snbiDir, ProcessFileName(item.href)), 'wb') - outputFile.write(content) - outputFile.close() - + # Convert & Resize image + self.HandleImage(content, os.path.join(snbiDir, ProcessFileName(item.href))) + # Package as SNB File snbFile = SNBFile() snbFile.FromDir(tdir) diff --git a/src/calibre/ebooks/snb/snbml.py b/src/calibre/ebooks/snb/snbml.py index 72600fa4d2..7be15d9fc6 100644 --- a/src/calibre/ebooks/snb/snbml.py +++ b/src/calibre/ebooks/snb/snbml.py @@ -23,10 +23,10 @@ def ProcessFileName(fileName): fileName = fileName.replace("#", "_") # Make it lower case fileName = fileName.lower() - # Change extension from jpeg to jpg + # Change all images to jpg root, ext = os.path.splitext(fileName) if ext in [ '.jpeg', '.jpg', '.gif', '.svg' ]: - fileName = root + '.png' + fileName = root + '.jpg' return fileName From 3c0673dcf5627dce63d568da615fda9dc614f98e Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Mon, 11 Oct 2010 00:51:34 +0800 Subject: [PATCH 13/75] [SBNOutput] Change debug log position to avoid confusion. --- src/calibre/ebooks/snb/output.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/ebooks/snb/output.py b/src/calibre/ebooks/snb/output.py index 7dd976ff25..7b661dfe7f 100644 --- a/src/calibre/ebooks/snb/output.py +++ b/src/calibre/ebooks/snb/output.py @@ -166,8 +166,8 @@ class SNBOutput(OutputFormatPlugin): log.debug('File %s is unused in TOC. Continue in last chapter' % item.href) mergeLast = True else: - log.debug('Output the modified chapter again: %s' % lastName) if oldTree != None and mergeLast: + log.debug('Output the modified chapter again: %s' % lastName) outputFile = open(os.path.join(snbcDir, lastName), 'wb') outputFile.write(etree.tostring(oldTree, pretty_print=True, encoding='utf-8')) outputFile.close() From dafa2c9034962d87ca03c4fd6302d658413751cf Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Mon, 11 Oct 2010 01:14:58 +0800 Subject: [PATCH 14/75] [SNBOutput] Improved unused pages handling. --- src/calibre/ebooks/snb/output.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/calibre/ebooks/snb/output.py b/src/calibre/ebooks/snb/output.py index 7b661dfe7f..ef008013df 100644 --- a/src/calibre/ebooks/snb/output.py +++ b/src/calibre/ebooks/snb/output.py @@ -116,11 +116,19 @@ class SNBOutput(OutputFormatPlugin): log.warn('This SNB file has no Table of Contents. ' 'Creating a default TOC') first = iter(oeb_book.spine).next() - oeb_book.toc.add(_('Start'), first.href) + oeb_book.toc.add(_('Start Page'), first.href) else: first = iter(oeb_book.spine).next() if oeb_book.toc[0].href != first.href: - oeb_book.toc.add(_('Start'), first.href) + # The pages before the fist item in toc will be stored as + # "Cover Pages". + # oeb_book.toc does not support "insert", so we generate + # the tocInfoTree directly instead of modifying the toc + ch = etree.SubElement(tocBody, "chapter") + ch.set("src", ProcessFileName(first.href) + ".snbc") + ch.text = _('Cover Pages') + outputFiles[first.href] = [] + outputFiles[first.href].append(("", _("Cover Pages"))) for tocitem in oeb_book.toc: if tocitem.href.find('#') != -1: @@ -133,10 +141,10 @@ class SNBOutput(OutputFormatPlugin): else: outputFiles[item[0]] = [] if not "" in outputFiles[item[0]]: - outputFiles[item[0]].append(("", _("Start"))) + outputFiles[item[0]].append(("", _("Chapter Start"))) ch = etree.SubElement(tocBody, "chapter") ch.set("src", ProcessFileName(item[0]) + ".snbc") - ch.text = _("Start") + ch.text = _("Chapter Start") outputFiles[item[0]].append((item[1], tocitem.title)) else: if tocitem.href in outputFiles: From 63bb69d4ecf65834390887626ede389db76e15ef Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Mon, 11 Oct 2010 01:37:29 +0800 Subject: [PATCH 15/75] [SNBOutput] Change wording for the unused page content on each page before the first bookmark appeared in TOC. --- src/calibre/ebooks/snb/output.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/snb/output.py b/src/calibre/ebooks/snb/output.py index ef008013df..cbe785d384 100644 --- a/src/calibre/ebooks/snb/output.py +++ b/src/calibre/ebooks/snb/output.py @@ -141,10 +141,10 @@ class SNBOutput(OutputFormatPlugin): else: outputFiles[item[0]] = [] if not "" in outputFiles[item[0]]: - outputFiles[item[0]].append(("", _("Chapter Start"))) + outputFiles[item[0]].append(("", tocitem.title + _(" (Preface)"))) ch = etree.SubElement(tocBody, "chapter") ch.set("src", ProcessFileName(item[0]) + ".snbc") - ch.text = _("Chapter Start") + ch.text = tocitem.title + _(" (Preface)") outputFiles[item[0]].append((item[1], tocitem.title)) else: if tocitem.href in outputFiles: From 424e69f5999d367e3582510c56d2ad4b07402319 Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Mon, 11 Oct 2010 15:47:48 +0800 Subject: [PATCH 16/75] [SNBOutput] Also strip \u3000 (Full Mode space character in Chinese) --- src/calibre/ebooks/snb/snbml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/ebooks/snb/snbml.py b/src/calibre/ebooks/snb/snbml.py index 7be15d9fc6..a5659bdca2 100644 --- a/src/calibre/ebooks/snb/snbml.py +++ b/src/calibre/ebooks/snb/snbml.py @@ -96,7 +96,7 @@ class SNBMLizer(object): subitem = '' for line in output.splitlines(): - line = line.strip(' \t\n\r') + line = line.strip(u' \t\n\r\u3000') if len(line) != 0: if line.find(CALIBRE_SNB_IMG_TAG) == 0: prefix = ProcessFileName(os.path.dirname(self.item.href)) From e89a58d7095ebfb0bde8443ead635b1f8f13e9fb Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Mon, 11 Oct 2010 16:19:46 +0800 Subject: [PATCH 17/75] [SNBOutput] Handle
tag to be a new line in output. --- src/calibre/ebooks/snb/snbml.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/calibre/ebooks/snb/snbml.py b/src/calibre/ebooks/snb/snbml.py index a5659bdca2..9b2c24c758 100644 --- a/src/calibre/ebooks/snb/snbml.py +++ b/src/calibre/ebooks/snb/snbml.py @@ -222,6 +222,9 @@ class SNBMLizer(object): if tag == 'img': text.append(u'%s%s' % (CALIBRE_SNB_IMG_TAG, ProcessFileName(elem.attrib['src']))) + if tag == 'br': + text.append(u'\n\n') + # Process tags that contain text. if hasattr(elem, 'text') and elem.text: text.append(elem.text) From 30e231c4a69a01818823681375d9e031b333bce5 Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Mon, 11 Oct 2010 17:12:58 +0800 Subject: [PATCH 18/75] [SNBOutput] Handle
 tag in html.

---
 src/calibre/ebooks/snb/snbml.py | 29 +++++++++++++++++++++--------
 1 file changed, 21 insertions(+), 8 deletions(-)

diff --git a/src/calibre/ebooks/snb/snbml.py b/src/calibre/ebooks/snb/snbml.py
index 9b2c24c758..f32ddaadf1 100644
--- a/src/calibre/ebooks/snb/snbml.py
+++ b/src/calibre/ebooks/snb/snbml.py
@@ -53,6 +53,7 @@ SPACE_TAGS = [
 
 CALIBRE_SNB_IMG_TAG = "<$$calibre_snb_temp_img$$>"
 CALIBRE_SNB_BM_TAG = "<$$calibre_snb_bm_tag$$>"
+CALIBRE_SNB_PRE_TAG = "<$$calibre_snb_pre_tag$$>"
 
 class SNBMLizer(object):
     
@@ -83,7 +84,7 @@ class SNBMLizer(object):
         output = [ u'' ]
         stylizer = Stylizer(self.item.data, self.item.href, self.oeb_book, self.opts, self.opts.output_profile)
         content = unicode(etree.tostring(self.item.data.find(XHTML('body')), encoding=unicode))
-        content = self.remove_newlines(content)
+#        content = self.remove_newlines(content)
         trees = { }
         for subitem, subtitle in self.subitems:
             snbcTree = etree.Element("snbc")
@@ -96,7 +97,12 @@ class SNBMLizer(object):
 
         subitem = ''
         for line in output.splitlines():
-            line = line.strip(u' \t\n\r\u3000')
+            if not line.find(CALIBRE_SNB_PRE_TAG) == 0:
+                line = line.strip(u' \t\n\r\u3000')
+            else:
+                etree.SubElement(trees[subitem].find(".//body"), "text").text = \
+                    etree.CDATA(line[len(CALIBRE_SNB_PRE_TAG):])
+                continue
             if len(line) != 0:
                 if line.find(CALIBRE_SNB_IMG_TAG) == 0:
                     prefix = ProcessFileName(os.path.dirname(self.item.href))
@@ -137,7 +143,7 @@ class SNBMLizer(object):
         text = re.sub('(?<=.)%s(?=.)' % os.linesep, ' ', text)
 
         # Remove multiple spaces.
-        text = re.sub('[ ]{2,}', ' ', text)
+        #text = re.sub('[ ]{2,}', ' ', text)
 
         # Remove excessive newlines.
         text = re.sub('\n[ ]+\n', '\n\n', text)
@@ -187,7 +193,7 @@ class SNBMLizer(object):
 
         return text
 
-    def dump_text(self, subitems, elem, stylizer, end=''):
+    def dump_text(self, subitems, elem, stylizer, end='', pre=False):
 
         if not isinstance(elem.tag, basestring) \
            or namespace(elem.tag) != XHTML_NS:
@@ -225,20 +231,27 @@ class SNBMLizer(object):
         if tag == 'br':
             text.append(u'\n\n')
 
+        pre = (tag == 'pre' or pre)
         # Process tags that contain text.
         if hasattr(elem, 'text') and elem.text:
-            text.append(elem.text)
-
+            if pre:
+                text.append((u'\n\n%s' % CALIBRE_SNB_PRE_TAG ).join(elem.text.splitlines()))
+            else:
+                text.append(elem.text)
+            
         for item in elem:
             en = u''
             if len(text) >= 2:
                 en = text[-1][-2:]
-            text += self.dump_text(subitems, item, stylizer, en)
+            text += self.dump_text(subitems, item, stylizer, en, pre)
 
         if in_block:
             text.append(u'\n\n')
 
         if hasattr(elem, 'tail') and elem.tail:
-            text.append(elem.tail)
+            if pre:
+                text.append((u'\n\n%s' % CALIBRE_SNB_PRE_TAG ).join(elem.tail.splitlines()))
+            else:
+                text.append(elem.tail)
 
         return text

From 8f3e1ca4d5749b033168777d94971e76aa9212c9 Mon Sep 17 00:00:00 2001
From: Li Fanxi 
Date: Mon, 11 Oct 2010 18:17:03 +0800
Subject: [PATCH 19/75] [SNBOutput] Better handling of 
  • tag. --- src/calibre/ebooks/snb/snbml.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/calibre/ebooks/snb/snbml.py b/src/calibre/ebooks/snb/snbml.py index f32ddaadf1..3542a2110f 100644 --- a/src/calibre/ebooks/snb/snbml.py +++ b/src/calibre/ebooks/snb/snbml.py @@ -92,7 +92,7 @@ class SNBMLizer(object): etree.SubElement(snbcTree, "body") trees[subitem] = snbcTree output.append(u'%s%s\n\n' % (CALIBRE_SNB_BM_TAG, "")) - output += self.dump_text(self.subitems, etree.fromstring(content), stylizer) + output += self.dump_text(self.subitems, etree.fromstring(content), stylizer)[0] output = self.cleanup_text(u''.join(output)) subitem = '' @@ -193,7 +193,7 @@ class SNBMLizer(object): return text - def dump_text(self, subitems, elem, stylizer, end='', pre=False): + def dump_text(self, subitems, elem, stylizer, end='', pre=False, li = ''): if not isinstance(elem.tag, basestring) \ or namespace(elem.tag) != XHTML_NS: @@ -231,19 +231,24 @@ class SNBMLizer(object): if tag == 'br': text.append(u'\n\n') + if tag == 'li': + li = '-- ' + pre = (tag == 'pre' or pre) # Process tags that contain text. if hasattr(elem, 'text') and elem.text: if pre: - text.append((u'\n\n%s' % CALIBRE_SNB_PRE_TAG ).join(elem.text.splitlines())) + text.append((u'\n\n%s' % CALIBRE_SNB_PRE_TAG ).join((li + elem.text).splitlines())) else: - text.append(elem.text) + text.append(li + elem.text) + li = '' for item in elem: en = u'' if len(text) >= 2: en = text[-1][-2:] - text += self.dump_text(subitems, item, stylizer, en, pre) + t, li = self.dump_text(subitems, item, stylizer, en, pre, li) + text += t if in_block: text.append(u'\n\n') @@ -252,6 +257,7 @@ class SNBMLizer(object): if pre: text.append((u'\n\n%s' % CALIBRE_SNB_PRE_TAG ).join(elem.tail.splitlines())) else: - text.append(elem.tail) + text.append(li + elem.tail) + li = '' - return text + return text, li From f0fb8fb12a24dc043c01e665f1d9aa74866d66b5 Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Tue, 12 Oct 2010 16:22:25 +0800 Subject: [PATCH 20/75] [SNBOutput] Fix bugs in handling
  • and bookmark. --- src/calibre/ebooks/snb/snbml.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/calibre/ebooks/snb/snbml.py b/src/calibre/ebooks/snb/snbml.py index 3542a2110f..af0106aaa0 100644 --- a/src/calibre/ebooks/snb/snbml.py +++ b/src/calibre/ebooks/snb/snbml.py @@ -206,7 +206,7 @@ class SNBMLizer(object): if elem.attrib.get('id') != None and elem.attrib['id'] in [ href for href, title in subitems ]: if self.curSubItem != None and self.curSubItem != elem.attrib['id']: self.curSubItem = elem.attrib['id'] - text.append(u'%s%s\n\n' % (CALIBRE_SNB_BM_TAG, self.curSubItem)) + text.append(u'\n\n%s%s\n\n' % (CALIBRE_SNB_BM_TAG, self.curSubItem)) if style['display'] in ('none', 'oeb-page-head', 'oeb-page-foot') \ or style['visibility'] == 'hidden': @@ -226,13 +226,13 @@ class SNBMLizer(object): text.append(u' ') if tag == 'img': - text.append(u'%s%s' % (CALIBRE_SNB_IMG_TAG, ProcessFileName(elem.attrib['src']))) + text.append(u'\n\n%s%s\n\n' % (CALIBRE_SNB_IMG_TAG, ProcessFileName(elem.attrib['src']))) if tag == 'br': text.append(u'\n\n') if tag == 'li': - li = '-- ' + li = '- ' pre = (tag == 'pre' or pre) # Process tags that contain text. From 6c9541723f2ac9677c4822bd61f2e75a043771bd Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Thu, 14 Oct 2010 16:53:39 +0800 Subject: [PATCH 21/75] [SNBOutput][Bug] Fixed a bug when using multiple SNBFile object, error will happen. [Feature] Get ready for SNB input plugin --- src/calibre/ebooks/snb/snbfile.py | 44 +++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/src/calibre/ebooks/snb/snbfile.py b/src/calibre/ebooks/snb/snbfile.py index ca10f800c7..34830fa808 100644 --- a/src/calibre/ebooks/snb/snbfile.py +++ b/src/calibre/ebooks/snb/snbfile.py @@ -5,6 +5,7 @@ __copyright__ = '2010, Li Fanxi ' __docformat__ = 'restructuredtext en' import sys, struct, zlib, bz2, os, math +from mimetypes import types_map class FileStream: def IsBinary(self): @@ -18,9 +19,6 @@ class BlockData: class SNBFile: - files = [] - blocks = [] - MAGIC = 'SNBP000B' REV80 = 0x00008000 REVA3 = 0x00A3A3A3 @@ -28,15 +26,21 @@ class SNBFile: REVZ2 = 0x00000000 def __init__(self, inputFile = None): + self.files = [] + self.blocks = [] + if inputFile != None: - self.Parse(inputFile); - - def Parse(self, inputFile): + self.Open(inputFile) + + def Open(self, inputFile): self.fileName = inputFile snbFile = open(self.fileName, "rb") snbFile.seek(0) + self.Parse(snbFile) + snbFile.close() + def Parse(self, snbFile, metaOnly = False): # Read header vmbr = snbFile.read(44) (self.magic, self.rev80, self.revA3, self.revZ1, @@ -47,7 +51,7 @@ class SNBFile: # Read FAT self.vfat = zlib.decompress(snbFile.read(self.vfatCompressed)) self.ParseFile(self.vfat, self.fileCount) - + # Read tail snbFile.seek(-16, os.SEEK_END) #plainStreamEnd = snbFile.tell() @@ -57,7 +61,7 @@ class SNBFile: self.vTailUncompressed = zlib.decompress(snbFile.read(self.tailSize)) self.tailSizeUncompressed = len(self.vTailUncompressed) self.ParseTail(self.vTailUncompressed, self.fileCount) - + # Uncompress file data # Read files binPos = 0 @@ -78,7 +82,7 @@ class SNBFile: try: data = snbFile.read(bSize) uncompressedData += bzdc.decompress(data) - except EOFError, e: + except Exception, e: print e f.fileBody = uncompressedData[plainPos:plainPos+f.fileSize] plainPos += f.fileSize @@ -90,7 +94,6 @@ class SNBFile: else: print f.attr, f.fileName raise Exception("Invalid file") - snbFile.close() def ParseFile(self, vfat, fileCount): fileNames = vfat[fileCount*12:].split('\0'); @@ -156,6 +159,24 @@ class SNBFile: f.fileBody = open(os.path.join(tdir,fileName), 'rb').read() f.fileName = fileName.replace(os.sep, '/') self.files.append(f) + + def GetFileStream(self, fileName): + for file in self.files: + if file.fileName == fileName: + return file.fileBody + return None + + def OutputImageFiles(self, path): + fileNames = [] + for f in self.files: + fname = os.path.basename(f.fileName) + root, ext = os.path.splitext(fname) + if ext in [ '.jpeg', '.jpg', '.gif', '.svg', '.png' ]: + file = open(os.path.join(path, fname), 'wb') + file.write(f.fileBody) + file.close() + fileNames.append((fname, types_map[ext])) + return fileNames def Output(self, outputFile): @@ -247,7 +268,8 @@ class SNBFile: return def Dump(self): - print "File Name:\t", self.fileName + if self.fileName: + print "File Name:\t", self.fileName print "File Count:\t", self.fileCount print "VFAT Size(Compressed):\t%d(%d)" % (self.vfatSize, self.vfatCompressed) print "Binary Stream Size:\t", self.binStreamSize From 83cf5d5f2820b985d72556b835578f2bf25ddac5 Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Thu, 14 Oct 2010 16:59:22 +0800 Subject: [PATCH 22/75] [SNBInput][SNBMetadataReader] Add SNB input plugin and SNB Metadata Reader plugin. --- src/calibre/customize/builtins.py | 13 ++++ src/calibre/ebooks/__init__.py | 2 +- src/calibre/ebooks/metadata/meta.py | 2 +- src/calibre/ebooks/metadata/snb.py | 47 +++++++++++++ src/calibre/ebooks/snb/input.py | 104 ++++++++++++++++++++++++++++ src/calibre/gui2/actions/add.py | 1 + 6 files changed, 167 insertions(+), 2 deletions(-) create mode 100755 src/calibre/ebooks/metadata/snb.py create mode 100755 src/calibre/ebooks/snb/input.py diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 8550f57ee6..fe187a1400 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -292,6 +292,17 @@ class RTFMetadataReader(MetadataReaderPlugin): def get_metadata(self, stream, ftype): from calibre.ebooks.metadata.rtf import get_metadata return get_metadata(stream) + +class SNBMetadataReader(MetadataReaderPlugin): + + name = 'Read SNB metadata' + file_types = set(['snb']) + description = _('Read metadata from %s files') % 'SNB' + author = 'Li Fanxi' + + def get_metadata(self, stream, ftype): + from calibre.ebooks.metadata.snb import get_metadata + return get_metadata(stream) class TOPAZMetadataReader(MetadataReaderPlugin): @@ -420,6 +431,7 @@ from calibre.ebooks.tcr.input import TCRInput from calibre.ebooks.txt.input import TXTInput from calibre.ebooks.lrf.input import LRFInput from calibre.ebooks.chm.input import CHMInput +from calibre.ebooks.snb.input import SNBInput from calibre.ebooks.epub.output import EPUBOutput from calibre.ebooks.fb2.output import FB2Output @@ -496,6 +508,7 @@ plugins += [ TXTInput, LRFInput, CHMInput, + SNBInput, ] plugins += [ EPUBOutput, diff --git a/src/calibre/ebooks/__init__.py b/src/calibre/ebooks/__init__.py index 624b277e61..9bdf937dd1 100644 --- a/src/calibre/ebooks/__init__.py +++ b/src/calibre/ebooks/__init__.py @@ -25,7 +25,7 @@ class DRMError(ValueError): BOOK_EXTENSIONS = ['lrf', 'rar', 'zip', 'rtf', 'lit', 'txt', 'htm', 'xhtm', 'html', 'xhtml', 'pdf', 'pdb', 'pdr', 'prc', 'mobi', 'azw', 'doc', 'epub', 'fb2', 'djvu', 'lrx', 'cbr', 'cbz', 'cbc', 'oebzip', - 'rb', 'imp', 'odt', 'chm', 'tpz', 'azw1', 'pml', 'mbp', 'tan'] + 'rb', 'imp', 'odt', 'chm', 'tpz', 'azw1', 'pml', 'mbp', 'tan', 'snb'] class HTMLRenderer(object): diff --git a/src/calibre/ebooks/metadata/meta.py b/src/calibre/ebooks/metadata/meta.py index 87b8d3b535..cbd9db3f04 100644 --- a/src/calibre/ebooks/metadata/meta.py +++ b/src/calibre/ebooks/metadata/meta.py @@ -15,7 +15,7 @@ _METADATA_PRIORITIES = [ 'html', 'htm', 'xhtml', 'xhtm', 'rtf', 'fb2', 'pdf', 'prc', 'odt', 'epub', 'lit', 'lrx', 'lrf', 'mobi', - 'rb', 'imp', 'azw' + 'rb', 'imp', 'azw', 'snb' ] # The priorities for loading metadata from different file types diff --git a/src/calibre/ebooks/metadata/snb.py b/src/calibre/ebooks/metadata/snb.py new file mode 100755 index 0000000000..67bbc89a32 --- /dev/null +++ b/src/calibre/ebooks/metadata/snb.py @@ -0,0 +1,47 @@ +'''Read meta information from SNB files''' + +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2010, Li Fanxi ' + +import re, os +from StringIO import StringIO +from calibre.ebooks.metadata import MetaInformation +from calibre.ebooks.snb.snbfile import SNBFile +from lxml import etree + +def get_metadata(stream, extract_cover=True): + """ Return metadata as a L{MetaInfo} object """ + mi = MetaInformation(_('Unknown'), [_('Unknown')]) + snbFile = SNBFile() + + try: + if not hasattr(stream, 'write'): + snbFile.Parse(StringIO(stream), True) + else: + stream.seek(0) + snbFile.Parse(stream, True) + + meta = snbFile.GetFileStream('snbf/book.snbf') + + if meta != None: + meta = etree.fromstring(meta) + mi.title = meta.find('.//head/name').text + mi.authors = [meta.find('.//head/author').text] + mi.language = meta.find('.//head/language').text.lower().replace('_', '-') + mi.publisher = meta.find('.//head/publisher').text + + if extract_cover: + cover = meta.find('.//head/cover') + if cover != None and cover.text != None: + root, ext = os.path.splitext(cover.text) + if ext == '.jpeg': + ext = '.jpg' + mi.cover_data = (ext[-3:], snbFile.GetFileStream('snbc/images/' + cover.text)) + + except Exception, e: + print e + pass + + return mi diff --git a/src/calibre/ebooks/snb/input.py b/src/calibre/ebooks/snb/input.py new file mode 100755 index 0000000000..a85feddbb2 --- /dev/null +++ b/src/calibre/ebooks/snb/input.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- + +__license__ = 'GPL 3' +__copyright__ = '2010, Li Fanxi ' +__docformat__ = 'restructuredtext en' + +import os, uuid + +from calibre.customize.conversion import InputFormatPlugin, OptionRecommendation +from calibre.ebooks.oeb.base import DirContainer +from calibre.ebooks.snb.snbfile import SNBFile +from calibre.ptempfile import TemporaryDirectory +from calibre.utils.filenames import ascii_filename +from calibre import prepare_string_for_xml +from lxml import etree + +HTML_TEMPLATE = u'%s\n%s\n' + +def html_encode(s): + return s.replace(u'&', u'&').replace(u'<', u'<').replace(u'>', u'>').replace(u'"', u'"').replace(u"'", u''').replace(u'\n', u'
    ').replace(u' ', u' ') + +class SNBInput(InputFormatPlugin): + + name = 'SNB Input' + author = 'Li Fanxi' + description = 'Convert SNB files to OEB' + file_types = set(['snb']) + + options = set([ + ]) + + def convert(self, stream, options, file_ext, log, + accelerators): + log.debug("Parsing SNB file...") + snbFile = SNBFile() + try: + snbFile.Parse(stream) + except: + raise ValueError("Invalid SNB file") + if not snbFile.IsValid(): + log.debug("Invaild SNB file") + raise ValueError("Invalid SNB file") + log.debug("Handle meta data ...") + from calibre.ebooks.conversion.plumber import create_oebbook + oeb = create_oebbook(log, None, options, self, + encoding=options.input_encoding, populate=False) + meta = snbFile.GetFileStream('snbf/book.snbf') + if meta != None: + meta = etree.fromstring(meta) + oeb.metadata.add('title', meta.find('.//head/name').text) + oeb.metadata.add('creator', meta.find('.//head/author').text, attrib={'role':'aut'}) + oeb.metadata.add('language', meta.find('.//head/language').text.lower().replace('_', '-')) + oeb.metadata.add('creator', meta.find('.//head/generator').text) + oeb.metadata.add('publisher', meta.find('.//head/publisher').text) + cover = meta.find('.//head/cover') + if cover != None and cover.text != None: + oeb.guide.add('cover', 'Cover', cover.text) + + bookid = str(uuid.uuid4()) + oeb.metadata.add('identifier', bookid, id='uuid_id', scheme='uuid') + for ident in oeb.metadata.identifier: + if 'id' in ident.attrib: + oeb.uid = oeb.metadata.identifier[0] + break + + with TemporaryDirectory('_chm2oeb', keep=True) as tdir: + log.debug('Process TOC ...') + toc = snbFile.GetFileStream('snbf/toc.snbf') + oeb.container = DirContainer(tdir, log) + if toc != None: + toc = etree.fromstring(toc) + i = 1 + for ch in toc.find('.//body'): + chapterName = ch.text + chapterSrc = ch.get('src') + fname = 'ch_%d.htm' % i + data = snbFile.GetFileStream('snbc/' + chapterSrc) + if data != None: + snbc = etree.fromstring(data) + outputFile = open(os.path.join(tdir, fname), 'wb') + lines = [] + for line in snbc.find('.//body'): + if line.tag == 'text': + lines.append(u'

    %s

    ' % html_encode(line.text)) + elif line.tag == 'img': + lines.append(u'

    ' % html_encode(line.text)) + outputFile.write((HTML_TEMPLATE % (chapterName, u'\n'.join(lines))).encode('utf-8', 'replace')) + outputFile.close() + oeb.toc.add(ch.text, fname) + id, href = oeb.manifest.generate(id='html', + href=ascii_filename(fname)) + item = oeb.manifest.add(id, href, 'text/html') + item.html_input_href = fname + oeb.spine.add(item, True) + i = i + 1 + imageFiles = snbFile.OutputImageFiles(tdir) + for f, m in imageFiles: + id, href = oeb.manifest.generate(id='image', + href=ascii_filename(f)) + item = oeb.manifest.add(id, href, m) + item.html_input_href = f + + return oeb + diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py index be1f8f4eaf..5bcdf2254e 100644 --- a/src/calibre/gui2/actions/add.py +++ b/src/calibre/gui2/actions/add.py @@ -166,6 +166,7 @@ class AddAction(InterfaceAction): (_('Topaz books'), ['tpz','azw1']), (_('Text books'), ['txt', 'rtf']), (_('PDF Books'), ['pdf']), + (_('SNB Books'), ['snb']), (_('Comics'), ['cbz', 'cbr', 'cbc']), (_('Archives'), ['zip', 'rar']), ] From 8d26343231ae58962b8b80756f427827b58f8b4d Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Thu, 14 Oct 2010 20:57:47 +0800 Subject: [PATCH 23/75] [SNBOutput] Also convert png image to jpg. --- src/calibre/ebooks/snb/snbml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/ebooks/snb/snbml.py b/src/calibre/ebooks/snb/snbml.py index af0106aaa0..1847e05a4b 100644 --- a/src/calibre/ebooks/snb/snbml.py +++ b/src/calibre/ebooks/snb/snbml.py @@ -25,7 +25,7 @@ def ProcessFileName(fileName): fileName = fileName.lower() # Change all images to jpg root, ext = os.path.splitext(fileName) - if ext in [ '.jpeg', '.jpg', '.gif', '.svg' ]: + if ext in [ '.jpeg', '.jpg', '.gif', '.svg', '.png' ]: fileName = root + '.jpg' return fileName From 565295b3531b9e6f1251c9ad9d352b6f2812003b Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Thu, 14 Oct 2010 21:19:08 +0800 Subject: [PATCH 24/75] [SNBOutput] Improve handling of comics, read screen size from profile. --- src/calibre/customize/profiles.py | 1 + src/calibre/ebooks/snb/output.py | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/calibre/customize/profiles.py b/src/calibre/customize/profiles.py index bda2103484..4fa53b1cdb 100644 --- a/src/calibre/customize/profiles.py +++ b/src/calibre/customize/profiles.py @@ -655,6 +655,7 @@ class BambookOutput(OutputProfile): # Screen size is a best guess screen_size = (800, 600) + comic_screen_size = (700, 540) dpi = 168.451 fbase = 12 fsizes = [10, 12, 14, 16] diff --git a/src/calibre/ebooks/snb/output.py b/src/calibre/ebooks/snb/output.py index cbe785d384..3aadb79185 100644 --- a/src/calibre/ebooks/snb/output.py +++ b/src/calibre/ebooks/snb/output.py @@ -50,6 +50,7 @@ class SNBOutput(OutputFormatPlugin): ]) def convert(self, oeb_book, output_path, input_plugin, opts, log): + self.opts = opts # Create temp dir with TemporaryDirectory('_snb_output') as tdir: # Create stub directories @@ -224,9 +225,12 @@ class SNBOutput(OutputFormatPlugin): img = Image() img.load(imageData) (x,y) = img.size - # TODO use the data from device profile - SCREEN_X = 540 - SCREEN_Y = 700 + if self.opts: + SCREEN_Y, SCREEN_X = self.opts.output_profile.comic_screen_size + print SCREEN_Y, SCREEN_X + else: + SCREEN_X = 540 + SCREEN_Y = 700 # Handle big image only if x > SCREEN_X or y > SCREEN_Y: SCREEN_RATIO = float(SCREEN_Y) / SCREEN_X From 0a4da453beca720a47f5d93da7a0e47b8f1e8dad Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 15 Oct 2010 09:46:42 -0600 Subject: [PATCH 25/75] ... --- src/calibre/manual/news.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/calibre/manual/news.rst b/src/calibre/manual/news.rst index de50fd1c19..88b6dd47bc 100644 --- a/src/calibre/manual/news.rst +++ b/src/calibre/manual/news.rst @@ -295,6 +295,9 @@ To learn more about writing advanced recipes using some of the facilities, avail `Built-in recipes `_ The source code for the built-in recipes that come with |app| + `The calibre recipes forum `_ + Lots of knowledgeable |app| recipe writers hang out here. + API documentation -------------------- From 1f0d61c630af6888841e83cfc3439dc816d69b94 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 15 Oct 2010 11:41:50 -0600 Subject: [PATCH 26/75] Basic book list in the new content server --- resources/content_server/browse/browse.css | 57 ++++++++++++++ resources/content_server/browse/browse.js | 53 ++++++++++++- resources/content_server/browse/summary.html | 14 ++++ src/calibre/library/comments.py | 2 + src/calibre/library/server/browse.py | 78 +++++++++++++++++--- 5 files changed, 188 insertions(+), 16 deletions(-) create mode 100644 resources/content_server/browse/summary.html diff --git a/resources/content_server/browse/browse.css b/resources/content_server/browse/browse.css index d66ae744cf..d2e8106ba0 100644 --- a/resources/content_server/browse/browse.css +++ b/resources/content_server/browse/browse.css @@ -265,9 +265,66 @@ h2.library_name { display: none; } +#booklist .load_data { display: none } + .loading img { vertical-align: middle; } +#booklist .summary { + margin-bottom: 2ex; + border-bottom: solid 1px black; +} + +#booklist div.left { + float: left; + height: 190px; + vertical-align: middle; + text-align: center; + margin-left: 1em; + margin-right: 2em; +} + +#booklist div.left img { + display: block; + margin-bottom: 1ex; +} + +#booklist div.right { + height: 190px; + overflow: auto; + margin-left: 1em; + margin-right: 1em; +} + +#booklist div.right .stars { + float:right; + margin-right: 1em; +} + +#booklist div.right .stars .rating_container { + display: block; +} + +#booklist .title { + font-size: larger; +} + +#booklist .formats a { + text-decoration: none; +} + +#booklist .formats a:hover { + color: red; +} + +#booklist .left .ui-button-text { + font-weight: bold; + padding-left: 0.25em; + padding-right: 0.25em; + padding-top: 0.25em; + padding-bottom: 0.25em; +} + /* }}} */ diff --git a/resources/content_server/browse/browse.js b/resources/content_server/browse/browse.js index f3f278fc48..5f2af3299e 100644 --- a/resources/content_server/browse/browse.js +++ b/resources/content_server/browse/browse.js @@ -89,11 +89,14 @@ function render_error(msg) { } // Category feed {{{ + +function category_clicked() { + var href = $(this).find("span.href").html(); + window.location = href; +} + function category() { - $(".category .category-item").click(function() { - var href = $(this).find("span.href").html(); - window.location = href; - }); + $(".category .category-item").click(category_clicked); $(".category a.navlink").button(); @@ -115,6 +118,7 @@ function category() { this.children(".loaded").html(data); this.children(".loaded").show(); this.children(".loading").hide(); + this.find('.category-item').click(category_clicked); }, context: ui.newContent, dataType: "json", @@ -132,4 +136,45 @@ function category() { } // }}} +// Booklist {{{ +function load_page(elem) { + var ids = elem.find(".load_data").attr('title'); + var href = elem.find(".load_data").html(); + $.ajax({ + url: href, + context: elem, + dataType: "json", + type: 'POST', + timeout: 600000, //milliseconds (10 minutes) + data: {'ids': ids}, + error: function(xhr, stat, err) { + this.children(".loaded").html(render_error(stat)); + this.children(".loaded").show(); + this.children(".loading").hide(); + }, + success: function(data) { + this.children(".loaded").html(data); + this.children(".loaded").show(); + this.children(".loading").hide(); + this.find(".left a.read").button(); + } + }); + $("#booklist .page").hide(); + elem.children(".loaded").hide(); + elem.children(".loading").show(); + elem.show(); +} + +function booklist() { + var test = $("#booklist #page0").html(); + if (!test) { + $("#booklist").html(render_error("No books found")); + return; + } + + load_page($("#booklist #page0")); + +} + +// }}} diff --git a/resources/content_server/browse/summary.html b/resources/content_server/browse/summary.html new file mode 100644 index 0000000000..0caf710a43 --- /dev/null +++ b/resources/content_server/browse/summary.html @@ -0,0 +1,14 @@ +
    + +
    +
    {stars}{series}
    +
    {title}
    +
    {authors}
    +
    {comments}
    +
    {tags}
    +
    {other_formats}
    +
    +
    diff --git a/src/calibre/library/comments.py b/src/calibre/library/comments.py index 32ae65b31e..670d9f2564 100644 --- a/src/calibre/library/comments.py +++ b/src/calibre/library/comments.py @@ -42,6 +42,8 @@ def comments_to_html(comments): Deprecated HTML returns as HTML via BeautifulSoup() ''' + if not comments: + return u'

    ' if not isinstance(comments, unicode): comments = comments.decode(preferred_encoding, 'replace') diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index ffbd958688..e1415decdb 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -14,16 +14,20 @@ import cherrypy from calibre.constants import filesystem_encoding from calibre import isbytestring, force_unicode, prepare_string_for_xml as xml from calibre.utils.ordered_dict import OrderedDict +from calibre.utils.filenames import ascii_filename +from calibre.utils.config import prefs +from calibre.library.comments import comments_to_html def render_book_list(ids): pages = [] + num = len(ids) while ids: page = list(ids[:25]) pages.append(page) ids = ids[25:] page_template = u'''\
    -
    +
    /browse/booklist_page
    {2}
    @@ -41,7 +45,7 @@ def render_book_list(ids): {pages} ''' - return templ.format(_('Browsing %d books')%len(ids), pages=rpages) + return templ.format(_('Browsing %d books')%num, pages=rpages) def utf8(x): # {{{ if isinstance(x, unicode): @@ -49,11 +53,13 @@ def utf8(x): # {{{ return x # }}} -def render_rating(rating, container='span'): # {{{ +def render_rating(rating, container='span', prefix=None): # {{{ if rating < 0.1: return '', '' added = 0 - rstring = xml(_('Average rating: %.1f stars')% (rating if rating else 0.0), + if prefix is None: + prefix = _('Average rating') + rstring = xml(_('%s: %.1f stars')% (prefix, rating if rating else 0.0), True) ans = ['<%s class="rating">' % (container)] for i in range(5): @@ -89,7 +95,7 @@ def get_category_items(category, items, db, datatype): # {{{ id_ = xml(str(id_)) desc = '' if i.count > 0: - desc += '[' + _('%d items')%i.count + ']' + desc += '[' + _('%d books')%i.count + ']' href = '/browse/matches/%s/%s'%(category, id_) return templ.format(xml(name), rating, xml(desc), xml(quote(href)), rstring) @@ -193,6 +199,14 @@ class BrowseServer(object): self.__browse_template__ = generate() return self.__browse_template__ + @property + def browse_summary_template(self): + if not hasattr(self, '__browse_summary_template__') or \ + self.opts.develop: + self.__browse_summary_template__ = \ + P('content_server/browse/summary.html', data=True).decode('utf-8') + return self.__browse_summary_template__ + # Catalogs {{{ def browse_toplevel(self): @@ -329,7 +343,6 @@ class BrowseServer(object): return json.dumps(entries, ensure_ascii=False) - @Endpoint() def browse_catalog(self, category=None, category_sort=None): 'Entry point for top-level, categories and sub-categories' @@ -401,16 +414,57 @@ class BrowseServer(object): ids = json.loads(ids) except: raise cherrypy.HTTPError(404, 'invalid ids') + summs = [] + for id_ in ids: + try: + id_ = int(id_) + mi = self.db.get_metadata(id_, index_is_id=True) + except: + continue + fmts = self.db.formats(id_, index_is_id=True) + if not fmts: + fmts = '' + fmts = [x.lower() for x in fmts.split(',') if x] + pf = prefs['output_format'].lower() + fmt = pf if pf in fmts else fmts[0] + args = {'id':id_, 'mi':mi, 'read_string':_('Read'),} + for key in mi.all_field_keys(): + val = mi.format_field(key)[1] + if not val: + val = '' + args[key] = xml(val, True) + fname = ascii_filename(args['title']) + ' - ' + ascii_filename(args['authors']) + args['href'] = '/get/%s/%s_%d.%s'%( + fmt, fname, id_, fmt) + args['comments'] = comments_to_html(mi.comments) + args['read_tooltip'] = \ + _('Read %s in the %s format')%(args['title'], fmt.upper()) + args['stars'] = '' + if mi.rating: + args['stars'] = render_rating(mi.rating/2.0, prefix=_('Rating'))[0] + if args['tags']: + args['tags'] = u'%s: '%_('Tags') + args['tags'] + args['other_formats'] = '' + other_fmts = [x for x in fmts if x.lower() != fmt.lower()] + + if other_fmts: + ofmts = [u'{3}'\ + .format(fmt, fname, id_, fmt.upper()) for fmt in + other_fmts] + ofmts = ', '.join(ofmts) + args['other_formats'] = u'%s: ' % \ + _('Other formats') + ofmts + + + summs.append(self.browse_summary_template.format(**args)) + + + return json.dumps('\n'.join(summs), ensure_ascii=False) # }}} # Search {{{ - def browse_search(self, query=None, offset=0, sort=None): - raise NotImplementedError() - # }}} - - # Book {{{ - def browse_book(self, uuid=None): + def browse_search(self, query=None): raise NotImplementedError() # }}} From 06c36eac92f848aa3312664ad56bab4bff5d0d93 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 15 Oct 2010 16:58:39 -0600 Subject: [PATCH 27/75] Pagination of book lists --- resources/content_server/browse/browse.css | 46 ++++++++++++++++++ resources/content_server/browse/browse.js | 39 +++++++++++++--- resources/content_server/browse/summary.html | 2 +- src/calibre/library/server/browse.py | 49 ++++++++++++++++---- 4 files changed, 119 insertions(+), 17 deletions(-) diff --git a/resources/content_server/browse/browse.css b/resources/content_server/browse/browse.css index d2e8106ba0..f3dfc89caf 100644 --- a/resources/content_server/browse/browse.css +++ b/resources/content_server/browse/browse.css @@ -279,6 +279,7 @@ h2.library_name { #booklist div.left { float: left; height: 190px; + width: 100px; vertical-align: middle; text-align: center; margin-left: 1em; @@ -288,6 +289,8 @@ h2.library_name { #booklist div.left img { display: block; margin-bottom: 1ex; + margin-left: auto; + margin-right: auto; } #booklist div.right { @@ -312,6 +315,7 @@ h2.library_name { #booklist .formats a { text-decoration: none; + color: blue; } #booklist .formats a:hover { @@ -326,5 +330,47 @@ h2.library_name { padding-bottom: 0.25em; } +#booklist .listnav { + display: table; + width: 100%; +} + +#booklist .listnav a { + color: blue; + text-decoration: none; +} + +#booklist .listnav a:hover { + color: red; +} + +#booklist .topnav { + border-bottom: solid black 1px; + margin-bottom: 1ex; +} + +#booklist .navleft { + display: table-cell; + text-align: left; +} + +#booklist .navleft a { + margin-right: 1em; +} + +#booklist .navmiddle { + display: table-cell; + text-align: center; +} + +#booklist .navright { + display: table-cell; + text-align: right; +} + +#booklist .navright a { + margin-left: 1em; +} + /* }}} */ diff --git a/resources/content_server/browse/browse.js b/resources/content_server/browse/browse.js index 5f2af3299e..d6383d3646 100644 --- a/resources/content_server/browse/browse.js +++ b/resources/content_server/browse/browse.js @@ -138,9 +138,33 @@ function category() { // Booklist {{{ +function first_page() { + load_page($("#booklist #page0")); +} + +function last_page() { + load_page($("#booklist .page").last()); +} + +function next_page() { + var elem = $("#booklist .page:visible").next('.page'); + if (elem.length > 0) load_page(elem); + else first_page(); +} + +function previous_page() { + var elem = $("#booklist .page:visible").prev('.page'); + if (elem.length > 0) load_page(elem); + else last_page(); +} + function load_page(elem) { - var ids = elem.find(".load_data").attr('title'); - var href = elem.find(".load_data").html(); + if (elem.is(":visible")) return; + var ld = elem.find('.load_data'); + var ids = ld.attr('title'); + var href = ld.find(".url").attr('title'); + elem.children(".loaded").hide(); + $.ajax({ url: href, context: elem, @@ -155,12 +179,14 @@ function load_page(elem) { }, success: function(data) { this.children(".loaded").html(data); - this.children(".loaded").show(); - this.children(".loading").hide(); this.find(".left a.read").button(); + this.children(".loading").hide(); + this.parent().find('.navmiddle .start').html(this.find('.load_data .start').attr('title')); + this.parent().find('.navmiddle .end').html(this.find('.load_data .end').attr('title')); + this.children(".loaded").fadeIn(1000); } }); - $("#booklist .page").hide(); + $("#booklist .page:visible").hide(); elem.children(".loaded").hide(); elem.children(".loading").show(); elem.show(); @@ -173,8 +199,7 @@ function booklist() { return; } - load_page($("#booklist #page0")); - + first_page(); } // }}} diff --git a/resources/content_server/browse/summary.html b/resources/content_server/browse/summary.html index 0caf710a43..01f2b333f2 100644 --- a/resources/content_server/browse/summary.html +++ b/resources/content_server/browse/summary.html @@ -1,6 +1,6 @@
    diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index e1415decdb..bd63325ecc 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -21,31 +21,62 @@ from calibre.library.comments import comments_to_html def render_book_list(ids): pages = [] num = len(ids) + pos = 0 + delta = 25 while ids: - page = list(ids[:25]) - pages.append(page) - ids = ids[25:] + page = list(ids[:delta]) + pages.append((page, pos)) + ids = ids[delta:] + pos += len(page) page_template = u'''\
    -
    /browse/booklist_page
    +
    + + + +
    {2}
    ''' rpages = [] - for i, pg in enumerate(pages): + for i, x in enumerate(pages): + pg, pos = x ld = xml(json.dumps(pg), True) rpages.append(page_template.format(i, ld, - xml(_('Loading, please wait')) + '…')) + xml(_('Loading, please wait')) + '…', + start=pos+1, end=pos+len(pg))) rpages = u'\n\n'.join(rpages) templ = u'''\

    {0}

    +
    + {navbar} +
    {pages} +
    + {navbar} +
    ''' - return templ.format(_('Browsing %d books')%num, pages=rpages) + + navbar = u'''\ + + + + '''.format(first=_('First'), last=_('Last'), previous=_('Previous'), + next=_('Next'), num=num) + + return templ.format(_('Browsing %d books')%num, pages=rpages, navbar=navbar) def utf8(x): # {{{ if isinstance(x, unicode): @@ -182,7 +213,7 @@ class BrowseServer(object): opts = ['' % ( 'selected="selected" ' if k==sort else '', xml(k), xml(n), ) for k, n in - sorted(sort_opts, key=operator.itemgetter(1))] + sorted(sort_opts, key=operator.itemgetter(1)) if k and n] ans = ans.replace('{sort_select_options}', ('\n'+' '*20).join(opts)) lp = self.db.library_path if isbytestring(lp): @@ -402,7 +433,7 @@ class BrowseServer(object): sort = self.browse_sort_book_list(items, list_sort) ids = [x[0] for x in items] html = render_book_list(ids) - return self.browse_template(sort).format( + return self.browse_template(sort, category=False).format( title=_('Books in') + " " +category_name, script='booklist();', main=html) From f7b3f6a442666358f78026f6bc23433affaa78bf Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 15 Oct 2010 17:26:49 -0600 Subject: [PATCH 28/75] Fix #5249 (Error parsing accented characters in content server) --- resources/content_server/browse/browse.html | 2 +- resources/content_server/index.html | 2 +- src/calibre/library/server/mobile.py | 6 +++++- src/calibre/library/server/xml.py | 2 ++ 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/resources/content_server/browse/browse.html b/resources/content_server/browse/browse.html index e1e4cd47f5..cd556b87a4 100644 --- a/resources/content_server/browse/browse.html +++ b/resources/content_server/browse/browse.html @@ -76,7 +76,7 @@
    ');/sw|se|ne|nw/.test(f)&&g.css({zIndex:++a.zIndex});"se"==f&&g.addClass("ui-icon ui-icon-gripsmall-diagonal-se");this.handles[f]=".ui-resizable-"+f;this.element.append(g)}}this._renderAxis=function(h){h=h||this.element;for(var i in this.handles){if(this.handles[i].constructor== +String)this.handles[i]=e(this.handles[i],this.element).show();if(this.elementIsWrapper&&this.originalElement[0].nodeName.match(/textarea|input|select|button/i)){var j=e(this.handles[i],this.element),k=0;k=/sw|ne|nw|se|n|s/.test(i)?j.outerHeight():j.outerWidth();j=["padding",/ne|nw|n/.test(i)?"Top":/se|sw|s/.test(i)?"Bottom":/^e$/.test(i)?"Right":"Left"].join("");h.css(j,k);this._proportionallyResize()}e(this.handles[i])}};this._renderAxis(this.element);this._handles=e(".ui-resizable-handle",this.element).disableSelection(); +this._handles.mouseover(function(){if(!b.resizing){if(this.className)var h=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i);b.axis=h&&h[1]?h[1]:"se"}});if(a.autoHide){this._handles.hide();e(this.element).addClass("ui-resizable-autohide").hover(function(){e(this).removeClass("ui-resizable-autohide");b._handles.show()},function(){if(!b.resizing){e(this).addClass("ui-resizable-autohide");b._handles.hide()}})}this._mouseInit()},destroy:function(){this._mouseDestroy();var b=function(c){e(c).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing").removeData("resizable").unbind(".resizable").find(".ui-resizable-handle").remove()}; +if(this.elementIsWrapper){b(this.element);var a=this.element;a.after(this.originalElement.css({position:a.css("position"),width:a.outerWidth(),height:a.outerHeight(),top:a.css("top"),left:a.css("left")})).remove()}this.originalElement.css("resize",this.originalResizeStyle);b(this.originalElement);return this},_mouseCapture:function(b){var a=false;for(var c in this.handles)if(e(this.handles[c])[0]==b.target)a=true;return!this.options.disabled&&a},_mouseStart:function(b){var a=this.options,c=this.element.position(), +d=this.element;this.resizing=true;this.documentScroll={top:e(document).scrollTop(),left:e(document).scrollLeft()};if(d.is(".ui-draggable")||/absolute/.test(d.css("position")))d.css({position:"absolute",top:c.top,left:c.left});e.browser.opera&&/relative/.test(d.css("position"))&&d.css({position:"relative",top:"auto",left:"auto"});this._renderProxy();c=m(this.helper.css("left"));var f=m(this.helper.css("top"));if(a.containment){c+=e(a.containment).scrollLeft()||0;f+=e(a.containment).scrollTop()||0}this.offset= +this.helper.offset();this.position={left:c,top:f};this.size=this._helper?{width:d.outerWidth(),height:d.outerHeight()}:{width:d.width(),height:d.height()};this.originalSize=this._helper?{width:d.outerWidth(),height:d.outerHeight()}:{width:d.width(),height:d.height()};this.originalPosition={left:c,top:f};this.sizeDiff={width:d.outerWidth()-d.width(),height:d.outerHeight()-d.height()};this.originalMousePosition={left:b.pageX,top:b.pageY};this.aspectRatio=typeof a.aspectRatio=="number"?a.aspectRatio: +this.originalSize.width/this.originalSize.height||1;a=e(".ui-resizable-"+this.axis).css("cursor");e("body").css("cursor",a=="auto"?this.axis+"-resize":a);d.addClass("ui-resizable-resizing");this._propagate("start",b);return true},_mouseDrag:function(b){var a=this.helper,c=this.originalMousePosition,d=this._change[this.axis];if(!d)return false;c=d.apply(this,[b,b.pageX-c.left||0,b.pageY-c.top||0]);if(this._aspectRatio||b.shiftKey)c=this._updateRatio(c,b);c=this._respectSize(c,b);this._propagate("resize", +b);a.css({top:this.position.top+"px",left:this.position.left+"px",width:this.size.width+"px",height:this.size.height+"px"});!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize();this._updateCache(c);this._trigger("resize",b,this.ui());return false},_mouseStop:function(b){this.resizing=false;var a=this.options,c=this;if(this._helper){var d=this._proportionallyResizeElements,f=d.length&&/textarea/i.test(d[0].nodeName);d=f&&e.ui.hasScroll(d[0],"left")?0:c.sizeDiff.height; +f={width:c.size.width-(f?0:c.sizeDiff.width),height:c.size.height-d};d=parseInt(c.element.css("left"),10)+(c.position.left-c.originalPosition.left)||null;var g=parseInt(c.element.css("top"),10)+(c.position.top-c.originalPosition.top)||null;a.animate||this.element.css(e.extend(f,{top:g,left:d}));c.helper.height(c.size.height);c.helper.width(c.size.width);this._helper&&!a.animate&&this._proportionallyResize()}e("body").css("cursor","auto");this.element.removeClass("ui-resizable-resizing");this._propagate("stop", +b);this._helper&&this.helper.remove();return false},_updateCache:function(b){this.offset=this.helper.offset();if(l(b.left))this.position.left=b.left;if(l(b.top))this.position.top=b.top;if(l(b.height))this.size.height=b.height;if(l(b.width))this.size.width=b.width},_updateRatio:function(b){var a=this.position,c=this.size,d=this.axis;if(b.height)b.width=c.height*this.aspectRatio;else if(b.width)b.height=c.width/this.aspectRatio;if(d=="sw"){b.left=a.left+(c.width-b.width);b.top=null}if(d=="nw"){b.top= +a.top+(c.height-b.height);b.left=a.left+(c.width-b.width)}return b},_respectSize:function(b){var a=this.options,c=this.axis,d=l(b.width)&&a.maxWidth&&a.maxWidthb.width,h=l(b.height)&&a.minHeight&&a.minHeight>b.height;if(g)b.width=a.minWidth;if(h)b.height=a.minHeight;if(d)b.width=a.maxWidth;if(f)b.height=a.maxHeight;var i=this.originalPosition.left+this.originalSize.width,j=this.position.top+this.size.height, +k=/sw|nw|w/.test(c);c=/nw|ne|n/.test(c);if(g&&k)b.left=i-a.minWidth;if(d&&k)b.left=i-a.maxWidth;if(h&&c)b.top=j-a.minHeight;if(f&&c)b.top=j-a.maxHeight;if((a=!b.width&&!b.height)&&!b.left&&b.top)b.top=null;else if(a&&!b.top&&b.left)b.left=null;return b},_proportionallyResize:function(){if(this._proportionallyResizeElements.length)for(var b=this.helper||this.element,a=0;a
    ');var a=e.browser.msie&&e.browser.version<7,c=a?1:0;a=a?2:-1;this.helper.addClass(this._helper).css({width:this.element.outerWidth()+a,height:this.element.outerHeight()+a,position:"absolute",left:this.elementOffset.left-c+"px",top:this.elementOffset.top-c+"px",zIndex:++b.zIndex});this.helper.appendTo("body").disableSelection()}else this.helper=this.element},_change:{e:function(b,a){return{width:this.originalSize.width+ +a}},w:function(b,a){return{left:this.originalPosition.left+a,width:this.originalSize.width-a}},n:function(b,a,c){return{top:this.originalPosition.top+c,height:this.originalSize.height-c}},s:function(b,a,c){return{height:this.originalSize.height+c}},se:function(b,a,c){return e.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[b,a,c]))},sw:function(b,a,c){return e.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[b,a,c]))},ne:function(b,a,c){return e.extend(this._change.n.apply(this, +arguments),this._change.e.apply(this,[b,a,c]))},nw:function(b,a,c){return e.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[b,a,c]))}},_propagate:function(b,a){e.ui.plugin.call(this,b,[a,this.ui()]);b!="resize"&&this._trigger(b,a,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}});e.extend(e.ui.resizable, +{version:"1.8.5"});e.ui.plugin.add("resizable","alsoResize",{start:function(){var b=e(this).data("resizable").options,a=function(c){e(c).each(function(){var d=e(this);d.data("resizable-alsoresize",{width:parseInt(d.width(),10),height:parseInt(d.height(),10),left:parseInt(d.css("left"),10),top:parseInt(d.css("top"),10),position:d.css("position")})})};if(typeof b.alsoResize=="object"&&!b.alsoResize.parentNode)if(b.alsoResize.length){b.alsoResize=b.alsoResize[0];a(b.alsoResize)}else e.each(b.alsoResize, +function(c){a(c)});else a(b.alsoResize)},resize:function(b,a){var c=e(this).data("resizable");b=c.options;var d=c.originalSize,f=c.originalPosition,g={height:c.size.height-d.height||0,width:c.size.width-d.width||0,top:c.position.top-f.top||0,left:c.position.left-f.left||0},h=function(i,j){e(i).each(function(){var k=e(this),q=e(this).data("resizable-alsoresize"),p={},r=j&&j.length?j:k.parents(a.originalElement[0]).length?["width","height"]:["width","height","top","left"];e.each(r,function(n,o){if((n= +(q[o]||0)+(g[o]||0))&&n>=0)p[o]=n||null});if(e.browser.opera&&/relative/.test(k.css("position"))){c._revertToRelativePosition=true;k.css({position:"absolute",top:"auto",left:"auto"})}k.css(p)})};typeof b.alsoResize=="object"&&!b.alsoResize.nodeType?e.each(b.alsoResize,function(i,j){h(i,j)}):h(b.alsoResize)},stop:function(){var b=e(this).data("resizable"),a=b.options,c=function(d){e(d).each(function(){var f=e(this);f.css({position:f.data("resizable-alsoresize").position})})};if(b._revertToRelativePosition){b._revertToRelativePosition= +false;typeof a.alsoResize=="object"&&!a.alsoResize.nodeType?e.each(a.alsoResize,function(d){c(d)}):c(a.alsoResize)}e(this).removeData("resizable-alsoresize")}});e.ui.plugin.add("resizable","animate",{stop:function(b){var a=e(this).data("resizable"),c=a.options,d=a._proportionallyResizeElements,f=d.length&&/textarea/i.test(d[0].nodeName),g=f&&e.ui.hasScroll(d[0],"left")?0:a.sizeDiff.height;f={width:a.size.width-(f?0:a.sizeDiff.width),height:a.size.height-g};g=parseInt(a.element.css("left"),10)+(a.position.left- +a.originalPosition.left)||null;var h=parseInt(a.element.css("top"),10)+(a.position.top-a.originalPosition.top)||null;a.element.animate(e.extend(f,h&&g?{top:h,left:g}:{}),{duration:c.animateDuration,easing:c.animateEasing,step:function(){var i={width:parseInt(a.element.css("width"),10),height:parseInt(a.element.css("height"),10),top:parseInt(a.element.css("top"),10),left:parseInt(a.element.css("left"),10)};d&&d.length&&e(d[0]).css({width:i.width,height:i.height});a._updateCache(i);a._propagate("resize", +b)}})}});e.ui.plugin.add("resizable","containment",{start:function(){var b=e(this).data("resizable"),a=b.element,c=b.options.containment;if(a=c instanceof e?c.get(0):/parent/.test(c)?a.parent().get(0):c){b.containerElement=e(a);if(/document/.test(c)||c==document){b.containerOffset={left:0,top:0};b.containerPosition={left:0,top:0};b.parentData={element:e(document),left:0,top:0,width:e(document).width(),height:e(document).height()||document.body.parentNode.scrollHeight}}else{var d=e(a),f=[];e(["Top", +"Right","Left","Bottom"]).each(function(i,j){f[i]=m(d.css("padding"+j))});b.containerOffset=d.offset();b.containerPosition=d.position();b.containerSize={height:d.innerHeight()-f[3],width:d.innerWidth()-f[1]};c=b.containerOffset;var g=b.containerSize.height,h=b.containerSize.width;h=e.ui.hasScroll(a,"left")?a.scrollWidth:h;g=e.ui.hasScroll(a)?a.scrollHeight:g;b.parentData={element:a,left:c.left,top:c.top,width:h,height:g}}}},resize:function(b){var a=e(this).data("resizable"),c=a.options,d=a.containerOffset, +f=a.position;b=a._aspectRatio||b.shiftKey;var g={top:0,left:0},h=a.containerElement;if(h[0]!=document&&/static/.test(h.css("position")))g=d;if(f.left<(a._helper?d.left:0)){a.size.width+=a._helper?a.position.left-d.left:a.position.left-g.left;if(b)a.size.height=a.size.width/c.aspectRatio;a.position.left=c.helper?d.left:0}if(f.top<(a._helper?d.top:0)){a.size.height+=a._helper?a.position.top-d.top:a.position.top;if(b)a.size.width=a.size.height*c.aspectRatio;a.position.top=a._helper?d.top:0}a.offset.left= +a.parentData.left+a.position.left;a.offset.top=a.parentData.top+a.position.top;c=Math.abs((a._helper?a.offset.left-g.left:a.offset.left-g.left)+a.sizeDiff.width);d=Math.abs((a._helper?a.offset.top-g.top:a.offset.top-d.top)+a.sizeDiff.height);f=a.containerElement.get(0)==a.element.parent().get(0);g=/relative|absolute/.test(a.containerElement.css("position"));if(f&&g)c-=a.parentData.left;if(c+a.size.width>=a.parentData.width){a.size.width=a.parentData.width-c;if(b)a.size.height=a.size.width/a.aspectRatio}if(d+ +a.size.height>=a.parentData.height){a.size.height=a.parentData.height-d;if(b)a.size.width=a.size.height*a.aspectRatio}},stop:function(){var b=e(this).data("resizable"),a=b.options,c=b.containerOffset,d=b.containerPosition,f=b.containerElement,g=e(b.helper),h=g.offset(),i=g.outerWidth()-b.sizeDiff.width;g=g.outerHeight()-b.sizeDiff.height;b._helper&&!a.animate&&/relative/.test(f.css("position"))&&e(this).css({left:h.left-d.left-c.left,width:i,height:g});b._helper&&!a.animate&&/static/.test(f.css("position"))&& +e(this).css({left:h.left-d.left-c.left,width:i,height:g})}});e.ui.plugin.add("resizable","ghost",{start:function(){var b=e(this).data("resizable"),a=b.options,c=b.size;b.ghost=b.originalElement.clone();b.ghost.css({opacity:0.25,display:"block",position:"relative",height:c.height,width:c.width,margin:0,left:0,top:0}).addClass("ui-resizable-ghost").addClass(typeof a.ghost=="string"?a.ghost:"");b.ghost.appendTo(b.helper)},resize:function(){var b=e(this).data("resizable");b.ghost&&b.ghost.css({position:"relative", +height:b.size.height,width:b.size.width})},stop:function(){var b=e(this).data("resizable");b.ghost&&b.helper&&b.helper.get(0).removeChild(b.ghost.get(0))}});e.ui.plugin.add("resizable","grid",{resize:function(){var b=e(this).data("resizable"),a=b.options,c=b.size,d=b.originalSize,f=b.originalPosition,g=b.axis;a.grid=typeof a.grid=="number"?[a.grid,a.grid]:a.grid;var h=Math.round((c.width-d.width)/(a.grid[0]||1))*(a.grid[0]||1);a=Math.round((c.height-d.height)/(a.grid[1]||1))*(a.grid[1]||1);if(/^(se|s|e)$/.test(g)){b.size.width= +d.width+h;b.size.height=d.height+a}else if(/^(ne)$/.test(g)){b.size.width=d.width+h;b.size.height=d.height+a;b.position.top=f.top-a}else{if(/^(sw)$/.test(g)){b.size.width=d.width+h;b.size.height=d.height+a}else{b.size.width=d.width+h;b.size.height=d.height+a;b.position.top=f.top-a}b.position.left=f.left-h}}});var m=function(b){return parseInt(b,10)||0},l=function(b){return!isNaN(parseInt(b,10))}})(jQuery); ;/* * jQuery UI Accordion 1.8.5 * @@ -119,222 +166,42 @@ c=a("").addClass("ui-button-text").html(this.options.label).appendT this.hasTitle||b.attr("title",c)}}else b.addClass("ui-button-text-only")}}});a.widget("ui.buttonset",{_create:function(){this.element.addClass("ui-buttonset");this._init()},_init:function(){this.refresh()},_setOption:function(b,c){b==="disabled"&&this.buttons.button("option",b,c);a.Widget.prototype._setOption.apply(this,arguments)},refresh:function(){this.buttons=this.element.find(":button, :submit, :reset, :checkbox, :radio, a, :data(button)").filter(":ui-button").button("refresh").end().not(":ui-button").button().end().map(function(){return a(this).button("widget")[0]}).removeClass("ui-corner-all ui-corner-left ui-corner-right").filter(":visible").filter(":first").addClass("ui-corner-left").end().filter(":last").addClass("ui-corner-right").end().end().end()}, destroy:function(){this.element.removeClass("ui-buttonset");this.buttons.map(function(){return a(this).button("widget")[0]}).removeClass("ui-corner-left ui-corner-right").end().button("destroy");a.Widget.prototype.destroy.call(this)}})})(jQuery); ;/* - * jQuery UI Effects 1.8.5 + * jQuery UI Dialog 1.8.5 * * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license * - * http://docs.jquery.com/UI/Effects/ - */ -jQuery.effects||function(f,j){function l(c){var a;if(c&&c.constructor==Array&&c.length==3)return c;if(a=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(c))return[parseInt(a[1],10),parseInt(a[2],10),parseInt(a[3],10)];if(a=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(c))return[parseFloat(a[1])*2.55,parseFloat(a[2])*2.55,parseFloat(a[3])*2.55];if(a=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(c))return[parseInt(a[1], -16),parseInt(a[2],16),parseInt(a[3],16)];if(a=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(c))return[parseInt(a[1]+a[1],16),parseInt(a[2]+a[2],16),parseInt(a[3]+a[3],16)];if(/rgba\(0, 0, 0, 0\)/.exec(c))return m.transparent;return m[f.trim(c).toLowerCase()]}function r(c,a){var b;do{b=f.curCSS(c,a);if(b!=""&&b!="transparent"||f.nodeName(c,"body"))break;a="backgroundColor"}while(c=c.parentNode);return l(b)}function n(){var c=document.defaultView?document.defaultView.getComputedStyle(this,null):this.currentStyle, -a={},b,d;if(c&&c.length&&c[0]&&c[c[0]])for(var e=c.length;e--;){b=c[e];if(typeof c[b]=="string"){d=b.replace(/\-(\w)/g,function(g,h){return h.toUpperCase()});a[d]=c[b]}}else for(b in c)if(typeof c[b]==="string")a[b]=c[b];return a}function o(c){var a,b;for(a in c){b=c[a];if(b==null||f.isFunction(b)||a in s||/scrollbar/.test(a)||!/color/i.test(a)&&isNaN(parseFloat(b)))delete c[a]}return c}function t(c,a){var b={_:0},d;for(d in a)if(c[d]!=a[d])b[d]=a[d];return b}function k(c,a,b,d){if(typeof c=="object"){d= -a;b=null;a=c;c=a.effect}if(f.isFunction(a)){d=a;b=null;a={}}if(typeof a=="number"||f.fx.speeds[a]){d=b;b=a;a={}}if(f.isFunction(b)){d=b;b=null}a=a||{};b=b||a.duration;b=f.fx.off?0:typeof b=="number"?b:f.fx.speeds[b]||f.fx.speeds._default;d=d||a.complete;return[c,a,b,d]}f.effects={};f.each(["backgroundColor","borderBottomColor","borderLeftColor","borderRightColor","borderTopColor","color","outlineColor"],function(c,a){f.fx.step[a]=function(b){if(!b.colorInit){b.start=r(b.elem,a);b.end=l(b.end);b.colorInit= -true}b.elem.style[a]="rgb("+Math.max(Math.min(parseInt(b.pos*(b.end[0]-b.start[0])+b.start[0],10),255),0)+","+Math.max(Math.min(parseInt(b.pos*(b.end[1]-b.start[1])+b.start[1],10),255),0)+","+Math.max(Math.min(parseInt(b.pos*(b.end[2]-b.start[2])+b.start[2],10),255),0)+")"}});var m={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189, -183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255, -165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0],transparent:[255,255,255]},p=["add","remove","toggle"],s={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};f.effects.animateClass=function(c,a,b,d){if(f.isFunction(b)){d=b;b=null}return this.each(function(){var e=f(this),g=e.attr("style")||" ",h=o(n.call(this)),q,u=e.attr("className");f.each(p,function(v, -i){c[i]&&e[i+"Class"](c[i])});q=o(n.call(this));e.attr("className",u);e.animate(t(h,q),a,b,function(){f.each(p,function(v,i){c[i]&&e[i+"Class"](c[i])});if(typeof e.attr("style")=="object"){e.attr("style").cssText="";e.attr("style").cssText=g}else e.attr("style",g);d&&d.apply(this,arguments)})})};f.fn.extend({_addClass:f.fn.addClass,addClass:function(c,a,b,d){return a?f.effects.animateClass.apply(this,[{add:c},a,b,d]):this._addClass(c)},_removeClass:f.fn.removeClass,removeClass:function(c,a,b,d){return a? -f.effects.animateClass.apply(this,[{remove:c},a,b,d]):this._removeClass(c)},_toggleClass:f.fn.toggleClass,toggleClass:function(c,a,b,d,e){return typeof a=="boolean"||a===j?b?f.effects.animateClass.apply(this,[a?{add:c}:{remove:c},b,d,e]):this._toggleClass(c,a):f.effects.animateClass.apply(this,[{toggle:c},a,b,d])},switchClass:function(c,a,b,d,e){return f.effects.animateClass.apply(this,[{add:a,remove:c},b,d,e])}});f.extend(f.effects,{version:"1.8.5",save:function(c,a){for(var b=0;b").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0});c.wrap(b);b=c.parent();if(c.css("position")=="static"){b.css({position:"relative"});c.css({position:"relative"})}else{f.extend(a,{position:c.css("position"),zIndex:c.css("z-index")});f.each(["top","left","bottom","right"],function(d,e){a[e]=c.css(e);if(isNaN(parseInt(a[e],10)))a[e]="auto"}); -c.css({position:"relative",top:0,left:0})}return b.css(a).show()},removeWrapper:function(c){if(c.parent().is(".ui-effects-wrapper"))return c.parent().replaceWith(c);return c},setTransition:function(c,a,b,d){d=d||{};f.each(a,function(e,g){unit=c.cssUnit(g);if(unit[0]>0)d[g]=unit[0]*b+unit[1]});return d}});f.fn.extend({effect:function(c){var a=k.apply(this,arguments);a={options:a[1],duration:a[2],callback:a[3]};var b=f.effects[c];return b&&!f.fx.off?b.call(this,a):this},_show:f.fn.show,show:function(c){if(!c|| -typeof c=="number"||f.fx.speeds[c]||!f.effects[c])return this._show.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="show";return this.effect.apply(this,a)}},_hide:f.fn.hide,hide:function(c){if(!c||typeof c=="number"||f.fx.speeds[c]||!f.effects[c])return this._hide.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="hide";return this.effect.apply(this,a)}},__toggle:f.fn.toggle,toggle:function(c){if(!c||typeof c=="number"||f.fx.speeds[c]||!f.effects[c]||typeof c== -"boolean"||f.isFunction(c))return this.__toggle.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="toggle";return this.effect.apply(this,a)}},cssUnit:function(c){var a=this.css(c),b=[];f.each(["em","px","%","pt"],function(d,e){if(a.indexOf(e)>0)b=[parseFloat(a),e]});return b}});f.easing.jswing=f.easing.swing;f.extend(f.easing,{def:"easeOutQuad",swing:function(c,a,b,d,e){return f.easing[f.easing.def](c,a,b,d,e)},easeInQuad:function(c,a,b,d,e){return d*(a/=e)*a+b},easeOutQuad:function(c, -a,b,d,e){return-d*(a/=e)*(a-2)+b},easeInOutQuad:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a+b;return-d/2*(--a*(a-2)-1)+b},easeInCubic:function(c,a,b,d,e){return d*(a/=e)*a*a+b},easeOutCubic:function(c,a,b,d,e){return d*((a=a/e-1)*a*a+1)+b},easeInOutCubic:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a+b;return d/2*((a-=2)*a*a+2)+b},easeInQuart:function(c,a,b,d,e){return d*(a/=e)*a*a*a+b},easeOutQuart:function(c,a,b,d,e){return-d*((a=a/e-1)*a*a*a-1)+b},easeInOutQuart:function(c,a,b,d,e){if((a/= -e/2)<1)return d/2*a*a*a*a+b;return-d/2*((a-=2)*a*a*a-2)+b},easeInQuint:function(c,a,b,d,e){return d*(a/=e)*a*a*a*a+b},easeOutQuint:function(c,a,b,d,e){return d*((a=a/e-1)*a*a*a*a+1)+b},easeInOutQuint:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a*a*a+b;return d/2*((a-=2)*a*a*a*a+2)+b},easeInSine:function(c,a,b,d,e){return-d*Math.cos(a/e*(Math.PI/2))+d+b},easeOutSine:function(c,a,b,d,e){return d*Math.sin(a/e*(Math.PI/2))+b},easeInOutSine:function(c,a,b,d,e){return-d/2*(Math.cos(Math.PI*a/e)-1)+ -b},easeInExpo:function(c,a,b,d,e){return a==0?b:d*Math.pow(2,10*(a/e-1))+b},easeOutExpo:function(c,a,b,d,e){return a==e?b+d:d*(-Math.pow(2,-10*a/e)+1)+b},easeInOutExpo:function(c,a,b,d,e){if(a==0)return b;if(a==e)return b+d;if((a/=e/2)<1)return d/2*Math.pow(2,10*(a-1))+b;return d/2*(-Math.pow(2,-10*--a)+2)+b},easeInCirc:function(c,a,b,d,e){return-d*(Math.sqrt(1-(a/=e)*a)-1)+b},easeOutCirc:function(c,a,b,d,e){return d*Math.sqrt(1-(a=a/e-1)*a)+b},easeInOutCirc:function(c,a,b,d,e){if((a/=e/2)<1)return-d/ -2*(Math.sqrt(1-a*a)-1)+b;return d/2*(Math.sqrt(1-(a-=2)*a)+1)+b},easeInElastic:function(c,a,b,d,e){c=1.70158;var g=0,h=d;if(a==0)return b;if((a/=e)==1)return b+d;g||(g=e*0.3);if(h").css({position:"absolute",visibility:"visible",left:-f*(h/d),top:-e*(i/c)}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:h/d,height:i/c,left:g.left+f*(h/d)+(a.options.mode=="show"?(f-Math.floor(d/2))*(h/d):0),top:g.top+e*(i/c)+(a.options.mode=="show"?(e-Math.floor(c/2))*(i/c):0),opacity:a.options.mode=="show"?0:1}).animate({left:g.left+f*(h/d)+(a.options.mode=="show"?0:(f-Math.floor(d/2))*(h/d)),top:g.top+ -e*(i/c)+(a.options.mode=="show"?0:(e-Math.floor(c/2))*(i/c)),opacity:a.options.mode=="show"?1:0},a.duration||500);setTimeout(function(){a.options.mode=="show"?b.css({visibility:"visible"}):b.css({visibility:"visible"}).hide();a.callback&&a.callback.apply(b[0]);b.dequeue();j("div.ui-effects-explode").remove()},a.duration||500)})}})(jQuery); -;/* - * jQuery UI Effects Fade 1.8.5 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/Fade - * - * Depends: - * jquery.effects.core.js - */ -(function(b){b.effects.fade=function(a){return this.queue(function(){var c=b(this),d=b.effects.setMode(c,a.options.mode||"hide");c.animate({opacity:d},{queue:false,duration:a.duration,easing:a.options.easing,complete:function(){a.callback&&a.callback.apply(this,arguments);c.dequeue()}})})}})(jQuery); -;/* - * jQuery UI Effects Fold 1.8.5 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/Fold - * - * Depends: - * jquery.effects.core.js - */ -(function(c){c.effects.fold=function(a){return this.queue(function(){var b=c(this),j=["position","top","left"],d=c.effects.setMode(b,a.options.mode||"hide"),g=a.options.size||15,h=!!a.options.horizFirst,k=a.duration?a.duration/2:c.fx.speeds._default/2;c.effects.save(b,j);b.show();var e=c.effects.createWrapper(b).css({overflow:"hidden"}),f=d=="show"!=h,l=f?["width","height"]:["height","width"];f=f?[e.width(),e.height()]:[e.height(),e.width()];var i=/([0-9]+)%/.exec(g);if(i)g=parseInt(i[1],10)/100* -f[d=="hide"?0:1];if(d=="show")e.css(h?{height:0,width:g}:{height:g,width:0});h={};i={};h[l[0]]=d=="show"?f[0]:g;i[l[1]]=d=="show"?f[1]:0;e.animate(h,k,a.options.easing).animate(i,k,a.options.easing,function(){d=="hide"&&b.hide();c.effects.restore(b,j);c.effects.removeWrapper(b);a.callback&&a.callback.apply(b[0],arguments);b.dequeue()})})}})(jQuery); -;/* - * jQuery UI Effects Highlight 1.8.5 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/Highlight - * - * Depends: - * jquery.effects.core.js - */ -(function(b){b.effects.highlight=function(c){return this.queue(function(){var a=b(this),e=["backgroundImage","backgroundColor","opacity"],d=b.effects.setMode(a,c.options.mode||"show"),f={backgroundColor:a.css("backgroundColor")};if(d=="hide")f.opacity=0;b.effects.save(a,e);a.show().css({backgroundImage:"none",backgroundColor:c.options.color||"#ffff99"}).animate(f,{queue:false,duration:c.duration,easing:c.options.easing,complete:function(){d=="hide"&&a.hide();b.effects.restore(a,e);d=="show"&&!b.support.opacity&& -this.style.removeAttribute("filter");c.callback&&c.callback.apply(this,arguments);a.dequeue()}})})}})(jQuery); -;/* - * jQuery UI Effects Pulsate 1.8.5 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/Pulsate - * - * Depends: - * jquery.effects.core.js - */ -(function(d){d.effects.pulsate=function(a){return this.queue(function(){var b=d(this),c=d.effects.setMode(b,a.options.mode||"show");times=(a.options.times||5)*2-1;duration=a.duration?a.duration/2:d.fx.speeds._default/2;isVisible=b.is(":visible");animateTo=0;if(!isVisible){b.css("opacity",0).show();animateTo=1}if(c=="hide"&&isVisible||c=="show"&&!isVisible)times--;for(c=0;c').appendTo(document.body).addClass(a.options.className).css({top:d.top,left:d.left,height:b.innerHeight(),width:b.innerWidth(),position:"absolute"}).animate(c,a.duration,a.options.easing,function(){f.remove();a.callback&&a.callback.apply(b[0],arguments); -b.dequeue()})})}})(jQuery); +(function(c,j){c.widget("ui.dialog",{options:{autoOpen:true,buttons:{},closeOnEscape:true,closeText:"close",dialogClass:"",draggable:true,hide:null,height:"auto",maxHeight:false,maxWidth:false,minHeight:150,minWidth:150,modal:false,position:{my:"center",at:"center",of:window,collision:"fit",using:function(a){var b=c(this).css(a).offset().top;b<0&&c(this).css("top",a.top-b)}},resizable:true,show:null,stack:true,title:"",width:300,zIndex:1E3},_create:function(){this.originalTitle=this.element.attr("title"); +if(typeof this.originalTitle!=="string")this.originalTitle="";this.options.title=this.options.title||this.originalTitle;var a=this,b=a.options,d=b.title||" ",f=c.ui.dialog.getTitleId(a.element),g=(a.uiDialog=c("
    ")).appendTo(document.body).hide().addClass("ui-dialog ui-widget ui-widget-content ui-corner-all "+b.dialogClass).css({zIndex:b.zIndex}).attr("tabIndex",-1).css("outline",0).keydown(function(i){if(b.closeOnEscape&&i.keyCode&&i.keyCode===c.ui.keyCode.ESCAPE){a.close(i);i.preventDefault()}}).attr({role:"dialog", +"aria-labelledby":f}).mousedown(function(i){a.moveToTop(false,i)});a.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(g);var e=(a.uiDialogTitlebar=c("
    ")).addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix").prependTo(g),h=c('').addClass("ui-dialog-titlebar-close ui-corner-all").attr("role","button").hover(function(){h.addClass("ui-state-hover")},function(){h.removeClass("ui-state-hover")}).focus(function(){h.addClass("ui-state-focus")}).blur(function(){h.removeClass("ui-state-focus")}).click(function(i){a.close(i); +return false}).appendTo(e);(a.uiDialogTitlebarCloseText=c("")).addClass("ui-icon ui-icon-closethick").text(b.closeText).appendTo(h);c("").addClass("ui-dialog-title").attr("id",f).html(d).prependTo(e);if(c.isFunction(b.beforeclose)&&!c.isFunction(b.beforeClose))b.beforeClose=b.beforeclose;e.find("*").add(e).disableSelection();b.draggable&&c.fn.draggable&&a._makeDraggable();b.resizable&&c.fn.resizable&&a._makeResizable();a._createButtons(b.buttons);a._isOpen=false;c.fn.bgiframe&& +g.bgiframe()},_init:function(){this.options.autoOpen&&this.open()},destroy:function(){var a=this;a.overlay&&a.overlay.destroy();a.uiDialog.hide();a.element.unbind(".dialog").removeData("dialog").removeClass("ui-dialog-content ui-widget-content").hide().appendTo("body");a.uiDialog.remove();a.originalTitle&&a.element.attr("title",a.originalTitle);return a},widget:function(){return this.uiDialog},close:function(a){var b=this,d;if(false!==b._trigger("beforeClose",a)){b.overlay&&b.overlay.destroy();b.uiDialog.unbind("keypress.ui-dialog"); +b._isOpen=false;if(b.options.hide)b.uiDialog.hide(b.options.hide,function(){b._trigger("close",a)});else{b.uiDialog.hide();b._trigger("close",a)}c.ui.dialog.overlay.resize();if(b.options.modal){d=0;c(".ui-dialog").each(function(){if(this!==b.uiDialog[0])d=Math.max(d,c(this).css("z-index"))});c.ui.dialog.maxZ=d}return b}},isOpen:function(){return this._isOpen},moveToTop:function(a,b){var d=this,f=d.options;if(f.modal&&!a||!f.stack&&!f.modal)return d._trigger("focus",b);if(f.zIndex>c.ui.dialog.maxZ)c.ui.dialog.maxZ= +f.zIndex;if(d.overlay){c.ui.dialog.maxZ+=1;d.overlay.$el.css("z-index",c.ui.dialog.overlay.maxZ=c.ui.dialog.maxZ)}a={scrollTop:d.element.attr("scrollTop"),scrollLeft:d.element.attr("scrollLeft")};c.ui.dialog.maxZ+=1;d.uiDialog.css("z-index",c.ui.dialog.maxZ);d.element.attr(a);d._trigger("focus",b);return d},open:function(){if(!this._isOpen){var a=this,b=a.options,d=a.uiDialog;a.overlay=b.modal?new c.ui.dialog.overlay(a):null;d.next().length&&d.appendTo("body");a._size();a._position(b.position);d.show(b.show); +a.moveToTop(true);b.modal&&d.bind("keypress.ui-dialog",function(f){if(f.keyCode===c.ui.keyCode.TAB){var g=c(":tabbable",this),e=g.filter(":first");g=g.filter(":last");if(f.target===g[0]&&!f.shiftKey){e.focus(1);return false}else if(f.target===e[0]&&f.shiftKey){g.focus(1);return false}}});c(a.element.find(":tabbable").get().concat(d.find(".ui-dialog-buttonpane :tabbable").get().concat(d.get()))).eq(0).focus();a._isOpen=true;a._trigger("open");return a}},_createButtons:function(a){var b=this,d=false, +f=c("
    ").addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"),g=c("
    ").addClass("ui-dialog-buttonset").appendTo(f);b.uiDialog.find(".ui-dialog-buttonpane").remove();typeof a==="object"&&a!==null&&c.each(a,function(){return!(d=true)});if(d){c.each(a,function(e,h){h=c.isFunction(h)?{click:h,text:e}:h;e=c("",h).unbind("click").click(function(){h.click.apply(b.element[0],arguments)}).appendTo(g);c.fn.button&&e.button()});f.appendTo(b.uiDialog)}},_makeDraggable:function(){function a(e){return{position:e.position, +offset:e.offset}}var b=this,d=b.options,f=c(document),g;b.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close",handle:".ui-dialog-titlebar",containment:"document",start:function(e,h){g=d.height==="auto"?"auto":c(this).height();c(this).height(c(this).height()).addClass("ui-dialog-dragging");b._trigger("dragStart",e,a(h))},drag:function(e,h){b._trigger("drag",e,a(h))},stop:function(e,h){d.position=[h.position.left-f.scrollLeft(),h.position.top-f.scrollTop()];c(this).removeClass("ui-dialog-dragging").height(g); +b._trigger("dragStop",e,a(h));c.ui.dialog.overlay.resize()}})},_makeResizable:function(a){function b(e){return{originalPosition:e.originalPosition,originalSize:e.originalSize,position:e.position,size:e.size}}a=a===j?this.options.resizable:a;var d=this,f=d.options,g=d.uiDialog.css("position");a=typeof a==="string"?a:"n,e,s,w,se,sw,ne,nw";d.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:d.element,maxWidth:f.maxWidth,maxHeight:f.maxHeight,minWidth:f.minWidth,minHeight:d._minHeight(), +handles:a,start:function(e,h){c(this).addClass("ui-dialog-resizing");d._trigger("resizeStart",e,b(h))},resize:function(e,h){d._trigger("resize",e,b(h))},stop:function(e,h){c(this).removeClass("ui-dialog-resizing");f.height=c(this).height();f.width=c(this).width();d._trigger("resizeStop",e,b(h));c.ui.dialog.overlay.resize()}}).css("position",g).find(".ui-resizable-se").addClass("ui-icon ui-icon-grip-diagonal-se")},_minHeight:function(){var a=this.options;return a.height==="auto"?a.minHeight:Math.min(a.minHeight, +a.height)},_position:function(a){var b=[],d=[0,0],f;if(a){if(typeof a==="string"||typeof a==="object"&&"0"in a){b=a.split?a.split(" "):[a[0],a[1]];if(b.length===1)b[1]=b[0];c.each(["left","top"],function(g,e){if(+b[g]===b[g]){d[g]=b[g];b[g]=e}});a={my:b.join(" "),at:b.join(" "),offset:d.join(" ")}}a=c.extend({},c.ui.dialog.prototype.options.position,a)}else a=c.ui.dialog.prototype.options.position;(f=this.uiDialog.is(":visible"))||this.uiDialog.show();this.uiDialog.css({top:0,left:0}).position(a); +f||this.uiDialog.hide()},_setOption:function(a,b){var d=this,f=d.uiDialog,g=f.is(":data(resizable)"),e=false;switch(a){case "beforeclose":a="beforeClose";break;case "buttons":d._createButtons(b);e=true;break;case "closeText":d.uiDialogTitlebarCloseText.text(""+b);break;case "dialogClass":f.removeClass(d.options.dialogClass).addClass("ui-dialog ui-widget ui-widget-content ui-corner-all "+b);break;case "disabled":b?f.addClass("ui-dialog-disabled"):f.removeClass("ui-dialog-disabled");break;case "draggable":b? +d._makeDraggable():f.draggable("destroy");break;case "height":e=true;break;case "maxHeight":g&&f.resizable("option","maxHeight",b);e=true;break;case "maxWidth":g&&f.resizable("option","maxWidth",b);e=true;break;case "minHeight":g&&f.resizable("option","minHeight",b);e=true;break;case "minWidth":g&&f.resizable("option","minWidth",b);e=true;break;case "position":d._position(b);break;case "resizable":g&&!b&&f.resizable("destroy");g&&typeof b==="string"&&f.resizable("option","handles",b);!g&&b!==false&& +d._makeResizable(b);break;case "title":c(".ui-dialog-title",d.uiDialogTitlebar).html(""+(b||" "));break;case "width":e=true;break}c.Widget.prototype._setOption.apply(d,arguments);e&&d._size()},_size:function(){var a=this.options,b;this.element.css({width:"auto",minHeight:0,height:0});if(a.minWidth>a.width)a.width=a.minWidth;b=this.uiDialog.css({height:"auto",width:a.width}).height();this.element.css(a.height==="auto"?{minHeight:Math.max(a.minHeight-b,0),height:c.support.minHeight?"auto":Math.max(a.minHeight- +b,0)}:{minHeight:0,height:Math.max(a.height-b,0)}).show();this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())}});c.extend(c.ui.dialog,{version:"1.8.5",uuid:0,maxZ:0,getTitleId:function(a){a=a.attr("id");if(!a){this.uuid+=1;a=this.uuid}return"ui-dialog-title-"+a},overlay:function(a){this.$el=c.ui.dialog.overlay.create(a)}});c.extend(c.ui.dialog.overlay,{instances:[],oldInstances:[],maxZ:0,events:c.map("focus,mousedown,mouseup,keydown,keypress,click".split(","), +function(a){return a+".dialog-overlay"}).join(" "),create:function(a){if(this.instances.length===0){setTimeout(function(){c.ui.dialog.overlay.instances.length&&c(document).bind(c.ui.dialog.overlay.events,function(d){if(c(d.target).zIndex()").addClass("ui-widget-overlay")).appendTo(document.body).css({width:this.width(),height:this.height()});c.fn.bgiframe&&b.bgiframe();this.instances.push(b);return b},destroy:function(a){this.oldInstances.push(this.instances.splice(c.inArray(a,this.instances),1)[0]);this.instances.length===0&&c([document,window]).unbind(".dialog-overlay");a.remove();var b=0;c.each(this.instances,function(){b=Math.max(b,this.css("z-index"))});this.maxZ=b},height:function(){var a, +b;if(c.browser.msie&&c.browser.version<7){a=Math.max(document.documentElement.scrollHeight,document.body.scrollHeight);b=Math.max(document.documentElement.offsetHeight,document.body.offsetHeight);return a Date: Fri, 15 Oct 2010 19:57:41 -0600 Subject: [PATCH 31/75] /browse: Show category being currently listed and hide sort combobox in newest book list --- resources/content_server/browse/browse.js | 3 ++- src/calibre/library/server/browse.py | 16 ++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/resources/content_server/browse/browse.js b/resources/content_server/browse/browse.js index 397db397bf..5e0d9f8ee4 100644 --- a/resources/content_server/browse/browse.js +++ b/resources/content_server/browse/browse.js @@ -193,7 +193,8 @@ function load_page(elem) { elem.show(); } -function booklist() { +function booklist(hide_sort) { + if (hide_sort) $("#content > .sort_select").hide(); var test = $("#booklist #page0").html(); if (!test) { $("#booklist").html(render_error("No books found")); diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index 69a931b1e2..c8c816d617 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -18,7 +18,7 @@ from calibre.utils.filenames import ascii_filename from calibre.utils.config import prefs from calibre.library.comments import comments_to_html -def render_book_list(ids): # {{{ +def render_book_list(ids, suffix=''): # {{{ pages = [] num = len(ids) pos = 0 @@ -49,7 +49,7 @@ def render_book_list(ids): # {{{ rpages = u'\n\n'.join(rpages) templ = u'''\ -

    {0}

    +

    {0} {suffix}

    {navbar} @@ -76,7 +76,8 @@ def render_book_list(ids): # {{{ '''.format(first=_('First'), last=_('Last'), previous=_('Previous'), next=_('Next'), num=num) - return templ.format(_('Browsing %d books')%num, pages=rpages, navbar=navbar) + return templ.format(_('Browsing %d books')%num, suffix=suffix, + pages=rpages, navbar=navbar) # }}} @@ -420,6 +421,7 @@ class BrowseServer(object): raise category_name = _('Newest') + hide_sort = 'false' if category == 'search': which = unhexlify(cid) try: @@ -428,6 +430,7 @@ class BrowseServer(object): raise cherrypy.HTTPError(404, 'Search: %r not understood'%which) elif category == 'newest': ids = list(self.db.data.iterallids()) + hide_sort = 'true' else: ids = self.db.get_books_for_category(category, cid) @@ -436,10 +439,11 @@ class BrowseServer(object): list_sort = 'timestamp' sort = self.browse_sort_book_list(items, list_sort) ids = [x[0] for x in items] - html = render_book_list(ids) + html = render_book_list(ids, suffix=_('in') + ' ' + category_name) + return self.browse_template(sort, category=False).format( title=_('Books in') + " " +category_name, - script='booklist();', main=html) + script='booklist(%s);'%hide_sort, main=html) @Endpoint(mimetype='application/json; charset=utf-8') def browse_booklist_page(self, ids=None, sort=None): @@ -509,7 +513,7 @@ class BrowseServer(object): items = [self.db.data._data[x] for x in ids] sort = self.browse_sort_book_list(items, list_sort) ids = [x[0] for x in items] - html = render_book_list(ids) + html = render_book_list(ids, suffix=_('in search')+': '+query) return self.browse_template(sort, category=False, initial_search=query).format( title=_('Matching books'), script='booklist();', main=html) From 5c02ca217343ee71004dfdcb1e8d19f653134b97 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 15 Oct 2010 19:59:29 -0600 Subject: [PATCH 32/75] ... --- src/calibre/library/server/browse.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index c8c816d617..439c0f4cf2 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -191,6 +191,7 @@ class BrowseServer(object): connect('browse_search', base_href+'/search', self.browse_search) + # Templates {{{ def browse_template(self, sort, category=True, initial_search=''): def generate(): @@ -243,6 +244,7 @@ class BrowseServer(object): P('content_server/browse/summary.html', data=True).decode('utf-8') return self.__browse_summary_template__ + # }}} # Catalogs {{{ def browse_toplevel(self): From 5b1cfa68650b742fabf781b5cc011433e1581ea7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 15 Oct 2010 20:49:57 -0600 Subject: [PATCH 33/75] ... --- resources/content_server/browse/browse.css | 12 +++-- resources/content_server/browse/browse.html | 1 + resources/content_server/browse/browse.js | 15 +++++- resources/content_server/browse/summary.html | 6 ++- .../images/ui-bg_glass_100_f5f0e5_1x400.png | Bin 157 -> 123 bytes .../images/ui-bg_glass_70_ede4d4_1x400.png | Bin 125 -> 161 bytes .../ui-bg_inset-soft_100_f4f0ec_1x100.png | Bin 141 -> 113 bytes .../images/ui-icons_f08000_256x240.png | Bin 5355 -> 4369 bytes .../images/ui-icons_f35f07_256x240.png | Bin 4369 -> 5355 bytes .../images/ui-icons_ff7519_256x240.png | Bin 4369 -> 5355 bytes .../jquery-ui-1.8.5.custom.css | 44 +++++++++++++++++- src/calibre/library/server/browse.py | 6 ++- 12 files changed, 76 insertions(+), 8 deletions(-) diff --git a/resources/content_server/browse/browse.css b/resources/content_server/browse/browse.css index f3dfc89caf..aceb595383 100644 --- a/resources/content_server/browse/browse.css +++ b/resources/content_server/browse/browse.css @@ -309,21 +309,27 @@ h2.library_name { display: block; } +#booklist div.right .stars .series { + display: block; +} + #booklist .title { font-size: larger; } -#booklist .formats a { +#booklist a { text-decoration: none; color: blue; } -#booklist .formats a:hover { +#booklist a:hover { color: red; } + #booklist .left .ui-button-text { - font-weight: bold; + font-size: medium; + color: black; padding-left: 0.25em; padding-right: 0.25em; padding-top: 0.25em; diff --git a/resources/content_server/browse/browse.html b/resources/content_server/browse/browse.html index 8c6d310015..b3039f8c4e 100644 --- a/resources/content_server/browse/browse.html +++ b/resources/content_server/browse/browse.html @@ -94,5 +94,6 @@
    + diff --git a/resources/content_server/browse/browse.js b/resources/content_server/browse/browse.js index 5e0d9f8ee4..9d3c01fa15 100644 --- a/resources/content_server/browse/browse.js +++ b/resources/content_server/browse/browse.js @@ -200,8 +200,21 @@ function booklist(hide_sort) { $("#booklist").html(render_error("No books found")); return; } - + $("#book_details_dialog").dialog({ + autoOpen: false, + modal: true + }); first_page(); } +function show_details(a_dom) { + var book = $(a_dom).closest('div.summary'); + var id = book.attr('id').split('_')[1]; + var bd = $('#book_details_dialog'); + bd.attr('title', 'test'); + bd.html('test'); + bd.dialog('option', 'width', $('#container').width() - 50); + bd.dialog('open'); +} + // }}} diff --git a/resources/content_server/browse/summary.html b/resources/content_server/browse/summary.html index 01f2b333f2..8df42de366 100644 --- a/resources/content_server/browse/summary.html +++ b/resources/content_server/browse/summary.html @@ -4,7 +4,11 @@ {read_string}
    -
    {stars}{series}
    +
    + {stars} + {series} + {details} +
    {title}
    {authors}
    {comments}
    diff --git a/resources/content_server/jquery_ui/css/humanity-custom/images/ui-bg_glass_100_f5f0e5_1x400.png b/resources/content_server/jquery_ui/css/humanity-custom/images/ui-bg_glass_100_f5f0e5_1x400.png index 688c2f3e8806f3afcc0b659d397d142ee4a0bb1c..4fb71be1b40892d5b6ef1e4091104a876a9892f6 100644 GIT binary patch delta 101 zcmV-r0Gj`u0eg@fUjT1d3flkx076MbK~y-6?awg^fKUts(V4yfUF|+7f)?_%u@_{U z$0Vc)>sXEelu~}nzKuC55oZ|!5|ve*qX@I!yoo0Axu-K~y-6?a?s`!!Qs;(Z63-Ain=S-6_bCiJKHT zfUyKh^VqF+hNYJ6BR~(@UNGe+njSYFr)%@0Rb29lgi|%|N_)RhGnz7n%Lu7%VB(+8 qOWO-Zh++Dj2)`@D2uc53#0gdk9<2slO@jac002ovPDHLk0$_qBz&J<% diff --git a/resources/content_server/jquery_ui/css/humanity-custom/images/ui-bg_glass_70_ede4d4_1x400.png b/resources/content_server/jquery_ui/css/humanity-custom/images/ui-bg_glass_70_ede4d4_1x400.png index 779ef0d0e24ce761cdbf952b5f1db4c80face3fe..0b4ae4776b3627a480c7fa2a04a7198c9a8f6584 100644 GIT binary patch delta 140 zcmV;70CWF+p#hK_e*qX@I!yoo0BA`>K~y-6?a@IB!axv2(ciz@D1!Gto(FK{A|W9W z-Q)<85ys7A%C=0HHSC1-3#|^I uCuV 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 delta 5037 zcmV;e6H@GvBI_xT90dWdPI_;#Ayol?6JJS0K~#90?Oh9F+qMw~$x2$Nn4pts2rkicqs$`0#`RooLep(~$=Dh)P5Z%?UHqR&Q(6DAs zo2i7L)F=(Hpn)7PCAh>RawtSt6Ie?Yrcr}TvtCM@0~9J5v`du%z{CZO%CUM>n6g2^ zw6M16e*bx7EmR#$X=U>lQ>#;dC`*9q#fTgllsq8o>tccY+nmiFk0xM;t zjqt1(`8g^%#{U^g8Y<;hMEalMZ}XMTz)NcBWE( zQ<#bcNToJ+-(PmY4PzZu%YrqQd6z(b8TV4xsvnKbBhdy*Iqd8rfBp|4>W;qoO3MX0 z&SaG&hb|4oGeB-uWp|zAC7E~QJ|h#C0nm29v49Xw7ev4s@o%39j}Xv&`_f(^ZbuPHbYz#BobCFnnASABo5Nn(idW!4EEoTfEF{r zQZ$&?ZS&}F(B+p^=wY6u=X3paLtQt2JP>6!EWErl38&i9 z$=}Ma))`e54v*Eu99I{dr!ATK%|pKy^vHHMqQiAmdnGErv^n|GBG2W&R)p-+dj8>0 z#73NlRk;Wh1vuf+j41$Fe!MFg;3eW%tu*gbRAE;{-BS)%)5v9Nbq19YUenQ&Nf__V zO%G=;Um6Zgy>DfIBH1RYei>~bgD2t&bNWw{h|^b+H}oxqvwz^%JB2eI;du3C684e^ zH+iFSh#J%*L%E&gD>-9pO&47*^*C`^j2L7iQoZqDko^F^ui5vi=WAhAUTda=_9Of+ zP(Lblt|Z2Oj|j5QWeipgN@dox8~pofF0j|UVP@-8ytf91JLkd+k1Rr z!7M*(zJNt9cE!Mhg=+&$w#+}-1^}aPJv(IAcx)Tk9Rm*)z(vk`6TkRDIzM5MO)|eE zM{$NiE0t&y0f$2cK6H>}Urzxz!LjhbWcZD!Fn3UFwndO&zzy|5LCJ#0!&~VtpTL@t zdJ$x`VE|Tt#DLQyPvkTv^9L5{(`)oU)9C!hRs^wgfa~S*p9~QvkUT+lhRkLM9WnT}$o|QtOM$r4oO| zwhTcR4^;G1jM43v^lfK4EG|zT(0kbcDTL<%D{3tReK+VaqV`hYaOw^ zsErW<9q)@TzAzkuSKz`cKcnmC720vIB1c!tsG6GPV0X==`|DS~LF3`O=jTS|EzJ;M zVbklY0A;IPoo*t~7@1;CLS4LYD>bFwzX5-o{S7mmM_p6HjxI3+Sz&mo%PH~R!-EHi z)m!|3<-0thzN9PYKJv#4b8Wv7-{@40))&}@DzkIRw4L(AEKA$Auw1H$_1kG`U>{}R z-yH*TS-!#>5das;IeB5)(5VWjOrv}T&z>m;Ok1!FR5wSX2&U@)6~MRmQv4IZ^-#VL zP`ZvJ(yDt1%*+;mq)ro2N%!`lFV=DRNjGCI*2m1uPEOq7k#KN{6I8aqC0 zJW)4B;^=nX%|RJJY|X%f2Z$ey49lP!RRivh7{IyS^uil#Mg}O<{d9mffV)2cyz6sQ z>4sbm0Pn#I`TJH+!??et6Rz-fSAe+z2lBxplTXypqYbz-1>JR|yzTzM(8Lv#Ch1sz zdZY#T?x)vyb_O#C3|x0Vz;{1A1~LN4R$SE&jMw+XKqaBWaj2NvKt>!}s|P!##2@~? zRVWI7zEao(UO2dfYnDHBONHP3ig!vVw(q9C1Om;Abm22pnEMSIOJHk^-uaY~k%G4l zB0@cY-2uIs9w>nC!dbWcpnIjX(Zq2vf$oOzS zm7j~?HS@4h8!uiQPY*l;Yc2ixMPxbY6uP>+It^>Zgd zR)Fy>$YxZ=;GXorea%NQb_RO|*(`imK^UaW^4K;8L5EIR$+$wX{oZ=7*}8!h#qZ#9 z1|nA1Q$U-*Cc#Q7wCS1@4IO2^UcmGcY@*F7U;a}ZH%md6G zwF=n1EvP@!{(u`01##z~cdClVkJVWO(nkd}joMTzP8wT~J_HN&W?5tagx>rH;5Nyr z*c04+ds=Dt?2L75pwR{1M4y}TO4FYj{L%5FQ(Rtp3`_+qbw;}U5_G(O%S(A^f)Mo+ zeQM*`WO8OC<;^qPJV7xqhsU3L3=|k~KA-D|*E&OFpqeh1##Odov{Ef5M90p%~n*GpK%NTU)M(lmCEBez;i_; zT6eh50P|tj8qUp+7TZRDRf62JP@31uJ%N7Uvq{8|`yqTNn_e3Rt_=g%hJlv!`^HbNPG-HZy~9*+dSUtn@EQxofL1_HFE76q z^3YT5&s|f0-Or7ycX+>4Zrj>{52{*6<0A-8{vz21z`xC5U2}ndwTji8=(hG)Hww#O z#{HuX*75pBdv@&j!nB3yKfYG3>9kj0!Hv#FSa&1~R2fe=6Mx=SE6X1WbznCBbBw*X_#_TN38IHRRGI5Q8cxl1%^@OB2U?h*f{9C^SKX{CG_ zKz1-=P8EfN%=Y{5*#i%owW-E-E6k}e6^2)u(&f8yO54qUJUvmJoo%$^TRq{?7zXYr z1C~KEOc;>Brrk=_;7lIjg&9~02#?IjNCO+orpduJI@PTjp&xo_O8sM|4=0rWW+s7& zkLgU?-E^6p%?AL?c6~9!?lsd4%zTBz#wDBEwv8p=GSKnwZ$rSAZ6M>`@n3kVT!8J~ zW3(w&{{vxvMzb?KZVyI$cLtkb4fvpy9v>{1g?^a6(93fGOY?9&?DF+OV%Cawra3W- zwr(Y3(c|t8@8SJU^UGKpI3M8dJAfbh+*Gqz0x@#}=Ykw~$1*Yn@c3bWdWbS`BjQin<9t9>RJymF$DQ&2 zVEj|=9Zk<%0lo`2X5PVo^8xODH{hny4Y?T^A@KfS;Idio-WpL*5HcYA{XzR<;Jzi0 zkr5!>)(-SkS-=36d#I<{3>`tL>nlI!*P)D08t$K{`9T+>)oVX zsMpAU7GC6oN<{8pqUAv_@+cs9xRQl>5x5@V$-@GI$U%vFge;1m$=WM`n1FgK7z&_= zfx`=61oqnncoRH$7s$xS$mqx3mEirVJU$L^TbmJ)2t2xPC;bW)VRn6HwNOi(hm#3F zbmYULcxGzEIG_-I0zZ6c*ltdnaIn+XVdy@C|}q z8GSiGkRx!4$M4TQ@+>E?Ku83d3`2fc0!RhM${!JfRWN+ZVo&0TfP4mH$@{O+a0xwu zk@Rb0+YI)bzIO#GBO~LXqu`S_(SJ)m5)g^pZ^ymR1Op^o(D~R1g~rcDhv$a{(p3J8M3{1|`y3Putzj(Y_VM$Y4ajEs*Od+ou2nPOTg zEL^52Ji8KLfaAb0^XYG}jNTW%C5)ASO5VU$K|S~XBTCkv--ZnLnFUM>GysB#yOaQ) zY)=aTFWjqQ#Hxo3^@%80fQhsKCV;So?A)&T1DRl*p4}K3SjQh)@);U;^beadGBS=1 z;k(v8Dnw@%zTzo^2^*jP@#ipy72UDi5rx<)vTt=tC_S^}*-hz|UJf8QNd;DaAbl*Y zW`qEO`2GTgKCLSu0KPtfl|PyWAcnd?w7rw~W&CPFfJ}hv!we3S7nMVDdLN;pSjgF{$MP zg!RN@Km=xlPA`MWs>i=(o|lM!9#G1IR$VG+nxfRe47%&#mv=6a5&-CzqJw)kx@iGO z>ves?@wrNFGn~Nb#r)?$1t5=hW%cFt59}1NV!i-m^*?vvQ2G*uf_cbS;T6k*tN_8q z#YMUWz;fBOqN#aFyeha-e^vl8enkZERHlCV6g^Jm>3_92Wz``GDtqXEEJpAxx$mI_ z@C!OFS+tMA9cL2=ASmyd0+aZ}ULh`kYN>!61~kZCHU;mB)V1vbHI{D=7zr5qLVbLG z`da`E*Ilj=J402q3R}EV+9`mM(P696R6g@J0u$kCRe^4tLu6VTF=z%p}mb%QIek@ z4zPj#bHjk^0&+o#uP<08bKm?~&CoZ0Le{6wcm23ew zH{I-miZ?{J z2Rf^(ZQ0WGjqkgE-+K|xycCBOfU7Vqyq^a$+f-G5_e|T?0#L+;@9|h4Kp^SYj(klc zu|$OMt1ouY1$q`A4uK!!SQOsX~wpa2fs1^7Ve-n&3X z#)HRygML43+0AU>$A;W5tebaikyHZp(!xxY78}yXhEl$6@yABIqPLS*7h>k$K_!c- zlk-XUXV&loLk;li#Vgw8 z)i9uG$UUdXSyLGq8J`IFeSjhQ7U|o~=yl6~+m;>zzYp*MERa?&Lc2Beorpts2rkicqs$`0#`RooLep(~$=Dh)P5Z%?UHqR&Q(6DAs zo2i7L)F=(Hpn)7PCAh>RawtSt6Ie?Yrcr}TvtCM@0~9J5v`du%z{CZO%CUM>n6g2^ zw6M16e*bx7EmR#$X=U>lQ>#;dC`*9q#fTgllsq8o>tccY+nmiFk0xM;t zjqt1(`8g^%#{U^g8Y<;hMEalMZ}XMTz)NcBWE( zQ<#bcNToJ+-(PmY4PzZu%YrqQd6z(b8TV4xsvnKbBhdy*Iqd8rfBp|4>W;qoO3MX0 z&SaG&hb|4oGeB-uWp|zAC7E~QJ|h#C0nm29v49Xw7ev4s@o%39j}Xv&`_f(^ZbuPHbYz#BobCFnnASABo5Nn(idW!4EEoTfEF{r zQZ$&?ZS&}F(B+p^=wY6u=X3paLtQt2JP>6!EWErl38&i9 z$=}Ma))`e54v*Eu99I{dr!ATK%|pKy^vHHMqQiAmdnGErv^n|GBG2W&R)p-+dj8>0 z#73NlRk;Wh1vuf+j41$Fe!MFg;3eW%tu*gbRAE;{-BS)%)5v9Nbq19YUenQ&Nf__V zO%G=;Um6Zgy>DfIBH1RYei>~bgD2t&bNWw{h|^b+H}oxqvwz^%JB2eI;du3C684e^ zH+iFSh#J%*L%E&gD>-9pO&47*^*C`^j2L7iQoZqDko^F^ui5vi=WAhAUTda=_9Of+ zP(Lblt|Z2Oj|j5QWeipgN@dox8~pofF0j|UVP@-8ytf91JLkd+k1Rr z!7M*(zJNt9cE!Mhg=+&$w#+}-1^}aPJv(IAcx)Tk9Rm*)z(vk`6TkRDIzM5MO)|eE zM{$NiE0t&y0f$2cK6H>}Urzxz!LjhbWcZD!Fn3UFwndO&zzy|5LCJ#0!&~VtpTL@t zdJ$x`VE|Tt#DLQyPvkTv^9L5{(`)oU)9C!hRs^wgfa~S*p9~QvkUT+lhRkLM9WnT}$o|QtOM$r4oO| zwhTcR4^;G1jM43v^lfK4EG|zT(0kbcDTL<%D{3tReK+VaqV`hYaOw^ zsErW<9q)@TzAzkuSKz`cKcnmC720vIB1c!tsG6GPV0X==`|DS~LF3`O=jTS|EzJ;M zVbklY0A;IPoo*t~7@1;CLS4LYD>bFwzX5-o{S7mmM_p6HjxI3+Sz&mo%PH~R!-EHi z)m!|3<-0thzN9PYKJv#4b8Wv7-{@40))&}@DzkIRw4L(AEKA$Auw1H$_1kG`U>{}R z-yH*TS-!#>5das;IeB5)(5VWjOrv}T&z>m;Ok1!FR5wSX2&U@)6~MRmQv4IZ^-#VL zP`ZvJ(yDt1%*+;mq)ro2N%!`lFV=DRNjGCI*2m1uPEOq7k#KN{6I8aqC0 zJW)4B;^=nX%|RJJY|X%f2Z$ey49lP!RRivh7{IyS^uil#Mg}O<{d9mffV)2cyz6sQ z>4sbm0Pn#I`TJH+!??et6Rz-fSAe+z2lBxplTXypqYbz-1>JR|yzTzM(8Lv#Ch1sz zdZY#T?x)vyb_O#C3|x0Vz;{1A1~LN4R$SE&jMw+XKqaBWaj2NvKt>!}s|P!##2@~? zRVWI7zEao(UO2dfYnDHBONHP3ig!vVw(q9C1Om;Abm22pnEMSIOJHk^-uaY~k%G4l zB0@cY-2uIs9w>nC!dbWcpnIjX(Zq2vf$oOzS zm7j~?HS@4h8!uiQPY*l;Yc2ixMPxbY6uP>+It^>Zgd zR)Fy>$YxZ=;GXorea%NQb_RO|*(`imK^UaW^4K;8L5EIR$+$wX{oZ=7*}8!h#qZ#9 z1|nA1Q$U-*Cc#Q7wCS1@4IO2^UcmGcY@*F7U;a}ZH%md6G zwF=n1EvP@!{(u`01##z~cdClVkJVWO(nkd}joMTzP8wT~J_HN&W?5tagx>rH;5Nyr z*c04+ds=Dt?2L75pwR{1M4y}TO4FYj{L%5FQ(Rtp3`_+qbw;}U5_G(O%S(A^f)Mo+ zeQM*`WO8OC<;^qPJV7xqhsU3L3=|k~KA-D|*E&OFpqeh1##Odov{Ef5M90p%~n*GpK%NTU)M(lmCEBez;i_; zT6eh50P|tj8qUp+7TZRDRf62JP@31uJ%N7Uvq{8|`yqTNn_e3Rt_=g%hJlv!`^HbNPG-HZy~9*+dSUtn@EQxofL1_HFE76q z^3YT5&s|f0-Or7ycX+>4Zrj>{52{*6<0A-8{vz21z`xC5U2}ndwTji8=(hG)Hww#O z#{HuX*75pBdv@&j!nB3yKfYG3>9kj0!Hv#FSa&1~R2fe=6Mx=SE6X1WbznCBbBw*X_#_TN38IHRRGI5Q8cxl1%^@OB2U?h*f{9C^SKX{CG_ zKz1-=P8EfN%=Y{5*#i%owW-E-E6k}e6^2)u(&f8yO54qUJUvmJoo%$^TRq{?7zXYr z1C~KEOc;>Brrk=_;7lIjg&9~02#?IjNCO+orpduJI@PTjp&xo_O8sM|4=0rWW+s7& zkLgU?-E^6p%?AL?c6~9!?lsd4%zTBz#wDBEwv8p=GSKnwZ$rSAZ6M>`@n3kVT!8J~ zW3(w&{{vxvMzb?KZVyI$cLtkb4fvpy9v>{1g?^a6(93fGOY?9&?DF+OV%Cawra3W- zwr(Y3(c|t8@8SJU^UGKpI3M8dJAfbh+*Gqz0x@#}=Ykw~$1*Yn@c3bWdWbS`BjQin<9t9>RJymF$DQ&2 zVEj|=9Zk<%0lo`2X5PVo^8xODH{hny4Y?T^A@KfS;Idio-WpL*5HcYA{XzR<;Jzi0 zkr5!>)(-SkS-=36d#I<{3>`tL>nlI!*P)D08t$K{`9T+>)oVX zsMpAU7GC6oN<{8pqUAv_@+cs9xRQl>5x5@V$-@GI$U%vFge;1m$=WM`n1FgK7z&_= zfx`=61oqnncoRH$7s$xS$mqx3mEirVJU$L^TbmJ)2t2xPC;bW)VRn6HwNOi(hm#3F zbmYULcxGzEIG_-I0zZ6c*ltdnaIn+XVdy@C|}q z8GSiGkRx!4$M4TQ@+>E?Ku83d3`2fc0!RhM${!JfRWN+ZVo&0TfP4mH$@{O+a0xwu zk@Rb0+YI)bzIO#GBO~LXqu`S_(SJ)m5)g^pZ^ymR1Op^o(D~R1g~rcDhv$a{(p3J8M3{1|`y3Putzj(Y_VM$Y4ajEs*Od+ou2nPOTg zEL^52Ji8KLfaAb0^XYG}jNTW%C5)ASO5VU$K|S~XBTCkv--ZnLnFUM>GysB#yOaQ) zY)=aTFWjqQ#Hxo3^@%80fQhsKCV;So?A)&T1DRl*p4}K3SjQh)@);U;^beadGBS=1 z;k(v8Dnw@%zTzo^2^*jP@#ipy72UDi5rx<)vTt=tC_S^}*-hz|UJf8QNd;DaAbl*Y zW`qEO`2GTgKCLSu0KPtfl|PyWAcnd?w7rw~W&CPFfJ}hv!we3S7nMVDdLN;pSjgF{$MP zg!RN@Km=xlPA`MWs>i=(o|lM!9#G1IR$VG+nxfRe47%&#mv=6a5&-CzqJw)kx@iGO z>ves?@wrNFGn~Nb#r)?$1t5=hW%cFt59}1NV!i-m^*?vvQ2G*uf_cbS;T6k*tN_8q z#YMUWz;fBOqN#aFyeha-e^vl8enkZERHlCV6g^Jm>3_92Wz``GDtqXEEJpAxx$mI_ z@C!OFS+tMA9cL2=ASmyd0+aZ}ULh`kYN>!61~kZCHU;mB)V1vbHI{D=7zr5qLVbLG z`da`E*Ilj=J402q3R}EV+9`mM(P696R6g@J0u$kCRe^4tLu6VTF=z%p}mb%QIek@ z4zPj#bHjk^0&+o#uP<08bKm?~&CoZ0Le{6wcm23ew zH{I-miZ?{J z2Rf^(ZQ0WGjqkgE-+K|xycCBOfU7Vqyq^a$+f-G5_e|T?0#L+;@9|h4Kp^SYj(klc zu|$OMt1ouY1$q`A4uK!!SQOsX~wpa2fs1^7Ve-n&3X z#)HRygML43+0AU>$A;W5tebaikyHZp(!xxY78}yXhEl$6@yABIqPLS*7h>k$K_!c- zlk-XUXV&loLk;li#Vgw8 z)i9uG$UUdXSyLGq8J`IFeSjhQ7U|o~=yl6~+m;>zzYp*MERa?&Lc2Beor2z%%*`;@ukZKw`#t}A{quf2-mk~&@qWJ+l%B{umS6)BEiUZT9K8$%_+l;0OzcBm zEIOySP1f^6d(F#cW1gyod(f|0p1QOC)bswBcSVlcn~^|^u}$)!9~8&xXN*Lit(BYH zcDFQQky|p7YBvC7JnQS}AwmRg=+|QNUV?}L&>s#}xVuLMv%r@^n`1QjSRrRWXkyJ9 zmDE;*DRaVsT=#K{K8I64nWZmZ2CJZxwB5j90TQ>YE)IWoHc6kou#GbZu9V5G>|hYN zVRMW8lgBiqT0nUXp=u}>7WWEfCP@4Ui-9jfUP?*4MKVF17iwB3mm$LqK*1}l_(_B`9~^*~GUu$qmn-^aeh}Y>05iSV908MyziiVl>p& zj6~f1+CKsSC#VDGYM*~fdr1-dUBzMNK#Lyw4WLYPoxcz})!ixTGcfkrhx_9t z?S~vP21SEgu)b_~6a8De8+J+{PiDHB1J0K@t`zPj_oVf;F!)uM^iS&fV0bX;WAZ;V zr>s4n?qP9_e?fMWA+IPgmPhPSzB#eyTyFX6!7n@*DlKK1Veq!gc+Hb5-d;~WPQE@vX$1JW2dd+>ea>3#CNb?R4k~RU!qW{HFL!Xx5c*m>p<;i za15`C5GqH-{Q~y|0+R9DnMzaWw{F0>zjW=J-ufAHZ->bT731?p zOD7gOc%3;Mu<5ZiZ__FhJu{2k%`iI&aQlp!2 zws3}z=_8(`W2Xf-GOVt1m3$_nZ>lF{S9H`*c^Dq)z<{V=TU3I3bk%zv&aXh6MQNwH zMfpOLCxKm*p9K!VI0aI!pGLCIpON+mEYvx{N7&KnRf9tO31IHjG!+7C|Lu1~`jZ-H zwCQ8q8B^quk#Eq8=209Tg2gfTBO9S0ZZI}0ZqkU*`_(_Bcr}5v{~6&M5w>k$N}zV9 z2^q||)dW=~`LiVN<8QK*0+vzrc;*qEzyheW_(mAviI!yC`ACU^+Jk9+Oln#jF{fh( zLbIgwA|g2YGmO}nt&@edRn~F@;^7K?J(7j9-o!^Kv54iBrf*mnXDb#Y;Q_7N&jRg^ z@M8>2%m|I+yL(e!C%jL!-@MH`VIqJE&~IuNSYeaGXIV+pwwi!65E&_PtpKW_u1;t! zUBS`rp7`!_AW-dZ3>M*;;ORVq?xvnhUez|87Z?_g^l>4Q!3r1xJLo4E;RD3rmVB+% zvcB+~VrhWcpBG*dDn)2F!a^0+U#M%7Eo!L0UmtxLWa zB|!K$Qks0`00}CNJr>CQdhNr~zzwZOKNPI}*8L$0gyS~lUkl4#3#)t%lTZz`xT2sC zaZAGc`@z3qH|&0QXLZf4-wTm-9lch6X(w>YGCY?#oZwYUnkA+?8F)wWQq9^%QTSQO;d%xhGZJU&9tTHxV z#U9_!QwzZ{)0^youAN0kuA4F{%6M+{W6upddRLJ3?#;GyfxPjj(JBUN*m+{IW8nBR zIRN*nqu|Dizr!A+#-%7PPDc_H&JtWGRZQJaP};Coy5Cx102Qo^w_}^ckgW%vGV)LI zZ$e~4bVem~K1GSwoSc^}SY&H?J9scn$-ZJfnHEPMmRfnnytYr`Q*zeiYrshPwx2F| zKb_YumV++);&U~8T4z{4FIq3VyAhHF7!f8jk5M{zMBh9J#M+zS59izMa|?Da)Om`Dvx zzol*neb#I_^7I2I4tZ5zW0z$*M*|$2gYWR&?%-RIwEN9Z1|p#vHwfcXy`HVH;@-Yg zp1gkjz+UmR+%8QqUKa$kp|zY}bF^Em$P=Lh;gU+hGkX?XpofgYdACwNO`+b4R|a0+ z8<+N4qbiPZKCP9z|F-yWXUGp;0_Ai~j4VDNqAkWJeB zsDyemKZfg`Zwo{2jLcF&A)7#5&v|z`x^HNH{{s{lj-FNeg zkhy>WFa^D`>3d=VqrhIrr^(Pb4e2__DJ}{qT7EMPU{~4I`5|+t?$XJChx!8_OROjH z4ogroCKKnXiyw4N6$L#IpW1MyG>T?zGFQHPX=QJNRY(vTepnmyt7AWOUTm%a>b>l{4NXv2z-nnXbomY24 zJ^x4mzV6f2fNrg2GVHcyhryl#yuVZk`WlgS_2AB*#N1Ee>8O(;+Z2xNp;3(M8O796 zLwPq^ln>>2f4`w*MI71t#agk}HpjgeK)jSG{;5xC>m@m1Z?PRJMK&)_=&M&chy=+J zWEy(BplP-vZuU(qmx!d0dJcw$NggLV8QAYW(iXAfyd0&Zok~BdeSD7d8t96=dIuw( zT`}PcF?P;5Y|?2ZB#ra-yK#n$vrqMyLXg1Fuk!bKfrLmX#a=2I|o2(d8vNfEf;U`~@5SZV&BHqANdeF(9+ubidb=Pzu4CHO}^oRf`AV5!>;=Kl3~k3a(E#?QOjkNRyKt zT>tj75Y5(uY|fT`+#qz=o(5R^+!r<&dwGP;JzkI5>=3qN$*D!(2@k6aZDBbI%Cuw& zsbzmz%m3jwn-|^KL|hU4V7ut@aAb1zfESw;K@o%S(Kzb8b_gr3_TqC)#HsyVZWolA zw*|ibkqd`(?+%VKBzRMC|DJ)}l&6162x;1B!J}P(kc+L2-Zs{@Akkg$6s?*fr;wk_wTMQ$?=^$HLA) z6(IW~YSVFurhHNlYBnmAhoRF_F%h($dBB}gf^nEkhXupIjPh@ZvRnIMHYnRH9<nvYnh8=^CXSVnqn%IVIv^OEyTR8~P^xXqk5pWH0HEVp(!h zI00>FYWYNp)RvgJWM?xeSvaBBwY<3#3aH?72LI`{j^cYexQu=g+Y05X338J^=f5)Z z+yCXAp&3unY>i^4%QxP)2F=QC1M6VvJ@AVNFSm{b2k+;@6KlbF+)fUyv(gbe@nnXO z*_W@0Xb1G{9Q+_O{s=)Ilt<@%IWbOIxPRKGjZxaX%6b@eIzECf{ z@;D2Y z0a}-6vBn5hu>2xvjIC{io;sP}HQn{2HKkIyq-GIyn}oV|`DBV$eCPYDg=VKf~M%DmHInYm5VrC|mvFLsd_w#VWx9wYm{82OabMS@_M%nwj&Diq{T=+C> zYO5LbPeuJ(QeWFn<(nfLIR(l_MVf@^hmb{W&SyhG94y;AA1^X|K0z8WLWT~>Qr?^Io)Lp;@!HxKxPZ0x4pxn qSjY#HREPziBU_jER_>FU-`mE$a{&_4r-N)jf3Y}=HmfwnME?hjxyfn( diff --git a/resources/content_server/jquery_ui/css/humanity-custom/images/ui-icons_ff7519_256x240.png b/resources/content_server/jquery_ui/css/humanity-custom/images/ui-icons_ff7519_256x240.png index 544517142205f1adfc22b44eda10028e10abd42c..5a4070af92983caef197fffc854029b8047b31e7 100644 GIT binary patch delta 5037 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 diff --git a/resources/content_server/jquery_ui/css/humanity-custom/jquery-ui-1.8.5.custom.css b/resources/content_server/jquery_ui/css/humanity-custom/jquery-ui-1.8.5.custom.css index f1625b027f..6b221bca77 100644 --- a/resources/content_server/jquery_ui/css/humanity-custom/jquery-ui-1.8.5.custom.css +++ b/resources/content_server/jquery_ui/css/humanity-custom/jquery-ui-1.8.5.custom.css @@ -50,13 +50,13 @@ * * http://docs.jquery.com/UI/Theming/API * - * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana,Arial,sans-serif&fwDefault=normal&fsDefault=1em&cornerRadius=6px&bgColorHeader=cb842e&bgTextureHeader=02_glass.png&bgImgOpacityHeader=25&borderColorHeader=d49768&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=f4f0ec&bgTextureContent=05_inset_soft.png&bgImgOpacityContent=100&borderColorContent=e0cfc2&fcContent=1e1b1d&iconColorContent=c47a23&bgColorDefault=ede4d4&bgTextureDefault=02_glass.png&bgImgOpacityDefault=70&borderColorDefault=cdc3b7&fcDefault=3f3731&iconColorDefault=f08000&bgColorHover=f5f0e5&bgTextureHover=02_glass.png&bgImgOpacityHover=100&borderColorHover=f5ad66&fcHover=a46313&iconColorHover=f08000&bgColorActive=f4f0ec&bgTextureActive=04_highlight_hard.png&bgImgOpacityActive=100&borderColorActive=e0cfc2&fcActive=b85700&iconColorActive=f35f07&bgColorHighlight=f5f5b5&bgTextureHighlight=04_highlight_hard.png&bgImgOpacityHighlight=75&borderColorHighlight=d9bb73&fcHighlight=060200&iconColorHighlight=cb672b&bgColorError=fee4bd&bgTextureError=04_highlight_hard.png&bgImgOpacityError=65&borderColorError=f8893f&fcError=592003&iconColorError=ff7519&bgColorOverlay=aaaaaa&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=75&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=01_flat.png&bgImgOpacityShadow=75&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px + * To view and modify this theme, visit http://jqueryui.com/themeroller/?tr=ffDefault=Helvetica,Arial,sans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=6px&bgColorHeader=cb842e&bgTextureHeader=02_glass.png&bgImgOpacityHeader=25&borderColorHeader=d49768&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=f4f0ec&bgTextureContent=05_inset_soft.png&bgImgOpacityContent=100&borderColorContent=e0cfc2&fcContent=1e1b1d&iconColorContent=c47a23&bgColorDefault=ede4d4&bgTextureDefault=02_glass.png&bgImgOpacityDefault=70&borderColorDefault=cdc3b7&fcDefault=3f3731&iconColorDefault=f08000&bgColorHover=f5f0e5&bgTextureHover=02_glass.png&bgImgOpacityHover=100&borderColorHover=f5ad66&fcHover=a46313&iconColorHover=f08000&bgColorActive=f4f0ec&bgTextureActive=04_highlight_hard.png&bgImgOpacityActive=100&borderColorActive=e0cfc2&fcActive=b85700&iconColorActive=f35f07&bgColorHighlight=f5f5b5&bgTextureHighlight=04_highlight_hard.png&bgImgOpacityHighlight=75&borderColorHighlight=d9bb73&fcHighlight=060200&iconColorHighlight=cb672b&bgColorError=fee4bd&bgTextureError=04_highlight_hard.png&bgImgOpacityError=65&borderColorError=f8893f&fcError=592003&iconColorError=ff7519&bgColorOverlay=aaaaaa&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=75&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=01_flat.png&bgImgOpacityShadow=75&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px */ /* Component containers ----------------------------------*/ -.ui-widget { font-family: Verdana,Arial,sans-serif; font-size: 1em; } +.ui-widget { font-family: Verdana,Arial,sans-serif; font-size: 1.1em; } .ui-widget .ui-widget { font-size: 1em; } .ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Verdana,Arial,sans-serif; font-size: 1em; } .ui-widget-content { border: 1px solid #e0cfc2; background: #f4f0ec url(images/ui-bg_inset-soft_100_f4f0ec_1x100.png) 50% bottom repeat-x; color: #1e1b1d; } @@ -293,6 +293,25 @@ /* Overlays */ .ui-widget-overlay { background: #aaaaaa url(images/ui-bg_flat_75_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); } .ui-widget-shadow { margin: -8px 0 0 -8px; padding: 8px; background: #aaaaaa url(images/ui-bg_flat_75_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); -moz-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; }/* + * jQuery UI Resizable @VERSION + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Resizable#theming + */ +.ui-resizable { position: relative;} +.ui-resizable-handle { position: absolute;font-size: 0.1px;z-index: 99999; display: block;} +.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; } +.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; } +.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; } +.ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; } +.ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; } +.ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; } +.ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; } +.ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; } +.ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}/* * jQuery UI Accordion @VERSION * * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) @@ -348,3 +367,24 @@ input.ui-button { padding: .4em 1em; } /* workarounds */ button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */ +/* + * jQuery UI Dialog @VERSION + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Dialog#theming + */ +.ui-dialog { position: absolute; padding: .2em; width: 300px; overflow: hidden; } +.ui-dialog .ui-dialog-titlebar { padding: .5em 1em .3em; position: relative; } +.ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .2em 0; } +.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; } +.ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; } +.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; } +.ui-dialog .ui-dialog-content { position: relative; border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; } +.ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; } +.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: right; } +.ui-dialog .ui-dialog-buttonpane button { margin: .5em .4em .5em 0; cursor: pointer; } +.ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; } +.ui-draggable .ui-dialog-titlebar { cursor: move; } diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index 439c0f4cf2..7d476ade3b 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -470,7 +470,11 @@ class BrowseServer(object): fmts = [x.lower() for x in fmts.split(',') if x] pf = prefs['output_format'].lower() fmt = pf if pf in fmts else fmts[0] - args = {'id':id_, 'mi':mi, 'read_string':_('Read'),} + args = {'id':id_, 'mi':mi, + 'read_string':xml(_('Read'), True), + 'details': xml(_('Details'), True), + 'details_tt': xml(_('Show book details'), True) + } for key in mi.all_field_keys(): val = mi.format_field(key)[1] if not val: From a657a3fb67f0593f89003d3b49fc8f4ae45cefd8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 15 Oct 2010 21:02:18 -0600 Subject: [PATCH 34/75] ... --- resources/content_server/browse/browse.js | 9 +- .../js/jquery-ui-1.8.5.custom.min.js | 219 ++++++++++++++++++ 2 files changed, 225 insertions(+), 3 deletions(-) diff --git a/resources/content_server/browse/browse.js b/resources/content_server/browse/browse.js index 9d3c01fa15..f5b04db5f8 100644 --- a/resources/content_server/browse/browse.js +++ b/resources/content_server/browse/browse.js @@ -202,7 +202,8 @@ function booklist(hide_sort) { } $("#book_details_dialog").dialog({ autoOpen: false, - modal: true + modal: true, + show: 'slide' }); first_page(); } @@ -211,9 +212,11 @@ function show_details(a_dom) { var book = $(a_dom).closest('div.summary'); var id = book.attr('id').split('_')[1]; var bd = $('#book_details_dialog'); - bd.attr('title', 'test'); - bd.html('test'); + bd.html('LoadingLoading, please wait…'); bd.dialog('option', 'width', $('#container').width() - 50); + bd.dialog('option', 'height', $(window).height() - 100); + + bd.dialog('option', 'title', book.find('.title').text()); bd.dialog('open'); } diff --git a/resources/content_server/jquery_ui/js/jquery-ui-1.8.5.custom.min.js b/resources/content_server/jquery_ui/js/jquery-ui-1.8.5.custom.min.js index 034d13a25a..d858ce51de 100644 --- a/resources/content_server/jquery_ui/js/jquery-ui-1.8.5.custom.min.js +++ b/resources/content_server/jquery_ui/js/jquery-ui-1.8.5.custom.min.js @@ -204,4 +204,223 @@ function(a){return a+".dialog-overlay"}).join(" "),create:function(a){if(this.in (this.oldInstances.pop()||c("
    ").addClass("ui-widget-overlay")).appendTo(document.body).css({width:this.width(),height:this.height()});c.fn.bgiframe&&b.bgiframe();this.instances.push(b);return b},destroy:function(a){this.oldInstances.push(this.instances.splice(c.inArray(a,this.instances),1)[0]);this.instances.length===0&&c([document,window]).unbind(".dialog-overlay");a.remove();var b=0;c.each(this.instances,function(){b=Math.max(b,this.css("z-index"))});this.maxZ=b},height:function(){var a, b;if(c.browser.msie&&c.browser.version<7){a=Math.max(document.documentElement.scrollHeight,document.body.scrollHeight);b=Math.max(document.documentElement.offsetHeight,document.body.offsetHeight);return a
    ").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0});c.wrap(b);b=c.parent();if(c.css("position")=="static"){b.css({position:"relative"});c.css({position:"relative"})}else{f.extend(a,{position:c.css("position"),zIndex:c.css("z-index")});f.each(["top","left","bottom","right"],function(d,e){a[e]=c.css(e);if(isNaN(parseInt(a[e],10)))a[e]="auto"}); +c.css({position:"relative",top:0,left:0})}return b.css(a).show()},removeWrapper:function(c){if(c.parent().is(".ui-effects-wrapper"))return c.parent().replaceWith(c);return c},setTransition:function(c,a,b,d){d=d||{};f.each(a,function(e,g){unit=c.cssUnit(g);if(unit[0]>0)d[g]=unit[0]*b+unit[1]});return d}});f.fn.extend({effect:function(c){var a=k.apply(this,arguments);a={options:a[1],duration:a[2],callback:a[3]};var b=f.effects[c];return b&&!f.fx.off?b.call(this,a):this},_show:f.fn.show,show:function(c){if(!c|| +typeof c=="number"||f.fx.speeds[c]||!f.effects[c])return this._show.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="show";return this.effect.apply(this,a)}},_hide:f.fn.hide,hide:function(c){if(!c||typeof c=="number"||f.fx.speeds[c]||!f.effects[c])return this._hide.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="hide";return this.effect.apply(this,a)}},__toggle:f.fn.toggle,toggle:function(c){if(!c||typeof c=="number"||f.fx.speeds[c]||!f.effects[c]||typeof c== +"boolean"||f.isFunction(c))return this.__toggle.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="toggle";return this.effect.apply(this,a)}},cssUnit:function(c){var a=this.css(c),b=[];f.each(["em","px","%","pt"],function(d,e){if(a.indexOf(e)>0)b=[parseFloat(a),e]});return b}});f.easing.jswing=f.easing.swing;f.extend(f.easing,{def:"easeOutQuad",swing:function(c,a,b,d,e){return f.easing[f.easing.def](c,a,b,d,e)},easeInQuad:function(c,a,b,d,e){return d*(a/=e)*a+b},easeOutQuad:function(c, +a,b,d,e){return-d*(a/=e)*(a-2)+b},easeInOutQuad:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a+b;return-d/2*(--a*(a-2)-1)+b},easeInCubic:function(c,a,b,d,e){return d*(a/=e)*a*a+b},easeOutCubic:function(c,a,b,d,e){return d*((a=a/e-1)*a*a+1)+b},easeInOutCubic:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a+b;return d/2*((a-=2)*a*a+2)+b},easeInQuart:function(c,a,b,d,e){return d*(a/=e)*a*a*a+b},easeOutQuart:function(c,a,b,d,e){return-d*((a=a/e-1)*a*a*a-1)+b},easeInOutQuart:function(c,a,b,d,e){if((a/= +e/2)<1)return d/2*a*a*a*a+b;return-d/2*((a-=2)*a*a*a-2)+b},easeInQuint:function(c,a,b,d,e){return d*(a/=e)*a*a*a*a+b},easeOutQuint:function(c,a,b,d,e){return d*((a=a/e-1)*a*a*a*a+1)+b},easeInOutQuint:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a*a*a+b;return d/2*((a-=2)*a*a*a*a+2)+b},easeInSine:function(c,a,b,d,e){return-d*Math.cos(a/e*(Math.PI/2))+d+b},easeOutSine:function(c,a,b,d,e){return d*Math.sin(a/e*(Math.PI/2))+b},easeInOutSine:function(c,a,b,d,e){return-d/2*(Math.cos(Math.PI*a/e)-1)+ +b},easeInExpo:function(c,a,b,d,e){return a==0?b:d*Math.pow(2,10*(a/e-1))+b},easeOutExpo:function(c,a,b,d,e){return a==e?b+d:d*(-Math.pow(2,-10*a/e)+1)+b},easeInOutExpo:function(c,a,b,d,e){if(a==0)return b;if(a==e)return b+d;if((a/=e/2)<1)return d/2*Math.pow(2,10*(a-1))+b;return d/2*(-Math.pow(2,-10*--a)+2)+b},easeInCirc:function(c,a,b,d,e){return-d*(Math.sqrt(1-(a/=e)*a)-1)+b},easeOutCirc:function(c,a,b,d,e){return d*Math.sqrt(1-(a=a/e-1)*a)+b},easeInOutCirc:function(c,a,b,d,e){if((a/=e/2)<1)return-d/ +2*(Math.sqrt(1-a*a)-1)+b;return d/2*(Math.sqrt(1-(a-=2)*a)+1)+b},easeInElastic:function(c,a,b,d,e){c=1.70158;var g=0,h=d;if(a==0)return b;if((a/=e)==1)return b+d;g||(g=e*0.3);if(h").css({position:"absolute",visibility:"visible",left:-f*(h/d),top:-e*(i/c)}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:h/d,height:i/c,left:g.left+f*(h/d)+(a.options.mode=="show"?(f-Math.floor(d/2))*(h/d):0),top:g.top+e*(i/c)+(a.options.mode=="show"?(e-Math.floor(c/2))*(i/c):0),opacity:a.options.mode=="show"?0:1}).animate({left:g.left+f*(h/d)+(a.options.mode=="show"?0:(f-Math.floor(d/2))*(h/d)),top:g.top+ +e*(i/c)+(a.options.mode=="show"?0:(e-Math.floor(c/2))*(i/c)),opacity:a.options.mode=="show"?1:0},a.duration||500);setTimeout(function(){a.options.mode=="show"?b.css({visibility:"visible"}):b.css({visibility:"visible"}).hide();a.callback&&a.callback.apply(b[0]);b.dequeue();j("div.ui-effects-explode").remove()},a.duration||500)})}})(jQuery); +;/* + * jQuery UI Effects Fade 1.8.5 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Effects/Fade + * + * Depends: + * jquery.effects.core.js + */ +(function(b){b.effects.fade=function(a){return this.queue(function(){var c=b(this),d=b.effects.setMode(c,a.options.mode||"hide");c.animate({opacity:d},{queue:false,duration:a.duration,easing:a.options.easing,complete:function(){a.callback&&a.callback.apply(this,arguments);c.dequeue()}})})}})(jQuery); +;/* + * jQuery UI Effects Fold 1.8.5 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Effects/Fold + * + * Depends: + * jquery.effects.core.js + */ +(function(c){c.effects.fold=function(a){return this.queue(function(){var b=c(this),j=["position","top","left"],d=c.effects.setMode(b,a.options.mode||"hide"),g=a.options.size||15,h=!!a.options.horizFirst,k=a.duration?a.duration/2:c.fx.speeds._default/2;c.effects.save(b,j);b.show();var e=c.effects.createWrapper(b).css({overflow:"hidden"}),f=d=="show"!=h,l=f?["width","height"]:["height","width"];f=f?[e.width(),e.height()]:[e.height(),e.width()];var i=/([0-9]+)%/.exec(g);if(i)g=parseInt(i[1],10)/100* +f[d=="hide"?0:1];if(d=="show")e.css(h?{height:0,width:g}:{height:g,width:0});h={};i={};h[l[0]]=d=="show"?f[0]:g;i[l[1]]=d=="show"?f[1]:0;e.animate(h,k,a.options.easing).animate(i,k,a.options.easing,function(){d=="hide"&&b.hide();c.effects.restore(b,j);c.effects.removeWrapper(b);a.callback&&a.callback.apply(b[0],arguments);b.dequeue()})})}})(jQuery); +;/* + * jQuery UI Effects Highlight 1.8.5 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Effects/Highlight + * + * Depends: + * jquery.effects.core.js + */ +(function(b){b.effects.highlight=function(c){return this.queue(function(){var a=b(this),e=["backgroundImage","backgroundColor","opacity"],d=b.effects.setMode(a,c.options.mode||"show"),f={backgroundColor:a.css("backgroundColor")};if(d=="hide")f.opacity=0;b.effects.save(a,e);a.show().css({backgroundImage:"none",backgroundColor:c.options.color||"#ffff99"}).animate(f,{queue:false,duration:c.duration,easing:c.options.easing,complete:function(){d=="hide"&&a.hide();b.effects.restore(a,e);d=="show"&&!b.support.opacity&& +this.style.removeAttribute("filter");c.callback&&c.callback.apply(this,arguments);a.dequeue()}})})}})(jQuery); +;/* + * jQuery UI Effects Pulsate 1.8.5 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Effects/Pulsate + * + * Depends: + * jquery.effects.core.js + */ +(function(d){d.effects.pulsate=function(a){return this.queue(function(){var b=d(this),c=d.effects.setMode(b,a.options.mode||"show");times=(a.options.times||5)*2-1;duration=a.duration?a.duration/2:d.fx.speeds._default/2;isVisible=b.is(":visible");animateTo=0;if(!isVisible){b.css("opacity",0).show();animateTo=1}if(c=="hide"&&isVisible||c=="show"&&!isVisible)times--;for(c=0;c').appendTo(document.body).addClass(a.options.className).css({top:d.top,left:d.left,height:b.innerHeight(),width:b.innerWidth(),position:"absolute"}).animate(c,a.duration,a.options.easing,function(){f.remove();a.callback&&a.callback.apply(b[0],arguments); +b.dequeue()})})}})(jQuery); ; \ No newline at end of file From 953da12b44fcf09cb34d4fe3473fc88b4813b08b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 15 Oct 2010 21:32:05 -0600 Subject: [PATCH 35/75] ... --- resources/content_server/browse/browse.css | 4 ++++ resources/content_server/browse/browse.html | 2 +- resources/content_server/browse/browse.js | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/resources/content_server/browse/browse.css b/resources/content_server/browse/browse.css index aceb595383..406de09f53 100644 --- a/resources/content_server/browse/browse.css +++ b/resources/content_server/browse/browse.css @@ -176,6 +176,10 @@ h2.library_name { overflow: hidden; } +#search_box .ui-button { + padding: 0.25em; +} + /* }}} */ /* Top level {{{ */ diff --git a/resources/content_server/browse/browse.html b/resources/content_server/browse/browse.html index b3039f8c4e..4acc15f3ea 100644 --- a/resources/content_server/browse/browse.html +++ b/resources/content_server/browse/browse.html @@ -94,6 +94,6 @@ - +
    diff --git a/resources/content_server/browse/browse.js b/resources/content_server/browse/browse.js index f5b04db5f8..16753a38b9 100644 --- a/resources/content_server/browse/browse.js +++ b/resources/content_server/browse/browse.js @@ -213,7 +213,7 @@ function show_details(a_dom) { var id = book.attr('id').split('_')[1]; var bd = $('#book_details_dialog'); bd.html('LoadingLoading, please wait…'); - bd.dialog('option', 'width', $('#container').width() - 50); + bd.dialog('option', 'width', $(window).width() - 100); bd.dialog('option', 'height', $(window).height() - 100); bd.dialog('option', 'title', book.find('.title').text()); From cf644ac63ecfd70e3d12614b82b6fff551ba3ee4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 15 Oct 2010 22:05:51 -0600 Subject: [PATCH 36/75] ... --- resources/content_server/browse/browse.js | 16 +++- resources/content_server/browse/summary.html | 1 + src/calibre/library/server/browse.py | 93 ++++++++++++++------ 3 files changed, 79 insertions(+), 31 deletions(-) diff --git a/resources/content_server/browse/browse.js b/resources/content_server/browse/browse.js index 16753a38b9..367b8341d9 100644 --- a/resources/content_server/browse/browse.js +++ b/resources/content_server/browse/browse.js @@ -210,13 +210,25 @@ function booklist(hide_sort) { function show_details(a_dom) { var book = $(a_dom).closest('div.summary'); - var id = book.attr('id').split('_')[1]; var bd = $('#book_details_dialog'); bd.html('LoadingLoading, please wait…'); bd.dialog('option', 'width', $(window).width() - 100); bd.dialog('option', 'height', $(window).height() - 100); - bd.dialog('option', 'title', book.find('.title').text()); + + $.ajax({ + url: book.find('.details-href').attr('title'), + context: bd, + dataType: "json", + timeout: 600000, //milliseconds (10 minutes) + error: function(xhr, stat, err) { + this.html(render_error(stat)); + }, + success: function(data) { + this.html(data); + } + }); + bd.dialog('open'); } diff --git a/resources/content_server/browse/summary.html b/resources/content_server/browse/summary.html index 8df42de366..ba23ed854c 100644 --- a/resources/content_server/browse/summary.html +++ b/resources/content_server/browse/summary.html @@ -15,4 +15,5 @@
    {tags}
    {other_formats}
    + diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index 7d476ade3b..805a8f6424 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -187,9 +187,10 @@ class BrowseServer(object): connect('browse_booklist_page', base_href+'/booklist_page', self.browse_booklist_page) - connect('browse_search', base_href+'/search', self.browse_search) + connect('browse_details', base_href+'/details/{id}', + self.browse_details) # Templates {{{ def browse_template(self, sort, category=True, initial_search=''): @@ -447,6 +448,23 @@ class BrowseServer(object): title=_('Books in') + " " +category_name, script='booklist(%s);'%hide_sort, main=html) + def browse_get_book_args(self, mi, id_): + fmts = self.db.formats(id_, index_is_id=True) + if not fmts: + fmts = '' + fmts = [x.lower() for x in fmts.split(',') if x] + pf = prefs['output_format'].lower() + fmt = pf if pf in fmts else fmts[0] + args = {'id':id_, 'mi':mi, + } + for key in mi.all_field_keys(): + val = mi.format_field(key)[1] + if not val: + val = '' + args[key] = xml(val, True) + fname = ascii_filename(args['title']) + ' - ' + ascii_filename(args['authors']) + return args, fmt, fmts, fname + @Endpoint(mimetype='application/json; charset=utf-8') def browse_booklist_page(self, ids=None, sort=None): if sort == 'null': @@ -464,36 +482,9 @@ class BrowseServer(object): mi = self.db.get_metadata(id_, index_is_id=True) except: continue - fmts = self.db.formats(id_, index_is_id=True) - if not fmts: - fmts = '' - fmts = [x.lower() for x in fmts.split(',') if x] - pf = prefs['output_format'].lower() - fmt = pf if pf in fmts else fmts[0] - args = {'id':id_, 'mi':mi, - 'read_string':xml(_('Read'), True), - 'details': xml(_('Details'), True), - 'details_tt': xml(_('Show book details'), True) - } - for key in mi.all_field_keys(): - val = mi.format_field(key)[1] - if not val: - val = '' - args[key] = xml(val, True) - fname = ascii_filename(args['title']) + ' - ' + ascii_filename(args['authors']) - args['href'] = '/get/%s/%s_%d.%s'%( - fmt, fname, id_, fmt) - args['comments'] = comments_to_html(mi.comments) - args['read_tooltip'] = \ - _('Read %s in the %s format')%(args['title'], fmt.upper()) - args['stars'] = '' - if mi.rating: - args['stars'] = render_rating(mi.rating/2.0, prefix=_('Rating'))[0] - if args['tags']: - args['tags'] = u'%s: '%_('Tags') + args['tags'] + args, fmt, fmts, fname = self.browse_get_book_args(mi, id_) args['other_formats'] = '' other_fmts = [x for x in fmts if x.lower() != fmt.lower()] - if other_fmts: ofmts = [u'{3}'\ .format(fmt, fname, id_, fmt.upper()) for fmt in @@ -502,12 +493,56 @@ class BrowseServer(object): args['other_formats'] = u'%s: ' % \ _('Other formats') + ofmts + args['details_href'] = '/browse/details/'+str(id_) + args['read_tooltip'] = \ + _('Read %s in the %s format')%(args['title'], fmt.upper()) + args['href'] = '/get/%s/%s_%d.%s'%( + fmt, fname, id_, fmt) + args['comments'] = comments_to_html(mi.comments) + args['stars'] = '' + if mi.rating: + args['stars'] = render_rating(mi.rating/2.0, prefix=_('Rating'))[0] + if args['tags']: + args['tags'] = u'%s: '%_('Tags') + \ + xml(args['tags']) + if args['series']: + args['series'] = xml(args['series']) + args['read_string'] = xml(_('Read'), True) + args['details'] = xml(_('Details'), True) + args['details_tt'] = xml(_('Show book details'), True) summs.append(self.browse_summary_template.format(**args)) return json.dumps('\n'.join(summs), ensure_ascii=False) + @Endpoint(mimetype='application/json; charset=utf-8') + def browse_details(self, id=None): + try: + id_ = int(id) + except: + raise cherrypy.HTTPError(404, 'invalid id: %r'%id) + + try: + mi = self.db.get_metadata(id_, index_is_id=True) + except: + ans = _('This book has been deleted') + else: + args, fmt, fmts, fname = self.browse_get_book_args(mi, id_) + args['formats'] = '' + if fmts: + ofmts = [u'{3}'\ + .format(fmt, fname, id_, fmt.upper()) for fmt in + fmts] + ofmts = ', '.join(ofmts) + args['formats'] = u'%s: ' % \ + _('Formats') + ofmts + + + return json.dumps(ans, ensure_ascii=False) + + + # }}} # Search {{{ From ff8f949f97a267be14552acd728177dc1e742ab9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 15 Oct 2010 23:05:35 -0600 Subject: [PATCH 37/75] /browse: Implement Book details --- resources/content_server/browse/browse.css | 38 ++++++++++++++++ resources/content_server/browse/details.html | 10 +++++ src/calibre/library/field_metadata.py | 2 +- src/calibre/library/server/browse.py | 47 +++++++++++++++++--- 4 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 resources/content_server/browse/details.html diff --git a/resources/content_server/browse/browse.css b/resources/content_server/browse/browse.css index 406de09f53..d50b6936ff 100644 --- a/resources/content_server/browse/browse.css +++ b/resources/content_server/browse/browse.css @@ -384,3 +384,41 @@ h2.library_name { /* }}} */ +/* Details {{{ */ + +.details .left { + float: left; + max-width: 50%; + margin-right: 2em; + overflow: auto; +} + +.details .right { + overflow: auto; +} + +.details .formats { + margin-bottom: 2ex; +} + +#book_details_dialog .details a { + color: blue; + text-decoration: none; +} + +#book_details_dialog .details a:hover { + color: red; +} + +.details .field { + margin-bottom: 0.5em; +} + +.details .comment { + margin-left: 1em; + overflow: auto; + max-height: 50%; +} + +/* }}} */ + diff --git a/resources/content_server/browse/details.html b/resources/content_server/browse/details.html new file mode 100644 index 0000000000..59af5c535e --- /dev/null +++ b/resources/content_server/browse/details.html @@ -0,0 +1,10 @@ +
    +
    + Cover of {title} +
    +
    +
    {formats}
    + {fields} + {comments} +
    +
    diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index b43a6620d0..69dd7ae636 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -161,7 +161,7 @@ class FieldMetadata(dict): 'datatype':'text', 'is_multiple':None, 'kind':'field', - 'name':None, + 'name':_('Comments'), 'search_terms':['comments', 'comment'], 'is_custom':False, 'is_category':False}), ('cover', {'table':None, diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index 805a8f6424..5e7de43d45 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -245,6 +245,14 @@ class BrowseServer(object): P('content_server/browse/summary.html', data=True).decode('utf-8') return self.__browse_summary_template__ + @property + def browse_details_template(self): + if not hasattr(self, '__browse_details_template__') or \ + self.opts.develop: + self.__browse_details_template__ = \ + P('content_server/browse/details.html', data=True).decode('utf-8') + return self.__browse_details_template__ + # }}} # Catalogs {{{ @@ -503,10 +511,10 @@ class BrowseServer(object): if mi.rating: args['stars'] = render_rating(mi.rating/2.0, prefix=_('Rating'))[0] if args['tags']: - args['tags'] = u'%s: '%_('Tags') + \ - xml(args['tags']) + args['tags'] = u'%s: '%xml(_('Tags')) + \ + args['tags'] if args['series']: - args['series'] = xml(args['series']) + args['series'] = args['series'] args['read_string'] = xml(_('Read'), True) args['details'] = xml(_('Details'), True) args['details_tt'] = xml(_('Show book details'), True) @@ -535,9 +543,38 @@ class BrowseServer(object): .format(fmt, fname, id_, fmt.upper()) for fmt in fmts] ofmts = ', '.join(ofmts) - args['formats'] = u'%s: ' % \ - _('Formats') + ofmts + args['formats'] = ofmts + fields, comments = [], [] + for field, m in list(mi.get_all_standard_metadata(False).items()) + \ + list(mi.get_all_user_metadata(False).items()): + if m['datatype'] == 'comments' or field == 'comments': + comments.append((m['name'], comments_to_html(mi.get(field, + '')))) + continue + if field in ('title', 'formats') or not args.get(field, False) \ + or not m['name']: + continue + if m['datatype'] == 'rating': + r = u'%s: '%xml(m['name']) + \ + render_rating(mi.rating/2.0, prefix=m['name'])[0] + else: + r = u'%s: '%xml(m['name']) + \ + args[field] + fields.append((m['name'], r)) + fields.sort(key=lambda x: x[0].lower()) + fields = [u'
    {0}
    '.format(f[1]) for f in + fields] + fields = u'
    %s
    '%('\n\n'.join(fields)) + + comments.sort(key=lambda x: x[0].lower()) + comments = [(u'
    %s: ' + u'
    %s
    ') % (xml(c[0]), + c[1]) for c in comments] + comments = u'
    %s
    '%('\n\n'.join(comments)) + ans = self.browse_details_template.format(id=id_, + title=xml(mi.title, True), fields=fields, + formats=args['formats'], comments=comments) return json.dumps(ans, ensure_ascii=False) From 9bb060124ed756196658f9485709261b8096b553 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 15 Oct 2010 23:07:37 -0600 Subject: [PATCH 38/75] ... --- src/calibre/gui2/dialogs/metadata_single.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index 64ac4a3ccb..8043016f9f 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -25,7 +25,7 @@ from calibre.ebooks.metadata.covers import download_cover from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.metadata import MetaInformation from calibre.utils.config import prefs, tweaks -from calibre.utils.date import qt_to_dt, local_tz, utcfromtimestamp, utc_tz +from calibre.utils.date import qt_to_dt, local_tz, utcfromtimestamp from calibre.customize.ui import run_plugins_on_import, get_isbndb_key from calibre.gui2.preferences.social import SocialMetadata from calibre.gui2.custom_column_widgets import populate_metadata_page From 848fe8096dd28fa6cf89661eb904b1f1ae7c5793 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 16 Oct 2010 06:57:16 -0600 Subject: [PATCH 39/75] ... --- src/calibre/library/server/content.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/library/server/content.py b/src/calibre/library/server/content.py index 8c5fef4ee1..d95cd1818c 100644 --- a/src/calibre/library/server/content.py +++ b/src/calibre/library/server/content.py @@ -140,7 +140,7 @@ class ContentServer(object): updated = self.build_time else: with cover as f: - updated = fromtimestamp(os.stat(f.name).st_mtime) + updated = fromtimestamp(os.fstat(f.fileno()).st_mtime) cover = f.read() cherrypy.response.headers['Last-Modified'] = self.last_modified(updated) From e46fed78ccf4d8c714c94317a960afe121d954f0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 16 Oct 2010 07:04:38 -0600 Subject: [PATCH 40/75] Fix regression in formats listing in /browse summaries --- src/calibre/library/server/browse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index 5e7de43d45..f28762dddf 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -495,7 +495,7 @@ class BrowseServer(object): other_fmts = [x for x in fmts if x.lower() != fmt.lower()] if other_fmts: ofmts = [u'{3}'\ - .format(fmt, fname, id_, fmt.upper()) for fmt in + .format(f, fname, id_, f.upper()) for f in other_fmts] ofmts = ', '.join(ofmts) args['other_formats'] = u'%s: ' % \ From ed3909b1a781d3ee1acfebba41d68c24b88f6edb Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 16 Oct 2010 07:29:06 -0600 Subject: [PATCH 41/75] Fix bugs in frwrapper returned by lopen on windows --- src/calibre/startup.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/calibre/startup.py b/src/calibre/startup.py index 1046cd93b3..b5741c1991 100644 --- a/src/calibre/startup.py +++ b/src/calibre/startup.py @@ -120,7 +120,8 @@ if not _run_once: object.__setattr__(self, 'name', name) def __getattribute__(self, attr): - if attr == 'name': + if attr in ('name', '__enter__', '__str__', '__unicode__', + '__repr__'): return object.__getattribute__(self, attr) fobject = object.__getattribute__(self, 'fobject') return getattr(fobject, attr) @@ -141,6 +142,10 @@ if not _run_once: def __unicode__(self): return repr(self).decode('utf-8') + def __enter__(self): + fobject = object.__getattribute__(self, 'fobject') + fobject.__enter__() + return self m = mode[0] random = len(mode) > 1 and mode[1] == '+' From 16368ec54b639ccf3d9fe86092aca8432a899d5e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 16 Oct 2010 07:43:10 -0600 Subject: [PATCH 42/75] /browse: Fix handling of books with no formats --- resources/content_server/browse/summary.html | 2 +- src/calibre/library/server/browse.py | 39 +++++++++++++------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/resources/content_server/browse/summary.html b/resources/content_server/browse/summary.html index ba23ed854c..4e9c9d2a77 100644 --- a/resources/content_server/browse/summary.html +++ b/resources/content_server/browse/summary.html @@ -1,7 +1,7 @@
    Cover of {title} - {read_string} + {get_button}
    diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index f28762dddf..89b1fe9afc 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -462,7 +462,10 @@ class BrowseServer(object): fmts = '' fmts = [x.lower() for x in fmts.split(',') if x] pf = prefs['output_format'].lower() - fmt = pf if pf in fmts else fmts[0] + try: + fmt = pf if pf in fmts else fmts[0] + except: + fmt = None args = {'id':id_, 'mi':mi, } for key in mi.all_field_keys(): @@ -492,20 +495,29 @@ class BrowseServer(object): continue args, fmt, fmts, fname = self.browse_get_book_args(mi, id_) args['other_formats'] = '' - other_fmts = [x for x in fmts if x.lower() != fmt.lower()] - if other_fmts: - ofmts = [u'{3}'\ - .format(f, fname, id_, f.upper()) for f in - other_fmts] - ofmts = ', '.join(ofmts) - args['other_formats'] = u'%s: ' % \ - _('Other formats') + ofmts + if fmts and fmt: + other_fmts = [x for x in fmts if x.lower() != fmt.lower()] + if other_fmts: + ofmts = [u'{3}'\ + .format(f, fname, id_, f.upper()) for f in + other_fmts] + ofmts = ', '.join(ofmts) + args['other_formats'] = u'%s: ' % \ + _('Other formats') + ofmts args['details_href'] = '/browse/details/'+str(id_) - args['read_tooltip'] = \ - _('Read %s in the %s format')%(args['title'], fmt.upper()) - args['href'] = '/get/%s/%s_%d.%s'%( - fmt, fname, id_, fmt) + + if fmt: + href = '/get/%s/%s_%d.%s'%( + fmt, fname, id_, fmt) + rt = xml(_('Read %s in the %s format')%(args['title'], + fmt.upper()), True) + + args['get_button'] = \ + '%s' % \ + (xml(href, True), rt, xml(_('Get'))) + else: + args['get_button'] = '' args['comments'] = comments_to_html(mi.comments) args['stars'] = '' if mi.rating: @@ -515,7 +527,6 @@ class BrowseServer(object): args['tags'] if args['series']: args['series'] = args['series'] - args['read_string'] = xml(_('Read'), True) args['details'] = xml(_('Details'), True) args['details_tt'] = xml(_('Show book details'), True) From 4c207d9b6f071f36e0507b1a5e60d2cee5e14f3f Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 16 Oct 2010 14:54:03 +0100 Subject: [PATCH 43/75] Remove non-displayable categories from the home page --- src/calibre/library/server/browse.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index a122f539c6..be8b55e648 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -267,6 +267,7 @@ class BrowseServer(object): def getter(x): return category_meta[x]['name'].lower() + displayed_custom_fields = custom_fields_to_display(self.db) for category in sorted(categories, cmp=lambda x,y: cmp(getter(x), getter(y))): if len(categories[category]) == 0: @@ -276,6 +277,8 @@ class BrowseServer(object): meta = category_meta.get(category, None) if meta is None: continue + if meta['is_custom'] and category not in displayed_custom_fields: + continue cats.append((meta['name'], category)) cats = ['
  • {0}/browse/category/{1}
  • '\ .format(xml(x, True), xml(quote(y)), xml(_('Browse books by'))) From 2e10fd115b7181d0616dd168184b986f25e578bf Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 16 Oct 2010 15:21:36 +0100 Subject: [PATCH 44/75] Put the icon file map into field metadata, then change tag_view to use it. --- src/calibre/gui2/tag_view.py | 18 ++++++------------ src/calibre/library/field_metadata.py | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 3505cc7344..6e0aef1b99 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -17,7 +17,7 @@ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \ from calibre.ebooks.metadata import title_sort from calibre.gui2 import config, NONE -from calibre.library.field_metadata import TagsIcons +from calibre.library.field_metadata import TagsIcons, category_icon_map from calibre.utils.search_query_parser import saved_searches from calibre.gui2 import error_dialog from calibre.gui2.dialogs.confirm_delete import confirm @@ -382,17 +382,11 @@ class TagsModel(QAbstractItemModel): # {{{ # must do this here because 'QPixmap: Must construct a QApplication # before a QPaintDevice'. The ':' in front avoids polluting either the # user-defined categories (':' at end) or columns namespaces (no ':'). - self.category_icon_map = TagsIcons({ - 'authors' : QIcon(I('user_profile.png')), - 'series' : QIcon(I('series.png')), - 'formats' : QIcon(I('book.png')), - 'publisher' : QIcon(I('publisher.png')), - 'rating' : QIcon(I('rating.png')), - 'news' : QIcon(I('news.png')), - 'tags' : QIcon(I('tags.png')), - ':custom' : QIcon(I('column.png')), - ':user' : QIcon(I('drawer.png')), - 'search' : QIcon(I('search.png'))}) + iconmap = {} + for key in category_icon_map: + iconmap[key] = QIcon(I(category_icon_map[key])) + self.category_icon_map = TagsIcons(iconmap) + self.categories_with_ratings = ['authors', 'series', 'publisher', 'tags'] self.drag_drop_finished = drag_drop_finished diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index 69dd7ae636..dbc871026e 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -22,6 +22,20 @@ class TagsIcons(dict): raise ValueError('Missing category icon [%s]'%a) self[a] = icon_dict[a] +category_icon_map = { + 'authors' : 'user_profile.png', + 'series' : 'series.png', + 'formats' : 'book.png', + 'publisher' : 'publisher.png', + 'rating' : 'rating.png', + 'news' : 'news.png', + 'tags' : 'tags.png', + ':custom' : 'column.png', + ':user' : 'drawer.png', + 'search' : 'search.png' + } + + class FieldMetadata(dict): ''' key: the key to the dictionary is: From 8a6e6c07e7b1d9edee0cc1aa9c775849d9e544aa Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 16 Oct 2010 15:28:25 +0100 Subject: [PATCH 45/75] Get the icons --- src/calibre/library/server/browse.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index dc8494dc13..e4d001c959 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -18,6 +18,7 @@ from calibre.utils.filenames import ascii_filename from calibre.utils.config import prefs from calibre.library.comments import comments_to_html from calibre.library.server import custom_fields_to_display +from calibre.library.field_metadata import category_icon_map def render_book_list(ids, suffix=''): # {{{ pages = [] @@ -279,6 +280,16 @@ class BrowseServer(object): continue if meta['is_custom'] and category not in displayed_custom_fields: continue + # get the icon files + if category in category_icon_map: + icon = I(category_icon_map[category]) + elif meta['is_custom']: + icon = I(category_icon_map[':custom']) + elif meta['kind'] == 'user': + icon = I(category_icon_map[':user']) + else: + icon = None # shouldn't get here + cats.append((meta['name'], category)) cats = ['
  • {0}/browse/category/{1}
  • '\ .format(xml(x, True), xml(quote(y)), xml(_('Browse books by'))) From a06618d4b1229a5b0636d06cc036df2bd10bfdf2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 16 Oct 2010 08:52:28 -0600 Subject: [PATCH 46/75] ... --- resources/content_server/browse/browse.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/content_server/browse/browse.css b/resources/content_server/browse/browse.css index a22386b7a0..bd713625b4 100644 --- a/resources/content_server/browse/browse.css +++ b/resources/content_server/browse/browse.css @@ -193,6 +193,7 @@ h2.library_name { margin: 0.75em; padding: 0.75em; cursor: pointer; + font-size: larger; border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius: 15px; @@ -201,6 +202,7 @@ h2.library_name { .toplevel li img { vertical-align: middle; + margin-right: 2em; } .toplevel li:hover { From 36c0079061683ef8b781c723dce32df7a6882d83 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 16 Oct 2010 08:58:30 -0600 Subject: [PATCH 47/75] /browse: Fix sorting in book list views when server is run without --develop --- src/calibre/library/server/browse.py | 73 ++++++++++++++-------------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index d1476b34e1..d3cfc0e84a 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -200,46 +200,45 @@ class BrowseServer(object): # Templates {{{ def browse_template(self, sort, category=True, initial_search=''): - def generate(): - scn = 'calibre_browse_server_sort_' + if not hasattr(self, '__browse_template__') or \ + self.opts.develop: + self.__browse_template__ = \ + P('content_server/browse/browse.html', data=True).decode('utf-8') - if category: - sort_opts = [('rating', _('Average rating')), ('name', - _('Name')), ('popularity', _('Popularity'))] - scn += 'category' - else: - scn += 'list' - fm = self.db.field_metadata - sort_opts, added = [], set([]) - for x in fm.sortable_field_keys(): - n = fm[x]['name'] - if n not in added: - added.add(n) - sort_opts.append((x, n)) + ans = self.__browse_template__ + scn = 'calibre_browse_server_sort_' - ans = P('content_server/browse/browse.html', - data=True).decode('utf-8') - ans = ans.replace('{sort_select_label}', xml(_('Sort by')+':')) - ans = ans.replace('{sort_cookie_name}', scn) - opts = ['' % ( - 'selected="selected" ' if k==sort else '', - xml(k), xml(n), ) for k, n in - sorted(sort_opts, key=operator.itemgetter(1)) if k and n] - ans = ans.replace('{sort_select_options}', ('\n'+' '*20).join(opts)) - lp = self.db.library_path - if isbytestring(lp): - lp = force_unicode(lp, filesystem_encoding) - if isinstance(ans, unicode): - ans = ans.encode('utf-8') - ans = ans.replace('{library_name}', xml(os.path.basename(lp))) - ans = ans.replace('{library_path}', xml(lp, True)) - ans = ans.replace('{initial_search}', initial_search) - return ans + if category: + sort_opts = [('rating', _('Average rating')), ('name', + _('Name')), ('popularity', _('Popularity'))] + scn += 'category' + else: + scn += 'list' + fm = self.db.field_metadata + sort_opts, added = [], set([]) + for x in fm.sortable_field_keys(): + n = fm[x]['name'] + if n not in added: + added.add(n) + sort_opts.append((x, n)) + + ans = ans.replace('{sort_select_label}', xml(_('Sort by')+':')) + ans = ans.replace('{sort_cookie_name}', scn) + opts = ['' % ( + 'selected="selected" ' if k==sort else '', + xml(k), xml(n), ) for k, n in + sorted(sort_opts, key=operator.itemgetter(1)) if k and n] + ans = ans.replace('{sort_select_options}', ('\n'+' '*20).join(opts)) + lp = self.db.library_path + if isbytestring(lp): + lp = force_unicode(lp, filesystem_encoding) + if isinstance(ans, unicode): + ans = ans.encode('utf-8') + ans = ans.replace('{library_name}', xml(os.path.basename(lp))) + ans = ans.replace('{library_path}', xml(lp, True)) + ans = ans.replace('{initial_search}', initial_search) + return ans - if self.opts.develop: - return generate() - if not hasattr(self, '__browse_template__'): - self.__browse_template__ = generate() return self.__browse_template__ @property From 40808b4a1b5e257ba1465b55e251931c9bae5ad4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 16 Oct 2010 09:04:38 -0600 Subject: [PATCH 48/75] /browse: Preserve aspect ratio when resizing category icons --- src/calibre/library/server/browse.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index d3cfc0e84a..5e24bb1bc7 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -12,7 +12,8 @@ from binascii import hexlify, unhexlify import cherrypy from calibre.constants import filesystem_encoding -from calibre import isbytestring, force_unicode, prepare_string_for_xml as xml +from calibre import isbytestring, force_unicode, fit_image, \ + prepare_string_for_xml as xml from calibre.utils.ordered_dict import OrderedDict from calibre.utils.filenames import ascii_filename from calibre.utils.config import prefs @@ -267,7 +268,10 @@ class BrowseServer(object): raise cherrypy.HTTPError(404, 'no icon named: %r'%name) img = Image() img.load(data) - img.size = (48, 48) + width, height = img.size + scaled, width, height = fit_image(width, height, 48, 48) + if scaled: + img.size = (width, height) cherrypy.response.headers['Content-Type'] = 'image/png' cherrypy.response.headers['Last-Modified'] = self.last_modified(self.build_time) @@ -277,7 +281,7 @@ class BrowseServer(object): categories = self.categories_cache() category_meta = self.db.field_metadata cats = [ - (_('Newest'), 'newest', 'blank.png'), + (_('Newest'), 'newest', 'forward.png'), ] def getter(x): From 9181d87588a0fc3fefd7ab0ac57bf04ea7a0c778 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 16 Oct 2010 09:07:36 -0600 Subject: [PATCH 49/75] /browse: Cache top level resized icons --- src/calibre/library/server/browse.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index 5e24bb1bc7..4557ed8773 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -262,20 +262,25 @@ class BrowseServer(object): # Catalogs {{{ def browse_icon(self, name='blank.png'): - try: - data = I(name, data=True) - except: - raise cherrypy.HTTPError(404, 'no icon named: %r'%name) - img = Image() - img.load(data) - width, height = img.size - scaled, width, height = fit_image(width, height, 48, 48) - if scaled: - img.size = (width, height) cherrypy.response.headers['Content-Type'] = 'image/png' cherrypy.response.headers['Last-Modified'] = self.last_modified(self.build_time) - return img.export('png') + if not hasattr(self, '__browse_icon_cache__'): + self.__browse_icon_cache__ = {} + if name not in self.__browse_icon_cache__: + try: + data = I(name, data=True) + except: + raise cherrypy.HTTPError(404, 'no icon named: %r'%name) + img = Image() + img.load(data) + width, height = img.size + scaled, width, height = fit_image(width, height, 48, 48) + if scaled: + img.size = (width, height) + + self.__browse_icon_cache__[name] = img.export('png') + return self.__browse_icon_cache__[name] def browse_toplevel(self): categories = self.categories_cache() From 0acd342fef67a015db8edab21f713674697ca428 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 16 Oct 2010 16:11:27 +0100 Subject: [PATCH 50/75] Filter ondevice, comments, and non-displayed custom fields from sort box --- src/calibre/library/server/browse.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index d3cfc0e84a..5881bc9207 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -216,7 +216,14 @@ class BrowseServer(object): scn += 'list' fm = self.db.field_metadata sort_opts, added = [], set([]) + displayed_custom_fields = custom_fields_to_display(self.db) for x in fm.sortable_field_keys(): + if x == 'ondevice': + continue + if fm[x]['is_custom'] and x not in displayed_custom_fields: + continue + if x == 'comments' or fm[x]['datatype'] == 'comments': + continue n = fm[x]['name'] if n not in added: added.add(n) From 1aab3395cccf8e621cf37f1ce9ed723565e9c474 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 16 Oct 2010 09:18:38 -0600 Subject: [PATCH 51/75] ... --- resources/content_server/browse/browse.css | 5 ++++- src/calibre/library/server/browse.py | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/resources/content_server/browse/browse.css b/resources/content_server/browse/browse.css index bd713625b4..6f740875f4 100644 --- a/resources/content_server/browse/browse.css +++ b/resources/content_server/browse/browse.css @@ -214,7 +214,10 @@ h2.library_name { } -.toplevel li span { display: none } +.toplevel li span.url { display: none } +.toplevel li span.label { +} + /* }}} */ diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index 4557ed8773..53c4771e38 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -315,8 +315,9 @@ class BrowseServer(object): icon = 'blank.png' cats.append((meta['name'], category, icon)) - cats = [('
  • {0} {0}' - '/browse/category/{1}
  • ') + cats = [('
  • {0}' + '{0}' + '/browse/category/{1}
  • ') .format(xml(x, True), xml(quote(y)), xml(_('Browse books by')), src='/browse/icon/'+z) for x, y, z in cats] From fa9f2134c0c216e2b969cca8a525aa54a7cb3cd5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 16 Oct 2010 09:56:58 -0600 Subject: [PATCH 52/75] /browse: A grid layout for the toplevel page --- resources/content_server/browse/browse.css | 10 +++++++++- resources/content_server/browse/browse.js | 6 ++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/resources/content_server/browse/browse.css b/resources/content_server/browse/browse.css index 6f740875f4..80f71db55e 100644 --- a/resources/content_server/browse/browse.css +++ b/resources/content_server/browse/browse.css @@ -187,6 +187,9 @@ h2.library_name { list-style-type: none; margin: 0; padding: 0; + margin-left: auto; + margin-right: auto; + display: block; } .toplevel li { @@ -194,15 +197,20 @@ h2.library_name { padding: 0.75em; cursor: pointer; font-size: larger; + float: left; border-radius: 15px; -moz-border-radius: 15px; -webkit-border-radius: 15px; + display: inline; + width: 250px; + height: 48px; + overflow: hidden; } .toplevel li img { vertical-align: middle; - margin-right: 2em; + margin-right: 1em; } .toplevel li:hover { diff --git a/resources/content_server/browse/browse.js b/resources/content_server/browse/browse.js index 367b8341d9..8637ef2c96 100644 --- a/resources/content_server/browse/browse.js +++ b/resources/content_server/browse/browse.js @@ -81,6 +81,12 @@ function toplevel() { var href = $(this).children("span").html(); window.location = href; }); + + + var last = $(".toplevel li").last(); + var bottom = last.position().top + last.height(); + $("#main").height(Math.max(680, bottom+150)); + } // }}} From 144fd1ddf3f31473c1ed41406106053dbda235d0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 16 Oct 2010 09:59:14 -0600 Subject: [PATCH 53/75] ... --- resources/content_server/browse/browse.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/content_server/browse/browse.js b/resources/content_server/browse/browse.js index 8637ef2c96..5505c5bccc 100644 --- a/resources/content_server/browse/browse.js +++ b/resources/content_server/browse/browse.js @@ -78,7 +78,7 @@ function toplevel() { $(".sort_select").hide(); $(".toplevel li").click(function() { - var href = $(this).children("span").html(); + var href = $(this).children("span.url").text(); window.location = href; }); From d4d2f2b2e38e1fe6a59c814c395dadaa6b6d9bd4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 16 Oct 2010 10:14:22 -0600 Subject: [PATCH 54/75] /browse: Removing quoting and fix the News category --- src/calibre/library/server/browse.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index aae3067ba0..d17733dd70 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -6,7 +6,6 @@ __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' import operator, os, json -from urllib import quote from binascii import hexlify, unhexlify import cherrypy @@ -136,7 +135,7 @@ def get_category_items(category, items, db, datatype): # {{{ desc += '[' + _('%d books')%i.count + ']' href = '/browse/matches/%s/%s'%(category, id_) return templ.format(xml(name), rating, - xml(desc), xml(quote(href)), rstring) + xml(desc), xml(href), rstring) items = list(map(item, items)) return '\n'.join(['
    '] + items + ['
    ']) @@ -325,7 +324,7 @@ class BrowseServer(object): cats = [('
  • {0}' '{0}' '/browse/category/{1}
  • ') - .format(xml(x, True), xml(quote(y)), xml(_('Browse books by')), + .format(xml(x, True), xml(y), xml(_('Browse books by')), src='/browse/icon/'+z) for x, y, z in cats] @@ -492,7 +491,10 @@ class BrowseServer(object): ids = list(self.db.data.iterallids()) hide_sort = 'true' else: - ids = self.db.get_books_for_category(category, cid) + q = category + if q == 'news': + q = 'tags' + ids = self.db.get_books_for_category(q, cid) items = [self.db.data._data[x] for x in ids] if category == 'newest': From 7f3212585a7b9da896663c0a3cb5e2b835408c55 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 16 Oct 2010 10:21:26 -0600 Subject: [PATCH 55/75] /browse: Fix handling of user categories --- src/calibre/library/server/browse.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index d17733dd70..9d483d1b4e 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -133,7 +133,10 @@ def get_category_items(category, items, db, datatype): # {{{ desc = '' if i.count > 0: desc += '[' + _('%d books')%i.count + ']' - href = '/browse/matches/%s/%s'%(category, id_) + q = i.category + if not q: + q = category + href = '/browse/matches/%s/%s'%(q, id_) return templ.format(xml(name), rating, xml(desc), xml(href), rstring) From 40d35ff9f7b9c3522112960bf7d5c15c29213ffd Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 16 Oct 2010 10:43:24 -0600 Subject: [PATCH 56/75] /browse: Make the sort cookies session cookies and remove formats and sort from list of sortable fields --- resources/content_server/browse/browse.js | 2 +- src/calibre/library/server/browse.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/content_server/browse/browse.js b/resources/content_server/browse/browse.js index 5505c5bccc..99755f5f99 100644 --- a/resources/content_server/browse/browse.js +++ b/resources/content_server/browse/browse.js @@ -55,7 +55,7 @@ function init_sort_combobox() { selectedList: 1, click: function(event, ui){ $(this).multiselect("close"); - cookie(sort_cookie_name, ui.value, {expires: 365}); + cookie(sort_cookie_name, ui.value); window.location.reload(); } }); diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index 9d483d1b4e..c45a47932d 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -221,7 +221,7 @@ class BrowseServer(object): sort_opts, added = [], set([]) displayed_custom_fields = custom_fields_to_display(self.db) for x in fm.sortable_field_keys(): - if x == 'ondevice': + if x in ('ondevice', 'formats', 'sort'): continue if fm[x]['is_custom'] and x not in displayed_custom_fields: continue From 9beeea57b116981bcccad9aa1ce594f990f28e59 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 16 Oct 2010 10:45:01 -0600 Subject: [PATCH 57/75] /browse: Make series sorting descending --- src/calibre/library/server/browse.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index c45a47932d..f3a319f173 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -464,7 +464,8 @@ class BrowseServer(object): sort = 'title' self.sort(items, 'title', True) if sort != 'title': - ascending = fm[sort]['datatype'] not in ('rating', 'datetime') + ascending = fm[sort]['datatype'] not in ('rating', 'datetime', + 'series') self.sort(items, sort, ascending) return sort From eb83ea3b9f0f09028b82e4dfedbf76b0b9b94a54 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 16 Oct 2010 12:07:31 -0600 Subject: [PATCH 58/75] /browse: Proper resizing of top level grid view --- resources/content_server/browse/browse.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/resources/content_server/browse/browse.js b/resources/content_server/browse/browse.js index 99755f5f99..777a285878 100644 --- a/resources/content_server/browse/browse.js +++ b/resources/content_server/browse/browse.js @@ -74,6 +74,14 @@ function init() { } // Top-level feed {{{ + +function toplevel_layout() { + var last = $(".toplevel li").last(); + var title = $('.toplevel h3').first(); + var bottom = last.position().top + last.height() - title.position().top; + $("#main").height(Math.max(200, bottom)); +} + function toplevel() { $(".sort_select").hide(); @@ -82,10 +90,8 @@ function toplevel() { window.location = href; }); - - var last = $(".toplevel li").last(); - var bottom = last.position().top + last.height(); - $("#main").height(Math.max(680, bottom+150)); + toplevel_layout(); + $(window).resize(toplevel_layout); } // }}} From 0f7478206258a7fbbaa47367d323f189dc1a2950 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 16 Oct 2010 12:08:15 -0600 Subject: [PATCH 59/75] ... --- resources/content_server/browse/browse.js | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/resources/content_server/browse/browse.js b/resources/content_server/browse/browse.js index 777a285878..b19b1c0804 100644 --- a/resources/content_server/browse/browse.js +++ b/resources/content_server/browse/browse.js @@ -1,5 +1,35 @@ // Cookies {{{ +/** + * Create a cookie with the given name and value and other optional parameters. + * + * @example $.cookie('the_cookie', 'the_value'); + * @desc Set the value of a cookie. + * @example $.cookie('the_cookie', 'the_value', { expires: 7, path: '/', domain: 'jquery.com', secure: true }); + * @desc Create a cookie with all available options. + * @example $.cookie('the_cookie', 'the_value'); + * @desc Create a session cookie. + * @example $.cookie('the_cookie', null); + * @desc Delete a cookie by passing null as value. Keep in mind that you have to use the same path and domain + * used when the cookie was set. + * + * @param String name The name of the cookie. + * @param String value The value of the cookie. + * @param Object options An object literal containing key/value pairs to provide optional cookie attributes. + * @option Number|Date expires Either an integer specifying the expiration date from now on in days or a Date object. + * If a negative value is specified (e.g. a date in the past), the cookie will be deleted. + * If set to null or omitted, the cookie will be a session cookie and will not be retained + * when the the browser exits. + * @option String path The value of the path atribute of the cookie (default: path of page that created the cookie). + * @option String domain The value of the domain attribute of the cookie (default: domain of page that created the cookie). + * @option Boolean secure If true, the secure attribute of the cookie will be set and the cookie transmission will + * require a secure protocol (like HTTPS). + * @type undefined + * + * @name $.cookie + * @cat Plugins/Cookie + * @author Klaus Hartl/klaus.hartl@stilbuero.de + */ function cookie(name, value, options) { if (typeof value != 'undefined') { // name and value given, set cookie From a490b3fbed8549e61107d7ab88fa69fddd8d6ad9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 16 Oct 2010 12:52:04 -0600 Subject: [PATCH 60/75] /browse: When browsing by series, force sorting to alway be by series --- src/calibre/library/server/browse.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index f3a319f173..7331442934 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -477,14 +477,17 @@ class BrowseServer(object): if category not in categories and category != 'newest': raise cherrypy.HTTPError(404, 'category not found') + fm = self.db.field_metadata try: - category_name = self.db.field_metadata[category]['name'] + category_name = fm[category]['name'] + dt = fm[category]['datatype'] except: if category != 'newest': raise category_name = _('Newest') + dt = None - hide_sort = 'false' + hide_sort = 'true' if dt == 'series' else 'false' if category == 'search': which = unhexlify(cid) try: @@ -503,6 +506,8 @@ class BrowseServer(object): items = [self.db.data._data[x] for x in ids] if category == 'newest': list_sort = 'timestamp' + if dt == 'series': + list_sort = category sort = self.browse_sort_book_list(items, list_sort) ids = [x[0] for x in items] html = render_book_list(ids, suffix=_('in') + ' ' + category_name) From c5ebc3e5a3679a9dc327df9aa23e1f45df997d54 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 16 Oct 2010 13:23:24 -0600 Subject: [PATCH 61/75] /browse: Add permalink for every book --- resources/content_server/browse/browse.css | 4 +-- resources/content_server/browse/browse.js | 14 +++++++- resources/content_server/browse/summary.html | 1 + src/calibre/library/server/browse.py | 37 +++++++++++++++----- 4 files changed, 44 insertions(+), 12 deletions(-) diff --git a/resources/content_server/browse/browse.css b/resources/content_server/browse/browse.css index 80f71db55e..92ed4c3ce6 100644 --- a/resources/content_server/browse/browse.css +++ b/resources/content_server/browse/browse.css @@ -417,12 +417,12 @@ h2.library_name { margin-bottom: 2ex; } -#book_details_dialog .details a { +.details .right .formats a { color: blue; text-decoration: none; } -#book_details_dialog .details a:hover { +.details .right .formats a:hover { color: red; } diff --git a/resources/content_server/browse/browse.js b/resources/content_server/browse/browse.js index b19b1c0804..29b84ac2d7 100644 --- a/resources/content_server/browse/browse.js +++ b/resources/content_server/browse/browse.js @@ -235,8 +235,10 @@ function load_page(elem) { elem.show(); } +function hidesort() {$("#content > .sort_select").hide();} + function booklist(hide_sort) { - if (hide_sort) $("#content > .sort_select").hide(); + if (hide_sort) hidesort(); var test = $("#booklist #page0").html(); if (!test) { $("#booklist").html(render_error("No books found")); @@ -275,3 +277,13 @@ function show_details(a_dom) { } // }}} + +function book() { + hidesort(); + $('.details .left img').load(function() { + var img = $('.details .left img'); + var height = $('#main').height(); + height = Math.max(height, img.height() + 100); + $('#main').height(height); + }); +} diff --git a/resources/content_server/browse/summary.html b/resources/content_server/browse/summary.html index 4e9c9d2a77..de175d3b53 100644 --- a/resources/content_server/browse/summary.html +++ b/resources/content_server/browse/summary.html @@ -8,6 +8,7 @@ {stars} {series} {details} + {permalink}
    {title}
    {authors}
    diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index 7331442934..247e6945e6 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -197,6 +197,8 @@ class BrowseServer(object): self.browse_search) connect('browse_details', base_href+'/details/{id}', self.browse_details) + connect('browse_book', base_href+'/book/{id}', + self.browse_book) connect('browse_category_icon', base_href+'/icon/{name}', self.browse_icon) @@ -589,23 +591,19 @@ class BrowseServer(object): args['series'] = args['series'] args['details'] = xml(_('Details'), True) args['details_tt'] = xml(_('Show book details'), True) + args['permalink'] = xml(_('Permalink'), True) + args['permalink_tt'] = xml(_('A permanent link to this book'), True) summs.append(self.browse_summary_template.format(**args)) return json.dumps('\n'.join(summs), ensure_ascii=False) - @Endpoint(mimetype='application/json; charset=utf-8') - def browse_details(self, id=None): - try: - id_ = int(id) - except: - raise cherrypy.HTTPError(404, 'invalid id: %r'%id) - + def browse_render_details(self, id_): try: mi = self.db.get_metadata(id_, index_is_id=True) except: - ans = _('This book has been deleted') + return _('This book has been deleted') else: args, fmt, fmts, fname = self.browse_get_book_args(mi, id_) args['formats'] = '' @@ -646,13 +644,34 @@ class BrowseServer(object): u'
    %s
    ') % (xml(c[0]), c[1]) for c in comments] comments = u'
    %s
    '%('\n\n'.join(comments)) - ans = self.browse_details_template.format(id=id_, + + return self.browse_details_template.format(id=id_, title=xml(mi.title, True), fields=fields, formats=args['formats'], comments=comments) + @Endpoint(mimetype='application/json; charset=utf-8') + def browse_details(self, id=None): + try: + id_ = int(id) + except: + raise cherrypy.HTTPError(404, 'invalid id: %r'%id) + + ans = self.browse_render_details(id_) + return json.dumps(ans, ensure_ascii=False) + @Endpoint() + def browse_book(self, id=None, category_sort=None): + try: + id_ = int(id) + except: + raise cherrypy.HTTPError(404, 'invalid id: %r'%id) + + ans = self.browse_render_details(id_) + return self.browse_template('').format( + title='', script='book();', main=ans) + # }}} From 1022ddd1014601649b4688adcd70e5b668418a4f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 16 Oct 2010 17:02:49 -0600 Subject: [PATCH 62/75] Linux device mounting: Use udisks, if it is available, to mount devices, so that I no longer have to hear bug reports from users using distro packages that have crippled calibre-mount-helper. You can turn off udisks by setting the environment variable CALIBRE_DISABLE_UDISKS=1 --- src/calibre/devices/udisks.py | 88 +++++++++++++++++++++++++++++ src/calibre/devices/usbms/device.py | 25 ++++---- src/calibre/manual/customize.rst | 1 + 3 files changed, 104 insertions(+), 10 deletions(-) create mode 100644 src/calibre/devices/udisks.py diff --git a/src/calibre/devices/udisks.py b/src/calibre/devices/udisks.py new file mode 100644 index 0000000000..ba26c2b56c --- /dev/null +++ b/src/calibre/devices/udisks.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import dbus +import os + +def node_mountpoint(node): + + def de_mangle(raw): + return raw.replace('\\040', ' ').replace('\\011', '\t').replace('\\012', + '\n').replace('\\0134', '\\') + + for line in open('/proc/mounts').readlines(): + line = line.split() + if line[0] == node: + return de_mangle(line[1]) + return None + + +class UDisks(object): + + def __init__(self): + if os.environ.get('CALIBRE_DISABLE_UDISKS', False): + raise Exception('User has aborted use of UDISKS') + self.bus = dbus.SystemBus() + self.main = dbus.Interface(self.bus.get_object('org.freedesktop.UDisks', + '/org/freedesktop/UDisks'), 'org.freedesktop.UDisks') + + def device(self, device_node_path): + devpath = self.main.FindDeviceByDeviceFile(device_node_path) + return dbus.Interface(self.bus.get_object('org.freedesktop.UDisks', + devpath), 'org.freedesktop.UDisks.Device') + + def mount(self, device_node_path): + d = self.device(device_node_path) + try: + return unicode(d.FilesystemMount('', + ['auth_no_user_interaction', 'rw', 'noexec', 'nosuid', + 'sync', 'nodev', 'uid=1000', 'gid=1000'])) + except: + # May be already mounted, check + mp = node_mountpoint(str(device_node_path)) + if mp is None: + raise + return mp + + def unmount(self, device_node_path): + d = self.device(device_node_path) + d.FilesystemUnmount(['force']) + + def eject(self, device_node_path): + parent = device_node_path + while parent[-1] in '0123456789': + parent = parent[:-1] + devices = [str(x) for x in self.main.EnumerateDeviceFiles()] + for d in devices: + if d.startswith(parent) and d != parent: + try: + self.unmount(d) + except: + import traceback + print 'Failed to unmount:', d + traceback.print_exc() + d = self.device(parent) + d.DriveEject([]) + +def mount(node_path): + u = UDisks() + u.mount(node_path) + +def eject(node_path): + u = UDisks() + u.eject(node_path) + +if __name__ == '__main__': + import sys + dev = sys.argv[1] + print 'Testing with node', dev + u = UDisks() + print 'Mounted at:', u.mount(dev) + print 'Ejecting' + u.eject(dev) + + diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py index 6fcfb9e7f0..6f938cbcbd 100644 --- a/src/calibre/devices/usbms/device.py +++ b/src/calibre/devices/usbms/device.py @@ -530,16 +530,8 @@ class Device(DeviceConfig, DevicePlugin): return drives def node_mountpoint(self, node): - - def de_mangle(raw): - return raw.replace('\\040', ' ').replace('\\011', '\t').replace('\\012', - '\n').replace('\\0134', '\\') - - for line in open('/proc/mounts').readlines(): - line = line.split() - if line[0] == node: - return de_mangle(line[1]) - return None + from calibre.devices.udisks import node_mountpoint + return node_mountpoint(node) def find_largest_partition(self, path): node = path.split('/')[-1] @@ -585,6 +577,13 @@ class Device(DeviceConfig, DevicePlugin): label += ' (%d)'%extra def do_mount(node, label): + try: + from calibre.devices.udisks import mount + mount(node) + return 0 + except: + pass + cmd = 'calibre-mount-helper' if getattr(sys, 'frozen_path', False): cmd = os.path.join(sys.frozen_path, cmd) @@ -617,6 +616,7 @@ class Device(DeviceConfig, DevicePlugin): if not mp.endswith('/'): mp += '/' self._linux_mount_map[main] = mp self._main_prefix = mp + self._linux_main_device_node = main cards = [(carda, '_card_a_prefix', 'carda'), (cardb, '_card_b_prefix', 'cardb')] for card, prefix, typ in cards: @@ -732,6 +732,11 @@ class Device(DeviceConfig, DevicePlugin): pass def eject_linux(self): + try: + from calibre.devices.udisks import eject + return eject(self._linux_main_device_node) + except: + pass drives = self.find_device_nodes() for drive in drives: if drive: diff --git a/src/calibre/manual/customize.rst b/src/calibre/manual/customize.rst index c35defc0b0..e0f799f572 100644 --- a/src/calibre/manual/customize.rst +++ b/src/calibre/manual/customize.rst @@ -24,6 +24,7 @@ Environment variables * ``CALIBRE_OVERRIDE_DATABASE_PATH`` - allows you to specify the full path to metadata.db. Using this variable you can have metadata.db be in a location other than the library folder. Useful if your library folder is on a networked drive that does not support file locking. * ``CALIBRE_DEVELOP_FROM`` - Used to run from a calibre development environment. See :ref:`develop`. * ``CALIBRE_OVERRIDE_LANG`` - Used to force the language used by the interface (ISO 639 language code) + * ``CALIBRE_DISABLE_UDISKS`` - Used to disable the use of udisks for mounting/ejecting. Set it to 1 to use calibre-mount-helper instead. * ``SYSFS_PATH`` - Use if sysfs is mounted somewhere other than /sys * ``http_proxy`` - Used on linux to specify an HTTP proxy From c2d0a57a4187bf32de5b669e922f1efc9c6c2907 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 16 Oct 2010 20:09:46 -0600 Subject: [PATCH 63/75] EPUB Input: Make parsing of toc.ncx more robust. Fixes #7170 (Some epub hierarchical tables of contents are not interpreted correctly) --- src/calibre/ebooks/metadata/toc.py | 74 +++++++++++++++++------------- 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/src/calibre/ebooks/metadata/toc.py b/src/calibre/ebooks/metadata/toc.py index 8c6f3f6baf..0ed527d26a 100644 --- a/src/calibre/ebooks/metadata/toc.py +++ b/src/calibre/ebooks/metadata/toc.py @@ -2,7 +2,7 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' -import os, glob, re +import os, glob, re, functools from urlparse import urlparse from urllib import unquote from uuid import uuid4 @@ -11,7 +11,7 @@ from lxml import etree from lxml.builder import ElementMaker from calibre.constants import __appname__, __version__ -from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup, BeautifulSoup +from calibre.ebooks.BeautifulSoup import BeautifulSoup from calibre.ebooks.chardet import xml_to_unicode NCX_NS = "http://www.daisy.org/z3986/2005/ncx/" @@ -26,14 +26,6 @@ E = ElementMaker(namespace=NCX_NS, nsmap=NSMAP) C = ElementMaker(namespace=CALIBRE_NS, nsmap=NSMAP) -class NCXSoup(BeautifulStoneSoup): - - NESTABLE_TAGS = {'navpoint':[]} - - def __init__(self, raw): - BeautifulStoneSoup.__init__(self, raw, - convertEntities=BeautifulSoup.HTML_ENTITIES, - selfClosingTags=['meta', 'content']) class TOC(list): @@ -166,40 +158,60 @@ class TOC(list): def read_ncx_toc(self, toc): self.base_path = os.path.dirname(toc) - raw = xml_to_unicode(open(toc, 'rb').read(), assume_utf8=True)[0] - soup = NCXSoup(raw) + raw = xml_to_unicode(open(toc, 'rb').read(), assume_utf8=True, + strip_encoding_pats=True)[0] + root = etree.fromstring(raw, parser=etree.XMLParser(recover=True, + no_network=True)) + xpn = {'re': 'http://exslt.org/regular-expressions'} + XPath = functools.partial(etree.XPath, namespaces=xpn) + + def get_attr(node, default=None, attr='playorder'): + for name, val in node.attrib.items(): + if name and val and name.lower().endswith(attr): + return val + return default + + nl_path = XPath('./*[re:match(local-name(), "navlabel$", "i")]') + txt_path = XPath('./*[re:match(local-name(), "text$", "i")]') + content_path = XPath('./*[re:match(local-name(), "content$", "i")]') + np_path = XPath('./*[re:match(local-name(), "navpoint$", "i")]') def process_navpoint(np, dest): - play_order = np.get('playOrder', None) - if play_order is None: - play_order = int(np.get('playorder', 1)) + try: + play_order = int(get_attr(np, 1)) + except: + play_order = 1 href = fragment = text = None - nl = np.find(re.compile('navlabel')) - if nl is not None: + nl = nl_path(np) + if nl: + nl = nl[0] text = u'' - for txt in nl.findAll(re.compile('text')): - text += u''.join([unicode(s) for s in txt.findAll(text=True)]) - content = np.find(re.compile('content')) - if content is None or not content.has_key('src') or not txt: + for txt in txt_path(nl): + text += etree.tostring(txt, method='text', + encoding=unicode, with_tail=False) + content = content_path(np) + if not content or not text: + return + content = content[0] + src = get_attr(content, attr='src') + if src is None: return - purl = urlparse(unquote(content['src'])) + purl = urlparse(unquote(content.get('src'))) href, fragment = purl[2], purl[5] nd = dest.add_item(href, fragment, text) nd.play_order = play_order - for c in np: - if 'navpoint' in getattr(c, 'name', ''): - process_navpoint(c, nd) + for c in np_path(np): + process_navpoint(c, nd) - nm = soup.find(re.compile('navmap')) - if nm is None: + nm = XPath('//*[re:match(local-name(), "navmap$", "i")]')(root) + if not nm: raise ValueError('NCX files must have a element.') + nm = nm[0] - for elem in nm: - if 'navpoint' in getattr(elem, 'name', ''): - process_navpoint(elem, self) - + for child in np_path(nm): + process_navpoint(child, self) def read_html_toc(self, toc): self.base_path = os.path.dirname(toc) From 3a5de491d08eeeb83b65c22e772e235d2dbce579 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 16 Oct 2010 22:13:39 -0600 Subject: [PATCH 64/75] Malaysian Mirror by Tony Stegall --- resources/recipes/malaysian_mirror.recipe | 89 +++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 resources/recipes/malaysian_mirror.recipe diff --git a/resources/recipes/malaysian_mirror.recipe b/resources/recipes/malaysian_mirror.recipe new file mode 100644 index 0000000000..e61538431a --- /dev/null +++ b/resources/recipes/malaysian_mirror.recipe @@ -0,0 +1,89 @@ +#!/usr/bin/env python +__license__ = 'GPL v3' +__author__ = 'Tony Stegall' +__copyright__ = '2010, Tony Stegall or Tonythebookworm on mobiread.com' +__version__ = '1' +__date__ = '16, October 2010' +__docformat__ = 'English' + + + +from calibre.web.feeds.news import BasicNewsRecipe + +class MalaysianMirror(BasicNewsRecipe): + title = 'MalaysianMirror' + __author__ = 'Tonythebookworm' + description = 'The Pulse of the Nation' + language = 'en' + no_stylesheets = True + publisher = 'Tonythebookworm' + category = 'news' + use_embedded_content= False + no_stylesheets = True + oldest_article = 24 + + remove_javascript = True + remove_empty_feeds = True + conversion_options = {'linearize_tables' : True} + extra_css = ''' + #content_heading{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;} + + td{text-align:right; font-size:small;margin-top:0px;margin-bottom: 0px;} + + #content_body{font-family:Helvetica,Arial,sans-serif;font-size:small;} + ''' + + keep_only_tags = [dict(name='table', attrs={'class':['contentpaneopen']}) + ] + remove_tags = [dict(name='table', attrs={'class':['buttonheading']})] + ####################################################################################################################### + + + max_articles_per_feed = 10 + + ''' + Make a variable that will hold the url for the main site because our links do not include the index + ''' + + INDEX = 'http://www.malaysianmirror.com' + + + + + def parse_index(self): + feeds = [] + for title, url in [ + (u"Media Buzz", u"http://www.malaysianmirror.com/media-buzz-front"), + (u"Life Style", u"http://www.malaysianmirror.com/lifestylefront"), + (u"Features", u"http://www.malaysianmirror.com/featurefront"), + + + ]: + articles = self.make_links(url) + if articles: + feeds.append((title, articles)) + return feeds + + def make_links(self, url): + title = 'Temp' + current_articles = [] + soup = self.index_to_soup(url) + # print 'The soup is: ', soup + for item in soup.findAll('div', attrs={'class':'contentheading'}): + #print 'item is: ', item + link = item.find('a') + #print 'the link is: ', link + if link: + url = self.INDEX + link['href'] + title = self.tag_to_string(link) + #print 'the title is: ', title + #print 'the url is: ', url + #print 'the title is: ', title + current_articles.append({'title': title, 'url': url, 'description':'', 'date':''}) # append all this + return current_articles + + def preprocess_html(self, soup): + for item in soup.findAll(attrs={'style':True}): + del item['style'] + return soup + From 9c0ac34187901475b0c61bb4db885994db09594f Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 17 Oct 2010 05:30:09 +0100 Subject: [PATCH 65/75] Extend functionality of template function 'lookup' --- src/calibre/utils/formatter.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 5e2cb6535a..b7ec14ce7a 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -22,11 +22,21 @@ class TemplateFormatter(string.Formatter): self.book = None self.kwargs = None - def _lookup(self, val, field_if_set, field_not_set): - if val: - return self.vformat('{'+field_if_set.strip()+'}', [], self.kwargs) - else: - return self.vformat('{'+field_not_set.strip()+'}', [], self.kwargs) + def _lookup(self, val, *args): + if len(args) == 2: + if val: + return self.vformat('{'+args[0].strip()+'}', [], self.kwargs) + else: + return self.vformat('{'+args[1].strip()+'}', [], self.kwargs) + if (len(args) % 2) != 1: + raise ValueError(_('lookup requires either 2 or an odd number of arguments')) + i = 0 + while i < len(args): + if i + 1 >= len(args): + return self.vformat('{' + args[i].strip() + '}', [], self.kwargs) + if re.search(args[i], val): + return self.vformat('{'+args[i+1].strip() + '}', [], self.kwargs) + i += 2 def _test(self, val, value_if_set, value_not_set): if val: @@ -41,6 +51,8 @@ class TemplateFormatter(string.Formatter): return value_if_not def _switch(self, val, *args): + if (len(args) % 2) != 1: + raise ValueError(_('switch requires an odd number of arguments')) i = 0 while i < len(args): if i + 1 >= len(args): @@ -73,7 +85,7 @@ class TemplateFormatter(string.Formatter): 'capitalize' : (0, lambda s,x: x.capitalize()), 'contains' : (3, _contains), 'ifempty' : (1, _ifempty), - 'lookup' : (2, _lookup), + 'lookup' : (-1, _lookup), 're' : (2, _re), 'shorten' : (3, _shorten), 'switch' : (-1, _switch), @@ -129,9 +141,9 @@ class TemplateFormatter(string.Formatter): (func[0] > 0 and func[0] != len(args)): raise ValueError('Incorrect number of arguments for function '+ fmt[0:p]) if func[0] == 0: - val = func[1](self, val) + val = func[1](self, val).strip() else: - val = func[1](self, val, *args) + val = func[1](self, val, *args).strip() if val: val = string.Formatter.format_field(self, val, dispfmt) if not val: From d4f409646a4a8556741957c21f31232b6adb1ac8 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 17 Oct 2010 05:36:10 +0100 Subject: [PATCH 66/75] 1) change author_sort tweak documentation to note that the values must be recomputed 2) change 'lookup' documentation in the template faq --- resources/default_tweaks.py | 3 +++ src/calibre/manual/template_lang.rst | 4 ++-- src/calibre/utils/formatter.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index 48845da920..86921886ad 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -25,6 +25,9 @@ series_index_auto_increment = 'next' # copy : copy author to author_sort without modification # comma : use 'copy' if there is a ',' in the name, otherwise use 'invert' # nocomma : "fn ln" -> "ln fn" (without the comma) +# When this tweak is changed, the author_sort values stored with each author +# must be recomputed by right-clicking on an author in the left-hand tags pane, +# selecting 'manage authors', and pressing 'Recalculate all author sort values'. author_sort_copy_method = 'invert' diff --git a/src/calibre/manual/template_lang.rst b/src/calibre/manual/template_lang.rst index b731dfe26e..e1eb876cb7 100644 --- a/src/calibre/manual/template_lang.rst +++ b/src/calibre/manual/template_lang.rst @@ -122,7 +122,7 @@ The functions available are: * ``switch(pattern, value, pattern, value, ..., else_value)`` -- for each ``pattern, value`` pair, checks if the field matches the regular expression ``pattern`` and if so, returns that ``value``. If no ``pattern`` matches, then ``else_value`` is returned. You can have as many ``pattern, value`` pairs as you want. * ``re(pattern, replacement)`` -- return the field after applying the regular expression. All instances of `pattern` are replaced with `replacement`. As in all of |app|, these are python-compatible regular expressions. * ``shorten(left chars, middle text, right chars)`` -- Return a shortened version of the field, consisting of `left chars` characters from the beginning of the field, followed by `middle text`, followed by `right chars` characters from the end of the string. `Left chars` and `right chars` must be integers. For example, assume the title of the book is `Ancient English Laws in the Times of Ivanhoe`, and you want it to fit in a space of at most 15 characters. If you use ``{title:shorten(9,-,5)}``, the result will be `Ancient E-nhoe`. If the field's length is less than ``left chars`` + ``right chars`` + the length of ``middle text``, then the field will be used intact. For example, the title `The Dome` would not be changed. - * ``lookup(field if not empty, field if empty)`` -- like test, except the arguments are field (metadata) names, not text. The value of the appropriate field will be fetched and used. Note that because composite columns are fields, you can use this function in one composite field to use the value of some other composite field. This is extremely useful when constructing variable save paths (more later). + * ``lookup(pattern, field, pattern, field, ..., else_field)`` -- like switch, except the arguments are field (metadata) names, not text. The value of the appropriate field will be fetched and used. Note that because composite columns are fields, you can use this function in one composite field to use the value of some other composite field. This is extremely useful when constructing variable save paths (more later). Now, about using functions and formatting in the same field. Suppose you have an integer custom column called ``#myint`` that you want to see with leading zeros, as in ``003``. To do this, you would use a format of ``0>3s``. However, by default, if a number (integer or float) equals zero then the field produces the empty value, so zero values will produce nothing, not ``000``. If you really want to see ``000`` values, then you use both the format string and the ``ifempty`` function to change the empty value back to a zero. The field reference would be:: @@ -151,7 +151,7 @@ The lookup function lets us do even fancier processing. For example, assume that To accomplish this, we: 1. Create a composite field (call it AA) containing ``{series}/{series_index} - {title'}``. If the series is not empty, then this template will produce `series/series_index - title`. 2. Create a composite field (call it BB) containing ``{#genre:ifempty(Unknown)}/{author_sort}/{title}``. This template produces `genre/author_sort/title`, where an empty genre is replaced wuth `Unknown`. - 3. Set the save template to ``{series:lookup(AA,BB)}``. This template chooses composite field AA if series is not empty, and composite field BB if series is empty. We therefore have two completely different save paths, depending on whether or not `series` is empty. + 3. Set the save template to ``{series:lookup(.,AA,BB)}``. This template chooses composite field AA if series is not empty, and composite field BB if series is empty. We therefore have two completely different save paths, depending on whether or not `series` is empty. Templates and Plugboards ------------------------ diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index b7ec14ce7a..76c086cc58 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -23,7 +23,7 @@ class TemplateFormatter(string.Formatter): self.kwargs = None def _lookup(self, val, *args): - if len(args) == 2: + if len(args) == 2: # here for backwards compatibility if val: return self.vformat('{'+args[0].strip()+'}', [], self.kwargs) else: From 2ac585471492e0dd4c3a2d5863878b1b1f9a818e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 17 Oct 2010 07:21:15 -0600 Subject: [PATCH 67/75] Fix #7195 (Height of Metadata Bulk Edit dialog) --- src/calibre/gui2/dialogs/metadata_bulk.ui | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui index 3897d6dbf9..8c60715ae2 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.ui +++ b/src/calibre/gui2/dialogs/metadata_bulk.ui @@ -7,7 +7,7 @@ 0 0 752 - 715 + 633 @@ -39,7 +39,7 @@ - 0 + 2 @@ -660,8 +660,8 @@ nothing should be put between the original text and the inserted text 0 0 - 122 - 34 + 726 + 334 @@ -682,19 +682,6 @@ nothing should be put between the original text and the inserted text - - - - Qt::Vertical - - - - 20 - 0 - - - - From 7020ce507edcf494d141697a1734081e6994c69b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 17 Oct 2010 08:14:12 -0600 Subject: [PATCH 68/75] Fix #7198 (EPUB issue- Incorrect format for language in toc.ncx) --- src/calibre/ebooks/oeb/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index e85098e293..2d2945c26a 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -1908,6 +1908,7 @@ class OEBBook(object): def _to_ncx(self): lang = unicode(self.metadata.language[0]) + lang = lang.replace('_', '-') ncx = etree.Element(NCX('ncx'), attrib={'version': '2005-1', XML('lang'): lang}, nsmap={None: NCX_NS}) From ca4953f0287194cc1bde6548d1e00e02c2314898 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 17 Oct 2010 08:33:32 -0600 Subject: [PATCH 69/75] Implement #7199 (Optional subscription) --- src/calibre/gui2/dialogs/metadata_bulk.ui | 2 +- src/calibre/gui2/dialogs/scheduler.py | 21 +++++++++++++++------ src/calibre/web/feeds/news.py | 7 +++++-- src/calibre/web/feeds/recipes/collection.py | 7 ++++++- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui index 8c60715ae2..0fe537b598 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.ui +++ b/src/calibre/gui2/dialogs/metadata_bulk.ui @@ -39,7 +39,7 @@ - 2 + 0 diff --git a/src/calibre/gui2/dialogs/scheduler.py b/src/calibre/gui2/dialogs/scheduler.py index 30f4a2d8a2..071c5778a8 100644 --- a/src/calibre/gui2/dialogs/scheduler.py +++ b/src/calibre/gui2/dialogs/scheduler.py @@ -120,12 +120,15 @@ class SchedulerDialog(QDialog, Ui_Dialog): if self.account.isVisible(): un, pw = map(unicode, (self.username.text(), self.password.text())) + un, pw = un.strip(), pw.strip() if not un and not pw and self.schedule.isChecked(): - error_dialog(self, _('Need username and password'), - _('You must provide a username and/or password to ' - 'use this news source.'), show=True) - return False - self.recipe_model.set_account_info(urn, un.strip(), pw.strip()) + if not getattr(self, 'subscription_optional', False): + error_dialog(self, _('Need username and password'), + _('You must provide a username and/or password to ' + 'use this news source.'), show=True) + return False + if un or pw: + self.recipe_model.set_account_info(urn, un, pw) if self.schedule.isChecked(): schedule_type = 'interval' if self.interval_button.isChecked() else 'day/time' @@ -157,7 +160,13 @@ class SchedulerDialog(QDialog, Ui_Dialog): account_info = self.recipe_model.account_info_from_urn(urn) customize_info = self.recipe_model.get_customize_info(urn) - self.account.setVisible(recipe.get('needs_subscription', '') == 'yes') + ns = recipe.get('needs_subscription', '') + self.account.setVisible(ns in ('yes', 'optional')) + self.subscription_optional = ns == 'optional' + act = _('Account') + act2 = _('(optional)') if self.subscription_optional else \ + _('(required)') + self.account.setTitle(act+' '+act2) un = pw = '' if account_info is not None: un, pw = account_info[:2] diff --git a/src/calibre/web/feeds/news.py b/src/calibre/web/feeds/news.py index d1e7866198..f3d77061c3 100644 --- a/src/calibre/web/feeds/news.py +++ b/src/calibre/web/feeds/news.py @@ -110,9 +110,11 @@ class BasicNewsRecipe(Recipe): #: If True the GUI will ask the user for a username and password #: to use while downloading - #: @type: boolean + #: If set to "optional" the use of a username and password becomes optional needs_subscription = False + #: + #: If True the navigation bar is center aligned, otherwise it is left aligned center_navbar = True @@ -609,7 +611,8 @@ class BasicNewsRecipe(Recipe): if self.needs_subscription and (\ self.username is None or self.password is None or \ (not self.username and not self.password)): - raise ValueError(_('The "%s" recipe needs a username and password.')%self.title) + if self.needs_subscription != 'optional': + raise ValueError(_('The "%s" recipe needs a username and password.')%self.title) self.browser = self.get_browser() self.image_map, self.image_counter = {}, 1 diff --git a/src/calibre/web/feeds/recipes/collection.py b/src/calibre/web/feeds/recipes/collection.py index 1dd19dc524..012e24a799 100644 --- a/src/calibre/web/feeds/recipes/collection.py +++ b/src/calibre/web/feeds/recipes/collection.py @@ -45,12 +45,17 @@ def serialize_recipe(urn, recipe_class): return ans default_author = _('You') if urn.startswith('custom:') else _('Unknown') + ns = attr('needs_subscription', False) + if not ns: + ns = 'no' + if ns is True: + ns = 'yes' return E.recipe({ 'id' : str(urn), 'title' : attr('title', _('Unknown')), 'author' : attr('__author__', default_author), 'language' : attr('language', 'und'), - 'needs_subscription' : 'yes' if attr('needs_subscription', False) else 'no', + 'needs_subscription' : ns, 'description' : attr('description', '') }) From 30b3d2a56485b01d68e18ad0a5a0dd94909e0fb7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 17 Oct 2010 09:05:40 -0600 Subject: [PATCH 70/75] CHM Input: Fix handling of relative file paths in tags. Fixes #7159 (Large CHM file fails conversion) --- src/calibre/ebooks/chm/reader.py | 42 +++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/src/calibre/ebooks/chm/reader.py b/src/calibre/ebooks/chm/reader.py index 73587edfa4..025e252005 100644 --- a/src/calibre/ebooks/chm/reader.py +++ b/src/calibre/ebooks/chm/reader.py @@ -93,6 +93,7 @@ class CHMReader(CHMFile): return data def ExtractFiles(self, output_dir=os.getcwdu()): + html_files = set([]) for path in self.Contents(): lpath = os.path.join(output_dir, path) self._ensure_dir(lpath) @@ -106,14 +107,27 @@ class CHMReader(CHMFile): lpath = lpath.split(';')[0] try: with open(lpath, 'wb') as f: - if guess_mimetype(path)[0] == ('text/html'): - data = self._reformat(data) f.write(data) + try: + if 'html' in guess_mimetype(path)[0]: + html_files.add(lpath) + except: + pass except: if iswindows and len(lpath) > 250: self.log.warn('%r filename too long, skipping'%path) continue raise + for lpath in html_files: + with open(lpath, 'r+b') as f: + data = f.read() + data = self._reformat(data, lpath) + if isinstance(data, unicode): + data = data.encode('utf-8') + f.seek(0) + f.truncate() + f.write(data) + self._extracted = True files = [x for x in os.listdir(output_dir) if os.path.isfile(os.path.join(output_dir, x))] @@ -125,7 +139,7 @@ class CHMReader(CHMFile): if self.hhc_path not in files and files: self.hhc_path = files[0] - def _reformat(self, data): + def _reformat(self, data, htmlpath): try: data = xml_to_unicode(data, strip_encoding_pats=True)[0] soup = BeautifulSoup(data) @@ -169,15 +183,19 @@ class CHMReader(CHMFile): br[0].extract() # some images seem to be broken in some chm's :/ - for img in soup('img'): - try: - # some are supposedly "relative"... lies. - while img['src'].startswith('../'): img['src'] = img['src'][3:] - # some have ";" at the end. - img['src'] = img['src'].split(';')[0] - except KeyError: - # and some don't even have a src= ?! - pass + base = os.path.dirname(htmlpath) + for img in soup('img', src=True): + src = img['src'] + ipath = os.path.join(base, *src.split('/')) + if os.path.exists(ipath): + continue + src = src.split(';')[0] + if not src: continue + ipath = os.path.join(base, *src.split('/')) + if not os.path.exists(ipath): + while src.startswith('../'): + src = src[3:] + img['src'] = src try: # if there is only a single table with a single element # in the body, replace it by the contents of this single element From 30296b34268d9bd7a37fd48bccd1c9b55a159754 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 17 Oct 2010 09:24:45 -0600 Subject: [PATCH 71/75] MOBI Output: Fix regression that broke conversion of elements in the input document when the element was followed by non-whitespace text. Fixes #7083 (Problem to convert epub->mobi) --- src/calibre/ebooks/oeb/base.py | 4 ++-- src/calibre/ebooks/oeb/transforms/rasterize.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index 2d2945c26a..cf80e4abe2 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -282,9 +282,9 @@ def XPath(expr): def xpath(elem, expr): return elem.xpath(expr, namespaces=XPNSMAP) -def xml2str(root, pretty_print=False, strip_comments=False): +def xml2str(root, pretty_print=False, strip_comments=False, with_tail=True): ans = etree.tostring(root, encoding='utf-8', xml_declaration=True, - pretty_print=pretty_print) + pretty_print=pretty_print, with_tail=with_tail) if strip_comments: ans = re.compile(r'', re.DOTALL).sub('', ans) diff --git a/src/calibre/ebooks/oeb/transforms/rasterize.py b/src/calibre/ebooks/oeb/transforms/rasterize.py index 1026b625bf..b09037498a 100644 --- a/src/calibre/ebooks/oeb/transforms/rasterize.py +++ b/src/calibre/ebooks/oeb/transforms/rasterize.py @@ -55,7 +55,7 @@ class SVGRasterizer(object): self.rasterize_cover() def rasterize_svg(self, elem, width=0, height=0, format='PNG'): - data = QByteArray(xml2str(elem)) + data = QByteArray(xml2str(elem, with_tail=False)) svg = QSvgRenderer(data) size = svg.defaultSize() view_box = elem.get('viewBox', elem.get('viewbox', None)) From 0f29b108c759d0d10023f2cd1f5463fd0f5f482a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 17 Oct 2010 09:42:11 -0600 Subject: [PATCH 72/75] Edit metadata dialog: If metadata is downloaded successfully, set focus to download cover button --- src/calibre/gui2/dialogs/metadata_single.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index 8043016f9f..ef1bddca0c 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -729,10 +729,13 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.series.setText(book.series) if book.series_index is not None: self.series_index.setValue(book.series_index) + # Needed because of Qt focus bug on OS X + self.fetch_cover_button.setFocus(Qt.OtherFocusReason) else: error_dialog(self, _('Cannot fetch metadata'), _('You must specify at least one of ISBN, Title, ' 'Authors or Publisher'), show=True) + self.title.setFocus(Qt.OtherFocusReason) def enable_series_index(self, *args): self.series_index.setEnabled(True) From 23461c99b9e34b47ebb9112b18d5046f1ec6e92f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 17 Oct 2010 10:20:15 -0600 Subject: [PATCH 73/75] ... --- src/calibre/ebooks/snb/output.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/calibre/ebooks/snb/output.py b/src/calibre/ebooks/snb/output.py index 73a726bd26..549ee51446 100644 --- a/src/calibre/ebooks/snb/output.py +++ b/src/calibre/ebooks/snb/output.py @@ -30,8 +30,7 @@ class SNBOutput(OutputFormatPlugin): OptionRecommendation(name='snb_output_encoding', recommended_value='utf-8', level=OptionRecommendation.LOW, help=_('Specify the character encoding of the output document. ' \ - 'The default is utf-8. Note: This option is not honored by all ' \ - 'formats.')), + 'The default is utf-8.')), # OptionRecommendation(name='inline_toc', # recommended_value=False, level=OptionRecommendation.LOW, # help=_('Add Table of Contents to beginning of the book.')), @@ -55,7 +54,7 @@ class SNBOutput(OutputFormatPlugin): rasterizer = SVGRasterizer() rasterizer(oeb_book, opts) except Unavailable: - self.log.warn('SVG rasterizer unavailable, SVG will not be converted') + log.warn('SVG rasterizer unavailable, SVG will not be converted') # Create temp dir with TemporaryDirectory('_snb_output') as tdir: From 00f358b17eff4dfd4bc8d033a73d7f3f59a1153c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 17 Oct 2010 14:16:44 -0600 Subject: [PATCH 74/75] Fix #7118 (Problem with MSWord HTML/CSS comments and Smart Punct combo) --- src/calibre/utils/smartypants.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/utils/smartypants.py b/src/calibre/utils/smartypants.py index 44aac4de8c..62845b8d7a 100644 --- a/src/calibre/utils/smartypants.py +++ b/src/calibre/utils/smartypants.py @@ -376,7 +376,8 @@ default_smartypants_attr = "1" import re -tags_to_skip_regex = re.compile(r"<(/)?(pre|code|kbd|script|math)[^>]*>", re.I) +# style added by Kovid +tags_to_skip_regex = re.compile(r"<(/)?(style|pre|code|kbd|script|math)[^>]*>", re.I) def verify_installation(request): From 258812b7a5380fccc38a9d97c5d8e5981b502a91 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 17 Oct 2010 14:44:24 -0600 Subject: [PATCH 75/75] ... --- setup/installer/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/setup/installer/__init__.py b/setup/installer/__init__.py index f38d175b4c..b976c4d448 100644 --- a/setup/installer/__init__.py +++ b/setup/installer/__init__.py @@ -38,13 +38,19 @@ class Push(Command): description = 'Push code to another host' def run(self, opts): + from threading import Thread + threads = [] for host in ( r'Owner@winxp:/cygdrive/c/Documents\ and\ Settings/Owner/calibre', 'kovid@ox:calibre' ): rcmd = BASE_RSYNC + EXCLUDES + ['.', host] print '\n\nPushing to:', host, '\n' + threads.append(Thread(target=subprocess.check_call, args=(rcmd,))) + threads[-1].start() subprocess.check_call(rcmd) + for thread in threads: + thread.join()