CS: Port /mobile

Useful for legacy devices that dont support html 5
This commit is contained in:
Kovid Goyal 2016-02-25 18:43:58 +05:30
parent 0ab7184b5c
commit 741d7d9efc
3 changed files with 317 additions and 6 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,105 @@
/* CSS for the mobile version of the content server webpage */
.body {
font-family: sans-serif;
}
.navigation table.buttons {
width: 100%;
}
.navigation .button {
width: 50%;
}
.button a, .button:visited a {
padding: 0.5em;
font-size: larger;
border: 1px solid black;
text-color: black;
text-decoration: none;
margin-right: 0.5em;
background-color: #ddd;
border-top: 1px solid ThreeDLightShadow;
border-right: 1px solid ButtonShadow;
border-bottom: 1px solid ButtonShadow;
border-left: 1 px solid ThreeDLightShadow;
-moz-border-radius: 0.25em;
-webkit-border-radius: 0.25em;
}
.button:hover a {
border-top: 1px solid #666;
border-right: 1px solid #CCC;
border-bottom: 1 px solid #CCC;
border-left: 1 px solid #666;
}
div.navigation {
padding-bottom: 1em;
clear: both;
}
#search_box {
border: 1px solid #393;
-moz-border-radius: 0.5em;
-webkit-border-radius: 0.5em;
padding: 1em;
margin-bottom: 0.5em;
float: right;
}
#listing {
width: 100%;
border-collapse: collapse;
}
#listing td {
padding: 0.25em;
vertical-align: middle;
}
#listing td.thumbnail {
height: 60px;
width: 60px;
}
#listing tr:nth-child(even) {
background: #eee;
}
#listing .button a{
display: inline-block;
width: 2.5em;
padding-left: 0em;
padding-right: 0em;
overflow: hidden;
text-align: center;
text-decoration: none;
vertical-align: middle;
}
#logo {
float: left;
}
#spacer {
clear: both;
}
.data-container {
display: inline-block;
vertical-align: middle;
}
.first-line {
font-size: larger;
font-weight: bold;
}
.second-line {
margin-top: 0.75ex;
display: block;
}

View File

