diff --git a/resources/content_server/browse/browse.css b/resources/content_server/browse/browse.css index 438e9ebb8e..aa3d3be0ef 100644 --- a/resources/content_server/browse/browse.css +++ b/resources/content_server/browse/browse.css @@ -148,19 +148,6 @@ h2.library_name { padding-top: 40px; } -/* }}} */ - -/* Widgets {{{ */ - -#content .ui-widget { font-size: medium; font-family: monospace; } - -#content .ui-button, #content .ui-button-text { - padding: 0.2em 0.5em; - font-family: monospace; - line-height: 1 -} - - /* }}} */ /* Sort select {{{ */ @@ -179,7 +166,6 @@ h2.library_name { /* }}} */ - /* Search bar {{{ */ #search_box { @@ -192,7 +178,6 @@ h2.library_name { /* }}} */ - /* Top level {{{ */ .toplevel ul { list-style-type: none; @@ -220,7 +205,6 @@ h2.library_name { /* }}} */ - /* Category {{{ */ .category ul { list-style-type: none; @@ -247,16 +231,19 @@ h2.library_name { .category li.category-item h4 { display: inline } .category li.category-item span.href { display: none } -/* -.category a.navlink { - text-decoration: none; - color: inherit; +#groups span.load_href { display: none } + +#groups h3 { + font-weight: bold; + margin-top: 1ex; + padding-left: 2em; + padding-top: 0.5ex; + padding-bottom: 0.5ex; } -.category a.navlink:hover { - color: red; +#groups h3 span { + font-weight: normal } -*/ /* }}} */ diff --git a/resources/content_server/browse/browse.js b/resources/content_server/browse/browse.js index c2d942efab..692dd5e630 100644 --- a/resources/content_server/browse/browse.js +++ b/resources/content_server/browse/browse.js @@ -86,6 +86,10 @@ function toplevel() { } // }}} +function render_error(msg) { + return '

 Error: '+msg+"

