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;
|
||||
-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 }
|
||||
|
||||
/* }}} */
|
||||
|
||||
|
||||
|
@ -15,7 +15,10 @@
|
||||
|
||||
<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_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>
|
||||
|
||||
|
@ -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>'
|
||||
__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'<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):
|
||||
|
||||
@ -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 = ['<option value="%s_%s">%s</option>' % (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 = ['<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]
|
||||
def browse_toplevel(self):
|
||||
categories = self.categories_cache()
|
||||
category_meta = self.db.field_metadata
|
||||
cats = [
|
||||
(_('Newest'), 'newest'),
|
||||
]
|
||||
|
||||
main = '<div class="toplevel"><h3>{0}</h3><ul>{1}</ul></div>'\
|
||||
.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 = ['<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')
|
||||
|
||||
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()
|
||||
# }}}
|
||||
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user