e?(i.comment(t,e+4,s-e-4),s+3):(n.error("Unclosed comment"),-1):-1;default:if("CDATA["==t.substr(e+3,6)){var s=t.indexOf("]]>",e+9);return i.startCDATA(),i.characters(t,e+9,s-e-9),i.endCDATA(),s+3}var r=function(t,e){var i,n=[],s=/'[^']+'|"[^"]+"|[^\s<>\/=]+=?|(\/?\s*>|<)/g;s.lastIndex=e,s.exec(t);for(;i=s.exec(t);)if(n.push(i),i[1])return n}(t,e),o=r.length;if(o>1&&/!doctype/i.test(r[0][0])){var a=r[1][0],h=!1,l=!1;o>3&&(/^public$/i.test(r[2][0])?(h=r[3][0],l=o>4&&r[4][0]):/^system$/i.test(r[2][0])&&(l=r[3][0]));var c=r[o-1];return i.startDTD(a,h,l),i.endDTD(),c.index+c[0].length}}return-1}function m(t,e,i){var n=t.indexOf("?>",e);if(n){var s=t.substring(e,n).match(/^<\?(\S*)\s*([\s\S]*?)\s*$/);if(s){s[0].length;return i.processingInstruction(s[1],s[2]),n+2}return-1}return-1}function v(){this.attributeNames={}}a.prototype=new Error,a.prototype.name=a.name,h.prototype={parse:function(t,e,i){var s=this.domBuilder;s.startDocument(),p(e,e={}),function(t,e,i,s,r){function o(t){var e=t.slice(1,-1);return e in i?i[e]:"#"===e.charAt(0)?function(t){if(t>65535){var e=55296+((t-=65536)>>10),i=56320+(1023&t);return String.fromCharCode(e,i)}return String.fromCharCode(t)}(parseInt(e.substr(1).replace("x","0x"))):(r.error("entity not found:"+t),t)}function h(e){if(e>N){var i=t.substring(N,e).replace(/?\w+;/g,o);x&&p(N),s.characters(i,0,e-N),N=e}}function p(e,i){for(;e>=b&&(i=w.exec(t));)y=i.index,b=y+i[0].length,x.lineNumber++;x.columnNumber=e-y+1}var y=0,b=0,w=/.*(?:\r\n?|\n)|.*$/g,x=s.locator,E=[{currentNSMap:e}],S={},N=0;for(;;){try{var _=t.indexOf("<",N);if(_<0){if(!t.substr(N).match(/^\s*$/)){var T=s.doc,C=T.createTextNode(t.substr(N));T.appendChild(C),s.currentElement=C}return}switch(_>N&&h(_),t.charAt(_+1)){case"/":var O=t.indexOf(">",_+3),I=t.substring(_+2,O).replace(/[ \t\n\r]+$/g,""),R=E.pop();O<0?(I=t.substring(_+2).replace(/[\s<].*/,""),r.error("end tag name: "+I+" is not complete:"+R.tagName),O=_+1+I.length):I.match(/\s)&&(I=I.replace(/[\s<].*/,""),r.error("end tag name: "+I+" maybe not complete"),O=_+1+I.length);var k=R.localNSMap,A=R.tagName==I;if(A||R.tagName&&R.tagName.toLowerCase()==I.toLowerCase()){if(s.endElement(R.uri,R.localName,I),k)for(var L in k)s.endPrefixMapping(L);A||r.fatalError("end tag name: "+I+" is not match the current start tagName:"+R.tagName)}else E.push(R);O++;break;case"?":x&&p(_),O=m(t,_,s);break;case"!":x&&p(_),O=g(t,_,s,r);break;default:x&&p(_);var j=new v,D=E[E.length-1].currentNSMap,P=(O=c(t,_,j,D,o,r),j.length);if(!j.closed&&f(t,O,j.tagName,S)&&(j.closed=!0,i.nbsp||r.warning("unclosed xml attribute")),x&&P){for(var M=l(x,{}),z=0;zN?N=O:h(Math.max(_,N)+1)}}(t,e,i,s,this.errorHandler),s.endDocument()}},v.prototype={setTagName:function(t){if(!o.test(t))throw new Error("invalid tagName:"+t);this.tagName=t},addValue:function(t,e,i){if(!o.test(t))throw new Error("invalid attribute:"+t);this.attributeNames[t]=this.length,this[this.length++]={qName:t,value:e,offset:i}},length:0,getLocalName:function(t){return this[t].localName},getLocator:function(t){return this[t].locator},getQName:function(t){return this[t].qName},getURI:function(t){return this[t].uri},getValue:function(t){return this[t].value}},e.XMLReader=h,e.ParseError=a},function(t,e,i){"use strict";function n(t){return document.createElementNS("http://www.w3.org/2000/svg",t)}Object.defineProperty(e,"__esModule",{value:!0}),e.createElement=n,e.default={createElement:n}},function(t,e,i){"use strict";function n(t,e){function i(i){for(var n=e.length-1;n>=0;n--){var o=e[n],a=i.clientX,h=i.clientY;if(i.touches&&i.touches.length&&(a=i.touches[0].clientX,h=i.touches[0].clientY),r(o,t,a,h)){o.dispatchEvent(s(i));break}}}if("iframe"===t.nodeName||"IFRAME"===t.nodeName)try{this.target=t.contentDocument}catch(e){this.target=t}else this.target=t;for(var n=["mouseup","mousedown","click","touchstart"],o=0;oi&&a>e}if(!r(t.getBoundingClientRect(),i,n))return!1;for(var o=t.getClientRects(),a=0,h=o.length;a${titleText}`;
+ this.chapters.push({
+ label: titleText,
+ href: index.toString(),
+ index: index
+ });
+ break;
+
+ case 'p':
+ html += `${this.extractText(child)}
`;
+ break;
+
+ case 'empty-line':
+ html += '
';
+ break;
+
+ case 'subtitle':
+ html += `${this.extractText(child)}
`;
+ break;
+
+ case 'epigraph':
+ html += `${this.parseInnerContent(child, ns)}
`;
+ break;
+
+ case 'poem':
+ html += `${this.parsePoem(child, ns)}
`;
+ break;
+
+ case 'cite':
+ html += `${this.parseInnerContent(child, ns)}
`;
+ break;
+
+ case 'image':
+ // Handle inline images if needed
+ break;
+
+ case 'section':
+ html += this.parseFb2Section(child, ns, this.chapters.length);
+ break;
+ }
+ }
+
+ return `${html}
`;
+ }
+
+ extractText(element) {
+ let text = '';
+ for (let node of element.childNodes) {
+ if (node.nodeType === Node.TEXT_NODE) {
+ text += node.textContent;
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
+ const tag = node.localName;
+ switch (tag) {
+ case 'strong':
+ text += `${this.extractText(node)}`;
+ break;
+ case 'emphasis':
+ text += `${this.extractText(node)}`;
+ break;
+ case 'a':
+ text += `${this.extractText(node)}`;
+ break;
+ case 'strikethrough':
+ text += `${this.extractText(node)}`;
+ break;
+ default:
+ text += this.extractText(node);
+ }
+ }
+ }
+ return text;
+ }
+
+ parseInnerContent(element, ns) {
+ let html = '';
+ for (let child of element.children) {
+ if (child.localName === 'p') {
+ html += `${this.extractText(child)}
`;
+ } else if (child.localName === 'text-author') {
+ html += `— ${this.extractText(child)}
`;
+ }
+ }
+ return html;
+ }
+
+ parsePoem(element, ns) {
+ let html = '';
+ for (let child of element.children) {
+ if (child.localName === 'stanza') {
+ html += '';
+ for (let v of child.children) {
+ if (v.localName === 'v') {
+ html += `
${this.extractText(v)}
`;
+ }
+ }
+ html += '
';
+ } else if (child.localName === 'title') {
+ html += `${this.extractText(child)}
`;
+ } else if (child.localName === 'text-author') {
+ html += `— ${this.extractText(child)}
`;
+ }
+ }
+ return html;
+ }
+
+ render() {
+ this.container.innerHTML = `
+
+
+ ${this.content}
+
+ `;
+ }
+
+ paginate() {
+ const inner = document.getElementById('fb2-inner');
+ if (!inner) return;
+
+ const containerWidth = this.container.clientWidth;
+ const scrollWidth = inner.scrollWidth;
+ const pageWidth = containerWidth;
+
+ this.totalPages = Math.ceil(scrollWidth / pageWidth);
+ if (this.totalPages < 1) this.totalPages = 1;
+
+ this.showPage(this.currentPageIndex);
+ }
+
+ showPage(pageIndex) {
+ if (pageIndex < 0) pageIndex = 0;
+ if (pageIndex >= this.totalPages) pageIndex = this.totalPages - 1;
+
+ this.currentPageIndex = pageIndex;
+ const inner = document.getElementById('fb2-inner');
+ if (inner) {
+ const offset = pageIndex * this.container.clientWidth;
+ inner.style.transform = `translateX(-${offset}px)`;
+ }
+ }
+
+ nextPage() {
+ if (this.currentPageIndex < this.totalPages - 1) {
+ this.showPage(this.currentPageIndex + 1);
+ }
+ }
+
+ prevPage() {
+ if (this.currentPageIndex > 0) {
+ this.showPage(this.currentPageIndex - 1);
+ }
+ }
+
+ getProgress() {
+ if (this.totalPages <= 1) return 0;
+ return this.currentPageIndex / (this.totalPages - 1);
+ }
+
+ getCurrentPage() {
+ return this.currentPageIndex + 1;
+ }
+
+ getTotalPages() {
+ return this.totalPages || 1;
+ }
+
+ getCurrentChapter() {
+ // Find which chapter we're in based on page position
+ const inner = document.getElementById('fb2-inner');
+ if (!inner) return '';
+
+ const titles = inner.querySelectorAll('.fb2-title');
+ let currentChapter = '';
+
+ for (let title of titles) {
+ const rect = title.getBoundingClientRect();
+ const containerRect = this.container.getBoundingClientRect();
+ const offset = this.currentPageIndex * this.container.clientWidth;
+
+ if (title.offsetLeft <= offset + this.container.clientWidth) {
+ currentChapter = title.textContent;
+ }
+ }
+
+ return currentChapter;
+ }
+
+ getChapters() {
+ return this.chapters;
+ }
+
+ setFontSize(size) {
+ this.fontSize = size;
+ this.render();
+ setTimeout(() => this.paginate(), 100);
+ }
+
+ setFontFamily(family) {
+ this.fontFamily = family;
+ this.render();
+ setTimeout(() => this.paginate(), 100);
+ }
+
+ goToChapter(chapterIndex) {
+ const inner = document.getElementById('fb2-inner');
+ if (!inner) return;
+
+ const section = inner.querySelector(`[data-chapter="${chapterIndex}"]`);
+ if (section) {
+ const offset = section.offsetLeft;
+ const pageIndex = Math.floor(offset / this.container.clientWidth);
+ this.showPage(pageIndex);
+ }
+ }
+
+ goToPosition(progress) {
+ const pageIndex = Math.floor(progress * (this.totalPages - 1));
+ this.showPage(pageIndex);
+ }
+}
\ No newline at end of file
diff --git a/BookReader/Resources/Raw/wwwroot/js/jszip.min.js b/BookReader/Resources/Raw/wwwroot/js/jszip.min.js
new file mode 100644
index 0000000..ff4cfd5
--- /dev/null
+++ b/BookReader/Resources/Raw/wwwroot/js/jszip.min.js
@@ -0,0 +1,13 @@
+/*!
+
+JSZip v3.10.1 - A JavaScript class for generating and reading zip files
+
+
+(c) 2009-2016 Stuart Knightley
+Dual licenced under the MIT license or GPLv3. See https://raw.github.com/Stuk/jszip/main/LICENSE.markdown.
+
+JSZip uses the library pako released under the MIT license :
+https://github.com/nodeca/pako/blob/main/LICENSE
+*/
+
+!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).JSZip=e()}}(function(){return function s(a,o,h){function u(r,e){if(!o[r]){if(!a[r]){var t="function"==typeof require&&require;if(!e&&t)return t(r,!0);if(l)return l(r,!0);var n=new Error("Cannot find module '"+r+"'");throw n.code="MODULE_NOT_FOUND",n}var i=o[r]={exports:{}};a[r][0].call(i.exports,function(e){var t=a[r][1][e];return u(t||e)},i,i.exports,s,a,o,h)}return o[r].exports}for(var l="function"==typeof require&&require,e=0;e>2,s=(3&t)<<4|r>>4,a=1>6:64,o=2>4,r=(15&i)<<4|(s=p.indexOf(e.charAt(o++)))>>2,n=(3&s)<<6|(a=p.indexOf(e.charAt(o++))),l[h++]=t,64!==s&&(l[h++]=r),64!==a&&(l[h++]=n);return l}},{"./support":30,"./utils":32}],2:[function(e,t,r){"use strict";var n=e("./external"),i=e("./stream/DataWorker"),s=e("./stream/Crc32Probe"),a=e("./stream/DataLengthProbe");function o(e,t,r,n,i){this.compressedSize=e,this.uncompressedSize=t,this.crc32=r,this.compression=n,this.compressedContent=i}o.prototype={getContentWorker:function(){var e=new i(n.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new a("data_length")),t=this;return e.on("end",function(){if(this.streamInfo.data_length!==t.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")}),e},getCompressedWorker:function(){return new i(n.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},o.createWorkerFrom=function(e,t,r){return e.pipe(new s).pipe(new a("uncompressedSize")).pipe(t.compressWorker(r)).pipe(new a("compressedSize")).withStreamInfo("compression",t)},t.exports=o},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(e,t,r){"use strict";var n=e("./stream/GenericWorker");r.STORE={magic:"\0\0",compressWorker:function(){return new n("STORE compression")},uncompressWorker:function(){return new n("STORE decompression")}},r.DEFLATE=e("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(e,t,r){"use strict";var n=e("./utils");var o=function(){for(var e,t=[],r=0;r<256;r++){e=r;for(var n=0;n<8;n++)e=1&e?3988292384^e>>>1:e>>>1;t[r]=e}return t}();t.exports=function(e,t){return void 0!==e&&e.length?"string"!==n.getTypeOf(e)?function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a>>8^i[255&(e^t[a])];return-1^e}(0|t,e,e.length,0):function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a>>8^i[255&(e^t.charCodeAt(a))];return-1^e}(0|t,e,e.length,0):0}},{"./utils":32}],5:[function(e,t,r){"use strict";r.base64=!1,r.binary=!1,r.dir=!1,r.createFolders=!0,r.date=null,r.compression=null,r.compressionOptions=null,r.comment=null,r.unixPermissions=null,r.dosPermissions=null},{}],6:[function(e,t,r){"use strict";var n=null;n="undefined"!=typeof Promise?Promise:e("lie"),t.exports={Promise:n}},{lie:37}],7:[function(e,t,r){"use strict";var n="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array,i=e("pako"),s=e("./utils"),a=e("./stream/GenericWorker"),o=n?"uint8array":"array";function h(e,t){a.call(this,"FlateWorker/"+e),this._pako=null,this._pakoAction=e,this._pakoOptions=t,this.meta={}}r.magic="\b\0",s.inherits(h,a),h.prototype.processChunk=function(e){this.meta=e.meta,null===this._pako&&this._createPako(),this._pako.push(s.transformTo(o,e.data),!1)},h.prototype.flush=function(){a.prototype.flush.call(this),null===this._pako&&this._createPako(),this._pako.push([],!0)},h.prototype.cleanUp=function(){a.prototype.cleanUp.call(this),this._pako=null},h.prototype._createPako=function(){this._pako=new i[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var t=this;this._pako.onData=function(e){t.push({data:e,meta:t.meta})}},r.compressWorker=function(e){return new h("Deflate",e)},r.uncompressWorker=function(){return new h("Inflate",{})}},{"./stream/GenericWorker":28,"./utils":32,pako:38}],8:[function(e,t,r){"use strict";function A(e,t){var r,n="";for(r=0;r>>=8;return n}function n(e,t,r,n,i,s){var a,o,h=e.file,u=e.compression,l=s!==O.utf8encode,f=I.transformTo("string",s(h.name)),c=I.transformTo("string",O.utf8encode(h.name)),d=h.comment,p=I.transformTo("string",s(d)),m=I.transformTo("string",O.utf8encode(d)),_=c.length!==h.name.length,g=m.length!==d.length,b="",v="",y="",w=h.dir,k=h.date,x={crc32:0,compressedSize:0,uncompressedSize:0};t&&!r||(x.crc32=e.crc32,x.compressedSize=e.compressedSize,x.uncompressedSize=e.uncompressedSize);var S=0;t&&(S|=8),l||!_&&!g||(S|=2048);var z=0,C=0;w&&(z|=16),"UNIX"===i?(C=798,z|=function(e,t){var r=e;return e||(r=t?16893:33204),(65535&r)<<16}(h.unixPermissions,w)):(C=20,z|=function(e){return 63&(e||0)}(h.dosPermissions)),a=k.getUTCHours(),a<<=6,a|=k.getUTCMinutes(),a<<=5,a|=k.getUTCSeconds()/2,o=k.getUTCFullYear()-1980,o<<=4,o|=k.getUTCMonth()+1,o<<=5,o|=k.getUTCDate(),_&&(v=A(1,1)+A(B(f),4)+c,b+="up"+A(v.length,2)+v),g&&(y=A(1,1)+A(B(p),4)+m,b+="uc"+A(y.length,2)+y);var E="";return E+="\n\0",E+=A(S,2),E+=u.magic,E+=A(a,2),E+=A(o,2),E+=A(x.crc32,4),E+=A(x.compressedSize,4),E+=A(x.uncompressedSize,4),E+=A(f.length,2),E+=A(b.length,2),{fileRecord:R.LOCAL_FILE_HEADER+E+f+b,dirRecord:R.CENTRAL_FILE_HEADER+A(C,2)+E+A(p.length,2)+"\0\0\0\0"+A(z,4)+A(n,4)+f+b+p}}var I=e("../utils"),i=e("../stream/GenericWorker"),O=e("../utf8"),B=e("../crc32"),R=e("../signature");function s(e,t,r,n){i.call(this,"ZipFileWorker"),this.bytesWritten=0,this.zipComment=t,this.zipPlatform=r,this.encodeFileName=n,this.streamFiles=e,this.accumulate=!1,this.contentBuffer=[],this.dirRecords=[],this.currentSourceOffset=0,this.entriesCount=0,this.currentFile=null,this._sources=[]}I.inherits(s,i),s.prototype.push=function(e){var t=e.meta.percent||0,r=this.entriesCount,n=this._sources.length;this.accumulate?this.contentBuffer.push(e):(this.bytesWritten+=e.data.length,i.prototype.push.call(this,{data:e.data,meta:{currentFile:this.currentFile,percent:r?(t+100*(r-n-1))/r:100}}))},s.prototype.openedSource=function(e){this.currentSourceOffset=this.bytesWritten,this.currentFile=e.file.name;var t=this.streamFiles&&!e.file.dir;if(t){var r=n(e,t,!1,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);this.push({data:r.fileRecord,meta:{percent:0}})}else this.accumulate=!0},s.prototype.closedSource=function(e){this.accumulate=!1;var t=this.streamFiles&&!e.file.dir,r=n(e,t,!0,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);if(this.dirRecords.push(r.dirRecord),t)this.push({data:function(e){return R.DATA_DESCRIPTOR+A(e.crc32,4)+A(e.compressedSize,4)+A(e.uncompressedSize,4)}(e),meta:{percent:100}});else for(this.push({data:r.fileRecord,meta:{percent:0}});this.contentBuffer.length;)this.push(this.contentBuffer.shift());this.currentFile=null},s.prototype.flush=function(){for(var e=this.bytesWritten,t=0;t=this.index;t--)r=(r<<8)+this.byteAt(t);return this.index+=e,r},readString:function(e){return n.transformTo("string",this.readData(e))},readData:function(){},lastIndexOfSignature:function(){},readAndCheckSignature:function(){},readDate:function(){var e=this.readInt(4);return new Date(Date.UTC(1980+(e>>25&127),(e>>21&15)-1,e>>16&31,e>>11&31,e>>5&63,(31&e)<<1))}},t.exports=i},{"../utils":32}],19:[function(e,t,r){"use strict";var n=e("./Uint8ArrayReader");function i(e){n.call(this,e)}e("../utils").inherits(i,n),i.prototype.readData=function(e){this.checkOffset(e);var t=this.data.slice(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./Uint8ArrayReader":21}],20:[function(e,t,r){"use strict";var n=e("./DataReader");function i(e){n.call(this,e)}e("../utils").inherits(i,n),i.prototype.byteAt=function(e){return this.data.charCodeAt(this.zero+e)},i.prototype.lastIndexOfSignature=function(e){return this.data.lastIndexOf(e)-this.zero},i.prototype.readAndCheckSignature=function(e){return e===this.readData(4)},i.prototype.readData=function(e){this.checkOffset(e);var t=this.data.slice(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./DataReader":18}],21:[function(e,t,r){"use strict";var n=e("./ArrayReader");function i(e){n.call(this,e)}e("../utils").inherits(i,n),i.prototype.readData=function(e){if(this.checkOffset(e),0===e)return new Uint8Array(0);var t=this.data.subarray(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./ArrayReader":17}],22:[function(e,t,r){"use strict";var n=e("../utils"),i=e("../support"),s=e("./ArrayReader"),a=e("./StringReader"),o=e("./NodeBufferReader"),h=e("./Uint8ArrayReader");t.exports=function(e){var t=n.getTypeOf(e);return n.checkSupport(t),"string"!==t||i.uint8array?"nodebuffer"===t?new o(e):i.uint8array?new h(n.transformTo("uint8array",e)):new s(n.transformTo("array",e)):new a(e)}},{"../support":30,"../utils":32,"./ArrayReader":17,"./NodeBufferReader":19,"./StringReader":20,"./Uint8ArrayReader":21}],23:[function(e,t,r){"use strict";r.LOCAL_FILE_HEADER="PK",r.CENTRAL_FILE_HEADER="PK",r.CENTRAL_DIRECTORY_END="PK",r.ZIP64_CENTRAL_DIRECTORY_LOCATOR="PK",r.ZIP64_CENTRAL_DIRECTORY_END="PK",r.DATA_DESCRIPTOR="PK\b"},{}],24:[function(e,t,r){"use strict";var n=e("./GenericWorker"),i=e("../utils");function s(e){n.call(this,"ConvertWorker to "+e),this.destType=e}i.inherits(s,n),s.prototype.processChunk=function(e){this.push({data:i.transformTo(this.destType,e.data),meta:e.meta})},t.exports=s},{"../utils":32,"./GenericWorker":28}],25:[function(e,t,r){"use strict";var n=e("./GenericWorker"),i=e("../crc32");function s(){n.call(this,"Crc32Probe"),this.withStreamInfo("crc32",0)}e("../utils").inherits(s,n),s.prototype.processChunk=function(e){this.streamInfo.crc32=i(e.data,this.streamInfo.crc32||0),this.push(e)},t.exports=s},{"../crc32":4,"../utils":32,"./GenericWorker":28}],26:[function(e,t,r){"use strict";var n=e("../utils"),i=e("./GenericWorker");function s(e){i.call(this,"DataLengthProbe for "+e),this.propName=e,this.withStreamInfo(e,0)}n.inherits(s,i),s.prototype.processChunk=function(e){if(e){var t=this.streamInfo[this.propName]||0;this.streamInfo[this.propName]=t+e.data.length}i.prototype.processChunk.call(this,e)},t.exports=s},{"../utils":32,"./GenericWorker":28}],27:[function(e,t,r){"use strict";var n=e("../utils"),i=e("./GenericWorker");function s(e){i.call(this,"DataWorker");var t=this;this.dataIsReady=!1,this.index=0,this.max=0,this.data=null,this.type="",this._tickScheduled=!1,e.then(function(e){t.dataIsReady=!0,t.data=e,t.max=e&&e.length||0,t.type=n.getTypeOf(e),t.isPaused||t._tickAndRepeat()},function(e){t.error(e)})}n.inherits(s,i),s.prototype.cleanUp=function(){i.prototype.cleanUp.call(this),this.data=null},s.prototype.resume=function(){return!!i.prototype.resume.call(this)&&(!this._tickScheduled&&this.dataIsReady&&(this._tickScheduled=!0,n.delay(this._tickAndRepeat,[],this)),!0)},s.prototype._tickAndRepeat=function(){this._tickScheduled=!1,this.isPaused||this.isFinished||(this._tick(),this.isFinished||(n.delay(this._tickAndRepeat,[],this),this._tickScheduled=!0))},s.prototype._tick=function(){if(this.isPaused||this.isFinished)return!1;var e=null,t=Math.min(this.max,this.index+16384);if(this.index>=this.max)return this.end();switch(this.type){case"string":e=this.data.substring(this.index,t);break;case"uint8array":e=this.data.subarray(this.index,t);break;case"array":case"nodebuffer":e=this.data.slice(this.index,t)}return this.index=t,this.push({data:e,meta:{percent:this.max?this.index/this.max*100:0}})},t.exports=s},{"../utils":32,"./GenericWorker":28}],28:[function(e,t,r){"use strict";function n(e){this.name=e||"default",this.streamInfo={},this.generatedError=null,this.extraStreamInfo={},this.isPaused=!0,this.isFinished=!1,this.isLocked=!1,this._listeners={data:[],end:[],error:[]},this.previous=null}n.prototype={push:function(e){this.emit("data",e)},end:function(){if(this.isFinished)return!1;this.flush();try{this.emit("end"),this.cleanUp(),this.isFinished=!0}catch(e){this.emit("error",e)}return!0},error:function(e){return!this.isFinished&&(this.isPaused?this.generatedError=e:(this.isFinished=!0,this.emit("error",e),this.previous&&this.previous.error(e),this.cleanUp()),!0)},on:function(e,t){return this._listeners[e].push(t),this},cleanUp:function(){this.streamInfo=this.generatedError=this.extraStreamInfo=null,this._listeners=[]},emit:function(e,t){if(this._listeners[e])for(var r=0;r "+e:e}},t.exports=n},{}],29:[function(e,t,r){"use strict";var h=e("../utils"),i=e("./ConvertWorker"),s=e("./GenericWorker"),u=e("../base64"),n=e("../support"),a=e("../external"),o=null;if(n.nodestream)try{o=e("../nodejs/NodejsStreamOutputAdapter")}catch(e){}function l(e,o){return new a.Promise(function(t,r){var n=[],i=e._internalType,s=e._outputType,a=e._mimeType;e.on("data",function(e,t){n.push(e),o&&o(t)}).on("error",function(e){n=[],r(e)}).on("end",function(){try{var e=function(e,t,r){switch(e){case"blob":return h.newBlob(h.transformTo("arraybuffer",t),r);case"base64":return u.encode(t);default:return h.transformTo(e,t)}}(s,function(e,t){var r,n=0,i=null,s=0;for(r=0;r>>6:(r<65536?t[s++]=224|r>>>12:(t[s++]=240|r>>>18,t[s++]=128|r>>>12&63),t[s++]=128|r>>>6&63),t[s++]=128|63&r);return t}(e)},s.utf8decode=function(e){return h.nodebuffer?o.transformTo("nodebuffer",e).toString("utf-8"):function(e){var t,r,n,i,s=e.length,a=new Array(2*s);for(t=r=0;t>10&1023,a[r++]=56320|1023&n)}return a.length!==r&&(a.subarray?a=a.subarray(0,r):a.length=r),o.applyFromCharCode(a)}(e=o.transformTo(h.uint8array?"uint8array":"array",e))},o.inherits(a,n),a.prototype.processChunk=function(e){var t=o.transformTo(h.uint8array?"uint8array":"array",e.data);if(this.leftOver&&this.leftOver.length){if(h.uint8array){var r=t;(t=new Uint8Array(r.length+this.leftOver.length)).set(this.leftOver,0),t.set(r,this.leftOver.length)}else t=this.leftOver.concat(t);this.leftOver=null}var n=function(e,t){var r;for((t=t||e.length)>e.length&&(t=e.length),r=t-1;0<=r&&128==(192&e[r]);)r--;return r<0?t:0===r?t:r+u[e[r]]>t?r:t}(t),i=t;n!==t.length&&(h.uint8array?(i=t.subarray(0,n),this.leftOver=t.subarray(n,t.length)):(i=t.slice(0,n),this.leftOver=t.slice(n,t.length))),this.push({data:s.utf8decode(i),meta:e.meta})},a.prototype.flush=function(){this.leftOver&&this.leftOver.length&&(this.push({data:s.utf8decode(this.leftOver),meta:{}}),this.leftOver=null)},s.Utf8DecodeWorker=a,o.inherits(l,n),l.prototype.processChunk=function(e){this.push({data:s.utf8encode(e.data),meta:e.meta})},s.Utf8EncodeWorker=l},{"./nodejsUtils":14,"./stream/GenericWorker":28,"./support":30,"./utils":32}],32:[function(e,t,a){"use strict";var o=e("./support"),h=e("./base64"),r=e("./nodejsUtils"),u=e("./external");function n(e){return e}function l(e,t){for(var r=0;r>8;this.dir=!!(16&this.externalFileAttributes),0==e&&(this.dosPermissions=63&this.externalFileAttributes),3==e&&(this.unixPermissions=this.externalFileAttributes>>16&65535),this.dir||"/"!==this.fileNameStr.slice(-1)||(this.dir=!0)},parseZIP64ExtraField:function(){if(this.extraFields[1]){var e=n(this.extraFields[1].value);this.uncompressedSize===s.MAX_VALUE_32BITS&&(this.uncompressedSize=e.readInt(8)),this.compressedSize===s.MAX_VALUE_32BITS&&(this.compressedSize=e.readInt(8)),this.localHeaderOffset===s.MAX_VALUE_32BITS&&(this.localHeaderOffset=e.readInt(8)),this.diskNumberStart===s.MAX_VALUE_32BITS&&(this.diskNumberStart=e.readInt(4))}},readExtraFields:function(e){var t,r,n,i=e.index+this.extraFieldsLength;for(this.extraFields||(this.extraFields={});e.index+4>>6:(r<65536?t[s++]=224|r>>>12:(t[s++]=240|r>>>18,t[s++]=128|r>>>12&63),t[s++]=128|r>>>6&63),t[s++]=128|63&r);return t},r.buf2binstring=function(e){return l(e,e.length)},r.binstring2buf=function(e){for(var t=new h.Buf8(e.length),r=0,n=t.length;r>10&1023,o[n++]=56320|1023&i)}return l(o,n)},r.utf8border=function(e,t){var r;for((t=t||e.length)>e.length&&(t=e.length),r=t-1;0<=r&&128==(192&e[r]);)r--;return r<0?t:0===r?t:r+u[e[r]]>t?r:t}},{"./common":41}],43:[function(e,t,r){"use strict";t.exports=function(e,t,r,n){for(var i=65535&e|0,s=e>>>16&65535|0,a=0;0!==r;){for(r-=a=2e3>>1:e>>>1;t[r]=e}return t}();t.exports=function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a>>8^i[255&(e^t[a])];return-1^e}},{}],46:[function(e,t,r){"use strict";var h,c=e("../utils/common"),u=e("./trees"),d=e("./adler32"),p=e("./crc32"),n=e("./messages"),l=0,f=4,m=0,_=-2,g=-1,b=4,i=2,v=8,y=9,s=286,a=30,o=19,w=2*s+1,k=15,x=3,S=258,z=S+x+1,C=42,E=113,A=1,I=2,O=3,B=4;function R(e,t){return e.msg=n[t],t}function T(e){return(e<<1)-(4e.avail_out&&(r=e.avail_out),0!==r&&(c.arraySet(e.output,t.pending_buf,t.pending_out,r,e.next_out),e.next_out+=r,t.pending_out+=r,e.total_out+=r,e.avail_out-=r,t.pending-=r,0===t.pending&&(t.pending_out=0))}function N(e,t){u._tr_flush_block(e,0<=e.block_start?e.block_start:-1,e.strstart-e.block_start,t),e.block_start=e.strstart,F(e.strm)}function U(e,t){e.pending_buf[e.pending++]=t}function P(e,t){e.pending_buf[e.pending++]=t>>>8&255,e.pending_buf[e.pending++]=255&t}function L(e,t){var r,n,i=e.max_chain_length,s=e.strstart,a=e.prev_length,o=e.nice_match,h=e.strstart>e.w_size-z?e.strstart-(e.w_size-z):0,u=e.window,l=e.w_mask,f=e.prev,c=e.strstart+S,d=u[s+a-1],p=u[s+a];e.prev_length>=e.good_match&&(i>>=2),o>e.lookahead&&(o=e.lookahead);do{if(u[(r=t)+a]===p&&u[r+a-1]===d&&u[r]===u[s]&&u[++r]===u[s+1]){s+=2,r++;do{}while(u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&sh&&0!=--i);return a<=e.lookahead?a:e.lookahead}function j(e){var t,r,n,i,s,a,o,h,u,l,f=e.w_size;do{if(i=e.window_size-e.lookahead-e.strstart,e.strstart>=f+(f-z)){for(c.arraySet(e.window,e.window,f,f,0),e.match_start-=f,e.strstart-=f,e.block_start-=f,t=r=e.hash_size;n=e.head[--t],e.head[t]=f<=n?n-f:0,--r;);for(t=r=f;n=e.prev[--t],e.prev[t]=f<=n?n-f:0,--r;);i+=f}if(0===e.strm.avail_in)break;if(a=e.strm,o=e.window,h=e.strstart+e.lookahead,u=i,l=void 0,l=a.avail_in,u=x)for(s=e.strstart-e.insert,e.ins_h=e.window[s],e.ins_h=(e.ins_h<=x&&(e.ins_h=(e.ins_h<=x)if(n=u._tr_tally(e,e.strstart-e.match_start,e.match_length-x),e.lookahead-=e.match_length,e.match_length<=e.max_lazy_match&&e.lookahead>=x){for(e.match_length--;e.strstart++,e.ins_h=(e.ins_h<=x&&(e.ins_h=(e.ins_h<=x&&e.match_length<=e.prev_length){for(i=e.strstart+e.lookahead-x,n=u._tr_tally(e,e.strstart-1-e.prev_match,e.prev_length-x),e.lookahead-=e.prev_length-1,e.prev_length-=2;++e.strstart<=i&&(e.ins_h=(e.ins_h<e.pending_buf_size-5&&(r=e.pending_buf_size-5);;){if(e.lookahead<=1){if(j(e),0===e.lookahead&&t===l)return A;if(0===e.lookahead)break}e.strstart+=e.lookahead,e.lookahead=0;var n=e.block_start+r;if((0===e.strstart||e.strstart>=n)&&(e.lookahead=e.strstart-n,e.strstart=n,N(e,!1),0===e.strm.avail_out))return A;if(e.strstart-e.block_start>=e.w_size-z&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):(e.strstart>e.block_start&&(N(e,!1),e.strm.avail_out),A)}),new M(4,4,8,4,Z),new M(4,5,16,8,Z),new M(4,6,32,32,Z),new M(4,4,16,16,W),new M(8,16,32,32,W),new M(8,16,128,128,W),new M(8,32,128,256,W),new M(32,128,258,1024,W),new M(32,258,258,4096,W)],r.deflateInit=function(e,t){return Y(e,t,v,15,8,0)},r.deflateInit2=Y,r.deflateReset=K,r.deflateResetKeep=G,r.deflateSetHeader=function(e,t){return e&&e.state?2!==e.state.wrap?_:(e.state.gzhead=t,m):_},r.deflate=function(e,t){var r,n,i,s;if(!e||!e.state||5>8&255),U(n,n.gzhead.time>>16&255),U(n,n.gzhead.time>>24&255),U(n,9===n.level?2:2<=n.strategy||n.level<2?4:0),U(n,255&n.gzhead.os),n.gzhead.extra&&n.gzhead.extra.length&&(U(n,255&n.gzhead.extra.length),U(n,n.gzhead.extra.length>>8&255)),n.gzhead.hcrc&&(e.adler=p(e.adler,n.pending_buf,n.pending,0)),n.gzindex=0,n.status=69):(U(n,0),U(n,0),U(n,0),U(n,0),U(n,0),U(n,9===n.level?2:2<=n.strategy||n.level<2?4:0),U(n,3),n.status=E);else{var a=v+(n.w_bits-8<<4)<<8;a|=(2<=n.strategy||n.level<2?0:n.level<6?1:6===n.level?2:3)<<6,0!==n.strstart&&(a|=32),a+=31-a%31,n.status=E,P(n,a),0!==n.strstart&&(P(n,e.adler>>>16),P(n,65535&e.adler)),e.adler=1}if(69===n.status)if(n.gzhead.extra){for(i=n.pending;n.gzindex<(65535&n.gzhead.extra.length)&&(n.pending!==n.pending_buf_size||(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending!==n.pending_buf_size));)U(n,255&n.gzhead.extra[n.gzindex]),n.gzindex++;n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),n.gzindex===n.gzhead.extra.length&&(n.gzindex=0,n.status=73)}else n.status=73;if(73===n.status)if(n.gzhead.name){i=n.pending;do{if(n.pending===n.pending_buf_size&&(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending===n.pending_buf_size)){s=1;break}s=n.gzindexi&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),0===s&&(n.gzindex=0,n.status=91)}else n.status=91;if(91===n.status)if(n.gzhead.comment){i=n.pending;do{if(n.pending===n.pending_buf_size&&(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending===n.pending_buf_size)){s=1;break}s=n.gzindexi&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),0===s&&(n.status=103)}else n.status=103;if(103===n.status&&(n.gzhead.hcrc?(n.pending+2>n.pending_buf_size&&F(e),n.pending+2<=n.pending_buf_size&&(U(n,255&e.adler),U(n,e.adler>>8&255),e.adler=0,n.status=E)):n.status=E),0!==n.pending){if(F(e),0===e.avail_out)return n.last_flush=-1,m}else if(0===e.avail_in&&T(t)<=T(r)&&t!==f)return R(e,-5);if(666===n.status&&0!==e.avail_in)return R(e,-5);if(0!==e.avail_in||0!==n.lookahead||t!==l&&666!==n.status){var o=2===n.strategy?function(e,t){for(var r;;){if(0===e.lookahead&&(j(e),0===e.lookahead)){if(t===l)return A;break}if(e.match_length=0,r=u._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++,r&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):e.last_lit&&(N(e,!1),0===e.strm.avail_out)?A:I}(n,t):3===n.strategy?function(e,t){for(var r,n,i,s,a=e.window;;){if(e.lookahead<=S){if(j(e),e.lookahead<=S&&t===l)return A;if(0===e.lookahead)break}if(e.match_length=0,e.lookahead>=x&&0e.lookahead&&(e.match_length=e.lookahead)}if(e.match_length>=x?(r=u._tr_tally(e,1,e.match_length-x),e.lookahead-=e.match_length,e.strstart+=e.match_length,e.match_length=0):(r=u._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++),r&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):e.last_lit&&(N(e,!1),0===e.strm.avail_out)?A:I}(n,t):h[n.level].func(n,t);if(o!==O&&o!==B||(n.status=666),o===A||o===O)return 0===e.avail_out&&(n.last_flush=-1),m;if(o===I&&(1===t?u._tr_align(n):5!==t&&(u._tr_stored_block(n,0,0,!1),3===t&&(D(n.head),0===n.lookahead&&(n.strstart=0,n.block_start=0,n.insert=0))),F(e),0===e.avail_out))return n.last_flush=-1,m}return t!==f?m:n.wrap<=0?1:(2===n.wrap?(U(n,255&e.adler),U(n,e.adler>>8&255),U(n,e.adler>>16&255),U(n,e.adler>>24&255),U(n,255&e.total_in),U(n,e.total_in>>8&255),U(n,e.total_in>>16&255),U(n,e.total_in>>24&255)):(P(n,e.adler>>>16),P(n,65535&e.adler)),F(e),0=r.w_size&&(0===s&&(D(r.head),r.strstart=0,r.block_start=0,r.insert=0),u=new c.Buf8(r.w_size),c.arraySet(u,t,l-r.w_size,r.w_size,0),t=u,l=r.w_size),a=e.avail_in,o=e.next_in,h=e.input,e.avail_in=l,e.next_in=0,e.input=t,j(r);r.lookahead>=x;){for(n=r.strstart,i=r.lookahead-(x-1);r.ins_h=(r.ins_h<>>=y=v>>>24,p-=y,0===(y=v>>>16&255))C[s++]=65535&v;else{if(!(16&y)){if(0==(64&y)){v=m[(65535&v)+(d&(1<>>=y,p-=y),p<15&&(d+=z[n++]<>>=y=v>>>24,p-=y,!(16&(y=v>>>16&255))){if(0==(64&y)){v=_[(65535&v)+(d&(1<>>=y,p-=y,(y=s-a)>3,d&=(1<<(p-=w<<3))-1,e.next_in=n,e.next_out=s,e.avail_in=n>>24&255)+(e>>>8&65280)+((65280&e)<<8)+((255&e)<<24)}function s(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new I.Buf16(320),this.work=new I.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function a(e){var t;return e&&e.state?(t=e.state,e.total_in=e.total_out=t.total=0,e.msg="",t.wrap&&(e.adler=1&t.wrap),t.mode=P,t.last=0,t.havedict=0,t.dmax=32768,t.head=null,t.hold=0,t.bits=0,t.lencode=t.lendyn=new I.Buf32(n),t.distcode=t.distdyn=new I.Buf32(i),t.sane=1,t.back=-1,N):U}function o(e){var t;return e&&e.state?((t=e.state).wsize=0,t.whave=0,t.wnext=0,a(e)):U}function h(e,t){var r,n;return e&&e.state?(n=e.state,t<0?(r=0,t=-t):(r=1+(t>>4),t<48&&(t&=15)),t&&(t<8||15=s.wsize?(I.arraySet(s.window,t,r-s.wsize,s.wsize,0),s.wnext=0,s.whave=s.wsize):(n<(i=s.wsize-s.wnext)&&(i=n),I.arraySet(s.window,t,r-n,i,s.wnext),(n-=i)?(I.arraySet(s.window,t,r-n,n,0),s.wnext=n,s.whave=s.wsize):(s.wnext+=i,s.wnext===s.wsize&&(s.wnext=0),s.whave>>8&255,r.check=B(r.check,E,2,0),l=u=0,r.mode=2;break}if(r.flags=0,r.head&&(r.head.done=!1),!(1&r.wrap)||(((255&u)<<8)+(u>>8))%31){e.msg="incorrect header check",r.mode=30;break}if(8!=(15&u)){e.msg="unknown compression method",r.mode=30;break}if(l-=4,k=8+(15&(u>>>=4)),0===r.wbits)r.wbits=k;else if(k>r.wbits){e.msg="invalid window size",r.mode=30;break}r.dmax=1<>8&1),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=3;case 3:for(;l<32;){if(0===o)break e;o--,u+=n[s++]<>>8&255,E[2]=u>>>16&255,E[3]=u>>>24&255,r.check=B(r.check,E,4,0)),l=u=0,r.mode=4;case 4:for(;l<16;){if(0===o)break e;o--,u+=n[s++]<>8),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=5;case 5:if(1024&r.flags){for(;l<16;){if(0===o)break e;o--,u+=n[s++]<>>8&255,r.check=B(r.check,E,2,0)),l=u=0}else r.head&&(r.head.extra=null);r.mode=6;case 6:if(1024&r.flags&&(o<(d=r.length)&&(d=o),d&&(r.head&&(k=r.head.extra_len-r.length,r.head.extra||(r.head.extra=new Array(r.head.extra_len)),I.arraySet(r.head.extra,n,s,d,k)),512&r.flags&&(r.check=B(r.check,n,d,s)),o-=d,s+=d,r.length-=d),r.length))break e;r.length=0,r.mode=7;case 7:if(2048&r.flags){if(0===o)break e;for(d=0;k=n[s+d++],r.head&&k&&r.length<65536&&(r.head.name+=String.fromCharCode(k)),k&&d>9&1,r.head.done=!0),e.adler=r.check=0,r.mode=12;break;case 10:for(;l<32;){if(0===o)break e;o--,u+=n[s++]<>>=7&l,l-=7&l,r.mode=27;break}for(;l<3;){if(0===o)break e;o--,u+=n[s++]<>>=1)){case 0:r.mode=14;break;case 1:if(j(r),r.mode=20,6!==t)break;u>>>=2,l-=2;break e;case 2:r.mode=17;break;case 3:e.msg="invalid block type",r.mode=30}u>>>=2,l-=2;break;case 14:for(u>>>=7&l,l-=7&l;l<32;){if(0===o)break e;o--,u+=n[s++]<>>16^65535)){e.msg="invalid stored block lengths",r.mode=30;break}if(r.length=65535&u,l=u=0,r.mode=15,6===t)break e;case 15:r.mode=16;case 16:if(d=r.length){if(o>>=5,l-=5,r.ndist=1+(31&u),u>>>=5,l-=5,r.ncode=4+(15&u),u>>>=4,l-=4,286>>=3,l-=3}for(;r.have<19;)r.lens[A[r.have++]]=0;if(r.lencode=r.lendyn,r.lenbits=7,S={bits:r.lenbits},x=T(0,r.lens,0,19,r.lencode,0,r.work,S),r.lenbits=S.bits,x){e.msg="invalid code lengths set",r.mode=30;break}r.have=0,r.mode=19;case 19:for(;r.have>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>>=_,l-=_,r.lens[r.have++]=b;else{if(16===b){for(z=_+2;l>>=_,l-=_,0===r.have){e.msg="invalid bit length repeat",r.mode=30;break}k=r.lens[r.have-1],d=3+(3&u),u>>>=2,l-=2}else if(17===b){for(z=_+3;l>>=_)),u>>>=3,l-=3}else{for(z=_+7;l>>=_)),u>>>=7,l-=7}if(r.have+d>r.nlen+r.ndist){e.msg="invalid bit length repeat",r.mode=30;break}for(;d--;)r.lens[r.have++]=k}}if(30===r.mode)break;if(0===r.lens[256]){e.msg="invalid code -- missing end-of-block",r.mode=30;break}if(r.lenbits=9,S={bits:r.lenbits},x=T(D,r.lens,0,r.nlen,r.lencode,0,r.work,S),r.lenbits=S.bits,x){e.msg="invalid literal/lengths set",r.mode=30;break}if(r.distbits=6,r.distcode=r.distdyn,S={bits:r.distbits},x=T(F,r.lens,r.nlen,r.ndist,r.distcode,0,r.work,S),r.distbits=S.bits,x){e.msg="invalid distances set",r.mode=30;break}if(r.mode=20,6===t)break e;case 20:r.mode=21;case 21:if(6<=o&&258<=h){e.next_out=a,e.avail_out=h,e.next_in=s,e.avail_in=o,r.hold=u,r.bits=l,R(e,c),a=e.next_out,i=e.output,h=e.avail_out,s=e.next_in,n=e.input,o=e.avail_in,u=r.hold,l=r.bits,12===r.mode&&(r.back=-1);break}for(r.back=0;g=(C=r.lencode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,r.length=b,0===g){r.mode=26;break}if(32&g){r.back=-1,r.mode=12;break}if(64&g){e.msg="invalid literal/length code",r.mode=30;break}r.extra=15&g,r.mode=22;case 22:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}r.was=r.length,r.mode=23;case 23:for(;g=(C=r.distcode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,64&g){e.msg="invalid distance code",r.mode=30;break}r.offset=b,r.extra=15&g,r.mode=24;case 24:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}if(r.offset>r.dmax){e.msg="invalid distance too far back",r.mode=30;break}r.mode=25;case 25:if(0===h)break e;if(d=c-h,r.offset>d){if((d=r.offset-d)>r.whave&&r.sane){e.msg="invalid distance too far back",r.mode=30;break}p=d>r.wnext?(d-=r.wnext,r.wsize-d):r.wnext-d,d>r.length&&(d=r.length),m=r.window}else m=i,p=a-r.offset,d=r.length;for(hd?(m=R[T+a[v]],A[I+a[v]]):(m=96,0),h=1<>S)+(u-=h)]=p<<24|m<<16|_|0,0!==u;);for(h=1<>=1;if(0!==h?(E&=h-1,E+=h):E=0,v++,0==--O[b]){if(b===w)break;b=t[r+a[v]]}if(k>>7)]}function U(e,t){e.pending_buf[e.pending++]=255&t,e.pending_buf[e.pending++]=t>>>8&255}function P(e,t,r){e.bi_valid>d-r?(e.bi_buf|=t<>d-e.bi_valid,e.bi_valid+=r-d):(e.bi_buf|=t<>>=1,r<<=1,0<--t;);return r>>>1}function Z(e,t,r){var n,i,s=new Array(g+1),a=0;for(n=1;n<=g;n++)s[n]=a=a+r[n-1]<<1;for(i=0;i<=t;i++){var o=e[2*i+1];0!==o&&(e[2*i]=j(s[o]++,o))}}function W(e){var t;for(t=0;t>1;1<=r;r--)G(e,s,r);for(i=h;r=e.heap[1],e.heap[1]=e.heap[e.heap_len--],G(e,s,1),n=e.heap[1],e.heap[--e.heap_max]=r,e.heap[--e.heap_max]=n,s[2*i]=s[2*r]+s[2*n],e.depth[i]=(e.depth[r]>=e.depth[n]?e.depth[r]:e.depth[n])+1,s[2*r+1]=s[2*n+1]=i,e.heap[1]=i++,G(e,s,1),2<=e.heap_len;);e.heap[--e.heap_max]=e.heap[1],function(e,t){var r,n,i,s,a,o,h=t.dyn_tree,u=t.max_code,l=t.stat_desc.static_tree,f=t.stat_desc.has_stree,c=t.stat_desc.extra_bits,d=t.stat_desc.extra_base,p=t.stat_desc.max_length,m=0;for(s=0;s<=g;s++)e.bl_count[s]=0;for(h[2*e.heap[e.heap_max]+1]=0,r=e.heap_max+1;r<_;r++)p<(s=h[2*h[2*(n=e.heap[r])+1]+1]+1)&&(s=p,m++),h[2*n+1]=s,u>=7;n>>=1)if(1&r&&0!==e.dyn_ltree[2*t])return o;if(0!==e.dyn_ltree[18]||0!==e.dyn_ltree[20]||0!==e.dyn_ltree[26])return h;for(t=32;t>>3,(s=e.static_len+3+7>>>3)<=i&&(i=s)):i=s=r+5,r+4<=i&&-1!==t?J(e,t,r,n):4===e.strategy||s===i?(P(e,2+(n?1:0),3),K(e,z,C)):(P(e,4+(n?1:0),3),function(e,t,r,n){var i;for(P(e,t-257,5),P(e,r-1,5),P(e,n-4,4),i=0;i>>8&255,e.pending_buf[e.d_buf+2*e.last_lit+1]=255&t,e.pending_buf[e.l_buf+e.last_lit]=255&r,e.last_lit++,0===t?e.dyn_ltree[2*r]++:(e.matches++,t--,e.dyn_ltree[2*(A[r]+u+1)]++,e.dyn_dtree[2*N(t)]++),e.last_lit===e.lit_bufsize-1},r._tr_align=function(e){P(e,2,3),L(e,m,z),function(e){16===e.bi_valid?(U(e,e.bi_buf),e.bi_buf=0,e.bi_valid=0):8<=e.bi_valid&&(e.pending_buf[e.pending++]=255&e.bi_buf,e.bi_buf>>=8,e.bi_valid-=8)}(e)}},{"../utils/common":41}],53:[function(e,t,r){"use strict";t.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],54:[function(e,t,r){(function(e){!function(r,n){"use strict";if(!r.setImmediate){var i,s,t,a,o=1,h={},u=!1,l=r.document,e=Object.getPrototypeOf&&Object.getPrototypeOf(r);e=e&&e.setTimeout?e:r,i="[object process]"==={}.toString.call(r.process)?function(e){process.nextTick(function(){c(e)})}:function(){if(r.postMessage&&!r.importScripts){var e=!0,t=r.onmessage;return r.onmessage=function(){e=!1},r.postMessage("","*"),r.onmessage=t,e}}()?(a="setImmediate$"+Math.random()+"$",r.addEventListener?r.addEventListener("message",d,!1):r.attachEvent("onmessage",d),function(e){r.postMessage(a+e,"*")}):r.MessageChannel?((t=new MessageChannel).port1.onmessage=function(e){c(e.data)},function(e){t.port2.postMessage(e)}):l&&"onreadystatechange"in l.createElement("script")?(s=l.documentElement,function(e){var t=l.createElement("script");t.onreadystatechange=function(){c(e),t.onreadystatechange=null,s.removeChild(t),t=null},s.appendChild(t)}):function(e){setTimeout(c,0,e)},e.setImmediate=function(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),r=0;r
+
+
\ No newline at end of file
diff --git a/BookReader/Resources/Styles/AppStyles.xaml b/BookReader/Resources/Styles/AppStyles.xaml
new file mode 100644
index 0000000..0660ebf
--- /dev/null
+++ b/BookReader/Resources/Styles/AppStyles.xaml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ #5D4037
+ #3E2723
+ #8D6E63
+ #6D4C41
+ #4CAF50
+ #EFEBE9
+ #A1887F
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/BookReader/Services/BookParserService.cs b/BookReader/Services/BookParserService.cs
new file mode 100644
index 0000000..857b4ae
--- /dev/null
+++ b/BookReader/Services/BookParserService.cs
@@ -0,0 +1,174 @@
+using BookReader.Models;
+using System.IO.Compression;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Xml.Linq;
+using VersOne.Epub;
+
+namespace BookReader.Services;
+
+public class BookParserService : IBookParserService
+{
+ private readonly IDatabaseService _databaseService;
+ private readonly string _booksDir;
+
+ public BookParserService(IDatabaseService databaseService)
+ {
+ _databaseService = databaseService;
+ _booksDir = Path.Combine(FileSystem.AppDataDirectory, "Books");
+ Directory.CreateDirectory(_booksDir);
+ }
+
+ public string GetBooksDirectory() => _booksDir;
+
+ public async Task ParseAndStoreBookAsync(string sourceFilePath, string originalFileName)
+ {
+ var extension = Path.GetExtension(originalFileName).ToLowerInvariant();
+ var bookId = Guid.NewGuid().ToString();
+ var destPath = Path.Combine(_booksDir, $"{bookId}{extension}");
+
+ // Copy file to app storage
+ if (sourceFilePath != destPath)
+ {
+ using var sourceStream = File.OpenRead(sourceFilePath);
+ using var destStream = File.Create(destPath);
+ await sourceStream.CopyToAsync(destStream);
+ }
+
+ var book = new Book
+ {
+ FilePath = destPath,
+ FileName = originalFileName,
+ Format = extension.TrimStart('.'),
+ DateAdded = DateTime.UtcNow,
+ LastRead = DateTime.UtcNow
+ };
+
+ switch (extension)
+ {
+ case ".epub":
+ await ParseEpubMetadataAsync(book);
+ break;
+ case ".fb2":
+ await ParseFb2MetadataAsync(book);
+ break;
+ default:
+ book.Title = Path.GetFileNameWithoutExtension(originalFileName);
+ break;
+ }
+
+ if (string.IsNullOrWhiteSpace(book.Title))
+ book.Title = Path.GetFileNameWithoutExtension(originalFileName);
+
+ await _databaseService.SaveBookAsync(book);
+ return book;
+ }
+
+ public Task GetBookContentPathAsync(Book book)
+ {
+ return Task.FromResult(book.FilePath);
+ }
+
+ private async Task ParseEpubMetadataAsync(Book book)
+ {
+ try
+ {
+ var epubBook = await EpubReader.ReadBookAsync(book.FilePath);
+
+ book.Title = epubBook.Title ?? "Unknown Title";
+ book.Author = epubBook.Author ?? "Unknown Author";
+
+ // Extract cover
+ if (epubBook.CoverImage != null)
+ {
+ book.CoverImage = epubBook.CoverImage;
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Error parsing EPUB: {ex.Message}");
+ book.Title = Path.GetFileNameWithoutExtension(book.FileName);
+ book.Author = "Unknown";
+ }
+ }
+
+ private async Task ParseFb2MetadataAsync(Book book)
+ {
+ try
+ {
+ string xml;
+ if (book.FilePath.EndsWith(".fb2.zip", StringComparison.OrdinalIgnoreCase))
+ {
+ using var zip = ZipFile.OpenRead(book.FilePath);
+ var entry = zip.Entries.FirstOrDefault(e => e.Name.EndsWith(".fb2", StringComparison.OrdinalIgnoreCase));
+ if (entry != null)
+ {
+ using var stream = entry.Open();
+ using var reader = new StreamReader(stream);
+ xml = await reader.ReadToEndAsync();
+ }
+ else return;
+ }
+ else
+ {
+ xml = await File.ReadAllTextAsync(book.FilePath);
+ }
+
+ var doc = XDocument.Parse(xml);
+ XNamespace fb = "http://www.gribuser.ru/xml/fictionbook/2.0";
+
+ var titleInfo = doc.Descendants(fb + "title-info").FirstOrDefault();
+ if (titleInfo != null)
+ {
+ // Title
+ var bookTitle = titleInfo.Element(fb + "book-title")?.Value;
+ book.Title = bookTitle ?? "Unknown Title";
+
+ // Author
+ var authorElement = titleInfo.Element(fb + "author");
+ if (authorElement != null)
+ {
+ var firstName = authorElement.Element(fb + "first-name")?.Value ?? "";
+ var lastName = authorElement.Element(fb + "last-name")?.Value ?? "";
+ var middleName = authorElement.Element(fb + "middle-name")?.Value ?? "";
+ book.Author = $"{firstName} {middleName} {lastName}".Trim();
+ }
+
+ // Cover
+ var coverPage = titleInfo.Element(fb + "coverpage");
+ if (coverPage != null)
+ {
+ var imageElement = coverPage.Descendants().FirstOrDefault(e => e.Name.LocalName == "image");
+ if (imageElement != null)
+ {
+ XNamespace xlink = "http://www.w3.org/1999/xlink";
+ var href = imageElement.Attribute(xlink + "href")?.Value
+ ?? imageElement.Attribute("href")?.Value;
+
+ if (href != null)
+ {
+ href = href.TrimStart('#');
+ var binary = doc.Descendants(fb + "binary")
+ .FirstOrDefault(b => b.Attribute("id")?.Value == href);
+
+ if (binary != null)
+ {
+ try
+ {
+ book.CoverImage = Convert.FromBase64String(binary.Value.Trim());
+ }
+ catch { }
+ }
+ }
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Error parsing FB2: {ex.Message}");
+ book.Title = Path.GetFileNameWithoutExtension(book.FileName);
+ book.Author = "Unknown";
+ }
+ }
+}
\ No newline at end of file
diff --git a/BookReader/Services/CalibreWebService.cs b/BookReader/Services/CalibreWebService.cs
new file mode 100644
index 0000000..3c94352
--- /dev/null
+++ b/BookReader/Services/CalibreWebService.cs
@@ -0,0 +1,142 @@
+using BookReader.Models;
+using Newtonsoft.Json.Linq;
+using System.Net.Http.Headers;
+using System.Text;
+
+namespace BookReader.Services;
+
+public class CalibreWebService : ICalibreWebService
+{
+ private readonly HttpClient _httpClient;
+ private string _baseUrl = string.Empty;
+ private string _username = string.Empty;
+ private string _password = string.Empty;
+
+ public CalibreWebService(HttpClient httpClient)
+ {
+ _httpClient = httpClient;
+ _httpClient.Timeout = TimeSpan.FromSeconds(30);
+ }
+
+ public void Configure(string url, string username, string password)
+ {
+ _baseUrl = url.TrimEnd('/');
+ _username = username;
+ _password = password;
+
+ if (!string.IsNullOrEmpty(_username))
+ {
+ var authBytes = Encoding.ASCII.GetBytes($"{_username}:{_password}");
+ _httpClient.DefaultRequestHeaders.Authorization =
+ new AuthenticationHeaderValue("Basic", Convert.ToBase64String(authBytes));
+ }
+ }
+
+ public async Task TestConnectionAsync(string url, string username, string password)
+ {
+ try
+ {
+ Configure(url, username, password);
+ var response = await _httpClient.GetAsync($"{_baseUrl}/ajax/search?query=&num=1");
+ return response.IsSuccessStatusCode;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ public async Task> GetBooksAsync(string? searchQuery = null, int page = 0, int pageSize = 20)
+ {
+ var books = new List();
+
+ try
+ {
+ var offset = page * pageSize;
+ var query = string.IsNullOrEmpty(searchQuery) ? "" : Uri.EscapeDataString(searchQuery);
+ var url = $"{_baseUrl}/ajax/search?query={query}&num={pageSize}&offset={offset}&sort=timestamp&sort_order=desc";
+
+ var response = await _httpClient.GetStringAsync(url);
+ var json = JObject.Parse(response);
+
+ var bookIds = json["book_ids"]?.ToObject>() ?? new List();
+
+ foreach (var bookId in bookIds)
+ {
+ try
+ {
+ var bookUrl = $"{_baseUrl}/ajax/book/{bookId}";
+ var bookResponse = await _httpClient.GetStringAsync(bookUrl);
+ var bookJson = JObject.Parse(bookResponse);
+
+ var formats = bookJson["formats"]?.ToObject>() ?? new List();
+ var supportedFormat = formats.FirstOrDefault(f =>
+ f.Equals("EPUB", StringComparison.OrdinalIgnoreCase) ||
+ f.Equals("FB2", StringComparison.OrdinalIgnoreCase));
+
+ if (supportedFormat == null) continue;
+
+ var authors = bookJson["authors"]?.ToObject>() ?? new List();
+
+ var calibreBook = new CalibreBook
+ {
+ Id = bookId.ToString(),
+ Title = bookJson["title"]?.ToString() ?? "Unknown",
+ Author = string.Join(", ", authors),
+ Format = supportedFormat.ToLowerInvariant(),
+ CoverUrl = $"{_baseUrl}/get/cover/{bookId}",
+ DownloadUrl = $"{_baseUrl}/get/{supportedFormat}/{bookId}"
+ };
+
+ // Try to load cover
+ try
+ {
+ calibreBook.CoverImage = await _httpClient.GetByteArrayAsync(calibreBook.CoverUrl);
+ }
+ catch { }
+
+ books.Add(calibreBook);
+ }
+ catch { continue; }
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Error fetching Calibre books: {ex.Message}");
+ }
+
+ return books;
+ }
+
+ public async Task DownloadBookAsync(CalibreBook book, IProgress? progress = null)
+ {
+ var booksDir = Path.Combine(FileSystem.AppDataDirectory, "Books");
+ Directory.CreateDirectory(booksDir);
+
+ var fileName = $"{Guid.NewGuid()}.{book.Format}";
+ var filePath = Path.Combine(booksDir, fileName);
+
+ using var response = await _httpClient.GetAsync(book.DownloadUrl, HttpCompletionOption.ResponseHeadersRead);
+ response.EnsureSuccessStatusCode();
+
+ var totalBytes = response.Content.Headers.ContentLength ?? -1;
+ var bytesRead = 0L;
+
+ using var contentStream = await response.Content.ReadAsStreamAsync();
+ using var fileStream = File.Create(filePath);
+
+ var buffer = new byte[8192];
+ int read;
+
+ while ((read = await contentStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
+ {
+ await fileStream.WriteAsync(buffer, 0, read);
+ bytesRead += read;
+
+ if (totalBytes > 0)
+ progress?.Report((double)bytesRead / totalBytes);
+ }
+
+ return filePath;
+ }
+}
\ No newline at end of file
diff --git a/BookReader/Services/DatabaseService.cs b/BookReader/Services/DatabaseService.cs
new file mode 100644
index 0000000..95a61c0
--- /dev/null
+++ b/BookReader/Services/DatabaseService.cs
@@ -0,0 +1,122 @@
+using BookReader.Models;
+using SQLite;
+
+namespace BookReader.Services;
+
+public class DatabaseService : IDatabaseService
+{
+ private SQLiteAsyncConnection? _database;
+ private readonly string _dbPath;
+
+ public DatabaseService()
+ {
+ _dbPath = Path.Combine(FileSystem.AppDataDirectory, "bookreader.db3");
+ }
+
+ public async Task InitializeAsync()
+ {
+ if (_database != null) return;
+
+ _database = new SQLiteAsyncConnection(_dbPath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create | SQLiteOpenFlags.SharedCache);
+
+ await _database.CreateTableAsync();
+ await _database.CreateTableAsync();
+ await _database.CreateTableAsync();
+ }
+
+ private async Task EnsureInitializedAsync()
+ {
+ if (_database == null)
+ await InitializeAsync();
+ }
+
+ // Books
+ public async Task> GetAllBooksAsync()
+ {
+ await EnsureInitializedAsync();
+ return await _database!.Table().OrderByDescending(b => b.LastRead).ToListAsync();
+ }
+
+ public async Task GetBookByIdAsync(int id)
+ {
+ await EnsureInitializedAsync();
+ return await _database!.Table().Where(b => b.Id == id).FirstOrDefaultAsync();
+ }
+
+ public async Task SaveBookAsync(Book book)
+ {
+ await EnsureInitializedAsync();
+ if (book.Id != 0)
+ return await _database!.UpdateAsync(book);
+ return await _database!.InsertAsync(book);
+ }
+
+ public async Task UpdateBookAsync(Book book)
+ {
+ await EnsureInitializedAsync();
+ return await _database!.UpdateAsync(book);
+ }
+
+ public async Task DeleteBookAsync(Book book)
+ {
+ await EnsureInitializedAsync();
+
+ // Delete associated file
+ if (File.Exists(book.FilePath))
+ {
+ try { File.Delete(book.FilePath); } catch { }
+ }
+
+ // Delete progress records
+ await _database!.Table().DeleteAsync(p => p.BookId == book.Id);
+
+ return await _database!.DeleteAsync(book);
+ }
+
+ // Settings
+ public async Task GetSettingAsync(string key)
+ {
+ await EnsureInitializedAsync();
+ var setting = await _database!.Table().Where(s => s.Key == key).FirstOrDefaultAsync();
+ return setting?.Value;
+ }
+
+ public async Task SetSettingAsync(string key, string value)
+ {
+ await EnsureInitializedAsync();
+ var existing = await _database!.Table().Where(s => s.Key == key).FirstOrDefaultAsync();
+ if (existing != null)
+ {
+ existing.Value = value;
+ await _database.UpdateAsync(existing);
+ }
+ else
+ {
+ await _database.InsertAsync(new AppSettings { Key = key, Value = value });
+ }
+ }
+
+ public async Task> GetAllSettingsAsync()
+ {
+ await EnsureInitializedAsync();
+ var settings = await _database!.Table().ToListAsync();
+ return settings.ToDictionary(s => s.Key, s => s.Value);
+ }
+
+ // Reading Progress
+ public async Task SaveProgressAsync(ReadingProgress progress)
+ {
+ await EnsureInitializedAsync();
+ progress.Timestamp = DateTime.UtcNow;
+ await _database!.InsertAsync(progress);
+ }
+
+ public async Task GetLatestProgressAsync(int bookId)
+ {
+ await EnsureInitializedAsync();
+ return await _database!.Table()
+ .Where(p => p.BookId == bookId)
+ .OrderByDescending(p => p.Timestamp)
+ .FirstOrDefaultAsync();
+ }
+}
\ No newline at end of file
diff --git a/BookReader/Services/IBookParserService.cs b/BookReader/Services/IBookParserService.cs
new file mode 100644
index 0000000..d084be7
--- /dev/null
+++ b/BookReader/Services/IBookParserService.cs
@@ -0,0 +1,10 @@
+using BookReader.Models;
+
+namespace BookReader.Services;
+
+public interface IBookParserService
+{
+ Task ParseAndStoreBookAsync(string sourceFilePath, string originalFileName);
+ Task GetBookContentPathAsync(Book book);
+ string GetBooksDirectory();
+}
\ No newline at end of file
diff --git a/BookReader/Services/ICalibreWebService.cs b/BookReader/Services/ICalibreWebService.cs
new file mode 100644
index 0000000..13c1397
--- /dev/null
+++ b/BookReader/Services/ICalibreWebService.cs
@@ -0,0 +1,11 @@
+using BookReader.Models;
+
+namespace BookReader.Services;
+
+public interface ICalibreWebService
+{
+ Task TestConnectionAsync(string url, string username, string password);
+ Task> GetBooksAsync(string? searchQuery = null, int page = 0, int pageSize = 20);
+ Task DownloadBookAsync(CalibreBook book, IProgress? progress = null);
+ void Configure(string url, string username, string password);
+}
\ No newline at end of file
diff --git a/BookReader/Services/IDatabaseService.cs b/BookReader/Services/IDatabaseService.cs
new file mode 100644
index 0000000..99e659d
--- /dev/null
+++ b/BookReader/Services/IDatabaseService.cs
@@ -0,0 +1,24 @@
+using BookReader.Models;
+
+namespace BookReader.Services;
+
+public interface IDatabaseService
+{
+ Task InitializeAsync();
+
+ // Books
+ Task> GetAllBooksAsync();
+ Task GetBookByIdAsync(int id);
+ Task SaveBookAsync(Book book);
+ Task UpdateBookAsync(Book book);
+ Task DeleteBookAsync(Book book);
+
+ // Settings
+ Task GetSettingAsync(string key);
+ Task SetSettingAsync(string key, string value);
+ Task> GetAllSettingsAsync();
+
+ // Reading Progress
+ Task SaveProgressAsync(ReadingProgress progress);
+ Task GetLatestProgressAsync(int bookId);
+}
\ No newline at end of file
diff --git a/BookReader/Services/ISettingsService.cs b/BookReader/Services/ISettingsService.cs
new file mode 100644
index 0000000..3daf552
--- /dev/null
+++ b/BookReader/Services/ISettingsService.cs
@@ -0,0 +1,10 @@
+namespace BookReader.Services;
+
+public interface ISettingsService
+{
+ Task GetAsync(string key, string defaultValue = "");
+ Task SetAsync(string key, string value);
+ Task GetIntAsync(string key, int defaultValue = 0);
+ Task SetIntAsync(string key, int value);
+ Task> GetAllAsync();
+}
\ No newline at end of file
diff --git a/BookReader/Services/SettingsService.cs b/BookReader/Services/SettingsService.cs
new file mode 100644
index 0000000..028bd85
--- /dev/null
+++ b/BookReader/Services/SettingsService.cs
@@ -0,0 +1,40 @@
+namespace BookReader.Services;
+
+public class SettingsService : ISettingsService
+{
+ private readonly IDatabaseService _databaseService;
+
+ public SettingsService(IDatabaseService databaseService)
+ {
+ _databaseService = databaseService;
+ }
+
+ public async Task GetAsync(string key, string defaultValue = "")
+ {
+ var value = await _databaseService.GetSettingAsync(key);
+ return value ?? defaultValue;
+ }
+
+ public async Task SetAsync(string key, string value)
+ {
+ await _databaseService.SetSettingAsync(key, value);
+ }
+
+ public async Task GetIntAsync(string key, int defaultValue = 0)
+ {
+ var value = await _databaseService.GetSettingAsync(key);
+ if (int.TryParse(value, out var result))
+ return result;
+ return defaultValue;
+ }
+
+ public async Task SetIntAsync(string key, int value)
+ {
+ await _databaseService.SetSettingAsync(key, value.ToString());
+ }
+
+ public async Task> GetAllAsync()
+ {
+ return await _databaseService.GetAllSettingsAsync();
+ }
+}
\ No newline at end of file
diff --git a/BookReader/ViewModels/BaseViewModel.cs b/BookReader/ViewModels/BaseViewModel.cs
new file mode 100644
index 0000000..eead744
--- /dev/null
+++ b/BookReader/ViewModels/BaseViewModel.cs
@@ -0,0 +1,15 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace BookReader.ViewModels;
+
+public partial class BaseViewModel : ObservableObject
+{
+ [ObservableProperty]
+ private bool _isBusy;
+
+ [ObservableProperty]
+ private string _title = string.Empty;
+
+ [ObservableProperty]
+ private string _statusMessage = string.Empty;
+}
\ No newline at end of file
diff --git a/BookReader/ViewModels/BookshelfViewModel.cs b/BookReader/ViewModels/BookshelfViewModel.cs
new file mode 100644
index 0000000..b11a92d
--- /dev/null
+++ b/BookReader/ViewModels/BookshelfViewModel.cs
@@ -0,0 +1,159 @@
+using BookReader.Models;
+using BookReader.Services;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using System.Collections.ObjectModel;
+
+namespace BookReader.ViewModels;
+
+public partial class BookshelfViewModel : BaseViewModel
+{
+ private readonly IDatabaseService _databaseService;
+ private readonly IBookParserService _bookParserService;
+ private readonly ISettingsService _settingsService;
+
+ public ObservableCollection Books { get; } = new();
+
+ [ObservableProperty]
+ private bool _isEmpty;
+
+ public BookshelfViewModel(
+ IDatabaseService databaseService,
+ IBookParserService bookParserService,
+ ISettingsService settingsService)
+ {
+ _databaseService = databaseService;
+ _bookParserService = bookParserService;
+ _settingsService = settingsService;
+ Title = "My Library";
+ }
+
+ [RelayCommand]
+ public async Task LoadBooksAsync()
+ {
+ if (IsBusy) return;
+ IsBusy = true;
+
+ try
+ {
+ var books = await _databaseService.GetAllBooksAsync();
+ Books.Clear();
+ foreach (var book in books)
+ {
+ Books.Add(book);
+ }
+ IsEmpty = Books.Count == 0;
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Error loading books: {ex.Message}");
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ [RelayCommand]
+ public async Task AddBookFromFileAsync()
+ {
+ try
+ {
+ var customFileTypes = new FilePickerFileType(new Dictionary>
+ {
+ { DevicePlatform.Android, new[] { "application/epub+zip", "application/x-fictionbook+xml", "application/octet-stream", "*/*" } }
+ });
+
+ var result = await FilePicker.Default.PickAsync(new PickOptions
+ {
+ PickerTitle = "Select a book",
+ FileTypes = customFileTypes
+ });
+
+ if (result == null) return;
+
+ var extension = Path.GetExtension(result.FileName).ToLowerInvariant();
+ if (extension != ".epub" && extension != ".fb2")
+ {
+ await Shell.Current.DisplayAlert("Error", "Only EPUB and FB2 formats are supported.", "OK");
+ return;
+ }
+
+ IsBusy = true;
+ StatusMessage = "Adding book...";
+
+ // Copy to temp if needed and parse
+ string filePath;
+ using var stream = await result.OpenReadAsync();
+ var tempPath = Path.Combine(FileSystem.CacheDirectory, result.FileName);
+ using (var fileStream = File.Create(tempPath))
+ {
+ await stream.CopyToAsync(fileStream);
+ }
+ filePath = tempPath;
+
+ var book = await _bookParserService.ParseAndStoreBookAsync(filePath, result.FileName);
+ Books.Insert(0, book);
+ IsEmpty = false;
+
+ // Clean temp
+ try { File.Delete(tempPath); } catch { }
+ }
+ catch (Exception ex)
+ {
+ await Shell.Current.DisplayAlert("Error", $"Failed to add book: {ex.Message}", "OK");
+ }
+ finally
+ {
+ IsBusy = false;
+ StatusMessage = string.Empty;
+ }
+ }
+
+ [RelayCommand]
+ public async Task DeleteBookAsync(Book book)
+ {
+ if (book == null) return;
+
+ var confirm = await Shell.Current.DisplayAlert("Delete Book",
+ $"Are you sure you want to delete \"{book.Title}\"?", "Delete", "Cancel");
+
+ if (!confirm) return;
+
+ try
+ {
+ await _databaseService.DeleteBookAsync(book);
+ Books.Remove(book);
+ IsEmpty = Books.Count == 0;
+ }
+ catch (Exception ex)
+ {
+ await Shell.Current.DisplayAlert("Error", $"Failed to delete book: {ex.Message}", "OK");
+ }
+ }
+
+ [RelayCommand]
+ public async Task OpenBookAsync(Book book)
+ {
+ if (book == null) return;
+
+ var navigationParameter = new Dictionary
+ {
+ { "Book", book }
+ };
+
+ await Shell.Current.GoToAsync("reader", navigationParameter);
+ }
+
+ [RelayCommand]
+ public async Task OpenSettingsAsync()
+ {
+ await Shell.Current.GoToAsync("settings");
+ }
+
+ [RelayCommand]
+ public async Task OpenCalibreLibraryAsync()
+ {
+ await Shell.Current.GoToAsync("calibre");
+ }
+}
\ No newline at end of file
diff --git a/BookReader/ViewModels/CalibreLibraryViewModel.cs b/BookReader/ViewModels/CalibreLibraryViewModel.cs
new file mode 100644
index 0000000..8187344
--- /dev/null
+++ b/BookReader/ViewModels/CalibreLibraryViewModel.cs
@@ -0,0 +1,157 @@
+using BookReader.Models;
+using BookReader.Services;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using System.Collections.ObjectModel;
+
+namespace BookReader.ViewModels;
+
+public partial class CalibreLibraryViewModel : BaseViewModel
+{
+ private readonly ICalibreWebService _calibreWebService;
+ private readonly IBookParserService _bookParserService;
+ private readonly IDatabaseService _databaseService;
+ private readonly ISettingsService _settingsService;
+
+ public ObservableCollection Books { get; } = new();
+
+ [ObservableProperty]
+ private string _searchQuery = string.Empty;
+
+ [ObservableProperty]
+ private bool _isConfigured;
+
+ [ObservableProperty]
+ private string _downloadStatus = string.Empty;
+
+ private int _currentPage;
+
+ public CalibreLibraryViewModel(
+ ICalibreWebService calibreWebService,
+ IBookParserService bookParserService,
+ IDatabaseService databaseService,
+ ISettingsService settingsService)
+ {
+ _calibreWebService = calibreWebService;
+ _bookParserService = bookParserService;
+ _databaseService = databaseService;
+ _settingsService = settingsService;
+ Title = "Calibre Library";
+ }
+
+ [RelayCommand]
+ public async Task InitializeAsync()
+ {
+ var url = await _settingsService.GetAsync(SettingsKeys.CalibreUrl);
+ var username = await _settingsService.GetAsync(SettingsKeys.CalibreUsername);
+ var password = await _settingsService.GetAsync(SettingsKeys.CalibrePassword);
+
+ IsConfigured = !string.IsNullOrWhiteSpace(url);
+
+ if (IsConfigured)
+ {
+ _calibreWebService.Configure(url, username, password);
+ await LoadBooksAsync();
+ }
+ }
+
+ [RelayCommand]
+ public async Task LoadBooksAsync()
+ {
+ if (IsBusy || !IsConfigured) return;
+ IsBusy = true;
+ _currentPage = 0;
+
+ try
+ {
+ var books = await _calibreWebService.GetBooksAsync(SearchQuery, _currentPage);
+ Books.Clear();
+ foreach (var book in books)
+ {
+ Books.Add(book);
+ }
+ }
+ catch (Exception ex)
+ {
+ await Shell.Current.DisplayAlert("Error", $"Failed to load library: {ex.Message}", "OK");
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ [RelayCommand]
+ public async Task LoadMoreBooksAsync()
+ {
+ if (IsBusy || !IsConfigured) return;
+ IsBusy = true;
+ _currentPage++;
+
+ try
+ {
+ var books = await _calibreWebService.GetBooksAsync(SearchQuery, _currentPage);
+ foreach (var book in books)
+ {
+ Books.Add(book);
+ }
+ }
+ catch { }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ [RelayCommand]
+ public async Task SearchAsync()
+ {
+ await LoadBooksAsync();
+ }
+
+ [RelayCommand]
+ public async Task DownloadBookAsync(CalibreBook calibreBook)
+ {
+ if (calibreBook == null) return;
+
+ IsBusy = true;
+ DownloadStatus = $"Downloading {calibreBook.Title}...";
+
+ try
+ {
+ var progress = new Progress(p =>
+ {
+ DownloadStatus = $"Downloading... {p * 100:F0}%";
+ });
+
+ var filePath = await _calibreWebService.DownloadBookAsync(calibreBook, progress);
+ var fileName = $"{calibreBook.Title}.{calibreBook.Format}";
+
+ var book = await _bookParserService.ParseAndStoreBookAsync(filePath, fileName);
+ book.CalibreId = calibreBook.Id;
+
+ if (calibreBook.CoverImage != null)
+ book.CoverImage = calibreBook.CoverImage;
+
+ await _databaseService.UpdateBookAsync(book);
+
+ DownloadStatus = "Download complete!";
+ await Shell.Current.DisplayAlert("Success", $"\"{calibreBook.Title}\" has been added to your library.", "OK");
+ }
+ catch (Exception ex)
+ {
+ await Shell.Current.DisplayAlert("Error", $"Failed to download: {ex.Message}", "OK");
+ }
+ finally
+ {
+ IsBusy = false;
+ DownloadStatus = string.Empty;
+ }
+ }
+
+ [RelayCommand]
+ public async Task OpenSettingsAsync()
+ {
+ await Shell.Current.GoToAsync("settings");
+ }
+}
\ No newline at end of file
diff --git a/BookReader/ViewModels/ReaderViewModel.cs b/BookReader/ViewModels/ReaderViewModel.cs
new file mode 100644
index 0000000..edcfc5e
--- /dev/null
+++ b/BookReader/ViewModels/ReaderViewModel.cs
@@ -0,0 +1,162 @@
+using Android.Graphics.Fonts;
+using BookReader.Models;
+using BookReader.Services;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+
+namespace BookReader.ViewModels;
+
+[QueryProperty(nameof(Book), "Book")]
+public partial class ReaderViewModel : BaseViewModel
+{
+ private readonly IDatabaseService _databaseService;
+ private readonly ISettingsService _settingsService;
+
+ [ObservableProperty]
+ private Book? _book;
+
+ [ObservableProperty]
+ private bool _isMenuVisible;
+
+ [ObservableProperty]
+ private bool _isChapterListVisible;
+
+ [ObservableProperty]
+ private int _fontSize;
+
+ [ObservableProperty]
+ private string _fontFamily = "serif";
+
+ [ObservableProperty]
+ private List _chapters = new();
+
+ [ObservableProperty]
+ private string? _selectedChapter;
+
+ public List AvailableFonts { get; } = new()
+ {
+ "serif",
+ "sans-serif",
+ "monospace",
+ "Georgia",
+ "Palatino",
+ "Times New Roman",
+ "Arial",
+ "Verdana",
+ "Courier New"
+ };
+
+ public List AvailableFontSizes { get; } = new()
+ {
+ 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 36, 40
+ };
+
+ // Events for the view to subscribe to
+ public event Action? OnJavaScriptRequested;
+ public event Action? OnBookReady;
+
+ public ReaderViewModel(IDatabaseService databaseService, ISettingsService settingsService)
+ {
+ _databaseService = databaseService;
+ _settingsService = settingsService;
+ _fontSize = 18;
+ }
+
+ public async Task InitializeAsync()
+ {
+ var savedFontSize = await _settingsService.GetIntAsync(SettingsKeys.DefaultFontSize, 18);
+ var savedFontFamily = await _settingsService.GetAsync(SettingsKeys.DefaultFontFamily, "serif");
+
+ FontSize = savedFontSize;
+ FontFamily = savedFontFamily;
+ }
+
+ [RelayCommand]
+ public void ToggleMenu()
+ {
+ IsMenuVisible = !IsMenuVisible;
+ if (!IsMenuVisible)
+ IsChapterListVisible = false;
+ }
+
+ [RelayCommand]
+ public void HideMenu()
+ {
+ IsMenuVisible = false;
+ IsChapterListVisible = false;
+ }
+
+ [RelayCommand]
+ public void ToggleChapterList()
+ {
+ IsChapterListVisible = !IsChapterListVisible;
+ }
+
+ [RelayCommand]
+ public void ChangeFontSize(int size)
+ {
+ FontSize = size;
+ OnJavaScriptRequested?.Invoke($"setFontSize({size})");
+ _ = _settingsService.SetIntAsync(SettingsKeys.DefaultFontSize, size);
+ }
+
+ [RelayCommand]
+ public void ChangeFontFamily(string family)
+ {
+ FontFamily = family;
+ OnJavaScriptRequested?.Invoke($"setFontFamily('{family}')");
+ _ = _settingsService.SetAsync(SettingsKeys.DefaultFontFamily, family);
+ }
+
+ [RelayCommand]
+ public void GoToChapter(string chapter)
+ {
+ if (string.IsNullOrEmpty(chapter)) return;
+ OnJavaScriptRequested?.Invoke($"goToChapter('{EscapeJs(chapter)}')");
+ IsChapterListVisible = false;
+ IsMenuVisible = false;
+ }
+
+ public async Task SaveProgressAsync(double progress, string? cfi, string? chapter, int currentPage, int totalPages)
+ {
+ if (Book == null) return;
+
+ Book.ReadingProgress = progress;
+ Book.LastCfi = cfi;
+ Book.LastChapter = chapter;
+ Book.CurrentPage = currentPage;
+ Book.TotalPages = totalPages;
+ Book.LastRead = DateTime.UtcNow;
+
+ await _databaseService.UpdateBookAsync(Book);
+
+ await _databaseService.SaveProgressAsync(new ReadingProgress
+ {
+ BookId = Book.Id,
+ Cfi = cfi,
+ Progress = progress,
+ CurrentPage = currentPage,
+ ChapterTitle = chapter
+ });
+ }
+
+ public string GetBookFilePath()
+ {
+ return Book?.FilePath ?? string.Empty;
+ }
+
+ public string GetBookFormat()
+ {
+ return Book?.Format ?? "epub";
+ }
+
+ public string? GetLastCfi()
+ {
+ return Book?.LastCfi;
+ }
+
+ private static string EscapeJs(string value)
+ {
+ return value.Replace("\\", "\\\\").Replace("'", "\\'").Replace("\n", "\\n").Replace("\r", "\\r");
+ }
+}
\ No newline at end of file
diff --git a/BookReader/ViewModels/SettingsViewModel.cs b/BookReader/ViewModels/SettingsViewModel.cs
new file mode 100644
index 0000000..172e928
--- /dev/null
+++ b/BookReader/ViewModels/SettingsViewModel.cs
@@ -0,0 +1,105 @@
+using BookReader.Models;
+using BookReader.Services;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+
+namespace BookReader.ViewModels;
+
+public partial class SettingsViewModel : BaseViewModel
+{
+ private readonly ISettingsService _settingsService;
+ private readonly ICalibreWebService _calibreWebService;
+
+ [ObservableProperty]
+ private string _calibreUrl = string.Empty;
+
+ [ObservableProperty]
+ private string _calibreUsername = string.Empty;
+
+ [ObservableProperty]
+ private string _calibrePassword = string.Empty;
+
+ [ObservableProperty]
+ private int _defaultFontSize = 18;
+
+ [ObservableProperty]
+ private string _defaultFontFamily = "serif";
+
+ [ObservableProperty]
+ private string _connectionStatus = string.Empty;
+
+ [ObservableProperty]
+ private bool _isConnectionTesting;
+
+ public List AvailableFonts { get; } = new()
+ {
+ "serif", "sans-serif", "monospace", "Georgia", "Palatino",
+ "Times New Roman", "Arial", "Verdana", "Courier New"
+ };
+
+ public List AvailableFontSizes { get; } = new()
+ {
+ 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 36, 40
+ };
+
+ public SettingsViewModel(ISettingsService settingsService, ICalibreWebService calibreWebService)
+ {
+ _settingsService = settingsService;
+ _calibreWebService = calibreWebService;
+ Title = "Settings";
+ }
+
+ [RelayCommand]
+ public async Task LoadSettingsAsync()
+ {
+ CalibreUrl = await _settingsService.GetAsync(SettingsKeys.CalibreUrl);
+ CalibreUsername = await _settingsService.GetAsync(SettingsKeys.CalibreUsername);
+ CalibrePassword = await _settingsService.GetAsync(SettingsKeys.CalibrePassword);
+ DefaultFontSize = await _settingsService.GetIntAsync(SettingsKeys.DefaultFontSize, 18);
+ DefaultFontFamily = await _settingsService.GetAsync(SettingsKeys.DefaultFontFamily, "serif");
+ }
+
+ [RelayCommand]
+ public async Task SaveSettingsAsync()
+ {
+ await _settingsService.SetAsync(SettingsKeys.CalibreUrl, CalibreUrl);
+ await _settingsService.SetAsync(SettingsKeys.CalibreUsername, CalibreUsername);
+ await _settingsService.SetAsync(SettingsKeys.CalibrePassword, CalibrePassword);
+ await _settingsService.SetIntAsync(SettingsKeys.DefaultFontSize, DefaultFontSize);
+ await _settingsService.SetAsync(SettingsKeys.DefaultFontFamily, DefaultFontFamily);
+
+ if (!string.IsNullOrEmpty(CalibreUrl))
+ {
+ _calibreWebService.Configure(CalibreUrl, CalibreUsername, CalibrePassword);
+ }
+
+ await Shell.Current.DisplayAlert("Settings", "Settings saved successfully.", "OK");
+ }
+
+ [RelayCommand]
+ public async Task TestConnectionAsync()
+ {
+ if (string.IsNullOrWhiteSpace(CalibreUrl))
+ {
+ ConnectionStatus = "Please enter a URL";
+ return;
+ }
+
+ IsConnectionTesting = true;
+ ConnectionStatus = "Testing connection...";
+
+ try
+ {
+ var success = await _calibreWebService.TestConnectionAsync(CalibreUrl, CalibreUsername, CalibrePassword);
+ ConnectionStatus = success ? "✅ Connection successful!" : "❌ Connection failed";
+ }
+ catch (Exception ex)
+ {
+ ConnectionStatus = $"❌ Error: {ex.Message}";
+ }
+ finally
+ {
+ IsConnectionTesting = false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/BookReader/Views/BookshelfPage.xaml b/BookReader/Views/BookshelfPage.xaml
new file mode 100644
index 0000000..ca276b3
--- /dev/null
+++ b/BookReader/Views/BookshelfPage.xaml
@@ -0,0 +1,198 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/BookReader/Views/BookshelfPage.xaml.cs b/BookReader/Views/BookshelfPage.xaml.cs
new file mode 100644
index 0000000..3128645
--- /dev/null
+++ b/BookReader/Views/BookshelfPage.xaml.cs
@@ -0,0 +1,40 @@
+using BookReader.ViewModels;
+
+namespace BookReader.Views;
+
+public partial class BookshelfPage : ContentPage
+{
+ private readonly BookshelfViewModel _viewModel;
+
+ public BookshelfPage(BookshelfViewModel viewModel)
+ {
+ InitializeComponent();
+ _viewModel = viewModel;
+ BindingContext = viewModel;
+ }
+
+ protected override async void OnAppearing()
+ {
+ base.OnAppearing();
+ await _viewModel.LoadBooksCommand.ExecuteAsync(null);
+ }
+
+ private async void OnMenuClicked(object? sender, EventArgs e)
+ {
+ var action = await DisplayActionSheet("Menu", "Cancel", null,
+ "⚙️ Settings", "☁️ Calibre Library", "ℹ️ About");
+
+ switch (action)
+ {
+ case "⚙️ Settings":
+ await _viewModel.OpenSettingsCommand.ExecuteAsync(null);
+ break;
+ case "☁️ Calibre Library":
+ await _viewModel.OpenCalibreLibraryCommand.ExecuteAsync(null);
+ break;
+ case "ℹ️ About":
+ await DisplayAlert("About", "BookReader v1.0\nEPUB & FB2 Reader", "OK");
+ break;
+ }
+ }
+}
\ No newline at end of file
diff --git a/BookReader/Views/CalibreLibraryPage.xaml b/BookReader/Views/CalibreLibraryPage.xaml
new file mode 100644
index 0000000..0a1fd28
--- /dev/null
+++ b/BookReader/Views/CalibreLibraryPage.xaml
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/BookReader/Views/CalibreLibraryPage.xaml.cs b/BookReader/Views/CalibreLibraryPage.xaml.cs
new file mode 100644
index 0000000..19a7878
--- /dev/null
+++ b/BookReader/Views/CalibreLibraryPage.xaml.cs
@@ -0,0 +1,21 @@
+using BookReader.ViewModels;
+
+namespace BookReader.Views;
+
+public partial class CalibreLibraryPage : ContentPage
+{
+ private readonly CalibreLibraryViewModel _viewModel;
+
+ public CalibreLibraryPage(CalibreLibraryViewModel viewModel)
+ {
+ InitializeComponent();
+ _viewModel = viewModel;
+ BindingContext = viewModel;
+ }
+
+ protected override async void OnAppearing()
+ {
+ base.OnAppearing();
+ await _viewModel.InitializeCommand.ExecuteAsync(null);
+ }
+}
\ No newline at end of file
diff --git a/BookReader/Views/ReaderPage.xaml b/BookReader/Views/ReaderPage.xaml
new file mode 100644
index 0000000..bcb791c
--- /dev/null
+++ b/BookReader/Views/ReaderPage.xaml
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/BookReader/Views/ReaderPage.xaml.cs b/BookReader/Views/ReaderPage.xaml.cs
new file mode 100644
index 0000000..a26ca58
--- /dev/null
+++ b/BookReader/Views/ReaderPage.xaml.cs
@@ -0,0 +1,494 @@
+using BookReader.ViewModels;
+using Newtonsoft.Json.Linq;
+
+namespace BookReader.Views;
+
+public partial class ReaderPage : ContentPage
+{
+ private readonly ReaderViewModel _viewModel;
+ private bool _isBookLoaded;
+ private readonly List _chapterData = new();
+ private IDispatcherTimer? _pollTimer;
+ private IDispatcherTimer? _progressTimer;
+ private bool _isActive;
+
+ public ReaderPage(ReaderViewModel viewModel)
+ {
+ InitializeComponent();
+ _viewModel = viewModel;
+ BindingContext = viewModel;
+ _viewModel.OnJavaScriptRequested += OnJavaScriptRequested;
+ }
+
+ protected override async void OnAppearing()
+ {
+ base.OnAppearing();
+ _isActive = true;
+
+ try
+ {
+ await _viewModel.InitializeAsync();
+ System.Diagnostics.Debug.WriteLine("[Reader] ViewModel initialized");
+
+ StartProgressTimer();
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[Reader] Init error: {ex.Message}");
+ }
+ }
+
+ protected override async void OnDisappearing()
+ {
+ _isActive = false;
+ StopProgressTimer();
+ base.OnDisappearing();
+ await SaveCurrentProgress();
+ }
+
+ // ========== ТАЙМЕРЫ ==========
+
+ private void StartProgressTimer()
+ {
+ _progressTimer = Dispatcher.CreateTimer();
+ _progressTimer.Interval = TimeSpan.FromSeconds(10);
+ _progressTimer.Tick += async (s, e) =>
+ {
+ if (_isActive && _isBookLoaded)
+ {
+ await SaveCurrentProgress();
+ }
+ };
+ _progressTimer.Start();
+ }
+
+ private void StopProgressTimer()
+ {
+ _progressTimer?.Stop();
+ _progressTimer = null;
+ }
+
+ // ========== ЗАГРУЗКА КНИГИ ==========
+
+ private async Task LoadBookIntoWebView()
+ {
+ try
+ {
+ var book = _viewModel.Book;
+ if (book == null)
+ {
+ System.Diagnostics.Debug.WriteLine("[Reader] Book is null");
+ return;
+ }
+
+ if (_isBookLoaded)
+ {
+ System.Diagnostics.Debug.WriteLine("[Reader] Already loaded");
+ return;
+ }
+
+ if (!File.Exists(book.FilePath))
+ {
+ await DisplayAlert("Error", "Book file not found", "OK");
+ return;
+ }
+
+ System.Diagnostics.Debug.WriteLine($"[Reader] Loading: {book.Title} ({book.Format})");
+ System.Diagnostics.Debug.WriteLine($"[Reader] Path: {book.FilePath}");
+
+ // Читаем файл и конвертируем в Base64
+ var fileBytes = await File.ReadAllBytesAsync(book.FilePath);
+ var base64 = Convert.ToBase64String(fileBytes);
+ var format = book.Format.ToLowerInvariant();
+ var lastCfi = _viewModel.GetLastCfi() ?? "";
+
+ System.Diagnostics.Debug.WriteLine($"[Reader] File: {fileBytes.Length} bytes, Base64: {base64.Length} chars");
+
+ // Отправляем данные чанками чтобы не превысить лимит JS строки
+ const int chunkSize = 400_000;
+
+ if (base64.Length > chunkSize)
+ {
+ var chunks = SplitString(base64, chunkSize);
+ System.Diagnostics.Debug.WriteLine($"[Reader] Sending {chunks.Count} chunks");
+
+ await EvalJsAsync("window._bkChunks = [];");
+
+ for (int i = 0; i < chunks.Count; i++)
+ {
+ await EvalJsAsync($"window._bkChunks.push('{chunks[i]}');");
+ System.Diagnostics.Debug.WriteLine($"[Reader] Chunk {i + 1}/{chunks.Count}");
+ }
+
+ await EvalJsAsync(
+ $"window.loadBookFromBase64(window._bkChunks.join(''), '{format}', '{EscapeJs(lastCfi)}');"
+ );
+ await EvalJsAsync("delete window._bkChunks;");
+ }
+ else
+ {
+ await EvalJsAsync(
+ $"window.loadBookFromBase64('{base64}', '{format}', '{EscapeJs(lastCfi)}');"
+ );
+ }
+
+ _isBookLoaded = true;
+ System.Diagnostics.Debug.WriteLine("[Reader] Book load command sent");
+
+
+
+ // Применяем настройки шрифта сразу
+ await EvalJsAsync($"window.setFontSize({_viewModel.FontSize})");
+ await EvalJsAsync($"window.setFontFamily('{EscapeJs(_viewModel.FontFamily)}')");
+
+ System.Diagnostics.Debug.WriteLine("[Reader] Book fully loaded");
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[Reader] Load error: {ex.Message}\n{ex.StackTrace}");
+ await DisplayAlert("Error", $"Failed to load book: {ex.Message}", "OK");
+ }
+ }
+
+ // ========== ПОЛУЧЕНИЕ ГЛАВ ИЗ JS ==========
+
+ private async Task FetchChaptersFromJs()
+ {
+ try
+ {
+ var result = await EvalJsWithResultAsync(@"
+ (function() {
+ try {
+ if (typeof book !== 'undefined' && book && book.navigation && book.navigation.toc) {
+ var toc = book.navigation.toc;
+ var arr = [];
+ for (var i = 0; i < toc.length; i++) {
+ arr.push({ label: (toc[i].label || '').trim(), href: toc[i].href || '' });
+ }
+ return JSON.stringify(arr);
+ }
+ var titles = document.querySelectorAll('.fb2-title');
+ if (titles.length > 0) {
+ var arr2 = [];
+ for (var j = 0; j < titles.length; j++) {
+ arr2.push({ label: titles[j].textContent.trim(), href: titles[j].getAttribute('data-chapter') || j.toString() });
+ }
+ return JSON.stringify(arr2);
+ }
+ return '[]';
+ } catch(e) {
+ return '[]';
+ }
+ })()
+ ");
+
+ System.Diagnostics.Debug.WriteLine($"[Reader] Chapters raw: {result}");
+
+ if (string.IsNullOrEmpty(result) || result == "null") return;
+
+ // EvaluateJavaScriptAsync может вернуть экранированную строку
+ result = UnescapeJsResult(result);
+
+ var chapters = JArray.Parse(result);
+ _chapterData.Clear();
+
+ var chapterLabels = new List();
+ foreach (var ch in chapters)
+ {
+ var obj = ch as JObject;
+ if (obj != null)
+ {
+ _chapterData.Add(obj);
+ var label = obj["label"]?.ToString() ?? "";
+ if (!string.IsNullOrWhiteSpace(label))
+ {
+ chapterLabels.Add(label);
+ }
+ }
+ }
+
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ _viewModel.Chapters = chapterLabels;
+ });
+
+ System.Diagnostics.Debug.WriteLine($"[Reader] Loaded {chapterLabels.Count} chapters");
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[Reader] FetchChapters error: {ex.Message}");
+ }
+ }
+
+ // ========== СОХРАНЕНИЕ ПРОГРЕССА ==========
+
+ private async Task SaveCurrentProgress()
+ {
+ if (!_isBookLoaded) return;
+
+ try
+ {
+ var result = await EvalJsWithResultAsync("window.getProgress()");
+
+ System.Diagnostics.Debug.WriteLine($"[Reader] Progress raw: {result}");
+
+ if (string.IsNullOrEmpty(result) || result == "null" || result == "{}" || result == "undefined")
+ return;
+
+ result = UnescapeJsResult(result);
+
+ var data = JObject.Parse(result);
+ var progress = data["progress"]?.Value() ?? 0;
+ var cfi = data["cfi"]?.ToString();
+ var currentPage = data["currentPage"]?.Value() ?? 0;
+ var totalPages = data["totalPages"]?.Value() ?? 0;
+
+ await _viewModel.SaveProgressAsync(progress, cfi, null, currentPage, totalPages);
+ System.Diagnostics.Debug.WriteLine($"[Reader] Saved progress: {progress:P0}");
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[Reader] Save progress error: {ex.Message}");
+ }
+ }
+
+ // ========== ОБРАБОТКА СООБЩЕНИЙ ОТ JS ==========
+
+ private async void OnRawMessageReceived(object? sender, HybridWebViewRawMessageReceivedEventArgs e)
+ {
+ try
+ {
+ var message = e.Message;
+ if (string.IsNullOrEmpty(message)) return;
+
+ // ... (оставляем логику логирования и парсинга JSON) ...
+ var json = JObject.Parse(message);
+ var action = json["action"]?.ToString();
+ var data = json["data"] as JObject;
+
+ switch (action)
+ {
+ case "readerReady":
+ System.Diagnostics.Debug.WriteLine("[Reader] JS is ready! Loading book...");
+ // Вызываем загрузку книги ТОЛЬКО после того, как JS подтвердил готовность
+ _ = MainThread.InvokeOnMainThreadAsync(LoadBookIntoWebView);
+ break;
+
+ case "toggleMenu":
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ _viewModel.ToggleMenuCommand.Execute(null);
+ });
+ break;
+
+ case "progressUpdate":
+ if (data != null)
+ {
+ var progress = data["progress"]?.Value() ?? 0;
+ var cfi = data["cfi"]?.ToString();
+ var chapter = data["chapter"]?.ToString();
+ var currentPage = data["currentPage"]?.Value() ?? 0;
+ var totalPages = data["totalPages"]?.Value() ?? 0;
+ await _viewModel.SaveProgressAsync(progress, cfi, chapter, currentPage, totalPages);
+ }
+ break;
+
+ case "chaptersLoaded":
+ if (data != null)
+ {
+ var chapters = data["chapters"]?.ToObject>() ?? new();
+ _chapterData.Clear();
+ _chapterData.AddRange(chapters);
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ _viewModel.Chapters = chapters
+ .Select(c => c["label"]?.ToString() ?? "")
+ .Where(l => !string.IsNullOrWhiteSpace(l))
+ .ToList();
+ });
+ }
+ break;
+
+ case "bookReady":
+ MainThread.BeginInvokeOnMainThread(async () =>
+ {
+ await EvalJsAsync($"window.setFontSize({_viewModel.FontSize})");
+ await EvalJsAsync($"window.setFontFamily('{EscapeJs(_viewModel.FontFamily)}')");
+ });
+ break;
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[Reader] RawMsg error: {ex.Message}");
+ }
+ }
+
+ // ========== ОБРАБОТКА ЗАПРОСОВ JS ОТ VIEWMODEL ==========
+
+ private async void OnJavaScriptRequested(string script)
+ {
+ if (!_isActive) return;
+ await EvalJsAsync(script);
+ }
+
+ // ========== UI EVENTS ==========
+
+ private void OnDecreaseFontSize(object? sender, EventArgs e)
+ {
+ var idx = _viewModel.AvailableFontSizes.IndexOf(_viewModel.FontSize);
+ if (idx > 0)
+ {
+ _viewModel.ChangeFontSizeCommand.Execute(_viewModel.AvailableFontSizes[idx - 1]);
+ }
+ }
+
+ private void OnIncreaseFontSize(object? sender, EventArgs e)
+ {
+ var idx = _viewModel.AvailableFontSizes.IndexOf(_viewModel.FontSize);
+ if (idx < _viewModel.AvailableFontSizes.Count - 1)
+ {
+ _viewModel.ChangeFontSizeCommand.Execute(_viewModel.AvailableFontSizes[idx + 1]);
+ }
+ }
+
+ private void OnFontFamilyChanged(object? sender, EventArgs e)
+ {
+ if (FontFamilyPicker.SelectedItem is string family)
+ {
+ _viewModel.ChangeFontFamilyCommand.Execute(family);
+ }
+ }
+
+ private void OnChapterSelected(object? sender, SelectionChangedEventArgs e)
+ {
+ if (e.CurrentSelection.FirstOrDefault() is string chapterLabel)
+ {
+ var chapterObj = _chapterData.FirstOrDefault(c => c["label"]?.ToString() == chapterLabel);
+ var href = chapterObj?["href"]?.ToString() ?? chapterLabel;
+ _viewModel.GoToChapterCommand.Execute(href);
+ }
+ }
+
+ private void OnMenuPanelTapped(object? sender, TappedEventArgs e)
+ {
+ // Предотвращаем всплытие тапа на оверлей
+ }
+
+ private async void OnBackToLibrary(object? sender, EventArgs e)
+ {
+ await SaveCurrentProgress();
+ await Shell.Current.GoToAsync("..");
+ }
+
+ // ========== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ==========
+
+ ///
+ /// Выполняет JavaScript без ожидания результата
+ ///
+ private async Task EvalJsAsync(string script)
+ {
+ try
+ {
+ await MainThread.InvokeOnMainThreadAsync(async () =>
+ {
+ try
+ {
+ await ReaderWebView.EvaluateJavaScriptAsync(script);
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[Reader] JS eval error: {ex.Message}");
+ }
+ });
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[Reader] JS dispatch error: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Выполняет JavaScript и возвращает результат
+ ///
+ private async Task EvalJsWithResultAsync(string script)
+ {
+ string? result = null;
+ try
+ {
+ await MainThread.InvokeOnMainThreadAsync(async () =>
+ {
+ try
+ {
+ result = await ReaderWebView.EvaluateJavaScriptAsync(script);
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[Reader] JS result error: {ex.Message}");
+ }
+ });
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[Reader] JS result dispatch error: {ex.Message}");
+ }
+ return result;
+ }
+
+ ///
+ /// Разбивает строку на чанки заданного размера
+ ///
+ private static List SplitString(string str, int chunkSize)
+ {
+ var chunks = new List();
+ for (int i = 0; i < str.Length; i += chunkSize)
+ {
+ chunks.Add(str.Substring(i, Math.Min(chunkSize, str.Length - i)));
+ }
+ return chunks;
+ }
+
+ ///
+ /// Экранирует строку для вставки в JS код (внутри одинарных кавычек)
+ ///
+ private static string EscapeJs(string value)
+ {
+ if (string.IsNullOrEmpty(value))
+ return string.Empty;
+
+ return value
+ .Replace("\\", "\\\\")
+ .Replace("'", "\\'")
+ .Replace("\"", "\\\"")
+ .Replace("\n", "\\n")
+ .Replace("\r", "\\r")
+ .Replace("\t", "\\t");
+ }
+
+ ///
+ /// Убирает экранирование из результата EvaluateJavaScriptAsync.
+ /// Android WebView оборачивает результат в кавычки и экранирует.
+ ///
+ private static string UnescapeJsResult(string result)
+ {
+ if (string.IsNullOrEmpty(result))
+ return result;
+
+ // Убираем обрамляющие кавычки если есть
+ if (result.StartsWith("\"") && result.EndsWith("\""))
+ {
+ result = result.Substring(1, result.Length - 2);
+ }
+
+ // Убираем экранирование
+ result = result
+ .Replace("\\\"", "\"")
+ .Replace("\\\\", "\\")
+ .Replace("\\/", "/")
+ .Replace("\\n", "\n")
+ .Replace("\\r", "\r")
+ .Replace("\\t", "\t");
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/BookReader/Views/SettingsPage.xaml b/BookReader/Views/SettingsPage.xaml
new file mode 100644
index 0000000..dfbe018
--- /dev/null
+++ b/BookReader/Views/SettingsPage.xaml
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/BookReader/Views/SettingsPage.xaml.cs b/BookReader/Views/SettingsPage.xaml.cs
new file mode 100644
index 0000000..7bbcba5
--- /dev/null
+++ b/BookReader/Views/SettingsPage.xaml.cs
@@ -0,0 +1,44 @@
+using BookReader.ViewModels;
+
+namespace BookReader.Views;
+
+public partial class SettingsPage : ContentPage
+{
+ private readonly SettingsViewModel _viewModel;
+
+ public SettingsPage(SettingsViewModel viewModel)
+ {
+ InitializeComponent();
+ _viewModel = viewModel;
+ BindingContext = viewModel;
+ }
+
+ protected override async void OnAppearing()
+ {
+ base.OnAppearing();
+
+ try
+ {
+ await _viewModel.LoadSettingsCommand.ExecuteAsync(null);
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Error loading settings: {ex.Message}");
+ }
+ }
+
+ protected override async void OnDisappearing()
+ {
+ base.OnDisappearing();
+
+ // Автосохранение при выходе со страницы настроек
+ try
+ {
+ await _viewModel.SaveSettingsCommand.ExecuteAsync(null);
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Error auto-saving settings: {ex.Message}");
+ }
+ }
+}
\ No newline at end of file