" +} + // Category feed {{{ function category() { $(".category li").corner("15px"); @@ -96,6 +100,39 @@ function category() { }); $(".category a.navlink").button(); + + $("#groups").accordion({ + collapsible: true, + active: false, + autoHeight: false, + clearStyle: true, + header: "h3", + + change: function(event, ui) { + if (ui.newContent) { + var href = ui.newContent.children("span.load_href").html(); + ui.newContent.children(".loading").show(); + if (href) { + $.ajax({ + url:href, + success: function(data) { + this.children(".loaded").html(data); + this.children(".loaded").show(); + this.children(".loading").hide(); + }, + context: ui.newContent, + dataType: "json", + timeout: 600000, //milliseconds (10 minutes) + error: function(xhr, stat, err) { + this.children(".loaded").html(render_error(stat)); + this.children(".loaded").show(); + this.children(".loading").hide(); + } + }); + } + } + } + }); } // }}} diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index a292db309c..87a6f6a9cf 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -5,7 +5,7 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import operator, os +import operator, os, json from urllib import quote from binascii import hexlify @@ -13,6 +13,7 @@ import cherrypy from calibre.constants import filesystem_encoding from calibre import isbytestring, force_unicode, prepare_string_for_xml as xml +from calibre.utils.ordered_dict import OrderedDict def paginate(offsets, content, base_url, up_url=None): # {{{ 'Create markup for pagination' @@ -102,7 +103,7 @@ def render_rating(rating, container='span'): # {{{ # }}} -def get_category_items(category, items, db): # {{{ +def get_category_items(category, items, db, datatype): # {{{ def item(i): templ = (u'
  • ' @@ -110,6 +111,8 @@ def get_category_items(category, items, db): # {{{ '{3}
  • ') rating, rstring = render_rating(i.avg_rating) name = xml(i.name) + if datatype == 'rating': + name = xml(_('%d stars')%int(i.avg_rating)) id_ = i.id if id_ is None: id_ = hexlify(force_unicode(name).encode('utf-8')) @@ -164,6 +167,9 @@ class BrowseServer(object): connect('browse', base_href, self.browse_catalog) connect('browse_catalog', base_href+'/category/{category}', self.browse_catalog) + connect('browse_category_group', + base_href+'/category_group/{category}/{group}', + self.browse_category_group) connect('browse_list', base_href+'/list/{query}', self.browse_list) connect('browse_search', base_href+'/search/{query}', self.browse_search) @@ -243,32 +249,66 @@ class BrowseServer(object): return self.browse_template('name').format(title='', script='toplevel();', main=main) - def browse_category(self, category, sort): + def browse_sort_categories(self, items, sort): if sort not in ('rating', 'name', 'popularity'): sort = 'name' + def sorter(x): + ans = getattr(x, 'sort', x.name) + if hasattr(ans, 'upper'): + ans = ans.upper() + return ans + items.sort(key=sorter) + if sort == 'popularity': + items.sort(key=operator.attrgetter('count'), reverse=True) + elif sort == 'rating': + items.sort(key=operator.attrgetter('avg_rating'), reverse=True) + return sort + + def browse_category(self, category, sort): categories = self.categories_cache() category_meta = self.db.field_metadata category_name = category_meta[category]['name'] + datatype = category_meta[category]['datatype'] if category not in categories: raise cherrypy.HTTPError(404, 'category not found') items = categories[category] + sort = self.browse_sort_categories(items, sort) - name_keyg = lambda x: getattr(x, 'sort', x.name).lower() - items.sort(key=name_keyg) - if sort == 'popularity': - items.sort(key=operator.attrgetter('count'), reverse=True) - elif sort == 'rating': - items.sort(key=operator.attrgetter('avg_rating'), reverse=True) + script = 'true' - base_url='/browse/category/'+category - if sort is not None: - base_url += '?sort='+quote(sort) + if len(items) <= self.opts.max_opds_ungrouped_items: + script = 'false' + items = get_category_items(category, items, self.db, datatype) + else: + getter = lambda x: unicode(getattr(x, 'sort', x.name)) + starts = set([]) + for x in items: + val = getter(x) + if not val: + val = u'A' + starts.add(val[0].upper()) + category_groups = OrderedDict() + for x in sorted(starts): + category_groups[x] = len([y for y in items if + getter(y).upper().startswith(x)]) + items = [(u'

    {0} [{2}]

    ' + u'' + u'
    {1}
    ' + u'{3}
    ').format( + xml(s, True), + xml(_('Loading, please wait'))+'…', + unicode(c), + xml(u'/browse/category_group/%s/%s'%(category, s))) + for s, c in category_groups.items()] + items = '\n\n'.join(items) + items = u'
    \n{0}
    '.format(items) - script = 'category();' - items = get_category_items(category, items, self.db) + + script = 'category(%s);'%script + main = u'''

    {0}

    @@ -283,6 +323,35 @@ class BrowseServer(object): return self.browse_template(sort).format(title=category_name, script=script, main=main) + @Endpoint(mimetype='application/json; charset=utf-8') + def browse_category_group(self, category=None, group=None, + category_sort=None): + sort = category_sort + if sort not in ('rating', 'name', 'popularity'): + sort = 'name' + categories = self.categories_cache() + category_meta = self.db.field_metadata + datatype = category_meta[category]['datatype'] + + if category not in categories: + raise cherrypy.HTTPError(404, 'category not found') + if not group: + raise cherrypy.HTTPError(404, 'invalid group') + + items = categories[category] + entries = [] + getter = lambda x: unicode(getattr(x, 'sort', x.name)) + for x in items: + val = getter(x) + if not val: + val = u'A' + if val.upper().startswith(group): + entries.append(x) + + sort = self.browse_sort_categories(entries, sort) + entries = get_category_items(category, entries, self.db, datatype) + return json.dumps(entries, ensure_ascii=False) + @Endpoint()