From 1e592badd1390e9b45c4c94097cec799455a2b9b Mon Sep 17 00:00:00 2001 From: Fred Chasen Date: Sat, 26 Jan 2013 21:01:14 -0800 Subject: [PATCH] added loading from zipped epub file --- .DS_Store | Bin 21508 -> 21508 bytes css/main.css | 9 +++ fpjs/reader/app.js | 10 +++ fpjs/render/book.js | 122 ++++++++++++++++++++++-------- fpjs/render/chapter.js | 4 +- fpjs/render/core.js | 18 +++++ fpjs/render/storage.js | 5 ++ fpjs/render/storage_filesystem.js | 89 +++++++++++++++++----- fpjs/render/storage_indexeddb.js | 4 +- fpjs/render/storage_websql.js | 3 +- fpjs/render/unarchiver.js | 77 +++++++++++++++++++ img/loader.gif | Bin 0 -> 6820 bytes index.html | 4 +- 13 files changed, 288 insertions(+), 57 deletions(-) create mode 100644 fpjs/render/unarchiver.js create mode 100644 img/loader.gif diff --git a/.DS_Store b/.DS_Store index 25894d536a0017c5b054a29cae26c2ad60421f12..ec0eb78c788704e8537b35e9ad5ca68f3321fb52 100644 GIT binary patch delta 169 zcmZo!!Pv5bae_ai)W(3_{EPyVc?8TSD=MV3n;RSIC|DRw?pJV`EGQ|wnNv`ULza8qD delta 76 zcmV-S0JHyur~!ni0gz7t7_m^h4*?F73=lVy^ciOeEjKqTATcsGld%vSlYbg0lN1pX ivj`C?2(#iDDF%^%ud@pv_6f7F8e0XCfUC0yF#RK!&>2kt diff --git a/css/main.css b/css/main.css index 100f160..4ef42a5 100755 --- a/css/main.css +++ b/css/main.css @@ -173,6 +173,15 @@ input:-moz-placeholder { top: 10%; opacity: .15; box-shadow: -2px 0 15px rgba(0, 0, 0, 1); + display: none; +} + +#loader { + position: absolute; + z-index: 10; + left: 50%; + top: 50%; + margin: -33px 0 0 -33px; } #toc { diff --git a/fpjs/reader/app.js b/fpjs/reader/app.js index 0c88ca8..e230238 100644 --- a/fpjs/reader/app.js +++ b/fpjs/reader/app.js @@ -33,6 +33,7 @@ FPR.app.init = (function($){ //-- Full list of event are at start of book.js Book.listen("book:metadataReady", meta); Book.listen("book:tocReady", toc); + Book.listen("book:bookReady", bookReady); Book.listen("book:chapterReady", chapterChange); Book.listen("book:online", goOnline); Book.listen("book:offline", goOffline); @@ -114,7 +115,16 @@ FPR.app.init = (function($){ }); return $container; } + + function bookReady(){ + var $divider = $("#divider"), + $loader = $("#loader"); + + $loader.hide(); + $divider.show(); + } + function goOnline(){ var $icon = $("#store"); offline = false; diff --git a/fpjs/render/book.js b/fpjs/render/book.js index 7ce5a37..a4b3155 100644 --- a/fpjs/render/book.js +++ b/fpjs/render/book.js @@ -1,4 +1,4 @@ -FP.Book = function(elem, bookUrl){ +FP.Book = function(elem, bookPath){ //-- Takes a string or a element if (typeof elem == "string") { @@ -36,8 +36,8 @@ FP.Book = function(elem, bookUrl){ this.determineStorageMethod(); // BookUrl is optional, but if present start loading process - if(bookUrl) { - this.display(bookUrl); + if(bookPath) { + this.display(bookPath); } @@ -73,29 +73,38 @@ FP.Book.prototype.listeners = function(){ } //-- Check bookUrl and start parsing book Assets or load them from storage -FP.Book.prototype.start = function(bookUrl){ +FP.Book.prototype.start = function(bookPath){ var pathname = window.location.pathname, folder = (pathname[pathname.length - 1] == "/") ? pathname : "/"; - this.bookUrl = (bookUrl[bookUrl.length - 1] == "/") ? bookUrl : bookUrl + "/"; + this.bookPath = bookPath; + - if(this.bookUrl.search("://") == -1){ - //-- get full path - this.bookUrl = window.location.origin + folder + this.bookUrl; - } - - //-- TODO: Checks if the url is a zip file and unpack - if(this.isContained(bookUrl)){ - console.error("Zipped!"); + //-- Checks if the url is a zip file and unpack + if(this.isContained(bookPath)){ + this.bookUrl = ""; + this.contained = true; + this.tell("book:offline"); + if(this.online) this.unarchive(bookPath); + return; + }else{ + this.bookUrl = (bookPath[bookPath.length - 1] == "/") ? bookPath : bookPath + "/"; + + if(this.bookUrl.search("://") == -1){ + //-- get full path + this.bookUrl = window.location.origin + folder + this.bookUrl; + } } if(!this.isSaved()){ + + if(!this.online) { + console.error("Not Online"); + return; + } + //-- Gets the root of the book and url of the opf - this.parseContainer(function(){ - //-- Gets all setup of the book from xml file - //-- TODO: add promise for this instead of callback? - //this.parseContents(); - }); + this.parseContainer(); }else{ //-- Events for elements loaded from storage @@ -109,15 +118,42 @@ FP.Book.prototype.start = function(bookUrl){ } +FP.Book.prototype.unarchive = function(bookPath){ + var unzipped; + + //-- TODO: make more DRY + + if(!this.isSaved()){ + + unzipped = new FP.Unarchiver(bookPath, function(){ + + FP.storage.get("META-INF/container.xml", function(url){ + this.parseContainer(url); + }.bind(this)); + + }.bind(this)); + + }else{ + //-- Events for elements loaded from storage + this.tell("book:tocReady"); + this.tell("book:metadataReady"); + this.tell("book:spineReady"); + + //-- Info is saved, start display + this.startDisplay(); + } + +} + FP.Book.prototype.isSaved = function(force) { //-- If url or version has changed invalidate stored data and reset - if (localStorage.getItem("bookUrl") != this.bookUrl || + if (localStorage.getItem("bookPath") != this.bookPath || localStorage.getItem("fpjs-version") != FP.VERSION || force == true) { localStorage.setItem("fpjs-version", FP.VERSION); - localStorage.setItem("bookUrl", this.bookUrl); + localStorage.setItem("bookPath", this.bookPath); localStorage.setItem("spinePos", 0); localStorage.setItem("stored", 0); @@ -193,9 +229,10 @@ FP.Book.prototype.resizeIframe = function(e, cWidth, cHeight){ this.iframe.width = width; } -FP.Book.prototype.parseContainer = function(callback){ +FP.Book.prototype.parseContainer = function(path){ var that = this, - url = this.bookUrl + "META-INF/container.xml"; + url = path || this.bookUrl + "META-INF/container.xml"; + FP.core.loadXML(url, function(container){ var fullpath; @@ -205,22 +242,36 @@ FP.Book.prototype.parseContainer = function(callback){ fullpath = rootfile.getAttribute('full-path').split("/"); that.basePath = that.bookUrl + fullpath[0] + "/"; - that.contentsPath = fullpath[1]; + + if(that.contained){ + that.basePath = fullpath[0] + "/"; + } + + that.contentsPath = that.basePath + fullpath[1]; localStorage.setItem("basePath", that.basePath); localStorage.setItem("contentsPath", that.contentsPath); //-- Now that we have the path we can parse the contents //-- TODO: move this and handle errors - that.parseContents(that.contentsPath); + + if(that.contained){ + FP.storage.get(that.contentsPath, function(url){ + that.parseContents(url); + }); + }else{ + //-- Gets the root of the book and url of the opf + that.parseContents(); + } + }); } -FP.Book.prototype.parseContents = function(){ +FP.Book.prototype.parseContents = function(path){ var that = this, - url = this.basePath + this.contentsPath; - + url = path || this.contentsPath; + FP.core.loadXML(url, function(contents){ var metadata = contents.querySelector("metadata"), manifest = contents.querySelector("manifest"), @@ -265,7 +316,14 @@ FP.Book.prototype.parseManifest = function(manifest){ //-- Find NCX: media-type="application/x-dtbncx+xml" href="toc.ncx" if(item.getAttribute('media-type') == "application/x-dtbncx+xml"){ - that.parseTOC(href); + if(that.contained){ + FP.storage.get(that.basePath + href, function(url){ + that.parseTOC(url); + }); + }else{ + that.parseTOC(that.basePath + href); + } + } }); @@ -301,10 +359,10 @@ FP.Book.prototype.parseSpine = function(spine){ FP.Book.prototype.parseTOC = function(path){ var that = this, - url = this.basePath + path; + url = path; this.toc = []; - + FP.core.loadXML(url, function(contents){ var navMap = contents.querySelector("navMap"), cover = contents.querySelector("meta[name='cover']"), @@ -396,7 +454,7 @@ FP.Book.prototype.startDisplay = function(){ this.displayChapter(this.spinePos, function(chapter){ //-- If there is network connection, store the books contents - if(this.online){ + if(this.online && !this.contained){ this.storeOffline(); } @@ -525,6 +583,8 @@ FP.Book.prototype.availableOffline = function() { FP.Book.prototype.fromStorage = function(stored) { + if(this.contained) return; + if(!stored){ this.online = true; this.tell("book:online"); diff --git a/fpjs/render/chapter.js b/fpjs/render/chapter.js index a5d8371..d4d7412 100644 --- a/fpjs/render/chapter.js +++ b/fpjs/render/chapter.js @@ -24,7 +24,7 @@ FP.Chapter = function(book, pos){ FP.Chapter.prototype.load = function(){ var path = this.path; - if(this.book.online){ + if(this.book.online && !this.book.contained){ this.setIframeSrc(path); }else{ this.loadFromStorage(path); @@ -214,7 +214,7 @@ FP.Chapter.prototype.replaceResources = function(callback){ //-- No need to replace if there is network connectivity //-- also Filesystem api links are relative, so no need to replace them - if(this.book.online || FP.storage.getStorageType() == "filesystem") { + if((this.book.online && !this.book.contained) || FP.storage.getStorageType() == "filesystem") { if(callback) callback(); return false; } diff --git a/fpjs/render/core.js b/fpjs/render/core.js index 8eeee54..a1b0f1d 100644 --- a/fpjs/render/core.js +++ b/fpjs/render/core.js @@ -163,3 +163,21 @@ FP.core.dataURLToBlob = function(dataURL) { return new Blob([uInt8Array], {type: contentType}); } +//-- Load scripts async: http://stackoverflow.com/questions/7718935/load-scripts-asynchronously +FP.core.loadScript = function(src, callback) { + var s, r; + r = false; + s = document.createElement('script'); + s.type = 'text/javascript'; + s.async = true; + s.src = src; + s.onload = s.onreadystatechange = function() { + //console.log( this.readyState ); //uncomment this line to see which ready states are called. + if ( !r && (!this.readyState || this.readyState == 'complete') ) + { + r = true; + callback(); + } + }; + document.body.appendChild(s); + } diff --git a/fpjs/render/storage.js b/fpjs/render/storage.js index cf684a4..11537e2 100644 --- a/fpjs/render/storage.js +++ b/fpjs/render/storage.js @@ -35,6 +35,10 @@ FP.storage = function(){ return this._store.getURL(path); } + function save(path, file, callback) { + return this._store.save(path, file, callback); + } + function _error(err){ console.log("error", err); @@ -50,6 +54,7 @@ FP.storage = function(){ "batch" : batch, "storageMethod": storageMethod, "getURL": getURL, + "save" : save, "getStorageType" : getStorageType } diff --git a/fpjs/render/storage_filesystem.js b/fpjs/render/storage_filesystem.js index f3e3a12..5ed2d61 100644 --- a/fpjs/render/storage_filesystem.js +++ b/fpjs/render/storage_filesystem.js @@ -49,14 +49,12 @@ FP.store.filesystem = function() { var url; //-- should only be checking urls? but blank on reload? if(fromCache){ - //console.log("c") url = getURL(path, fromCache); if(typeof(callback) != "undefined"){ callback(url); } }else{ _queue.add(path, function(url){ - console.log("url", url) check(url, function(file){ url = getURL(path, file); if(typeof(callback) != "undefined"){ @@ -103,24 +101,46 @@ FP.store.filesystem = function() { xhr.start(); } - function save(path, file) { - var entry = {"path" : path, "file": file}, - request; + function save(path, file, callback) { + openFs(function(fs){ + var base = path.split('/').slice(0,-1); + createDir(fs.root, base); - var transaction = _db.transaction(["files"], "readwrite"); - var store = transaction.objectStore("files"); - request = store.put(entry); - - request.onerror = function(event) { - console.log("failed: " + event.target.errorCode); - }; - - request.onsuccess = function(event) { - //-- Do nothing for now - console.log("saved", path); - }; + fs.root.getFile(path, {create: true}, + function(fileEntry) { + + fileEntry.createWriter(function(fileWriter) { + + fileWriter.onwriteend = function(e) { + if(callback) callback(e); + }; + + fileWriter.onerror = function(e){ + _error(err); + }; + + fileWriter.write(file); + + }); + + }, _error ); + }); } - + + function createDir(rootDirEntry, folders) { + // Throw out './' or '/' and move on to prevent something like '/foo/.//bar'. + if (folders[0] == '.' || folders[0] == '') { + folders = folders.slice(1); + } + + rootDirEntry.getDirectory(folders[0], {create: true}, function(dirEntry) { + // Recursively add the new subfolder (if we still have another to create). + if (folders.length) { + createDir(dirEntry, folders.slice(1)); + } + }, _error); + } + //-- end worker function getURL(path, fileEntry){ @@ -139,17 +159,44 @@ FP.store.filesystem = function() { function _error(err){ if(typeof(this.failed) == "undefined"){ - console.log("Error: ", err); + console.log("Error: ", errorHandler(err)); }else{ this.failed(err); } } - + + function errorHandler(e) { + switch (e.code) { + case FileError.QUOTA_EXCEEDED_ERR: + return 'QUOTA_EXCEEDED_ERR'; + break; + case FileError.NOT_FOUND_ERR: + return 'NOT_FOUND_ERR'; + break; + case FileError.SECURITY_ERR: + return 'SECURITY_ERR'; + break; + case FileError.INVALID_MODIFICATION_ERR: + return 'INVALID_MODIFICATION_ERR'; + break; + case FileError.INVALID_STATE_ERR: + return 'INVALID_STATE_ERR'; + break; + case FileError.TYPE_MISMATCH_ERR: + return 'TYPE_MISMATCH_ERR'; + break; + default: + return 'Unknown Error:' + e.code ; + break; + } + } return { "get" : get, "preload" : preload, - "batch" : batch + "batch" : batch, + "getURL" : getURL, + "save" : save } } diff --git a/fpjs/render/storage_indexeddb.js b/fpjs/render/storage_indexeddb.js index b44c920..e6d753a 100644 --- a/fpjs/render/storage_indexeddb.js +++ b/fpjs/render/storage_indexeddb.js @@ -197,6 +197,8 @@ FP.store.indexedDB = function() { return { "get" : get, "preload" : preload, - "batch" : batch + "batch" : batch, + "getURL" : getURL, + "save" : save } } diff --git a/fpjs/render/storage_websql.js b/fpjs/render/storage_websql.js index afefa33..51e3a03 100644 --- a/fpjs/render/storage_websql.js +++ b/fpjs/render/storage_websql.js @@ -202,6 +202,7 @@ FP.store.websql = function() { "get" : get, "preload" : preload, "batch" : batch, - "getURL" : getURL + "getURL" : getURL, + "save" : save } } diff --git a/fpjs/render/unarchiver.js b/fpjs/render/unarchiver.js new file mode 100644 index 0000000..4f24f52 --- /dev/null +++ b/fpjs/render/unarchiver.js @@ -0,0 +1,77 @@ +FP.Unarchiver = function(url, callback){ + this.libPath = "fpjs/libs/"; + this.zipUrl = url; + this.callback = callback; + this.loadLib(function(){ + this.getZip(this.zipUrl); + }.bind(this)); +} + +FP.Unarchiver.prototype.loadLib = function(callback){ + if(typeof(zip) != "undefined") callback(); + //-- load script + FP.core.loadScript(this.libPath+"zip.js", function(){ + //-- Tell zip where it is located + zip.workerScriptsPath = this.libPath; + callback(); + }.bind(this)); +} + +FP.Unarchiver.prototype.getZip = function(zipUrl){ + var xhr = new FP.core.loadFile(zipUrl); + + xhr.succeeded = function(file) { + this.getEntries(file, this.toStorage.bind(this)); + }.bind(this); + + xhr.failed = this.failed; + + xhr.start(); + +} + +FP.Unarchiver.prototype.getEntries = function(file, callback){ + zip.createReader(new zip.BlobReader(file), function(zipReader) { + zipReader.getEntries(callback); + }, this.failed); +} + +FP.Unarchiver.prototype.failed = function(error){ + console.log("Error:", error); +} + +FP.Unarchiver.prototype.afterSaved = function(error){ + this.callback(); +} + +FP.Unarchiver.prototype.toStorage = function(entries){ + var timeout = 0, + delay = 20, + that = this, + count = entries.length; + + function callback(){ + count--; + if(count == 0) that.afterSaved(); + } + + entries.forEach(function(entry){ + + setTimeout(function(entry){ + that.saveEntryFileToStorage(entry, callback); + }, timeout, entry); + + timeout += delay; + }); + + console.log("time", timeout); + + //entries.forEach(this.saveEntryFileToStorage.bind(this)); +} + +FP.Unarchiver.prototype.saveEntryFileToStorage = function(entry, callback){ + var that = this; + entry.getData(new zip.BlobWriter(), function(blob) { + FP.storage.save(entry.filename, blob, callback); + }); +} \ No newline at end of file diff --git a/img/loader.gif b/img/loader.gif new file mode 100644 index 0000000000000000000000000000000000000000..68005bcbe63e2924bc8f6eccf5175db07490d3a7 GIT binary patch literal 6820 zcma)>Wmwa1--pMFfy5XC#vHhT(w##QM|U$E(ka~y>gZ9@jf9jUNGc&BAsvDsARvMu zB`6{wCJ%c5pLfswKc4ILb^NY3=W%|%pYKmyQw=X?s{~L2d;tJXPEOq1+_JN?8yXt6 zx3>od2EKp)9u^k1u&@C5zYgx?jGn5Av9h9ps)UFbi0bqX?l)T5*U{GB(E%Ie;_r;L zb+rw4_p)^maq)EW0^q)ZX=qPx3H|xnpGN`U6ZoL!gQq5#(I+P?KFFB{VxKM9T|1uF zF`Ig2sl;~PSEyUC5H5xyNtIhUdc#SuZk&1$p(H=c%*jL?9&Dti1&08#m<3SFF|n7R z$qm=`6)t8P{#dNn=m1H&5n(78P>&qeqlNmegV?Y-Ry ze%aE9mOwm7(E*;PsSEPLB^eE|9T3@1YUlp@ZeZV_>8A1q0G!YY=U!V6MI; zN7%D`I?|v4Q)D6=O}?Voa!y79<^-c8r$)dH^8e7Qwis%x(7pn+lZj{lQgJ>BFBj_W zi%lCV#Cyl6z!A-h@`xpb*`JQ4~n67#@S#=q`o$`aEI@pdn6 z^5xCg3}fK@J%a44ugY_$Gr5R^db~4_Ps;TyVLk+-Y$!Ox&DCvc<+@kj(3@$WeK-iR z|6P3IGl#-xfO-Hl9MtTDD})3uO_7F;X7V(f*pqv_QTN9Y1O2vMhbhBl62-t@!NXh1 zG}byh2ENHQ1C65i{jppnC(Xwn9DglNnx#prT3yI#K6_66ZQ+f$x;QKR*Y|TYH)4iA zBj0Rt4$mCvorK0D+Otd!U_xEaM;D7tnGu%3x*VI-Mzl&0vIzrp?VLQ*DNg?t7}y_R zaL5@S(qCYhoI*@_{s8mP8ggRSo@9mBf?E#ph{ z0ViuWwDI+&MZpYFpC_<31S$&C#-urKX%`>?eX{JhQ}1Kh%?4A5#SGyryS+GI77p=L z80CDt-}d5AQkeA2QZ8G@zS88rRF&Z^q&&}^ z*UT2ixAO#8H`N}kae13a_d7_ zzOwUq=e0gQcyzozkQ}CDedinh_g&RhCT>B_S~lH_QlE_$;dOIR8-4H|o9WdRnm%nQ zgx3C&#r7^U)&Gl|7@JekFb$aIy;p3-JHw8`J1dEvFjT5QR9c-8o=zx=x1os{n>&8N z(HpM^Z+79WxQnt=(j@tQ)Sb8bT!E(E^URP2@Dxam_+o6x2wfjdq^^-W&((-9NU%9D zF$|c_2Fo!QXN7`pi#WI-xC)HLJiU0bgovPAK&)K?i$@in7!82J%H^Jxb151Zqo1WX z@lX%8X+9ZBa!%NkaZlh5U&-|U%K)QtH# z%p8*n_dXB^n68^A3<7jip0*6&;B=LU%?U^Za-{z9lufy!gNpgn%XgiYrx=p5w#tZ}bAtJWmiLN|PmbD2OjivG7~v2bIp*P8@mgWp5wf zS9jW_krfeAapKuP)gS%b(q06=yk;Ra(Pc)c4o_@V&VK5IngX*~O{j&dWK3e8ljK~p z;N&xSAq)2_o!uFi8#)XS#3S+`kzp@xnbB3~}9vX%{OKq_Z4dLf2 zg6ypvU}ytcr>#||R{jkc|Np24o9#{di%h)vh3A=?r^t-D-RSHnG%(^LN%f<3Bk^D)ajY7K_LJ?V^Rz_NmX7F-3qx`<{-~yl1=^Hvmb;)#qAo(^m_g>(ws`Cs^3kC5 zkH%ITM;D!Tzb0(>uN)J1-pbxOLFf%uPN0B-l%)^I)K_zxkamDl>6CdzjXLy?s{;`mK)=3tub}PTIzsY41 zx6RSs=XKtM>M7wUIH=eBh()uN8 zH0x#A@ZlH39SdiI@OvM69}Cf-AU9!rok(4{|C1R(%DLij_TX>4aU!Y+t`vW|(r3_>s zhC^Nw8m5IhF4Uz=Ksxa+NAB&q6LB@ijZMBDonnAWYHWxfrD=jRXP9;b#8kxXVMB#(H6UCOJuCJ8{QD z*NAwTA&hPi3)Ngjn$gSlb^n|xyM?)VW+ut#SM6h6X7&xc)e9M1O1{$$hc!AVTvcZ( zrKQ9w>)o7-TOr65=G}Be|Iu_8;26bDuP+HVdgwIyMx*S?*%DO0 z&Ts-l;r|jq|ImWXcEUwbJrq*S&8jCx5t{LQl?U?d5~l$~NEL`N9OYGn(PjfP=U%_z zySD|F(d6XS<2l?HfL(YZNl;^=nfBa0w%a>;h!7!n^+Pu}MCEO?%yrB>fIw?MFw7MZ z$P9)AJAkqw`m6!jf@Z0v_Te(P1Yf}bqg1{mIlYITQTPCmlLt7pIVJvi{(t#Tv3TqE2V1pTD&6?;{VKSH6mm{PT9y%;dZ}#R_ZTP==yN7{M z>mLw59T&n$APxA{ZU-Q@Cez#}EL3Y7tl_=pVXGV`irih`9RR3$F`Qg-&WuRXdziZR zvGmWRJCXwvY>(1ZT#ZQn(Y{qEle`Y5l8u07Z8qJTEYnyC!Jv$;v#4Ku-GB`3%D#|b zc+xf+EV+r~R@39Dg=F4~NK~g0)1B|(zUIv}lP|S@l;2tHJ4tXHG*X#OKkS-``O0-~ zIzBS_VjI5`;w?NX&KY8`0jOq#ZUQ^bhLGmQg~&T2KMuqRAVp2;-$AVY1?hJgTgTne z4clix)uzjm7vs1auDJf_+)4ET!s*pH`8!fyg_wcLQ3Te-RX(0}ADwyzNA=P4DaPCG%&?JlU zV&uvO4@ zv_7@Fyhh;1!NQFgl(;hiXB+`jn4fvAwr5zZmW8_ksLr|DelQ1Jb9O^LPoU{dD9i<+ zDeBBYjt2x7IuYgU)2;+000J=QVnMRKH$FBlu;l&~aIpxqx$cpWU;9mW>!(l9 zg`GH_dfUXjJ{GG%Y(_D(`(`T+R*52WmyL?>178~#BrQD#t;n2P;GSOR+Ir5}f_}5k zGFSQ0cjoKr(QlwtE>9{umEZ&Tf#95-db`L2HPmZfxLRxCyHN~AJhGJ9f9qt}r&U#a z`FskW@o6>ktw_reGtb78Ki_4#P2)ITGGyEpart7@X*HVW)-1ILxSoeg-g^<8ncHJR zdRg38^v;U2#|kE20=z zSo`jHB1)lbaVNc}t3yzoajK-c^NH*Zm5MJ$`w|Cpf-ui z@&>|~h|rILS)m+=Vcro!DzL;`O2JY%4_Ti)7KcVa9{Yt&i-N%%8 zP)A!!ZF|-uichDPO+TATllRa-y|GSOSGST)2%GWrD?x{jIL_RZ@mH}OW!OgjwQh^v zhS5b1|Pc-(@IBc$XWcmG=bTyQh|leSB#f&4199XpXc z1$&|ll%Lagh@k=+RiAI&NgD~}GL$i4?=L?caQ#9vvtDr0=88B7gSHKMy49_(eIJ1# zMzyHKvd6dO(kP>1b^fts)d5yBcd|`8_6LY`k(m$nG=1X!5sYx)~tk^=Dx>q+Z`F)T>` z?NGQqVMVooJ~7^sai6La+u9`%{(Waf7+618bABEg*;8pzte4l_oH+O_o@G6;P-#Up z-Cm_Jh@0_OBLQdfpBG;Kw-SxS{ZUH<+|-by@^|ZE9#h!3y;S0CtSZl5$>Isa zrV2zG(!XwUrf}wZrypqQJmO|KvujcXaEaxi`Z&zv@fBzca#~*3@4Q_18trZLYw_Bc zrvVgfg9sCa#o)AC%jn9w@LfKI3Tp#M034-BuC8tp(0keJT^9#7KZQoLKt-B54*=c(X;_5 zgp^iTu$NVlJtP9|fk|Ng(O`*74ELxRC`1_*#^$v+EZ^)oTSoV%FctDb0BU$2xp!!} zkU0e?AJa}F4_BcmGYD};k#f_z;Qd`C?0YkKG9xT_^n>VoZ$Htifr~!8Z`is?6fwuV ztLwCpPc|_ha=y^tE#`+Wo8>64&N)-PH8;2ACDJ&4%jU>OKupYF!I7GmyMZl%Xs;l9k0uqSvtOld%s$ucaWrE?+0y=oMSsd4JXDs19KtM z9LrGl5K+p&q&tvQZ|&9UGCg+-tZ?p}lZOSTgeasQGA)vXHR4XoHn23$PZKm!ae!6`8@XCNs8p|y21H7E zRAn}cSS1rWC1qH}UdX3rgL_h?PAy6Zi!9WZQqSFFj z_{S_gXElr*APHxTS%WpwOEe|~pK7aGO_RTtsuTtlexq z7qgl;BE1yhfoFlfuCGCJ@)#RlQR*c9cfM}(P%3-c$NVq~Uh%l>eS^&o^n%D}A+x%ZLyK?zLwj4K~+80%>3~FZL+E3FSxH&%Yv&RDjqZ zGGt;hddXw*EIUvf7Aq|_N>D- z#8ZPRY@XsOxH3=)IgmbGKFAOXmXnw{ZVsEQW`Vo{w$3Sn7bxezBOUM9)-qPu=0A-` ze>fC;e&j@X`|?}U#N>J81W`Em;w|51RrBEM%d*iNtf3MEo?VI*%Qf4@!9Sn~oxoTv z!0D0CGq}#s^2y=m$v{zS8T-GWylb{x8;l3FIiA<9loG$ar0>o_i*K`s z!Zxz+nO;imb#+U)?=MeHns)A5i2z!RX)s-y0Yz5L!n0WxUPQL2Ub^(VnbN($%m0lp zcBMd@u!P@?ekxIN#^N%_f2to&FxwY+oE;^l)<=&lY)kNfm+a(5PUaE5m)wqg9Xt6_ zjc))F!4Qb7mY&GED~|=)(rxB;?d%FlW)1T|YFG=xrPs<+`0Jq90-z(CMoJv3?8K4t z!t54G^}vVjMw5}g=WBBz;a_)WE!8CikgDrWSF$n!u~~}G_}jxXeY2)7y<@z1CZIN zgV^|-W0NxQAY$l^B;mkND9l0@s^Ngli%^a&$qE$&S2{x>bw$voyK%}D#5)w7hwg^J z$CT2${pA{IT{HOanHNA?>w$L_&q4DR*DT5;K$_+hWyJ z7md6BX+rq(J-IMHU?dRlr+_4v^U@K;yl7tqH&&iDp^QeRYZ=728=%l@H2IRn(G+5} zfrW-xre2|Bu+du1w4R>0MGD#^!P)wWXX#QAMWE!~(;JNME8_<{m6Kl}ClE z2S_TfSUy*ovw{jKl}FaI^TN?Wy~azo5#v z;%J?nO2&v^e_T%FLMJRW%x+OEG(JebtL!Z{5?4vvwlZNhK?15owF`2YX_ literal 0 HcmV?d00001 diff --git a/index.html b/index.html index 7ee2893..4d231be 100755 --- a/index.html +++ b/index.html @@ -18,7 +18,7 @@ "use strict"; var FP = FP || {}; - FP.VERSION = "0.1"; + FP.VERSION = "0.1.2"; document.onreadystatechange = function () { if (document.readyState == "complete") { @@ -33,6 +33,7 @@ + @@ -67,6 +68,7 @@
+