Merge branch 'Develop':
- Fix for new tornado version - bookmark for comic viewer - Bugfix for showing series containing only one book in list view containing having this book no series_index value set - updated requirements
This commit is contained in:
+1
-1
@@ -33,7 +33,7 @@ from functools import wraps
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g, Response
|
||||
from flask import Markup
|
||||
from markupsafe import Markup
|
||||
from flask_login import login_required, current_user, logout_user
|
||||
from flask_babel import gettext as _
|
||||
from flask_babel import get_locale, format_time, format_datetime, format_timedelta
|
||||
|
||||
@@ -663,7 +663,7 @@ class CalibreDB:
|
||||
|
||||
cls.session_factory = scoped_session(sessionmaker(autocommit=False,
|
||||
autoflush=True,
|
||||
bind=cls.engine))
|
||||
bind=cls.engine, future=True))
|
||||
for inst in cls.instances:
|
||||
inst.init_session()
|
||||
|
||||
|
||||
+2
-3
@@ -25,16 +25,15 @@ from datetime import datetime
|
||||
import json
|
||||
from shutil import copyfile
|
||||
from uuid import uuid4
|
||||
from markupsafe import escape # dependency of flask
|
||||
from markupsafe import escape, Markup # dependency of flask
|
||||
from functools import wraps
|
||||
import re
|
||||
|
||||
try:
|
||||
from lxml.html.clean import clean_html, Cleaner
|
||||
except ImportError:
|
||||
clean_html = None
|
||||
|
||||
from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response
|
||||
from flask import Blueprint, request, flash, redirect, url_for, abort, Response
|
||||
from flask_babel import gettext as _
|
||||
from flask_babel import lazy_gettext as N_
|
||||
from flask_babel import get_locale
|
||||
|
||||
+1
-32
@@ -166,12 +166,6 @@ def HandleSyncRequest():
|
||||
only_kobo_shelves = current_user.kobo_only_shelves_sync
|
||||
|
||||
if only_kobo_shelves:
|
||||
#if sqlalchemy_version2:
|
||||
# changed_entries = select(db.Books,
|
||||
# ub.ArchivedBook.last_modified,
|
||||
# ub.BookShelf.date_added,
|
||||
# ub.ArchivedBook.is_archived)
|
||||
#else:
|
||||
changed_entries = calibre_db.session.query(db.Books,
|
||||
ub.ArchivedBook.last_modified,
|
||||
ub.BookShelf.date_added,
|
||||
@@ -192,9 +186,6 @@ def HandleSyncRequest():
|
||||
.filter(ub.Shelf.kobo_sync)
|
||||
.distinct())
|
||||
else:
|
||||
#if sqlalchemy_version2:
|
||||
# changed_entries = select(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived)
|
||||
#else:
|
||||
changed_entries = calibre_db.session.query(db.Books,
|
||||
ub.ArchivedBook.last_modified,
|
||||
ub.ArchivedBook.is_archived)
|
||||
@@ -209,9 +200,6 @@ def HandleSyncRequest():
|
||||
.order_by(db.Books.id))
|
||||
|
||||
reading_states_in_new_entitlements = []
|
||||
#if sqlalchemy_version2:
|
||||
# books = calibre_db.session.execute(changed_entries.limit(SYNC_ITEM_LIMIT))
|
||||
#else:
|
||||
books = changed_entries.limit(SYNC_ITEM_LIMIT)
|
||||
log.debug("Books to Sync: {}".format(len(books.all())))
|
||||
for book in books:
|
||||
@@ -255,13 +243,6 @@ def HandleSyncRequest():
|
||||
new_books_last_created = max(ts_created, new_books_last_created)
|
||||
kobo_sync_status.add_synced_books(book.Books.id)
|
||||
|
||||
'''if sqlalchemy_version2:
|
||||
max_change = calibre_db.session.execute(changed_entries
|
||||
.filter(ub.ArchivedBook.is_archived)
|
||||
.filter(ub.ArchivedBook.user_id == current_user.id)
|
||||
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc()))\
|
||||
.columns(db.Books).first()
|
||||
else:'''
|
||||
max_change = changed_entries.filter(ub.ArchivedBook.is_archived)\
|
||||
.filter(ub.ArchivedBook.user_id == current_user.id) \
|
||||
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc()).first()
|
||||
@@ -271,10 +252,6 @@ def HandleSyncRequest():
|
||||
new_archived_last_modified = max(new_archived_last_modified, max_change)
|
||||
|
||||
# no. of books returned
|
||||
'''if sqlalchemy_version2:
|
||||
entries = calibre_db.session.execute(changed_entries).all()
|
||||
book_count = len(entries)
|
||||
else:'''
|
||||
book_count = changed_entries.count()
|
||||
# last entry:
|
||||
cont_sync = bool(book_count)
|
||||
@@ -523,7 +500,7 @@ def get_metadata(book):
|
||||
@requires_kobo_auth
|
||||
# Creates a Shelf with the given items, and returns the shelf's uuid.
|
||||
def HandleTagCreate():
|
||||
# catch delete requests, otherwise the are handled in the book delete handler
|
||||
# catch delete requests, otherwise they are handled in the book delete handler
|
||||
if request.method == "DELETE":
|
||||
abort(405)
|
||||
name, items = None, None
|
||||
@@ -717,14 +694,6 @@ def sync_shelves(sync_token, sync_results, only_kobo_shelves=False):
|
||||
})
|
||||
extra_filters.append(ub.Shelf.kobo_sync)
|
||||
|
||||
'''if sqlalchemy_version2:
|
||||
shelflist = ub.session.execute(select(ub.Shelf).outerjoin(ub.BookShelf).filter(
|
||||
or_(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified,
|
||||
func.datetime(ub.BookShelf.date_added) > sync_token.tags_last_modified),
|
||||
ub.Shelf.user_id == current_user.id,
|
||||
*extra_filters
|
||||
).distinct().order_by(func.datetime(ub.Shelf.last_modified).asc())).columns(ub.Shelf)
|
||||
else:'''
|
||||
shelflist = ub.session.query(ub.Shelf).outerjoin(ub.BookShelf).filter(
|
||||
or_(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified,
|
||||
func.datetime(ub.BookShelf.date_added) > sync_token.tags_last_modified),
|
||||
|
||||
+4
-1
@@ -288,4 +288,7 @@ class WebServer(object):
|
||||
if _GEVENT:
|
||||
self.wsgiserver.close()
|
||||
else:
|
||||
self.wsgiserver.add_callback_from_signal(self.wsgiserver.stop)
|
||||
if restart:
|
||||
self.wsgiserver.call_later(1.0, self.wsgiserver.stop)
|
||||
else:
|
||||
self.wsgiserver.add_callback_from_signal(self.wsgiserver.stop)
|
||||
|
||||
@@ -19,10 +19,8 @@
|
||||
|
||||
import sys
|
||||
from base64 import b64decode, b64encode
|
||||
from jsonschema import validate, exceptions, __version__
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from urllib.parse import unquote
|
||||
from jsonschema import validate, exceptions
|
||||
from datetime import datetime
|
||||
|
||||
from flask import json
|
||||
from .. import logger
|
||||
|
||||
+112
-35
@@ -71,7 +71,8 @@ var settings = {
|
||||
fitMode: kthoom.Key.B,
|
||||
theme: "light",
|
||||
direction: 0, // 0 = Left to Right, 1 = Right to Left
|
||||
scrollbar: 1, // 0 = Hide Scrollbar, 1 = Show Scrollbar
|
||||
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
|
||||
};
|
||||
|
||||
@@ -131,8 +132,8 @@ var createURLFromArray = function(array, mimeType) {
|
||||
}
|
||||
|
||||
if ((typeof URL !== "function" && typeof URL !== "object") ||
|
||||
typeof URL.createObjectURL !== "function") {
|
||||
throw "Browser support for Object URLs is missing";
|
||||
typeof URL.createObjectURL !== "function") {
|
||||
throw "Browser support for Object URLs is missing";
|
||||
}
|
||||
|
||||
return URL.createObjectURL(blob);
|
||||
@@ -177,12 +178,36 @@ kthoom.ImageFile = function(file) {
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
@@ -222,6 +247,7 @@ function loadFromArrayBuffer(ab) {
|
||||
|
||||
// display first page if we haven't yet
|
||||
if (imageFiles.length === currentImage + 1) {
|
||||
updateDirectionButtons();
|
||||
updatePage();
|
||||
}
|
||||
} else {
|
||||
@@ -241,7 +267,7 @@ function scrollTocToActive() {
|
||||
|
||||
// Mark the current page in the TOC
|
||||
$("#tocView a[data-page]")
|
||||
// Remove the currently active thumbnail
|
||||
// Remove the currently active thumbnail
|
||||
.removeClass("active")
|
||||
// Find the new one
|
||||
.filter("[data-page=" + (currentImage + 1) + "]")
|
||||
@@ -409,6 +435,7 @@ function showLeftPage() {
|
||||
} else {
|
||||
showNextPage();
|
||||
}
|
||||
setBookmark();
|
||||
}
|
||||
|
||||
function showRightPage() {
|
||||
@@ -417,6 +444,7 @@ function showRightPage() {
|
||||
} else {
|
||||
showPrevPage();
|
||||
}
|
||||
setBookmark();
|
||||
}
|
||||
|
||||
function showPrevPage() {
|
||||
@@ -427,6 +455,7 @@ function showPrevPage() {
|
||||
} else {
|
||||
updatePage();
|
||||
}
|
||||
updateDirectionButtons();
|
||||
}
|
||||
|
||||
function showNextPage() {
|
||||
@@ -437,6 +466,7 @@ function showNextPage() {
|
||||
} else {
|
||||
updatePage();
|
||||
}
|
||||
updateDirectionButtons();
|
||||
}
|
||||
|
||||
function scrollCurrentImageIntoView() {
|
||||
@@ -621,11 +651,21 @@ function drawCanvas() {
|
||||
$("#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() {
|
||||
request.addEventListener("load", function () {
|
||||
if (request.status >= 200 && request.status < 300) {
|
||||
loadFromArrayBuffer(request.response);
|
||||
} else {
|
||||
@@ -641,18 +681,18 @@ function init(filename) {
|
||||
|
||||
$(document).keydown(keyHandler);
|
||||
|
||||
$(window).resize(function() {
|
||||
$(window).resize(function () {
|
||||
updateScale();
|
||||
});
|
||||
|
||||
// Open TOC menu
|
||||
$("#slider").click(function() {
|
||||
$("#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() {
|
||||
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();
|
||||
@@ -660,12 +700,12 @@ function init(filename) {
|
||||
});
|
||||
|
||||
// Open Settings modal
|
||||
$("#setting").click(function() {
|
||||
$("#setting").click(function () {
|
||||
$("#settings-modal").toggleClass("md-show");
|
||||
});
|
||||
|
||||
// On Settings input change
|
||||
$("#settings input").on("change", function() {
|
||||
$("#settings input").on("change", function () {
|
||||
// Get either the checked boolean or the assigned value
|
||||
var value = this.type === "checkbox" ? this.checked : this.value;
|
||||
|
||||
@@ -674,39 +714,40 @@ function init(filename) {
|
||||
|
||||
settings[this.name] = value;
|
||||
|
||||
if(["hflip", "vflip", "rotateTimes"].includes(this.name)) {
|
||||
if (["hflip", "vflip", "rotateTimes"].includes(this.name)) {
|
||||
reloadImages();
|
||||
} else if(this.name === "direction") {
|
||||
} else if (this.name === "direction") {
|
||||
updateDirectionButtons();
|
||||
return updateProgress();
|
||||
}
|
||||
|
||||
|
||||
updatePage();
|
||||
updateScale();
|
||||
});
|
||||
|
||||
// Close modal
|
||||
$(".closer, .overlay").click(function() {
|
||||
$(".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
|
||||
$("#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() {
|
||||
$("#thumbnails").on("click", "a", function () {
|
||||
currentImage = $(this).data("page") - 1;
|
||||
updatePage();
|
||||
});
|
||||
|
||||
// Fullscreen mode
|
||||
if (typeof screenfull !== "undefined") {
|
||||
$("#fullscreen").click(function() {
|
||||
$("#fullscreen").click(function () {
|
||||
screenfull.toggle($("#container")[0]);
|
||||
// Focus on main container so you can use up/down keys immediately after fullscreen
|
||||
$("#mainContent").focus();
|
||||
// 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() {
|
||||
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");
|
||||
@@ -717,16 +758,16 @@ function init(filename) {
|
||||
// Focus the scrollable area so that keyboard scrolling work as expected
|
||||
$("#mainContent").focus();
|
||||
|
||||
$("#mainContent").swipe( {
|
||||
swipeRight:function() {
|
||||
$("#mainContent").swipe({
|
||||
swipeRight: function () {
|
||||
showLeftPage();
|
||||
},
|
||||
swipeLeft:function() {
|
||||
swipeLeft: function () {
|
||||
showRightPage();
|
||||
},
|
||||
});
|
||||
$(".mainImage").click(function(evt) {
|
||||
// Firefox does not support offsetX/Y so we have to manually calculate
|
||||
$(".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();
|
||||
@@ -762,30 +803,38 @@ function init(filename) {
|
||||
});
|
||||
|
||||
// Scrolling up/down will update current image if a new image is into view (for Long Strip Display)
|
||||
$("#mainContent").scroll(function(){
|
||||
$("#mainContent").scroll(function (){
|
||||
var scroll = $("#mainContent").scrollTop();
|
||||
if(settings.pageDisplay === 0) {
|
||||
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) {
|
||||
} else if (scroll > prevScrollPosition) {
|
||||
//Scroll Down
|
||||
if(currentImage + 1 < imageFiles.length) {
|
||||
if(currentImageOffset(currentImage + 1) <= 1) {
|
||||
currentImage++;
|
||||
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--;
|
||||
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;
|
||||
});
|
||||
@@ -794,3 +843,31 @@ function init(filename) {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -333,7 +333,6 @@ $(function() {
|
||||
} else {
|
||||
$("#parent").addClass('hidden')
|
||||
}
|
||||
// console.log(data);
|
||||
data.files.forEach(function(entry) {
|
||||
if(entry.type === "dir") {
|
||||
var type = "<span class=\"glyphicon glyphicon-folder-close\"></span>";
|
||||
|
||||
+84
-76
@@ -1,5 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
@@ -20,23 +21,6 @@
|
||||
<script src="{{ url_for('static', filename='js/libs/screenfull.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/compress/uncompress.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/kthoom.js') }}"></script>
|
||||
<script>
|
||||
var updateArrows = function() {
|
||||
if ($('input[name="direction"]:checked').val() === "0") {
|
||||
$("#prev_page_key").html("←");
|
||||
$("#next_page_key").html("→");
|
||||
} else {
|
||||
$("#prev_page_key").html("→");
|
||||
$("#next_page_key").html("←");
|
||||
}
|
||||
};
|
||||
document.onreadystatechange = function () {
|
||||
if (document.readyState == "complete") {
|
||||
init("{{ url_for('web.serve_book', book_id=comicfile, book_format=extension) }}");
|
||||
updateArrows();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="sidebar">
|
||||
@@ -77,8 +61,8 @@
|
||||
<div id="mainContent" tabindex="-1">
|
||||
<div id="mainText" style="display:none"></div>
|
||||
</div>
|
||||
<div id="left" class="arrow" onclick="showLeftPage()">‹</div>
|
||||
<div id="right" class="arrow" onclick="showRightPage()">›</div>
|
||||
<div id="left" class="arrow" style="display:none">‹</div>
|
||||
<div id="right" class="arrow" style="display:none">›</div>
|
||||
</div>
|
||||
|
||||
<div class="modal md-effect-1" id="settings-modal">
|
||||
@@ -89,8 +73,8 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th colspan="2">{{_('Keyboard Shortcuts')}}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td id="prev_page_key">←</td> <td>{{_('Previous Page')}}</td></tr>
|
||||
<tr><td id="next_page_key">→</td> <td>{{_('Next Page')}}</td></tr>
|
||||
<tr><td>S</td> <td>{{_('Single Page Display')}}</td></tr>
|
||||
@@ -102,21 +86,21 @@
|
||||
<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">
|
||||
</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>
|
||||
@@ -139,59 +123,83 @@
|
||||
<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°</label>
|
||||
<label for="r90"><input type="radio" id="r90" name="rotateTimes" value="1" /> 90°</label>
|
||||
<label for="r180"><input type="radio" id="r180" name="rotateTimes" value="2" /> 180°</label>
|
||||
<label for="r270"><input type="radio" id="r270" name="rotateTimes" value="3" /> 270°</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>
|
||||
<tr>
|
||||
<th>{{_('Direction')}}:</th>
|
||||
<td>
|
||||
<div class="inputs">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{_('Rotate')}}:</th>
|
||||
<td>
|
||||
<div class="inputs">
|
||||
<label for="r0"><input type="radio" id="r0" name="rotateTimes" value="0" /> 0°</label>
|
||||
<label for="r90"><input type="radio" id="r90" name="rotateTimes" value="1" /> 90°</label>
|
||||
<label for="r180"><input type="radio" id="r180" name="rotateTimes" value="2" /> 180°</label>
|
||||
<label for="r270"><input type="radio" id="r270" name="rotateTimes" value="3" /> 270°</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>
|
||||
<tr>
|
||||
<th>{{_('Direction')}}:</th>
|
||||
<td>
|
||||
<div class="inputs">
|
||||
<label for="leftToRight"><input type="radio" id="leftToRight" name="direction" value="0" /> {{_('Left to Right')}}</label>
|
||||
<label for="rightToLeft"><input type="radio" id="rightToLeft" name="direction" value="1" /> {{_('Right to Left')}}</label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{_('Next Page')}}:</th>
|
||||
<td>
|
||||
<div class="inputs">
|
||||
<label for="resetToTop"><input type="radio" id="resetToTop" name="nextPage" value="0" /> {{_('Reset to Top')}}</label>
|
||||
<label for="rememberPosition"><input type="radio" id="rememberPosition" name="nextPage" value="1" /> {{_('Remember Position')}}</label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{_('Scrollbar')}}:</th>
|
||||
<td>
|
||||
<div class="inputs">
|
||||
<label for="showScrollbar"><input type="radio" id="showScrollbar" name="scrollbar" value="1" /> {{_('Show')}}</label>
|
||||
<label for="hideScrollbar"><input type="radio" id="hideScrollbar" name="scrollbar" value="0" /> {{_('Hide')}}</label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="closer icon-cancel-circled"></div>
|
||||
</div>
|
||||
<div class="closer icon-cancel-circled"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay"></div>
|
||||
<script>
|
||||
$('input[name="direction"]').change(function() {
|
||||
updateArrows();
|
||||
});
|
||||
</script>
|
||||
<div class="overlay"></div>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<script>
|
||||
window.calibre = {
|
||||
bookmarkUrl: "{{ url_for('web.set_bookmark', book_id=comicfile, book_format=extension.upper()) }}",
|
||||
bookmark: "{{ bookmark.bookmark_key if bookmark != None }}",
|
||||
useBookmarks: "{{ current_user.is_authenticated | tojson }}"
|
||||
};
|
||||
|
||||
document.onreadystatechange = function () {
|
||||
if (document.readyState == "complete") {
|
||||
if (calibre.useBookmarks) {
|
||||
currentImage = eval(calibre.bookmark);
|
||||
if (typeof currentImage !== 'number') {
|
||||
currentImage = 0;
|
||||
}
|
||||
}
|
||||
init("{{ url_for('web.serve_book', book_id=comicfile, book_format=extension) }}");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+59
-53
@@ -16,12 +16,12 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from tornado.wsgi import WSGIContainer
|
||||
import tornado
|
||||
|
||||
from tornado import escape
|
||||
from tornado import httputil
|
||||
from tornado.ioloop import IOLoop
|
||||
|
||||
from typing import List, Tuple, Optional, Callable, Any, Dict, Text
|
||||
from types import TracebackType
|
||||
@@ -34,61 +34,67 @@ if typing.TYPE_CHECKING:
|
||||
class MyWSGIContainer(WSGIContainer):
|
||||
|
||||
def __call__(self, request: httputil.HTTPServerRequest) -> None:
|
||||
data = {} # type: Dict[str, Any]
|
||||
response = [] # type: List[bytes]
|
||||
if tornado.version_info < (6, 3, 0, -99):
|
||||
data = {} # type: Dict[str, Any]
|
||||
response = [] # type: List[bytes]
|
||||
|
||||
def start_response(
|
||||
status: str,
|
||||
headers: List[Tuple[str, str]],
|
||||
exc_info: Optional[
|
||||
Tuple[
|
||||
"Optional[Type[BaseException]]",
|
||||
Optional[BaseException],
|
||||
Optional[TracebackType],
|
||||
]
|
||||
] = None,
|
||||
) -> Callable[[bytes], Any]:
|
||||
data["status"] = status
|
||||
data["headers"] = headers
|
||||
return response.append
|
||||
def start_response(
|
||||
status: str,
|
||||
headers: List[Tuple[str, str]],
|
||||
exc_info: Optional[
|
||||
Tuple[
|
||||
"Optional[Type[BaseException]]",
|
||||
Optional[BaseException],
|
||||
Optional[TracebackType],
|
||||
]
|
||||
] = None,
|
||||
) -> Callable[[bytes], Any]:
|
||||
data["status"] = status
|
||||
data["headers"] = headers
|
||||
return response.append
|
||||
|
||||
app_response = self.wsgi_application(
|
||||
MyWSGIContainer.environ(request), start_response
|
||||
)
|
||||
app_response = self.wsgi_application(
|
||||
MyWSGIContainer.environ(self, request), start_response
|
||||
)
|
||||
try:
|
||||
response.extend(app_response)
|
||||
body = b"".join(response)
|
||||
finally:
|
||||
if hasattr(app_response, "close"):
|
||||
app_response.close() # type: ignore
|
||||
if not data:
|
||||
raise Exception("WSGI app did not call start_response")
|
||||
|
||||
status_code_str, reason = data["status"].split(" ", 1)
|
||||
status_code = int(status_code_str)
|
||||
headers = data["headers"] # type: List[Tuple[str, str]]
|
||||
header_set = set(k.lower() for (k, v) in headers)
|
||||
body = escape.utf8(body)
|
||||
if status_code != 304:
|
||||
if "content-length" not in header_set:
|
||||
headers.append(("Content-Length", str(len(body))))
|
||||
if "content-type" not in header_set:
|
||||
headers.append(("Content-Type", "text/html; charset=UTF-8"))
|
||||
if "server" not in header_set:
|
||||
headers.append(("Server", "TornadoServer/%s" % tornado.version))
|
||||
|
||||
start_line = httputil.ResponseStartLine("HTTP/1.1", status_code, reason)
|
||||
header_obj = httputil.HTTPHeaders()
|
||||
for key, value in headers:
|
||||
header_obj.add(key, value)
|
||||
assert request.connection is not None
|
||||
request.connection.write_headers(start_line, header_obj, chunk=body)
|
||||
request.connection.finish()
|
||||
self._log(status_code, request)
|
||||
else:
|
||||
IOLoop.current().spawn_callback(self.handle_request, request)
|
||||
|
||||
|
||||
def environ(self, request: httputil.HTTPServerRequest) -> Dict[Text, Any]:
|
||||
try:
|
||||
response.extend(app_response)
|
||||
body = b"".join(response)
|
||||
finally:
|
||||
if hasattr(app_response, "close"):
|
||||
app_response.close() # type: ignore
|
||||
if not data:
|
||||
raise Exception("WSGI app did not call start_response")
|
||||
|
||||
status_code_str, reason = data["status"].split(" ", 1)
|
||||
status_code = int(status_code_str)
|
||||
headers = data["headers"] # type: List[Tuple[str, str]]
|
||||
header_set = set(k.lower() for (k, v) in headers)
|
||||
body = escape.utf8(body)
|
||||
if status_code != 304:
|
||||
if "content-length" not in header_set:
|
||||
headers.append(("Content-Length", str(len(body))))
|
||||
if "content-type" not in header_set:
|
||||
headers.append(("Content-Type", "text/html; charset=UTF-8"))
|
||||
if "server" not in header_set:
|
||||
headers.append(("Server", "TornadoServer/%s" % tornado.version))
|
||||
|
||||
start_line = httputil.ResponseStartLine("HTTP/1.1", status_code, reason)
|
||||
header_obj = httputil.HTTPHeaders()
|
||||
for key, value in headers:
|
||||
header_obj.add(key, value)
|
||||
assert request.connection is not None
|
||||
request.connection.write_headers(start_line, header_obj, chunk=body)
|
||||
request.connection.finish()
|
||||
self._log(status_code, request)
|
||||
|
||||
@staticmethod
|
||||
def environ(request: httputil.HTTPServerRequest) -> Dict[Text, Any]:
|
||||
environ = WSGIContainer.environ(request)
|
||||
environ = WSGIContainer.environ(self, request)
|
||||
except TypeError as e:
|
||||
environ = WSGIContainer.environ(request)
|
||||
environ['RAW_URI'] = request.path
|
||||
return environ
|
||||
|
||||
|
||||
+2
-2
@@ -1014,7 +1014,7 @@ def series_list():
|
||||
func.max(db.Books.series_index), db.Books.id)
|
||||
.join(db.books_series_link).join(db.Series).filter(calibre_db.common_filters())
|
||||
.group_by(text('books_series_link.series'))
|
||||
.having(func.max(db.Books.series_index))
|
||||
.having(or_(func.max(db.Books.series_index), db.Books.series_index==""))
|
||||
.order_by(order)
|
||||
.all())
|
||||
return render_title_template('grid.html', entries=entries, folder='web.books_list', charlist=char_list,
|
||||
@@ -1569,7 +1569,7 @@ def read_book(book_id, book_format):
|
||||
title = title + " #" + '{0:.2f}'.format(book.series_index).rstrip('0').rstrip('.')
|
||||
log.debug("Start comic reader for %d", book_id)
|
||||
return render_title_template('readcbr.html', comicfile=all_name, title=title,
|
||||
extension=fileExt)
|
||||
extension=fileExt, bookmark=bookmark)
|
||||
log.debug("Selected book is unavailable. File does not exist or is not accessible")
|
||||
flash(_("Oops! Selected book is unavailable. File does not exist or is not accessible"),
|
||||
category="error")
|
||||
|
||||
Reference in New Issue
Block a user