diff --git a/resources/images/donate.svg b/resources/images/donate.svg
index b17d0ec7a0..603e672f6f 100644
--- a/resources/images/donate.svg
+++ b/resources/images/donate.svg
@@ -1,24 +1,31 @@
+
\ No newline at end of file
diff --git a/resources/images/lt.png b/resources/images/lt.png
new file mode 100644
index 0000000000..c29efb9f88
Binary files /dev/null and b/resources/images/lt.png differ
diff --git a/resources/images/news/elpais_impreso.png b/resources/images/news/elpais_impreso.png
new file mode 100644
index 0000000000..35dcaf2d44
Binary files /dev/null and b/resources/images/news/elpais_impreso.png differ
diff --git a/resources/recipes/ap.recipe b/resources/recipes/ap.recipe
index 572c0aa392..0118cf0726 100644
--- a/resources/recipes/ap.recipe
+++ b/resources/recipes/ap.recipe
@@ -12,9 +12,9 @@ class AssociatedPress(BasicNewsRecipe):
max_articles_per_feed = 15
html2lrf_options = ['--force-page-break-before-tag="chapter"']
-
-
- preprocess_regexps = [ (re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in
+
+
+ preprocess_regexps = [ (re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in
[
(r'
.*?' , lambda match : ''),
(r'.*?', lambda match : ''),
@@ -25,10 +25,10 @@ class AssociatedPress(BasicNewsRecipe):
(r'', lambda match : '
'),
(r'Learn more about our Privacy Policy.*?', lambda match : ''),
]
- ]
-
+ ]
+
+
-
feeds = [ ('AP Headlines', 'http://hosted.ap.org/lineups/TOPHEADS-rss_2.0.xml?SITE=ORAST&SECTION=HOME'),
('AP US News', 'http://hosted.ap.org/lineups/USHEADS-rss_2.0.xml?SITE=CAVIC&SECTION=HOME'),
('AP World News', 'http://hosted.ap.org/lineups/WORLDHEADS-rss_2.0.xml?SITE=SCAND&SECTION=HOME'),
@@ -38,4 +38,4 @@ class AssociatedPress(BasicNewsRecipe):
('AP Health News', 'http://hosted.ap.org/lineups/HEALTHHEADS-rss_2.0.xml?SITE=FLDAY&SECTION=HOME'),
('AP Science News', 'http://hosted.ap.org/lineups/SCIENCEHEADS-rss_2.0.xml?SITE=OHCIN&SECTION=HOME'),
('AP Strange News', 'http://hosted.ap.org/lineups/STRANGEHEADS-rss_2.0.xml?SITE=WCNC&SECTION=HOME'),
- ]
\ No newline at end of file
+ ]
diff --git a/resources/recipes/elpais_impreso.recipe b/resources/recipes/elpais_impreso.recipe
new file mode 100644
index 0000000000..b30db0707a
--- /dev/null
+++ b/resources/recipes/elpais_impreso.recipe
@@ -0,0 +1,86 @@
+# -*- coding: utf-8 -*-
+__license__ = 'GPL v3'
+__copyright__ = '2010, Darko Miletic '
+'''
+www.elpais.com/diario/
+'''
+
+from calibre import strftime
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class ElPaisImpresa(BasicNewsRecipe):
+ title = 'El País - edicion impresa'
+ __author__ = 'Darko Miletic'
+ description = 'el periodico global en Español'
+ 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
+ encoding = 'latin1'
+ use_embedded_content = False
+ language = 'es'
+ publication_type = 'newspaper'
+ masthead_url = 'http://www.elpais.com/im/tit_logo_global.gif'
+ index = 'http://www.elpais.com/diario/'
+ extra_css = ' p{text-align: justify} body{ text-align: left; font-family: Georgia,"Times New Roman",Times,serif } h2{font-family: Arial,Helvetica,sans-serif} img{margin-bottom: 0.4em} '
+
+ conversion_options = {
+ 'comment' : description
+ , 'tags' : category
+ , 'publisher' : publisher
+ , 'language' : language
+ }
+
+ feeds = [
+ (u'Internacional' , index + u'internacional/' )
+ ,(u'España' , index + u'espana/' )
+ ,(u'Economia' , index + u'economia/' )
+ ,(u'Opinion' , index + u'opinion/' )
+ ,(u'Viñetas' , index + u'vineta/' )
+ ,(u'Sociedad' , index + u'sociedad/' )
+ ,(u'Cultura' , index + u'cultura/' )
+ ,(u'Tendencias' , index + u'tendencias/' )
+ ,(u'Gente' , index + u'gente/' )
+ ,(u'Obituarios' , index + u'obituarios/' )
+ ,(u'Deportes' , index + u'deportes/' )
+ ,(u'Pantallas' , index + u'radioytv/' )
+ ,(u'Ultima' , index + u'ultima/' )
+ ,(u'Educacion' , index + u'educacion/' )
+ ,(u'Saludo' , index + u'salud/' )
+ ,(u'Ciberpais' , index + u'ciberpais/' )
+ ,(u'EP3' , index + u'ep3/' )
+ ,(u'Cine' , index + u'cine/' )
+ ,(u'Babelia' , index + u'babelia/' )
+ ,(u'El viajero' , index + u'viajero/' )
+ ,(u'Negocios' , index + u'negocios/' )
+ ,(u'Domingo' , index + u'domingo/' )
+ ,(u'El Pais semanal' , index + u'eps/' )
+ ,(u'Quadern Catalunya' , index + u'quadern-catalunya/' )
+ ]
+
+ keep_only_tags=[dict(attrs={'class':['cabecera_noticia','contenido_noticia']})]
+ remove_attributes=['width','height']
+ remove_tags=[dict(name='link')]
+
+ def parse_index(self):
+ totalfeeds = []
+ lfeeds = self.get_feeds()
+ for feedobj in lfeeds:
+ feedtitle, feedurl = feedobj
+ self.report_progress(0, _('Fetching feed')+' %s...'%(feedtitle if feedtitle else feedurl))
+ articles = []
+ soup = self.index_to_soup(feedurl)
+ for item in soup.findAll('a',attrs={'class':['g19r003','g19i003','g17r003','g17i003']}):
+ url = 'http://www.elpais.com' + item['href'].rpartition('/')[0]
+ title = self.tag_to_string(item)
+ date = strftime(self.timefmt)
+ articles.append({
+ 'title' :title
+ ,'date' :date
+ ,'url' :url
+ ,'description':''
+ })
+ totalfeeds.append((feedtitle, articles))
+ return totalfeeds
+
+ def print_version(self, url):
+ return url + '?print=1'
diff --git a/resources/recipes/greader.recipe b/resources/recipes/greader.recipe
index cbf4c0226b..2c9d5aa015 100644
--- a/resources/recipes/greader.recipe
+++ b/resources/recipes/greader.recipe
@@ -4,29 +4,27 @@ from calibre import __appname__
class GoogleReader(BasicNewsRecipe):
title = 'Google Reader'
- description = 'This recipe downloads feeds you have tagged from your Google Reader account.'
+ description = 'This recipe fetches from your Google Reader account unread Starred items and unread Feeds you have placed in a folder via the manage subscriptions feature.'
needs_subscription = True
- __author__ = 'davec'
+ __author__ = 'davec, rollercoaster, Starson17'
base_url = 'http://www.google.com/reader/atom/'
- max_articles_per_feed = 50
+ oldest_article = 365
+ max_articles_per_feed = 250
get_options = '?n=%d&xt=user/-/state/com.google/read' % max_articles_per_feed
use_embedded_content = True
def get_browser(self):
- br = BasicNewsRecipe.get_browser()
-
+ br = BasicNewsRecipe.get_browser(self)
if self.username is not None and self.password is not None:
request = urllib.urlencode([('Email', self.username), ('Passwd', self.password),
- ('service', 'reader'), ('source', __appname__)])
+ ('service', 'reader'), ('accountType', 'HOSTED_OR_GOOGLE'), ('source', __appname__)])
response = br.open('https://www.google.com/accounts/ClientLogin', request)
- sid = re.search('SID=(\S*)', response.read()).group(1)
-
+ auth = re.search('Auth=(\S*)', response.read()).group(1)
cookies = mechanize.CookieJar()
br = mechanize.build_opener(mechanize.HTTPCookieProcessor(cookies))
- cookies.set_cookie(mechanize.Cookie(None, 'SID', sid, None, False, '.google.com', True, True, '/', True, False, None, True, '', '', None))
+ br.addheaders = [('Authorization', 'GoogleLogin auth='+auth)]
return br
-
def get_feeds(self):
feeds = []
soup = self.index_to_soup('http://www.google.com/reader/api/0/tag/list')
diff --git a/resources/recipes/greader_uber.recipe b/resources/recipes/greader_uber.recipe
index ee48e7069d..5e02cdef5d 100644
--- a/resources/recipes/greader_uber.recipe
+++ b/resources/recipes/greader_uber.recipe
@@ -3,10 +3,10 @@ from calibre.web.feeds.recipes import BasicNewsRecipe
from calibre import __appname__
class GoogleReaderUber(BasicNewsRecipe):
- title = 'Google Reader Uber'
- description = 'This recipe downloads all unread feedsfrom your Google Reader account.'
+ title = 'Google Reader uber'
+ description = 'Fetches all feeds from your Google Reader account including the uncategorized items.'
needs_subscription = True
- __author__ = 'rollercoaster, davec'
+ __author__ = 'davec, rollercoaster, Starson17'
base_url = 'http://www.google.com/reader/atom/'
oldest_article = 365
max_articles_per_feed = 250
@@ -14,20 +14,17 @@ class GoogleReaderUber(BasicNewsRecipe):
use_embedded_content = True
def get_browser(self):
- br = BasicNewsRecipe.get_browser()
-
+ br = BasicNewsRecipe.get_browser(self)
if self.username is not None and self.password is not None:
request = urllib.urlencode([('Email', self.username), ('Passwd', self.password),
- ('service', 'reader'), ('source', __appname__)])
+ ('service', 'reader'), ('accountType', 'HOSTED_OR_GOOGLE'), ('source', __appname__)])
response = br.open('https://www.google.com/accounts/ClientLogin', request)
- sid = re.search('SID=(\S*)', response.read()).group(1)
-
+ auth = re.search('Auth=(\S*)', response.read()).group(1)
cookies = mechanize.CookieJar()
br = mechanize.build_opener(mechanize.HTTPCookieProcessor(cookies))
- cookies.set_cookie(mechanize.Cookie(None, 'SID', sid, None, False, '.google.com', True, True, '/', True, False, None, True, '', '', None))
+ br.addheaders = [('Authorization', 'GoogleLogin auth='+auth)]
return br
-
def get_feeds(self):
feeds = []
soup = self.index_to_soup('http://www.google.com/reader/api/0/tag/list')
diff --git a/resources/recipes/waco_tribune.recipe b/resources/recipes/waco_tribune.recipe
new file mode 100644
index 0000000000..18eb61fb26
--- /dev/null
+++ b/resources/recipes/waco_tribune.recipe
@@ -0,0 +1,34 @@
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class AdvancedUserRecipe1278773519(BasicNewsRecipe):
+ title = u'Waco Tribune Herald'
+ __author__ = 'rty'
+ pubisher = 'A Robinson Media Company'
+ description = 'Waco, Texas, Newspaper'
+ category = 'News, Texas, Waco'
+ oldest_article = 7
+ max_articles_per_feed = 100
+
+ feeds = [
+ (u'News', u'http://www.wacotrib.com/news/index.rss2'),
+ (u'Sports', u'http://www.wacotrib.com/sports/index.rss2'),
+ (u'AccessWaco', u'http://www.wacotrib.com/accesswaco/index.rss2'),
+ (u'Opinions', u'http://www.wacotrib.com/opinion/index.rss2')
+ ]
+
+ remove_javascript = True
+ use_embedded_content = False
+ no_stylesheets = True
+ language = 'en'
+ encoding = 'utf-8'
+ conversion_options = {'linearize_tables':True}
+ masthead_url = 'http://media.wacotrib.com/designimages/wacotrib_logo.jpg'
+ keep_only_tags = [
+ dict(name='div', attrs={'class':'twoColumn left'}),
+ ]
+ remove_tags = [
+ dict(name='div', attrs={'class':'right blueLinks'}),
+ ]
+ remove_tags_after = [
+ dict(name='div', attrs={'class':'dottedRule'}),
+ ]
diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py
index 5642235b31..5d9d094b26 100644
--- a/src/calibre/devices/android/driver.py
+++ b/src/calibre/devices/android/driver.py
@@ -30,7 +30,8 @@ class ANDROID(USBMS):
0x18d1 : { 0x4e11 : [0x0100, 0x226], 0x4e12: [0x0100, 0x226]},
# Samsung
- 0x04e8 : { 0x681d : [0x0222, 0x0400], 0x681c : [0x0222, 0x0224]},
+ 0x04e8 : { 0x681d : [0x0222, 0x0400],
+ 0x681c : [0x0222, 0x0224, 0x0400]},
# Acer
0x502 : { 0x3203 : [0x0100]},
@@ -70,6 +71,16 @@ class ANDROID(USBMS):
dirs = [x.strip() for x in dirs.split(',')]
self.EBOOK_DIR_MAIN = dirs
+ def get_main_ebook_dir(self, for_upload=False):
+ dirs = self.EBOOK_DIR_MAIN
+ if not for_upload:
+ def aldiko_tweak(x):
+ return 'eBooks' if x == 'eBooks/import' else x
+ if isinstance(dirs, basestring):
+ dirs = [dirs]
+ dirs = list(map(aldiko_tweak, dirs))
+ return dirs
+
class S60(USBMS):
name = 'S60 driver'
diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py
index 3156542a92..618fc27545 100644
--- a/src/calibre/devices/apple/driver.py
+++ b/src/calibre/devices/apple/driver.py
@@ -2586,14 +2586,20 @@ class ITUNES(DriverBase):
if metadata.series and self.settings().read_metadata:
if DEBUG:
self.log.info(" using Series name as Genre")
+
+ # Format the index as a sort key
+ index = metadata.series_index
+ integer = int(index)
+ fraction = index-integer
+ series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0'))
if lb_added:
- lb_added.sort_name.set("%s %04f" % (metadata.series, metadata.series_index))
+ lb_added.sort_name.set("%s %s" % (metadata.series, series_index))
lb_added.genre.set(metadata.series)
lb_added.episode_ID.set(metadata.series)
lb_added.episode_number.set(metadata.series_index)
if db_added:
- db_added.sort_name.set("%s %04f" % (metadata.series, metadata.series_index))
+ db_added.sort_name.set("%s %s" % (metadata.series, series_index))
db_added.genre.set(metadata.series)
db_added.episode_ID.set(metadata.series)
db_added.episode_number.set(metadata.series_index)
@@ -2658,8 +2664,13 @@ class ITUNES(DriverBase):
if metadata.series and self.settings().read_metadata:
if DEBUG:
self.log.info(" using Series name as Genre")
+ # Format the index as a sort key
+ index = metadata.series_index
+ integer = int(index)
+ fraction = index-integer
+ series_index = '%04d%%s' % (integer, str('%0.4f' % fraction).lstrip('0'))
if lb_added:
- lb_added.SortName = "%s %04f" % (metadata.series, metadata.series_index)
+ lb_added.SortName = "%s %s" % (metadata.series, series_index)
lb_added.Genre = metadata.series
lb_added.EpisodeID = metadata.series
try:
@@ -2667,7 +2678,7 @@ class ITUNES(DriverBase):
except:
pass
if db_added:
- db_added.SortName = "%s %04f" % (metadata.series, metadata.series_index)
+ db_added.SortName = "%s %s" % (metadata.series, series_index)
db_added.Genre = metadata.series
db_added.EpisodeID = metadata.series
try:
diff --git a/src/calibre/devices/prs505/sony_cache.py b/src/calibre/devices/prs505/sony_cache.py
index b87ca937bc..3ac35df9b2 100644
--- a/src/calibre/devices/prs505/sony_cache.py
+++ b/src/calibre/devices/prs505/sony_cache.py
@@ -10,10 +10,10 @@ from base64 import b64decode
from uuid import uuid4
from lxml import etree
-from calibre import prints, guess_type
+from calibre import prints, guess_type, isbytestring
from calibre.devices.errors import DeviceError
from calibre.devices.usbms.driver import debug_print
-from calibre.constants import DEBUG
+from calibre.constants import DEBUG, preferred_encoding
from calibre.ebooks.chardet import xml_to_unicode
from calibre.ebooks.metadata import authors_to_string, title_sort
@@ -473,6 +473,13 @@ class XMLCache(object):
# if the case of a tie, and hope it is right.
timestamp = os.path.getmtime(path)
rec_date = record.get('date', None)
+
+ def clean(x):
+ if isbytestring(x):
+ x = x.decode(preferred_encoding, 'replace')
+ x.replace(u'\0', '')
+ return x
+
if not getattr(book, '_new_book', False): # book is not new
if strftime(timestamp, zone=time.gmtime) == rec_date:
gtz_count += 1
@@ -486,19 +493,19 @@ class XMLCache(object):
tz = time.gmtime
debug_print("Using GMT TZ for new book", book.lpath)
date = strftime(timestamp, zone=tz)
- record.set('date', date)
+ record.set('date', clean(date))
- record.set('size', str(os.stat(path).st_size))
+ record.set('size', clean(str(os.stat(path).st_size)))
title = book.title if book.title else _('Unknown')
- record.set('title', title)
+ record.set('title', clean(title))
ts = book.title_sort
if not ts:
ts = title_sort(title)
- record.set('titleSorter', ts)
+ record.set('titleSorter', clean(ts))
if self.use_author_sort and book.author_sort is not None:
- record.set('author', book.author_sort)
+ record.set('author', clean(book.author_sort))
else:
- record.set('author', authors_to_string(book.authors))
+ record.set('author', clean(authors_to_string(book.authors)))
ext = os.path.splitext(path)[1]
if ext:
ext = ext[1:].lower()
@@ -506,7 +513,7 @@ class XMLCache(object):
if mime is None:
mime = guess_type('a.'+ext)[0]
if mime is not None:
- record.set('mime', mime)
+ record.set('mime', clean(mime))
if 'sourceid' not in record.attrib:
record.set('sourceid', '1')
if 'id' not in record.attrib:
diff --git a/src/calibre/devices/scanner.py b/src/calibre/devices/scanner.py
index ceba5d37d0..dd789dd668 100644
--- a/src/calibre/devices/scanner.py
+++ b/src/calibre/devices/scanner.py
@@ -98,6 +98,9 @@ class LinuxScanner(object):
def __call__(self):
ans = set([])
+ if not self.ok:
+ raise RuntimeError('DeviceScanner requires the /sys filesystem to work.')
+
for x in os.listdir(self.base):
base = os.path.join(self.base, x)
ven = os.path.join(base, 'idVendor')
@@ -145,8 +148,6 @@ class DeviceScanner(object):
def __init__(self, *args):
if isosx and osx_scanner is None:
raise RuntimeError('The Python extension usbobserver must be available on OS X.')
- if islinux and not linux_scanner.ok:
- raise RuntimeError('DeviceScanner requires the /sys filesystem to work.')
self.scanner = win_scanner if iswindows else osx_scanner if isosx else linux_scanner
self.devices = []
diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py
index 55790420f2..c07b7fd761 100644
--- a/src/calibre/devices/usbms/device.py
+++ b/src/calibre/devices/usbms/device.py
@@ -732,7 +732,7 @@ class Device(DeviceConfig, DevicePlugin):
traceback.print_exc()
self._main_prefix = self._card_a_prefix = self._card_b_prefix = None
- def get_main_ebook_dir(self):
+ def get_main_ebook_dir(self, for_upload=False):
return self.EBOOK_DIR_MAIN
def _sanity_check(self, on_card, files):
@@ -750,7 +750,7 @@ class Device(DeviceConfig, DevicePlugin):
path = os.path.join(self._card_b_prefix,
*(self.EBOOK_DIR_CARD_B.split('/')))
else:
- candidates = self.get_main_ebook_dir()
+ candidates = self.get_main_ebook_dir(for_upload=True)
if isinstance(candidates, basestring):
candidates = [candidates]
candidates = [
diff --git a/src/calibre/gui2/convert/structure_detection.ui b/src/calibre/gui2/convert/structure_detection.ui
index e4414473f5..2e97c0d3ca 100644
--- a/src/calibre/gui2/convert/structure_detection.ui
+++ b/src/calibre/gui2/convert/structure_detection.ui
@@ -28,7 +28,11 @@
-
-
+
+
+ 20
+
+
-
diff --git a/src/calibre/gui2/convert/xexp_edit.ui b/src/calibre/gui2/convert/xexp_edit.ui
index 1b0196a8a1..f98eb8b1b8 100644
--- a/src/calibre/gui2/convert/xexp_edit.ui
+++ b/src/calibre/gui2/convert/xexp_edit.ui
@@ -43,6 +43,15 @@
0
+
+
+ 500
+ 16777215
+
+
+
+ 30
+
diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py
index 2a9c81e8ee..96232fe85f 100644
--- a/src/calibre/gui2/custom_column_widgets.py
+++ b/src/calibre/gui2/custom_column_widgets.py
@@ -10,9 +10,10 @@ from functools import partial
from PyQt4.Qt import QComboBox, QLabel, QSpinBox, QDoubleSpinBox, QDateEdit, \
QDate, QGroupBox, QVBoxLayout, QPlainTextEdit, QSizePolicy, \
- QSpacerItem, QIcon, QCheckBox, QWidget, QHBoxLayout, SIGNAL
+ QSpacerItem, QIcon, QCheckBox, QWidget, QHBoxLayout, SIGNAL, \
+ QPushButton
-from calibre.utils.date import qt_to_dt
+from calibre.utils.date import qt_to_dt, now
from calibre.gui2.widgets import TagsLineEdit, EnComboBox
from calibre.gui2 import UNDEFINED_QDATE
from calibre.utils.config import tweaks
@@ -132,20 +133,30 @@ class DateEdit(QDateEdit):
def focusInEvent(self, x):
self.setSpecialValueText('')
+ QDateEdit.focusInEvent(self, x)
def focusOutEvent(self, x):
self.setSpecialValueText(_('Undefined'))
+ QDateEdit.focusOutEvent(self, x)
+
+ def set_to_today(self):
+ self.setDate(now())
class DateTime(Base):
def setup_ui(self, parent):
- self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
- DateEdit(parent)]
+ cm = self.col_metadata
+ self.widgets = [QLabel('&'+cm['name']+':', parent), DateEdit(parent),
+ QLabel(''), QPushButton(_('Set \'%s\' to today')%cm['name'], parent)]
w = self.widgets[1]
- w.setDisplayFormat('dd MMM yyyy')
+ format = cm['display'].get('date_format','')
+ if not format:
+ format = 'dd MMM yyyy'
+ w.setDisplayFormat(format)
w.setCalendarPopup(True)
w.setMinimumDate(UNDEFINED_QDATE)
w.setSpecialValueText(_('Undefined'))
+ self.widgets[3].clicked.connect(w.set_to_today)
def setter(self, val):
if val is None:
diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py
index 91afac8aa2..d81918c307 100644
--- a/src/calibre/gui2/device.py
+++ b/src/calibre/gui2/device.py
@@ -765,6 +765,7 @@ class DeviceMixin(object): # {{{
self.book_details.reset_info()
self.location_view.setCurrentIndex(self.location_view.model().index(0))
self.refresh_ondevice_info (device_connected = False)
+ self.tool_bar.device_status_changed(bool(connected))
def info_read(self, job):
'''
diff --git a/src/calibre/gui2/dialogs/config/__init__.py b/src/calibre/gui2/dialogs/config/__init__.py
index bf9dc0a623..b064dc53c2 100644
--- a/src/calibre/gui2/dialogs/config/__init__.py
+++ b/src/calibre/gui2/dialogs/config/__init__.py
@@ -334,7 +334,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
def __init__(self, parent, library_view, server=None):
ResizableDialog.__init__(self, parent)
- self.ICON_SIZES = {0:QSize(48, 48), 1:QSize(32,32), 2:QSize(24,24)}
self._category_model = CategoryModel()
self.category_view.currentChanged = self.category_current_changed
@@ -389,10 +388,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.add_custcol_button.clicked.connect(self.add_custcol)
self.edit_custcol_button.clicked.connect(self.edit_custcol)
- icons = config['toolbar_icon_size']
- self.toolbar_button_size.setCurrentIndex(0 if icons == self.ICON_SIZES[0] else 1 if icons == self.ICON_SIZES[1] else 2)
- self.show_toolbar_text.setChecked(config['show_text_in_toolbar'])
-
output_formats = sorted(available_output_formats())
output_formats.remove('oeb')
for f in output_formats:
@@ -845,8 +840,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
must_restart = self.apply_custom_column_changes()
- config['toolbar_icon_size'] = self.ICON_SIZES[self.toolbar_button_size.currentIndex()]
- config['show_text_in_toolbar'] = bool(self.show_toolbar_text.isChecked())
config['separate_cover_flow'] = bool(self.separate_cover_flow.isChecked())
config['disable_tray_notification'] = not self.systray_notifications.isChecked()
p = {0:'normal', 1:'high', 2:'low'}[self.priority.currentIndex()]
diff --git a/src/calibre/gui2/dialogs/config/config.ui b/src/calibre/gui2/dialogs/config/config.ui
index b473ee7846..5f890631b2 100644
--- a/src/calibre/gui2/dialogs/config/config.ui
+++ b/src/calibre/gui2/dialogs/config/config.ui
@@ -422,54 +422,6 @@
-
-
-
- Toolbar
-
-
-
-
-
-
-
-
- Large
-
-
- -
-
- Medium
-
-
- -
-
- Small
-
-
-
-
- -
-
-
- &Button size in toolbar
-
-
- toolbar_button_size
-
-
-
- -
-
-
- Show &text in toolbar buttons
-
-
- true
-
-
-
-
-
-
- -
-
diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py
index 2d038d9ddc..2474685522 100644
--- a/src/calibre/gui2/init.py
+++ b/src/calibre/gui2/init.py
@@ -176,12 +176,6 @@ class ToolbarMixin(object): # {{{
def show_help(self, *args):
open_url(QUrl('http://calibre-ebook.com/user_manual'))
- def read_toolbar_settings(self):
- self.tool_bar.setIconSize(config['toolbar_icon_size'])
- self.tool_bar.setToolButtonStyle(
- Qt.ToolButtonTextUnderIcon if \
- config['show_text_in_toolbar'] else \
- Qt.ToolButtonIconOnly)
# }}}
diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py
index c44efa2354..0228249e8d 100644
--- a/src/calibre/gui2/layout.py
+++ b/src/calibre/gui2/layout.py
@@ -5,6 +5,8 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal '
__docformat__ = 'restructuredtext en'
+from operator import attrgetter
+
from PyQt4.Qt import QIcon, Qt, QWidget, QAction, QToolBar, QSize, QVariant, \
QAbstractListModel, QFont, QApplication, QPalette, pyqtSignal, QToolButton, \
QModelIndex, QListView, QAbstractButton, QPainter, QPixmap, QColor, \
@@ -13,41 +15,11 @@ from PyQt4.Qt import QIcon, Qt, QWidget, QAction, QToolBar, QSize, QVariant, \
from calibre.constants import __appname__, filesystem_encoding
from calibre.gui2.search_box import SearchBox2, SavedSearchBox
from calibre.gui2.throbber import ThrobbingButton
-from calibre.gui2 import NONE
+from calibre.gui2 import NONE, config
from calibre.gui2.widgets import ComboBoxWithHelp
from calibre import human_readable
-class ToolBar(QToolBar): # {{{
-
- def __init__(self, parent=None):
- QToolBar.__init__(self, parent)
- self.setContextMenuPolicy(Qt.PreventContextMenu)
- self.setMovable(False)
- self.setFloatable(False)
- self.setOrientation(Qt.Horizontal)
- self.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea)
- self.setIconSize(QSize(48, 48))
- self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
-
- def add_actions(self, *args):
- self.left_space = QWidget(self)
- self.left_space.setSizePolicy(QSizePolicy.Expanding,
- QSizePolicy.Minimum)
- self.addWidget(self.left_space)
- for action in args:
- if action is None:
- self.addSeparator()
- else:
- self.addAction(action)
- self.right_space = QWidget(self)
- self.right_space.setSizePolicy(QSizePolicy.Expanding,
- QSizePolicy.Minimum)
- self.addWidget(self.right_space)
-
- def contextMenuEvent(self, *args):
- pass
-
-# }}}
+ICON_SIZE = 48
# Location View {{{
@@ -191,14 +163,15 @@ class LocationView(QListView):
self.setTabKeyNavigation(True)
self.setProperty("showDropIndicator", True)
self.setSelectionMode(self.SingleSelection)
- self.setIconSize(QSize(40, 40))
+ self.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
self.setMovement(self.Static)
self.setFlow(self.LeftToRight)
- self.setGridSize(QSize(175, 90))
+ self.setGridSize(QSize(175, ICON_SIZE))
self.setViewMode(self.ListMode)
self.setWordWrap(True)
self.setObjectName("location_view")
- self.setMaximumHeight(74)
+ self.setMaximumSize(QSize(600, ICON_SIZE+16))
+ self.setMinimumWidth(400)
def eject_clicked(self, *args):
self.unmount_device.emit()
@@ -207,6 +180,10 @@ class LocationView(QListView):
self.model().count = new_count
self.model().reset()
+ @property
+ def book_count(self):
+ return self.model().count
+
def current_changed(self, current, previous):
if current.isValid():
i = current.row()
@@ -248,12 +225,15 @@ class EjectButton(QAbstractButton):
def __init__(self, parent):
QAbstractButton.__init__(self, parent)
self.mouse_over = False
+ self.setMouseTracking(True)
def enterEvent(self, event):
self.mouse_over = True
+ QAbstractButton.enterEvent(self, event)
def leaveEvent(self, event):
self.mouse_over = False
+ QAbstractButton.leaveEvent(self, event)
def paintEvent(self, event):
painter = QPainter(self)
@@ -344,33 +324,84 @@ class SearchBar(QWidget): # {{{
# }}}
-class LocationBar(ToolBar): # {{{
+class ToolBar(QToolBar): # {{{
def __init__(self, actions, donate, location_view, parent=None):
- ToolBar.__init__(self, parent)
-
- for ac in actions:
- self.addAction(ac)
-
- self.addWidget(location_view)
- self.w = QWidget()
- self.w.setLayout(QVBoxLayout())
- self.w.layout().addWidget(donate)
- donate.setAutoRaise(True)
- donate.setCursor(Qt.PointingHandCursor)
- self.addWidget(self.w)
- self.setIconSize(QSize(50, 50))
+ QToolBar.__init__(self, parent)
+ self.setContextMenuPolicy(Qt.PreventContextMenu)
+ self.setMovable(False)
+ self.setFloatable(False)
+ self.setOrientation(Qt.Horizontal)
+ self.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea)
+ self.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
- def button_for_action(self, ac):
- b = QToolButton(self)
- b.setDefaultAction(ac)
- for x in ('ToolTip', 'StatusTip', 'WhatsThis'):
- getattr(b, 'set'+x)(b.text())
+ self.showing_device = False
+ self.all_actions = actions
+ self.donate = donate
+ self.location_view = location_view
+ self.d_widget = QWidget()
+ self.d_widget.setLayout(QVBoxLayout())
+ self.d_widget.layout().addWidget(donate)
+ donate.setAutoRaise(True)
+ donate.setCursor(Qt.PointingHandCursor)
+ self.build_bar()
+
+ def contextMenuEvent(self, *args):
+ pass
+
+ def device_status_changed(self, connected):
+ self.showing_device = connected
+ self.build_bar()
+
+ def build_bar(self):
+ order_field = 'device' if self.showing_device else 'normal'
+ o = attrgetter(order_field+'_order')
+ sepvals = [2] if self.showing_device else [1]
+ sepvals += [3]
+ actions = [x for x in self.all_actions if o(x) > -1]
+ actions.sort(cmp=lambda x,y : cmp(o(x), o(y)))
+ self.clear()
+ for x in actions:
+ self.addAction(x)
+ ch = self.widgetForAction(x)
+ ch.setCursor(Qt.PointingHandCursor)
+ ch.setAutoRaise(True)
+
+ if x.action_name == 'choose_library':
+ self.location_action = self.addWidget(self.location_view)
+ self.choose_action = x
+ if config['show_donate_button']:
+ self.addWidget(self.d_widget)
+ if x.action_name not in ('choose_library', 'help'):
+ ch.setPopupMode(ch.MenuButtonPopup)
+
+
+ for x in actions:
+ if x.separator_before in sepvals:
+ self.insertSeparator(x)
+
+
+ self.location_action.setVisible(self.showing_device)
+ self.choose_action.setVisible(not self.showing_device)
+
+ def count_changed(self, new_count):
+ text = _('%d books')%new_count
+ a = self.choose_action
+ a.setText(text)
+
+ def resizeEvent(self, ev):
+ style = Qt.ToolButtonTextUnderIcon
+ if self.size().width() < 1260:
+ style = Qt.ToolButtonIconOnly
+ self.setToolButtonStyle(style)
+ QToolBar.resizeEvent(self, ev)
- return b
# }}}
+class Action(QAction):
+ pass
+
class MainWindowMixin(object):
def __init__(self):
@@ -385,12 +416,19 @@ class MainWindowMixin(object):
self.centralwidget.setLayout(self._central_widget_layout)
self.resize(1012, 740)
self.donate_button = ThrobbingButton(self.centralwidget)
- self.donate_button.set_normal_icon_size(64, 64)
+ self.donate_button.set_normal_icon_size(ICON_SIZE, ICON_SIZE)
# Actions {{{
- def ac(name, text, icon, shortcut=None, tooltip=None):
- action = QAction(QIcon(I(icon)), text, self)
+ all_actions = []
+
+ def ac(normal_order, device_order, separator_before,
+ name, text, icon, shortcut=None, tooltip=None):
+ action = Action(QIcon(I(icon)), text, self)
+ action.normal_order = normal_order
+ action.device_order = device_order
+ action.separator_before = separator_before
+ action.action_name = name
text = tooltip if tooltip else text
action.setToolTip(text)
action.setStatusTip(text)
@@ -400,56 +438,46 @@ class MainWindowMixin(object):
if shortcut:
action.setShortcut(shortcut)
setattr(self, 'action_'+name, action)
+ all_actions.append(action)
- ac('add', _('Add books'), 'add_book.svg', _('A'))
- ac('del', _('Remove books'), 'trash.svg', _('Del'))
- ac('edit', _('Edit meta info'), 'edit_input.svg', _('E'))
- ac('merge', _('Merge book records'), 'merge_books.svg', _('M'))
- ac('sync', _('Send to device'), 'sync.svg')
- ac('save', _('Save to disk'), 'save.svg', _('S'))
- ac('news', _('Fetch news'), 'news.svg', _('F'))
- ac('convert', _('Convert books'), 'convert.svg', _('C'))
- ac('view', _('View'), 'view.svg', _('V'))
- ac('open_containing_folder', _('Open containing folder'),
+ ac(0, 7, 0, 'add', _('Add books'), 'add_book.svg', _('A'))
+ ac(1, 1, 0, 'edit', _('Edit metadata'), 'edit_input.svg', _('E'))
+ ac(2, 2, 3, 'convert', _('Convert books'), 'convert.svg', _('C'))
+ ac(3, 3, 0, 'view', _('View'), 'view.svg', _('V'))
+ ac(4, 4, 3, 'choose_library', _('%d books')%0, 'lt.png',
+ tooltip=_('Choose calibre library to work with'))
+ ac(5, 5, 3, 'news', _('Fetch news'), 'news.svg', _('F'))
+ ac(6, 6, 0, 'save', _('Save to disk'), 'save.svg', _('S'))
+ ac(7, 0, 0, 'sync', _('Send to device'), 'sync.svg')
+ ac(8, 8, 3, 'del', _('Remove books'), 'trash.svg', _('Del'))
+ ac(9, 9, 3, 'help', _('Help'), 'help.svg', _('F1'), _("Browse the calibre User Manual"))
+ ac(10, 10, 0, 'preferences', _('Preferences'), 'config.svg', _('Ctrl+P'))
+
+ ac(-1, -1, 0, 'merge', _('Merge book records'), 'merge_books.svg', _('M'))
+ ac(-1, -1, 0, 'open_containing_folder', _('Open containing folder'),
'document_open.svg')
- ac('show_book_details', _('Show book details'),
+ ac(-1, -1, 0, 'show_book_details', _('Show book details'),
'dialog_information.svg')
- ac('books_by_same_author', _('Books by same author'),
+ ac(-1, -1, 0, 'books_by_same_author', _('Books by same author'),
'user_profile.svg')
- ac('books_in_this_series', _('Books in this series'),
+ ac(-1, -1, 0, 'books_in_this_series', _('Books in this series'),
'books_in_series.svg')
- ac('books_by_this_publisher', _('Books by this publisher'),
+ ac(-1, -1, 0, 'books_by_this_publisher', _('Books by this publisher'),
'publisher.png')
- ac('books_with_the_same_tags', _('Books with the same tags'),
+ ac(-1, -1, 0, 'books_with_the_same_tags', _('Books with the same tags'),
'tags.svg')
- ac('preferences', _('Preferences'), 'config.svg', _('Ctrl+P'))
- ac('help', _('Help'), 'help.svg', _('F1'), _("Browse the calibre User Manual"))
# }}}
- self.tool_bar = ToolBar(self)
- self.addToolBar(Qt.BottomToolBarArea, self.tool_bar)
- self.tool_bar.add_actions(self.action_convert, self.action_view,
- None, self.action_edit, None,
- self.action_save, self.action_del,
- None,
- self.action_help, None, self.action_preferences)
-
self.location_view = LocationView(self.centralwidget)
self.search_bar = SearchBar(self)
- self.location_bar = LocationBar([self.action_add, self.action_sync,
- self.action_news], self.donate_button, self.location_view, self)
- self.addToolBar(Qt.TopToolBarArea, self.location_bar)
+ self.tool_bar = ToolBar(all_actions, self.donate_button, self.location_view, self)
+ self.addToolBar(Qt.TopToolBarArea, self.tool_bar)
l = self.centralwidget.layout()
l.addWidget(self.search_bar)
- for ch in list(self.tool_bar.children()) + list(self.location_bar.children()):
- if isinstance(ch, QToolButton):
- ch.setCursor(Qt.PointingHandCursor)
- ch.setAutoRaise(True)
- if ch is not self.donate_button:
- ch.setPopupMode(ch.MenuButtonPopup)
-
+ def read_toolbar_settings(self):
+ pass
diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py
index 529055ecd2..40f7a2e4e0 100644
--- a/src/calibre/gui2/library/delegates.py
+++ b/src/calibre/gui2/library/delegates.py
@@ -96,7 +96,7 @@ class DateDelegate(QStyledItemDelegate): # {{{
def displayText(self, val, locale):
d = val.toDate()
- if d == UNDEFINED_QDATE:
+ if d <= UNDEFINED_QDATE:
return ''
return format_date(d.toPyDate(), 'dd MMM yyyy')
@@ -116,7 +116,7 @@ class PubDateDelegate(QStyledItemDelegate): # {{{
def displayText(self, val, locale):
d = val.toDate()
- if d == UNDEFINED_QDATE:
+ if d <= UNDEFINED_QDATE:
return ''
format = tweaks['gui_pubdate_display_format']
if format is None:
@@ -194,7 +194,7 @@ class CcDateDelegate(QStyledItemDelegate): # {{{
def displayText(self, val, locale):
d = val.toDate()
- if d == UNDEFINED_QDATE:
+ if d <= UNDEFINED_QDATE:
return ''
return format_date(d.toPyDate(), self.format)
@@ -217,7 +217,7 @@ class CcDateDelegate(QStyledItemDelegate): # {{{
def setModelData(self, editor, model, index):
val = editor.date()
- if val == UNDEFINED_QDATE:
+ if val <= UNDEFINED_QDATE:
val = None
model.setData(index, QVariant(val), Qt.EditRole)
diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py
index 9f1a72b021..89008735fe 100644
--- a/src/calibre/gui2/library/models.py
+++ b/src/calibre/gui2/library/models.py
@@ -1216,7 +1216,9 @@ class DeviceBooksModel(BooksModel): # {{{
return done
def set_editable(self, editable):
- self.editable = editable
+ # Cannot edit if metadata is sent on connect. Reason: changes will
+ # revert to what is in the library on next connect.
+ self.editable = editable and prefs['manage_device_metadata']!='on_connect'
def set_search_restriction(self, s):
pass
diff --git a/src/calibre/gui2/search_restriction_mixin.py b/src/calibre/gui2/search_restriction_mixin.py
index f677c839d8..a4186ad8d1 100644
--- a/src/calibre/gui2/search_restriction_mixin.py
+++ b/src/calibre/gui2/search_restriction_mixin.py
@@ -13,6 +13,7 @@ class SearchRestrictionMixin(object):
self.search_restriction.setSizeAdjustPolicy(self.search_restriction.AdjustToMinimumContentsLengthWithIcon)
self.search_restriction.setMinimumContentsLength(10)
self.search_restriction.setStatusTip(self.search_restriction.toolTip())
+ self.search_count.setText(_("(all books)"))
'''
Adding and deleting books while restricted creates a complexity. When added,
diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py
index 6bd7b2b502..ba4c637932 100644
--- a/src/calibre/gui2/ui.py
+++ b/src/calibre/gui2/ui.py
@@ -167,8 +167,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
self.eject_action = self.system_tray_menu.addAction(
QIcon(I('eject.svg')), _('&Eject connected device'))
self.eject_action.setEnabled(False)
- if not config['show_donate_button']:
- self.donate_button.setVisible(False)
self.addAction(self.quit_action)
self.action_restart = QAction(_('&Restart'), self)
self.addAction(self.action_restart)
@@ -220,8 +218,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
if self.system_tray_icon.isVisible() and opts.start_in_tray:
self.hide_windows()
- self.library_view.model().count_changed_signal.connect \
- (self.location_view.count_changed)
+ for t in (self.location_view, self.tool_bar):
+ self.library_view.model().count_changed_signal.connect \
+ (t.count_changed)
if not gprefs.get('quick_start_guide_added', False):
from calibre.ebooks.metadata import MetaInformation
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
@@ -274,8 +273,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
SIGNAL('start_recipe_fetch(PyQt_PyObject)'),
self.download_scheduled_recipe, Qt.QueuedConnection)
- self.location_view.setCurrentIndex(self.location_view.model().index(0))
-
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
AddAction.__init__(self)
diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py
index d46ae23d90..af950a36fc 100644
--- a/src/calibre/library/caches.py
+++ b/src/calibre/library/caches.py
@@ -209,13 +209,13 @@ class ResultCache(SearchQueryParser):
if query == 'false':
for item in self._data:
if item is None: continue
- if item[loc] is None or item[loc] == UNDEFINED_DATE:
+ if item[loc] is None or item[loc] <= UNDEFINED_DATE:
matches.add(item[0])
return matches
if query == 'true':
for item in self._data:
if item is None: continue
- if item[loc] is not None and item[loc] != UNDEFINED_DATE:
+ if item[loc] is not None and item[loc] > UNDEFINED_DATE:
matches.add(item[0])
return matches
diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py
index f991df7e23..04b85687c5 100644
--- a/src/calibre/library/catalog.py
+++ b/src/calibre/library/catalog.py
@@ -3,7 +3,7 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Greg Riker '
-import datetime, htmlentitydefs, os, re, shutil
+import datetime, htmlentitydefs, os, re, shutil, codecs
from collections import namedtuple
from copy import deepcopy
@@ -96,17 +96,20 @@ class CSV_XML(CatalogPlugin):
fields = self.get_output_fields(opts)
if self.fmt == 'csv':
- outfile = open(path_to_output, 'w')
+ outfile = codecs.open(path_to_output, 'w', 'utf8')
# Output the field headers
outfile.write(u'%s\n' % u','.join(fields))
# Output the entry fields
for entry in data:
- outstr = ''
- for (x, field) in enumerate(fields):
+ outstr = []
+ for field in fields:
item = entry[field]
- if field == 'formats':
+ if item is None:
+ outstr.append('""')
+ continue
+ elif field == 'formats':
fmt_list = []
for format in item:
fmt_list.append(format.rpartition('.')[2].lower())
@@ -118,19 +121,13 @@ class CSV_XML(CatalogPlugin):
item = u'%s' % re.sub(r'[\D]', '', item)
elif field in ['pubdate', 'timestamp']:
item = isoformat(item)
-
- #Format the line
- if x < len(fields) - 1:
- if item is not None:
- outstr += u'"%s",' % unicode(item).replace('"','""')
- else:
- outstr += '"",'
- else:
- if item is not None:
- outstr += u'"%s"\n' % unicode(item).replace('"','""')
- else:
- outstr += '""\n'
- outfile.write(outstr.encode('utf-8'))
+ elif field == 'comments':
+ item = item.replace(u'\r\n',u' ')
+ item = item.replace(u'\n',u' ')
+
+ outstr.append(u'"%s"' % unicode(item).replace('"','""'))
+
+ outfile.write(u','.join(outstr) + u'\n')
outfile.close()
elif self.fmt == 'xml':
@@ -269,7 +266,6 @@ class BIBTEX(CatalogPlugin):
def run(self, path_to_output, opts, db, notification=DummyReporter()):
- import codecs
from types import StringType, UnicodeType
from calibre.library.save_to_disk import preprocess_template