diff --git a/resources/content_server/browse/browse.css b/resources/content_server/browse/browse.css
index b4256f974b..39d1d86241 100644
--- a/resources/content_server/browse/browse.css
+++ b/resources/content_server/browse/browse.css
@@ -82,9 +82,9 @@ body {
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
text-shadow: #27211b 1px 1px 1px;
- -moz-box-shadow: 5px 5px 5px #222;
- -webkit-box-shadow: 5px 5px 5px #222;
- box-shadow: 5px 5px 5px #222;
+ -moz-box-shadow: 5px 5px 5px #222;
+ -webkit-box-shadow: 5px 5px 5px #222;
+ box-shadow: 5px 5px 5px #222;
}
@@ -220,3 +220,32 @@ h2.library_name {
/* }}} */
+/* Category {{{ */
+.category ul {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+}
+
+.category li.category-item {
+ margin: 0.75em;
+ padding: 0.75em;
+ text-align: center;
+ cursor: pointer;
+}
+
+.category li.category-item:hover {
+ background-color: #d6d3c9;
+ font-weight: bold;
+ -moz-box-shadow: 5px 5px 5px #ccc;
+ -webkit-box-shadow: 5px 5px 5px #ccc;
+ box-shadow: 5px 5px 5px #ccc;
+
+}
+
+.category li.category-item h4 { display: inline }
+.category li.category-item span.href { display: none }
+
+/* }}} */
+
+
diff --git a/resources/content_server/browse/browse.html b/resources/content_server/browse/browse.html
index 9ae37e5c43..1258088741 100644
--- a/resources/content_server/browse/browse.html
+++ b/resources/content_server/browse/browse.html
@@ -15,7 +15,10 @@
-
+
+
+
diff --git a/resources/content_server/browse/browse.js b/resources/content_server/browse/browse.js
index e2a8dc934a..d534ea15c1 100644
--- a/resources/content_server/browse/browse.js
+++ b/resources/content_server/browse/browse.js
@@ -127,3 +127,16 @@ function toplevel() {
});
}
// }}}
+
+// Category feed {{{
+function category() {
+ $(".category li").corner("15px");
+
+ $(".category li").click(function() {
+ var href = $(this).children("span.href").html();
+ window.location = href;
+ });
+}
+// }}}
+
+
diff --git a/resources/content_server/star-half.png b/resources/content_server/star-half.png
new file mode 100644
index 0000000000..3c19e90a8a
Binary files /dev/null and b/resources/content_server/star-half.png differ
diff --git a/resources/content_server/star-off.png b/resources/content_server/star-off.png
new file mode 100644
index 0000000000..956fa7c637
Binary files /dev/null and b/resources/content_server/star-off.png differ
diff --git a/resources/content_server/star-on.png b/resources/content_server/star-on.png
new file mode 100644
index 0000000000..975fe7f323
Binary files /dev/null and b/resources/content_server/star-on.png differ
diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py
index e058ccd4f3..fc49a92025 100644
--- a/src/calibre/library/server/browse.py
+++ b/src/calibre/library/server/browse.py
@@ -5,12 +5,122 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal '
__docformat__ = 'restructuredtext en'
-import operator, os
+import operator, os, sys
+from urllib import quote
+from binascii import hexlify
import cherrypy
from calibre.constants import filesystem_encoding
from calibre import isbytestring, force_unicode, prepare_string_for_xml as xml
+from calibre.library.server.utils import Offsets
+
+def paginate(offsets, content, base_url, up_url=None):
+ 'Create markup for pagination'
+
+ if '?' not in base_url:
+ base_url += '?'
+
+ if base_url[-1] != '?':
+ base_url += '&'
+
+ def navlink(decoration, name, cls, offset):
+ label = xml(name)
+ if cls in ('next', 'last'):
+ label += ' ' + decoration
+ else:
+ label = decoration + ' ' + label
+ return (u''
+ u'{label}').format(cls=cls, decoration=decoration,
+ name=xml(name, True), offset=offset,
+ base_url=xml(base_url, True), label=label)
+ left = ''
+ if offsets.offset > 0 and offsets.previous_offset > 0:
+ left += navlink(u'\u219e', _('First'), 'first', 0)
+ if offsets.offset > 0:
+ left += ' ' + navlink('←', _('Previous'), 'previous',
+ offsets.previous_offset)
+
+ middle = ''
+ if up_url:
+ middle = '[{1} ↑]'.format(xml(up_url, True),
+ xml(_('Up')))
+
+ right = ''
+ if offsets.next_offset > -1:
+ right += navlink('&rarr', _('Next'), 'next', offsets.next_offset)
+ if offsets.last_offset > offsets.next_offset and offsets.last_offset > 0:
+ right += ' ' + navlink(u'\u21A0', _('Last'), 'last', offsets.last_offset)
+
+ navbar = u'''
+
+
+ {left} |
+ {middle} |
+ {right} |
+
+
+ '''.format(left=left, right=right, middle=middle)
+
+ templ = u'''
+
+ {navbar}
+
+ {content}
+
+ {navbar}
+
+ '''
+ return templ.format(navbar=navbar, content=content)
+
+def utf8(x):
+ if isinstance(x, unicode):
+ x = x.encode('utf-8')
+ return x
+
+def render_rating(rating, container='span'):
+ if rating < 0.1:
+ return '', ''
+ added = 0
+ rstring = xml(_('Average rating: %.1f stars')% (rating if rating else 0.0),
+ True)
+ ans = ['<%s class="rating">' % (container)]
+ for i in range(5):
+ n = rating - added
+ x = 'half'
+ if n <= 0.1:
+ x = 'off'
+ elif n >= 0.9:
+ x = 'on'
+ ans.append(
+ u'
'.format(
+ rstring, x))
+ added += 1
+ ans.append('%s>'%container)
+ return u''.join(ans), rstring
+
+def get_category_items(category, items, offsets, db):
+
+ def item(i):
+ templ = (u''
+ '{0} {1}
{2}'
+ '{3}')
+ rating, rstring = render_rating(i.avg_rating)
+ name = xml(i.name)
+ id_ = i.id
+ if id_ is None:
+ id_ = hexlify(force_unicode(name).encode('utf-8'))
+ id_ = xml(str(id_))
+ desc = ''
+ if i.count > 0:
+ desc += '[' + _('%d items')%i.count + ']'
+ href = '/browse/matches/%s/%s'%(category, id_)
+ return templ.format(xml(name), rating,
+ xml(desc), xml(quote(href)), rstring)
+
+ items = list(map(item, items[offsets.offset:offsets.slice_upper_bound]))
+ return '\n'.join([''])
+
class BrowseServer(object):
@@ -23,7 +133,6 @@ class BrowseServer(object):
connect('browse_search', base_href+'/search/{query}',
self.browse_search)
connect('browse_book', base_href+'/book/{uuid}', self.browse_book)
- connect('browse_json', base_href+'/json/{query}', self.browse_json)
def browse_template(self, category=True):
@@ -36,7 +145,8 @@ class BrowseServer(object):
sort_opts = [(x, fm[x]['name']) for x in fm.sortable_field_keys()
if fm[x]['name']]
prefix = 'category' if category else 'book'
- ans = P('content_server/browse/browse.html', data=True)
+ ans = P('content_server/browse/browse.html',
+ data=True).decode('utf-8')
ans = ans.replace('{sort_select_label}', xml(_('Sort by')+':'))
opts = ['' % (prefix, xml(k),
xml(n)) for k, n in
@@ -61,50 +171,102 @@ class BrowseServer(object):
# Catalogs {{{
- def browse_catalog(self, category=None):
- if category == None:
- categories = self.categories_cache()
- category_meta = self.db.field_metadata
- cats = [
- (_('Newest'), 'newest'),
- ]
- def getter(x):
- return category_meta[x]['name'].lower()
- for category in sorted(categories,
- cmp=lambda x,y: cmp(getter(x), getter(y))):
- if len(categories[category]) == 0:
- continue
- if category == 'formats':
- continue
- meta = category_meta.get(category, None)
- if meta is None:
- continue
- cats.append((meta['name'], category))
- cats = ['{0}/browse/category/{1}'.format(xml(x, True),
- xml(y), xml(_('Browse books by'))) for x, y in cats]
+ def browse_toplevel(self):
+ categories = self.categories_cache()
+ category_meta = self.db.field_metadata
+ cats = [
+ (_('Newest'), 'newest'),
+ ]
- main = ''\
- .format(_('Choose a category to browse by:'), '\n\n'.join(cats))
- ans = self.browse_template().format(title='',
- script='toplevel();', main=main)
- else:
+ def getter(x):
+ return category_meta[x]['name'].lower()
+
+ for category in sorted(categories,
+ cmp=lambda x,y: cmp(getter(x), getter(y))):
+ if len(categories[category]) == 0:
+ continue
+ if category == 'formats':
+ continue
+ meta = category_meta.get(category, None)
+ if meta is None:
+ continue
+ cats.append((meta['name'], category))
+ cats = ['{0}/browse/category/{1}'\
+ .format(xml(x, True), xml(quote(y)), xml(_('Browse books by')))
+ for x, y in cats]
+
+ main = ''\
+ .format(_('Choose a category to browse by:'), '\n\n'.join(cats))
+ return self.browse_template().format(title='',
+ script='toplevel();', main=main)
+
+ def browse_category(self, category, offset, sort, subcategory=None):
+ categories = self.categories_cache()
+ category_meta = self.db.field_metadata
+ category_name = category_meta[category]['name']
+
+ if category not in categories:
+ raise cherrypy.HTTPError(404, 'category not found')
+
+ items = categories[category]
+
+ base_url='/browse/category/'+category+'?'
+ if subcategory is not None:
+ base_url += 'subcategory='+quote(subcategory)
+ if sort is not None:
+ base_url += 'sort='+quote(sort)
+
+ script = 'category();'
+
+ max_items = sys.maxint
+ offsets = Offsets(offset, max_items, len(items))
+ items = list(items)[offsets.offset:offsets.offset+max_items]
+ items = get_category_items(category, items, offsets, self.db)
+ main = u'''
+
+ '''.format(
+ xml(_('Browsing by')+': ' + category_name), items,
+ xml(_('Up'), True))
+
+ return self.browse_template().format(title=category_name,
+ script=script, main=main)
+
+
+
+ def browse_catalog(self, category=None, offset=0, sort=None,
+ subcategory=None):
+ 'Entry point for top-level, categories and sub-categories'
+ try:
+ offset = int(offset)
+ except:
raise cherrypy.HTTPError(404, 'Not found')
+ if category == None:
+ ans = self.browse_toplevel()
+ else:
+ ans = self.browse_category(category, offset, sort)
+
cherrypy.response.headers['Content-Type'] = 'text/html'
updated = self.db.last_modified()
cherrypy.response.headers['Last-Modified'] = \
self.last_modified(max(updated, self.build_time))
- return ans
+ return utf8(ans)
# }}}
# Book Lists {{{
- def browse_list(self, query=None):
+ def browse_list(self, query=None, offset=0, sort=None):
raise NotImplementedError()
# }}}
# Search {{{
- def browse_search(self, query=None):
+ def browse_search(self, query=None, offset=0, sort=None):
raise NotImplementedError()
# }}}
@@ -113,8 +275,4 @@ class BrowseServer(object):
raise NotImplementedError()
# }}}
- # JSON {{{
- def browse_json(self, query=None):
- raise NotImplementedError()
- # }}}
diff --git a/src/calibre/library/server/utils.py b/src/calibre/library/server/utils.py
index 4c286b555b..35c92f7ae2 100644
--- a/src/calibre/library/server/utils.py
+++ b/src/calibre/library/server/utils.py
@@ -23,6 +23,7 @@ class Offsets(object):
raise cherrypy.HTTPError(404, 'Invalid offset: %r'%offset)
last_allowed_index = total - 1
last_current_index = offset + delta - 1
+ self.slice_upper_bound = offset+delta
self.offset = offset
self.next_offset = last_current_index + 1
if self.next_offset > last_allowed_index: