diff --git a/cps/helper.py b/cps/helper.py index 5285b13e..17d6d0ae 100755 --- a/cps/helper.py +++ b/cps/helper.py @@ -27,6 +27,7 @@ except ImportError: import web import server import random +import subprocess try: import unidecode @@ -512,3 +513,28 @@ class Updater(threading.Thread): except Exception: logging.getLogger('cps.web').debug("Could not remove:" + item_path) shutil.rmtree(source, ignore_errors=True) + + +def check_unrar(unrarLocation): + error = False + if os.path.exists(unrarLocation): + try: + if sys.version_info < (3, 0): + unrarLocation = unrarLocation.encode(sys.getfilesystemencoding()) + p = subprocess.Popen(unrarLocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p.wait() + for lines in p.stdout.readlines(): + if isinstance(lines, bytes): + lines = lines.decode('utf-8') + value=re.search('UNRAR (.*) freeware', lines) + if value: + version = value.group(1) + except OSError as e: + error = True + web.app.logger.exception(e) + version =_(u'Error excecuting UnRar') + else: + version = _(u'Unrar binary file not found') + error=True + return (error, version) + diff --git a/cps/static/css/kthoom.css b/cps/static/css/kthoom.css index a6b41a32..770b94a2 100644 --- a/cps/static/css/kthoom.css +++ b/cps/static/css/kthoom.css @@ -1,32 +1,133 @@ body { - background: #444; - overflow: hidden; - color: white; - font-family: sans-serif; - margin: 0px; + background: #444; + overflow-x: hidden; + overflow-y: auto; + color: white; + font-family: sans-serif; + margin: 0px; } -.main { - position: re; - left: 5px; - overflow: hidden; - right: 5px; - text-align: center; - top: 5px; +#main { + text-align: center; + z-index: 2; +} + +.view { + padding-top:0px; +} + +#sidebar a, +#sidebar ul, +#sidebar li, +#sidebar li img { + max-width: 100%; + text-align: center; +} + +#sidebar ul { + position: relative; +} + +#sidebar a { + display: inline-block; + position: relative; + cursor: pointer; + padding: 4px; + + transition: all .2s ease; +} + +#sidebar a:hover, +#sidebar a:focus { + outline: none; + box-shadow: 0px 2px 8px 1px black; +} + +#sidebar a.active, +#sidebar a.active img + span { + background-color: #45B29D; +} + +#sidebar li img { + display: block; + max-height: 200px; +} + +#sidebar li img + span { + position: absolute; + bottom: 0; + right: 0; + padding: 2px; + min-width: 25px; + line-height: 25px; + background: #6b6b6b; + border-top-left-radius: 5px; +} + +#sidebar #panels { + z-index: 1; } #progress { - position: absolute; - display: inline; - left: 90px; - right: 160px; - height: 20px; - margin-top: 1px; - text-align: right; + position: absolute; + display: inline; + top: 0; + left: 0; + right: 0; + min-height: 4px; + font-family: sans-serif; + font-size: 10px; + line-height: 10px; + text-align: right; + + transition: min-height 150ms ease-in-out; +} + +#progress .bar-load, +#progress .bar-read { + display: flex; + align-items: flex-end; + justify-content: flex-end; + position: absolute; + top: 0; + left: 0; + bottom: 0; + + transition: width 150ms ease-in-out; +} + +#progress .bar-load { + color: #000; + background-color: #CCC; +} + +#progress .bar-read { + color: #FFF; + background-color: #45B29D; +} + +#progress .text { + display: none; + padding: 0 5px; +} + +#progress.loading, +#titlebar:hover #progress { + min-height: 10px; +} + +#progress.loading .text, +#titlebar:hover #progress .text { + display: inline-block; } .hide { - display: none !important; + display: none !important; +} + +#mainContent { + overflow: auto; + outline: none; } #mainText { @@ -42,29 +143,13 @@ body { word-wrap: break-word; } -#mainImage{ - margin-top: 32px; +#titlebar { + min-height: 25px; + height: auto; } -#titlebar.main { - opacity: 0; - position: absolute; - top: 0; - height: 30px; - left: 0; - right: 0; - background-color: black; - padding-bottom: 70px; - -webkit-transition: opacity 0.2s ease; - -moz-transition: opacity 0.2s ease; - transition: opacity 0.2s ease; - background: -moz-linear-gradient(top, rgba(0,2,34,1) 0%, rgba(0,1,24,1) 30%, rgba(0,0,0,0) 100%); /* FF3.6+ */ - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(0,2,34,1)), color-stop(30%,rgba(0,1,24,1)), color-stop(100%,rgba(0,0,0,0))); /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(top, rgba(0,2,34,1) 0%,rgba(0,1,24,1) 30%,rgba(0,0,0,0) 100%); /* Chrome10+,Safari5.1+ */ - background: -o-linear-gradient(top, rgba(0,2,34,1) 0%,rgba(0,1,24,1) 30%,rgba(0,0,0,0) 100%); /* Opera11.10+ */ - background: -ms-linear-gradient(top, rgba(0,2,34,1) 0%,rgba(0,1,24,1) 30%,rgba(0,0,0,0) 100%); /* IE10+ */ - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#000222', endColorstr='#00000000',GradientType=0 ); /* IE6-9 */ - background: linear-gradient(top, rgba(0,2,34,1) 0%,rgba(0,1,24,1) 30%,rgba(0,0,0,0) 100%); /* W3C */ +#metainfo { + max-width: 70%; } #prev { @@ -100,6 +185,72 @@ body { color: #000; } +th, td { + padding: 5px; +} + +th { + text-align: right; + vertical-align: top; +} + +.modal { + /* Makes the modal responsive. Note sure if this should be moved to main.css */ + margin: 0; + max-width: 96%; + transform: translate(-50%, -50%); +} +.md-content { + min-height: 320px; + height: auto; +} +.md-content > div { + overflow: hidden; +} +.md-content > div p { + padding: 5px 0; +} + +.settings-column { + float: left; + min-width: 35%; + padding-bottom: 10px; +} + +.inputs { + margin: -5px; +} + +.inputs input { + vertical-align: middle; +} + +.inputs label { + display: inline-block; + margin: 5px; + white-space: nowrap; +} + +.dark-theme #main { + background-color: #000; +} + +.dark-theme #titlebar { + color: #DDD; +} + +.dark-theme #titlebar a:active { + color: #FFF; +} + +.dark-theme #progress .bar-read { + background-color: red; +} + + +.dark-theme .overlay { + background-color: rgba(0,0,0,0.8); +} diff --git a/cps/static/js/bytestream.js b/cps/static/js/bytestream.js new file mode 100644 index 00000000..cb5df363 --- /dev/null +++ b/cps/static/js/bytestream.js @@ -0,0 +1,308 @@ +/* + * bytestream.js + * + * Provides readers for byte streams. + * + * Licensed under the MIT License + * + * Copyright(c) 2011 Google Inc. + * Copyright(c) 2011 antimatter15 + */ + +var bitjs = bitjs || {}; +bitjs.io = bitjs.io || {}; + + +/** + * This object allows you to peek and consume bytes as numbers and strings out + * of a stream. More bytes can be pushed into the back of the stream via the + * push() method. + */ +bitjs.io.ByteStream = class { + /** + * @param {ArrayBuffer} ab The ArrayBuffer object. + * @param {number=} opt_offset The offset into the ArrayBuffer + * @param {number=} opt_length The length of this BitStream + */ + constructor(ab, opt_offset, opt_length) { + if (!(ab instanceof ArrayBuffer)) { + throw 'Error! BitArray constructed with an invalid ArrayBuffer object'; + } + + const offset = opt_offset || 0; + const length = opt_length || ab.byteLength; + + /** + * The current page of bytes in the stream. + * @type {Uint8Array} + * @private + */ + this.bytes = new Uint8Array(ab, offset, length); + + /** + * The next pages of bytes in the stream. + * @type {Array} + * @private + */ + this.pages_ = []; + + /** + * The byte in the current page that we will read next. + * @type {Number} + * @private + */ + this.ptr = 0; + + /** + * An ever-increasing number. + * @type {Number} + * @private + */ + this.bytesRead_ = 0; + } + + /** + * Returns how many bytes have been read in the stream since the beginning of time. + */ + getNumBytesRead() { + return this.bytesRead_; + } + + /** + * Returns how many bytes are currently in the stream left to be read. + */ + getNumBytesLeft() { + const bytesInCurrentPage = (this.bytes.byteLength - this.ptr); + return this.pages_.reduce((acc, arr) => acc + arr.length, bytesInCurrentPage); + } + + /** + * Move the pointer ahead n bytes. If the pointer is at the end of the current array + * of bytes and we have another page of bytes, point at the new page. This is a private + * method, no validation is done. + * @param {number} n Number of bytes to increment. + * @private + */ + movePointer_(n) { + this.ptr += n; + this.bytesRead_ += n; + while (this.ptr >= this.bytes.length && this.pages_.length > 0) { + this.ptr -= this.bytes.length; + this.bytes = this.pages_.shift(); + } + } + + /** + * Peeks at the next n bytes as an unsigned number but does not advance the + * pointer. + * @param {number} n The number of bytes to peek at. Must be a positive integer. + * @return {number} The n bytes interpreted as an unsigned number. + */ + peekNumber(n) { + const num = parseInt(n, 10); + if (n !== num || num < 0) { + throw 'Error! Called peekNumber() with a non-positive integer'; + } else if (num === 0) { + return 0; + } + + if (n > 4) { + throw 'Error! Called peekNumber(' + n + + ') but this method can only reliably read numbers up to 4 bytes long'; + } + + if (this.getNumBytesLeft() < num) { + throw 'Error! Overflowed the byte stream while peekNumber()! n=' + num + + ', ptr=' + this.ptr + ', bytes.length=' + this.getNumBytesLeft(); + } + + let result = 0; + let curPage = this.bytes; + let pageIndex = 0; + let ptr = this.ptr; + for (let i = 0; i < num; ++i) { + result |= (curPage[ptr++] << (i * 8)); + + if (ptr >= curPage.length) { + curPage = this.pages_[pageIndex++]; + ptr = 0; + } + } + + return result; + } + + + /** + * Returns the next n bytes as an unsigned number (or -1 on error) + * and advances the stream pointer n bytes. + * @param {number} n The number of bytes to read. Must be a positive integer. + * @return {number} The n bytes interpreted as an unsigned number. + */ + readNumber(n) { + const num = this.peekNumber(n); + this.movePointer_(n); + return num; + } + + + /** + * Returns the next n bytes as a signed number but does not advance the + * pointer. + * @param {number} n The number of bytes to read. Must be a positive integer. + * @return {number} The bytes interpreted as a signed number. + */ + peekSignedNumber(n) { + let num = this.peekNumber(n); + const HALF = Math.pow(2, (n * 8) - 1); + const FULL = HALF * 2; + + if (num >= HALF) num -= FULL; + + return num; + } + + + /** + * Returns the next n bytes as a signed number and advances the stream pointer. + * @param {number} n The number of bytes to read. Must be a positive integer. + * @return {number} The bytes interpreted as a signed number. + */ + readSignedNumber(n) { + const num = this.peekSignedNumber(n); + this.movePointer_(n); + return num; + } + + + /** + * This returns n bytes as a sub-array, advancing the pointer if movePointers + * is true. + * @param {number} n The number of bytes to read. Must be a positive integer. + * @param {boolean} movePointers Whether to move the pointers. + * @return {Uint8Array} The subarray. + */ + peekBytes(n, movePointers) { + const num = parseInt(n, 10); + if (n !== num || num < 0) { + throw 'Error! Called peekBytes() with a non-positive integer'; + } else if (num === 0) { + return new Uint8Array(); + } + + const totalBytesLeft = this.getNumBytesLeft(); + if (num > totalBytesLeft) { + throw 'Error! Overflowed the byte stream during peekBytes! n=' + num + + ', ptr=' + this.ptr + ', bytes.length=' + this.getNumBytesLeft(); + } + + const result = new Uint8Array(num); + let curPage = this.bytes; + let ptr = this.ptr; + let bytesLeftToCopy = num; + let pageIndex = 0; + while (bytesLeftToCopy > 0) { + const bytesLeftInPage = curPage.length - ptr; + const sourceLength = Math.min(bytesLeftToCopy, bytesLeftInPage); + + result.set(curPage.subarray(ptr, ptr + sourceLength), num - bytesLeftToCopy); + + ptr += sourceLength; + if (ptr >= curPage.length) { + curPage = this.pages_[pageIndex++]; + ptr = 0; + } + + bytesLeftToCopy -= sourceLength; + } + + if (movePointers) { + this.movePointer_(num); + } + + return result; + } + + /** + * Reads the next n bytes as a sub-array. + * @param {number} n The number of bytes to read. Must be a positive integer. + * @return {Uint8Array} The subarray. + */ + readBytes(n) { + return this.peekBytes(n, true); + } + + /** + * Peeks at the next n bytes as an ASCII string but does not advance the pointer. + * @param {number} n The number of bytes to peek at. Must be a positive integer. + * @return {string} The next n bytes as a string. + */ + peekString(n) { + const num = parseInt(n, 10); + if (n !== num || num < 0) { + throw 'Error! Called peekString() with a non-positive integer'; + } else if (num === 0) { + return ''; + } + + const totalBytesLeft = this.getNumBytesLeft(); + if (num > totalBytesLeft) { + throw 'Error! Overflowed the byte stream while peekString()! n=' + num + + ', ptr=' + this.ptr + ', bytes.length=' + this.getNumBytesLeft(); + } + + let result = new Array(num); + let curPage = this.bytes; + let pageIndex = 0; + let ptr = this.ptr; + for (let i = 0; i < num; ++i) { + result[i] = String.fromCharCode(curPage[ptr++]); + if (ptr >= curPage.length) { + curPage = this.pages_[pageIndex++]; + ptr = 0; + } + } + + return result.join(''); + } + + /** + * Returns the next n bytes as an ASCII string and advances the stream pointer + * n bytes. + * @param {number} n The number of bytes to read. Must be a positive integer. + * @return {string} The next n bytes as a string. + */ + readString(n) { + const strToReturn = this.peekString(n); + this.movePointer_(n); + return strToReturn; + } + + /** + * Feeds more bytes into the back of the stream. + * @param {ArrayBuffer} ab + */ + push(ab) { + if (!(ab instanceof ArrayBuffer)) { + throw 'Error! ByteStream.push() called with an invalid ArrayBuffer object'; + } + + this.pages_.push(new Uint8Array(ab)); + // If the pointer is at the end of the current page of bytes, this will advance + // to the next page. + this.movePointer_(0); + } + + /** + * Creates a new ByteStream from this ByteStream that can be read / peeked. + * @return {bitjs.io.ByteStream} A clone of this ByteStream. + */ + tee() { + const clone = new bitjs.io.ByteStream(this.bytes.buffer); + clone.bytes = this.bytes; + clone.ptr = this.ptr; + clone.pages_ = this.pages_.slice(); + clone.bytesRead_ = this.bytesRead_; + return clone; + } +} diff --git a/cps/static/js/kthoom.js b/cps/static/js/kthoom.js index 16c51cdd..6a107b9a 100644 --- a/cps/static/js/kthoom.js +++ b/cps/static/js/kthoom.js @@ -15,7 +15,7 @@ * Typed Arrays: http://www.khronos.org/registry/typedarray/specs/latest/#6 */ -/* global bitjs */ +/* global screenfull, bitjs */ if (window.opera) { window.console.log = function(str) { @@ -35,62 +35,69 @@ function getElem(id) { return document.getElementById(id); } -if (window.kthoom === undefined) { +if (typeof window.kthoom === "undefined" ) { kthoom = {}; } // key codes kthoom.Key = { ESCAPE: 27, + SPACE: 32, LEFT: 37, UP: 38, RIGHT: 39, - DOWN: 40, - A: 65, B: 66, C: 67, D: 68, E: 69, F: 70, G: 71, H: 72, I: 73, J: 74, K: 75, L: 76, M: 77, + DOWN: 40, + A: 65, B: 66, C: 67, D: 68, E: 69, F: 70, G: 71, H: 72, I: 73, J: 74, K: 75, L: 76, M: 77, N: 78, O: 79, P: 80, Q: 81, R: 82, S: 83, T: 84, U: 85, V: 86, W: 87, X: 88, Y: 89, Z: 90, QUESTION_MARK: 191, LEFT_SQUARE_BRACKET: 219, RIGHT_SQUARE_BRACKET: 221 }; -// The rotation orientation of the comic. -kthoom.rotateTimes = 0; - // global variables var unarchiver = null; var currentImage = 0; var imageFiles = []; var imageFilenames = []; var totalImages = 0; -var lastCompletion = 0; -var hflip = false, vflip = false, fitMode = kthoom.Key.B; -var canKeyNext = true, canKeyPrev = true; +var settings = { + hflip: false, + vflip: false, + rotateTimes: 0, + fitMode: kthoom.Key.B, + theme: "light" +}; kthoom.saveSettings = function() { - localStorage.kthoomSettings = JSON.stringify({ - rotateTimes: kthoom.rotateTimes, - hflip: hflip, - vflip: vflip, - fitMode: fitMode - }); + localStorage.kthoomSettings = JSON.stringify(settings); }; kthoom.loadSettings = function() { try { - if (localStorage.kthoomSettings.length < 10){ + if (!localStorage.kthoomSettings) { return; } - var s = JSON.parse(localStorage.kthoomSettings); - kthoom.rotateTimes = s.rotateTimes; - hflip = s.hflip; - vflip = s.vflip; - fitMode = s.fitMode; + + $.extend(settings, JSON.parse(localStorage.kthoomSettings)); + + kthoom.setSettings(); } catch (err) { alert("Error load settings"); } }; +kthoom.setSettings = function() { + // Set settings control values + $.each(settings, function(key, value) { + if (typeof value === "boolean") { + $("input[name=" + key + "]").prop("checked", value); + } else { + $("input[name=" + key + "]").val([value]); + } + }); +}; + var createURLFromArray = function(array, mimeType) { var offset = array.byteOffset, len = array.byteLength; var url; @@ -127,9 +134,6 @@ var createURLFromArray = function(array, mimeType) { // Stores an image filename and its data: URI. -// TODO: investigate if we really need to store as base64 (leave off ;base64 and just -// non-safe URL characters are encoded as %xx ?) -// This would save 25% on memory since base64-encoded strings are 4/3 the size of the binary kthoom.ImageFile = function(file) { this.filename = file.filename; var fileExtension = file.filename.split(".").pop().toLowerCase(); @@ -141,138 +145,13 @@ kthoom.ImageFile = function(file) { }; -kthoom.initProgressMeter = function() { - var svgns = "http://www.w3.org/2000/svg"; - var pdiv = $("#progress")[0]; - var svg = document.createElementNS(svgns, "svg"); - svg.style.width = "100%"; - svg.style.height = "100%"; - - var defs = document.createElementNS(svgns, "defs"); - - var patt = document.createElementNS(svgns, "pattern"); - patt.id = "progress_pattern"; - patt.setAttribute("width", "30"); - patt.setAttribute("height", "20"); - patt.setAttribute("patternUnits", "userSpaceOnUse"); - - var rect = document.createElementNS(svgns, "rect"); - rect.setAttribute("width", "100%"); - rect.setAttribute("height", "100%"); - rect.setAttribute("fill", "#cc2929"); - - var poly = document.createElementNS(svgns, "polygon"); - poly.setAttribute("fill", "yellow"); - poly.setAttribute("points", "15,0 30,0 15,20 0,20"); - - patt.appendChild(rect); - patt.appendChild(poly); - defs.appendChild(patt); - - svg.appendChild(defs); - - var g = document.createElementNS(svgns, "g"); - - var outline = document.createElementNS(svgns, "rect"); - outline.setAttribute("y", "1"); - outline.setAttribute("width", "100%"); - outline.setAttribute("height", "15"); - outline.setAttribute("fill", "#777"); - outline.setAttribute("stroke", "white"); - outline.setAttribute("rx", "5"); - outline.setAttribute("ry", "5"); - g.appendChild(outline); - - var title = document.createElementNS(svgns, "text"); - title.id = "progress_title"; - title.appendChild(document.createTextNode("0%")); - title.setAttribute("y", "13"); - title.setAttribute("x", "99.5%"); - title.setAttribute("fill", "white"); - title.setAttribute("font-size", "12px"); - title.setAttribute("text-anchor", "end"); - g.appendChild(title); - - var meter = document.createElementNS(svgns, "rect"); - meter.id = "meter"; - meter.setAttribute("width", "0%"); - meter.setAttribute("height", "17"); - meter.setAttribute("fill", "url(#progress_pattern)"); - meter.setAttribute("rx", "5"); - meter.setAttribute("ry", "5"); - - var meter2 = document.createElementNS(svgns, "rect"); - meter2.id = "meter2"; - meter2.setAttribute("width", "0%"); - meter2.setAttribute("height", "17"); - meter2.setAttribute("opacity", "0.8"); - meter2.setAttribute("fill", "#007fff"); - meter2.setAttribute("rx", "5"); - meter2.setAttribute("ry", "5"); - - g.appendChild(meter); - g.appendChild(meter2); - - var page = document.createElementNS(svgns, "text"); - page.id = "page"; - page.appendChild(document.createTextNode("0/0")); - page.setAttribute("y", "13"); - page.setAttribute("x", "0.5%"); - page.setAttribute("fill", "white"); - page.setAttribute("font-size", "12px"); - g.appendChild(page); - - - svg.appendChild(g); - pdiv.appendChild(svg); - var l; - svg.onclick = function(e) { - for (var x = pdiv, l = 0; x !== document.documentElement; x = x.parentNode) l += x.offsetLeft; - var page = Math.max(1, Math.ceil(((e.clientX - l) / pdiv.offsetWidth) * totalImages)) - 1; +function initProgressClick() { + $("#progress").click(function(e) { + var page = Math.max(1, Math.ceil((e.offsetX / $(this).width()) * totalImages)) - 1; currentImage = page; updatePage(); - }; -} - -kthoom.setProgressMeter = function(pct, optLabel) { - pct = (pct * 100); - var part = 1 / totalImages; - var remain = ((pct - lastCompletion) / 100) / part; - var fract = Math.min(1, remain); - var smartpct = ((imageFiles.length / totalImages) + (fract * part)) * 100; - if (totalImages === 0) smartpct = pct; - - // + Math.min((pct - lastCompletion), 100/totalImages * 0.9 + (pct - lastCompletion - 100/totalImages)/2, 100/totalImages); - var oldval = parseFloat(getElem("meter").getAttribute("width")); - if (isNaN(oldval)) oldval = 0; - var weight = 0.5; - smartpct = ((weight * smartpct) + ((1 - weight) * oldval)); - if (pct === 100) smartpct = 100; - - if (!isNaN(smartpct)) { - getElem("meter").setAttribute("width", smartpct + "%"); - } - var title = getElem("progress_title"); - while (title.firstChild) title.removeChild(title.firstChild); - - var labelText = pct.toFixed(2) + "% " + imageFiles.length + "/" + totalImages + ""; - if (optLabel) { - labelText = optLabel + " " + labelText; - } - title.appendChild(document.createTextNode(labelText)); - - getElem("meter2").setAttribute("width", - 100 * (totalImages === 0 ? 0 : ((currentImage + 1) / totalImages)) + "%"); - - var titlePage = getElem("page"); - while (titlePage.firstChild) titlePage.removeChild(titlePage.firstChild); - titlePage.appendChild(document.createTextNode( (currentImage + 1) + "/" + totalImages )); - - if (pct > 0) { - //getElem('nav').className = ''; - getElem("progress").className = ""; - } -} + }); +}; function loadFromArrayBuffer(ab) { var start = (new Date).getTime(); @@ -291,8 +170,7 @@ function loadFromArrayBuffer(ab) { function(e) { var percentage = e.currentBytesUnarchived / e.totalUncompressedBytesInArchive; totalImages = e.totalFilesInArchive; - kthoom.setProgressMeter(percentage, "Unzipping"); - // display nav + updateProgress(percentage *100); lastCompletion = percentage * 100; }); unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.EXTRACT, @@ -304,11 +182,20 @@ function loadFromArrayBuffer(ab) { if (imageFilenames.indexOf(f.filename) === -1) { imageFilenames.push(f.filename); imageFiles.push(new kthoom.ImageFile(f)); + // add thumbnails to the TOC list + $("#thumbnails").append( + "
  • " + + "" + + "" + + "" + imageFiles.length + "" + + "" + + "
  • " + ); } } // display first page if we haven't yet if (imageFiles.length === currentImage + 1) { - updatePage(); + updatePage(lastCompletion); } }); unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.FINISH, @@ -322,19 +209,54 @@ function loadFromArrayBuffer(ab) { } } +function scrollTocToActive() { + // Scroll to the thumbnail in the TOC on page change + $('#tocView').stop().animate({ + scrollTop: $('#tocView a.active').position().top + }, 200); +} function updatePage() { - var title = getElem("page"); - while (title.firstChild) title.removeChild(title.firstChild); - title.appendChild(document.createTextNode( (currentImage + 1 ) + "/" + totalImages )); + $('.page').text((currentImage + 1 ) + "/" + totalImages); + + // Mark the current page in the TOC + $('#tocView a[data-page]') + // Remove the currently active thumbnail + .removeClass('active') + // Find the new one + .filter('[data-page='+ (currentImage + 1) +']') + // Set it to active + .addClass('active'); + + scrollTocToActive(); + updateProgress(); - getElem("meter2").setAttribute("width", - 100 * (totalImages === 0 ? 0 : ((currentImage + 1 ) / totalImages)) + "%"); if (imageFiles[currentImage]) { setImage(imageFiles[currentImage].dataURI); } else { setImage("loading"); } + + $("body").toggleClass("dark-theme", settings.theme === "dark"); + + kthoom.setSettings(); + kthoom.saveSettings(); +} + +function updateProgress(loadPercentage) { + // Set the load/unzip progress if it's passed in + if (loadPercentage) { + $("#progress .bar-load").css({ width: loadPercentage + "%" }); + + if (loadPercentage === 100) { + $("#progress") + .removeClass('loading') + .find(".load").text(''); + } + } + + // Set page progress bar + $("#progress .bar-read").css({ width: totalImages === 0 ? 0 : Math.round((currentImage + 1) / totalImages * 100) + "%"}); } function setImage(url) { @@ -345,81 +267,92 @@ function setImage(url) { updateScale(true); canvas.width = innerWidth - 100; canvas.height = 200; - x.fillStyle = "red"; - x.font = "50px sans-serif"; + x.fillStyle = "black"; + x.textAlign = "center"; + x.font = "24px sans-serif"; x.strokeStyle = "black"; - x.fillText("Loading Page #" + (currentImage + 1), 100, 100); + x.fillText("Loading Page #" + (currentImage + 1), innerWidth / 2, 100); } else { - if ($("body").css("scrollHeight") / innerHeight > 1) { - $("body").css("overflowY", "scroll"); - } - - var img = new Image(); - img.onerror = function() { - canvas.width = innerWidth - 100; - canvas.height = 300; + if (url === "error") { updateScale(true); - x.fillStyle = "orange"; - x.font = "50px sans-serif"; + canvas.width = innerWidth - 100; + canvas.height = 200; + x.fillStyle = "black"; + x.textAlign = "center"; + x.font = "24px sans-serif"; x.strokeStyle = "black"; - x.fillText("Page #" + (currentImage + 1) + " (" + - imageFiles[currentImage].filename + ")", 100, 100); - x.fillStyle = "red"; - x.fillText("Is corrupt or not an image", 100, 200); - - var xhr = new XMLHttpRequest(); - if (/(html|htm)$/.test(imageFiles[currentImage].filename)) { - xhr.open("GET", url, true); - xhr.onload = function() { - //document.getElementById('mainText').style.display = ''; - $("#mainText").css("display", ""); - $("#mainText").innerHTML(""); - } - xhr.send(null); - } else if (!/(jpg|jpeg|png|gif)$/.test(imageFiles[currentImage].filename) && imageFiles[currentImage].data.uncompressedSize < 10 * 1024) { - xhr.open("GET", url, true); - xhr.onload = function() { - $("#mainText").css("display", ""); - $("#mainText").innerText(xhr.responseText); - }; - xhr.send(null); + x.fillText("Unable to decompress image #" + (currentImage + 1), innerWidth / 2, 100); + } else { + if ($("body").css("scrollHeight") / innerHeight > 1) { + $("body").css("overflowY", "scroll"); } - }; - img.onload = function() { - var h = img.height, - w = img.width, - sw = w, - sh = h; - kthoom.rotateTimes = (4 + kthoom.rotateTimes) % 4; - x.save(); - if (kthoom.rotateTimes % 2 === 1) { - sh = w; - sw = h; - } - canvas.height = sh; - canvas.width = sw; - x.translate(sw / 2, sh / 2); - x.rotate(Math.PI / 2 * kthoom.rotateTimes); - x.translate(-w / 2, -h / 2); - if (vflip) { - x.scale(1, -1); - x.translate(0, -h); - } - if (hflip) { - x.scale(-1, 1); - x.translate(-w, 0); - } - canvas.style.display = "none"; - scrollTo(0, 0); - x.drawImage(img, 0, 0); - updateScale(); + var img = new Image(); + img.onerror = function() { + canvas.width = innerWidth - 100; + canvas.height = 300; + updateScale(true); + x.fillStyle = "black"; + x.font = "50px sans-serif"; + x.strokeStyle = "black"; + x.fillText("Page #" + (currentImage + 1) + " (" + + imageFiles[currentImage].filename + ")", innerWidth / 2, 100); + x.fillStyle = "black"; + x.fillText("Is corrupt or not an image", innerWidth / 2, 200); + + var xhr = new XMLHttpRequest(); + if (/(html|htm)$/.test(imageFiles[currentImage].filename)) { + xhr.open("GET", url, true); + xhr.onload = function() { + $("#mainText").css("display", ""); + $("#mainText").innerHTML(""); + } + xhr.send(null); + } else if (!/(jpg|jpeg|png|gif)$/.test(imageFiles[currentImage].filename) && imageFiles[currentImage].data.uncompressedSize < 10 * 1024) { + xhr.open("GET", url, true); + xhr.onload = function() { + $("#mainText").css("display", ""); + $("#mainText").innerText(xhr.responseText); + }; + xhr.send(null); + } + }; + img.onload = function() { + var h = img.height, + w = img.width, + sw = w, + sh = h; + settings.rotateTimes = (4 + settings.rotateTimes) % 4; + x.save(); + if (settings.rotateTimes % 2 === 1) { + sh = w; + sw = h; + } + canvas.height = sh; + canvas.width = sw; + x.translate(sw / 2, sh / 2); + x.rotate(Math.PI / 2 * settings.rotateTimes); + x.translate(-w / 2, -h / 2); + if (settings.vflip) { + x.scale(1, -1); + x.translate(0, -h); + } + if (settings.hflip) { + x.scale(-1, 1); + x.translate(-w, 0); + } + canvas.style.display = "none"; + scrollTo(0, 0); + x.drawImage(img, 0, 0); + + updateScale(false); - canvas.style.display = ""; - $("body").css("overflowY", ""); - x.restore(); - }; - img.src = url; + canvas.style.display = ""; + $("body").css("overflowY", ""); + x.restore(); + }; + img.src = url; + } } } @@ -449,149 +382,254 @@ function updateScale(clear) { mainImageStyle.height = ""; mainImageStyle.maxWidth = ""; mainImageStyle.maxHeight = ""; - var maxheight = innerHeight - 15; - if (!/main/.test(getElem("titlebar").className)) { - maxheight -= 25; - } - if (clear || fitMode === kthoom.Key.N) { - } else if (fitMode === kthoom.Key.B) { - mainImageStyle.maxWidth = "100%"; - mainImageStyle.maxHeight = maxheight + "px"; - } else if (fitMode === kthoom.Key.H) { - mainImageStyle.height = maxheight + "px"; - } else if (fitMode === kthoom.Key.W) { - mainImageStyle.width = "100%"; + var maxheight = innerHeight - 50; + + if (!clear) { + switch (settings.fitMode) { + case kthoom.Key.B: + mainImageStyle.maxWidth = "100%"; + mainImageStyle.maxHeight = maxheight + "px"; + break; + case kthoom.Key.H: + mainImageStyle.height = maxheight + "px"; + break; + case kthoom.Key.W: + mainImageStyle.width = "100%"; + break; + default: + break; + } } + $("#mainContent").css({maxHeight: maxheight + 5}); + kthoom.setSettings(); kthoom.saveSettings(); } function keyHandler(evt) { - var code = evt.keyCode; - - if ($("#progress").css("display") === "none"){ - return; - } - canKeyNext = (($("body").css("offsetWidth") + $("body").css("scrollLeft")) / $("body").css("scrollWidth")) >= 1; - canKeyPrev = (scrollX <= 0); - - if (evt.ctrlKey || evt.shiftKey || evt.metaKey) return; - switch (code) { + var hasModifier = evt.ctrlKey || evt.shiftKey || evt.metaKey; + switch (evt.keyCode) { case kthoom.Key.LEFT: - if (canKeyPrev) showPrevPage(); + if (hasModifier) break; + showPrevPage(); break; case kthoom.Key.RIGHT: - if (canKeyNext) showNextPage(); + if (hasModifier) break; + showNextPage(); break; case kthoom.Key.L: - kthoom.rotateTimes--; - if (kthoom.rotateTimes < 0) { - kthoom.rotateTimes = 3; + if (hasModifier) break; + settings.rotateTimes--; + if (settings.rotateTimes < 0) { + settings.rotateTimes = 3; } updatePage(); break; case kthoom.Key.R: - kthoom.rotateTimes++; - if (kthoom.rotateTimes > 3) { - kthoom.rotateTimes = 0; + if (hasModifier) break; + settings.rotateTimes++; + if (settings.rotateTimes > 3) { + settings.rotateTimes = 0; } updatePage(); break; case kthoom.Key.F: - if (!hflip && !vflip) { - hflip = true; - } else if (hflip === true) { - vflip = true; - hflip = false; - } else if (vflip === true) { - vflip = false; + if (hasModifier) break; + if (!settings.hflip && !settings.vflip) { + settings.hflip = true; + } else if (settings.hflip === true && settings.vflip === true) { + settings.vflip = false; + settings.hflip = false; + } else if (settings.hflip === true) { + settings.vflip = true; + settings.hflip = false; + } else if (settings.vflip === true) { + settings.hflip = true; } updatePage(); break; case kthoom.Key.W: - fitMode = kthoom.Key.W; - updateScale(); + if (hasModifier) break; + settings.fitMode = kthoom.Key.W; + updateScale(false); break; case kthoom.Key.H: - fitMode = kthoom.Key.H; - updateScale(); + if (hasModifier) break; + settings.fitMode = kthoom.Key.H; + updateScale(false); break; case kthoom.Key.B: - fitMode = kthoom.Key.B; - updateScale(); + if (hasModifier) break; + settings.fitMode = kthoom.Key.B; + updateScale(false); break; case kthoom.Key.N: - fitMode = kthoom.Key.N; - updateScale(); + if (hasModifier) break; + settings.fitMode = kthoom.Key.N; + updateScale(false); + break; + case kthoom.Key.SPACE: + var container = $('#mainContent'); + var atTop = container.scrollTop() === 0; + var atBottom = container.scrollTop() >= container[0].scrollHeight - container.height(); + + if (evt.shiftKey && atTop) { + evt.preventDefault(); + // If it's Shift + Space and the container is at the top of the page + showPrevPage(); + } else if (!evt.shiftKey && atBottom) { + evt.preventDefault(); + // If you're at the bottom of the page and you only pressed space + showNextPage(); + container.scrollTop(0); + } break; default: - //console.log('KeyCode = ' + code); + //console.log('KeyCode', evt.keyCode); break; } } -function init(filename) { - if (!window.FileReader) { - alert("Sorry, kthoom will not work with your browser because it does not support the File API. Please try kthoom with Chrome 12+ or Firefox 7+"); +/*function ImageLoadCallback() { + var jso = this.response; + // Unable to decompress file, or no response from server + if (jso === null) { + setImage("error"); } else { - var request = new XMLHttpRequest(); - request.open("GET", filename); - request.responseType = "arraybuffer"; - request.setRequestHeader("X-Test", "test1"); - request.setRequestHeader("X-Test", "test2"); - request.addEventListener("load", function(event) { - if (request.status >= 200 && request.status < 300) { - loadFromArrayBuffer(request.response); - } else { - console.warn(request.statusText, request.responseText); - } - }); - request.send(); - kthoom.initProgressMeter(); - document.body.className += /AppleWebKit/.test(navigator.userAgent) ? " webkit" : ""; - updateScale(true); - kthoom.loadSettings(); - $(document).keydown(keyHandler); + // IE 11 sometimes sees the response as a string + if (typeof jso !== "object") { + jso = JSON.parse(jso); + } + + if (jso.page !== jso.last) { + this.open("GET", this.fileid + "/" + (jso.page + 1)); + this.addEventListener("load", ImageLoadCallback); + this.send(); + } + + loadFromArrayBuffer(jso); + } +}*/ +function init(filename) { + var request = new XMLHttpRequest(); + request.open("GET", filename); + request.responseType = "arraybuffer"; + request.setRequestHeader("X-Test", "test1"); + request.setRequestHeader("X-Test", "test2"); + request.addEventListener("load", function(event) { + if (request.status >= 200 && request.status < 300) { + loadFromArrayBuffer(request.response); + } else { + console.warn(request.statusText, request.responseText); + } + }); + request.send(); + initProgressClick(); + document.body.className += /AppleWebKit/.test(navigator.userAgent) ? " webkit" : ""; + kthoom.loadSettings(); + updateScale(true); + + $(document).keydown(keyHandler); + + $(window).resize(function() { + updateScale(false); + }); + + // Open TOC menu + $("#slider").click(function() { + $("#sidebar").toggleClass("open"); + $("#main").toggleClass("closed"); + $(this).toggleClass("icon-menu icon-right"); + + // We need this in a timeout because if we call it during the CSS transition, IE11 shakes the page ¯\_(ツ)_/¯ + setTimeout(function(){ + // Focus on the TOC or the main content area, depending on which is open + $('#main:not(.closed) #mainContent, #sidebar.open #tocView').focus(); + scrollTocToActive(); + }, 500); + }); + + // Open Settings modal + $("#setting").click(function() { + $("#settings-modal").toggleClass("md-show"); + }); + + // On Settings input change + $("#settings input").on("change", function() { + // Get either the checked boolean or the assigned value + var value = this.type === "checkbox" ? this.checked : this.value; - $(window).resize(function() { - var f = (screen.width - innerWidth < 4 && screen.height - innerHeight < 4); - getElem("titlebar").className = f ? "main" : ""; - updateScale(); + // If it's purely numeric, parse it to an integer + value = /^\d+$/.test(value) ? parseInt(value) : value; + + settings[this.name] = value; + updatePage(); + updateScale(false); + }); + + // Close modal + $(".closer, .overlay").click(function() { + $(".md-show").removeClass("md-show"); + }); + + // TOC thumbnail pagination + $("#thumbnails").on("click", "a", function() { + currentImage = $(this).data("page") - 1; + updatePage(); + }); + + // Fullscreen mode + if (typeof screenfull !== "undefined") { + $("#fullscreen").click(function() { + screenfull.toggle($("#container")[0]); }); - $("#mainImage").click(function(evt) { - // Firefox does not support offsetX/Y so we have to manually calculate - // where the user clicked in the image. - var mainContentWidth = $("#mainContent").width(); - var mainContentHeight = $("#mainContent").height(); - var comicWidth = evt.target.clientWidth; - var comicHeight = evt.target.clientHeight; - var offsetX = (mainContentWidth - comicWidth) / 2; - var offsetY = (mainContentHeight - comicHeight) / 2; - var clickX = !!evt.offsetX ? evt.offsetX : (evt.clientX - offsetX); - var clickY = !!evt.offsetY ? evt.offsetY : (evt.clientY - offsetY); - - // Determine if the user clicked/tapped the left side or the - // right side of the page. - var clickedPrev = false; - switch (kthoom.rotateTimes) { + if (screenfull.raw) { + var $button = $("#fullscreen"); + document.addEventListener(screenfull.raw.fullscreenchange, function() { + screenfull.isFullscreen + ? $button.addClass("icon-resize-small").removeClass("icon-resize-full") + : $button.addClass("icon-resize-full").removeClass("icon-resize-small"); + }); + } + } + + // Focus the scrollable area so that keyboard scrolling work as expected + $('#mainContent').focus(); + + $("#mainImage").click(function(evt) { + // Firefox does not support offsetX/Y so we have to manually calculate + // where the user clicked in the image. + var mainContentWidth = $("#mainContent").width(); + var mainContentHeight = $("#mainContent").height(); + var comicWidth = evt.target.clientWidth; + var comicHeight = evt.target.clientHeight; + var offsetX = (mainContentWidth - comicWidth) / 2; + var offsetY = (mainContentHeight - comicHeight) / 2; + var clickX = evt.offsetX ? evt.offsetX : (evt.clientX - offsetX); + var clickY = evt.offsetY ? evt.offsetY : (evt.clientY - offsetY); + + // Determine if the user clicked/tapped the left side or the + // right side of the page. + var clickedPrev = false; + switch (settings.rotateTimes) { case 0: - clickedPrev = clickX < (comicWidth / 2); - break; + clickedPrev = clickX < (comicWidth / 2); + break; case 1: - clickedPrev = clickY < (comicHeight / 2); - break; + clickedPrev = clickY < (comicHeight / 2); + break; case 2: - clickedPrev = clickX > (comicWidth / 2); - break; + clickedPrev = clickX > (comicWidth / 2); + break; case 3: - clickedPrev = clickY > (comicHeight / 2); - break; - } - if (clickedPrev) { - showPrevPage(); - } else { - showNextPage(); - } - }); - } + clickedPrev = clickY > (comicHeight / 2); + break; + } + if (clickedPrev) { + showPrevPage(); + } else { + showNextPage(); + } + }); } + diff --git a/cps/static/js/untar.js b/cps/static/js/untar.js index fb8cdb8d..ae6a206c 100644 --- a/cps/static/js/untar.js +++ b/cps/static/js/untar.js @@ -1,54 +1,76 @@ /** - * untar.js - * - * Copyright(c) 2011 Google Inc. - * - * Reference Documentation: - * - * TAR format: http://www.gnu.org/software/automake/manual/tar/Standard.html - */ -/* global bitjs, importScripts, Uint8Array */ +* untar.js +* +* Licensed under the MIT License +* +* Copyright(c) 2011 Google Inc. +* +* Reference Documentation: +* +* TAR format: http://www.gnu.org/software/automake/manual/tar/Standard.html +*/ // This file expects to be invoked as a Worker (see onmessage below). -importScripts("io.js"); -importScripts("archive.js"); +importScripts('bytestream.js'); +importScripts('archive.js'); + +const UnarchiveState = { + NOT_STARTED: 0, + UNARCHIVING: 1, + WAITING: 2, + FINISHED: 3, +}; + +// State - consider putting these into a class. +let unarchiveState = UnarchiveState.NOT_STARTED; +let bytestream = null; +let allLocalFiles = null; +let logToConsole = false; // Progress variables. -var currentFilename = ""; -var currentFileNumber = 0; -var currentBytesUnarchivedInFile = 0; -var currentBytesUnarchived = 0; -var totalUncompressedBytesInArchive = 0; -var totalFilesInArchive = 0; +let currentFilename = ""; +let currentFileNumber = 0; +let currentBytesUnarchivedInFile = 0; +let currentBytesUnarchived = 0; +let totalUncompressedBytesInArchive = 0; +let totalFilesInArchive = 0; // Helper functions. -var info = function(str) { - postMessage(new bitjs.archive.UnarchiveInfoEvent(str)); +const info = function(str) { + postMessage(new bitjs.archive.UnarchiveInfoEvent(str)); }; -var err = function(str) { - postMessage(new bitjs.archive.UnarchiveErrorEvent(str)); -}; -var postProgress = function() { - postMessage(new bitjs.archive.UnarchiveProgressEvent( - currentFilename, - currentFileNumber, - currentBytesUnarchivedInFile, - currentBytesUnarchived, - totalUncompressedBytesInArchive, - totalFilesInArchive)); +const err = function(str) { + postMessage(new bitjs.archive.UnarchiveErrorEvent(str)); }; // Removes all characters from the first zero-byte in the string onwards. var readCleanString = function(bstr, numBytes) { - var str = bstr.readString(numBytes); - var zIndex = str.indexOf(String.fromCharCode(0)); - return zIndex !== -1 ? str.substr(0, zIndex) : str; + var str = bstr.readString(numBytes); + var zIndex = str.indexOf(String.fromCharCode(0)); + return zIndex != -1 ? str.substr(0, zIndex) : str; }; -// takes a ByteStream and parses out the local file information -var TarLocalFile = function(bstream) { + +const postProgress = function() { + postMessage(new bitjs.archive.UnarchiveProgressEvent( + currentFilename, + currentFileNumber, + currentBytesUnarchivedInFile, + currentBytesUnarchived, + totalUncompressedBytesInArchive, + totalFilesInArchive, + bytestream.getNumBytesRead(), + )); +}; + + +class TarLocalFile { + // takes a ByteStream and parses out the local file information + constructor(bstream) { this.isValid = false; + let bytesRead = 0; + // Read in the header block this.name = readCleanString(bstream, 100); this.mode = readCleanString(bstream, 8); @@ -60,23 +82,25 @@ var TarLocalFile = function(bstream) { this.typeflag = readCleanString(bstream, 1); this.linkname = readCleanString(bstream, 100); this.maybeMagic = readCleanString(bstream, 6); - - if (this.maybeMagic === "ustar") { - this.version = readCleanString(bstream, 2); - this.uname = readCleanString(bstream, 32); - this.gname = readCleanString(bstream, 32); - this.devmajor = readCleanString(bstream, 8); - this.devminor = readCleanString(bstream, 8); - this.prefix = readCleanString(bstream, 155); - - if (this.prefix.length) { - this.name = this.prefix + this.name; - } - bstream.readBytes(12); // 512 - 500 + + if (this.maybeMagic == "ustar") { + this.version = readCleanString(bstream, 2); + this.uname = readCleanString(bstream, 32); + this.gname = readCleanString(bstream, 32); + this.devmajor = readCleanString(bstream, 8); + this.devminor = readCleanString(bstream, 8); + this.prefix = readCleanString(bstream, 155); + + if (this.prefix.length) { + this.name = this.prefix + this.name; + } + bstream.readBytes(12); // 512 - 500in } else { - bstream.readBytes(255); // 512 - 257 + bstream.readBytes(255); // 512 - 257 } + bytesRead += 512; + // Done header, now rest of blocks are the file contents. this.filename = this.name; this.fileData = null; @@ -86,84 +110,100 @@ var TarLocalFile = function(bstream) { info(" typeflag = " + this.typeflag); // A regular file. - if (this.typeflag === 0) { - info(" This is a regular file."); - // var sizeInBytes = parseInt(this.size); - this.fileData = new Uint8Array(bstream.bytes.buffer, bstream.ptr, this.size); - if (this.name.length > 0 && this.size > 0 && this.fileData && this.fileData.buffer) { - this.isValid = true; - } - - bstream.readBytes(this.size); - - // Round up to 512-byte blocks. - var remaining = 512 - this.size % 512; - if (remaining > 0 && remaining < 512) { - bstream.readBytes(remaining); - } - } else if (this.typeflag === 5) { - info(" This is a directory."); + if (this.typeflag == 0) { + info(" This is a regular file."); + const sizeInBytes = parseInt(this.size); + this.fileData = new Uint8Array(bstream.readBytes(sizeInBytes)); + bytesRead += sizeInBytes; + if (this.name.length > 0 && this.size > 0 && this.fileData && this.fileData.buffer) { + this.isValid = true; + } + + // Round up to 512-byte blocks. + const remaining = 512 - bytesRead % 512; + if (remaining > 0 && remaining < 512) { + bstream.readBytes(remaining); + } + } else if (this.typeflag == 5) { + info(" This is a directory.") } + } +} + +const untar = function() { + let bstream = bytestream.tee(); + + // While we don't encounter an empty block, keep making TarLocalFiles. + while (bstream.peekNumber(4) != 0) { + const oneLocalFile = new TarLocalFile(bstream); + if (oneLocalFile && oneLocalFile.isValid) { + // If we make it to this point and haven't thrown an error, we have successfully + // read in the data for a local file, so we can update the actual bytestream. + bytestream = bstream.tee(); + + allLocalFiles.push(oneLocalFile); + totalUncompressedBytesInArchive += oneLocalFile.size; + + // update progress + currentFilename = oneLocalFile.filename; + currentFileNumber = totalFilesInArchive++; + currentBytesUnarchivedInFile = oneLocalFile.size; + currentBytesUnarchived += oneLocalFile.size; + postMessage(new bitjs.archive.UnarchiveExtractEvent(oneLocalFile)); + postProgress(); + } + } + totalFilesInArchive = allLocalFiles.length; + + postProgress(); + + bytestream = bstream.tee(); }; -// Takes an ArrayBuffer of a tar file in -// returns null on error -// returns an array of DecompressedFile objects on success -var untar = function(arrayBuffer) { +// event.data.file has the first ArrayBuffer. +// event.data.bytes has all subsequent ArrayBuffers. +onmessage = function(event) { + const bytes = event.data.file || event.data.bytes; + logToConsole = !!event.data.logToConsole; + + // This is the very first time we have been called. Initialize the bytestream. + if (!bytestream) { + bytestream = new bitjs.io.ByteStream(bytes); + } else { + bytestream.push(bytes); + } + + if (unarchiveState === UnarchiveState.NOT_STARTED) { currentFilename = ""; currentFileNumber = 0; currentBytesUnarchivedInFile = 0; currentBytesUnarchived = 0; totalUncompressedBytesInArchive = 0; totalFilesInArchive = 0; - + allLocalFiles = []; + postMessage(new bitjs.archive.UnarchiveStartEvent()); - var bstream = new bitjs.io.ByteStream(arrayBuffer); - var localFiles = []; - - // While we don't encounter an empty block, keep making TarLocalFiles. - while (bstream.peekNumber(4) !== 0) { - var oneLocalFile = new TarLocalFile(bstream); - if (oneLocalFile && oneLocalFile.isValid) { - localFiles.push(oneLocalFile); - totalUncompressedBytesInArchive += oneLocalFile.size; - } - } - totalFilesInArchive = localFiles.length; - - // got all local files, now sort them - localFiles.sort(function(a, b) { - var aname = a.filename.toLowerCase(); - var bname = b.filename.toLowerCase(); - return aname > bname ? 1 : -1; - }); - - // report # files and total length - if (localFiles.length > 0) { - postProgress(); - } - // now do the shipping of each file - for (var i = 0; i < localFiles.length; ++i) { - var localfile = localFiles[i]; - info("Sending file '" + localfile.filename + "' up"); - - // update progress - currentFilename = localfile.filename; - currentFileNumber = i; - currentBytesUnarchivedInFile = localfile.size; - currentBytesUnarchived += localfile.size; - postMessage(new bitjs.archive.UnarchiveExtractEvent(localfile)); - postProgress(); - } + unarchiveState = UnarchiveState.UNARCHIVING; postProgress(); - - postMessage(new bitjs.archive.UnarchiveFinishEvent()); -}; - -// event.data.file has the ArrayBuffer. -onmessage = function(event) { - var ab = event.data.file; - untar(ab); + } + + if (unarchiveState === UnarchiveState.UNARCHIVING || + unarchiveState === UnarchiveState.WAITING) { + try { + untar(); + unarchiveState = UnarchiveState.FINISHED; + postMessage(new bitjs.archive.UnarchiveFinishEvent()); + } catch (e) { + if (typeof e === 'string' && e.startsWith('Error! Overflowed')) { + // Overrun the buffer. + unarchiveState = UnarchiveState.WAITING; + } else { + console.error('Found an error while untarring'); + console.dir(e); + throw e; + } + } + } }; diff --git a/cps/static/js/unzip.js b/cps/static/js/unzip.js index 8a76ca7b..2b5be91a 100644 --- a/cps/static/js/unzip.js +++ b/cps/static/js/unzip.js @@ -9,7 +9,7 @@ * ZIP format: http://www.pkware.com/documents/casestudies/APPNOTE.TXT * DEFLATE format: http://tools.ietf.org/html/rfc1951 */ -/* global bitjs, importScripts, Uint8Array */ +/* global bitjs */ // This file expects to be invoked as a Worker (see onmessage below). importScripts("io.js"); @@ -44,12 +44,12 @@ var zLocalFileHeaderSignature = 0x04034b50; var zArchiveExtraDataSignature = 0x08064b50; var zCentralFileHeaderSignature = 0x02014b50; var zDigitalSignatureSignature = 0x05054b50; -//var zEndOfCentralDirSignature = 0x06064b50; -//var zEndOfCentralDirLocatorSignature = 0x07064b50; +var zEndOfCentralDirSignature = 0x06064b50; +var zEndOfCentralDirLocatorSignature = 0x07064b50; // takes a ByteStream and parses out the local file information var ZipLocalFile = function(bstream) { - if (typeof bstream !== typeof {} || !bstream.readNumber || typeof bstream.readNumber !== typeof function() {} ) { + if (typeof bstream != typeof {} || !bstream.readNumber || typeof bstream.readNumber != typeof function(){}) { return null; } @@ -100,7 +100,7 @@ var ZipLocalFile = function(bstream) { // "This descriptor exists only if bit 3 of the general purpose bit flag is set" // But how do you figure out how big the file data is if you don't know the compressedSize // from the header?!? - if ((this.generalPurpose & bitjs.BIT[3]) !== 0) { + if ((this.generalPurpose & bitjs.BIT[3]) != 0) { this.crc32 = bstream.readNumber(4); this.compressedSize = bstream.readNumber(4); this.uncompressedSize = bstream.readNumber(4); @@ -111,15 +111,17 @@ var ZipLocalFile = function(bstream) { ZipLocalFile.prototype.unzip = function() { // Zip Version 1.0, no compression (store only) - if (this.compressionMethod === 0 ) { - info("ZIP v" + this.version + ", store only: " + this.filename + " (" + this.compressedSize + " bytes)"); + if (this.compressionMethod == 0 ) { + info("ZIP v"+this.version+", store only: " + this.filename + " (" + this.compressedSize + " bytes)"); currentBytesUnarchivedInFile = this.compressedSize; currentBytesUnarchived += this.compressedSize; - } else if (this.compressionMethod === 8) { - // version == 20, compression method == 8 (DEFLATE) + } + // version == 20, compression method == 8 (DEFLATE) + else if (this.compressionMethod == 8) { info("ZIP v2.0, DEFLATE: " + this.filename + " (" + this.compressedSize + " bytes)"); this.fileData = inflate(this.fileData, this.uncompressedSize); - } else { + } + else { err("UNSUPPORTED VERSION/FORMAT: ZIP v" + this.version + ", compression method=" + this.compressionMethod + ": " + this.filename + " (" + this.compressedSize + " bytes)"); this.fileData = null; } @@ -142,10 +144,10 @@ var unzip = function(arrayBuffer) { var bstream = new bitjs.io.ByteStream(arrayBuffer); // detect local file header signature or return null - if (bstream.peekNumber(4) === zLocalFileHeaderSignature) { + if (bstream.peekNumber(4) == zLocalFileHeaderSignature) { var localFiles = []; // loop until we don't see any more local files - while (bstream.peekNumber(4) === zLocalFileHeaderSignature) { + while (bstream.peekNumber(4) == zLocalFileHeaderSignature) { var oneLocalFile = new ZipLocalFile(bstream); // this should strip out directories/folders if (oneLocalFile && oneLocalFile.uncompressedSize > 0 && oneLocalFile.fileData) { @@ -156,14 +158,14 @@ var unzip = function(arrayBuffer) { totalFilesInArchive = localFiles.length; // got all local files, now sort them - localFiles.sort(function(a, b) { + localFiles.sort(function(a,b) { var aname = a.filename.toLowerCase(); var bname = b.filename.toLowerCase(); return aname > bname ? 1 : -1; }); // archive extra data record - if (bstream.peekNumber(4) === zArchiveExtraDataSignature) { + if (bstream.peekNumber(4) == zArchiveExtraDataSignature) { info(" Found an Archive Extra Data Signature"); // skipping this record for now @@ -174,11 +176,11 @@ var unzip = function(arrayBuffer) { // central directory structure // TODO: handle the rest of the structures (Zip64 stuff) - if (bstream.peekNumber(4) === zCentralFileHeaderSignature) { + if (bstream.peekNumber(4) == zCentralFileHeaderSignature) { info(" Found a Central File Header"); // read all file headers - while (bstream.peekNumber(4) === zCentralFileHeaderSignature) { + while (bstream.peekNumber(4) == zCentralFileHeaderSignature) { bstream.readNumber(4); // signature bstream.readNumber(2); // version made by bstream.readNumber(2); // version needed to extract @@ -204,7 +206,7 @@ var unzip = function(arrayBuffer) { } // digital signature - if (bstream.peekNumber(4) === zDigitalSignatureSignature) { + if (bstream.peekNumber(4) == zDigitalSignatureSignature) { info(" Found a Digital Signature"); bstream.readNumber(4); @@ -229,68 +231,66 @@ var unzip = function(arrayBuffer) { // actually do the unzipping localfile.unzip(); - if (localfile.fileData !== null) { + if (localfile.fileData != null) { postMessage(new bitjs.archive.UnarchiveExtractEvent(localfile)); postProgress(); } } postProgress(); postMessage(new bitjs.archive.UnarchiveFinishEvent()); - } -}; + } +} // returns a table of Huffman codes // each entry's index is its code and its value is a JavaScript object // containing {length: 6, symbol: X} function getHuffmanCodes(bitLengths) { - var len; // ensure bitLengths is an array containing at least one element - if (typeof bitLengths !== typeof [] || bitLengths.length < 1) { + if (typeof bitLengths != typeof [] || bitLengths.length < 1) { err("Error! getHuffmanCodes() called with an invalid array"); return null; } // Reference: http://tools.ietf.org/html/rfc1951#page-8 var numLengths = bitLengths.length, - blCount = [], + bl_count = [], MAX_BITS = 1; - // Step 1: count up how many codes of each length we have for (var i = 0; i < numLengths; ++i) { - len = bitLengths[i]; + var length = bitLengths[i]; // test to ensure each bit length is a positive, non-zero number - if (typeof len !== typeof 1 || len < 0) { - err("bitLengths contained an invalid number in getHuffmanCodes(): " + len + " of type " + (typeof len)); + if (typeof length != typeof 1 || length < 0) { + err("bitLengths contained an invalid number in getHuffmanCodes(): " + length + " of type " + (typeof length)); return null; } // increment the appropriate bitlength count - if (blCount[len] === undefined) blCount[len] = 0; + if (bl_count[length] == undefined) bl_count[length] = 0; // a length of zero means this symbol is not participating in the huffman coding - if (len > 0) blCount[len]++; + if (length > 0) bl_count[length]++; - if (len > MAX_BITS) MAX_BITS = len; + if (length > MAX_BITS) MAX_BITS = length; } // Step 2: Find the numerical value of the smallest code for each code length - var nextCode = [], + var next_code = [], code = 0; for (var bits = 1; bits <= MAX_BITS; ++bits) { - len = bits - 1; + var length = bits-1; // ensure undefined lengths are zero - if (blCount[len] === undefined) blCount[len] = 0; - code = (code + blCount[bits - 1]) << 1; - nextCode[bits] = code; + if (bl_count[length] == undefined) bl_count[length] = 0; + code = (code + bl_count[bits-1]) << 1; + next_code[bits] = code; } // Step 3: Assign numerical values to all codes var table = {}, tableLength = 0; for (var n = 0; n < numLengths; ++n) { - len = bitLengths[n]; - if (len !== 0) { - table[nextCode[len]] = { length: len, symbol: n }; //, bitstring: binaryValueToString(nextCode[len],len) }; + var len = bitLengths[n]; + if (len != 0) { + table[next_code[len]] = { length: len, symbol: n }; //, bitstring: binaryValueToString(next_code[len],len) }; tableLength++; - nextCode[len]++; + next_code[len]++; } } table.maxLength = tableLength; @@ -318,11 +318,10 @@ function getHuffmanCodes(bitLengths) { var fixedHCtoLiteral = null; var fixedHCtoDistance = null; function getFixedLiteralTable() { - var i; // create once if (!fixedHCtoLiteral) { var bitlengths = new Array(288); - for (i = 0; i <= 143; ++i) bitlengths[i] = 8; + for (var i = 0; i <= 143; ++i) bitlengths[i] = 8; for (i = 144; i <= 255; ++i) bitlengths[i] = 9; for (i = 256; i <= 279; ++i) bitlengths[i] = 7; for (i = 280; i <= 287; ++i) bitlengths[i] = 8; @@ -336,9 +335,7 @@ function getFixedDistanceTable() { // create once if (!fixedHCtoDistance) { var bitlengths = new Array(32); - for (var i = 0; i < 32; ++i) { - bitlengths[i] = 5; - } + for (var i = 0; i < 32; ++i) { bitlengths[i] = 5; } // get huffman code table fixedHCtoDistance = getHuffmanCodes(bitlengths); @@ -350,17 +347,17 @@ function getFixedDistanceTable() { // then return that symbol function decodeSymbol(bstream, hcTable) { var code = 0, len = 0; - // var match = false; + var match = false; // loop until we match for (;;) { // read in next bit var bit = bstream.readBits(1); - code = (code << 1) | bit; + code = (code<<1) | bit; ++len; // check against Huffman Code table and break if found - if (hcTable.hasOwnProperty(code) && hcTable[code].length === len) { + if (hcTable.hasOwnProperty(code) && hcTable[code].length == len) { break; } @@ -375,31 +372,31 @@ function decodeSymbol(bstream, hcTable) { var CodeLengthCodeOrder = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]; -/* - Extra Extra Extra -Code Bits Length(s) Code Bits Lengths Code Bits Length(s) ----- ---- ------ ---- ---- ------- ---- ---- ------- - 257 0 3 267 1 15,16 277 4 67-82 - 258 0 4 268 1 17,18 278 4 83-98 - 259 0 5 269 2 19-22 279 4 99-114 - 260 0 6 270 2 23-26 280 4 115-130 - 261 0 7 271 2 27-30 281 5 131-162 - 262 0 8 272 2 31-34 282 5 163-194 - 263 0 9 273 3 35-42 283 5 195-226 - 264 0 10 274 3 43-50 284 5 227-257 - 265 1 11,12 275 3 51-58 285 0 258 - 266 1 13,14 276 3 59-66 -*/ + /* + Extra Extra Extra + Code Bits Length(s) Code Bits Lengths Code Bits Length(s) + ---- ---- ------ ---- ---- ------- ---- ---- ------- + 257 0 3 267 1 15,16 277 4 67-82 + 258 0 4 268 1 17,18 278 4 83-98 + 259 0 5 269 2 19-22 279 4 99-114 + 260 0 6 270 2 23-26 280 4 115-130 + 261 0 7 271 2 27-30 281 5 131-162 + 262 0 8 272 2 31-34 282 5 163-194 + 263 0 9 273 3 35-42 283 5 195-226 + 264 0 10 274 3 43-50 284 5 227-257 + 265 1 11,12 275 3 51-58 285 0 258 + 266 1 13,14 276 3 59-66 + */ var LengthLookupTable = [ - [0, 3], [0, 4], [0, 5], [0, 6], - [0, 7], [0, 8], [0, 9], [0, 10], - [1, 11], [1, 13], [1, 15], [1, 17], - [2, 19], [2, 23], [2, 27], [2, 31], - [3, 35], [3, 43], [3, 51], [3, 59], - [4, 67], [4, 83], [4, 99], [4, 115], - [5, 131], [5, 163], [5, 195], [5, 227], - [0, 258] + [0,3], [0,4], [0,5], [0,6], + [0,7], [0,8], [0,9], [0,10], + [1,11], [1,13], [1,15], [1,17], + [2,19], [2,23], [2,27], [2,31], + [3,35], [3,43], [3,51], [3,59], + [4,67], [4,83], [4,99], [4,115], + [5,131], [5,163], [5,195], [5,227], + [0,258] ]; /* Extra Extra Extra @@ -417,20 +414,20 @@ var LengthLookupTable = [ 9 3 25-32 19 8 769-1024 29 13 24577-32768 */ var DistLookupTable = [ - [0, 1], [0, 2], [0, 3], [0, 4], - [1, 5], [1, 7], - [2, 9], [2, 13], - [3, 17], [3, 25], - [4, 33], [4, 49], - [5, 65], [5, 97], - [6, 129], [6, 193], - [7, 257], [7, 385], - [8, 513], [8, 769], - [9, 1025], [9, 1537], - [10, 2049], [10, 3073], - [11, 4097], [11, 6145], - [12, 8193], [12, 12289], - [13, 16385], [13, 24577] + [0,1], [0,2], [0,3], [0,4], + [1,5], [1,7], + [2,9], [2,13], + [3,17], [3,25], + [4,33], [4,49], + [5,65], [5,97], + [6,129], [6,193], + [7,257], [7,385], + [8,513], [8,769], + [9,1025], [9,1537], + [10,2049], [10,3073], + [11,4097], [11,6145], + [12,8193], [12,12289], + [13,16385], [13,24577] ]; function inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer) { @@ -449,10 +446,10 @@ function inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer) { stream, and copy length bytes from this position to the output stream. */ - var blockSize = 0; + var numSymbols = 0, blockSize = 0; for (;;) { var symbol = decodeSymbol(bstream, hcLiteralTable); - // ++numSymbols; + ++numSymbols; if (symbol < 256) { // copy literal byte to output buffer.insertByte(symbol); @@ -460,11 +457,11 @@ function inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer) { } else { // end of block reached - if (symbol === 256) { + if (symbol == 256) { break; } else { - var lengthLookup = LengthLookupTable[symbol - 257], + var lengthLookup = LengthLookupTable[symbol-257], length = lengthLookup[1] + bstream.readBits(lengthLookup[0]), distLookup = DistLookupTable[decodeSymbol(bstream, hcDistanceTable)], distance = distLookup[1] + bstream.readBits(distLookup[0]); @@ -482,13 +479,13 @@ function inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer) { // loop for each character var ch = buffer.ptr - distance; blockSize += length; - if (length > distance) { - var data = buffer.data; - while (length--) { - buffer.insertByte(data[ch++]); - } + if(length > distance) { + var data = buffer.data; + while (length--) { + buffer.insertByte(data[ch++]); + } } else { - buffer.insertBytes(buffer.data.subarray(ch, ch + length)); + buffer.insertBytes(buffer.data.subarray(ch, ch + length)) } } // length-distance pair @@ -507,38 +504,37 @@ function inflate(compressedData, numDecompressedBytes) { compressedData.byteOffset, compressedData.byteLength); var buffer = new bitjs.io.ByteBuffer(numDecompressedBytes); - //var numBlocks = 0; - var blockSize = 0; - var bFinal; + var numBlocks = 0, blockSize = 0; + // block format: http://tools.ietf.org/html/rfc1951#page-9 do { - bFinal = bstream.readBits(1); - var bType = bstream.readBits(2); + var bFinal = bstream.readBits(1), + bType = bstream.readBits(2); blockSize = 0; - // ++numBlocks; + ++numBlocks; // no compression - if (bType === 0) { + if (bType == 0) { // skip remaining bits in this byte - while (bstream.bitPtr !== 0) bstream.readBits(1); - var len = bstream.readBits(16); - // nlen = bstream.readBits(16); + while (bstream.bitPtr != 0) bstream.readBits(1); + var len = bstream.readBits(16), + nlen = bstream.readBits(16); // TODO: check if nlen is the ones-complement of len? - if (len > 0) buffer.insertBytes(bstream.readBytes(len)); + if(len > 0) buffer.insertBytes(bstream.readBytes(len)); blockSize = len; } // fixed Huffman codes - else if (bType === 1) { + else if(bType == 1) { blockSize = inflateBlockData(bstream, getFixedLiteralTable(), getFixedDistanceTable(), buffer); } // dynamic Huffman codes - else if (bType === 2) { + else if(bType == 2) { var numLiteralLengthCodes = bstream.readBits(5) + 257; var numDistanceCodes = bstream.readBits(5) + 1, numCodeLengthCodes = bstream.readBits(4) + 4; // populate the array of code length codes (first de-compaction) - var codeLengthsCodeLengths = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + var codeLengthsCodeLengths = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; for (var i = 0; i < numCodeLengthCodes; ++i) { codeLengthsCodeLengths[ CodeLengthCodeOrder[i] ] = bstream.readBits(3); } @@ -566,25 +562,24 @@ function inflate(compressedData, numDecompressedBytes) { var prevCodeLength = 0; while (literalCodeLengths.length < numLiteralLengthCodes + numDistanceCodes) { var symbol = decodeSymbol(bstream, codeLengthsCodes); - var repeat; if (symbol <= 15) { literalCodeLengths.push(symbol); prevCodeLength = symbol; } - else if (symbol === 16) { - repeat = bstream.readBits(2) + 3; + else if (symbol == 16) { + var repeat = bstream.readBits(2) + 3; while (repeat--) { literalCodeLengths.push(prevCodeLength); } } - else if (symbol === 17) { - repeat = bstream.readBits(3) + 3; + else if (symbol == 17) { + var repeat = bstream.readBits(3) + 3; while (repeat--) { literalCodeLengths.push(0); } } - else if (symbol === 18) { - repeat = bstream.readBits(7) + 11; + else if (symbol == 18) { + var repeat = bstream.readBits(7) + 11; while (repeat--) { literalCodeLengths.push(0); } @@ -610,7 +605,7 @@ function inflate(compressedData, numDecompressedBytes) { currentBytesUnarchived += blockSize; postProgress(); - } while (bFinal !== 1); + } while (bFinal != 1); // we are done reading blocks if the bFinal bit was set for this block // return the buffer data bytes diff --git a/cps/templates/config_edit.html b/cps/templates/config_edit.html index b332a45a..00f78176 100644 --- a/cps/templates/config_edit.html +++ b/cps/templates/config_edit.html @@ -170,7 +170,7 @@

    - {{_('E-Book converter')}} + {{_('External binaries')}}

    @@ -179,9 +179,9 @@
    -
    + {% if rarfile_support %} +
    + + +
    + {% endif %}
    diff --git a/cps/templates/languages.html b/cps/templates/languages.html index eba7af04..4c582d27 100644 --- a/cps/templates/languages.html +++ b/cps/templates/languages.html @@ -2,15 +2,15 @@ {% block body %}

    {{title}}

    -
    +
    {% for lang in languages %} {% if loop.index0 == (loop.length/2)|int and loop.length > 20 %}
    -
    +
    {% endif %}
    -
    {{lang_counter[loop.index0].bookcount}}
    - +
    {{lang_counter[loop.index0].bookcount}}
    +
    {% endfor %}
    diff --git a/cps/templates/readcbr.html b/cps/templates/readcbr.html index ca14781c..bdc2a3ea 100644 --- a/cps/templates/readcbr.html +++ b/cps/templates/readcbr.html @@ -1,44 +1,157 @@ - - - - Comic Reader - - - - - - - + + + + Comic Reader + + + - + + + + + + + - - - -
    -
    -
    - + + +
    + + diff --git a/cps/ub.py b/cps/ub.py index d2d6a25d..f1b19d02 100644 --- a/cps/ub.py +++ b/cps/ub.py @@ -311,6 +311,7 @@ class Settings(Base): config_ebookconverter = Column(Integer, default=0) config_converterpath = Column(String) config_calibre = Column(String) + config_rarfile_location = Column(String) def __repr__(self): pass @@ -383,6 +384,7 @@ class Config: self.config_mature_content_tags = u'' if data.config_logfile: self.config_logfile = data.config_logfile + self.config_rarfile_location = data.config_rarfile_location @property def get_main_dir(self): @@ -566,6 +568,13 @@ def migrate_Database(): conn = engine.connect() conn.execute("ALTER TABLE book_shelf_link ADD column 'order' INTEGER DEFAULT 1") session.commit() + try: + session.query(exists().where(Settings.config_rarfile_location)).scalar() + session.commit() + except exc.OperationalError: # Database is not compatible, some rows are missing + conn = engine.connect() + conn.execute("ALTER TABLE Settings ADD column `config_rarfile_location` String DEFAULT ''") + session.commit() try: create = False session.query(exists().where(User.sidebar_view)).scalar() diff --git a/cps/web.py b/cps/web.py index d7b95cd5..65dce315 100644 --- a/cps/web.py +++ b/cps/web.py @@ -22,6 +22,18 @@ try: except ImportError: pass # We're not using Python 3 +try: + import rarfile + rar_support=True +except ImportError: + rar_support=False + +try: + from natsort import natsorted as sort +except ImportError: + sort=sorted # Just use regular sort then + # may cause issues with badly named pages in cbz/cbr files + import mimetypes import logging from logging.handlers import RotatingFileHandler @@ -47,6 +59,8 @@ from flask_babel import Babel from flask_babel import gettext as _ import requests +# import zipfile +# import tarfile from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.datastructures import Headers from babel import Locale as LC @@ -83,9 +97,10 @@ try: except ImportError: from flask_login.__about__ import __version__ as flask_loginVersion +# import codecs import time import server -import random +# import random current_milli_time = lambda: int(round(time.time() * 1000)) @@ -95,7 +110,8 @@ gdrive_watch_callback_token = 'target=calibreweb-watch_files' ALLOWED_EXTENSIONS = {'txt', 'pdf', 'epub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'cbt', 'djvu', 'prc', 'doc', 'docx', 'fb2'} - +# READER_EXTENSIONS = set(['txt', 'pdf', 'epub', 'zip', 'cbz', 'tar', 'cbt'] + (['rar','cbr'] if rar_support else [])) +# READER_EXTENSIONS = set(['txt', 'pdf', 'epub', 'zip', 'cbz', 'tar', 'cbt', 'rar', 'cbr']) def md5(fname): hash_md5 = hashlib.md5() @@ -403,6 +419,13 @@ def yesno(value, yes, no): return yes if value else no +'''@app.template_filter('canread') +def canread(ext): + if isinstance(ext, db.Data): + ext = ext.format + return ext.lower() in READER_EXTENSIONS''' + + def admin_required(f): """ Checks if current_user.role == 1 @@ -961,7 +984,57 @@ def list_domain(): response.headers["Content-Type"] = "application/json; charset=utf-8" return response - +''' +@app.route("/ajax/getcomic///") +@login_required +def get_comic_book(book_id, book_format, page): + book = db.session.query(db.Books).filter(db.Books.id == book_id).first() + if not book: + return "", 204 + else: + for bookformat in book.data: + if bookformat.format.lower() == book_format.lower(): + cbr_file = os.path.join(config.config_calibre_dir, book.path, bookformat.name) + "." + book_format + if book_format in ("cbr", "rar"): + if rar_support == True: + rarfile.UNRAR_TOOL = config.config_rarfile_location + try: + rf = rarfile.RarFile(cbr_file) + names = sort(rf.namelist()) + extract = lambda page: rf.read(names[page]) + except: + # rarfile not valid + app.logger.error('Unrar binary not found, or unable to decompress file ' + cbr_file) + return "", 204 + else: + app.logger.info('Unrar is not supported please install python rarfile extension') + # no support means return nothing + return "", 204 + elif book_format in ("cbz", "zip"): + zf = zipfile.ZipFile(cbr_file) + names=sort(zf.namelist()) + extract = lambda page: zf.read(names[page]) + elif book_format in ("cbt", "tar"): + tf = tarfile.TarFile(cbr_file) + names=sort(tf.getnames()) + extract = lambda page: tf.extractfile(names[page]).read() + else: + app.logger.error('unsupported comic format') + return "", 204 + + if sys.version_info.major >= 3: + b64 = codecs.encode(extract(page), 'base64').decode() + else: + b64 = extract(page).encode('base64') + ext = names[page].rpartition('.')[-1] + if ext not in ('png', 'gif', 'jpg', 'jpeg'): + ext = 'png' + extractedfile="data:image/" + ext + ";base64," + b64 + fileData={"name": names[page], "page":page, "last":len(names)-1, "content": extractedfile} + return make_response(json.dumps(fileData)) + return "", 204 +''' + @app.route("/get_authors_json", methods=['GET', 'POST']) @login_required_if_no_ano def get_authors_json(): @@ -1379,7 +1452,6 @@ def toggle_read(book_id): except KeyError: app.logger.error( u"Custom Column No.%d is not exisiting in calibre database" % config.config_read_column) - return "" @app.route("/book/") @@ -1427,6 +1499,8 @@ def show_book(book_id): else: have_read = None + entries.tags = sort(entries.tags, key = lambda tag: tag.name) + return render_title_template('detail.html', entry=entries, cc=cc, is_xhr=request.is_xhr, title=entries.title, books_shelfs=book_in_shelfs, have_read=have_read, page="book") @@ -1454,6 +1528,7 @@ def bookmark(book_id, book_format): ub.session.commit() return "", 201 + @app.route("/tasks") @login_required def get_tasks_status(): @@ -1510,7 +1585,6 @@ def stats(): versions.update(converter.versioncheck()) versions.update(server.Server.getNameVersion()) versions['Python'] = sys.version - return render_title_template('stats.html', bookcounter=counter, authorcounter=authors, versions=versions, categorycounter=categorys, seriecounter=series, title=_(u"Statistics"), page="stat") @@ -2011,6 +2085,15 @@ def read_book(book_id, book_format): # copyfile(cbr_file, tmp_file) return render_title_template('readcbr.html', comicfile=all_name, title=_(u"Read a Book"), extension=fileext) + '''if rar_support == True: + extensionList = ["cbr","cbt","cbz"] + else: + extensionList = ["cbt","cbz"] + for fileext in extensionList: + if book_format.lower() == fileext: + return render_title_template('readcbr.html', comicfile=book_id, extension=fileext, title=_(u"Read a Book"), book=book) + flash(_(u"Error opening eBook. File does not exist or file is not accessible."), category="error") + return redirect(url_for("index"))''' @app.route("/download//") @@ -2823,7 +2906,6 @@ def configuration_helper(origin): content.config_goodreads_api_key = to_save["config_goodreads_api_key"] if "config_goodreads_api_secret" in to_save: content.config_goodreads_api_secret = to_save["config_goodreads_api_secret"] - if "config_log_level" in to_save: content.config_log_level = int(to_save["config_log_level"]) if content.config_logfile != to_save["config_logfile"]: @@ -2841,6 +2923,17 @@ def configuration_helper(origin): else: content.config_logfile = to_save["config_logfile"] reboot_required = True + + # Rarfile Content configuration + if "config_rarfile_location" in to_save: + check = helper.check_unrar(to_save["config_rarfile_location"].strip()) + if not check[0] : + content.config_rarfile_location = to_save["config_rarfile_location"].strip() + else: + flash(check[1], category="error") + return render_title_template("config_edit.html", content=config, origin=origin, gdrive=gdriveutils.gdrive_support, + goodreads=goodreads_support, rarfile_support=rar_support, + title=_(u"Basic Configuration")) try: if content.config_use_google_drive and is_gdrive_ready() and not os.path.exists(config.config_calibre_dir + "/metadata.db"): gdriveutils.downloadFile(None, "metadata.db", config.config_calibre_dir + "/metadata.db") @@ -2856,14 +2949,14 @@ def configuration_helper(origin): except Exception as e: flash(e, category="error") return render_title_template("config_edit.html", content=config, origin=origin, gdrive=gdriveutils.gdrive_support, - gdriveError=gdriveError, goodreads=goodreads_support, + gdriveError=gdriveError, goodreads=goodreads_support, rarfile_support=rar_support, title=_(u"Basic Configuration"), page="config") if db_change: reload(db) if not db.setup_db(): flash(_(u'DB location is not valid, please enter correct path'), category="error") return render_title_template("config_edit.html", content=config, origin=origin, gdrive=gdriveutils.gdrive_support, - gdriveError=gdriveError, goodreads=goodreads_support, + gdriveError=gdriveError, goodreads=goodreads_support, rarfile_support=rar_support, title=_(u"Basic Configuration"), page="config") if reboot_required: ub.session.close() @@ -2880,7 +2973,7 @@ def configuration_helper(origin): gdrivefolders=list() return render_title_template("config_edit.html", origin=origin, success=success, content=config, show_authenticate_google_drive=not is_gdrive_ready(), gdrive=gdriveutils.gdrive_support, - gdriveError=gdriveError, gdrivefolders=gdrivefolders, + gdriveError=gdriveError, gdrivefolders=gdrivefolders, rarfile_support=rar_support, goodreads=goodreads_support, title=_(u"Basic Configuration"), page="config") @@ -3607,4 +3700,3 @@ def upload(): return render_title_template('detail.html', entry=book, cc=cc, title=book.title, books_shelfs=book_in_shelfs, page="upload") return redirect(url_for("index")) - diff --git a/optional-requirements.txt b/optional-requirements.txt index ca86cc3a..22accfc1 100644 --- a/optional-requirements.txt +++ b/optional-requirements.txt @@ -16,3 +16,5 @@ goodreads>=0.3.2 python-Levenshtein>=0.12.0 # other lxml==3.7.2 +rarfile>=2.7 +natsort>=2.2.0