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''' + + + + + + + + '''.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'{0}'.format( + rstring, x)) + added += 1 + ans.append(''%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 = '

    {0}

    '\ - .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 = '

    {0}

    '\ + .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''' +
    +

    {0}

    +

    [{2} ↑] +

    + {1} +
    + '''.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: