Sync to trunk.

This commit is contained in:
John Schember 2010-12-24 17:18:01 -05:00
commit ca4356f2f1
92 changed files with 67488 additions and 53943 deletions

View File

@ -4,6 +4,101 @@
# for important features/bug fixes. # for important features/bug fixes.
# Also, each release can have new and improved recipes. # Also, each release can have new and improved recipes.
- version: 0.7.35
date: 2010-12-23
new features:
- title: "Add a simple to use Rich text editor for comments to the edit metadata dialog."
description: >
"You can now easily add formatting like bold/italic/lists/headings/colors/etc. to book comments via the
edit metadata dialog"
type: major
- title: "E-book viewer: Add a right click menu item 'Inspect' that allows you to inspect the underlying HTML/CSS source of the currently displayed content"
type: major
- title: "When deleting books from the library if a device is connected and the books are also present on the device ask the user if the books should be deleted from the device, the library, or both."
- title: "Add device drivers for Trekstore eBook Player 7, Sanda Bambook, ALuratek Color, Samsung Galaxy, LG Optimus, Motorola Droid 2 and Sunstech EB700"
tickets: [8021, 7966, 7973, 7956]
- title: "Add an entry to the menu of the calibre library button to select a random book from your calibre library"
tickets: [8010]
- title: "SONY driver: Add a couple of special extra collections for all books by author and all books by title, to workaround the broken sorting on newer SONY models. To enable these collections, go to Preferences->Plugins->Device Interface plugins and customize the SONY plugin."
- title: "Edit metadata dialog: When downloading metadata, make the table of matching books sortable"
tickets: [7951]
- title: "Add a success message after a database integrity check completes successfully"
- title: "Search and replace: When using regular expression mode, add a special input field '{template}' that allows use the templating language to create complex input fields. Also allow setting of series_index by search and replace using the same syntax as in the book list, namely, Series Name [series number]"
- title: "Bulk metadata edit: Add option to automatically set cover from the cover present in the actual ebook files"
tickets: [7947]
- title: "E-book viewer: Show format of current book in the title bar."
tickets: [7974]
- title: "Add a tweak to control how author names are displayed in the Tag Browser and Content Server"
- title: "FB2 Output: Restore sectionizing functionality"
bug fixes:
- title: "When in narrow layout, reserve 40% of available width in the book details panel for series/formats/etc and use the rest for comments"
tickets: [8028]
- title: "PDB Input: Fix failure to block-indent PML \t sections"
tickets: [8019]
- title: "Tag browser: When renaming items dont reset the library view and try not to scroll the Tag Browser itself"
- title: "Conversion pipeline: Fix broken link rewriting for inline CSS embedded in HTML"
- title: "Fix regression in 0.7.34 that broke recipes using extra_css to link to SONY device fonts"
tickets: [7995]
- title: "SONY driver: Don't upload thumbnails as they slow down post disconnect processing on older models"
- title: "Content server: Fix a bug that allowed remote users to read arbitrary png/gif/js/css/html files"
tickets: [7980]
- title: "On X11 initialize fontconfig in the GUI thread as Qt also uses fontconfig internally and fontconfig is not thread safe. Fixes a few random crashes on calibre strartup"
- title: "When using the remove specific format actions, only show available formats in the selected books"
tickets: [7967]
- title: "Linux binary build: If setting system default locale fails, try setting locale to en_US.UTF-8 instead"
- title: "Have the title sort tweak respected everywhere"
- title: "PocketBook 701 driver: Swap the main memory and card drives on windows"
- title: "Fix regression in templating that caused series_index to be shown even when book had no series"
tickets: [7949]
- title: "Content server: Fix regressiont hat broke browsing by rating"
- title: "Content server OPDS feeds: Fix parsing of author names as XML"
tickets: [7938]
improved recipes:
- Business Week Magazine
- Gazet van Antwerpen
- La Nacion
- New England Journal of Medicine
- Journal of Hospital Medicine
new recipes:
- title: "NRC Handelsblad (EPUB version)"
author: "veezh"
- title: "CND and wenxuecity - znjy"
author: "Derek Liang"
- title: "Mish's Global Economic Trend Analysis"
author: "Darko Miletic"
- version: 0.7.34 - version: 0.7.34
date: 2010-12-17 date: 2010-12-17

View File

@ -135,32 +135,53 @@ auto_connect_to_folder = ''
# metadata management is set to automatic. Collections on Sonys are named # metadata management is set to automatic. Collections on Sonys are named
# depending upon whether the field is standard or custom. A collection derived # depending upon whether the field is standard or custom. A collection derived
# from a standard field is named for the value in that field. For example, if # from a standard field is named for the value in that field. For example, if
# the standard 'series' column contains the name 'Darkover', then the series # the standard 'series' column contains the value 'Darkover', then the
# will be named 'Darkover'. A collection derived from a custom field will have # collection name is 'Darkover'. A collection derived from a custom field will
# the name of the field added to the value. For example, if a custom series # have the name of the field added to the value. For example, if a custom series
# column named 'My Series' contains the name 'Darkover', then the collection # column named 'My Series' contains the name 'Darkover', then the collection
# will be named 'Darkover (My Series)'. If two books have fields that generate # will by default be named 'Darkover (My Series)'. For purposes of this
# the same collection name, then both books will be in that collection. This # documentation, 'Darkover' is called the value and 'My Series' is called the
# tweak lets you specify for a standard or custom field the value to be put # category. If two books have fields that generate the same collection name,
# inside the parentheses. You can use it to add a parenthetical description to a # then both books will be in that collection.
# This set of tweaks lets you specify for a standard or custom field how
# the collections are to be named. You can use it to add a description to a
# standard field, for example 'Foo (Tag)' instead of the 'Foo'. You can also use # standard field, for example 'Foo (Tag)' instead of the 'Foo'. You can also use
# it to force multiple fields to end up in the same collection. For example, you # it to force multiple fields to end up in the same collection. For example, you
# could force the values in 'series', '#my_series_1', and '#my_series_2' to # could force the values in 'series', '#my_series_1', and '#my_series_2' to
# appear in collections named 'some_value (Series)', thereby merging all of the # appear in collections named 'some_value (Series)', thereby merging all of the
# fields into one set of collections. The syntax of this tweak is # fields into one set of collections.
# {'field_lookup_name':'name_to_use', 'lookup_name':'name', ...} # There are two related tweaks. The first determines the category name to use
# Example 1: I want three series columns to be merged into one set of # for a metadata field. The second is a template, used to determines how the
# collections. If the column lookup names are 'series', '#series_1' and # value and category are combined to create the collection name.
# '#series_2', and if I want nothing in the parenthesis, then the value to use # The syntax of the first tweak, sony_collection_renaming_rules, is:
# in the tweak value would be: # {'field_lookup_name':'category_name_to_use', 'lookup_name':'name', ...}
# sony_collection_renaming_rules={'series':'', '#series_1':'', '#series_2':''} # The second tweak, sony_collection_name_template, is a template. It uses the
# Example 2: I want the word '(Series)' to appear on collections made from # same template language as plugboards and save templates. This tweak controls
# series, and the word '(Tag)' to appear on collections made from tags. Use: # how the value and category are combined together to make the collection name.
# sony_collection_renaming_rules={'series':'Series', 'tags':'Tag'} # The only two fields available are {category} and {value}. The {value} field is
# Example 3: I want 'series' and '#myseries' to be merged, and for the # never empty. The {category} field can be empty. The default is to put the
# collection name to have '(Series)' appended. The renaming rule is: # value first, then the category enclosed in parentheses, it is isn't empty:
# sony_collection_renaming_rules={'series':'Series', '#myseries':'Series'} # '{value} {category:|(|)}'
# Examples: The first three examples assume that the second tweak
# has not been changed.
# 1: I want three series columns to be merged into one set of collections. The
# column lookup names are 'series', '#series_1' and '#series_2'. I want nothing
# in the parenthesis. The value to use in the tweak value would be:
# sony_collection_renaming_rules={'series':'', '#series_1':'', '#series_2':''}
# 2: I want the word '(Series)' to appear on collections made from series, and
# the word '(Tag)' to appear on collections made from tags. Use:
# sony_collection_renaming_rules={'series':'Series', 'tags':'Tag'}
# 3: I want 'series' and '#myseries' to be merged, and for the collection name
# to have '(Series)' appended. The renaming rule is:
# sony_collection_renaming_rules={'series':'Series', '#myseries':'Series'}
# 4: Same as example 2, but instead of having the category name in parentheses
# and appended to the value, I want it prepended and separated by a colon, such
# as in Series: Darkover. I must change the template used to format the category name
# The resulting two tweaks are:
# sony_collection_renaming_rules={'series':'Series', 'tags':'Tag'}
# sony_collection_name_template='{category:||: }{value}'
sony_collection_renaming_rules={} sony_collection_renaming_rules={}
sony_collection_name_template='{value}{category:| (|)}'
# Specify how sony collections are sorted. This tweak is only applicable if # Specify how sony collections are sorted. This tweak is only applicable if
@ -244,8 +265,10 @@ generate_cover_title_font = None
generate_cover_foot_font = None generate_cover_foot_font = None
# Behavior of doubleclick on the books list. Choices: # Behavior of doubleclick on the books list. Choices: open_viewer, do_nothing,
# open_viewer, do_nothing, edit_cell. Default: open_viewer. # edit_cell, edit_metadata. Selecting edit_metadata has the side effect of
# disabling editing a field using a single click.
# Default: open_viewer.
# Example: doubleclick_on_library_view = 'do_nothing' # Example: doubleclick_on_library_view = 'do_nothing'
doubleclick_on_library_view = 'open_viewer' doubleclick_on_library_view = 'open_viewer'
@ -265,4 +288,4 @@ locale_for_sorting = ''
# Set whether to use one or two columns for custom metadata when editing # Set whether to use one or two columns for custom metadata when editing
# metadata one book at a time. If True, then the fields are laid out using two # metadata one book at a time. If True, then the fields are laid out using two
# columns. If False, one column is used. # columns. If False, one column is used.
metadata_single_use_2_cols_for_custom_fields = True metadata_single_use_2_cols_for_custom_fields = True

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
__appname__ = 'calibre' __appname__ = 'calibre'
__version__ = '0.7.34' __version__ = '0.7.35'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>" __author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re import re

