Pull from trunk

This commit is contained in:
Kovid Goyal 2010-08-13 13:37:10 -06:00
commit 3a06faa17d
50 changed files with 39701 additions and 37363 deletions

View File

@ -4,6 +4,64 @@
# for important features/bug fixes.
# Also, each release can have new and improved recipes.
- version: 0.7.14
date: 2010-08-13
new features:
- title: "Device drivers for the: Teclast K-5, Samsung SNE-60 and Samsung i7500"
- title: "When showing cover browser in a separate window, remember the last used window size"
- title: "Add keyboard shortcuts to show/hide the Tag Browser, Book details and Cover Browser panels. Hover your mouse over the buttons that toggle them to see the shortcuts."
- title: "Calibre library: When the case of title or author is changed, automatically rename the folders to reflect the new case, even on case insensitive filesystems"
- title: "Metadata download: If downloaded title or author is all upper case, automatically fix the case"
- title: "Add method to add books by ISBN. Click the arrow next to Add Books to add from a list of ISBNs."
tickets: [6327]
- title: "Allow editing of tweaks via Preferences->Advanced"
- title: "Add button to manage authors dialog to automatically reset all author sort values"
bug fixes:
- title: "Fix regression in 0.7.13 that broke changing libraries"
- title: "MOBI Output: When processing an input document that specifies non-existant files in the OPF guide, don't crash."
tickets: [6490]
- title: "E-book viewer: When opening consecutive documents in the same viewer, show the correct title in the titlebar"
- title: "Set screen size to 540x718 in Kobo output profile"
- title: "Dont allow calibredb to create custom columns with invalid labels."
tickets: [6487]
- title: "Fix preference to 'search as you type' not working"
- title: "iTunes driver: Fixed bug in PDF file name searching after adding to iTunes database (Windows only)"
- title: "Displaying HTML comments: Do not start a new paragraph at the period in words like Ph.D"
tickets: [6462]
- title: "Respect restriction in effect when refreshing book list"
- title: "Fix drives being reversed for softrooted nook"
new recipes:
- title: "Yahoo News, Skeptical Enquirer and Skeptic"
author: Startson17
- title: "Bolivian newspapers"
author: Darko Miletic
improved recipes:
- Esquire
- Big Oven
- NSPM
- version: 0.7.13
date: 2010-08-06

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

View File

@ -1,4 +1,5 @@
from calibre.web.feeds.news import BasicNewsRecipe
import re
class BigOven(BasicNewsRecipe):
title = 'BigOven'
@ -26,39 +27,38 @@ class BigOven(BasicNewsRecipe):
def get_browser(self):
br = BasicNewsRecipe.get_browser()
if self.username is not None and self.password is not None:
br.open('http://www.bigoven.com/')
br.select_form(name='form1')
br['TopMenu_bo1$email'] = self.username
br['TopMenu_bo1$password'] = self.password
br.open('http://www.bigoven.com/account/login?ReturnUrl=/')
br.select_form(nr=1)
br['Email'] = self.username
br['Password'] = self.password
br.submit()
return br
remove_attributes = ['style', 'font']
keep_only_tags = [dict(name='h1')
,dict(name='div', attrs={'class':'img'})
,dict(name='div', attrs={'id':'intro'})
]
remove_tags = [dict(name='div', attrs={'class':['ppy-caption']})
,dict(name='div', attrs={'id':['float_corner']})
]
remove_tags = [dict(name='div', attrs={'style':["overflow: visible;"]})
,dict(name='div', attrs={'class':['ctas']})
#,dict(name='a', attrs={'class':['edit']})
,dict(name='p', attrs={'class':['byline']})
]
def preprocess_html(self, soup):
for tag in soup.findAll(name='a', attrs={'class':['deflink']}):
tag.replaceWith(tag.string)
for tag in soup.findAll(name='a', text=re.compile(r'.*View Metric.*', re.DOTALL)):
tag.parent.parent.extract()
for tag in soup.findAll(name='a', text=re.compile(r'.*Add my own photo.*', re.DOTALL)):
tag.parent.parent.extract()
for tag in soup.findAll(name='div', attrs={'class':['container']}):
if tag.find(name='h1'):
continue
if tag.find(name='h2', text=re.compile(r'.*Ingredients.*', re.DOTALL)):
print 'tag found Ingred h2'
continue
if tag.find(name='h2', text=re.compile(r'Preparation.*', re.DOTALL)):
print 'tag found Prep h2'
continue
tag.extract()
return soup
feeds = [(u'4 & 5 Star Rated Recipes', u'http://feeds.feedburner.com/Bigovencom-RecipeRaves?format=xml')]
def preprocess_html(self, soup):
for tag in soup.findAll(name='a', attrs={'class':['edit']}):
tag.parent.extract()
for tag in soup.findAll(name='a', attrs={'class':['deflink']}):
tag.replaceWith(tag.string)
return soup
extra_css = '''
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:medium;}
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
'''

