/* * kthoom.js * * Licensed under the MIT License * * Copyright(c) 2011 Google Inc. * Copyright(c) 2011 antimatter15 */ /* Reference Documentation: * Web Workers: http://www.whatwg.org/specs/web-workers/current-work/ * Web Workers in Mozilla: https://developer.mozilla.org/En/Using_web_workers * File API (FileReader): http://www.w3.org/TR/FileAPI/ * Typed Arrays: http://www.khronos.org/registry/typedarray/specs/latest/#6 */ /* global screenfull, bitjs, Uint8Array, opera, loadArchiveFormats, archiveOpenFile */ /* exported init, event */ if (window.opera) { window.console.log = function(str) { opera.postError(str); }; } var kthoom; // gets the element with the given id function getElem(id) { if (document.documentElement.querySelector) { // querySelector lookup return document.body.querySelector("#" + id); } // getElementById lookup return document.getElementById(id); } 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, 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 }; // global variables var unarchiver = null; var currentImage = 0; var imageFiles = []; var imageFilenames = []; var totalImages = 0; var prevScrollPosition = 0; var settings = { hflip: false, vflip: false, rotateTimes: 0, fitMode: kthoom.Key.B, theme: "light", direction: 0, // 0 = Left to Right, 1 = Right to Left nextPage: 0, // 0 = Reset to Top, 1 = Remember Position scrollbar: 1, // 0 = Hide Scrollbar, 1 = Show Scrollbar pageDisplay: 0 // 0 = Single Page, 1 = Long Strip }; kthoom.saveSettings = function() { localStorage.kthoomSettings = JSON.stringify(settings); }; kthoom.loadSettings = function() { try { if (!localStorage.kthoomSettings) { return; } $.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 = 0; // array.byteOffset; var len = array.byteLength; var blob; if (mimeType === "image/xml+svg") { var xmlStr = new TextDecoder("utf-8").decode(array); return "data:image/svg+xml;UTF-8," + encodeURIComponent(xmlStr); } // TODO: Move all this browser support testing to a common place // and do it just once. // Blob constructor, see http://dev.w3.org/2006/webapi/FileAPI/#dfn-Blob. if (typeof Blob === "function") { blob = new Blob([array], {type: mimeType}); } else { throw "Browser support for Blobs is missing."; } if (blob.slice) { blob = blob.slice(offset, offset + len, mimeType); } else { throw "Browser support for Blobs is missing."; } if ((typeof URL !== "function" && typeof URL !== "object") || typeof URL.createObjectURL !== "function") { throw "Browser support for Object URLs is missing"; } return URL.createObjectURL(blob); }; // Stores an image filename and its data: URI. kthoom.ImageFile = function(file) { this.filename = file.filename; var fileExtension = file.filename.split(".").pop().toLowerCase(); switch (fileExtension) { case "jpg": case "jpeg": this.mimeType = "image/jpeg"; break; case "png": this.mimeType = "image/png"; break; case "gif": this.mimeType = "image/gif"; break; case "svg": this.mimeType = "image/svg+xml"; break; case "webp": this.mimeType = "image/webp"; break; default: this.mimeType = undefined; break; } // Reset mime type for special files originating from Apple devices // This folder may contain files having image extensions (for example .jpg) but those files are not actual images // Trying to view these files cause corrupted/empty pages in the comic reader and files should be ignored if (this.filename.indexOf("__MACOSX") !== -1) { this.mimeType = undefined; } if ( this.mimeType !== undefined) { this.dataURI = createURLFromArray(file.fileData, this.mimeType); } }; function updateDirectionButtons(){ $("#right").show(); $("#left").show(); if (currentImage == 0 ) { if (settings.direction === 0) { $("#right").show(); $("#left").hide(); } else { $("#left").show(); $("#right").hide(); } } if ((currentImage + 1) >= Math.max(totalImages, imageFiles.length)) { if (settings.direction === 0) { $("#left").show(); $("#right").hide(); } else { $("#right").show(); $("#left").hide(); } } } function initProgressClick() { $("#progress").click(function(e) { var offset = $(this).offset(); var x = e.pageX - offset.left; var rate = settings.direction === 0 ? x / $(this).width() : 1 - x / $(this).width(); currentImage = Math.max(1, Math.ceil(rate * totalImages)) - 1; updateDirectionButtons(); setBookmark(); updatePage(); }); } function loadFromArrayBuffer(ab) { const collator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' }); loadArchiveFormats(['rar', 'zip', 'tar'], function() { // Open the file as an archive archiveOpenFile(ab, function (archive) { if (archive) { totalImages = archive.entries.length console.info('Uncompressing ' + archive.archive_type + ' ...'); entries = archive.entries.sort((a,b) => collator.compare(a.name, b.name)); entries.forEach(function(e, i) { updateProgress( (i + 1)/ totalImages * 100); if (e.is_file) { e.readData(function(d) { // add any new pages based on the filename if (imageFilenames.indexOf(e.name) === -1) { let data = {filename: e.name, fileData: d}; var test = new kthoom.ImageFile(data); if (test.mimeType !== undefined) { imageFilenames.push(e.name); imageFiles.push(test); // add thumbnails to the TOC list $("#thumbnails").append( "
  • " + "" + "" + "" + imageFiles.length + "" + "" + "
  • " ); drawCanvas(); setImage(test.dataURI, null); // display first page if we haven't yet if (imageFiles.length === currentImage + 1) { updateDirectionButtons(); updatePage(); } } else { totalImages--; } } }); } }); } }); }); } function scrollTocToActive() { $(".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"); // Scroll to the thumbnail in the TOC on page change $("#tocView").stop().animate({ scrollTop: $("#tocView a.active").position().top }, 200); } function updatePage() { scrollTocToActive(); scrollCurrentImageIntoView(); updateProgress(); pageDisplayUpdate(); setTheme(); kthoom.setSettings(); kthoom.saveSettings(); } function setTheme() { $("body").toggleClass("dark-theme", settings.theme === "dark"); $("#mainContent").toggleClass("disabled-scrollbar", settings.scrollbar === 0); } function pageDisplayUpdate() { if(settings.pageDisplay === 0) { $(".mainImage").addClass("hide"); $(".mainImage").eq(currentImage).removeClass("hide"); $("#mainContent").removeClass("long-strip"); } else { $(".mainImage").removeClass("hide"); $("#mainContent").addClass("long-strip"); scrollCurrentImageIntoView(); } } function updateProgress(loadPercentage) { if (settings.direction === 0) { $("#progress .bar-read") .removeClass("from-right") .addClass("from-left"); $("#progress .bar-load") .removeClass("from-right") .addClass("from-left"); } else { $("#progress .bar-read") .removeClass("from-left") .addClass("from-right"); $("#progress .bar-load") .removeClass("from-left") .addClass("from-right"); } // 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, _canvas) { var canvas = _canvas || $(".mainImage").slice(-1)[0]; // Select the last item on the array if _canvas is null var x = canvas.getContext("2d"); $("#mainText").hide(); if (url === "error") { x.fillStyle = "black"; x.textAlign = "center"; x.font = "24px sans-serif"; x.strokeStyle = (settings.theme === "dark") ? "white" : "black"; x.fillText("Unable to decompress image #" + (currentImage + 1), innerWidth / 2, 100); $(".mainImage").slice(-1).addClass("error"); } 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; 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|webp)$/.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); canvas.style.display = ""; $("body").css("overflowY", ""); x.restore(); }; img.src = url; } } // reloadImages is a slow process when multiple images are involved. Only used when rotating/mirroring function reloadImages() { for(i=0; i < imageFiles.length; i++) { setImage(imageFiles[i].dataURI, $(".mainImage")[i]); } } function showLeftPage() { if (settings.direction === 0) { showPrevPage(); } else { showNextPage(); } setBookmark(); } function showRightPage() { if (settings.direction === 0) { showNextPage(); } else { showPrevPage(); } setBookmark(); } function showPrevPage() { currentImage--; if (currentImage < 0) { // Freeze on the current page. currentImage++; } else { updatePage(); } updateDirectionButtons(); } function showNextPage() { currentImage++; if (currentImage >= Math.max(totalImages, imageFiles.length)) { // Freeze on the current page. currentImage--; } else { updatePage(); } updateDirectionButtons(); } function scrollCurrentImageIntoView() { if(settings.pageDisplay == 0) { // This will scroll all the way up when Single Page is selected $("#mainContent").scrollTop(0); } else { // This will scroll to the image when Long Strip is selected $("#mainContent").stop().animate({ scrollTop: $(".mainImage").eq(currentImage).offset().top + $("#mainContent").scrollTop() - $("#mainContent").offset().top }, 200); } } function updateScale() { var canvasArray = $("#mainContent > canvas"); var maxheight = innerHeight - 50; canvasArray.css("width", ""); canvasArray.css("height", ""); canvasArray.css("maxWidth", ""); canvasArray.css("maxHeight", ""); if(settings.pageDisplay === 0) { canvasArray.addClass("hide"); pageDisplayUpdate(); } switch (settings.fitMode) { case kthoom.Key.B: canvasArray.css("maxWidth", "100%"); canvasArray.css("maxHeight", maxheight + "px"); break; case kthoom.Key.H: canvasArray.css("maxHeight", maxheight + "px"); break; case kthoom.Key.W: canvasArray.css("width", "100%"); break; default: break; } $("#mainContent > canvas.error").css("width", innerWidth - 100); $("#mainContent > canvas.error").css("height", 200); $("#mainContent").css({maxHeight: maxheight + 5}); kthoom.setSettings(); kthoom.saveSettings(); } function keyHandler(evt) { var hasModifier = evt.ctrlKey || evt.shiftKey || evt.metaKey; switch (evt.keyCode) { case kthoom.Key.LEFT: if (hasModifier) break; showLeftPage(); break; case kthoom.Key.RIGHT: if (hasModifier) break; showRightPage(); break; case kthoom.Key.S: if (hasModifier) break; settings.pageDisplay = 0; pageDisplayUpdate(); kthoom.setSettings(); kthoom.saveSettings(); break; case kthoom.Key.O: if (hasModifier) break; settings.pageDisplay = 1; pageDisplayUpdate(); kthoom.setSettings(); kthoom.saveSettings(); break; case kthoom.Key.L: if (hasModifier) break; settings.rotateTimes--; if (settings.rotateTimes < 0) { settings.rotateTimes = 3; } updatePage(); reloadImages(); break; case kthoom.Key.R: if (hasModifier) break; settings.rotateTimes++; if (settings.rotateTimes > 3) { settings.rotateTimes = 0; } updatePage(); reloadImages(); break; case kthoom.Key.F: 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(); reloadImages(); break; case kthoom.Key.W: if (hasModifier) break; settings.fitMode = kthoom.Key.W; updateScale(); break; case kthoom.Key.H: if (hasModifier) break; settings.fitMode = kthoom.Key.H; updateScale(); break; case kthoom.Key.B: if (hasModifier) break; settings.fitMode = kthoom.Key.B; updateScale(); break; case kthoom.Key.N: if (hasModifier) break; settings.fitMode = kthoom.Key.N; updateScale(); break; case kthoom.Key.SPACE: if (evt.shiftKey) { evt.preventDefault(); // If it's Shift + Space and the container is at the top of the page showPrevPage(); } else { evt.preventDefault(); // If you're at the bottom of the page and you only pressed space showNextPage(); } break; default: //console.log('KeyCode', evt.keyCode); break; } } function drawCanvas() { var maxheight = innerHeight - 50; var canvasElement = $(""); var x = canvasElement[0].getContext("2d"); canvasElement.addClass("mainImage"); switch (settings.fitMode) { case kthoom.Key.B: canvasElement.css("maxWidth", "100%"); canvasElement.css("maxHeight", maxheight + "px"); break; case kthoom.Key.H: canvasElement.css("maxHeight", maxheight + "px"); break; case kthoom.Key.W: canvasElement.css("width", "100%"); break; default: break; } if(settings.pageDisplay === 0) { canvasElement.addClass("hide"); } //Fill with Placeholder text. setImage will override this canvasElement.width = innerWidth - 100; canvasElement.height = 200; x.fillStyle = "black"; x.textAlign = "center"; x.font = "24px sans-serif"; x.strokeStyle = (settings.theme === "dark") ? "white" : "black"; x.fillText("Loading Page #" + (currentImage + 1), innerWidth / 2, 100); $("#mainContent").append(canvasElement); } function updateArrows() { if ($('input[name="direction"]:checked').val() === "0") { $("#prev_page_key").html("←"); $("#next_page_key").html("→"); } else { $("#prev_page_key").html("→"); $("#next_page_key").html("←"); } }; function init(filename) { var request = new XMLHttpRequest(); request.open("GET", filename); request.responseType = "arraybuffer"; request.addEventListener("load", function () { if (request.status >= 200 && request.status < 300) { loadFromArrayBuffer(request.response); } else { console.warn(request.statusText, request.responseText); } }); kthoom.loadSettings(); setTheme(); updateScale(); request.send(); initProgressClick(); document.body.className += /AppleWebKit/.test(navigator.userAgent) ? " webkit" : ""; $(document).keydown(keyHandler); $(window).resize(function () { updateScale(); }); // 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; if (["hflip", "vflip", "rotateTimes"].includes(this.name)) { reloadImages(); } else if (this.name === "direction") { updateDirectionButtons(); return updateProgress(); } updatePage(); updateScale(); }); // Close modal $(".closer, .overlay").click(function () { $(".md-show").removeClass("md-show"); $("#mainContent").focus(); // focus back on the main container so you use up/down keys without having to click on it }); // 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]); // Focus on main container so you can use up/down keys immediately after fullscreen $("#mainContent").focus(); }); 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(); $("#mainContent").swipe({ swipeRight: function () { showLeftPage(); }, swipeLeft: function () { showRightPage(); }, }); $(".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 clickedLeft = false; switch (settings.rotateTimes) { case 0: clickedLeft = clickX < (comicWidth / 2); break; case 1: clickedLeft = clickY < (comicHeight / 2); break; case 2: clickedLeft = clickX > (comicWidth / 2); break; case 3: clickedLeft = clickY > (comicHeight / 2); break; } if (clickedLeft) { showLeftPage(); } else { showRightPage(); } }); // Scrolling up/down will update current image if a new image is into view (for Long Strip Display) $("#mainContent").scroll(function (){ var scroll = $("#mainContent").scrollTop(); var viewLength = 0; $(".mainImage").each(function(){ viewLength += $(this).height(); }); if (settings.pageDisplay === 0) { // Don't trigger the scroll for Single Page } else if (scroll > prevScrollPosition) { //Scroll Down if (currentImage + 1 < imageFiles.length) { if (currentImageOffset(currentImage + 1) <= 1) { currentImage = Math.floor((imageFiles.length) / (viewLength-viewLength/(imageFiles.length)) * scroll, 0); if ( currentImage >= imageFiles.length) { currentImage = imageFiles.length - 1; } console.log(currentImage); scrollTocToActive(); updateProgress(); } } } else { //Scroll Up if (currentImage - 1 > -1) { if (currentImageOffset(currentImage - 1) >= 0) { currentImage = Math.floor((imageFiles.length) / (viewLength-viewLength/(imageFiles.length)) * scroll, 0); console.log(currentImage); scrollTocToActive(); updateProgress(); } } } // Update scroll position prevScrollPosition = scroll; }); } function currentImageOffset(imageIndex) { return $(".mainImage").eq(imageIndex).offset().top - $("#mainContent").position().top } function setBookmark() { // get csrf_token let csrf_token = $("input[name='csrf_token']").val(); //This sends a bookmark update to calibreweb. $.ajax(calibre.bookmarkUrl, { method: "post", data: { csrf_token: csrf_token, bookmark: currentImage } }).fail(function (xhr, status, error) { console.error(error); }); } $(function() { $('input[name="direction"]').change(function () { updateArrows(); }); $('#left').click(function () { showLeftPage(); }); $('#right').click(function () { showRightPage(); }); });