From 2a8c2661a6fa64a3d1f2d8c40f25eb22ec85952c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 13 Oct 2010 12:54:20 -0600 Subject: [PATCH] Initial simple implementation of category view in new content server frontend --- resources/content_server/browse/browse.css | 35 ++- resources/content_server/browse/browse.html | 5 +- resources/content_server/browse/browse.js | 13 ++ resources/content_server/star-half.png | Bin 0 -> 667 bytes resources/content_server/star-off.png | Bin 0 -> 685 bytes resources/content_server/star-on.png | Bin 0 -> 631 bytes src/calibre/library/server/browse.py | 230 +++++++++++++++++--- src/calibre/library/server/utils.py | 1 + 8 files changed, 244 insertions(+), 40 deletions(-) create mode 100644 resources/content_server/star-half.png create mode 100644 resources/content_server/star-off.png create mode 100644 resources/content_server/star-on.png 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 0000000000000000000000000000000000000000..3c19e90a8a755e004424db205c8433f0f63048b4 GIT binary patch literal 667 zcmV;M0%ZM(P)5kxfVxVHn4M^Ulocu4}uCxq(JO)+Y9GD5Jat zbqNy5lR|3Ugd)g`R}H!Z-82#+xHAej^Uvb`)a1J$lTNk&_XNAE zBfTQ9v_v8Sa5IxmRs3@SYh=d-l+~gM@B>A4P0dc=P9~jd$fQ$sTUl-3B9(2Y;#Z9F ziv79~Bz4_Q3b9eCYwPF+d^xg&l#+bj;nT_rzOS%t8z2j`7`B7!=X2+jXB|TV=B8-| z5I_(F*tU&nN^1AkB4vn~=}AV%Mjv-|U3#ej40jVdS&P?6I*kPtxhy74l2*jJNmZ458vW9PWLT-hGu|=LysO@uBuA(2q79M zrYCoMu8-X7>n;ia!^1;Yo0`tX3%(WvK`H*P`T^?|->M&U?OXr=002ovPDHLkV1gTF BCtCmj literal 0 HcmV?d00001 diff --git a/resources/content_server/star-off.png b/resources/content_server/star-off.png new file mode 100644 index 0000000000000000000000000000000000000000..956fa7c637cddb4db6a091556cc63a0f6a186264 GIT binary patch literal 685 zcmV;e0#f~nP)R5*>Dl0QrnVHn1Ly)Enp;##exZh7()Uk zR1!C3adKe*!lVgta&se2n3x!5BLe|s!Nt*~X-#y7B3eoUik5~%+lD(Tz0ZL^0YjVU zv%Pu#yl=keg9gZ?Q_Q(|Z0g_5!WIoz~V%6X6SCq;x5j%6#A6o&%l(m4W2o+DSkrok{@j zfS9fu_%t7u6-EyR*{?cCDbX|y$Ei}>*}-*d*tQMu1GuLQBnNZ({Kj-B6r%H5Cv9zQ z1OftQ-v&TRiEZ1cs+abw?R0hbV45b`)$HqmfpE80>NxE=6Ci}>F?0hV0tABwPH{}X=zm$k-d TM0>cp00000NkvXXu0mjfLk}+u0K@`S+@BV*_sWrqlwFo8}(kQkaqzD;A z=u{lE-2?}_II1YR)S)U)E*26HCl}q?MW=Ld6)l=U=ui@ZMFoe7jcsaT?tgaXnG9{0ZQ-FLi$F8Gk$pICVsN_6A+-I}LP#xf!zP`KEQ*A|86(eYX?Vrxqg`*m?> zyc17#YQ&Emm{YDS9%UIE2;KXq0p(YRuVBj4qCoke?8ek~ZfK?*SF$toUz&(q^LzVF zc)m+{o=z$CtKX9fo)4nH88Gx=Z0LtIJw+2pF{o}Aa&I@D_|-bv)_zWzpnQfNU|%D| z2ONlib6`yct1JCRlQx>P4J2Q!lkr}!P)`rD5RWn;!ch^BrauSPHXPUh?BDz@Ut;od zmQ4>}ahc!a2^JEAoD+yQ#m?=B%^vZad=f}S8X&Tqm1m)@;e`m!IcA(C>>M0F#2E0tt%Gn#jhcX3%^aLBZN-xDTU?LbBKJ1?Kz|Loh!06cE>#5u?R;17H*+`wdH ROO^lt002ovPDHLkV1mP{6ITEL literal 0 HcmV?d00001 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(['
      '] + items + ['
    ']) + 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}

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

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