View File

@ -9,9 +9,9 @@ from calibre import strftime
from calibre.web.feeds.news import BasicNewsRecipe
class ElPaisImpresa(BasicNewsRecipe):
title = 'El País - edicion impresa'
title = u'El Pa\xeds - edicion impresa'
__author__ = 'Darko Miletic'
description = 'el periodico global en Español'
description = u'el periodico global en Espa\xf1ol'
publisher = 'EDICIONES EL PAIS, S.L.'
category = 'news, politics,Spain,actualidad,noticias,informacion,videos,fotografias,audios,graficos,nacional,internacional,deportes,economia,tecnologia,cultura,gente,television,sociedad,opinion,blogs,foros,chats,encuestas,entrevistas,participacion'
no_stylesheets = True
@ -32,10 +32,10 @@ class ElPaisImpresa(BasicNewsRecipe):
feeds = [
(u'Internacional' , index + u'internacional/' )
,(u'España' , index + u'espana/' )
,(u'Espa\xf1a' , index + u'espana/' )
,(u'Economia' , index + u'economia/' )
,(u'Opinion' , index + u'opinion/' )
,(u'Viñetas' , index + u'vineta/' )
,(u'Vi\xf1etas' , index + u'vineta/' )
,(u'Sociedad' , index + u'sociedad/' )
,(u'Cultura' , index + u'cultura/' )
,(u'Tendencias' , index + u'tendencias/' )

View File

@ -1,7 +1,5 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
__copyright__ = '2009-2010, Darko Miletic <darko.miletic at gmail.com>'
'''
www.esquire.com
@ -9,7 +7,6 @@ www.esquire.com
from calibre import strftime
from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import Tag
class Esquire(BasicNewsRecipe):
title = 'Esquire'
@ -22,22 +19,20 @@ class Esquire(BasicNewsRecipe):
no_stylesheets = True
encoding = 'cp1250'
use_embedded_content = False
language = 'en'
lang = 'en-US'
cover_url = strftime('http://www.esquire.com/cm/esquire/cover-images/%Y_') + strftime('%m').strip('0') + '.jpg'
language = 'en'
publication_type = 'magazine'
masthead_url = 'http://www.esquire.com/cm/shared/site_images/print_this/esquire_logo.gif'
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : lang
, 'pretty_print' : True
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
keep_only_tags = [dict(name='div', attrs={'id':'content'})]
remove_tags = [dict(name=['object','link','embed','iframe'])]
keep_only_tags = [dict(name='div', attrs={'id':['article_header','article_content']})]
remove_tags = [dict(name=['object','link','embed','iframe','base'])]
remove_attributes = ['width','height']
feeds = [
(u'Style' , u'http://www.esquire.com/style/rss/' )
@ -47,17 +42,7 @@ class Esquire(BasicNewsRecipe):
,(u'Frontpage', u'http://www.esquire.com/rss/' )
]
def print_version(self, url):
rest = url.rpartition('?')[0]
article = rest.rpartition('/')[2]
return 'http://www.esquire.com/print-this/' + article
def preprocess_html(self, soup):
soup.html['xml:lang'] = self.lang
soup.html['lang'] = self.lang
mlang = Tag(soup,'meta',[("http-equiv","Content-Language"),("content",self.lang)])
soup.head.insert(0,mlang)
for item in soup.findAll(style=True):
del item['style']
return soup

View File

@ -0,0 +1,46 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
financialexpress.com
'''
from calibre.web.feeds.news import BasicNewsRecipe
class FE_India(BasicNewsRecipe):
title = 'The Financial Express'
__author__ = 'Darko Miletic'
description = 'Financial news from India'
publisher = 'The Indian Express Limited'
category = 'news, politics, finances, India'
oldest_article = 30
max_articles_per_feed = 200
no_stylesheets = True
encoding = 'cp1252'
use_embedded_content = False
language = 'en_IN'
remove_empty_feeds = True
masthead_url = 'http://static.expressindia.com/frontend/fe/images/fe_logo.jpg'
publication_type = 'magazine'
extra_css = ' body{font-family: Arial,Helvetica,sans-serif } '
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
keep_only_tags = [dict(attrs={'class':'txt'})]
remove_attributes = ['width','height']
feeds = [(u'Articles', u'http://www.expressindia.com/syndications/fe.xml')]
def print_version(self, url):
article_raw = url.rpartition('/')[0]
article_id = article_raw.rpartition('/')[2]
return 'http://www.financialexpress.com/printer/news/' + article_id + '/'
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return soup

View File

@ -1,13 +1,10 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008-2009, Darko Miletic <darko.miletic at gmail.com>'
__copyright__ = '2008-2010, Darko Miletic <darko.miletic at gmail.com>'
'''
economictimes.indiatimes.com
'''
from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import Tag
class TheEconomicTimes(BasicNewsRecipe):
title = 'The Economic Times India'
@ -21,18 +18,21 @@ class TheEconomicTimes(BasicNewsRecipe):
use_embedded_content = False
simultaneous_downloads = 1
encoding = 'utf-8'
lang = 'en-IN'
language = 'en_IN'
language = 'en_IN'
publication_type = 'newspaper'
masthead_url = 'http://economictimes.indiatimes.com/photo/2676871.cms'
extra_css = """ body{font-family: Arial,Helvetica,sans-serif}
.heading1{font-size: xx-large; font-weight: bold} """
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
html2lrf_options = [
'--comment', description
, '--category', category
, '--publisher', publisher
, '--ignore-tables'
]
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\nlinearize_tables=True'
keep_only_tags = [dict(attrs={'class':['heading1','headingnext','Normal']})]
remove_tags = [dict(name=['object','link','embed','iframe','base','table','meta'])]
feeds = [(u'All articles', u'http://economictimes.indiatimes.com/rssfeedsdefault.cms')]
@ -47,11 +47,6 @@ class TheEconomicTimes(BasicNewsRecipe):
return rurl
def preprocess_html(self, soup):
soup.html['xml:lang'] = self.lang
soup.html['lang'] = self.lang
mlang = Tag(soup,'meta',[("http-equiv","Content-Language"),("content",self.lang)])
mcharset = Tag(soup,'meta',[("http-equiv","Content-Type"),("content","text/html; charset=utf-8")])
soup.head.insert(0,mlang)
soup.head.insert(1,mcharset)
for item in soup.findAll(style=True):
del item['style']
return self.adeify_images(soup)

View File

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

View File

@ -426,7 +426,7 @@ class KoboReaderOutput(OutputProfile):
description = _('This profile is intended for the Kobo Reader.')
screen_size = (590, 775)
screen_size = (540, 718)
comic_screen_size = (540, 718)
dpi = 168.451
fbase = 12

View File

@ -33,7 +33,9 @@ class ANDROID(USBMS):
# Samsung
0x04e8 : { 0x681d : [0x0222, 0x0400],
0x681c : [0x0222, 0x0224, 0x0400]},
0x681c : [0x0222, 0x0224, 0x0400],
0x6640 : [0x0100],
},
# Acer
0x502 : { 0x3203 : [0x0100]},
@ -54,9 +56,9 @@ class ANDROID(USBMS):
'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX']
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD',
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959']
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD',
'FILE-STOR_GADGET', 'SGH-T959']
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID']
OSX_MAIN_MEM = 'HTC Android Phone Media'

View File

@ -56,6 +56,8 @@ class WinPNPScanner(object):
def drive_order(self, pnp_id):
order = 0
match = re.search(r'REV_.*?&(\d+)#', pnp_id)
if match is None:
match = re.search(r'REV_.*?&(\d+)', pnp_id)
if match is not None:
order = int(match.group(1))
return order

View File

@ -20,20 +20,19 @@ class SNE(USBMS):
# Ordered list of supported formats
# Be sure these have an entry in calibre.devices.mime
FORMATS = ['epub', 'txt']
FORMATS = ['epub', 'pdf', 'txt']
VENDOR_ID = [0x04e8]
PRODUCT_ID = [0x2051]
PRODUCT_ID = [0x2051, 0x2053]
BCD = [0x0323]
VENDOR_NAME = ''
#WINDOWS_MAIN_MEM = 'MASS_STORAGE'
#WINDOWS_CARD_A_MEM = 'MASS_STORAGE'
VENDOR_NAME = 'SAMSUNG'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'SNE-60'
MAIN_MEMORY_VOLUME_LABEL = 'SNE Main Memory'
STORAGE_CARD_VOLUME_LABEL = 'SNE Storage Card'
EBOOK_DIR_MAIN = 'Book'
EBOOK_DIR_MAIN = 'Books'
SUPPORTS_SUB_DIRS = True

View File

@ -6,9 +6,9 @@ from calibre.devices.usbms.driver import USBMS
class TECLAST_K3(USBMS):
name = 'Teclast K3 Device Interface'
gui_name = 'K3'
description = _('Communicate with the Teclast K3 reader.')
name = 'Teclast K3/K5 Device Interface'
gui_name = 'K3/K5'
description = _('Communicate with the Teclast K3/K5 reader.')
author = 'Kovid Goyal'
supported_platforms = ['windows', 'osx', 'linux']
@ -17,11 +17,10 @@ class TECLAST_K3(USBMS):
VENDOR_ID = [0x071b]
PRODUCT_ID = [0x3203]
BCD = [0x0000]
BCD = [0x0000, 0x0100]
VENDOR_NAME = 'TECLAST'
WINDOWS_MAIN_MEM = 'DIGITAL_PLAYER'
WINDOWS_CARD_A_MEM = 'DIGITAL_PLAYER'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['DIGITAL_PLAYER', 'TL-K5']
MAIN_MEMORY_VOLUME_LABEL = 'K3 Main Memory'
STORAGE_CARD_VOLUME_LABEL = 'K3 Storage Card'

View File

@ -185,7 +185,7 @@ class Serializer(object):
buffer.write('<guide>')
for ref in self.oeb.guide.values():
path = urldefrag(ref.href)[0]
if hrefs[path].media_type not in OEB_DOCS:
if path not in hrefs or hrefs[path].media_type not in OEB_DOCS:
continue
buffer.write('<reference type="')

View File

@ -10,10 +10,10 @@ Module to implement the Cover Flow feature
import sys, os, time
from PyQt4.Qt import QImage, QSizePolicy, QTimer, QDialog, Qt, QSize, \
QStackedLayout, QLabel
QStackedLayout, QLabel, QByteArray, pyqtSignal
from calibre import plugins
from calibre.gui2 import config, available_height, available_width
from calibre.gui2 import config, available_height, available_width, gprefs
pictureflow, pictureflowerror = plugins['pictureflow']
@ -107,6 +107,28 @@ else:
DatabaseImages = None
FileSystemImages = None
class CBDialog(QDialog):
closed = pyqtSignal()
def __init__(self, parent, cover_flow):
QDialog.__init__(self, parent)
self._layout = QStackedLayout()
self.setLayout(self._layout)
self.setWindowTitle(_('Browse by covers'))
self.layout().addWidget(cover_flow)
geom = gprefs.get('cover_browser_dialog_geometry', bytearray(''))
geom = QByteArray(geom)
if not self.restoreGeometry(geom):
h, w = available_height()-60, int(available_width()/1.5)
self.resize(w, h)
def closeEvent(self, *args):
geom = bytearray(self.saveGeometry())
gprefs['cover_browser_dialog_geometry'] = geom
self.closed.emit()
class CoverFlowMixin(object):
def __init__(self):
@ -129,6 +151,8 @@ class CoverFlowMixin(object):
self.cover_flow.setWordWrap(True)
if config['separate_cover_flow']:
self.cb_splitter.button.clicked.connect(self.toggle_cover_browser)
self.cb_splitter.button.set_state_to_show()
self.cb_splitter.action_toggle.triggered.connect(self.toggle_cover_browser)
if CoverFlow is not None:
self.cover_flow.stop.connect(self.hide_cover_browser)
else:
@ -137,7 +161,7 @@ class CoverFlowMixin(object):
self.cover_flow.stop.connect(self.cb_splitter.hide_side_pane)
self.cb_splitter.button.toggled.connect(self.cover_browser_toggled)
def toggle_cover_browser(self):
def toggle_cover_browser(self, *args):
cbd = getattr(self, 'cb_dialog', None)
if cbd is not None:
self.hide_cover_browser()
@ -171,25 +195,26 @@ class CoverFlowMixin(object):
def show_cover_browser(self):
d = QDialog(self)
ah, aw = available_height(), available_width()
d.resize(int(aw/1.5), ah-60)
d._layout = QStackedLayout()
d.setLayout(d._layout)
d.setWindowTitle(_('Browse by covers'))
d.layout().addWidget(self.cover_flow)
d = CBDialog(self, self.cover_flow)
self.cover_flow.setVisible(True)
self.cover_flow.setFocus(Qt.OtherFocusReason)
d.show()
self.cb_splitter.button.set_state_to_hide()
d.finished.connect(self.cb_splitter.button.set_state_to_show)
d.closed.connect(self.cover_browser_closed)
self.cb_dialog = d
self.cb_splitter.button.set_state_to_hide()
def hide_cover_browser(self):
def cover_browser_closed(self, *args):
self.cb_dialog = None
self.cb_splitter.button.set_state_to_show()
def hide_cover_browser(self, *args):
cbd = getattr(self, 'cb_dialog', None)
if cbd is not None:
cbd.accept()
self.cb_dialog = None
self.cb_splitter.button.set_state_to_show()
def sync_cf_to_listview(self, current, previous):
if self.cover_flow_sync_flag and self.cover_flow.isVisible() and \

View File

@ -515,15 +515,15 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.reset_confirmation_button.clicked.connect(self.reset_confirmation)
deft, curt = read_raw_tweaks()
self.current_tweaks.setPlainText(curt)
self.default_tweaks.setPlainText(deft)
self.current_tweaks.setPlainText(curt.decode('utf-8'))
self.default_tweaks.setPlainText(deft.decode('utf-8'))
self.restore_tweaks_to_default_button.clicked.connect(self.restore_tweaks_to_default)
self.category_view.setCurrentIndex(self.category_view.model().index_for_name(initial_category))
def restore_tweaks_to_default(self, *args):
deft, curt = read_raw_tweaks()
self.current_tweaks.setPlainText(deft)
self.current_tweaks.setPlainText(deft.decode('utf-8'))
def reset_confirmation(self):
@ -698,8 +698,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.input_order.setCurrentRow(idx-1)
def set_tweaks(self):
raw = unicode(self.current_tweaks.toPlainText())
raw = re.sub(r'(?m)^#.*fileencoding.*', '# ', raw)
raw = unicode(self.current_tweaks.toPlainText()).encode('utf-8')
try:
exec raw
except:

View File

@ -100,11 +100,11 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
def accept(self):
col = unicode(self.column_name_box.text()).lower()
col = unicode(self.column_name_box.text())
if not col:
return self.simple_error('', _('No lookup name was provided'))
if re.match('^\w*$', col) is None or not col[0].isalpha():
return self.simple_error('', _('The label must contain only letters, digits and underscores, and start with a letter'))
if re.match('^\w*$', col) is None or not col[0].isalpha() or col.lower() != col:
return self.simple_error('', _('The lookup name must contain only lower case letters, digits and underscores, and start with a letter'))
col_heading = unicode(self.column_heading_box.text())
col_type = self.column_types[self.column_type_box.currentIndex()]['datatype']
if col_type == '*text':
@ -130,8 +130,6 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
bad_head = True
if bad_head:
return self.simple_error('', _('The heading %s is already used')%col_heading)
if ':' in col or ' ' in col or col.lower() != col:
return self.simple_error('', _('The lookup name must be lower case and cannot contain ":"s or spaces'))
date_format = {}
if col_type == 'datetime':

View File

@ -166,7 +166,8 @@ class LibraryWidget(Splitter): # {{{
I('cover_flow.svg'),
orientation=orientation, parent=parent,
connect_button=not config['separate_cover_flow'],
side_index=idx, initial_side_size=size, initial_show=False)
side_index=idx, initial_side_size=size, initial_show=False,
shortcut=_('Shift+Alt+B'))
parent.library_view = BooksView(parent)
parent.library_view.setObjectName('library_view')
self.addWidget(parent.library_view)
@ -181,7 +182,8 @@ class Stack(QStackedWidget): # {{{
self.tb_widget = TagBrowserWidget(parent)
parent.tb_splitter = Splitter('tag_browser_splitter',
_('Tag Browser'), I('tags.svg'),
parent=parent, side_index=0, initial_side_size=200)
parent=parent, side_index=0, initial_side_size=200,
shortcut=_('Shift+Alt+T'))
parent.tb_splitter.addWidget(self.tb_widget)
parent.tb_splitter.addWidget(parent.cb_splitter)
parent.tb_splitter.setCollapsible(parent.tb_splitter.other_index, False)
@ -274,7 +276,8 @@ class LayoutMixin(object): # {{{
self.stack = Stack(self)
self.bd_splitter = Splitter('book_details_splitter',
_('Book Details'), I('book.svg'),
orientation=Qt.Vertical, parent=self, side_index=1)
orientation=Qt.Vertical, parent=self, side_index=1,
shortcut=_('Alt+D'))
self.bd_splitter.addWidget(self.stack)
self.bd_splitter.addWidget(self.book_details)
self.bd_splitter.setCollapsible(self.bd_splitter.other_index, False)
@ -283,7 +286,8 @@ class LayoutMixin(object): # {{{
else: # wide {{{
self.bd_splitter = Splitter('book_details_splitter',
_('Book Details'), I('book.svg'), initial_side_size=200,
orientation=Qt.Horizontal, parent=self, side_index=1)
orientation=Qt.Horizontal, parent=self, side_index=1,
shortcut=_('Shift+Alt+D'))
self.stack = Stack(self)
self.bd_splitter.addWidget(self.stack)
self.book_details = BookDetails(True, self)

View File

@ -166,6 +166,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
def __init__(self, pathtoebook=None, debug_javascript=False):
MainWindow.__init__(self, None)
self.setupUi(self)
self.base_window_title = unicode(self.windowTitle())
self.iterator = None
self.current_page = None
self.pending_search = None
@ -602,7 +603,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.toc_model = TOC(self.iterator.toc)
self.toc.setModel(self.toc_model)
self.current_title = title
self.setWindowTitle(unicode(self.windowTitle())+' - '+title)
self.setWindowTitle(self.base_window_title+' - '+title)
self.pos.setMaximum(sum(self.iterator.pages))
self.pos.setSuffix(' / %d'%sum(self.iterator.pages))
self.vertical_scrollbar.setMinimum(100)

View File

@ -5,7 +5,7 @@ Miscellaneous widgets used in the GUI
'''
import re, os, traceback
from PyQt4.Qt import QIcon, QFont, QLabel, QListWidget, \
from PyQt4.Qt import QIcon, QFont, QLabel, QListWidget, QAction, \
QListWidgetItem, QTextCharFormat, QApplication, \
QSyntaxHighlighter, QCursor, QColor, QWidget, \
QPixmap, QSplitterHandle, QToolButton, \
@ -855,7 +855,7 @@ class SplitterHandle(QSplitterHandle):
class LayoutButton(QToolButton):
def __init__(self, icon, text, splitter, parent=None):
def __init__(self, icon, text, splitter, parent=None, shortcut=None):
QToolButton.__init__(self, parent)
self.label = text
self.setIcon(QIcon(icon))
@ -864,18 +864,21 @@ class LayoutButton(QToolButton):
self.splitter = splitter
splitter.state_changed.connect(self.update_state)
self.setCursor(Qt.PointingHandCursor)
self.shortcut = ''
if shortcut:
self.shortcut = shortcut
def set_state_to_show(self, *args):
self.setChecked(False)
label =_('Show')
self.setText(label + ' ' + self.label)
self.setText(label + ' ' + self.label + ' ' + self.shortcut)
self.setToolTip(self.text())
self.setStatusTip(self.text())
def set_state_to_hide(self, *args):
self.setChecked(True)
label = _('Hide')
self.setText(label + ' ' + self.label)
self.setText(label + ' ' + self.label+ ' ' + self.shortcut)
self.setToolTip(self.text())
self.setStatusTip(self.text())
@ -891,7 +894,7 @@ class Splitter(QSplitter):
def __init__(self, name, label, icon, initial_show=True,
initial_side_size=120, connect_button=True,
orientation=Qt.Horizontal, side_index=0, parent=None):
orientation=Qt.Horizontal, side_index=0, parent=None, shortcut=None):
QSplitter.__init__(self, parent)
self.resize_timer = QTimer(self)
self.resize_timer.setSingleShot(True)
@ -906,10 +909,21 @@ class Splitter(QSplitter):
self.initial_side_size = initial_side_size
self.initial_show = initial_show
self.splitterMoved.connect(self.splitter_moved, type=Qt.QueuedConnection)
self.button = LayoutButton(icon, label, self)
self.button = LayoutButton(icon, label, self, shortcut=shortcut)
if connect_button:
self.button.clicked.connect(self.double_clicked)
if shortcut is not None:
self.action_toggle = QAction(QIcon(icon), _('Toggle') + ' ' + label,
self)
self.action_toggle.triggered.connect(self.toggle_triggered)
self.action_toggle.setShortcut(shortcut)
if parent is not None:
parent.addAction(self.action_toggle)
def toggle_triggered(self, *args):
self.toggle_side_pane()
def createHandle(self):
return SplitterHandle(self.orientation(), self)

View File

@ -6,7 +6,7 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import json
import json, re
from functools import partial
from math import floor
@ -430,6 +430,10 @@ class CustomColumns(object):
def create_custom_column(self, label, name, datatype, is_multiple,
editable=True, display={}):
if not label:
raise ValueError(_('No label was provided'))
if re.match('^\w*$', label) is None or not label[0].isalpha() or label.lower() != label:
raise ValueError(_('The label must contain only lower case letters, digits and underscores, and start with a letter'))
if datatype not in self.CUSTOM_DATA_TYPES:
raise ValueError('%r is not a supported data type'%datatype)
normalized = datatype not in ('datetime', 'comments', 'int', 'bool',

View File

@ -419,7 +419,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
for format in formats:
# Get data as string (can't use file as source and target files may be the same)
f = self.format(id, format, index_is_id=True, as_file=False)
if not f:
if not f:
continue
stream = cStringIO.StringIO(f)
self.add_format(id, format, stream, index_is_id=True, path=tpath)
@ -430,9 +430,31 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if current_path and os.path.exists(spath):
if self.normpath(spath) != self.normpath(tpath):
self.rmtree(spath, permanent=True)
parent = os.path.dirname(spath)
parent = os.path.dirname(spath)
if len(os.listdir(parent)) == 0:
self.rmtree(parent, permanent=True)
curpath = self.library_path
c1, c2 = current_path.split('/'), path.split('/')
if not self.is_case_sensitive and len(c1) == len(c2):
# On case-insensitive systems, title and author renames that only
# change case don't cause any changes to the directories in the file
# system. This can lead to having the directory names not match the
# title/author, which leads to trouble when libraries are copied to
# a case-sensitive system. The following code fixes this by checking
# each segment. If they are different because of case, then rename
# the segment to some temp file name, then rename it back to the
# correct name. Note that the code above correctly handles files in
# the directories, so no need to do them here.
for oldseg, newseg in zip(c1, c2):
if oldseg.lower() == newseg.lower() and oldseg != newseg:
while True:
# need a temp name in the current segment for renames
tempname = os.path.join(curpath, 'TEMP.%f'%time.time())
if not os.path.exists(tempname):
break
os.rename(os.path.join(curpath, oldseg), tempname)
os.rename(tempname, os.path.join(curpath, newseg))
curpath = os.path.join(curpath, newseg)
def add_listener(self, listener):
'''

View File

@ -30,8 +30,8 @@ Environment variables
Tweaks
------------
Tweaks are small changes that you can specify to control various aspects of |app|'s behavior. You specify them by editing the 2tweaks.py file in the config directory.
The default tweaks.py file is reproduced below
Tweaks are small changes that you can specify to control various aspects of |app|'s behavior. You can change them by going to Preferences->Advanced->Tweaks.
The default values for the tweaks are reproduced below
.. literalinclude:: ../../../resources/default_tweaks.py

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