View File

@ -477,9 +477,11 @@ from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS, \
SOVOS, PICO, SUNSTECH_EB700 SOVOS, PICO, SUNSTECH_EB700
from calibre.devices.sne.driver import SNE from calibre.devices.sne.driver import SNE
from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN, \ from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN, \
GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, Q600, LUMIREAD, ALURATEK_COLOR GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, Q600, LUMIREAD, ALURATEK_COLOR, \
TREKSTOR, EEEREADER
from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
from calibre.devices.kobo.driver import KOBO from calibre.devices.kobo.driver import KOBO
from calibre.devices.bambook.driver import BAMBOOK
from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon, \ from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon, \
LibraryThing LibraryThing
@ -601,6 +603,9 @@ plugins += [
PDNOVEL_KOBO, PDNOVEL_KOBO,
LUMIREAD, LUMIREAD,
ALURATEK_COLOR, ALURATEK_COLOR,
BAMBOOK,
TREKSTOR,
EEEREADER,
ITUNES, ITUNES,
] ]
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \ plugins += [x for x in list(locals().values()) if isinstance(x, type) and \

View File

@ -696,8 +696,9 @@ class BambookOutput(OutputProfile):
short_name = 'bambook' short_name = 'bambook'
description = _('This profile is intended for the Sanda Bambook.') description = _('This profile is intended for the Sanda Bambook.')
# Screen size is a best guess # Screen size is for full screen display
screen_size = (600, 800) screen_size = (580, 780)
# Comic size is for normal display
comic_screen_size = (540, 700) comic_screen_size = (540, 700)
dpi = 168.451 dpi = 168.451
fbase = 12 fbase = 12

View File

View File

@ -0,0 +1,477 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL v3'
__copyright__ = '2010, Li Fanxi <lifanxi at freemindworld.com>'
__docformat__ = 'restructuredtext en'
'''
Device driver for Sanda's Bambook
'''
import time, os, hashlib
from itertools import cycle
from calibre.devices.interface import DevicePlugin
from calibre.devices.usbms.deviceconfig import DeviceConfig
from calibre.devices.bambook.libbambookcore import Bambook, text_encoding, CONN_CONNECTED, is_bambook_lib_ready
from calibre.devices.usbms.books import Book, BookList
from calibre.ebooks.metadata.book.json_codec import JsonCodec
from calibre.ptempfile import TemporaryDirectory, TemporaryFile
from calibre.constants import __appname__, __version__
from calibre.devices.errors import OpenFeedback
class BAMBOOK(DeviceConfig, DevicePlugin):
name = 'Bambook Device Interface'
description = _('Communicate with the Sanda Bambook eBook reader.')
author = _('Li Fanxi')
supported_platforms = ['windows', 'linux', 'osx']
log_packets = False
booklist_class = BookList
book_class = Book
FORMATS = [ "snb" ]
VENDOR_ID = 0x230b
PRODUCT_ID = 0x0001
BCD = None
CAN_SET_METADATA = False
THUMBNAIL_HEIGHT = 155
icon = I("devices/bambook.png")
# OPEN_FEEDBACK_MESSAGE = _(
# 'Connecting to Bambook device, please wait ...')
BACKLOADING_ERROR_MESSAGE = _(
'Unable to add book to library directly from Bambook. '
'Please save the book to disk and add the file to library from disk.')
METADATA_CACHE = '.calibre.bambook'
METADATA_FILE_GUID = 'calibremetadata.snb'
bambook = None
def reset(self, key='-1', log_packets=False, report_progress=None,
detected_device=None) :
self.open()
def open(self):
# Make sure the Bambook library is ready
if not is_bambook_lib_ready():
raise OpenFeedback(_("Unable to connect to Bambook, you need to install Bambook library first."))
# Disconnect first if connected
self.eject()
# Connect
self.bambook = Bambook()
self.bambook.Connect()
if self.bambook.GetState() != CONN_CONNECTED:
self.bambook = None
raise Exception(_("Unable to connect to Bambook."))
def eject(self):
if self.bambook:
self.bambook.Disconnect()
self.bambook = None
def post_yank_cleanup(self):
self.eject()
def set_progress_reporter(self, report_progress):
'''
:param report_progress: Function that is called with a % progress
(number between 0 and 100) for various tasks
If it is called with -1 that means that the
task does not have any progress information
'''
self.report_progress = report_progress
def get_device_information(self, end_session=True):
"""
Ask device for device information. See L{DeviceInfoQuery}.
:return: (device name, device version, software version on device, mime type)
"""
if self.bambook:
deviceInfo = self.bambook.GetDeviceInfo()
return (_("Bambook"), "SD928", deviceInfo.firmwareVersion, "MimeType")
def card_prefix(self, end_session=True):
'''
Return a 2 element list of the prefix to paths on the cards.
If no card is present None is set for the card's prefix.
E.G.
('/place', '/place2')
(None, 'place2')
('place', None)
(None, None)
'''
return (None, None)
def total_space(self, end_session=True):
"""
Get total space available on the mountpoints:
1. Main memory
2. Memory Card A
3. Memory Card B
:return: A 3 element list with total space in bytes of (1, 2, 3). If a
particular device doesn't have any of these locations it should return 0.
"""
deviceInfo = self.bambook.GetDeviceInfo()
return (deviceInfo.deviceVolume * 1024, 0, 0)
def free_space(self, end_session=True):
"""
Get free space available on the mountpoints:
1. Main memory
2. Card A
3. Card B
:return: A 3 element list with free space in bytes of (1, 2, 3). If a
particular device doesn't have any of these locations it should return -1.
"""
deviceInfo = self.bambook.GetDeviceInfo()
return (deviceInfo.spareVolume * 1024, -1, -1)
def books(self, oncard=None, end_session=True):
"""
Return a list of ebooks on the device.
:param oncard: If 'carda' or 'cardb' return a list of ebooks on the
specific storage card, otherwise return list of ebooks
in main memory of device. If a card is specified and no
books are on the card return empty list.
:return: A BookList.
"""
# Bambook has no memroy card
if oncard:
return self.booklist_class(None, None, None)
# Get metadata cache
prefix = ''
booklist = self.booklist_class(oncard, prefix, self.settings)
need_sync = self.parse_metadata_cache(booklist)
# Get book list from device
devicebooks = self.bambook.GetBookList()
books = []
for book in devicebooks:
if book.bookGuid == self.METADATA_FILE_GUID:
continue
b = self.book_class('', book.bookGuid)
b.title = book.bookName.decode(text_encoding)
b.authors = [ book.bookAuthor.decode(text_encoding) ]
b.size = 0
b.datatime = time.gmtime()
b.lpath = book.bookGuid
b.thumbnail = None
b.tags = None
b.comments = book.bookAbstract.decode(text_encoding)
books.append(b)
# make a dict cache of paths so the lookup in the loop below is faster.
bl_cache = {}
for idx, b in enumerate(booklist):
bl_cache[b.lpath] = idx
def update_booklist(book, prefix):
changed = False
try:
idx = bl_cache.get(book.lpath, None)
if idx is not None:
bl_cache[book.lpath] = None
if self.update_metadata_item(book, booklist[idx]):
changed = True
else:
if booklist.add_book(book,
replace_metadata=False):
changed = True
except: # Probably a filename encoding error
import traceback
traceback.print_exc()
return changed
# Check each book on device whether it has a correspondig item
# in metadata cache. If not, add it to cache.
for i, book in enumerate(books):
self.report_progress(i/float(len(books)), _('Getting list of books on device...'))
changed = update_booklist(book, prefix)
if changed:
need_sync = True
# Remove books that are no longer in the Bambook. Cache contains
# indices into the booklist if book not in filesystem, None otherwise
# Do the operation in reverse order so indices remain valid
for idx in sorted(bl_cache.itervalues(), reverse=True):
if idx is not None:
need_sync = True
del booklist[idx]
if need_sync:
self.sync_booklists((booklist, None, None))
self.report_progress(1.0, _('Getting list of books on device...'))
return booklist
def upload_books(self, files, names, on_card=None, end_session=True,
metadata=None):
'''
Upload a list of books to the device. If a file already
exists on the device, it should be replaced.
This method should raise a :class:`FreeSpaceError` if there is not enough
free space on the device. The text of the FreeSpaceError must contain the
word "card" if ``on_card`` is not None otherwise it must contain the word "memory".
:param files: A list of paths and/or file-like objects. If they are paths and
the paths point to temporary files, they may have an additional
attribute, original_file_path pointing to the originals. They may have
another optional attribute, deleted_after_upload which if True means
that the file pointed to by original_file_path will be deleted after
being uploaded to the device.
:param names: A list of file names that the books should have
once uploaded to the device. len(names) == len(files)
:param metadata: If not None, it is a list of :class:`Metadata` objects.
The idea is to use the metadata to determine where on the device to
put the book. len(metadata) == len(files). Apart from the regular
cover (path to cover), there may also be a thumbnail attribute, which should
be used in preference. The thumbnail attribute is of the form
(width, height, cover_data as jpeg).
:return: A list of 3-element tuples. The list is meant to be passed
to :meth:`add_books_to_metadata`.
'''
self.report_progress(0, _('Transferring books to device...'))
paths = []
if self.bambook:
for (i, f) in enumerate(files):
self.report_progress((i+1) / float(len(files)), _('Transferring books to device...'))
if not hasattr(f, 'read'):
if self.bambook.VerifySNB(f):
guid = self.bambook.SendFile(f, self.get_guid(metadata[i].uuid))
if guid:
paths.append(guid)
else:
print "Send fail"
else:
print "book invalid"
ret = zip(paths, cycle([on_card]))
self.report_progress(1.0, _('Transferring books to device...'))
return ret
def add_books_to_metadata(self, locations, metadata, booklists):
metadata = iter(metadata)
for i, location in enumerate(locations):
self.report_progress((i+1) / float(len(locations)), _('Adding books to device metadata listing...'))
info = metadata.next()
# Extract the correct prefix from the pathname. To do this correctly,
# we must ensure that both the prefix and the path are normalized
# so that the comparison will work. Book's __init__ will fix up
# lpath, so we don't need to worry about that here.
book = self.book_class('', location[0], other=info)
if book.size is None:
book.size = 0
b = booklists[0].add_book(book, replace_metadata=True)
if b:
b._new_book = True
self.report_progress(1.0, _('Adding books to device metadata listing...'))
def delete_books(self, paths, end_session=True):
'''
Delete books at paths on device.
'''
if self.bambook:
for i, path in enumerate(paths):
self.report_progress((i+1) / float(len(paths)), _('Removing books from device...'))
self.bambook.DeleteFile(path)
self.report_progress(1.0, _('Removing books from device...'))
def remove_books_from_metadata(self, paths, booklists):
'''
Remove books from the metadata list. This function must not communicate
with the device.
:param paths: paths to books on the device.
:param booklists: A tuple containing the result of calls to
(:meth:`books(oncard=None)`,
:meth:`books(oncard='carda')`,
:meth`books(oncard='cardb')`).
'''
for i, path in enumerate(paths):
self.report_progress((i+1) / float(len(paths)), _('Removing books from device metadata listing...'))
for bl in booklists:
for book in bl:
if book.lpath == path:
bl.remove_book(book)
self.report_progress(1.0, _('Removing books from device metadata listing...'))
def sync_booklists(self, booklists, end_session=True):
'''
Update metadata on device.
:param booklists: A tuple containing the result of calls to
(:meth:`books(oncard=None)`,
:meth:`books(oncard='carda')`,
:meth`books(oncard='cardb')`).
'''
if not self.bambook:
return
json_codec = JsonCodec()
# Create stub virtual book for sync info
with TemporaryDirectory() as tdir:
snbcdir = os.path.join(tdir, 'snbc')
snbfdir = os.path.join(tdir, 'snbf')
os.mkdir(snbcdir)
os.mkdir(snbfdir)
f = open(os.path.join(snbfdir, 'book.snbf'), 'wb')
f.write('''<book-snbf version="1.0">
<head>
<name>calibre同步信息</name>
<author>calibre</author>
<language>ZH-CN</language>
<rights/>
<publisher>calibre</publisher>
<generator>''' + __appname__ + ' ' + __version__ + '''</generator>
<created/>
<abstract></abstract>
<cover/>
</head>
</book-snbf>
''')
f.close()
f = open(os.path.join(snbfdir, 'toc.snbf'), 'wb')
f.write('''<toc-snbf>
<head>
<chapters>0</chapters>
</head>
<body>
</body>
</toc-snbf>
''');
f.close()
cache_name = os.path.join(snbcdir, self.METADATA_CACHE)
with open(cache_name, 'wb') as f:
json_codec.encode_to_file(f, booklists[0])
with TemporaryFile('.snb') as f:
if self.bambook.PackageSNB(f, tdir):
if not self.bambook.SendFile(f, self.METADATA_FILE_GUID):
print "Upload failed"
else:
print "Package failed"
# Clear the _new_book indication, as we are supposed to be done with
# adding books at this point
for blist in booklists:
if blist is not None:
for book in blist:
book._new_book = False
self.report_progress(1.0, _('Sending metadata to device...'))
def get_file(self, path, outfile, end_session=True):
'''
Read the file at ``path`` on the device and write it to outfile.
:param outfile: file object like ``sys.stdout`` or the result of an
:func:`open` call.
'''
if self.bambook:
with TemporaryDirectory() as tdir:
if self.bambook.GetFile(path, tdir):
filepath = os.path.join(tdir, path)
f = file(filepath, 'rb')
outfile.write(f.read())
f.close()
else:
print "Unable to get file from Bambook:", path
@classmethod
def config_widget(cls):
'''
Should return a QWidget. The QWidget contains the settings for the device interface
'''
from calibre.gui2.device_drivers.configwidget import ConfigWidget
cw = ConfigWidget(cls.settings(), cls.FORMATS, cls.SUPPORTS_SUB_DIRS,
cls.MUST_READ_METADATA, cls.SUPPORTS_USE_AUTHOR_SORT,
cls.EXTRA_CUSTOMIZATION_MESSAGE)
# Turn off the Save template
cw.opt_save_template.setVisible(False)
cw.label.setVisible(False)
# Repurpose the metadata checkbox
cw.opt_read_metadata.setVisible(False)
# Repurpose the use_subdirs checkbox
cw.opt_use_subdirs.setVisible(False)
return cw
# @classmethod
# def save_settings(cls, settings_widget):
# '''
# Should save settings to disk. Takes the widget created in
# :meth:`config_widget` and saves all settings to disk.
# '''
# raise NotImplementedError()
# @classmethod
# def settings(cls):
# '''
# Should return an opts object. The opts object should have at least one attribute
# `format_map` which is an ordered list of formats for the device.
# '''
# raise NotImplementedError()
def parse_metadata_cache(self, bl):
need_sync = True
if not self.bambook:
return need_sync
# Get the metadata virtual book from Bambook
with TemporaryDirectory() as tdir:
if self.bambook.GetFile(self.METADATA_FILE_GUID, tdir):
cache_name = os.path.join(tdir, self.METADATA_CACHE)
if self.bambook.ExtractSNBContent(os.path.join(tdir, self.METADATA_FILE_GUID),
'snbc/' + self.METADATA_CACHE,
cache_name):
json_codec = JsonCodec()
if os.access(cache_name, os.R_OK):
try:
with open(cache_name, 'rb') as f:
json_codec.decode_from_file(f, bl, self.book_class, '')
need_sync = False
except:
import traceback
traceback.print_exc()
bl = []
return need_sync
@classmethod
def update_metadata_item(cls, book, blb):
# Currently, we do not have enough information
# from Bambook SDK to judge whether a book has
# been changed, we assume all books has been
# changed.
changed = True
# if book.bookName.decode(text_encoding) != blb.title:
# changed = True
# if book.bookAuthor.decode(text_encoding) != blb.authors[0]:
# changed = True
# if book.bookAbstract.decode(text_encoding) != blb.comments:
# changed = True
return changed
@staticmethod
def get_guid(uuid):
guid = hashlib.md5(uuid).hexdigest()[0:15] + ".snb"
return guid

View File

@ -0,0 +1,530 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL v3'
__copyright__ = '2010, Li Fanxi <lifanxi at freemindworld.com>'
__docformat__ = 'restructuredtext en'
'''
Sanda library wrapper
'''
import ctypes, uuid, hashlib, os, sys
from threading import Event, Lock
from calibre.constants import iswindows, islinux, isosx
from calibre import load_library
try:
_lib_name = 'libBambookCore'
cdll = ctypes.cdll
if iswindows:
_lib_name = 'BambookCore'
if hasattr(sys, 'frozen') and iswindows:
lp = os.path.join(os.path.dirname(sys.executable), 'DLLs', 'BambookCore.dll')
lib_handle = cdll.LoadLibrary(lp)
elif hasattr(sys, 'frozen_path'):
lp = os.path.join(sys.frozen_path, 'lib', 'libBambookCore.so')
lib_handle = cdll.LoadLibrary(lp)
else:
lib_handle = load_library(_lib_name, cdll)
except:
lib_handle = None
if iswindows:
text_encoding = 'mbcs'
elif islinux:
text_encoding = 'utf-8'
elif isosx:
text_encoding = 'utf-8'
def is_bambook_lib_ready():
return lib_handle != None
# Constant
DEFAULT_BAMBOOK_IP = '192.168.250.2'
BAMBOOK_SDK_VERSION = 0x00090000
BR_SUCC = 0 # 操作成功
BR_FAIL = 1001 # 操作失败
BR_NOT_IMPL = 1002 # 该功能还未实现
BR_DISCONNECTED = 1003 # 与设备的连接已断开
BR_PARAM_ERROR = 1004 # 调用函数传入的参数错误
BR_TIMEOUT = 1005 # 操作或通讯超时
BR_INVALID_HANDLE = 1006 # 传入的句柄无效
BR_INVALID_FILE = 1007 # 传入的文件不存在或格式无效
BR_INVALID_DIR = 1008 # 传入的目录不存在
BR_BUSY = 1010 # 设备忙,另一个操作还未完成
BR_EOF = 1011 # 文件或操作已结束
BR_IO_ERROR = 1012 # 文件读写失败
BR_FILE_NOT_INSIDE = 1013 # 指定的文件不在包里
# 当前连接状态
CONN_CONNECTED = 0 # 已连接
CONN_DISCONNECTED = 1 # 未连接或连接已断开
CONN_CONNECTING = 2 # 正在连接
CONN_WAIT_FOR_AUTH = 3 # 已连接,正在等待身份验证(暂未实现)
#传输状态
TRANS_STATUS_TRANS = 0 #正在传输
TRANS_STATUS_DONE = 1 #传输完成
TRANS_STATUS_ERR = 2 #传输出错
# Key Enums
BBKeyNum0 = 0
BBKeyNum1 = 1
BBKeyNum2 = 2
BBKeyNum3 = 3
BBKeyNum4 = 4
BBKeyNum5 = 5
BBKeyNum6 = 6
BBKeyNum7 = 7
BBKeyNum8 = 8
BBKeyNum9 = 9
BBKeyStar = 10
BBKeyCross = 11
BBKeyUp = 12
BBKeyDown = 13
BBKeyLeft = 14
BBKeyRight = 15
BBKeyPageUp = 16
BBKeyPageDown = 17
BBKeyOK = 18
BBKeyESC = 19
BBKeyBookshelf = 20
BBKeyStore = 21
BBKeyTTS = 22
BBKeyMenu = 23
BBKeyInteract =24
class DeviceInfo(ctypes.Structure):
_fields_ = [ ("cbSize", ctypes.c_int),
("sn", ctypes.c_char * 20),
("firmwareVersion", ctypes.c_char * 20),
("deviceVolume", ctypes.c_int),
("spareVolume", ctypes.c_int),
]
def __init__(self):
self.cbSize = ctypes.sizeof(self)
class PrivBookInfo(ctypes.Structure):
_fields_ = [ ("cbSize", ctypes.c_int),
("bookGuid", ctypes.c_char * 20),
("bookName", ctypes.c_char * 80),
("bookAuthor", ctypes.c_char * 40),
("bookAbstract", ctypes.c_char * 256),
]
def Clone(self):
bookInfo = PrivBookInfo()
bookInfo.cbSize = self.cbSize
bookInfo.bookGuid = self.bookGuid
bookInfo.bookName = self.bookName
bookInfo.bookAuthor = self.bookAuthor
bookInfo.bookAbstract = self.bookAbstract
return bookInfo
def __init__(self):
self.cbSize = ctypes.sizeof(self)
# extern "C"_declspec(dllexport) BB_RESULT BambookConnect(const char* lpszIP, int timeOut, BB_HANDLE* hConn);
def BambookConnect(ip = DEFAULT_BAMBOOK_IP, timeout = 0):
if isinstance(ip, unicode):
ip = ip.encode('ascii')
handle = ctypes.c_void_p(0)
if lib_handle == None:
raise Exception(_('Bambook SDK has not been installed.'))
ret = lib_handle.BambookConnect(ip, timeout, ctypes.byref(handle))
if ret == BR_SUCC:
return handle
else:
return None
# extern "C" _declspec(dllexport) BB_RESULT BambookGetConnectStatus(BB_HANDLE hConn, int* status);
def BambookGetConnectStatus(handle):
status = ctypes.c_int(0)
ret = lib_handle.BambookGetConnectStatus(handle, ctypes.byref(status))
if ret == BR_SUCC:
return status.value
else:
return None
# extern "C" _declspec(dllexport) BB_RESULT BambookDisconnect(BB_HANDLE hConn);
def BambookDisconnect(handle):
ret = lib_handle.BambookDisconnect(handle)
if ret == BR_SUCC:
return True
else:
return False
# extern "C" const char * BambookGetErrorString(BB_RESULT nCode)
def BambookGetErrorString(code):
func = lib_handle.BambookGetErrorString
func.restype = ctypes.c_char_p
return func(code)
# extern "C" BB_RESULT BambookGetSDKVersion(uint32_t * version);
def BambookGetSDKVersion():
version = ctypes.c_int(0)
lib_handle.BambookGetSDKVersion(ctypes.byref(version))
return version.value
# extern "C" BB_RESULT BambookGetDeviceInfo(BB_HANDLE hConn, DeviceInfo* pInfo);
def BambookGetDeviceInfo(handle):
deviceInfo = DeviceInfo()
ret = lib_handle.BambookGetDeviceInfo(handle, ctypes.byref(deviceInfo))
if ret == BR_SUCC:
return deviceInfo
else:
return None
# extern "C" BB_RESULT BambookKeyPress(BB_HANDLE hConn, BambookKey key);
def BambookKeyPress(handle, key):
ret = lib_handle.BambookKeyPress(handle, key)
if ret == BR_SUCC:
return True
else:
return False
# extern "C" BB_RESULT BambookGetFirstPrivBookInfo(BB_HANDLE hConn, PrivBookInfo * pInfo);
def BambookGetFirstPrivBookInfo(handle, bookInfo):
bookInfo.contents.cbSize = ctypes.sizeof(bookInfo.contents)
ret = lib_handle.BambookGetFirstPrivBookInfo(handle, bookInfo)
if ret == BR_SUCC:
return True
else:
return False
# extern "C" BB_RESULT BambookGetNextPrivBookInfo(BB_HANDLE hConn, PrivBookInfo * pInfo);
def BambookGetNextPrivBookInfo(handle, bookInfo):
bookInfo.contents.cbSize = ctypes.sizeof(bookInfo.contents)
ret = lib_handle.BambookGetNextPrivBookInfo(handle, bookInfo)
if ret == BR_SUCC:
return True
elif ret == BR_EOF:
return False
else:
return False
# extern "C" BB_RESULT BambookDeletePrivBook(BB_HANDLE hConn, const char * lpszBookID);
def BambookDeletePrivBook(handle, guid):
if isinstance(guid, unicode):
guid = guid.encode('ascii')
ret = lib_handle.BambookDeletePrivBook(handle, guid)
if ret == BR_SUCC:
return True
else:
return False
class JobQueue:
jobs = {}
maxID = 0
lock = Lock()
def __init__(self):
self.maxID = 0
def NewJob(self):
self.lock.acquire()
self.maxID = self.maxID + 1
maxid = self.maxID
self.lock.release()
event = Event()
self.jobs[maxid] = (event, TRANS_STATUS_TRANS)
return maxid
def FinishJob(self, jobID, status):
self.jobs[jobID] = (self.jobs[jobID][0], status)
self.jobs[jobID][0].set()
def WaitJob(self, jobID):
self.jobs[jobID][0].wait()
return (self.jobs[jobID][1] == TRANS_STATUS_DONE)
def DeleteJob(self, jobID):
del self.jobs[jobID]
job = JobQueue()
def BambookTransferCallback(status, progress, userData):
if status == TRANS_STATUS_DONE and progress == 100:
job.FinishJob(userData, status)
elif status == TRANS_STATUS_ERR:
job.FinishJob(userData, status)
TransCallback = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_int, ctypes.c_int)
bambookTransferCallback = TransCallback(BambookTransferCallback)
# extern "C" BB_RESULT BambookAddPrivBook(BB_HANDLE hConn, const char * pszSnbFile,
# TransCallback pCallbackFunc, intptr_t userData);
def BambookAddPrivBook(handle, filename, callback, userData):
if isinstance(filename, unicode):
filename = filename.encode('ascii')
ret = lib_handle.BambookAddPrivBook(handle, filename, callback, userData)
if ret == BR_SUCC:
return True
else:
return False
# extern "C" BB_RESULT BambookReplacePrivBook(BB_HANDLE hConn, const char *
# pszSnbFile, const char * lpszBookID, TransCallback pCallbackFunc, intptr_t userData);
def BambookReplacePrivBook(handle, filename, bookID, callback, userData):
if isinstance(filename, unicode):
filename = filename.encode('ascii')
if isinstance(bookID, unicode):
bookID = bookID.encode('ascii')
ret = lib_handle.BambookReplacePrivBook(handle, filename, bookID, callback, userData)
if ret == BR_SUCC:
return True
else:
return False
# extern "C" BB_RESULT BambookFetchPrivBook(BB_HANDLE hConn, const char *
# lpszBookID, const char * lpszFilePath, TransCallback pCallbackFunc, intptr_t userData);
def BambookFetchPrivBook(handle, bookID, filename, callback, userData):
if isinstance(filename, unicode):
filename = filename.encode('ascii')
if isinstance(bookID, unicode):
bookID = bookID.encode('ascii')
ret = lib_handle.BambookFetchPrivBook(handle, bookID, filename, bambookTransferCallback, userData)
if ret == BR_SUCC:
return True
else:
return False
# extern "C" BB_RESULT BambookVerifySnbFile(const char * snbName)
def BambookVerifySnbFile(filename):
if isinstance(filename, unicode):
filename = filename.encode('ascii')
if lib_handle.BambookVerifySnbFile(filename) == BR_SUCC:
return True
else:
return False
# BB_RESULT BambookPackSnbFromDir ( const char * snbName,, const char * rootDir );
def BambookPackSnbFromDir(snbFileName, rootDir):
if isinstance(snbFileName, unicode):
snbFileName = snbFileName.encode('ascii')
if isinstance(rootDir, unicode):
rootDir = rootDir.encode('ascii')
ret = lib_handle.BambookPackSnbFromDir(snbFileName, rootDir)
if ret == BR_SUCC:
return True
else:
return False
# BB_RESULT BambookUnpackFileFromSnb ( const char * snbName,, const char * relativePath, const char * outfname );
def BambookUnpackFileFromSnb(snbFileName, relPath, outFileName):
if isinstance(snbFileName, unicode):
snbFileName = snbFileName.encode('ascii')
if isinstance(relPath, unicode):
relPath = relPath.encode('ascii')
if isinstance(outFileName, unicode):
outFileName = outFileName.encode('ascii')
ret = lib_handle.BambookUnpackFileFromSnb(snbFileName, relPath, outFileName)
if ret == BR_SUCC:
return True
else:
return False
class Bambook:
def __init__(self):
self.handle = None
def Connect(self, ip = DEFAULT_BAMBOOK_IP, timeout = 10000):
self.handle = BambookConnect(ip, timeout)
if self.handle and self.handle != 0:
return True
else:
return False
def Disconnect(self):
if self.handle:
return BambookDisconnect(self.handle)
return False
def GetState(self):
if self.handle:
return BambookGetConnectStatus(self.handle)
return CONN_DISCONNECTED
def GetDeviceInfo(self):
if self.handle:
return BambookGetDeviceInfo(self.handle)
return None
def SendFile(self, fileName, guid = None):
if self.handle:
taskID = job.NewJob()
if guid:
if BambookReplacePrivBook(self.handle, fileName, guid,
bambookTransferCallback, taskID):
if(job.WaitJob(taskID)):
job.DeleteJob(taskID)
return guid
else:
job.DeleteJob(taskID)
return None
else:
job.DeleteJob(taskID)
return None
else:
guid = hashlib.md5(str(uuid.uuid4())).hexdigest()[0:15] + ".snb"
if BambookReplacePrivBook(self.handle, fileName, guid,
bambookTransferCallback, taskID):
if job.WaitJob(taskID):
job.DeleteJob(taskID)
return guid
else:
job.DeleteJob(taskID)
return None
else:
job.DeleteJob(taskID)
return None
return False
def GetFile(self, guid, fileName):
if self.handle:
taskID = job.NewJob()
ret = BambookFetchPrivBook(self.handle, guid, fileName, bambookTransferCallback, taskID)
if ret:
ret = job.WaitJob(taskID)
job.DeleteJob(taskID)
return ret
else:
job.DeleteJob(taskID)
return False
return False
def DeleteFile(self, guid):
if self.handle:
ret = BambookDeletePrivBook(self.handle, guid)
return ret
return False
def GetBookList(self):
if self.handle:
books = []
bookInfo = PrivBookInfo()
bi = ctypes.pointer(bookInfo)
ret = BambookGetFirstPrivBookInfo(self.handle, bi)
while ret:
books.append(bi.contents.Clone())
ret = BambookGetNextPrivBookInfo(self.handle, bi)
return books
@staticmethod
def GetSDKVersion():
return BambookGetSDKVersion()
@staticmethod
def VerifySNB(fileName):
return BambookVerifySnbFile(fileName);
@staticmethod
def ExtractSNBContent(fileName, relPath, path):
return BambookUnpackFileFromSnb(fileName, relPath, path)
@staticmethod
def ExtractSNB(fileName, path):
ret = BambookUnpackFileFromSnb(fileName, 'snbf/book.snbf', path + '/snbf/book.snbf')
if not ret:
return False
ret = BambookUnpackFileFromSnb(fileName, 'snbf/toc.snbf', path + '/snbf/toc.snbf')
if not ret:
return False
return True
@staticmethod
def PackageSNB(fileName, path):
return BambookPackSnbFromDir(fileName, path)
def passed():
print "> Pass"
def failed():
print "> Failed"
if __name__ == "__main__":
print "Bambook SDK Unit Test"
bb = Bambook()
print "Disconnect State"
if bb.GetState() == CONN_DISCONNECTED:
passed()
else:
failed()
print "Get SDK Version"
if bb.GetSDKVersion() == BAMBOOK_SDK_VERSION:
passed()
else:
failed()
print "Verify good SNB File"
if bb.VerifySNB(u'/tmp/f8268e6c1f4e78c.snb'):
passed()
else:
failed()
print "Verify bad SNB File"
if not bb.VerifySNB('./libwrapper.py'):
passed()
else:
failed()
print "Extract SNB File"
if bb.ExtractSNB('./test.snb', '/tmp/test'):
passed()
else:
failed()
print "Packet SNB File"
if bb.PackageSNB('/tmp/tmp.snb', '/tmp/test') and bb.VerifySNB('/tmp/tmp.snb'):
passed()
else:
failed()
print "Connect to Bambook"
if bb.Connect('192.168.250.2', 10000) and bb.GetState() == CONN_CONNECTED:
passed()
else:
failed()
print "Get Bambook Info"
devInfo = bb.GetDeviceInfo()
if devInfo:
# print "Info Size: ", devInfo.cbSize
# print "SN: ", devInfo.sn
# print "Firmware: ", devInfo.firmwareVersion
# print "Capacity: ", devInfo.deviceVolume
# print "Free: ", devInfo.spareVolume
if devInfo.cbSize == 52 and devInfo.deviceVolume == 1714232:
passed()
else:
failed()
print "Send file"
if bb.SendFile('/tmp/tmp.snb'):
passed()
else:
failed()
print "Get book list"
books = bb.GetBookList()
if len(books) > 10:
passed()
else:
failed()
print "Get book"
if bb.GetFile('f8268e6c1f4e78c.snb', '/tmp') and bb.VerifySNB('/tmp/f8268e6c1f4e78c.snb'):
passed()
else:
failed()
print "Disconnect"
if bb.Disconnect():
passed()
else:
failed()