@ -4,19 +4,225 @@
from __future__ import (unicode_literals, division, absolute_import,
print_function)
from functools import partial
from lxml.html import tostring
from lxml.html.builder import E as E_
from urllib import urlencode
from calibre.srv.errors import HTTPRedirect
from calibre import strftime
from calibre.constants import __appname__
from calibre.db.view import sanitize_sort_field_name
from calibre.ebooks.metadata import authors_to_string
from calibre.srv.errors import HTTPRedirect, HTTPBadRequest
from calibre.srv.routes import endpoint
from calibre.srv.utils import get_library_data, http_date
from calibre.utils.cleantext import clean_xml_chars
from calibre.utils.date import timestampfromdt, dt_as_local
# /mobile {{{
def clean(x):
if isinstance(x, basestring):
x = clean_xml_chars(x)
return x
def E(tag, *children, **attribs):
children = list(map(clean, children))
attribs = {k.rstrip('_').replace('_', '-'):clean(v) for k, v in attribs.iteritems()}
return getattr(E_, tag)(*children, **attribs)
for tag in 'HTML HEAD TITLE LINK DIV IMG BODY OPTION SELECT INPUT FORM SPAN TABLE TR TD A HR META'.split():
setattr(E, tag, partial(E, tag))
tag = tag.lower()
setattr(E, tag, partial(E, tag))
def html(ctx, rd, endpoint, output):
rd.outheaders.set('Content-Type', 'text/html; charset=UTF-8', replace_all=True)
if isinstance(output, bytes):
ans = output # Assume output is already UTF-8 encoded html
else:
ans = tostring(output, include_meta_content_type=True, pretty_print=True, encoding='utf-8', doctype='<!DOCTYPE html>', with_tail=False)
if not isinstance(ans, bytes):
ans = ans.encode('utf-8')
return ans
def build_search_box(num, search, sort, order, ctx, field_metadata): # {{{
div = E.div(id='search_box')
form = E.form('Show ', method='get', action=ctx.url_for('/mobile'))
form.set('accept-charset', 'UTF-8')
div.append(form)
num_select = E.select(name='num')
for option in (5, 10, 25, 100):
kwargs = {'value':str(option)}
if option == num:
kwargs['SELECTED'] = 'SELECTED'
num_select.append(E.option(str(option), **kwargs))
num_select.tail = ' books matching '
form.append(num_select)
searchf = E.input(name='search', id='s', value=search if search else '')
searchf.tail = ' sorted by '
form.append(searchf)
sort_select = E.select(name='sort')
for option in ('date','author','title','rating','size','tags','series'):
q = sanitize_sort_field_name(field_metadata, option)
kwargs = {'value':option}
if q == sanitize_sort_field_name(field_metadata, sort):
kwargs['SELECTED'] = 'SELECTED'
sort_select.append(E.option(option, **kwargs))
form.append(sort_select)
order_select = E.select(name='order')
for option in ('ascending','descending'):
kwargs = {'value':option}
if option == order:
kwargs['SELECTED'] = 'SELECTED'
order_select.append(E.option(option, **kwargs))
form.append(order_select)
form.append(E.input(id='go', type='submit', value='Search'))
return div
# }}}
def build_navigation(start, num, total, url_base): # {{{
end = min((start+num-1), total)
tagline = E.span('Books %d to %d of %d'%(start, end, total),
style='display: block; text-align: center;')
left_buttons = E.td(class_='button', style='text-align:left')
right_buttons = E.td(class_='button', style='text-align:right')
if start > 1:
for t,s in [('First', 1), ('Previous', max(start-num,1))]:
left_buttons.append(E.a(t, href='%s&start=%d'%(url_base, s)))
if total > start + num:
for t,s in [('Next', start+num), ('Last', total-num+1)]:
right_buttons.append(E.a(t, href='%s&start=%d'%(url_base, s)))
buttons = E.table(
E.tr(left_buttons, right_buttons),
class_='buttons')
return E.div(tagline, buttons, class_='navigation')
# }}}
def build_index(books, num, search, sort, order, start, total, url_base, field_metadata, ctx): # {{{
logo = E.div(E.img(src=ctx.url_for('/static', what='calibre.png'), alt=__appname__), id='logo')
search_box = build_search_box(num, search, sort, order, ctx, field_metadata)
navigation = build_navigation(start, num, total, url_base)
navigation2 = build_navigation(start, num, total, url_base)
books_table = E.table(id='listing')
body = E.body(
logo,
search_box,
navigation,
E.hr(class_='spacer'),
books_table,
E.hr(class_='spacer'),
navigation2
)
for book in books:
thumbnail = E.td(
E.img(type='image/jpeg', border='0', src=ctx.url_for('/get', what='thumb', book_id=book.id),
class_='thumbnail')
)
data = E.td()
for fmt in book.formats or ():
if not fmt or fmt.lower().startswith('original_'):
continue
s = E.span(
E.a(
fmt.lower(),
href=ctx.url_for('/get', what=fmt, book_id=book.id)
),
class_='button')
s.tail = u''
data.append(s)
div = E.div(class_='data-container')
data.append(div)
series = ('[%s - %s]'%(book.series, book.series_index)) if book.series else ''
tags = ('Tags=[%s]'%', '.join(book.tags)) if book.tags else ''
ctext = ''
for key in filter(ctx.is_field_displayable, field_metadata.ignorable_field_keys()):
fm = field_metadata[key]
if fm['datatype'] == 'comments':
continue
name, val = book.format_field(key)
if val:
ctext += '%s=[%s] '%(name, val)
first = E.span(u'\u202f%s %s by %s' % (book.title, series,
authors_to_string(book.authors)), class_='first-line')
div.append(first)
second = E.span(u'%s %s %s' % (strftime('%d %b, %Y', t=dt_as_local(book.timestamp).timetuple()),
tags, ctext), class_='second-line')
div.append(second)
books_table.append(E.tr(thumbnail, data))
body.append(E.div(
E.a(_('Switch to the full interface (non-mobile interface)'),
href=ctx.url_for(None),
style="text-decoration: none; color: blue",
title=_('The full interface gives you many more features, '
'but it may not work well on a small screen')),
style="text-align:center"))
return E.html(
E.head(
E.title(__appname__ + ' Library'),
E.link(rel='icon', href=ctx.url_for('/favicon.png'), type='image/png'),
E.link(rel='stylesheet', type='text/css', href=ctx.url_for('/static', what='mobile.css')),
E.link(rel='apple-touch-icon', href=ctx.url_for("/static", what='calibre.png')),
E.meta(name="robots", content="noindex")
), # End head
body
) # End html
# }}}
@endpoint('/mobile', postprocess=html)
def mobile(ctx, rd):
db, library_id, library_map, default_library = get_library_data(ctx, rd)
try:
start = max(1, int(rd.query.get('start', 1)))
except ValueError:
raise HTTPBadRequest('start is not an integer')
try:
num = max(0, int(rd.query.get('num', 25)))
except ValueError:
raise HTTPBadRequest('num is not an integer')
search = rd.query.get('search') or ''
with db.safe_read_lock:
book_ids = ctx.search(rd, db, search)
total = len(book_ids)
ascending = rd.query.get('order', '').lower().strip() == 'ascending'
sort_by = sanitize_sort_field_name(db.field_metadata, rd.query.get('sort') or 'date')
try:
book_ids = db.multisort([(sort_by, ascending)], book_ids)
except Exception:
sort_by = 'date'
book_ids = db.multisort([(sort_by, ascending)], book_ids)
books = [db.get_metadata(book_id) for book_id in book_ids[(start-1):(start-1)+num]]
rd.outheaders['Last-Modified'] = http_date(timestampfromdt(db.last_modified()))
order = 'ascending' if ascending else 'descending'
q = {b'search':search.encode('utf-8'), b'order':bytes(order), b'sort':sort_by.encode('utf-8'), b'num':bytes(num)}
url_base = ctx.url_for('/mobile') + '?' + urlencode(q)
return build_index(books, num, search, sort_by, order, start, total, url_base, db.field_metadata, ctx)
# }}}
@endpoint('/browse/{+rest=""}')
def browse(ctx, rd, rest):
raise HTTPRedirect(ctx.url_for(None))
@endpoint('/mobile/{+rest=""}')
def mobile(ctx, rd, rest):
raise HTTPRedirect(ctx.url_for(None))
@endpoint('/stanza/{+rest=""}')
def stanza(ctx, rd, rest):
raise HTTPRedirect(ctx.url_for('/opds'))