mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Sorting for CS category views
This commit is contained in:
parent
a5f2c1fbb1
commit
4f031e05dd
@ -12,17 +12,22 @@
|
|||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="/static/browse/browse.css" />
|
<link rel="stylesheet" type="text/css" href="/static/browse/browse.css" />
|
||||||
<link type="text/css" href="/static/jquery_ui/css/pepper-grinder/jquery-ui-1.8.5.custom.css" rel="stylesheet" />
|
<link type="text/css" href="/static/jquery_ui/css/pepper-grinder/jquery-ui-1.8.5.custom.css" rel="stylesheet" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/jquery.multiselect.css" />
|
||||||
|
|
||||||
<script type="text/javascript" src="/static/jquery.js"></script>
|
<script type="text/javascript" src="/static/jquery.js"></script>
|
||||||
<script type="text/javascript" src="/static/jquery.corner.js"></script>
|
<script type="text/javascript" src="/static/jquery.corner.js"></script>
|
||||||
|
|
||||||
<script type="text/javascript"
|
<script type="text/javascript"
|
||||||
src="/static/jquery_ui/js/jquery-ui-1.8.5.custom.min.js"></script>
|
src="/static/jquery_ui/js/jquery-ui-1.8.5.custom.min.js"></script>
|
||||||
|
<script type="text/javascript"
|
||||||
|
src="/static/jquery.multiselect.min.js"></script>
|
||||||
|
|
||||||
|
|
||||||
<script type="text/javascript" src="/static/browse/browse.js"></script>
|
<script type="text/javascript" src="/static/browse/browse.js"></script>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
var sort_cookie_name = "{sort_cookie_name}";
|
||||||
|
var sort_select_label = "{sort_select_label}";
|
||||||
$(document).ready(function() {{
|
$(document).ready(function() {{
|
||||||
init();
|
init();
|
||||||
{script}
|
{script}
|
||||||
@ -67,7 +72,7 @@
|
|||||||
|
|
||||||
<div id="content">
|
<div id="content">
|
||||||
<div class="sort_select">
|
<div class="sort_select">
|
||||||
<label>{sort_select_label} </label>
|
<label>{sort_select_label}</label>
|
||||||
<select id="sort_combobox">
|
<select id="sort_combobox">
|
||||||
{sort_select_options}
|
{sort_select_options}
|
||||||
</select>
|
</select>
|
||||||
|
@ -1,106 +1,64 @@
|
|||||||
|
|
||||||
// Widgets {{{
|
// Cookies {{{
|
||||||
|
|
||||||
// Combobox {{{
|
function cookie(name, value, options) {
|
||||||
|
if (typeof value != 'undefined') { // name and value given, set cookie
|
||||||
(function( $ ) {
|
options = options || {};
|
||||||
$.widget( "ui.combobox", {
|
if (value === null) {
|
||||||
_create: function() {
|
value = '';
|
||||||
var self = this,
|
options.expires = -1;
|
||||||
select = this.element.hide(),
|
|
||||||
selected = select.children( ":selected" ),
|
|
||||||
value = selected.val() ? selected.text() : "";
|
|
||||||
var input = $( "<input>" )
|
|
||||||
.insertAfter( select )
|
|
||||||
.val( value )
|
|
||||||
.autocomplete({
|
|
||||||
delay: 0,
|
|
||||||
minLength: 0,
|
|
||||||
source: function( request, response ) {
|
|
||||||
var matcher = new RegExp( $.ui.autocomplete.escapeRegex(request.term), "i" );
|
|
||||||
response( select.children( "option" ).map(function() {
|
|
||||||
var text = $( this ).text();
|
|
||||||
if ( this.value && ( !request.term || matcher.test(text) ) )
|
|
||||||
return {
|
|
||||||
label: text.replace(
|
|
||||||
new RegExp(
|
|
||||||
"(?![^&;]+;)(?!<[^<>]*)(" +
|
|
||||||
$.ui.autocomplete.escapeRegex(request.term) +
|
|
||||||
")(?![^<>]*>)(?![^&;]+;)", "gi"
|
|
||||||
), "<strong>$1</strong>" ),
|
|
||||||
value: text,
|
|
||||||
option: this
|
|
||||||
};
|
|
||||||
}) );
|
|
||||||
},
|
|
||||||
select: function( event, ui ) {
|
|
||||||
ui.item.option.selected = true;
|
|
||||||
self._trigger( "selected", event, {
|
|
||||||
item: ui.item.option
|
|
||||||
});
|
|
||||||
},
|
|
||||||
change: function( event, ui ) {
|
|
||||||
if ( !ui.item ) {
|
|
||||||
var matcher = new RegExp( "^" + $.ui.autocomplete.escapeRegex( $(this).val() ) + "$", "i" ),
|
|
||||||
valid = false;
|
|
||||||
select.children( "option" ).each(function() {
|
|
||||||
if ( this.value.match( matcher ) ) {
|
|
||||||
this.selected = valid = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if ( !valid ) {
|
|
||||||
// remove invalid value, as it didn't match anything
|
|
||||||
$( this ).val( "" );
|
|
||||||
select.val( "" );
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.addClass( "ui-widget ui-widget-content ui-corner-left" );
|
|
||||||
|
|
||||||
input.data( "autocomplete" )._renderItem = function( ul, item ) {
|
|
||||||
return $( "<li></li>" )
|
|
||||||
.data( "item.autocomplete", item )
|
|
||||||
.append( "<a>" + item.label + "</a>" )
|
|
||||||
.appendTo( ul );
|
|
||||||
};
|
|
||||||
|
|
||||||
$( "<button> </button>" )
|
|
||||||
.attr( "tabIndex", -1 )
|
|
||||||
.attr( "title", "Show All Items" )
|
|
||||||
.insertAfter( input )
|
|
||||||
.button({
|
|
||||||
icons: {
|
|
||||||
primary: "ui-icon-triangle-1-s"
|
|
||||||
},
|
|
||||||
text: false
|
|
||||||
})
|
|
||||||
.removeClass( "ui-corner-all" )
|
|
||||||
.addClass( "ui-corner-right ui-button-icon" )
|
|
||||||
.click(function() {
|
|
||||||
// close if already visible
|
|
||||||
if ( input.autocomplete( "widget" ).is( ":visible" ) ) {
|
|
||||||
input.autocomplete( "close" );
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// pass empty string as value to search for, displaying all results
|
|
||||||
input.autocomplete( "search", "" );
|
|
||||||
input.focus();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
var expires = '';
|
||||||
})( jQuery );
|
if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) {
|
||||||
// }}}
|
var date;
|
||||||
|
if (typeof options.expires == 'number') {
|
||||||
|
date = new Date();
|
||||||
|
date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000));
|
||||||
|
} else {
|
||||||
|
date = options.expires;
|
||||||
|
}
|
||||||
|
expires = '; expires=' + date.toUTCString(); // use expires attribute, max-age is not supported by IE
|
||||||
|
}
|
||||||
|
// CAUTION: Needed to parenthesize options.path and options.domain
|
||||||
|
// in the following expressions, otherwise they evaluate to undefined
|
||||||
|
// in the packed version for some reason...
|
||||||
|
var path = options.path ? '; path=' + (options.path) : '';
|
||||||
|
var domain = options.domain ? '; domain=' + (options.domain) : '';
|
||||||
|
var secure = options.secure ? '; secure' : '';
|
||||||
|
document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join('');
|
||||||
|
} else { // only name given, get cookie
|
||||||
|
var cookieValue = null;
|
||||||
|
if (document.cookie && document.cookie != '') {
|
||||||
|
var cookies = document.cookie.split(';');
|
||||||
|
for (var i = 0; i < cookies.length; i++) {
|
||||||
|
var cookie = jQuery.trim(cookies[i]);
|
||||||
|
// Does this cookie string begin with the name we want?
|
||||||
|
if (cookie.substring(0, name.length + 1) == (name + '=')) {
|
||||||
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookieValue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// }}}
|
// }}}
|
||||||
|
|
||||||
// Sort {{{
|
// Sort {{{
|
||||||
|
|
||||||
function init_sort_combobox() {
|
function init_sort_combobox() {
|
||||||
$("#sort_combobox").combobox();
|
$("#sort_combobox").multiselect({
|
||||||
|
multiple: false,
|
||||||
|
header: sort_select_label,
|
||||||
|
noneSelectedText: sort_select_label,
|
||||||
|
selectedList: 1,
|
||||||
|
click: function(event, ui){
|
||||||
|
$(this).multiselect("close");
|
||||||
|
cookie(sort_cookie_name, ui.value, {expires: 365});
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// }}}
|
// }}}
|
||||||
|
21
resources/content_server/jquery.multiselect.css
Normal file
21
resources/content_server/jquery.multiselect.css
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
.ui-multiselect { padding:2px 0 2px 4px; text-align:left }
|
||||||
|
.ui-multiselect span.ui-icon { float:right }
|
||||||
|
|
||||||
|
.ui-multiselect-header { margin-bottom:3px; padding:3px 0 3px 4px }
|
||||||
|
.ui-multiselect-header ul { font-size:0.9em }
|
||||||
|
.ui-multiselect-header ul li { float:left; padding:0 10px 0 0 }
|
||||||
|
.ui-multiselect-header a { text-decoration:none }
|
||||||
|
.ui-multiselect-header a:hover { text-decoration:underline }
|
||||||
|
.ui-multiselect-header span.ui-icon { float:left }
|
||||||
|
.ui-multiselect-header li.ui-multiselect-close { float:right; text-align:right; padding-right:0 }
|
||||||
|
|
||||||
|
.ui-multiselect-menu { display:none; padding:3px; position:absolute; z-index:10000 }
|
||||||
|
.ui-multiselect-checkboxes { position:relative /* fixes bug in IE6/7 */; overflow-y:scroll }
|
||||||
|
.ui-multiselect-checkboxes label { cursor:default; display:block; border:1px solid transparent; padding:3px 1px }
|
||||||
|
.ui-multiselect-checkboxes label input { position:relative; top:1px }
|
||||||
|
.ui-multiselect-checkboxes li { clear:both; font-size:0.9em; padding-right:3px }
|
||||||
|
.ui-multiselect-checkboxes li.ui-multiselect-optgroup-label { text-align:center; font-weight:bold; border-bottom:1px solid }
|
||||||
|
.ui-multiselect-checkboxes li.ui-multiselect-optgroup-label a { display:block; padding:3px; margin:1px 0; text-decoration:none }
|
||||||
|
|
||||||
|
/* remove label borders in IE6 because IE6 does not support transparency */
|
||||||
|
* html .ui-multiselect-checkboxes label { border:none }
|
16
resources/content_server/jquery.multiselect.min.js
vendored
Normal file
16
resources/content_server/jquery.multiselect.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -5,7 +5,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import operator, os, sys
|
import operator, os
|
||||||
from urllib import quote
|
from urllib import quote
|
||||||
from binascii import hexlify
|
from binascii import hexlify
|
||||||
|
|
||||||
@ -13,9 +13,8 @@ import cherrypy
|
|||||||
|
|
||||||
from calibre.constants import filesystem_encoding
|
from calibre.constants import filesystem_encoding
|
||||||
from calibre import isbytestring, force_unicode, prepare_string_for_xml as xml
|
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):
|
def paginate(offsets, content, base_url, up_url=None): # {{{
|
||||||
'Create markup for pagination'
|
'Create markup for pagination'
|
||||||
|
|
||||||
if '?' not in base_url:
|
if '?' not in base_url:
|
||||||
@ -72,13 +71,15 @@ def paginate(offsets, content, base_url, up_url=None):
|
|||||||
</div>
|
</div>
|
||||||
'''
|
'''
|
||||||
return templ.format(navbar=navbar, content=content)
|
return templ.format(navbar=navbar, content=content)
|
||||||
|
# }}}
|
||||||
|
|
||||||
def utf8(x):
|
def utf8(x): # {{{
|
||||||
if isinstance(x, unicode):
|
if isinstance(x, unicode):
|
||||||
x = x.encode('utf-8')
|
x = x.encode('utf-8')
|
||||||
return x
|
return x
|
||||||
|
# }}}
|
||||||
|
|
||||||
def render_rating(rating, container='span'):
|
def render_rating(rating, container='span'): # {{{
|
||||||
if rating < 0.1:
|
if rating < 0.1:
|
||||||
return '', ''
|
return '', ''
|
||||||
added = 0
|
added = 0
|
||||||
@ -99,7 +100,9 @@ def render_rating(rating, container='span'):
|
|||||||
ans.append('</%s>'%container)
|
ans.append('</%s>'%container)
|
||||||
return u''.join(ans), rstring
|
return u''.join(ans), rstring
|
||||||
|
|
||||||
def get_category_items(category, items, offsets, db):
|
# }}}
|
||||||
|
|
||||||
|
def get_category_items(category, items, db): # {{{
|
||||||
|
|
||||||
def item(i):
|
def item(i):
|
||||||
templ = (u'<li title="{4}" class="category-item">'
|
templ = (u'<li title="{4}" class="category-item">'
|
||||||
@ -118,9 +121,41 @@ def get_category_items(category, items, offsets, db):
|
|||||||
return templ.format(xml(name), rating,
|
return templ.format(xml(name), rating,
|
||||||
xml(desc), xml(quote(href)), rstring)
|
xml(desc), xml(quote(href)), rstring)
|
||||||
|
|
||||||
items = list(map(item, items[offsets.offset:offsets.slice_upper_bound]))
|
items = list(map(item, items))
|
||||||
return '\n'.join(['<ul>'] + items + ['</ul>'])
|
return '\n'.join(['<ul>'] + items + ['</ul>'])
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class Endpoint(object): # {{{
|
||||||
|
'Manage encoding, mime-type, last modified, cookies, etc.'
|
||||||
|
|
||||||
|
def __init__(self, mimetype='text/html', sort_type='category'):
|
||||||
|
self.mimetype = mimetype
|
||||||
|
self.sort_type = sort_type
|
||||||
|
self.sort_kwarg = sort_type + '_sort'
|
||||||
|
self.sort_cookie_name = 'calibre_browse_server_sort_'+self.sort_type
|
||||||
|
|
||||||
|
def __call__(eself, func):
|
||||||
|
|
||||||
|
def do(self, *args, **kwargs):
|
||||||
|
sort_val = None
|
||||||
|
cookie = cherrypy.request.cookie
|
||||||
|
if cookie.has_key(eself.sort_cookie_name):
|
||||||
|
sort_val = cookie[eself.sort_cookie_name].value
|
||||||
|
kwargs[eself.sort_kwarg] = sort_val
|
||||||
|
|
||||||
|
ans = func(self, *args, **kwargs)
|
||||||
|
cherrypy.response.headers['Content-Type'] = eself.mimetype
|
||||||
|
updated = self.db.last_modified()
|
||||||
|
cherrypy.response.headers['Last-Modified'] = \
|
||||||
|
self.last_modified(max(updated, self.build_time))
|
||||||
|
ans = utf8(ans)
|
||||||
|
return ans
|
||||||
|
|
||||||
|
do.__name__ = func.__name__
|
||||||
|
|
||||||
|
return do
|
||||||
|
# }}}
|
||||||
|
|
||||||
class BrowseServer(object):
|
class BrowseServer(object):
|
||||||
|
|
||||||
@ -134,26 +169,34 @@ class BrowseServer(object):
|
|||||||
self.browse_search)
|
self.browse_search)
|
||||||
connect('browse_book', base_href+'/book/{uuid}', self.browse_book)
|
connect('browse_book', base_href+'/book/{uuid}', self.browse_book)
|
||||||
|
|
||||||
def browse_template(self, category=True):
|
def browse_template(self, sort, category=True):
|
||||||
|
|
||||||
def generate():
|
def generate():
|
||||||
|
scn = 'calibre_browse_server_sort_'
|
||||||
|
|
||||||
if category:
|
if category:
|
||||||
sort_opts = [('rating', _('Average rating')), ('name',
|
sort_opts = [('rating', _('Average rating')), ('name',
|
||||||
_('Name')), ('popularity', _('Popularity'))]
|
_('Name')), ('popularity', _('Popularity'))]
|
||||||
|
scn += 'category'
|
||||||
else:
|
else:
|
||||||
|
scn += 'list'
|
||||||
fm = self.db.field_metadata
|
fm = self.db.field_metadata
|
||||||
sort_opts = [(x, fm[x]['name']) for x in fm.sortable_field_keys()
|
sort_opts, added = [], set([])
|
||||||
if fm[x]['name']]
|
for x in fm.sortable_field_keys():
|
||||||
prefix = 'category' if category else 'book'
|
n = fm[x]['name']
|
||||||
|
if n not in added:
|
||||||
|
added.add(n)
|
||||||
|
sort_opts.append((x, n))
|
||||||
|
|
||||||
ans = P('content_server/browse/browse.html',
|
ans = P('content_server/browse/browse.html',
|
||||||
data=True).decode('utf-8')
|
data=True).decode('utf-8')
|
||||||
ans = ans.replace('{sort_select_label}', xml(_('Sort by')+':'))
|
ans = ans.replace('{sort_select_label}', xml(_('Sort by')+':'))
|
||||||
opts = ['<option value="%s_%s">%s</option>' % (prefix, xml(k),
|
ans = ans.replace('{sort_cookie_name}', scn)
|
||||||
xml(n)) for k, n in
|
opts = ['<option %svalue="%s">%s</option>' % (
|
||||||
|
'selected="selected" ' if k==sort else '',
|
||||||
|
xml(k), xml(n), ) for k, n in
|
||||||
sorted(sort_opts, key=operator.itemgetter(1))]
|
sorted(sort_opts, key=operator.itemgetter(1))]
|
||||||
opts = ['<option value="_default">'+xml(_('Select one')) +
|
ans = ans.replace('{sort_select_options}', ('\n'+' '*20).join(opts))
|
||||||
'…</option>'] + opts
|
|
||||||
ans = ans.replace('{sort_select_options}', '\n\t\t\t'.join(opts))
|
|
||||||
lp = self.db.library_path
|
lp = self.db.library_path
|
||||||
if isbytestring(lp):
|
if isbytestring(lp):
|
||||||
lp = force_unicode(lp, filesystem_encoding)
|
lp = force_unicode(lp, filesystem_encoding)
|
||||||
@ -197,10 +240,12 @@ class BrowseServer(object):
|
|||||||
|
|
||||||
main = '<div class="toplevel"><h3>{0}</h3><ul>{1}</ul></div>'\
|
main = '<div class="toplevel"><h3>{0}</h3><ul>{1}</ul></div>'\
|
||||||
.format(_('Choose a category to browse by:'), '\n\n'.join(cats))
|
.format(_('Choose a category to browse by:'), '\n\n'.join(cats))
|
||||||
return self.browse_template().format(title='',
|
return self.browse_template('name').format(title='',
|
||||||
script='toplevel();', main=main)
|
script='toplevel();', main=main)
|
||||||
|
|
||||||
def browse_category(self, category, offset, sort, subcategory=None):
|
def browse_category(self, category, sort):
|
||||||
|
if sort not in ('rating', 'name', 'popularity'):
|
||||||
|
sort = 'name'
|
||||||
categories = self.categories_cache()
|
categories = self.categories_cache()
|
||||||
category_meta = self.db.field_metadata
|
category_meta = self.db.field_metadata
|
||||||
category_name = category_meta[category]['name']
|
category_name = category_meta[category]['name']
|
||||||
@ -210,18 +255,20 @@ class BrowseServer(object):
|
|||||||
|
|
||||||
items = categories[category]
|
items = categories[category]
|
||||||
|
|
||||||
base_url='/browse/category/'+category+'?'
|
name_keyg = lambda x: getattr(x, 'sort', x.name).lower()
|
||||||
if subcategory is not None:
|
items.sort(key=name_keyg)
|
||||||
base_url += 'subcategory='+quote(subcategory)
|
if sort == 'popularity':
|
||||||
|
items.sort(key=operator.attrgetter('count'), reverse=True)
|
||||||
|
elif sort == 'rating':
|
||||||
|
items.sort(key=operator.attrgetter('avg_rating'), reverse=True)
|
||||||
|
|
||||||
|
base_url='/browse/category/'+category
|
||||||
if sort is not None:
|
if sort is not None:
|
||||||
base_url += 'sort='+quote(sort)
|
base_url += '?sort='+quote(sort)
|
||||||
|
|
||||||
script = 'category();'
|
script = 'category();'
|
||||||
|
|
||||||
max_items = sys.maxint
|
items = get_category_items(category, items, self.db)
|
||||||
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'''
|
main = u'''
|
||||||
<div class="category">
|
<div class="category">
|
||||||
<h3>{0}</h3>
|
<h3>{0}</h3>
|
||||||
@ -234,29 +281,20 @@ class BrowseServer(object):
|
|||||||
xml(_('Browsing by')+': ' + category_name), items,
|
xml(_('Browsing by')+': ' + category_name), items,
|
||||||
xml(_('Up'), True))
|
xml(_('Up'), True))
|
||||||
|
|
||||||
return self.browse_template().format(title=category_name,
|
return self.browse_template(sort).format(title=category_name,
|
||||||
script=script, main=main)
|
script=script, main=main)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def browse_catalog(self, category=None, offset=0, sort=None,
|
@Endpoint()
|
||||||
subcategory=None):
|
def browse_catalog(self, category=None, category_sort=None):
|
||||||
'Entry point for top-level, categories and sub-categories'
|
'Entry point for top-level, categories and sub-categories'
|
||||||
try:
|
|
||||||
offset = int(offset)
|
|
||||||
except:
|
|
||||||
raise cherrypy.HTTPError(404, 'Not found')
|
|
||||||
|
|
||||||
if category == None:
|
if category == None:
|
||||||
ans = self.browse_toplevel()
|
ans = self.browse_toplevel()
|
||||||
else:
|
else:
|
||||||
ans = self.browse_category(category, offset, sort)
|
ans = self.browse_category(category, category_sort)
|
||||||
|
|
||||||
cherrypy.response.headers['Content-Type'] = 'text/html'
|
return ans
|
||||||
updated = self.db.last_modified()
|
|
||||||
cherrypy.response.headers['Last-Modified'] = \
|
|
||||||
self.last_modified(max(updated, self.build_time))
|
|
||||||
return utf8(ans)
|
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user