View File

@ -224,3 +224,43 @@ class ALURATEK_COLOR(USBMS):
VENDOR_NAME = 'USB_2.0' VENDOR_NAME = 'USB_2.0'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'USB_FLASH_DRIVER' WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'USB_FLASH_DRIVER'
class TREKSTOR(USBMS):
name = 'Trekstor E-book player device interface'
gui_name = 'Trekstor'
description = _('Communicate with the Trekstor')
author = 'Kovid Goyal'
supported_platforms = ['windows', 'osx', 'linux']
# Ordered list of supported formats
FORMATS = ['epub', 'txt', 'pdf']
VENDOR_ID = [0x1e68]
PRODUCT_ID = [0x0041]
BCD = [0x0002]
EBOOK_DIR_MAIN = 'Ebooks'
VENDOR_NAME = 'TREKSTOR'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'EBOOK_PLAYER_7'
class EEEREADER(USBMS):
name = 'Asus EEE Reader device interface'
gui_name = 'EEE Reader'
description = _('Communicate with the EEE Reader')
author = 'Kovid Goyal'
supported_platforms = ['windows', 'osx', 'linux']
# Ordered list of supported formats
FORMATS = ['epub', 'fb2', 'txt', 'pdf']
VENDOR_ID = [0x0b05]
PRODUCT_ID = [0x178f]
BCD = [0x0319]
EBOOK_DIR_MAIN = 'Books'
VENDOR_NAME = 'LINUX'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'FILE-STOR_GADGET'

