diff --git a/setup.py b/setup.py index 948db8039c..35a2ae176d 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from __future__ import with_statement __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' -import sys, re, os, shutil, cStringIO, tempfile, subprocess +import sys, re, os, shutil, cStringIO, tempfile, subprocess, time sys.path.append('src') iswindows = re.search('win(32|64)', sys.platform) isosx = 'darwin' in sys.platform @@ -146,7 +146,7 @@ if __name__ == '__main__': metadata_sqlite = 'library/metadata_sqlite.sql', jquery = 'gui2/viewer/jquery.js', jquery_scrollTo = 'gui2/viewer/jquery_scrollTo.js', - ) + ) DEST = os.path.join('src', APPNAME, 'resources.py') @@ -165,6 +165,15 @@ if __name__ == '__main__': print 'WARNING: Could not find Qt transations' return data + def get_static_resources(self): + sdir = os.path.join('src', 'calibre', 'library', 'static') + resources, max = {}, 0 + for f in os.listdir(sdir): + resources[f] = open(os.path.join(sdir, f), 'rb').read() + mtime = os.stat(os.path.join(sdir, f)).st_mtime + max = mtime if mtime > max else max + return resources, max + def run(self): data, dest, RESOURCES = {}, self.DEST, self.RESOURCES for key in RESOURCES: @@ -173,12 +182,15 @@ if __name__ == '__main__': RESOURCES[key] = os.path.join('src', APPNAME, path) translations = self.get_qt_translations() RESOURCES.update(translations) - if newer([dest], RESOURCES.values()): + static, smax = self.get_static_resources() + if newer([dest], RESOURCES.values()) or os.stat(dest).st_mtime < smax: print 'Compiling resources...' with open(dest, 'wb') as f: for key in RESOURCES: data = open(RESOURCES[key], 'rb').read() f.write(key + ' = ' + repr(data)+'\n\n') + f.write('server_resources = %s\n\n'%repr(static)) + f.write('build_time = "%s"\n\n'%time.strftime('%d %m %Y %H%M%S')) else: print 'Resources are up to date' diff --git a/src/calibre/gui2/dialogs/search.ui b/src/calibre/gui2/dialogs/search.ui index f1ba9e60ae..f5813c18ee 100644 --- a/src/calibre/gui2/dialogs/search.ui +++ b/src/calibre/gui2/dialogs/search.ui @@ -103,6 +103,19 @@ + + + + + 16777215 + 30 + + + + See the <a href="http://calibre.kovidgoyal.net/user_manual/gui.html#the-search-interface">User Manual</a> for more help + + + diff --git a/src/calibre/library/server.py b/src/calibre/library/server.py index 6840b6aad4..9b044f478e 100644 --- a/src/calibre/library/server.py +++ b/src/calibre/library/server.py @@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en' HTTP server for remote access to the calibre database. ''' -import sys, textwrap, cStringIO, mimetypes, operator, os +import sys, textwrap, cStringIO, mimetypes, operator, os, re from datetime import datetime import cherrypy from PIL import Image @@ -16,6 +16,11 @@ from calibre.constants import __version__, __appname__ from calibre.utils.config import StringConfig, Config from calibre.utils.genshi.template import MarkupTemplate from calibre import fit_image +from calibre.resources import jquery, server_resources, build_time +from calibre.library.database2 import LibraryDatabase2, FIELD_MAP + +build_time = datetime.strptime(build_time, '%d %m %Y %H%M%S') +server_resources['jquery.js'] = jquery def expose(func): @@ -37,7 +42,7 @@ class LibraryServer(object): author_sort="${r[12]}" authors="${authors}" rating="${r[4]}" - timestamp="${r[5].ctime()}" + timestamp="${r[5].strftime('%Y/%m/%d %H:%M:%S')}" size="${r[6]}" isbn="${r[14] if r[14] else ''}" formats="${r[13] if r[13] else ''}" @@ -100,6 +105,7 @@ class LibraryServer(object): self.opts = opts cherrypy.config.update({ + 'server.socket_host': '0.0.0.0', 'server.socket_port': opts.port, 'server.socket_timeout': opts.timeout, #seconds 'server.thread_pool': opts.thread_pool, # number of threads @@ -108,7 +114,7 @@ class LibraryServer(object): [global] engine.autoreload_on = %(autoreload)s tools.gzip.on = True - tools.gzip.mime_types = ['text/html', 'text/plain', 'text/xml'] + tools.gzip.mime_types = ['text/html', 'text/plain', 'text/xml', 'text/javascript', 'text/css'] ''')%dict(autoreload=opts.develop) def start(self): @@ -121,7 +127,7 @@ class LibraryServer(object): cherrypy.response.headers['Content-Type'] = 'image/jpeg' path = getattr(cover, 'name', None) if path and os.path.exists(path): - updated = datetime.fromutctimestamp(os.stat(path).st_mtime) + updated = datetime.utcfromtimestamp(os.stat(path).st_mtime) cherrypy.response.headers['Last-Modified'] = self.last_modified(updated) if not thumbnail: return cover.read() @@ -149,25 +155,28 @@ class LibraryServer(object): cherrypy.response.headers['Content-Type'] = mt path = getattr(fmt, 'name', None) if path and os.path.exists(path): - updated = datetime.fromutctimestamp(os.stat(path).st_mtime) + updated = datetime.utcfromtimestamp(os.stat(path).st_mtime) cherrypy.response.headers['Last-Modified'] = self.last_modified(updated) return fmt.read() - def sort(self, items, field): + def sort(self, items, field, order): field = field.lower().strip() if field == 'author': field = 'authors' - if field not in ('title', 'authors', 'rating'): + if field == 'date': + field = 'timestamp' + if field not in ('title', 'authors', 'rating', 'timestamp', 'tags', 'size', 'series'): raise cherrypy.HTTPError(400, '%s is not a valid sort field'%field) - cmpf = cmp if field == 'rating' else lambda x, y: cmp(x.lower(), y.lower()) - field = {'title':11, 'authors':12, 'rating':4}[field] + cmpf = cmp if field in ('rating', 'size', 'timestamp') else \ + lambda x, y: cmp(x.lower() if x else '', y.lower() if y else '') + field = FIELD_MAP[field] getter = operator.itemgetter(field) - items.sort(cmp=lambda x, y: cmpf(getter(x), getter(y))) + items.sort(cmp=lambda x, y: cmpf(getter(x), getter(y)), reverse=not order) def last_modified(self, updated): lm = updated.strftime('day, %d month %Y %H:%M:%S GMT') day ={0:'Sun', 1:'Mon', 2:'Tue', 3:'Wed', 4:'Thu', 5:'Fri', 6:'Sat'} - lm = lm.replace('day', day[int(lm.strftime('%w'))]) + lm = lm.replace('day', day[int(updated.strftime('%w'))]) month = {1:'Jan', 2:'Feb', 3:'Mar', 4:'Apr', 5:'May', 6:'Jun', 7:'Jul', 8:'Aug', 9:'Sep', 10:'Oct', 11:'Nov', 12:'Dec'} return lm.replace('month', month[updated.month]) @@ -175,6 +184,7 @@ class LibraryServer(object): @expose def stanza(self): + ' Feeds to read calibre books on a ipod with stanza.' books = [] for record in iter(self.db): if 'EPUB' in record['formats'].upper(): @@ -193,11 +203,14 @@ class LibraryServer(object): updated=updated, id='urn:calibre:main').render('xml') @expose - def library(self, start='0', num='50', sort=None, search=None): + def library(self, start='0', num='50', sort=None, search=None, _=None, order='ascending'): ''' + Serves metadata from the calibre database as XML. + :param sort: Sort results by ``sort``. Can be one of `title,author,rating`. :param search: Filter results by ``search`` query. See :class:`SearchQueryParser` for query syntax - :param start,num: Return the slice `[start:start+num]` of the sorted and filtered results + :param start,num: Return the slice `[start:start+num]` of the sorted and filtered results + :param _: Firefox seems to sometimes send this when using XMLHttpRequest with no caching ''' try: start = int(start) @@ -207,33 +220,40 @@ class LibraryServer(object): num = int(num) except ValueError: raise cherrypy.HTTPError(400, 'num: %s is not an integer'%num) - ids = self.db.data.parse(search) if search else self.db.data.universal_set() + order = order.lower().strip() == 'ascending' + ids = self.db.data.parse(search) if search and search.strip() else self.db.data.universal_set() ids = sorted(ids) items = [r for r in iter(self.db) if r[0] in ids] if sort is not None: - self.sort(items, sort) + self.sort(items, sort, order) book, books = MarkupTemplate(self.BOOK), [] for record in items[start:start+num]: - authors = ' & '.join([i.replace('|', ',') for i in record[2].split(',')]) + authors = '|'.join([i.replace('|', ',') for i in record[2].split(',')]) books.append(book.generate(r=record, authors=authors).render('xml').decode('utf-8')) updated = self.db.last_modified() cherrypy.response.headers['Content-Type'] = 'text/xml' cherrypy.response.headers['Last-Modified'] = self.last_modified(updated) return self.LIBRARY.generate(books=books, start=start, updated=updated, - total=self.db.count()).render('xml') + total=len(ids)).render('xml') @expose def index(self): - return 'Hello, World!' + 'The / URL' + return self.static('index.html') @expose def get(self, what, id): + 'Serves files, covers, thumbnails from the calibre database' try: id = int(id) except ValueError: - raise cherrypy.HTTPError(400, 'id:%s not an integer'%id) + id = id.rpartition('_')[-1].partition('.')[0] + match = re.search(r'\d+', id) + if not match: + raise cherrypy.HTTPError(400, 'id:%s not an integer'%id) + id = int(match.group()) if not self.db.has_id(id): raise cherrypy.HTTPError(400, 'id:%d does not exist in database'%id) if what == 'thumb': @@ -242,6 +262,31 @@ class LibraryServer(object): return self.get_cover(id) return self.get_format(id, what) + @expose + def static(self, name): + 'Serves static content' + name = name.lower() + cherrypy.response.headers['Content-Type'] = { + 'js' : 'text/javascript', + 'css' : 'text/css', + 'png' : 'image/png', + 'gif' : 'image/gif', + 'html' : 'text/html', + '' : 'application/octet-stream', + }[name.rpartition('.')[-1].lower()] + cherrypy.response.headers['Last-Modified'] = self.last_modified(build_time) + if self.opts.develop and name in ('gui.js', 'gui.css', 'index.html'): + path = os.path.join(os.path.dirname(__file__), 'static', name) + lm = datetime.fromtimestamp(os.stat(path).st_mtime) + cherrypy.response.headers['Last-Modified'] = self.last_modified(lm) + return open(path, 'rb').read() + else: + if server_resources.has_key(name): + return server_resources[name] + raise cherrypy.HTTPError(404, '%s not found'%name) + + + def config(defaults=None): desc=_('Settings to control the calibre content server') c = Config('server', desc) if defaults is None else StringConfig(defaults, desc) @@ -256,7 +301,7 @@ def config(defaults=None): help=_('The hostname of the machine the server is running on. Used when generating the stanza feeds. Default is %default')) c.add_opt('develop', ['--develop'], default=False, - help='Development mode. Server automatically restarts on file changes.') + help='Development mode. Server automatically restarts on file changes and serves code files (html, css, js) from the file system instead of calibre\'s resource system.') return c def option_parser(): @@ -267,7 +312,6 @@ def main(args=sys.argv): opts, args = parser.parse_args(args) cherrypy.log.screen = True from calibre.utils.config import prefs - from calibre.library.database2 import LibraryDatabase2 db = LibraryDatabase2(prefs['library_path'], row_factory=True) server = LibraryServer(db, opts) server.start() diff --git a/src/calibre/library/static/bg_search_box.png b/src/calibre/library/static/bg_search_box.png new file mode 100644 index 0000000000..83f87e42b9 Binary files /dev/null and b/src/calibre/library/static/bg_search_box.png differ diff --git a/src/calibre/library/static/btn_search_box.png b/src/calibre/library/static/btn_search_box.png new file mode 100644 index 0000000000..00302ed5eb Binary files /dev/null and b/src/calibre/library/static/btn_search_box.png differ diff --git a/src/calibre/library/static/calibre.png b/src/calibre/library/static/calibre.png new file mode 100644 index 0000000000..f42e4926ca Binary files /dev/null and b/src/calibre/library/static/calibre.png differ diff --git a/src/calibre/library/static/date.js b/src/calibre/library/static/date.js new file mode 100644 index 0000000000..77f498645c --- /dev/null +++ b/src/calibre/library/static/date.js @@ -0,0 +1,104 @@ +/** + * Version: 1.0 Alpha-1 + * Build Date: 13-Nov-2007 + * Copyright (c) 2006-2007, Coolite Inc. (http://www.coolite.com/). All rights reserved. + * License: Licensed under The MIT License. See license.txt and http://www.datejs.com/license/. + * Website: http://www.datejs.com/ or http://www.coolite.com/datejs/ + */ +Date.CultureInfo={name:"en-US",englishName:"English (United States)",nativeName:"English (United States)",dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],abbreviatedDayNames:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],shortestDayNames:["Su","Mo","Tu","We","Th","Fr","Sa"],firstLetterDayNames:["S","M","T","W","T","F","S"],monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],abbreviatedMonthNames:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],amDesignator:"AM",pmDesignator:"PM",firstDayOfWeek:0,twoDigitYearMax:2029,dateElementOrder:"mdy",formatPatterns:{shortDate:"M/d/yyyy",longDate:"dddd, MMMM dd, yyyy",shortTime:"h:mm tt",longTime:"h:mm:ss tt",fullDateTime:"dddd, MMMM dd, yyyy h:mm:ss tt",sortableDateTime:"yyyy-MM-ddTHH:mm:ss",universalSortableDateTime:"yyyy-MM-dd HH:mm:ssZ",rfc1123:"ddd, dd MMM yyyy HH:mm:ss GMT",monthDay:"MMMM dd",yearMonth:"MMMM, yyyy"},regexPatterns:{jan:/^jan(uary)?/i,feb:/^feb(ruary)?/i,mar:/^mar(ch)?/i,apr:/^apr(il)?/i,may:/^may/i,jun:/^jun(e)?/i,jul:/^jul(y)?/i,aug:/^aug(ust)?/i,sep:/^sep(t(ember)?)?/i,oct:/^oct(ober)?/i,nov:/^nov(ember)?/i,dec:/^dec(ember)?/i,sun:/^su(n(day)?)?/i,mon:/^mo(n(day)?)?/i,tue:/^tu(e(s(day)?)?)?/i,wed:/^we(d(nesday)?)?/i,thu:/^th(u(r(s(day)?)?)?)?/i,fri:/^fr(i(day)?)?/i,sat:/^sa(t(urday)?)?/i,future:/^next/i,past:/^last|past|prev(ious)?/i,add:/^(\+|after|from)/i,subtract:/^(\-|before|ago)/i,yesterday:/^yesterday/i,today:/^t(oday)?/i,tomorrow:/^tomorrow/i,now:/^n(ow)?/i,millisecond:/^ms|milli(second)?s?/i,second:/^sec(ond)?s?/i,minute:/^min(ute)?s?/i,hour:/^h(ou)?rs?/i,week:/^w(ee)?k/i,month:/^m(o(nth)?s?)?/i,day:/^d(ays?)?/i,year:/^y((ea)?rs?)?/i,shortMeridian:/^(a|p)/i,longMeridian:/^(a\.?m?\.?|p\.?m?\.?)/i,timezone:/^((e(s|d)t|c(s|d)t|m(s|d)t|p(s|d)t)|((gmt)?\s*(\+|\-)\s*\d\d\d\d?)|gmt)/i,ordinalSuffix:/^\s*(st|nd|rd|th)/i,timeContext:/^\s*(\:|a|p)/i},abbreviatedTimeZoneStandard:{GMT:"-000",EST:"-0400",CST:"-0500",MST:"-0600",PST:"-0700"},abbreviatedTimeZoneDST:{GMT:"-000",EDT:"-0500",CDT:"-0600",MDT:"-0700",PDT:"-0800"}}; +Date.getMonthNumberFromName=function(name){var n=Date.CultureInfo.monthNames,m=Date.CultureInfo.abbreviatedMonthNames,s=name.toLowerCase();for(var i=0;idate)?1:(this=start.getTime()&&t<=end.getTime();};Date.prototype.addMilliseconds=function(value){this.setMilliseconds(this.getMilliseconds()+value);return this;};Date.prototype.addSeconds=function(value){return this.addMilliseconds(value*1000);};Date.prototype.addMinutes=function(value){return this.addMilliseconds(value*60000);};Date.prototype.addHours=function(value){return this.addMilliseconds(value*3600000);};Date.prototype.addDays=function(value){return this.addMilliseconds(value*86400000);};Date.prototype.addWeeks=function(value){return this.addMilliseconds(value*604800000);};Date.prototype.addMonths=function(value){var n=this.getDate();this.setDate(1);this.setMonth(this.getMonth()+value);this.setDate(Math.min(n,this.getDaysInMonth()));return this;};Date.prototype.addYears=function(value){return this.addMonths(value*12);};Date.prototype.add=function(config){if(typeof config=="number"){this._orient=config;return this;} +var x=config;if(x.millisecond||x.milliseconds){this.addMilliseconds(x.millisecond||x.milliseconds);} +if(x.second||x.seconds){this.addSeconds(x.second||x.seconds);} +if(x.minute||x.minutes){this.addMinutes(x.minute||x.minutes);} +if(x.hour||x.hours){this.addHours(x.hour||x.hours);} +if(x.month||x.months){this.addMonths(x.month||x.months);} +if(x.year||x.years){this.addYears(x.year||x.years);} +if(x.day||x.days){this.addDays(x.day||x.days);} +return this;};Date._validate=function(value,min,max,name){if(typeof value!="number"){throw new TypeError(value+" is not a Number.");}else if(valuemax){throw new RangeError(value+" is not a valid value for "+name+".");} +return true;};Date.validateMillisecond=function(n){return Date._validate(n,0,999,"milliseconds");};Date.validateSecond=function(n){return Date._validate(n,0,59,"seconds");};Date.validateMinute=function(n){return Date._validate(n,0,59,"minutes");};Date.validateHour=function(n){return Date._validate(n,0,23,"hours");};Date.validateDay=function(n,year,month){return Date._validate(n,1,Date.getDaysInMonth(year,month),"days");};Date.validateMonth=function(n){return Date._validate(n,0,11,"months");};Date.validateYear=function(n){return Date._validate(n,1,9999,"seconds");};Date.prototype.set=function(config){var x=config;if(!x.millisecond&&x.millisecond!==0){x.millisecond=-1;} +if(!x.second&&x.second!==0){x.second=-1;} +if(!x.minute&&x.minute!==0){x.minute=-1;} +if(!x.hour&&x.hour!==0){x.hour=-1;} +if(!x.day&&x.day!==0){x.day=-1;} +if(!x.month&&x.month!==0){x.month=-1;} +if(!x.year&&x.year!==0){x.year=-1;} +if(x.millisecond!=-1&&Date.validateMillisecond(x.millisecond)){this.addMilliseconds(x.millisecond-this.getMilliseconds());} +if(x.second!=-1&&Date.validateSecond(x.second)){this.addSeconds(x.second-this.getSeconds());} +if(x.minute!=-1&&Date.validateMinute(x.minute)){this.addMinutes(x.minute-this.getMinutes());} +if(x.hour!=-1&&Date.validateHour(x.hour)){this.addHours(x.hour-this.getHours());} +if(x.month!==-1&&Date.validateMonth(x.month)){this.addMonths(x.month-this.getMonth());} +if(x.year!=-1&&Date.validateYear(x.year)){this.addYears(x.year-this.getFullYear());} +if(x.day!=-1&&Date.validateDay(x.day,this.getFullYear(),this.getMonth())){this.addDays(x.day-this.getDate());} +if(x.timezone){this.setTimezone(x.timezone);} +if(x.timezoneOffset){this.setTimezoneOffset(x.timezoneOffset);} +return this;};Date.prototype.clearTime=function(){this.setHours(0);this.setMinutes(0);this.setSeconds(0);this.setMilliseconds(0);return this;};Date.prototype.isLeapYear=function(){var y=this.getFullYear();return(((y%4===0)&&(y%100!==0))||(y%400===0));};Date.prototype.isWeekday=function(){return!(this.is().sat()||this.is().sun());};Date.prototype.getDaysInMonth=function(){return Date.getDaysInMonth(this.getFullYear(),this.getMonth());};Date.prototype.moveToFirstDayOfMonth=function(){return this.set({day:1});};Date.prototype.moveToLastDayOfMonth=function(){return this.set({day:this.getDaysInMonth()});};Date.prototype.moveToDayOfWeek=function(day,orient){var diff=(day-this.getDay()+7*(orient||+1))%7;return this.addDays((diff===0)?diff+=7*(orient||+1):diff);};Date.prototype.moveToMonth=function(month,orient){var diff=(month-this.getMonth()+12*(orient||+1))%12;return this.addMonths((diff===0)?diff+=12*(orient||+1):diff);};Date.prototype.getDayOfYear=function(){return Math.floor((this-new Date(this.getFullYear(),0,1))/86400000);};Date.prototype.getWeekOfYear=function(firstDayOfWeek){var y=this.getFullYear(),m=this.getMonth(),d=this.getDate();var dow=firstDayOfWeek||Date.CultureInfo.firstDayOfWeek;var offset=7+1-new Date(y,0,1).getDay();if(offset==8){offset=1;} +var daynum=((Date.UTC(y,m,d,0,0,0)-Date.UTC(y,0,1,0,0,0))/86400000)+1;var w=Math.floor((daynum-offset+7)/7);if(w===dow){y--;var prevOffset=7+1-new Date(y,0,1).getDay();if(prevOffset==2||prevOffset==8){w=53;}else{w=52;}} +return w;};Date.prototype.isDST=function(){console.log('isDST');return this.toString().match(/(E|C|M|P)(S|D)T/)[2]=="D";};Date.prototype.getTimezone=function(){return Date.getTimezoneAbbreviation(this.getUTCOffset,this.isDST());};Date.prototype.setTimezoneOffset=function(s){var here=this.getTimezoneOffset(),there=Number(s)*-6/10;this.addMinutes(there-here);return this;};Date.prototype.setTimezone=function(s){return this.setTimezoneOffset(Date.getTimezoneOffset(s));};Date.prototype.getUTCOffset=function(){var n=this.getTimezoneOffset()*-10/6,r;if(n<0){r=(n-10000).toString();return r[0]+r.substr(2);}else{r=(n+10000).toString();return"+"+r.substr(1);}};Date.prototype.getDayName=function(abbrev){return abbrev?Date.CultureInfo.abbreviatedDayNames[this.getDay()]:Date.CultureInfo.dayNames[this.getDay()];};Date.prototype.getMonthName=function(abbrev){return abbrev?Date.CultureInfo.abbreviatedMonthNames[this.getMonth()]:Date.CultureInfo.monthNames[this.getMonth()];};Date.prototype._toString=Date.prototype.toString;Date.prototype.toString=function(format){var self=this;var p=function p(s){return(s.toString().length==1)?"0"+s:s;};return format?format.replace(/dd?d?d?|MM?M?M?|yy?y?y?|hh?|HH?|mm?|ss?|tt?|zz?z?/g,function(format){switch(format){case"hh":return p(self.getHours()<13?self.getHours():(self.getHours()-12));case"h":return self.getHours()<13?self.getHours():(self.getHours()-12);case"HH":return p(self.getHours());case"H":return self.getHours();case"mm":return p(self.getMinutes());case"m":return self.getMinutes();case"ss":return p(self.getSeconds());case"s":return self.getSeconds();case"yyyy":return self.getFullYear();case"yy":return self.getFullYear().toString().substring(2,4);case"dddd":return self.getDayName();case"ddd":return self.getDayName(true);case"dd":return p(self.getDate());case"d":return self.getDate().toString();case"MMMM":return self.getMonthName();case"MMM":return self.getMonthName(true);case"MM":return p((self.getMonth()+1));case"M":return self.getMonth()+1;case"t":return self.getHours()<12?Date.CultureInfo.amDesignator.substring(0,1):Date.CultureInfo.pmDesignator.substring(0,1);case"tt":return self.getHours()<12?Date.CultureInfo.amDesignator:Date.CultureInfo.pmDesignator;case"zzz":case"zz":case"z":return"";}}):this._toString();}; +Date.now=function(){return new Date();};Date.today=function(){return Date.now().clearTime();};Date.prototype._orient=+1;Date.prototype.next=function(){this._orient=+1;return this;};Date.prototype.last=Date.prototype.prev=Date.prototype.previous=function(){this._orient=-1;return this;};Date.prototype._is=false;Date.prototype.is=function(){this._is=true;return this;};Number.prototype._dateElement="day";Number.prototype.fromNow=function(){var c={};c[this._dateElement]=this;return Date.now().add(c);};Number.prototype.ago=function(){var c={};c[this._dateElement]=this*-1;return Date.now().add(c);};(function(){var $D=Date.prototype,$N=Number.prototype;var dx=("sunday monday tuesday wednesday thursday friday saturday").split(/\s/),mx=("january february march april may june july august september october november december").split(/\s/),px=("Millisecond Second Minute Hour Day Week Month Year").split(/\s/),de;var df=function(n){return function(){if(this._is){this._is=false;return this.getDay()==n;} +return this.moveToDayOfWeek(n,this._orient);};};for(var i=0;i0&&!last){try{q=d.call(this,r[1]);}catch(ex){last=true;}}else{last=true;} +if(!last&&q[1].length===0){last=true;} +if(!last){var qx=[];for(var j=0;j0){rx[0]=rx[0].concat(p[0]);rx[1]=p[1];}} +if(rx[1].length1){args=Array.prototype.slice.call(arguments);}else if(arguments[0]instanceof Array){args=arguments[0];} +if(args){for(var i=0,px=args.shift();i2)?n:(n+(((n+2000)Date.getDaysInMonth(this.year,this.month)){throw new RangeError(this.day+" is not a valid value for days.");} +var r=new Date(this.year,this.month,this.day,this.hour,this.minute,this.second);if(this.timezone){r.set({timezone:this.timezone});}else if(this.timezoneOffset){r.set({timezoneOffset:this.timezoneOffset});} +return r;},finish:function(x){x=(x instanceof Array)?flattenAndCompact(x):[x];if(x.length===0){return null;} +for(var i=0;i'; + var id = book.attr("id"); + var comments = $.trim(book.text()).replace(/\n\n/, '
'); + var formats = new Array(); + var size = (parseFloat(book.attr('size'))/(1024*1024)).toFixed(1); + var tags = book.attr('tags').replace(/,/g, ', '); + formats = book.attr("formats").split(","); + if (formats.length > 0) { + for (i=0; i < formats.length; i++) { + title += ''+formats[i]+', '; + } + title = title.slice(0, title.length-2); + title += ' ({0} MB) '.format(size); + } + if (tags) title += '[{0}]'.format(tags); + title += '
'.format(id); + title += '

{0}

'.format(comments) + // Render authors cell + var _authors = new Array(); + var authors = ''; + _authors = book.attr('authors').split('|'); + for (i = 0; i < _authors.length; i++) { + authors += jQuery.trim(_authors[i]).replace(/ /g, ' ')+'
'; + } + if (authors) { authors = authors.slice(0, authors.length-6); } + + // Render rating cell + var _rating = parseFloat(book.attr('rating'))/2.; + var rating = ''; + for (i = 0; i < _rating; i++) { rating += '★'} + + // Render date cell + var _date = Date.parseExact(book.attr('timestamp'), 'yyyy/MM/dd HH:mm:ss'); + var date = _date.toString('d MMM yyyy').replace(/ /g, ' '); + + // Render series cell + var series = book.attr("series") + if (series) { + series += ' [{0}]'.format(book.attr('series_index')); + } + + var cells = { + 'title' : title, + 'authors' : authors, + 'rating' : rating, + 'date' : date, + 'series' : series + }; + + var row = ''; + for (i = 0; i < cmap.length; i++) { + row += '{1}'.format(cmap[i], cells[cmap[i]]); + } + return '{1}'.format(id, row); +} + +function fetch_library_books(start, num, timeout, sort, order, search) { + // null, 0, false are False + data = {"start":start+'', "num":num+''}; + if (sort) { data["sort"] = sort; } + if (search) { data["search"] = search; } + if (order) { data['order'] = order; } + last_num = num; + last_start = start; + last_search = search; + last_sort = sort; + last_sort_order = order; + + if (current_library_request != null) { + current_library_request.abort(); + current_library_request = null; + } + + $('#loading').css('visibility', 'visible'); + + current_library_request = $.ajax({ + type: "GET", + url: "/library", + data: data, + cache: false, + timeout: timeout, //milliseconds + dataType: "xml", + + error : function(XMLHttpRequest, textStatus, errorThrown) { + alert('Error: '+textStatus+'\n\n'+errorThrown); + }, + + success : function(xml, textStatus) { + var library = $(xml).find('library'); + total = parseInt(library.attr('total')); + var num = parseInt(library.attr('num')); + var start = parseInt(library.attr('start')); + update_count_bar(start, num, total); + var display = ''; + library.find('book').each( function() { + var book = $(this); + var row = render_book(book); + display += row+'\n\n'; + }); + $("#book_list tbody").html(display); + $("#book_list tbody tr").mouseover(function() { + var row = $(this); + var cover = row.find('img').attr('src'); + row.css('background-color', "#fff2a8"); + row.find('.comments').css('display', 'inherit'); + $('#cover_pane img').attr('src', cover); + $('#cover_pane').css('visibility', 'visible'); + row.bind('mouseout', function(){ + row.css('background-color', "white"); + row.find('.comments').css('display', 'none'); + $('#book_list tbody tr:even()').css('background-color', '#eeeeee'); + row.unbind('mouseout'); + }); + }); + $('#book_list').mouseout(function(){ + $('#cover_pane').css('visibility', 'hidden') + }); + + layout(); + $('#book_list tbody tr:even()').css('background-color', '#eeeeee'); + }, + + complete : function(XMLHttpRequest, textStatus) { + current_library_request = null; + document.getElementById('main').scrollTop = 0; + $('#loading').css('visibility', 'hidden'); + } + + }); + +} + + +////////////////////////////// COUNT BAR ////////////////////////////// + +function update_count_bar(start, num, total) { + var cb = $('#count_bar'); + cb.find('#count').html('Books {0} to {1} of {2}'.format(start+1, start+num, total)); + var left = cb.find('#left'); + left.css('opacity', (start <= 0) ? 0.3 : 1); + var right = cb.find('#right'); + right.css('opacity', (start + num >= total) ? 0.3 : 1); + +} + +function setup_count_bar() { + $('#count_bar * img:eq(0)').click(function(){ + if (last_start > 0) { + fetch_library_books(0, last_num, LIBRARY_FETCH_TIMEOUT, last_sort, last_sort_order, last_search); + } + }); + + $('#count_bar * img:eq(1)').click(function(){ + if (last_start > 0) { + var new_start = last_start - last_num; + if (new_start < 0) { + new_start = 0; + } + fetch_library_books(new_start, last_num, LIBRARY_FETCH_TIMEOUT, last_sort, last_sort_order, last_search); + } + }); + + $('#count_bar * img:eq(2)').click(function(){ + if (last_start + last_num < total) { + var new_start = last_start + last_num; + fetch_library_books(new_start, last_num, LIBRARY_FETCH_TIMEOUT, last_sort, last_sort_order, last_search); + } + }); + + $('#count_bar * img:eq(3)').click(function(){ + if (total - last_num > 0) { + fetch_library_books(total - last_num, last_num, LIBRARY_FETCH_TIMEOUT, last_sort, last_sort_order, last_search); + } + }); +} + +////////////////////////////// SEARCH ///////////////////////////////////////// + +function search() { + var search = $.trim($('#search_box * #s').val()); + fetch_library_books(last_start, last_num, LIBRARY_FETCH_TIMEOUT, + last_sort, last_sort_order, search); +} + + +/////////////////////////// SORTING ///////////////////////////////////// + +function setup_sorting() { + $('table#book_list thead tr td').mouseover(function() { + this.style.backgroundColor = "#fff2a8"; + }); + + $('table#book_list thead tr td').mouseout(function() { + this.style.backgroundColor = "inherit"; + }); + + for (i = 0; i < cmap.length; i++) { + $('table#book_list span#{0}_sort'.format(cmap[i])).parent().click(function() { + var sort_indicator = $($(this).find('span')); + var cell = $(sort_indicator.parent()); + var id = sort_indicator.attr("id"); + var col = id.slice(0, id.indexOf("_")); + var order = 'ascending'; + var html = '↑'; + + if (sort_indicator.html() == '↑') { + order = 'descending'; html = '↓'; + } + sort_indicator.html(html); + $('#book_list * .sort_indicator').css('visibility', 'hidden'); + sort_indicator.css('visibility', 'visible'); + fetch_library_books(last_start, last_num, LIBRARY_FETCH_TIMEOUT, col, order, last_search); + }); + } +} + +///////////////////////// STARTUP //////////////////////////////////////// + +function layout() { + var main = $('#main'); var cb = $('#count_bar'); + main.css('height', ($(window).height() - main.offset().top - 20)+'px') + main.css('width', ($(window).width() - main.offset().left - 15)+'px') + cb.css('right', '20px'); + cb.css('top', (main.offset().top - cb.height()-5)+'px'); + $('#loading').css('height', ($(window).height()-20)+'px'); + $('#loading').css('width', ($(window).width()-20)+'px'); + var cover = $('#cover_pane'); + cover.css('width', (main.width()/2.0)+'px') + cover.css('height', main.height()+'px') + cover.css('left', (main.offset().left + main.width()/2.0)+'px'); + cover.css('top', main.offset().top+'px'); +} + +$(function() { + // document is ready + create_table_headers(); + + // Setup widgets + setup_sorting(); + setup_count_bar(); + $('#search_box * #s').val(''); + $(window).resize(layout); + + $($('#book_list * span#date_sort').parent()).click(); + +}); \ No newline at end of file diff --git a/src/calibre/library/static/index.html b/src/calibre/library/static/index.html new file mode 100644 index 0000000000..45c902a080 --- /dev/null +++ b/src/calibre/library/static/index.html @@ -0,0 +1,49 @@ + + + + + calibre library + + + + + + + + + + + +
+ Show first set of books Show previous set of books              Show first set of books Show previous set of books +
+ +
+ + + + + + + +
+
+ +
+
+ Loading... Loading… +
+
+ +
+ Cover +
+ + diff --git a/src/calibre/library/static/last.png b/src/calibre/library/static/last.png new file mode 100644 index 0000000000..3967fa4e52 Binary files /dev/null and b/src/calibre/library/static/last.png differ diff --git a/src/calibre/library/static/next.png b/src/calibre/library/static/next.png new file mode 100644 index 0000000000..fed82945ce Binary files /dev/null and b/src/calibre/library/static/next.png differ diff --git a/src/calibre/library/static/previous.png b/src/calibre/library/static/previous.png new file mode 100644 index 0000000000..457d873c55 Binary files /dev/null and b/src/calibre/library/static/previous.png differ