mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
CS: Port /mobile
Useful for legacy devices that dont support html 5
This commit is contained in:
parent
0ab7184b5c
commit
741d7d9efc
BIN
resources/content-server/calibre.png
Normal file
BIN
resources/content-server/calibre.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
105
resources/content-server/mobile.css
Normal file
105
resources/content-server/mobile.css
Normal 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;
|
||||||
|
}
|
@ -4,19 +4,225 @@
|
|||||||
|
|
||||||
from __future__ import (unicode_literals, division, absolute_import,
|
from __future__ import (unicode_literals, division, absolute_import,
|
||||||
print_function)
|
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 import strftime
|
||||||
from calibre.srv.errors import HTTPRedirect
|
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.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=""}')
|
@endpoint('/browse/{+rest=""}')
|
||||||
def browse(ctx, rd, rest):
|
def browse(ctx, rd, rest):
|
||||||
raise HTTPRedirect(ctx.url_for(None))
|
raise HTTPRedirect(ctx.url_for(None))
|
||||||
|
|
||||||
@endpoint('/mobile/{+rest=""}')
|
|
||||||
def mobile(ctx, rd, rest):
|
|
||||||
raise HTTPRedirect(ctx.url_for(None))
|
|
||||||
|
|
||||||
@endpoint('/stanza/{+rest=""}')
|
@endpoint('/stanza/{+rest=""}')
|
||||||
def stanza(ctx, rd, rest):
|
def stanza(ctx, rd, rest):
|
||||||
raise HTTPRedirect(ctx.url_for('/opds'))
|
raise HTTPRedirect(ctx.url_for('/opds'))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user