View File

@ -14,6 +14,22 @@ from calibre.constants import preferred_encoding
from calibre import isbytestring, force_unicode from calibre import isbytestring, force_unicode
from calibre.utils.config import prefs, tweaks from calibre.utils.config import prefs, tweaks
from calibre.utils.icu import strcmp from calibre.utils.icu import strcmp
from calibre.utils.formatter import TemplateFormatter
class SafeFormat(TemplateFormatter):
'''
Provides a format function that substitutes '' for any missing value
'''
def get_value(self, key, args, kwargs):
try:
if key in kwargs:
return kwargs[key]
return key
except:
return key
safe_formatter = SafeFormat()
class Book(Metadata): class Book(Metadata):
def __init__(self, prefix, lpath, size=None, other=None): def __init__(self, prefix, lpath, size=None, other=None):
@ -107,23 +123,25 @@ class CollectionsBookList(BookList):
return sortattr return sortattr
return None return None
def compute_category_name(self, attr, category, field_meta): def compute_category_name(self, field_key, field_value, field_meta):
renames = tweaks['sony_collection_renaming_rules'] renames = tweaks['sony_collection_renaming_rules']
attr_name = renames.get(attr, None) field_name = renames.get(field_key, None)
if attr_name is None: if field_name is None:
if field_meta['is_custom']: if field_meta['is_custom']:
attr_name = '(%s)'%field_meta['name'] field_name = field_meta['name']
else: else:
attr_name = '' field_name = ''
elif attr_name != '': cat_name = safe_formatter.safe_format(
attr_name = '(%s)'%attr_name fmt=tweaks['sony_collection_name_template'],
cat_name = '%s %s'%(category, attr_name) kwargs={'category':field_name, 'value':field_value},
error_value='', book=None)
return cat_name.strip() return cat_name.strip()
def get_collections(self, collection_attributes): def get_collections(self, collection_attributes):
from calibre.devices.usbms.driver import debug_print from calibre.devices.usbms.driver import debug_print
debug_print('Starting get_collections:', prefs['manage_device_metadata']) debug_print('Starting get_collections:', prefs['manage_device_metadata'])
debug_print('Renaming rules:', tweaks['sony_collection_renaming_rules']) debug_print('Renaming rules:', tweaks['sony_collection_renaming_rules'])
debug_print('Formatting template:', tweaks['sony_collection_name_template'])
debug_print('Sorting rules:', tweaks['sony_collection_sorting_rules']) debug_print('Sorting rules:', tweaks['sony_collection_sorting_rules'])
# Complexity: we can use renaming rules only when using automatic # Complexity: we can use renaming rules only when using automatic
@ -139,9 +157,9 @@ class CollectionsBookList(BookList):
ca = [] ca = []
for c in collection_attributes: for c in collection_attributes:
if c.startswith('aba:') and c[4:]: if c.startswith('aba:') and c[4:]:
all_by_author = c[4:] all_by_author = c[4:].strip()
elif c.startswith('abt:') and c[4:]: elif c.startswith('abt:') and c[4:]:
all_by_title = c[4:] all_by_title = c[4:].strip()
else: else:
ca.append(c.lower()) ca.append(c.lower())
collection_attributes = ca collection_attributes = ca

View File

@ -468,8 +468,9 @@ class MobiMLizer(object):
vtag.append(child) vtag.append(child)
else: else:
break break
for child in vbstate.para: if vbstate.para is not None:
vtag.append(child) for child in vbstate.para:
vtag.append(child)
return return
if text or tag in CONTENT_TAGS or tag in NESTABLE_TAGS: if text or tag in CONTENT_TAGS or tag in NESTABLE_TAGS:

View File

@ -0,0 +1,56 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
class RemoveFakeMargins(object):
'''
Try to detect and remove fake margins inserted by asinine ebook creation
software on each paragraph/wrapper div. Can be used only after CSS
flattening.
'''
def __call__(self, oeb, opts, log):
self.oeb, self.opts, self.log = oeb, opts, log
from calibre.ebooks.oeb.base import XPath, OEB_STYLES
stylesheet = None
for item in self.oeb.manifest:
if item.media_type.lower() in OEB_STYLES:
stylesheet = item.data
break
if stylesheet is None:
return
top_level_elements = {}
second_level_elements = {}
for x in self.oeb.spine:
root = x.data
body = XPath('//h:body')(root)
if body:
body = body[0]
if not hasattr(body, 'xpath'):
continue
# Check for margins on top level elements
for lb in XPath('./h:div|./h:p|./*/h:div|./*/h:p')(body):
cls = lb.get('class', '')
level = top_level_elements if lb.getparent() is body else \
second_level_elements
if cls not in level:
level[cls] = []
top_level_elements[cls] = []
level[cls].append(lb)
def get_margins(self, stylesheet, cls):
pass

View File

@ -62,7 +62,7 @@ class SNBInput(InputFormatPlugin):
oeb.uid = oeb.metadata.identifier[0] oeb.uid = oeb.metadata.identifier[0]
break break
with TemporaryDirectory('_chm2oeb', keep=True) as tdir: with TemporaryDirectory('_snb2oeb', keep=True) as tdir:
log.debug('Process TOC ...') log.debug('Process TOC ...')
toc = snbFile.GetFileStream('snbf/toc.snbf') toc = snbFile.GetFileStream('snbf/toc.snbf')
oeb.container = DirContainer(tdir, log) oeb.container = DirContainer(tdir, log)
@ -74,17 +74,18 @@ class SNBInput(InputFormatPlugin):
chapterSrc = ch.get('src') chapterSrc = ch.get('src')
fname = 'ch_%d.htm' % i fname = 'ch_%d.htm' % i
data = snbFile.GetFileStream('snbc/' + chapterSrc) data = snbFile.GetFileStream('snbc/' + chapterSrc)
if data != None: if data == None:
snbc = etree.fromstring(data) continue
outputFile = open(os.path.join(tdir, fname), 'wb') snbc = etree.fromstring(data)
lines = [] outputFile = open(os.path.join(tdir, fname), 'wb')
for line in snbc.find('.//body'): lines = []
if line.tag == 'text': for line in snbc.find('.//body'):
lines.append(u'<p>%s</p>' % html_encode(line.text)) if line.tag == 'text':
elif line.tag == 'img': lines.append(u'<p>%s</p>' % html_encode(line.text))
lines.append(u'<p><img src="%s" /></p>' % html_encode(line.text)) elif line.tag == 'img':
outputFile.write((HTML_TEMPLATE % (chapterName, u'\n'.join(lines))).encode('utf-8', 'replace')) lines.append(u'<p><img src="%s" /></p>' % html_encode(line.text))
outputFile.close() outputFile.write((HTML_TEMPLATE % (chapterName, u'\n'.join(lines))).encode('utf-8', 'replace'))
outputFile.close()
oeb.toc.add(ch.text, fname) oeb.toc.add(ch.text, fname)
id, href = oeb.manifest.generate(id='html', id, href = oeb.manifest.generate(id='html',
href=ascii_filename(fname)) href=ascii_filename(fname))

View File

@ -35,14 +35,17 @@ class SNBOutput(OutputFormatPlugin):
recommended_value=False, level=OptionRecommendation.LOW, recommended_value=False, level=OptionRecommendation.LOW,
help=_('Specify whether or not to insert an empty line between ' help=_('Specify whether or not to insert an empty line between '
'two paragraphs.')), 'two paragraphs.')),
OptionRecommendation(name='snb_indent_first_line', OptionRecommendation(name='snb_dont_indent_first_line',
recommended_value=True, level=OptionRecommendation.LOW, recommended_value=False, level=OptionRecommendation.LOW,
help=_('Specify whether or not to insert two space characters ' help=_('Specify whether or not to insert two space characters '
'to indent the first line of each paragraph.')), 'to indent the first line of each paragraph.')),
OptionRecommendation(name='snb_hide_chapter_name', OptionRecommendation(name='snb_hide_chapter_name',
recommended_value=False, level=OptionRecommendation.LOW, recommended_value=False, level=OptionRecommendation.LOW,
help=_('Specify whether or not to hide the chapter title for each ' help=_('Specify whether or not to hide the chapter title for each '
'chapter. Useful for image-only output (eg. comics).')), 'chapter. Useful for image-only output (eg. comics).')),
OptionRecommendation(name='snb_full_screen',
recommended_value=False, level=OptionRecommendation.LOW,
help=_('Resize all the images for full screen view. ')),
]) ])
def convert(self, oeb_book, output_path, input_plugin, opts, log): def convert(self, oeb_book, output_path, input_plugin, opts, log):
@ -228,7 +231,10 @@ class SNBOutput(OutputFormatPlugin):
img.load(imageData) img.load(imageData)
(x,y) = img.size (x,y) = img.size
if self.opts: if self.opts:
SCREEN_X, SCREEN_Y = self.opts.output_profile.comic_screen_size if self.opts.snb_full_screen:
SCREEN_X, SCREEN_Y = self.opts.output_profile.screen_size
else:
SCREEN_X, SCREEN_Y = self.opts.output_profile.comic_screen_size
else: else:
SCREEN_X = 540 SCREEN_X = 540
SCREEN_Y = 700 SCREEN_Y = 700

View File

@ -121,7 +121,7 @@ class SNBMLizer(object):
subitem = line[len(CALIBRE_SNB_BM_TAG):] subitem = line[len(CALIBRE_SNB_BM_TAG):]
bodyTree = trees[subitem].find(".//body") bodyTree = trees[subitem].find(".//body")
else: else:
if self.opts and self.opts.snb_indent_first_line: if self.opts and not self.opts.snb_dont_indent_first_line:
prefix = u'\u3000\u3000' prefix = u'\u3000\u3000'
else: else:
prefix = u'' prefix = u''

View File

@ -83,7 +83,7 @@ def _config():
c.add_opt('LRF_ebook_viewer_options', default=None, c.add_opt('LRF_ebook_viewer_options', default=None,
help=_('Options for the LRF ebook viewer')) help=_('Options for the LRF ebook viewer'))
c.add_opt('internally_viewed_formats', default=['LRF', 'EPUB', 'LIT', c.add_opt('internally_viewed_formats', default=['LRF', 'EPUB', 'LIT',
'MOBI', 'PRC', 'HTML', 'FB2', 'PDB', 'RB'], 'MOBI', 'PRC', 'HTML', 'FB2', 'PDB', 'RB', 'SNB'],
help=_('Formats that are viewed using the internal viewer')) help=_('Formats that are viewed using the internal viewer'))
c.add_opt('column_map', default=ALL_COLUMNS, c.add_opt('column_map', default=ALL_COLUMNS,
help=_('Columns to be displayed in the book list')) help=_('Columns to be displayed in the book list'))

View File

@ -249,7 +249,7 @@ class BookInfo(QWebView):
left_pane = u'<table>%s</table>'%rows left_pane = u'<table>%s</table>'%rows
right_pane = u'<div>%s</div>'%comments right_pane = u'<div>%s</div>'%comments
self.setHtml(templ%(u'<table><tr><td valign="top" ' self.setHtml(templ%(u'<table><tr><td valign="top" '
'style="padding-right:2em">%s</td><td valign="top">%s</td></tr></table>' 'style="padding-right:2em; width:40%%">%s</td><td valign="top">%s</td></tr></table>'
% (left_pane, right_pane))) % (left_pane, right_pane)))
def mouseDoubleClickEvent(self, ev): def mouseDoubleClickEvent(self, ev):

View File

