mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
Initial simple implementation of category view in new content server frontend
This commit is contained in:
parent
404a5080a9
commit
2a8c2661a6
@ -82,9 +82,9 @@ body {
|
|||||||
-moz-border-radius: 5px;
|
-moz-border-radius: 5px;
|
||||||
-webkit-border-radius: 5px;
|
-webkit-border-radius: 5px;
|
||||||
text-shadow: #27211b 1px 1px 1px;
|
text-shadow: #27211b 1px 1px 1px;
|
||||||
-moz-box-shadow: 5px 5px 5px #222;
|
-moz-box-shadow: 5px 5px 5px #222;
|
||||||
-webkit-box-shadow: 5px 5px 5px #222;
|
-webkit-box-shadow: 5px 5px 5px #222;
|
||||||
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 }
|
||||||
|
|
||||||
|
/* }}} */
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,7 +15,10 @@
|
|||||||
|
|
||||||
<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" src="/static/jquery_ui/js/jquery-ui-1.8.5.custom.min.js"></script>
|
|
||||||
|
<script type="text/javascript"
|
||||||
|
src="/static/jquery_ui/js/jquery-ui-1.8.5.custom.min.js"></script>
|
||||||
|
|
||||||
|
|
||||||
<script type="text/javascript" src="/static/browse/browse.js"></script>
|
<script type="text/javascript" src="/static/browse/browse.js"></script>
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
|
||||||
|
|
||||||
|
BIN
resources/content_server/star-half.png
Normal file
BIN
resources/content_server/star-half.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 667 B |
BIN
resources/content_server/star-off.png
Normal file
BIN
resources/content_server/star-off.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 685 B |
BIN
resources/content_server/star-on.png
Normal file
BIN
resources/content_server/star-on.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 631 B |
@ -5,12 +5,122 @@ __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
|
import operator, os, sys
|
||||||
|
from urllib import quote
|
||||||
|
from binascii import hexlify
|
||||||
|
|
||||||
import cherrypy
|
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):
|
||||||
|
'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'<a class="{cls}" href="{base_url}&offset={offset}" title={name}>'
|
||||||
|
u'{label}</a>').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 = '<a href="{0}" title="{1}">[{1} ↑]</a>'.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'''
|
||||||
|
<table class="navbar">
|
||||||
|
<tr>
|
||||||
|
<td class="left">{left}</td>
|
||||||
|
<td class="middle">{middle}</td>
|
||||||
|
<td class="right">{right}</td>
|
||||||
|
</tr>
|
||||||
|
<table>
|
||||||
|
'''.format(left=left, right=right, middle=middle)
|
||||||
|
|
||||||
|
templ = u'''
|
||||||
|
<div class="page">
|
||||||
|
{navbar}
|
||||||
|
<div class="page-contents">
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
{navbar}
|
||||||
|
</div>
|
||||||
|
'''
|
||||||
|
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'<img alt="{0}" title="{0}" src="/static/star-{1}.png" />'.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'<li title="{4}" class="category-item">'
|
||||||
|
'<h4>{0} {1}</h4> {2}'
|
||||||
|
'<span class="href">{3}</span></li>')
|
||||||
|
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(['<ul>'] + items + ['</ul>'])
|
||||||
|
|
||||||
|
|
||||||
class BrowseServer(object):
|
class BrowseServer(object):
|
||||||
|
|
||||||
@ -23,7 +133,6 @@ class BrowseServer(object):
|
|||||||
connect('browse_search', base_href+'/search/{query}',
|
connect('browse_search', base_href+'/search/{query}',
|
||||||
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)
|
||||||
connect('browse_json', base_href+'/json/{query}', self.browse_json)
|
|
||||||
|
|
||||||
def browse_template(self, category=True):
|
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()
|
sort_opts = [(x, fm[x]['name']) for x in fm.sortable_field_keys()
|
||||||
if fm[x]['name']]
|
if fm[x]['name']]
|
||||||
prefix = 'category' if category else 'book'
|
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')+':'))
|
ans = ans.replace('{sort_select_label}', xml(_('Sort by')+':'))
|
||||||
opts = ['<option value="%s_%s">%s</option>' % (prefix, xml(k),
|
opts = ['<option value="%s_%s">%s</option>' % (prefix, xml(k),
|
||||||
xml(n)) for k, n in
|
xml(n)) for k, n in
|
||||||
@ -61,50 +171,102 @@ class BrowseServer(object):
|
|||||||
|
|
||||||
|
|
||||||
# Catalogs {{{
|
# Catalogs {{{
|
||||||
def browse_catalog(self, category=None):
|
def browse_toplevel(self):
|
||||||
if category == None:
|
categories = self.categories_cache()
|
||||||
categories = self.categories_cache()
|
category_meta = self.db.field_metadata
|
||||||
category_meta = self.db.field_metadata
|
cats = [
|
||||||
cats = [
|
(_('Newest'), 'newest'),
|
||||||
(_('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 = ['<li title="{2} {0}">{0}<span>/browse/category/{1}</span></li>'.format(xml(x, True),
|
|
||||||
xml(y), xml(_('Browse books by'))) for x, y in cats]
|
|
||||||
|
|
||||||
main = '<div class="toplevel"><h3>{0}</h3><ul>{1}</ul></div>'\
|
def getter(x):
|
||||||
.format(_('Choose a category to browse by:'), '\n\n'.join(cats))
|
return category_meta[x]['name'].lower()
|
||||||
ans = self.browse_template().format(title='',
|
|
||||||
script='toplevel();', main=main)
|
for category in sorted(categories,
|
||||||
else:
|
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 = ['<li title="{2} {0}">{0}<span>/browse/category/{1}</span></li>'\
|
||||||
|
.format(xml(x, True), xml(quote(y)), xml(_('Browse books by')))
|
||||||
|
for x, y in cats]
|
||||||
|
|
||||||
|
main = '<div class="toplevel"><h3>{0}</h3><ul>{1}</ul></div>'\
|
||||||
|
.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'''
|
||||||
|
<div class="category">
|
||||||
|
<h3>{0}</h3>
|
||||||
|
<p><a class="navlink" href="/browse" title="{2}"
|
||||||
|
>[{2} ↑]</a>
|
||||||
|
</p>
|
||||||
|
{1}
|
||||||
|
</div>
|
||||||
|
'''.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')
|
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'
|
cherrypy.response.headers['Content-Type'] = 'text/html'
|
||||||
updated = self.db.last_modified()
|
updated = self.db.last_modified()
|
||||||
cherrypy.response.headers['Last-Modified'] = \
|
cherrypy.response.headers['Last-Modified'] = \
|
||||||
self.last_modified(max(updated, self.build_time))
|
self.last_modified(max(updated, self.build_time))
|
||||||
return ans
|
return utf8(ans)
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
# Book Lists {{{
|
# Book Lists {{{
|
||||||
def browse_list(self, query=None):
|
def browse_list(self, query=None, offset=0, sort=None):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
# Search {{{
|
# Search {{{
|
||||||
def browse_search(self, query=None):
|
def browse_search(self, query=None, offset=0, sort=None):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
@ -113,8 +275,4 @@ class BrowseServer(object):
|
|||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
# JSON {{{
|
|
||||||
def browse_json(self, query=None):
|
|
||||||
raise NotImplementedError()
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ class Offsets(object):
|
|||||||
raise cherrypy.HTTPError(404, 'Invalid offset: %r'%offset)
|
raise cherrypy.HTTPError(404, 'Invalid offset: %r'%offset)
|
||||||
last_allowed_index = total - 1
|
last_allowed_index = total - 1
|
||||||
last_current_index = offset + delta - 1
|
last_current_index = offset + delta - 1
|
||||||
|
self.slice_upper_bound = offset+delta
|
||||||
self.offset = offset
|
self.offset = offset
|
||||||
self.next_offset = last_current_index + 1
|
self.next_offset = last_current_index + 1
|
||||||
if self.next_offset > last_allowed_index:
|
if self.next_offset > last_allowed_index:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user