'
'''
@@ -23,7 +22,14 @@ class Danas(BasicNewsRecipe):
language = 'sr'
publication_type = 'newspaper'
remove_empty_feeds = True
- extra_css = '@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} .article_description,body,.lokacija{font-family: Tahoma,Arial,Helvetica,sans1,sans-serif} .nadNaslov,h1,.preamble{font-family: Georgia,"Times New Roman",Times,serif1,serif} .antrfileText{border-left: 2px solid #999999; margin-left: 0.8em; padding-left: 1.2em; margin-bottom: 0; margin-top: 0} h2,.datum,.lokacija,.autor{font-size: small} .antrfileNaslov{border-left: 2px solid #999999; margin-left: 0.8em; padding-left: 1.2em; font-weight:bold; margin-bottom: 0; margin-top: 0} img{margin-bottom: 0.8em} '
+ extra_css = """ @font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)}
+ @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)}
+ .article_description,body,.lokacija{font-family: Tahoma,Arial,Helvetica,sans1,sans-serif}
+ .nadNaslov,h1,.preamble{font-family: Georgia,"Times New Roman",Times,serif1,serif}
+ .antrfileText{border-left: 2px solid #999999; margin-left: 0.8em; padding-left: 1.2em;
+ margin-bottom: 0; margin-top: 0} h2,.datum,.lokacija,.autor{font-size: small}
+ .antrfileNaslov{border-left: 2px solid #999999; margin-left: 0.8em; padding-left: 1.2em;
+ font-weight:bold; margin-bottom: 0; margin-top: 0} img{margin-bottom: 0.8em} """
conversion_options = {
'comment' : description
@@ -42,19 +48,32 @@ class Danas(BasicNewsRecipe):
]
feeds = [
- (u'Politika' , u'http://www.danas.rs/rss/rss.asp?column_id=27')
- ,(u'Hronika' , u'http://www.danas.rs/rss/rss.asp?column_id=2' )
- ,(u'Dru\xc5\xa1tvo', u'http://www.danas.rs/rss/rss.asp?column_id=24')
- ,(u'Dijalog' , u'http://www.danas.rs/rss/rss.asp?column_id=1' )
- ,(u'Ekonomija', u'http://www.danas.rs/rss/rss.asp?column_id=6' )
- ,(u'Svet' , u'http://www.danas.rs/rss/rss.asp?column_id=25')
- ,(u'Srbija' , u'http://www.danas.rs/rss/rss.asp?column_id=28')
- ,(u'Kultura' , u'http://www.danas.rs/rss/rss.asp?column_id=5' )
- ,(u'Sport' , u'http://www.danas.rs/rss/rss.asp?column_id=13')
- ,(u'Scena' , u'http://www.danas.rs/rss/rss.asp?column_id=42')
- ,(u'Feljton' , u'http://www.danas.rs/rss/rss.asp?column_id=19')
- ,(u'Periskop' , u'http://www.danas.rs/rss/rss.asp?column_id=4' )
- ,(u'Famozno' , u'http://www.danas.rs/rss/rss.asp?column_id=47')
+ (u'Politika' , u'http://www.danas.rs/rss/rss.asp?column_id=27')
+ ,(u'Hronika' , u'http://www.danas.rs/rss/rss.asp?column_id=2' )
+ ,(u'Drustvo' , u'http://www.danas.rs/rss/rss.asp?column_id=24')
+ ,(u'Dijalog' , u'http://www.danas.rs/rss/rss.asp?column_id=1' )
+ ,(u'Ekonomija' , u'http://www.danas.rs/rss/rss.asp?column_id=6' )
+ ,(u'Svet' , u'http://www.danas.rs/rss/rss.asp?column_id=25')
+ ,(u'Srbija' , u'http://www.danas.rs/rss/rss.asp?column_id=28')
+ ,(u'Kultura' , u'http://www.danas.rs/rss/rss.asp?column_id=5' )
+ ,(u'Sport' , u'http://www.danas.rs/rss/rss.asp?column_id=13')
+ ,(u'Scena' , u'http://www.danas.rs/rss/rss.asp?column_id=42')
+ ,(u'Feljton' , u'http://www.danas.rs/rss/rss.asp?column_id=19')
+ ,(u'Periskop' , u'http://www.danas.rs/rss/rss.asp?column_id=4' )
+ ,(u'Famozno' , u'http://www.danas.rs/rss/rss.asp?column_id=47')
+ ,(u'Sluzbena beleska' , u'http://www.danas.rs/rss/rss.asp?column_id=48')
+ ,(u'Suocavanja' , u'http://www.danas.rs/rss/rss.asp?column_id=49')
+ ,(u'Moj Izbor' , u'http://www.danas.rs/rss/rss.asp?column_id=50')
+ ,(u'Direktno' , u'http://www.danas.rs/rss/rss.asp?column_id=51')
+ ,(u'I tome slicno' , u'http://www.danas.rs/rss/rss.asp?column_id=52')
+ ,(u'No longer and not yet', u'http://www.danas.rs/rss/rss.asp?column_id=53')
+ ,(u'Resetovanje' , u'http://www.danas.rs/rss/rss.asp?column_id=54')
+ ,(u'Iza scene' , u'http://www.danas.rs/rss/rss.asp?column_id=60')
+ ,(u'Drustvoslovlje' , u'http://www.danas.rs/rss/rss.asp?column_id=55')
+ ,(u'Zvaka u pepeljari' , u'http://www.danas.rs/rss/rss.asp?column_id=56')
+ ,(u'Vostani Serbie' , u'http://www.danas.rs/rss/rss.asp?column_id=57')
+ ,(u'Med&Jad-a' , u'http://www.danas.rs/rss/rss.asp?column_id=58')
+ ,(u'Svetlosti pozornice' , u'http://www.danas.rs/rss/rss.asp?column_id=59')
]
def preprocess_html(self, soup):
@@ -65,3 +84,10 @@ class Danas(BasicNewsRecipe):
def print_version(self, url):
return url + '&action=print'
+ def get_cover_url(self):
+ cover_url = None
+ soup = self.index_to_soup('http://www.danas.rs/')
+ for citem in soup.findAll('img'):
+ if citem['src'].endswith('naslovna.jpg'):
+ return 'http://www.danas.rs' + citem['src']
+ return cover_url
diff --git a/resources/recipes/thairath.recipe b/resources/recipes/thairath.recipe
new file mode 100644
index 0000000000..6ebb84f3a5
--- /dev/null
+++ b/resources/recipes/thairath.recipe
@@ -0,0 +1,58 @@
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class AdvancedUserRecipe1271637235(BasicNewsRecipe):
+
+ title = u'Thairath'
+ __author__ = 'Anat R.'
+ language = 'th'
+
+ oldest_article = 7
+
+ max_articles_per_feed = 100
+ no_stylesheets = True
+
+ remove_javascript = True
+
+ use_embedded_content = False
+ feeds = [(u'News',
+u'http://www.thairath.co.th/rss/news.xml'), (u'Politics',
+u'http://www.thairath.co.th/rss/pol.xml'), (u'Economy',
+u'http://www.thairath.co.th/rss/eco.xml'), (u'International',
+u'http://www.thairath.co.th/rss/oversea.xml'), (u'Sports',
+u'http://www.thairath.co.th/rss/sport.xml'), (u'Life',
+u'http://www.thairath.co.th/rss/life.xml'), (u'Education',
+u'http://www.thairath.co.th/rss/edu.xml'), (u'Tech',
+u'http://www.thairath.co..th/rss/tech.xml'), (u'Entertainment',
+u'http://www.thairath.co.th/rss/ent.xml')]
+ keep_only_tags = []
+
+ keep_only_tags.append(dict(name = 'h1', attrs = {'id' : 'title'}))
+
+ keep_only_tags.append(dict(name = 'ul', attrs = {'class' :
+'detail-info'}))
+
+ keep_only_tags.append(dict(name = 'img', attrs = {'class' :
+'detail-image'}))
+
+ keep_only_tags.append(dict(name = 'div', attrs = {'class' :
+'entry'}))
+ remove_tags = []
+ remove_tags.append(dict(name = 'div', attrs = {'id':
+'menu-holder'}))
+
+ remove_tags.append(dict(name = 'div', attrs = {'class':
+'addthis_toolbox addthis_default_style'}))
+
+ remove_tags.append(dict(name = 'div', attrs = {'class': 'box top-item'}))
+
+ remove_tags.append(dict(name = 'div', attrs = {'class': 'column-200 column-margin-430'}))
+
+ remove_tags.append(dict(name = 'div', attrs = {'id':
+'detail-related'}))
+
+ remove_tags.append(dict(name = 'div', attrs = {'id': 'related'}))
+
+ remove_tags.append(dict(name = 'id', attrs = {'class': 'footer'}))
+
+ remove_tags.append(dict(name = "ul",attrs =
+{'id':'banner-highlights-images'}))
diff --git a/resources/recipes/the_nation_thai.recipe b/resources/recipes/the_nation_thai.recipe
new file mode 100644
index 0000000000..a33a16e0a5
--- /dev/null
+++ b/resources/recipes/the_nation_thai.recipe
@@ -0,0 +1,44 @@
+
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class AdvancedUserRecipe1271596863(BasicNewsRecipe):
+
+ title = u'The Nation'
+ __author__ = 'Anat R.'
+ language = 'en_TH'
+
+ oldest_article = 7
+
+ max_articles_per_feed = 100
+ no_stylesheets = True
+
+ remove_javascript = True
+
+ use_embedded_content = False
+ feeds = [(u'Topstory',
+u'http://www.nationmultimedia.com/home/rss/topstories.rss'),
+(u'National', u'http://www.nationmultimedia.com/home/rss/national.rss'),
+ (u'Politics',
+u'http://www.nationmultimedia.com/home/rss/politics.rss'), (u'Business',
+ u'http://www.nationmultimedia.com/home/rss/business.rss'),
+(u'Regional', u'http://www.nationmultimedia.com/home/rss/regional.rss'),
+ (u'Sports', u'http://www.nationmultimedia.com/home/rss/sport.rss'),
+(u'Travel', u'http://www.nationmultimedia.com/home/rss/travel.rss'),
+(u'Life', u'http://www.nationmultimedia.com/home/rss/life.rss')]
+ keep_only_tags = []
+
+ keep_only_tags.append(dict(name = 'div', attrs = {'class' :
+'pd10'}))
+ remove_tags = []
+
+ remove_tags.append(dict(name = 'div', attrs = {'class':
+'WrapperHeaderCol2-2'}))
+
+ remove_tags.append(dict(name = 'div', attrs = {'class':
+'LayoutMenu2'}))
+
+ remove_tags.append(dict(name = 'div', attrs = {'class':
+'TextHeaderRight'}))
+
+ remove_tags.append(dict(name = "ul",attrs = {'id':'toolZoom'}))
+
diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py
index ca93990420..ec895cb8a4 100644
--- a/src/calibre/customize/builtins.py
+++ b/src/calibre/customize/builtins.py
@@ -436,7 +436,7 @@ from calibre.devices.blackberry.driver import BLACKBERRY
from calibre.devices.cybook.driver import CYBOOK
from calibre.devices.eb600.driver import EB600, COOL_ER, SHINEBOOK, \
POCKETBOOK360, GER2, ITALICA, ECLICTO, DBOOK, INVESBOOK, \
- BOOQ, ELONEX
+ BOOQ, ELONEX, POCKETBOOK301
from calibre.devices.iliad.driver import ILIAD
from calibre.devices.irexdr.driver import IREXDR1000, IREXDR800
from calibre.devices.jetbook.driver import JETBOOK
@@ -507,6 +507,7 @@ plugins += [
JETBOOK,
SHINEBOOK,
POCKETBOOK360,
+ POCKETBOOK301,
KINDLE,
KINDLE2,
KINDLE_DX,
diff --git a/src/calibre/devices/eb600/driver.py b/src/calibre/devices/eb600/driver.py
index 307531c357..9b7a21a3bb 100644
--- a/src/calibre/devices/eb600/driver.py
+++ b/src/calibre/devices/eb600/driver.py
@@ -201,4 +201,21 @@ class ELONEX(EB600):
def can_handle(cls, dev, debug=False):
return dev[3] == 'Elonex' and dev[4] == 'eBook'
+class POCKETBOOK301(USBMS):
+
+ name = 'PocketBook 301 Device Interface'
+ description = _('Communicate with the PocketBook 301 reader.')
+ author = 'Kovid Goyal'
+ supported_platforms = ['windows', 'osx', 'linux']
+ FORMATS = ['epub', 'fb2', 'prc', 'mobi', 'pdf', 'djvu', 'rtf', 'chm', 'txt']
+
+ SUPPORTS_SUB_DIRS = True
+
+ MAIN_MEMORY_VOLUME_LABEL = 'PocketBook 301 Main Memory'
+ STORAGE_CARD_VOLUME_LABEL = 'PocketBook 301 Storage Card'
+
+ VENDOR_ID = [0x1]
+ PRODUCT_ID = [0x301]
+ BCD = [0x132]
+
diff --git a/src/calibre/ebooks/epub/output.py b/src/calibre/ebooks/epub/output.py
index ee779aaefa..8708b98d97 100644
--- a/src/calibre/ebooks/epub/output.py
+++ b/src/calibre/ebooks/epub/output.py
@@ -385,14 +385,6 @@ class EPUBOutput(OutputFormatPlugin):
if val and not pval:
rule.style.setProperty('padding-left', val)
- if stylesheet is not None:
- stylesheet.data.add('a { color: inherit; text-decoration: inherit; '
- 'cursor: default; }')
- stylesheet.data.add('a[href] { color: blue; '
- 'text-decoration: underline; cursor:pointer; }')
- else:
- self.oeb.log.warn('No stylesheet found')
-
# }}}
def workaround_sony_quirks(self): # {{{
diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py
index c633e5149b..8caca1f261 100644
--- a/src/calibre/ebooks/metadata/__init__.py
+++ b/src/calibre/ebooks/metadata/__init__.py
@@ -223,6 +223,7 @@ class MetaInformation(object):
'isbn', 'tags', 'cover_data', 'application_id', 'guide',
'manifest', 'spine', 'toc', 'cover', 'language',
'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc',
+ 'author_sort_map',
'pubdate', 'rights', 'publication_type', 'uuid'):
if hasattr(mi, attr):
setattr(ans, attr, getattr(mi, attr))
@@ -244,6 +245,7 @@ class MetaInformation(object):
self.tags = getattr(mi, 'tags', [])
#: mi.cover_data = (ext, data)
self.cover_data = getattr(mi, 'cover_data', (None, None))
+ self.author_sort_map = getattr(mi, 'author_sort_map', {})
for x in ('author_sort', 'title_sort', 'comments', 'category', 'publisher',
'series', 'series_index', 'rating', 'isbn', 'language',
@@ -258,7 +260,7 @@ class MetaInformation(object):
'series', 'series_index', 'tags', 'rating', 'isbn', 'language',
'application_id', 'manifest', 'toc', 'spine', 'guide', 'cover',
'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate',
- 'rights', 'publication_type', 'uuid'
+ 'rights', 'publication_type', 'uuid', 'author_sort_map'
):
prints(x, getattr(self, x, 'None'))
@@ -288,6 +290,9 @@ class MetaInformation(object):
self.tags += mi.tags
self.tags = list(set(self.tags))
+ if mi.author_sort_map:
+ self.author_sort_map.update(mi.author_sort_map)
+
if getattr(mi, 'cover_data', False):
other_cover = mi.cover_data[-1]
self_cover = self.cover_data[-1] if self.cover_data else ''
diff --git a/src/calibre/ebooks/metadata/book/__init__.py b/src/calibre/ebooks/metadata/book/__init__.py
index 8483c2bddb..c3b95f1188 100644
--- a/src/calibre/ebooks/metadata/book/__init__.py
+++ b/src/calibre/ebooks/metadata/book/__init__.py
@@ -35,6 +35,8 @@ PUBLICATION_METADATA_FIELDS = frozenset([
'title_sort',
# Ordered list of authors. Must never be None, can be [_('Unknown')]
'authors',
+ # Map of sort strings for each author
+ 'author_sort_map',
# Pseudo field that can be set, but if not set is auto generated
# from authors and languages
'author_sort',
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index ad5dd17ace..ba34f04f95 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -16,6 +16,7 @@ NULL_VALUES = {
'classifiers' : {},
'languages' : [],
'device_collections': [],
+ 'author_sort_map': {},
'authors' : [_('Unknown')],
'title' : _('Unknown'),
}
diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py
index 306bbc77e6..1056f6ced6 100644
--- a/src/calibre/gui2/__init__.py
+++ b/src/calibre/gui2/__init__.py
@@ -43,8 +43,8 @@ def _config():
help=_('Notify when a new version is available'))
c.add_opt('use_roman_numerals_for_series_number', default=True,
help=_('Use Roman numerals for series number'))
- c.add_opt('sort_by_popularity', default=False,
- help=_('Sort tags list by popularity'))
+ c.add_opt('sort_tags_by', default='name',
+ help=_('Sort tags list by name, popularity, or rating'))
c.add_opt('cover_flow_queue_length', default=6,
help=_('Number of covers to show in the cover browsing mode'))
c.add_opt('LRF_conversion_defaults', default=[],
diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py
index 75c045d011..8759dd360b 100644
--- a/src/calibre/gui2/book_details.py
+++ b/src/calibre/gui2/book_details.py
@@ -62,11 +62,13 @@ def render_rows(data):
class CoverView(QWidget): # {{{
- def __init__(self, parent=None):
+ def __init__(self, vertical, parent=None):
QWidget.__init__(self, parent)
self.setMaximumSize(QSize(120, 120))
- self.setMinimumSize(QSize(120, 1))
+ self.setMinimumSize(QSize(120 if vertical else 20, 120 if vertical else
+ 20))
self._current_pixmap_size = self.maximumSize()
+ self.vertical = vertical
self.animation = QPropertyAnimation(self, 'current_pixmap_size', self)
self.animation.setEasingCurve(QEasingCurve(QEasingCurve.OutExpo))
@@ -74,7 +76,8 @@ class CoverView(QWidget): # {{{
self.animation.setStartValue(QSize(0, 0))
self.animation.valueChanged.connect(self.value_changed)
- self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+ self.setSizePolicy(QSizePolicy.Expanding if vertical else
+ QSizePolicy.Minimum, QSizePolicy.Expanding)
self.default_pixmap = QPixmap(I('book.svg'))
self.pixmap = self.default_pixmap
@@ -98,8 +101,12 @@ class CoverView(QWidget): # {{{
self.animation.setEndValue(self.current_pixmap_size)
def relayout(self, parent_size):
- self.setMaximumSize(parent_size.width(),
- min(int(parent_size.height()/2.),int(4/3. * parent_size.width())+1))
+ if self.vertical:
+ self.setMaximumSize(parent_size.width(),
+ min(int(parent_size.height()/2.),int(4/3. * parent_size.width())+1))
+ else:
+ self.setMaximumSize(1+int(3/4. * parent_size.height()),
+ parent_size.height())
self.resize(self.maximumSize())
self.animation.stop()
self.do_layout()
@@ -109,8 +116,7 @@ class CoverView(QWidget): # {{{
def show_data(self, data):
self.animation.stop()
- if data.get('id', None) == self.data.get('id', None):
- return
+ same_item = data.get('id', True) == self.data.get('id', False)
self.data = {'id':data.get('id', None)}
if data.has_key('cover'):
self.pixmap = QPixmap.fromImage(data.pop('cover'))
@@ -120,7 +126,8 @@ class CoverView(QWidget): # {{{
self.pixmap = self.default_pixmap
self.do_layout()
self.update()
- self.animation.start()
+ if not same_item:
+ self.animation.start()
def paintEvent(self, event):
canvas_size = self.rect()
@@ -147,6 +154,7 @@ class CoverView(QWidget): # {{{
# }}}
+# Book Info {{{
class Label(QLabel):
mr = pyqtSignal(object)
@@ -174,8 +182,9 @@ class Label(QLabel):
class BookInfo(QScrollArea):
- def __init__(self, parent=None):
+ def __init__(self, vertical, parent=None):
QScrollArea.__init__(self, parent)
+ self.vertical = vertical
self.setWidgetResizable(True)
self.label = Label()
self.setWidget(self.label)
@@ -188,13 +197,25 @@ class BookInfo(QScrollArea):
rows = render_rows(data)
rows = u'\n'.join([u'%s: | %s |
'%(k,t) for
k, t in rows])
- if _('Comments') in data and data[_('Comments')]:
- comments = comments_to_html(data[_('Comments')])
- rows += u'%s |
'%comments
+ if self.vertical:
+ if _('Comments') in data and data[_('Comments')]:
+ comments = comments_to_html(data[_('Comments')])
+ rows += u'%s |
'%comments
+ self.label.setText(u''%rows)
+ else:
+ comments = ''
+ if _('Comments') in data:
+ comments = comments_to_html(data[_('Comments')])
+ left_pane = u''%rows
+ right_pane = u'%s
'%comments
+ self.label.setText(u''
+ % (left_pane, right_pane))
- self.label.setText(u''%rows)
-class BookDetails(QWidget):
+# }}}
+
+class BookDetails(QWidget): # {{{
resized = pyqtSignal(object)
show_book_info = pyqtSignal()
@@ -234,20 +255,26 @@ class BookDetails(QWidget):
# }}}
- def __init__(self, parent=None):
+ def __init__(self, vertical, parent=None):
QWidget.__init__(self, parent)
+ self.setAcceptDrops(True)
self._layout = QVBoxLayout()
-
+ if not vertical:
+ self._layout.setDirection(self._layout.LeftToRight)
self.setLayout(self._layout)
- self.cover_view = CoverView(self)
+
+ self.cover_view = CoverView(vertical, self)
self.cover_view.relayout(self.size())
self.resized.connect(self.cover_view.relayout, type=Qt.QueuedConnection)
- self._layout.addWidget(self.cover_view, alignment=Qt.AlignHCenter)
- self.book_info = BookInfo(self)
+ self._layout.addWidget(self.cover_view)
+ self.book_info = BookInfo(vertical, self)
self._layout.addWidget(self.book_info)
self.book_info.link_clicked.connect(self._link_clicked)
self.book_info.mr.connect(self.mouseReleaseEvent)
- self.setMinimumSize(QSize(190, 200))
+ if vertical:
+ self.setMinimumSize(QSize(190, 200))
+ else:
+ self.setMinimumSize(120, 120)
self.setCursor(Qt.PointingHandCursor)
def _link_clicked(self, link):
@@ -258,8 +285,7 @@ class BookDetails(QWidget):
id_, fmt = val.split(':')
self.view_specific_format.emit(int(id_), fmt)
elif typ == 'devpath':
- path = os.path.dirname(val)
- QDesktopServices.openUrl(QUrl.fromLocalFile(path))
+ QDesktopServices.openUrl(QUrl.fromLocalFile(val))
def mouseReleaseEvent(self, ev):
@@ -275,10 +301,8 @@ class BookDetails(QWidget):
self.setToolTip(''+_('Click to open Book Details window') +
'
' + _('Path') + ': ' + data.get(_('Path'), ''))
-
-
def reset_info(self):
self.show_data({})
-
+# }}}
diff --git a/src/calibre/gui2/dialogs/config/config.ui b/src/calibre/gui2/dialogs/config/config.ui
index ba92c0d301..efda00fc97 100644
--- a/src/calibre/gui2/dialogs/config/config.ui
+++ b/src/calibre/gui2/dialogs/config/config.ui
@@ -7,7 +7,7 @@
0
0
- 884
+ 1000
730
@@ -89,7 +89,7 @@
0
0
- 604
+ 720
679
@@ -370,7 +370,7 @@
- -
+
-
Show &average ratings in the tags browser
diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py
index 1277cb06c7..0a82d3b75b 100644
--- a/src/calibre/gui2/init.py
+++ b/src/calibre/gui2/init.py
@@ -8,17 +8,18 @@ __docformat__ = 'restructuredtext en'
import functools
from PyQt4.Qt import QMenu, Qt, pyqtSignal, QToolButton, QIcon, QStackedWidget, \
- QWidget, QHBoxLayout, QToolBar, QSize, QSizePolicy
+ QSize, QSizePolicy, QStatusBar
from calibre.utils.config import prefs
from calibre.ebooks import BOOK_EXTENSIONS
-from calibre.constants import isosx, __appname__
+from calibre.constants import isosx, __appname__, preferred_encoding
from calibre.gui2 import config, is_widescreen
from calibre.gui2.library.views import BooksView, DeviceBooksView
from calibre.gui2.widgets import Splitter
from calibre.gui2.tag_view import TagBrowserWidget
-from calibre.gui2.status import StatusBar, HStatusBar
from calibre.gui2.book_details import BookDetails
+from calibre.gui2.notify import get_notifier
+
_keep_refs = []
@@ -332,26 +333,24 @@ class Stack(QStackedWidget): # {{{
# }}}
-class SideBar(QToolBar): # {{{
+class StatusBar(QStatusBar): # {{{
+ def initialize(self, systray=None):
+ self.systray = systray
+ self.notifier = get_notifier(systray)
- def __init__(self, splitters, jobs_button, parent=None):
- QToolBar.__init__(self, _('Side bar'), parent)
- self.setOrientation(Qt.Vertical)
- self.setMovable(False)
- self.setFloatable(False)
- self.setToolButtonStyle(Qt.ToolButtonIconOnly)
- self.setIconSize(QSize(48, 48))
- self.spacer = QWidget(self)
- self.spacer.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding)
- for s in splitters:
- self.addWidget(s.button)
- self.addWidget(self.spacer)
- self.addWidget(jobs_button)
+ def show_message(self, msg, timeout=0):
+ QStatusBar.showMessage(self, msg, timeout)
+ if self.notifier is not None and not config['disable_tray_notification']:
+ if isosx and isinstance(msg, unicode):
+ try:
+ msg = msg.encode(preferred_encoding)
+ except UnicodeEncodeError:
+ msg = msg.encode('utf-8')
+ self.notifier(msg)
- for ch in self.children():
- if isinstance(ch, QToolButton):
- ch.setCursor(Qt.PointingHandCursor)
+ def clear_message(self):
+ QStatusBar.clearMessage(self)
# }}}
@@ -361,45 +360,46 @@ class LayoutMixin(object): # {{{
self.setupUi(self)
self.setWindowTitle(__appname__)
- if config['gui_layout'] == 'narrow':
- self.status_bar = self.book_details = StatusBar(self)
+ if config['gui_layout'] == 'narrow': # narrow {{{
+ self.book_details = BookDetails(False, self)
self.stack = Stack(self)
self.bd_splitter = Splitter('book_details_splitter',
_('Book Details'), I('book.svg'),
orientation=Qt.Vertical, parent=self, side_index=1)
- self._layout_mem = [QWidget(self), QHBoxLayout()]
- self._layout_mem[0].setLayout(self._layout_mem[1])
- l = self._layout_mem[1]
- l.addWidget(self.stack)
- self.sidebar = SideBar([getattr(self, x+'_splitter')
- for x in ('bd', 'tb', 'cb')], self.jobs_button, parent=self)
- l.addWidget(self.sidebar)
- self.bd_splitter.addWidget(self._layout_mem[0])
- self.bd_splitter.addWidget(self.status_bar)
+ self.bd_splitter.addWidget(self.stack)
+ self.bd_splitter.addWidget(self.book_details)
self.bd_splitter.setCollapsible(self.bd_splitter.other_index, False)
self.centralwidget.layout().addWidget(self.bd_splitter)
- else:
- self.status_bar = HStatusBar(self)
- self.setStatusBar(self.status_bar)
+ # }}}
+ 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)
self.stack = Stack(self)
self.bd_splitter.addWidget(self.stack)
- self.book_details = BookDetails(self)
+ self.book_details = BookDetails(True, self)
self.bd_splitter.addWidget(self.book_details)
self.bd_splitter.setCollapsible(self.bd_splitter.other_index, False)
self.bd_splitter.setSizePolicy(QSizePolicy(QSizePolicy.Expanding,
QSizePolicy.Expanding))
self.centralwidget.layout().addWidget(self.bd_splitter)
+ # }}}
- for x in ('cb', 'tb', 'bd'):
- button = getattr(self, x+'_splitter').button
- button.setIconSize(QSize(22, 22))
- self.status_bar.addPermanentWidget(button)
- self.status_bar.addPermanentWidget(self.jobs_button)
+ self.status_bar = StatusBar(self)
+ for x in ('cb', 'tb', 'bd'):
+ button = getattr(self, x+'_splitter').button
+ button.setIconSize(QSize(24, 24))
+ self.status_bar.addPermanentWidget(button)
+ self.status_bar.addPermanentWidget(self.jobs_button)
+ self.setStatusBar(self.status_bar)
def finalize_layout(self):
+ self.status_bar.initialize(self.system_tray_icon)
+ self.book_details.show_book_info.connect(self.show_book_info)
+ self.book_details.files_dropped.connect(self.files_dropped_on_book)
+ self.book_details.open_containing_folder.connect(self.view_folder_for_id)
+ self.book_details.view_specific_format.connect(self.view_format_by_id)
+
m = self.library_view.model()
if m.rowCount(None) > 0:
self.library_view.set_current_row(0)
diff --git a/src/calibre/gui2/status.py b/src/calibre/gui2/status.py
deleted file mode 100644
index 90426f8021..0000000000
--- a/src/calibre/gui2/status.py
+++ /dev/null
@@ -1,254 +0,0 @@
-__license__ = 'GPL v3'
-__copyright__ = '2008, Kovid Goyal '
-
-import os
-
-from PyQt4.Qt import QStatusBar, QLabel, QWidget, QHBoxLayout, QPixmap, \
- QSizePolicy, QScrollArea, Qt, QSize, pyqtSignal, \
- QPropertyAnimation, QEasingCurve, QDesktopServices, QUrl
-
-
-from calibre import fit_image, preferred_encoding, isosx
-from calibre.gui2 import config
-from calibre.gui2.widgets import IMAGE_EXTENSIONS
-from calibre.gui2.notify import get_notifier
-from calibre.ebooks import BOOK_EXTENSIONS
-from calibre.library.comments import comments_to_html
-from calibre.gui2.book_details import render_rows
-
-class BookInfoDisplay(QWidget):
-
- DROPABBLE_EXTENSIONS = IMAGE_EXTENSIONS+BOOK_EXTENSIONS
- files_dropped = pyqtSignal(object, object)
-
- @classmethod
- def paths_from_event(cls, event):
- '''
- Accept a drop event and return a list of paths that can be read from
- and represent files with extensions.
- '''
- if event.mimeData().hasFormat('text/uri-list'):
- urls = [unicode(u.toLocalFile()) for u in event.mimeData().urls()]
- urls = [u for u in urls if os.path.splitext(u)[1] and os.access(u, os.R_OK)]
- return [u for u in urls if os.path.splitext(u)[1][1:].lower() in cls.DROPABBLE_EXTENSIONS]
-
- def dragEnterEvent(self, event):
- if int(event.possibleActions() & Qt.CopyAction) + \
- int(event.possibleActions() & Qt.MoveAction) == 0:
- return
- paths = self.paths_from_event(event)
- if paths:
- event.acceptProposedAction()
-
- def dropEvent(self, event):
- paths = self.paths_from_event(event)
- event.setDropAction(Qt.CopyAction)
- self.files_dropped.emit(event, paths)
-
- def dragMoveEvent(self, event):
- event.acceptProposedAction()
-
-
- class BookCoverDisplay(QLabel): # {{{
-
- def __init__(self, coverpath=I('book.svg')):
- QLabel.__init__(self)
- self.animation = QPropertyAnimation(self, 'size', self)
- self.animation.setEasingCurve(QEasingCurve(QEasingCurve.OutExpo))
- self.animation.setDuration(1000)
- self.animation.setStartValue(QSize(0, 0))
- self.setMaximumWidth(81)
- self.setMaximumHeight(108)
- self.default_pixmap = QPixmap(coverpath)
- self.setScaledContents(True)
- self.statusbar_height = 120
- self.setPixmap(self.default_pixmap)
-
- def do_layout(self):
- self.animation.stop()
- pixmap = self.pixmap()
- pwidth, pheight = pixmap.width(), pixmap.height()
- width, height = fit_image(pwidth, pheight,
- pwidth, self.statusbar_height-20)[1:]
- self.setMaximumHeight(height)
- try:
- aspect_ratio = pwidth/float(pheight)
- except ZeroDivisionError:
- aspect_ratio = 1
- self.setMaximumWidth(int(aspect_ratio*self.maximumHeight()))
- self.animation.setEndValue(self.maximumSize())
-
- def setPixmap(self, pixmap):
- QLabel.setPixmap(self, pixmap)
- self.do_layout()
- self.animation.start()
-
- def sizeHint(self):
- return QSize(self.maximumWidth(), self.maximumHeight())
-
- def relayout(self, statusbar_size):
- self.statusbar_height = statusbar_size.height()
- self.do_layout()
-
- # }}}
-
- class BookDataDisplay(QLabel):
-
- mr = pyqtSignal(object)
- link_clicked = pyqtSignal(object)
-
- def __init__(self):
- QLabel.__init__(self)
- self.setText('')
- self.setWordWrap(True)
- self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding))
- self.linkActivated.connect(self.link_activated)
- self._link_clicked = False
-
- def mouseReleaseEvent(self, ev):
- QLabel.mouseReleaseEvent(self, ev)
- if not self._link_clicked:
- self.mr.emit(ev)
- self._link_clicked = False
-
- def link_activated(self, link):
- self._link_clicked = True
- link = unicode(link)
- self.link_clicked.emit(link)
-
- show_book_info = pyqtSignal()
-
- def __init__(self, clear_message):
- QWidget.__init__(self)
- self.setCursor(Qt.PointingHandCursor)
- self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding))
- self._layout = QHBoxLayout()
- self.setLayout(self._layout)
- self.clear_message = clear_message
- self.cover_display = BookInfoDisplay.BookCoverDisplay()
- self._layout.addWidget(self.cover_display)
- self.book_data = BookInfoDisplay.BookDataDisplay()
- self.book_data.mr.connect(self.mouseReleaseEvent)
- self._layout.addWidget(self.book_data)
- self.data = {}
- self.setVisible(False)
- self._layout.setAlignment(self.cover_display, Qt.AlignTop|Qt.AlignLeft)
-
- def mouseReleaseEvent(self, ev):
- ev.accept()
- self.show_book_info.emit()
-
- def show_data(self, data):
- if data.has_key('cover'):
- self.cover_display.setPixmap(QPixmap.fromImage(data.pop('cover')))
- else:
- self.cover_display.setPixmap(self.cover_display.default_pixmap)
-
- rows, comments = [], ''
- self.book_data.setText('')
- self.data = data.copy()
- rows = render_rows(self.data)
- rows = '\n'.join([u'
%s: | %s |
'%(k,t) for
- k, t in rows])
- if _('Comments') in self.data:
- comments = comments_to_html(self.data[_('Comments')])
- comments = ('%s:'%_('Comments'))+comments
- left_pane = u''%rows
- right_pane = u'%s
'%comments
- self.book_data.setText(u''
- % (left_pane, right_pane))
-
- self.clear_message()
- self.book_data.updateGeometry()
- self.updateGeometry()
- self.setVisible(True)
- self.setToolTip(''+_('Click to open Book Details window') +
- '
' + _('Path') + ': ' + data.get(_('Path'), ''))
-
-
-
-class StatusBarInterface(object):
-
- def initialize(self, systray=None):
- self.systray = systray
- self.notifier = get_notifier(systray)
-
- def show_message(self, msg, timeout=0):
- QStatusBar.showMessage(self, msg, timeout)
- if self.notifier is not None and not config['disable_tray_notification']:
- if isosx and isinstance(msg, unicode):
- try:
- msg = msg.encode(preferred_encoding)
- except UnicodeEncodeError:
- msg = msg.encode('utf-8')
- self.notifier(msg)
-
- def clear_message(self):
- QStatusBar.clearMessage(self)
-
-class BookDetailsInterface(object):
-
- # These signals must be defined in the class implementing this interface
- files_dropped = None
- show_book_info = None
- open_containing_folder = None
- view_specific_format = None
-
- def reset_info(self):
- raise NotImplementedError()
-
- def show_data(self, data):
- raise NotImplementedError()
-
-class HStatusBar(QStatusBar, StatusBarInterface):
- pass
-
-class StatusBar(QStatusBar, StatusBarInterface, BookDetailsInterface):
-
- files_dropped = pyqtSignal(object, object)
- show_book_info = pyqtSignal()
- open_containing_folder = pyqtSignal(int)
- view_specific_format = pyqtSignal(int, object)
-
- resized = pyqtSignal(object)
-
- def initialize(self, systray=None):
- StatusBarInterface.initialize(self, systray=systray)
- self.book_info = BookInfoDisplay(self.clear_message)
- self.book_info.setAcceptDrops(True)
- self.scroll_area = QScrollArea()
- self.scroll_area.setWidget(self.book_info)
- self.scroll_area.setWidgetResizable(True)
- self.book_info.show_book_info.connect(self.show_book_info.emit,
- type=Qt.QueuedConnection)
- self.book_info.files_dropped.connect(self.files_dropped.emit,
- type=Qt.QueuedConnection)
- self.book_info.book_data.link_clicked.connect(self._link_clicked)
- self.addWidget(self.scroll_area, 100)
- self.setMinimumHeight(120)
- self.resized.connect(self.book_info.cover_display.relayout)
- self.book_info.cover_display.relayout(self.size())
-
-
- def _link_clicked(self, link):
- typ, _, val = link.partition(':')
- if typ == 'path':
- self.open_containing_folder.emit(int(val))
- elif typ == 'format':
- id_, fmt = val.split(':')
- self.view_specific_format.emit(int(id_), fmt)
- elif typ == 'devpath':
- path = os.path.dirname(val)
- QDesktopServices.openUrl(QUrl.fromLocalFile(path))
-
-
- def resizeEvent(self, ev):
- self.resized.emit(self.size())
-
- def reset_info(self):
- self.book_info.show_data({})
-
- def show_data(self, data):
- self.book_info.show_data(data)
-
diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py
index 9cc90ca83f..daea4e86ea 100644
--- a/src/calibre/gui2/tag_view.py
+++ b/src/calibre/gui2/tag_view.py
@@ -10,14 +10,13 @@ Browsing book collection by tags.
from itertools import izip
from functools import partial
-from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QCheckBox, \
+from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
QFont, QSize, QIcon, QPoint, QVBoxLayout, QComboBox, \
QAbstractItemModel, QVariant, QModelIndex, QMenu, \
- QPushButton, QWidget, QItemDelegate, QString, QPen, \
- QColor, QLinearGradient, QBrush
+ QPushButton, QWidget, QItemDelegate
from calibre.gui2 import config, NONE
-from calibre.utils.config import prefs, tweaks
+from calibre.utils.config import prefs
from calibre.library.field_metadata import TagsIcons
from calibre.utils.search_query_parser import saved_searches
from calibre.gui2 import error_dialog
@@ -25,12 +24,7 @@ from calibre.gui2.dialogs.tag_categories import TagCategories
from calibre.gui2.dialogs.tag_list_editor import TagListEditor
from calibre.gui2.dialogs.edit_authors_dialog import EditAuthorsDialog
-class TagDelegate(QItemDelegate):
-
- def __init__(self, parent):
- QItemDelegate.__init__(self, parent)
- self._parent = parent
- self.icon = QIcon(I('star.png'))
+class TagDelegate(QItemDelegate): # {{{
def paint(self, painter, option, index):
item = index.internalPointer()
@@ -38,51 +32,29 @@ class TagDelegate(QItemDelegate):
QItemDelegate.paint(self, painter, option, index)
return
r = option.rect
- # Paint the decoration icon
- icon = self._parent.model().data(index, Qt.DecorationRole).toPyObject()
- icon.paint(painter, r, Qt.AlignLeft)
+ model = self.parent().model()
+ icon = model.data(index, Qt.DecorationRole).toPyObject()
+ painter.save()
+ if item.tag.state != 0 or not config['show_avg_rating'] or \
+ item.tag.avg_rating is None:
+ icon.paint(painter, r, Qt.AlignLeft)
+ else:
+ painter.setOpacity(0.3)
+ icon.paint(painter, r, Qt.AlignLeft)
+ painter.setOpacity(1)
+ rating = item.tag.avg_rating
+ painter.setClipRect(r.left(), r.bottom()-int(r.height()*(rating/5.0)),
+ r.width(), r.height())
+ icon.paint(painter, r, Qt.AlignLeft)
+ painter.setClipRect(r)
- # Paint the rating, if any. The decoration icon is assumed to be square,
- # filling the row top to bottom. The three is arbitrary, there to
- # provide a little space between the icon and what follows
- r.setLeft(r.left()+r.height()+3)
- rating = item.tag.avg_rating
- if config['show_avg_rating'] and item.tag.avg_rating is not None:
- painter.save()
- if tweaks['render_avg_rating_using'] == 'star':
- painter.setClipRect(r.left(), r.top(),
- int(r.height()*(rating/5.0)), r.height())
- self.icon.paint(painter, r, Qt.AlignLeft | Qt.AlignVCenter)
- r.setLeft(r.left() + r.height())
- else:
- painter.translate(r.left(), r.top())
- # Compute factor so sizes can be expressed in percentages of the
- # box defined by the row height
- factor = r.height()/100.
- width = 20
- height = 80
- left_offset = 5
- top_offset = 10
- if r > 0.0:
- color = QColor(100, 100, 255) #medium blue, less glare
- pen = QPen(color, 5, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
- painter.setPen(pen)
- painter.scale(factor, factor)
- painter.drawRect(left_offset, top_offset, width, height)
- fill_height = height*(rating/5.0)
- gradient = QLinearGradient(0, 0, 0, 100)
- gradient.setColorAt(0.0, color)
- gradient.setColorAt(1.0, color)
- painter.setBrush(QBrush(gradient))
- painter.drawRect(left_offset, top_offset+(height-fill_height),
- width, fill_height)
- # The '3' is arbitrary, there because we need a little space
- # between the rectangle and the text.
- r.setLeft(r.left() + ((width+left_offset*2)*factor) + 3)
- painter.restore()
# Paint the text
+ r.setLeft(r.left()+r.height()+3)
painter.drawText(r, Qt.AlignLeft|Qt.AlignVCenter,
- QString('[%d] %s'%(item.tag.count, item.tag.name)))
+ model.data(index, Qt.DisplayRole).toString())
+ painter.restore()
+
+ # }}}
class TagsView(QTreeView): # {{{
@@ -107,12 +79,12 @@ class TagsView(QTreeView): # {{{
self.setHeaderHidden(True)
self.setItemDelegate(TagDelegate(self))
- def set_database(self, db, tag_match, popularity):
+ def set_database(self, db, tag_match, sort_by):
self.hidden_categories = config['tag_browser_hidden_categories']
self._model = TagsModel(db, parent=self,
hidden_categories=self.hidden_categories,
search_restriction=None)
- self.popularity = popularity
+ self.sort_by = sort_by
self.tag_match = tag_match
self.db = db
self.search_restriction = None
@@ -120,8 +92,9 @@ class TagsView(QTreeView): # {{{
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.clicked.connect(self.toggle)
self.customContextMenuRequested.connect(self.show_context_menu)
- self.popularity.setChecked(config['sort_by_popularity'])
- self.popularity.stateChanged.connect(self.sort_changed)
+ pop = config['sort_tags_by']
+ self.sort_by.setCurrentIndex(self.db.CATEGORY_SORTS.index(pop))
+ self.sort_by.currentIndexChanged.connect(self.sort_changed)
self.refresh_required.connect(self.recount, type=Qt.QueuedConnection)
db.add_listener(self.database_changed)
@@ -132,8 +105,8 @@ class TagsView(QTreeView): # {{{
def match_all(self):
return self.tag_match and self.tag_match.currentIndex() > 0
- def sort_changed(self, state):
- config.set('sort_by_popularity', state == Qt.Checked)
+ def sort_changed(self, pop):
+ config.set('sort_tags_by', self.db.CATEGORY_SORTS[pop])
self.recount()
def set_search_restriction(self, s):
@@ -372,11 +345,7 @@ class TagTreeItem(object): # {{{
if self.tag.count == 0:
return QVariant('%s'%(self.tag.name))
else:
- if self.tag.avg_rating is None:
- return QVariant('[%d] %s'%(self.tag.count, self.tag.name))
- else:
- return QVariant('[%d][%3.1f] %s'%(self.tag.count,
- self.tag.avg_rating, self.tag.name))
+ return QVariant('[%d] %s'%(self.tag.count, self.tag.name))
if role == Qt.EditRole:
return QVariant(self.tag.name)
if role == Qt.DecorationRole:
@@ -420,7 +389,7 @@ class TagsModel(QAbstractItemModel): # {{{
self.row_map = []
# get_node_tree cannot return None here, because row_map is empty
- data = self.get_node_tree(config['sort_by_popularity'])
+ data = self.get_node_tree(config['sort_tags_by'])
self.root_item = TagTreeItem()
for i, r in enumerate(self.row_map):
if self.hidden_categories and self.categories[i] in self.hidden_categories:
@@ -464,11 +433,11 @@ class TagsModel(QAbstractItemModel): # {{{
# Now get the categories
if self.search_restriction:
- data = self.db.get_categories(sort_on_count=sort,
+ data = self.db.get_categories(sort=sort,
icon_map=self.category_icon_map,
ids=self.db.search('', return_matches=True))
else:
- data = self.db.get_categories(sort_on_count=sort, icon_map=self.category_icon_map)
+ data = self.db.get_categories(sort=sort, icon_map=self.category_icon_map)
tb_categories = self.db.field_metadata
for category in tb_categories:
@@ -482,7 +451,7 @@ class TagsModel(QAbstractItemModel): # {{{
return data
def refresh(self):
- data = self.get_node_tree(config['sort_by_popularity']) # get category data
+ data = self.get_node_tree(config['sort_tags_by']) # get category data
if data is None:
return False
row_index = -1
@@ -691,7 +660,7 @@ class TagBrowserMixin(object): # {{{
def __init__(self, db):
self.library_view.model().count_changed_signal.connect(self.tags_view.recount)
self.tags_view.set_database(self.library_view.model().db,
- self.tag_match, self.popularity)
+ self.tag_match, self.sort_by)
self.tags_view.tags_marked.connect(self.search.search_from_tags)
self.tags_view.tags_marked.connect(self.saved_search.clear_to_help)
self.tags_view.tag_list_edit.connect(self.do_tags_list_edit)
@@ -752,9 +721,13 @@ class TagBrowserWidget(QWidget): # {{{
parent.tags_view = TagsView(parent)
self._layout.addWidget(parent.tags_view)
- parent.popularity = QCheckBox(parent)
- parent.popularity.setText(_('Sort by &popularity'))
- self._layout.addWidget(parent.popularity)
+ parent.sort_by = QComboBox(parent)
+ # Must be in the same order as db2.CATEGORY_SORTS
+ for x in (_('Sort by name'), _('Sort by popularity'),
+ _('Sort by average rating')):
+ parent.sort_by.addItem(x)
+ parent.sort_by.setCurrentIndex(0)
+ self._layout.addWidget(parent.sort_by)
parent.tag_match = QComboBox(parent)
for x in (_('Match any'), _('Match all')):
diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py
index 383c67a773..aa2d94a637 100644
--- a/src/calibre/gui2/ui.py
+++ b/src/calibre/gui2/ui.py
@@ -126,8 +126,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
# Jobs Button {{{
self.job_manager = JobManager()
self.jobs_dialog = JobsDialog(self, self.job_manager)
- self.jobs_button = JobsButton(horizontal=config['gui_layout'] !=
- 'narrow')
+ self.jobs_button = JobsButton(horizontal=True)
self.jobs_button.initialize(self.jobs_dialog, self.job_manager)
# }}}
@@ -216,12 +215,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
self.vanity.setText(self.vanity_template%dict(version=' ', device=' '))
self.device_info = ' '
UpdateMixin.__init__(self, opts)
- ####################### Status Bar #####################
- self.status_bar.initialize(self.system_tray_icon)
- self.book_details.show_book_info.connect(self.show_book_info)
- self.book_details.files_dropped.connect(self.files_dropped_on_book)
- self.book_details.open_containing_folder.connect(self.view_folder_for_id)
- self.book_details.view_specific_format.connect(self.view_format_by_id)
####################### Setup Toolbar #####################
ToolbarMixin.__init__(self)
@@ -430,7 +423,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
self.book_on_device(None, reset=True)
db.set_book_on_device_func(self.book_on_device)
self.library_view.set_database(db)
- self.tags_view.set_database(db, self.tag_match, self.popularity)
+ self.tags_view.set_database(db, self.tag_match, self.sort_by)
self.library_view.model().set_book_on_device_func(self.book_on_device)
self.status_bar.clear_message()
self.search.clear_to_help()
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index 2fb22a27f4..c7830187df 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -64,6 +64,10 @@ class Tag(object):
self.state = state
self.avg_rating = avg/2.0 if avg is not None else 0
self.sort = sort
+ if self.avg_rating > 0:
+ if tooltip:
+ tooltip = tooltip + ': '
+ tooltip = _('%sAverage rating is %3.1f')%(tooltip, self.avg_rating)
self.tooltip = tooltip
self.icon = icon
@@ -429,7 +433,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if aum: aum = [a.strip().replace('|', ',') for a in aum.split(',')]
mi = MetaInformation(self.title(idx, index_is_id=index_is_id), aum)
mi.author_sort = self.author_sort(idx, index_is_id=index_is_id)
- mi.authors_sort_strings = self.authors_sort_strings(idx, index_is_id)
+ if mi.authors:
+ mi.author_sort_map = {}
+ for name, sort in zip(mi.authors, self.authors_sort_strings(idx,
+ index_is_id)):
+ mi.author_sort_map[name] = sort
mi.comments = self.comments(idx, index_is_id=index_is_id)
mi.publisher = self.publisher(idx, index_is_id=index_is_id)
mi.timestamp = self.timestamp(idx, index_is_id=index_is_id)
@@ -687,7 +695,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
tn=field['table'], col=field['link_column']), (id_,))
return set(x[0] for x in ans)
- def get_categories(self, sort_on_count=False, ids=None, icon_map=None):
+ CATEGORY_SORTS = ('name', 'popularity', 'rating')
+
+ def get_categories(self, sort='name', ids=None, icon_map=None):
self.books_list_filter.change([] if not ids else ids)
categories = {}
@@ -711,10 +721,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
else:
query = '''SELECT id, {0}, count, avg_rating, sort
FROM tag_browser_filtered_{1}'''.format(cn, tn)
- if sort_on_count:
- query += ' ORDER BY count DESC'
- else:
+ if sort == 'popularity':
+ query += ' ORDER BY count DESC, sort ASC'
+ elif sort == 'name':
query += ' ORDER BY sort ASC'
+ else:
+ query += ' ORDER BY avg_rating DESC, sort ASC'
data = self.conn.get(query)
# icon_map is not None if get_categories is to store an icon and
@@ -770,11 +782,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if count > 0:
categories['formats'].append(Tag(fmt, count=count, icon=icon))
- if sort_on_count:
- categories['formats'].sort(cmp=lambda x,y:cmp(x.count, y.count),
- reverse=True)
- else:
- categories['formats'].sort(cmp=lambda x,y:cmp(x.name, y.name))
+ if sort == 'popularity':
+ categories['formats'].sort(key=lambda x: x.count, reverse=True)
+ else: # no ratings exist to sort on
+ categories['formats'].sort(key = lambda x:x.name)
#### Now do the user-defined categories. ####
user_categories = prefs['user_categories']
@@ -799,12 +810,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# Not a problem if we accumulate entries in the icon map
if icon_map is not None:
icon_map[cat_name] = icon_map[':user']
- if sort_on_count:
+ if sort == 'popularity':
categories[cat_name] = \
- sorted(items, cmp=(lambda x, y: cmp(y.count, x.count)))
+ sorted(items, key=lambda x: x.count, reverse=True)
+ elif sort == 'name':
+ categories[cat_name] = \
+ sorted(items, key=lambda x: x.sort.lower())
else:
categories[cat_name] = \
- sorted(items, cmp=(lambda x, y: cmp(x.name.lower(), y.name.lower())))
+ sorted(items, key=lambda x:x.avg_rating, reverse=True)
#### Finally, the saved searches category ####
items = []
diff --git a/src/calibre/library/server/opds.py b/src/calibre/library/server/opds.py
index d396d73af2..7b8d609dda 100644
--- a/src/calibre/library/server/opds.py
+++ b/src/calibre/library/server/opds.py
@@ -99,17 +99,20 @@ def html_to_lxml(raw):
raw = etree.tostring(root, encoding=None)
return etree.fromstring(raw)
-def CATALOG_ENTRY(item, base_href, version, updated):
+def CATALOG_ENTRY(item, base_href, version, updated, ignore_count=False):
id_ = 'calibre:category:'+item.name
iid = 'N' + item.name
if item.id is not None:
iid = 'I' + str(item.id)
link = NAVLINK(href = base_href + '/' + hexlify(iid))
+ count = _('%d books')%item.count
+ if ignore_count:
+ count = ''
return E.entry(
TITLE(item.name),
ID(id_),
UPDATED(updated),
- E.content(_('%d books')%item.count, type='text'),
+ E.content(count, type='text'),
link
)
@@ -265,8 +268,12 @@ class CategoryFeed(NavFeed):
def __init__(self, items, which, id_, updated, version, offsets, page_url, up_url):
NavFeed.__init__(self, id_, updated, version, offsets, page_url, up_url)
base_href = self.base_href + '/category/' + hexlify(which)
+ ignore_count = False
+ if which == 'search':
+ ignore_count = True
for item in items:
- self.root.append(CATALOG_ENTRY(item, base_href, version, updated))
+ self.root.append(CATALOG_ENTRY(item, base_href, version, updated,
+ ignore_count=ignore_count))
class CategoryGroupFeed(NavFeed):
@@ -393,7 +400,7 @@ class OPDSServer(object):
owhich = hexlify('N'+which)
up_url = url_for('opdsnavcatalog', version, which=owhich)
items = categories[category]
- items = [x for x in items if x.name.startswith(which)]
+ items = [x for x in items if getattr(x, 'sort', x.name).startswith(which)]
if not items:
raise cherrypy.HTTPError(404, 'No items in group %r:%r'%(category,
which))
@@ -458,11 +465,11 @@ class OPDSServer(object):
def __init__(self, text, count):
self.text, self.count = text, count
- starts = set([x.name[0] for x in items])
+ starts = set([getattr(x, 'sort', x.name)[0] for x in items])
category_groups = OrderedDict()
for x in sorted(starts, cmp=lambda x,y:cmp(x.lower(), y.lower())):
category_groups[x] = len([y for y in items if
- y.name.startswith(x)])
+ getattr(y, 'sort', y.name).startswith(x)])
items = [Group(x, y) for x, y in category_groups.items()]
max_items = self.opts.max_opds_items
offsets = OPDSOffsets(offset, max_items, len(items))
diff --git a/src/calibre/utils/localization.py b/src/calibre/utils/localization.py
index 026547ee2e..e60a3233c6 100644
--- a/src/calibre/utils/localization.py
+++ b/src/calibre/utils/localization.py
@@ -103,6 +103,7 @@ _extra_lang_codes = {
'en_TH' : _('English (Thailand)'),
'en_CY' : _('English (Cyprus)'),
'en_PK' : _('English (Pakistan)'),
+ 'en_IL' : _('English (Israel)'),
'en_SG' : _('English (Singapore)'),
'en_YE' : _('English (Yemen)'),
'en_IE' : _('English (Ireland)'),