@ -62,6 +62,8 @@ class EditorWidget(QWebView): # {{{
def __init__(self, parent=None): def __init__(self, parent=None):
QWebView.__init__(self, parent) QWebView.__init__(self, parent)
self.comments_pat = re.compile(r'<!--.*?-->', re.DOTALL)
for wac, name, icon, text, checkable in [ for wac, name, icon, text, checkable in [
('ToggleBold', 'bold', 'format-text-bold', _('Bold'), True), ('ToggleBold', 'bold', 'format-text-bold', _('Bold'), True),
('ToggleItalic', 'italic', 'format-text-italic', _('Italic'), ('ToggleItalic', 'italic', 'format-text-italic', _('Italic'),
@ -137,10 +139,19 @@ class EditorWidget(QWebView): # {{{
self.action_insert_link = QAction(QIcon(I('insert-link.png')), self.action_insert_link = QAction(QIcon(I('insert-link.png')),
_('Insert link'), self) _('Insert link'), self)
self.action_insert_link.triggered.connect(self.insert_link) self.action_insert_link.triggered.connect(self.insert_link)
self.action_clear = QAction(QIcon(I('edit-clear')), _('Clear'), self)
self.action_clear.triggered.connect(self.clear_text)
self.page().setLinkDelegationPolicy(QWebPage.DelegateAllLinks) self.page().setLinkDelegationPolicy(QWebPage.DelegateAllLinks)
self.page().linkClicked.connect(self.link_clicked) self.page().linkClicked.connect(self.link_clicked)
self.setHtml('')
self.page().setContentEditable(True)
def clear_text(self, *args):
self.action_select_all.trigger()
self.action_cut.trigger()
def link_clicked(self, url): def link_clicked(self, url):
open_url(url) open_url(url)
@ -210,6 +221,7 @@ class EditorWidget(QWebView): # {{{
raw = unicode(self.page().mainFrame().toHtml()) raw = unicode(self.page().mainFrame().toHtml())
raw = xml_to_unicode(raw, strip_encoding_pats=True, raw = xml_to_unicode(raw, strip_encoding_pats=True,
resolve_entities=True)[0] resolve_entities=True)[0]
raw = self.comments_pat.sub('', raw)
try: try:
root = html.fromstring(raw) root = html.fromstring(raw)
@ -218,12 +230,17 @@ class EditorWidget(QWebView): # {{{
elems = [] elems = []
for body in root.xpath('//body'): for body in root.xpath('//body'):
if body.text:
elems.append(body.text)
elems += [html.tostring(x, encoding=unicode) for x in body if elems += [html.tostring(x, encoding=unicode) for x in body if
x.tag != 'script'] x.tag not in ('script', 'style')]
if len(elems) > 1: if len(elems) > 1:
ans = u'<div>%s</div>'%(u''.join(elems)) ans = u'<div>%s</div>'%(u''.join(elems))
else: else:
ans = u''.join(elems) ans = u''.join(elems)
if not ans.startswith('<'):
ans = '<p>%s</p>'%ans
ans = xml_replace_entities(ans) ans = xml_replace_entities(ans)
except: except:
import traceback import traceback
@ -462,6 +479,7 @@ class Editor(QWidget): # {{{
QWidget.__init__(self, parent) QWidget.__init__(self, parent)
self.toolbar1 = QToolBar(self) self.toolbar1 = QToolBar(self)
self.toolbar2 = QToolBar(self) self.toolbar2 = QToolBar(self)
self.toolbar3 = QToolBar(self)
self.editor = EditorWidget(self) self.editor = EditorWidget(self)
self.tabs = QTabWidget(self) self.tabs = QTabWidget(self)
self.tabs.setTabPosition(self.tabs.South) self.tabs.setTabPosition(self.tabs.South)
@ -476,6 +494,7 @@ class Editor(QWidget): # {{{
l.setContentsMargins(0, 0, 0, 0) l.setContentsMargins(0, 0, 0, 0)
l.addWidget(self.toolbar1) l.addWidget(self.toolbar1)
l.addWidget(self.toolbar2) l.addWidget(self.toolbar2)
l.addWidget(self.toolbar3)
l.addWidget(self.editor) l.addWidget(self.editor)
self._layout.addWidget(self.tabs) self._layout.addWidget(self.tabs)
self.tabs.addTab(self.wyswyg, _('Normal view')) self.tabs.addTab(self.wyswyg, _('Normal view'))
@ -483,43 +502,50 @@ class Editor(QWidget): # {{{
self.tabs.currentChanged[int].connect(self.change_tab) self.tabs.currentChanged[int].connect(self.change_tab)
self.highlighter = Highlighter(self.code_edit.document()) self.highlighter = Highlighter(self.code_edit.document())
for x in ('bold', 'italic', 'underline', 'strikethrough', # toolbar1 {{{
'superscript', 'subscript', 'indent', 'outdent'):
ac = getattr(self.editor, 'action_'+x)
if x in ('superscript', 'indent'):
self.toolbar2.addSeparator()
self.toolbar2.addAction(ac)
self.toolbar2.addSeparator()
for x in ('left', 'center', 'right', 'justified'):
ac = getattr(self.editor, 'action_align_'+x)
self.toolbar2.addAction(ac)
self.toolbar2.addSeparator()
self.toolbar1.addAction(self.editor.action_undo) self.toolbar1.addAction(self.editor.action_undo)
self.toolbar1.addAction(self.editor.action_redo) self.toolbar1.addAction(self.editor.action_redo)
self.toolbar1.addAction(self.editor.action_select_all) self.toolbar1.addAction(self.editor.action_select_all)
self.toolbar1.addAction(self.editor.action_remove_format) self.toolbar1.addAction(self.editor.action_remove_format)
self.toolbar1.addAction(self.editor.action_clear)
self.toolbar1.addSeparator() self.toolbar1.addSeparator()
for x in ('copy', 'cut', 'paste'): for x in ('copy', 'cut', 'paste'):
ac = getattr(self.editor, 'action_'+x) ac = getattr(self.editor, 'action_'+x)
self.toolbar1.addAction(ac) self.toolbar1.addAction(ac)
self.toolbar1.addSeparator()
self.toolbar1.addSeparator()
self.toolbar1.addAction(self.editor.action_background)
# }}}
# toolbar2 {{{
for x in ('', 'un'): for x in ('', 'un'):
ac = getattr(self.editor, 'action_%sordered_list'%x) ac = getattr(self.editor, 'action_%sordered_list'%x)
self.toolbar1.addAction(ac) self.toolbar2.addAction(ac)
self.toolbar1.addSeparator() self.toolbar2.addSeparator()
for x in ('superscript', 'subscript', 'indent', 'outdent'):
self.toolbar2.addAction(getattr(self.editor, 'action_' + x))
if x in ('subscript', 'outdent'):
self.toolbar2.addSeparator()
self.toolbar1.addAction(self.editor.action_color) self.toolbar2.addAction(self.editor.action_block_style)
self.toolbar1.addAction(self.editor.action_background) w = self.toolbar2.widgetForAction(self.editor.action_block_style)
self.toolbar1.addSeparator()
self.toolbar1.addAction(self.editor.action_block_style)
w = self.toolbar1.widgetForAction(self.editor.action_block_style)
w.setPopupMode(w.InstantPopup) w.setPopupMode(w.InstantPopup)
self.toolbar1.addAction(self.editor.action_insert_link) self.toolbar2.addAction(self.editor.action_insert_link)
# }}}
# toolbar3 {{{
for x in ('bold', 'italic', 'underline', 'strikethrough'):
ac = getattr(self.editor, 'action_'+x)
self.toolbar3.addAction(ac)
self.toolbar3.addSeparator()
for x in ('left', 'center', 'right', 'justified'):
ac = getattr(self.editor, 'action_align_'+x)
self.toolbar3.addAction(ac)
self.toolbar3.addSeparator()
self.toolbar3.addAction(self.editor.action_color)
# }}}
self.code_edit.textChanged.connect(self.code_dirtied) self.code_edit.textChanged.connect(self.code_dirtied)
self.editor.page().contentsChanged.connect(self.wyswyg_dirtied) self.editor.page().contentsChanged.connect(self.wyswyg_dirtied)

View File

@ -18,8 +18,8 @@ class PluginWidget(Widget, Ui_Form):
def __init__(self, parent, get_option, get_help, db=None, book_id=None): def __init__(self, parent, get_option, get_help, db=None, book_id=None):
Widget.__init__(self, parent, Widget.__init__(self, parent,
['snb_insert_empty_line', 'snb_indent_first_line', ['snb_insert_empty_line', 'snb_dont_indent_first_line',
'snb_hide_chapter_name',]) 'snb_hide_chapter_name','snb_full_screen'])
self.db, self.book_id = db, book_id self.db, self.book_id = db, book_id
self.initialize_options(get_option, get_help, db, book_id) self.initialize_options(get_option, get_help, db, book_id)

View File

@ -13,8 +13,8 @@
<property name="windowTitle"> <property name="windowTitle">
<string>Form</string> <string>Form</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout" rowstretch="0,0,0,0,0"> <layout class="QGridLayout" name="gridLayout" rowstretch="0,0,0,0,0,0" rowminimumheight="0,0,0,0,0,0">
<item row="4" column="0"> <item row="5" column="0">
<spacer name="verticalSpacer"> <spacer name="verticalSpacer">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Vertical</enum>
@ -35,9 +35,9 @@
</widget> </widget>
</item> </item>
<item row="2" column="0"> <item row="2" column="0">
<widget class="QCheckBox" name="opt_snb_indent_first_line"> <widget class="QCheckBox" name="opt_snb_dont_indent_first_line">
<property name="text"> <property name="text">
<string>Insert space before the first line for each paragraph</string> <string>Don't indent the first line for each paragraph</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -48,6 +48,13 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="0">
<widget class="QCheckBox" name="opt_snb_full_screen">
<property name="text">
<string>Optimize for full-sceen view </string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<resources/> <resources/>

View File

@ -414,6 +414,9 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
self.s_r_template.completer().setCaseSensitivity(Qt.CaseSensitive) self.s_r_template.completer().setCaseSensitivity(Qt.CaseSensitive)
self.s_r_search_mode_changed(self.search_mode.currentIndex()) self.s_r_search_mode_changed(self.search_mode.currentIndex())
self.multiple_separator.setFixedWidth(30)
self.multiple_separator.setText(' ::: ')
self.multiple_separator.textChanged.connect(self.s_r_separator_changed)
def s_r_get_field(self, mi, field): def s_r_get_field(self, mi, field):
if field: if field:
@ -451,19 +454,22 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
mi = self.db.get_metadata(self.ids[i], index_is_id=True) mi = self.db.get_metadata(self.ids[i], index_is_id=True)
src = unicode(self.search_field.currentText()) src = unicode(self.search_field.currentText())
t = self.s_r_get_field(mi, src) t = self.s_r_get_field(mi, src)
w.setText(''.join(t[0:1])) w.setText(unicode(self.multiple_separator.text()).join(t))
if self.search_mode.currentIndex() == 0: if self.search_mode.currentIndex() == 0:
self.destination_field.setCurrentIndex(idx) self.destination_field.setCurrentIndex(idx)
else: else:
self.s_r_destination_field_changed(self.destination_field.currentText())
self.s_r_paint_results(None) self.s_r_paint_results(None)
def s_r_destination_field_changed(self, txt): def s_r_destination_field_changed(self, txt):
txt = unicode(txt) txt = unicode(txt)
if not txt:
txt = unicode(self.search_field.currentText())
self.comma_separated.setEnabled(True) self.comma_separated.setEnabled(True)
if txt: if txt and txt in self.writable_fields:
fm = self.db.metadata_for_field(txt) self.destination_field_fm = self.db.metadata_for_field(txt)
if fm['is_multiple']: if self.destination_field_fm['is_multiple']:
self.comma_separated.setEnabled(False) self.comma_separated.setEnabled(False)
self.comma_separated.setChecked(True) self.comma_separated.setChecked(True)
self.s_r_paint_results(None) self.s_r_paint_results(None)
@ -493,6 +499,9 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
self.s_r_heading.setText('<p>'+self.main_heading + self.regexp_heading) self.s_r_heading.setText('<p>'+self.main_heading + self.regexp_heading)
self.s_r_paint_results(None) self.s_r_paint_results(None)
def s_r_separator_changed(self, txt):
self.s_r_search_field_changed(self.search_field.currentIndex())
def s_r_set_colors(self): def s_r_set_colors(self):
if self.s_r_error is not None: if self.s_r_error is not None:
col = 'rgb(255, 0, 0, 20%)' col = 'rgb(255, 0, 0, 20%)'
@ -592,8 +601,12 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
wr = getattr(self, 'book_%d_result'%(i+1)) wr = getattr(self, 'book_%d_result'%(i+1))
try: try:
result = self.s_r_do_regexp(mi) result = self.s_r_do_regexp(mi)
t = self.s_r_do_destination(mi, result[0:1]) t = self.s_r_do_destination(mi, result)
t = self.s_r_replace_mode_separator().join(t) if len(result) > 1 and self.destination_field_fm is not None and \
self.destination_field_fm['is_multiple']:
t = unicode(self.multiple_separator.text()).join(t)
else:
t = self.s_r_replace_mode_separator().join(t)
wr.setText(t) wr.setText(t)
except Exception as e: except Exception as e:
self.s_r_error = e self.s_r_error = e

View File

@ -478,7 +478,7 @@ Future conversion of these books will use the default settings.</string>
<item> <item>
<widget class="QLabel" name="xlabel_24"> <widget class="QLabel" name="xlabel_24">
<property name="text"> <property name="text">
<string>Search mode:</string> <string>Search &amp;mode:</string>
</property> </property>
<property name="buddy"> <property name="buddy">
<cstring>search_mode</cstring> <cstring>search_mode</cstring>
@ -559,7 +559,7 @@ Future conversion of these books will use the default settings.</string>
<string>Check this box if the search string must match exactly upper and lower case. Uncheck it if case is to be ignored</string> <string>Check this box if the search string must match exactly upper and lower case. Uncheck it if case is to be ignored</string>
</property> </property>
<property name="text"> <property name="text">
<string>Case sensitive</string> <string>Cas&amp;e sensitive</string>
</property> </property>
<property name="checked"> <property name="checked">
<bool>true</bool> <bool>true</bool>
@ -588,7 +588,7 @@ Future conversion of these books will use the default settings.</string>
<item> <item>
<widget class="QLabel" name="label_41"> <widget class="QLabel" name="label_41">
<property name="text"> <property name="text">
<string>Apply function after replace:</string> <string>&amp;Apply function after replace:</string>
</property> </property>
<property name="buddy"> <property name="buddy">
<cstring>replace_func</cstring> <cstring>replace_func</cstring>
@ -641,7 +641,7 @@ If blank, the source field is used if the field is modifiable</string>
<item> <item>
<widget class="QLabel" name="replace_mode_label"> <widget class="QLabel" name="replace_mode_label">
<property name="text"> <property name="text">
<string>Mode:</string> <string>M&amp;ode:</string>
</property> </property>
<property name="buddy"> <property name="buddy">
<cstring>replace_mode</cstring> <cstring>replace_mode</cstring>
@ -658,11 +658,11 @@ If blank, the source field is used if the field is modifiable</string>
<item> <item>
<widget class="QCheckBox" name="comma_separated"> <widget class="QCheckBox" name="comma_separated">
<property name="toolTip"> <property name="toolTip">
<string>If the replace mode is prepend or append, then this box indicates whether a comma or <string>Specifies whether a comma should be put between values when copying from a
nothing should be put between the original text and the inserted text</string> multiple-valued field to a single-valued field</string>
</property> </property>
<property name="text"> <property name="text">
<string>use comma</string> <string>&amp;Use comma</string>
</property> </property>
<property name="checked"> <property name="checked">
<bool>true</bool> <bool>true</bool>
@ -687,7 +687,7 @@ nothing should be put between the original text and the inserted text</string>
<item row="8" column="1"> <item row="8" column="1">
<widget class="QLabel" name="xlabel_3"> <widget class="QLabel" name="xlabel_3">
<property name="text"> <property name="text">
<string>Test &amp;text</string> <string>Test text</string>
</property> </property>
<property name="buddy"> <property name="buddy">
<cstring>test_text</cstring> <cstring>test_text</cstring>
@ -695,14 +695,48 @@ nothing should be put between the original text and the inserted text</string>
</widget> </widget>
</item> </item>
<item row="8" column="2"> <item row="8" column="2">
<widget class="QLabel" name="label_51"> <layout class="QHBoxLayout" name="horizontalLayout_21">
<property name="text"> <item>
<string>Test re&amp;sult</string> <widget class="QLabel" name="label_51">
</property> <property name="text">
<property name="buddy"> <string>Test result</string>
<cstring>test_result</cstring> </property>
</property> <property name="buddy">
</widget> <cstring>test_result</cstring>
</property>
</widget>
</item>
<item>
<spacer name="HSpacer_347">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="xlabel_41">
<property name="text">
<string>Multi&amp;ple separator:</string>
</property>
<property name="buddy">
<cstring>multiple_separator</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="multiple_separator">
<property name="toolTip">
<string>Used when displaying test results to separate values in multiple-valued fields</string>
</property>
</widget>
</item>
</layout>
</item> </item>
<item row="9" column="0" colspan="4"> <item row="9" column="0" colspan="4">
<widget class="QScrollArea" name="scrollArea11"> <widget class="QScrollArea" name="scrollArea11">
@ -823,7 +857,7 @@ nothing should be put between the original text and the inserted text</string>
<tabstop>destination_field</tabstop> <tabstop>destination_field</tabstop>
<tabstop>replace_mode</tabstop> <tabstop>replace_mode</tabstop>
<tabstop>comma_separated</tabstop> <tabstop>comma_separated</tabstop>
<tabstop>scrollArea11</tabstop> <tabstop>multiple_separator</tabstop>
<tabstop>test_text</tabstop> <tabstop>test_text</tabstop>
<tabstop>test_result</tabstop> <tabstop>test_result</tabstop>
</tabstops> </tabstops>

View File

@ -57,6 +57,11 @@ class BooksView(QTableView): # {{{
elif tweaks['doubleclick_on_library_view'] == 'open_viewer': elif tweaks['doubleclick_on_library_view'] == 'open_viewer':
self.setEditTriggers(self.SelectedClicked|self.editTriggers()) self.setEditTriggers(self.SelectedClicked|self.editTriggers())
self.doubleClicked.connect(parent.iactions['View'].view_triggered) self.doubleClicked.connect(parent.iactions['View'].view_triggered)
elif tweaks['doubleclick_on_library_view'] == 'edit_metadata':
# Must not enable single-click to edit, or the field will remain
# open in edit mode underneath the edit metadata dialog
self.doubleClicked.connect(
partial(parent.iactions['Edit Metadata'].edit_metadata, checked=False))
self.drag_allowed = True self.drag_allowed = True
self.setDragEnabled(True) self.setDragEnabled(True)

View File

@ -103,7 +103,7 @@ class PluginModel(QAbstractItemModel): # {{{
plugin = self.index_to_plugin(index) plugin = self.index_to_plugin(index)
if role == Qt.DisplayRole: if role == Qt.DisplayRole:
ver = '.'.join(map(str, plugin.version)) ver = '.'.join(map(str, plugin.version))
desc = '\n'.join(textwrap.wrap(plugin.description, 50)) desc = '\n'.join(textwrap.wrap(plugin.description, 100))
ans='%s (%s) %s %s\n%s'%(plugin.name, ver, _('by'), plugin.author, desc) ans='%s (%s) %s %s\n%s'%(plugin.name, ver, _('by'), plugin.author, desc)
c = plugin_customization(plugin) c = plugin_customization(plugin)
if c: if c:

View File

@ -206,17 +206,23 @@ class SearchBox2(QComboBox): # {{{
self.line_edit.blockSignals(yes) self.line_edit.blockSignals(yes)
def set_search_string(self, txt, store_in_history=False, emit_changed=True): def set_search_string(self, txt, store_in_history=False, emit_changed=True):
self.setFocus(Qt.OtherFocusReason) if not store_in_history:
if not txt: self.activated.disconnect()
self.clear() try:
else: self.setFocus(Qt.OtherFocusReason)
self.normalize_state() if not txt:
self.setEditText(txt) self.clear()
self.line_edit.end(False) else:
if emit_changed: self.normalize_state()
self.changed.emit() self.setEditText(txt)
self._do_search(store_in_history=store_in_history) self.line_edit.end(False)
self.focus_to_library.emit() if emit_changed:
self.changed.emit()
self._do_search(store_in_history=store_in_history)
self.focus_to_library.emit()
finally:
if not store_in_history:
self.activated.connect(self.history_selected)
def search_as_you_type(self, enabled): def search_as_you_type(self, enabled):
self.as_you_type = enabled self.as_you_type = enabled

View File

@ -769,7 +769,7 @@ class DocumentView(QWebView): # {{{
self.to_bottom = True self.to_bottom = True
if epf: if epf:
self.flipper.initialize(self.current_page_image(), False) self.flipper.initialize(self.current_page_image(), False)
self.manager.previous_document() self.manager.previous_document()
else: else:
opos = self.document.ypos opos = self.document.ypos
upper_limit = opos - delta_y upper_limit = opos - delta_y
@ -783,8 +783,8 @@ class DocumentView(QWebView): # {{{
if epf: if epf:
self.flipper(self.current_page_image(), self.flipper(self.current_page_image(),
duration=self.document.page_flip_duration) duration=self.document.page_flip_duration)
if self.manager is not None: if self.manager is not None:
self.manager.scrolled(self.scroll_fraction) self.manager.scrolled(self.scroll_fraction)
def next_page(self): def next_page(self):
if self.flipper.running and not self.is_auto_repeat_event: if self.flipper.running and not self.is_auto_repeat_event:

View File

@ -51,6 +51,10 @@ def comments_to_html(comments):
if not isinstance(comments, unicode): if not isinstance(comments, unicode):
comments = comments.decode(preferred_encoding, 'replace') comments = comments.decode(preferred_encoding, 'replace')
if comments.lstrip().startswith('<'):
# Comment is already HTML do not mess with it
return comments
if '<' not in comments: if '<' not in comments:
comments = prepare_string_for_xml(comments) comments = prepare_string_for_xml(comments)
parts = [u'<p class="description">%s</p>'%x.replace(u'\n', u'<br />') parts = [u'<p class="description">%s</p>'%x.replace(u'\n', u'<br />')

View File

@ -188,7 +188,7 @@ class PostInstall:
from calibre.utils.smtp import option_parser as smtp_op from calibre.utils.smtp import option_parser as smtp_op
from calibre.ebooks.epub.fix.main import option_parser as fix_op from calibre.ebooks.epub.fix.main import option_parser as fix_op
any_formats = ['epub', 'htm', 'html', 'xhtml', 'xhtm', 'rar', 'zip', any_formats = ['epub', 'htm', 'html', 'xhtml', 'xhtm', 'rar', 'zip',
'txt', 'lit', 'rtf', 'pdf', 'prc', 'mobi', 'fb2', 'odt', 'lrf'] 'txt', 'lit', 'rtf', 'pdf', 'prc', 'mobi', 'fb2', 'odt', 'lrf', 'snb']
bc = os.path.join(os.path.dirname(self.opts.staging_sharedir), bc = os.path.join(os.path.dirname(self.opts.staging_sharedir),
'bash-completion') 'bash-completion')
if os.path.exists(bc): if os.path.exists(bc):

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff