Merge remote-tracking branch 'Comic/comic_server'

# Conflicts:
#	cps/helper.py
#	cps/static/js/archive.js
#	cps/static/js/io.js
#	cps/static/js/kthoom.js
#	cps/static/js/unrar.js
#	cps/static/js/untar.js
#	cps/static/js/unzip.js
#	cps/templates/config_edit.html
#	cps/templates/detail.html
#	cps/templates/readcbr.html
#	cps/templates/stats.html
#	cps/ub.py
#	cps/web.py
#	optional-requirements.txt
This commit is contained in:
OzzieIsaacs 2018-08-31 10:47:58 +02:00
commit 3f35200a0b
12 changed files with 1443 additions and 663 deletions

View File

@ -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)

View File

@ -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);
}

308
cps/static/js/bytestream.js Normal file
View File

@ -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<Uint8Array>}
* @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;
}
}

View File

@ -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(
"<li>" +
"<a data-page='" + imageFiles.length + "'>" +
"<img src='" + imageFiles[imageFiles.length - 1].dataURI + "'/>" +
"<span>" + imageFiles.length + "</span>" +
"</a>" +
"</li>"
);
}
}
// 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);
x.fillText("Unable to decompress image #" + (currentImage + 1), innerWidth / 2, 100);
} else {
if ($("body").css("scrollHeight") / innerHeight > 1) {
$("body").css("overflowY", "scroll");
}
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("<iframe style=\"width:100%;height:700px;border:0\" src=\"data:text/html," + escape(xhr.responseText) + "\"></iframe>");
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("<iframe style=\"width:100%;height:700px;border:0\" src=\"data:text/html," + escape(xhr.responseText) + "\"></iframe>");
}
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);
}
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;
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);
};
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();
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);
}
$(window).resize(function() {
var f = (screen.width - innerWidth < 4 && screen.height - innerHeight < 4);
getElem("titlebar").className = f ? "main" : "";
updateScale();
});
if (jso.page !== jso.last) {
this.open("GET", this.fileid + "/" + (jso.page + 1));
this.addEventListener("load", ImageLoadCallback);
this.send();
}
$("#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) {
case 0:
clickedPrev = clickX < (comicWidth / 2);
break;
case 1:
clickedPrev = clickY < (comicHeight / 2);
break;
case 2:
clickedPrev = clickX > (comicWidth / 2);
break;
case 3:
clickedPrev = clickY > (comicHeight / 2);
break;
}
if (clickedPrev) {
showPrevPage();
} else {
showNextPage();
}
});
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;
// 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]);
});
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;
case 1:
clickedPrev = clickY < (comicHeight / 2);
break;
case 2:
clickedPrev = clickX > (comicWidth / 2);
break;
case 3:
clickedPrev = clickY > (comicHeight / 2);
break;
}
if (clickedPrev) {
showPrevPage();
} else {
showNextPage();
}
});
}

View File

@ -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.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.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;
}
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;
}
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.");
// 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;
}
}
}
};

View File

@ -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

View File

@ -170,7 +170,7 @@
<h4 class="panel-title">
<a class="accordion-toggle" data-toggle="collapse" href="#collapseeight">
<span class="glyphicon glyphicon-plus"></span>
{{_('E-Book converter')}}
{{_('External binaries')}}
</a>
</h4>
</div>
@ -179,9 +179,9 @@
<div class="form-group">
<div><input type="radio" name="config_ebookconverter" id="converter0" value="0" {% if content.config_ebookconverter == 0 %}checked{% endif %}>
<label for="converter0">{{_('No converter')}}</label></div>
<div><label><input type="radio" name="config_ebookconverter" id="converter1" value="1" {% if content.config_ebookconverter == 1 %}checked{% endif %}>
<div><input type="radio" name="config_ebookconverter" id="converter1" value="1" {% if content.config_ebookconverter == 1 %}checked{% endif %}>
<label for="converter1">{{_('Use Kindlegen')}}</label></div>
<div><label><input type="radio" name="config_ebookconverter" id="converter2" value="2" {% if content.config_ebookconverter == 2 %}checked{% endif %}>
<div><input type="radio" name="config_ebookconverter" id="converter2" value="2" {% if content.config_ebookconverter == 2 %}checked{% endif %}>
<label for="converter2">{{_('Use calibre\'s ebook converter')}}</label></div>
</div>
<div data-related="calibre">
@ -194,6 +194,12 @@
<input type="text" class="form-control" id="config_converterpath" name="config_converterpath" value="{% if content.config_converterpath != None %}{{ content.config_converterpath }}{% endif %}" autocomplete="off">
</div>
</div>
{% if rarfile_support %}
<div class="form-group">
<label for="config_rarfile_location">{{_('Location of Unrar binary')}}</label>
<input type="text" class="form-control" name="config_rarfile_location" id="config_rarfile_location" value="{% if content.config_rarfile_location != None %}{{ content.config_rarfile_location }}{% endif %}" autocomplete="off">
</div>
{% endif %}
</div>
</div>
</div>

View File

@ -2,15 +2,15 @@
{% block body %}
<h1>{{title}}</h1>
<div class="container">
<div class="col-sm-6">
<div class="col-xs-12 col-sm-6">
{% for lang in languages %}
{% if loop.index0 == (loop.length/2)|int and loop.length > 20 %}
</div>
<div class="col-sm-6">
<div class="col-xs-12 col-sm-6">
{% endif %}
<div class="row">
<div class="col-xs-1" align="left"><span class="badge">{{lang_counter[loop.index0].bookcount}}</span></div>
<div class="col-xs-6"><a id="list_{{loop.index0}}" href="{{url_for('language', name=lang.lang_code)}}">{{lang.name}}</a></div>
<div class="col-xs-2 col-sm-2 col-md-1" align="left"><span class="badge">{{lang_counter[loop.index0].bookcount}}</span></div>
<div class="col-xs-10 col-sm-10 col-md-11"><a id="list_{{loop.index0}}" href="{{url_for('language', name=lang.lang_code)}}">{{lang.name}}</a></div>
</div>
{% endfor %}
</div>

View File

@ -1,44 +1,157 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Comic Reader</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="stylesheet" href="{{ url_for('static', filename='css/kthoom.css') }}" type="text/css"/>
<link href="{{ url_for('static', filename='css/libs/bootstrap.min.css') }}" rel="stylesheet" media="screen">
<script src="{{ url_for('static', filename='js/libs/jquery.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap.min.js') }}"></script>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Comic Reader</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<script src="{{ url_for('static', filename='js/kthoom.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/libs/normalize.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}" type="text/css"/>
<link rel="stylesheet" href="{{ url_for('static', filename='css/kthoom.css') }}" type="text/css"/>
<script src="{{ url_for('static', filename='js/libs/jquery.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/screenfull.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/kthoom.js') }}"></script>
<script src="{{ url_for('static', filename='js/archive.js') }}"></script>
<script>
document.onreadystatechange = function () {
if (document.readyState == "complete") {
init("{{ url_for('serve_book', book_id=comicfile, book_format=extension) }}");
}
};
</script>
</head>
<body>
<div class="main" id="main">
<div id="titlebar">
<div id="progress" class="hide"></div>
<div class="progress2" style="display:none">
<div class="progress-bar" role="progressbar" aria-valuenow="70"
aria-valuemin="0" aria-valuemax="100" style="width:70%">
70%
</div>
</div>
</div>
<div id="mainContent">
<div id="mainText" style="display:none"></div>
<canvas id="mainImage"></canvas>
</div>
<div id="prev" class="arrow" onclick="showPrevPage()"></div>
<div id="next" class="arrow" onclick="showNextPage()"></div>
<script>
document.onreadystatechange = function () {
if (document.readyState == "complete") {
init("{{ url_for('serve_book', book_id=comicfile, book_format=extension) }}");
}
};
</script>
</head>
<body>
<div id="sidebar">
<!--
<div id="panels">
<a id="show-Toc" class="show_view icon-list-1 active" data-view="Toc">TOC</a>
<a id="show-Bookmarks" class="show_view icon-bookmark" data-view="Bookmarks">Bookmarks</a>
</div>
-->
<div id="tocView" class="view" tabindex="-1">
<ul id="thumbnails"></ul>
</div>
<div id="bookmarksView" class="view">
<ul id="bookmarks"></ul>
</div>
</div>
<div id="main" class="main">
<div id="titlebar" class="main">
<div id="opener">
<a id="slider" class="icon-menu">Menu</a>
</div>
</body>
<div id="metainfo">
<span id="book-title">{{ title | shortentitle }}</span>
<span id="title-seperator">&nbsp;&nbsp;&nbsp;&nbsp;</span>
<span id="chapter-title"></span>
</div>
<div id="title-controls">
<a id="setting" class="icon-cog">Settings</a>
<a id="fullscreen" class="icon-resize-full">Fullscreen</a>
</div>
<div id="progress" role="progressbar" class="loading">
<div class="bar-load">
<div class="text load">
Loading...
</div>
</div>
<div class="bar-read">
<div class="text page"></div>
</div>
</div>
</div>
<div id="mainContent" tabindex="-1">
<div id="mainText" style="display:none"></div>
<canvas id="mainImage"></canvas>
</div>
<div id="prev" class="arrow" onclick="showPrevPage()"></div>
<div id="next" class="arrow" onclick="showNextPage()"></div>
</div>
<div class="modal md-effect-1" id="settings-modal">
<div class="md-content">
<h3>{{_('Settings')}}</h3>
<div>
<div class="settings-column">
<table>
<thead>
<tr><th colspan="2">{{_('Keyboard Shortcuts')}}</th></tr>
</thead>
<tbody>
<tr><td>&larr;</td> <td>{{_('Previous Page')}}</td></tr>
<tr><td>&rarr;</td> <td>{{_('Next Page')}}</td></tr>
<tr><td>B</td> <td>{{_('Scale to Best')}}</td></tr>
<tr><td>W</td> <td>{{_('Scale to Width')}}</td></tr>
<tr><td>H</td> <td>{{_('Scale to Height')}}</td></tr>
<tr><td>N</td> <td>{{_('Scale to Native')}}</td></tr>
<tr><td>R</td> <td>{{_('Rotate Right')}}</td></tr>
<tr><td>L</td> <td>{{_('Rotate Left')}}</td></tr>
<tr><td>F</td> <td>{{_('Flip Image')}}</td></tr>
</tbody>
</table>
</div>
<div class="settings-column">
<table id="settings">
<thead>
<tr>
<th>{{_('Settings')}}</th>
</tr>
</thead>
<tbody>
<tr>
<th>{{_('Theme')}}:</th>
<td>
<div class="inputs">
<label for="lightTheme"><input type="radio" id="lightTheme" name="theme" value="light" /> {{_('Light')}}</label>
<label for="darkTheme"><input type="radio" id="darkTheme" name="theme" value="dark" /> {{_('Dark')}}</label>
</div>
</td>
</tr>
<tr>
<th>{{_('Scale')}}:</th>
<td>
<div class="inputs">
<label for="fitBest"><input type="radio" id="fitBest" name="fitMode" value="66" /> {{_('Best')}}</label>
<label for="fitWidth"><input type="radio" id="fitWidth" name="fitMode" value="87" /> {{_('Width')}}</label>
<label for="fitHeight"><input type="radio" id="fitHeight" name="fitMode" value="72" /> {{_('Height')}}</label>
<label for="fitNative"><input type="radio" id="fitNative" name="fitMode" value="78" /> {{_('Native')}}</label>
</div>
</td>
</tr>
<tr>
<th>{{_('Rotate')}}:</th>
<td>
<div class="inputs">
<label for="r0"><input type="radio" id="r0" name="rotateTimes" value="0" /> 0&deg;</label>
<label for="r90"><input type="radio" id="r90" name="rotateTimes" value="1" /> 90&deg;</label>
<label for="r180"><input type="radio" id="r180" name="rotateTimes" value="2" /> 180&deg;</label>
<label for="r270"><input type="radio" id="r270" name="rotateTimes" value="3" /> 270&deg;</label>
</div>
</td>
</tr>
<tr>
<th>{{_('Flip')}}:</th>
<td>
<div class="inputs">
<label for="vflip"><input type="checkbox" id="vflip" name="vflip" /> {{_('Horizontal')}}</label>
<label for="hflip"><input type="checkbox" id="hflip" name="hflip" /> {{_('Vertical')}}</label>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="closer icon-cancel-circled"></div>
</div>
</div>
<div class="overlay"></div>
</body>
</html>

View File

@ -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()

View File

@ -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/<int:book_id>/<book_format>/<int:page>")
@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/<int:book_id>")
@ -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/<int:book_id>/<book_format>")
@ -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"))

View File

@ -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