mirror of
				https://github.com/janeczku/calibre-web
				synced 2025-10-31 15:23:02 +00:00 
			
		
		
		
	Merge branch 'master' into Develop
# Conflicts: # cps/services/SyncToken.py # cps/static/js/kthoom.js # cps/templates/readcbr.html
This commit is contained in:
		| @@ -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() | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										24
									
								
								cps/epub.py
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								cps/epub.py
									
									
									
									
									
								
							| @@ -21,10 +21,11 @@ import zipfile | ||||
| from lxml import etree | ||||
|  | ||||
| from . import isoLanguages, cover | ||||
| from . import config | ||||
| from . import config, logger | ||||
| from .helper import split_authors | ||||
| from .constants import BookMeta | ||||
|  | ||||
| log = logger.create() | ||||
|  | ||||
| def _extract_cover(zip_file, cover_file, cover_path, tmp_file_name): | ||||
|     if cover_file is None: | ||||
| @@ -49,15 +50,20 @@ def get_epub_layout(book, book_data): | ||||
|     } | ||||
|     file_path = os.path.normpath(os.path.join(config.config_calibre_dir, book.path, book_data.name + "." + book_data.format.lower())) | ||||
|  | ||||
|     epubZip = zipfile.ZipFile(file_path) | ||||
|     txt = epubZip.read('META-INF/container.xml') | ||||
|     tree = etree.fromstring(txt) | ||||
|     cfname = tree.xpath('n:rootfiles/n:rootfile/@full-path', namespaces=ns)[0] | ||||
|     cf = epubZip.read(cfname) | ||||
|     tree = etree.fromstring(cf) | ||||
|     p = tree.xpath('/pkg:package/pkg:metadata', namespaces=ns)[0] | ||||
|     try: | ||||
|         epubZip = zipfile.ZipFile(file_path) | ||||
|         txt = epubZip.read('META-INF/container.xml') | ||||
|         tree = etree.fromstring(txt) | ||||
|         cfname = tree.xpath('n:rootfiles/n:rootfile/@full-path', namespaces=ns)[0] | ||||
|         cf = epubZip.read(cfname) | ||||
|  | ||||
|     layout = p.xpath('pkg:meta[@property="rendition:layout"]/text()', namespaces=ns) | ||||
|         tree = etree.fromstring(cf) | ||||
|         p = tree.xpath('/pkg:package/pkg:metadata', namespaces=ns)[0] | ||||
|  | ||||
|         layout = p.xpath('pkg:meta[@property="rendition:layout"]/text()', namespaces=ns) | ||||
|     except (etree.XMLSyntaxError, KeyError, IndexError) as e: | ||||
|         log.error("Could not parse epub metadata of book {} during kobo sync: {}".format(book.id, e)) | ||||
|         layout = [] | ||||
|  | ||||
|     if len(layout) == 0: | ||||
|         return None | ||||
|   | ||||
| @@ -732,28 +732,27 @@ def delete_book(book, calibrepath, book_format): | ||||
|         return delete_book_file(book, calibrepath, book_format) | ||||
|  | ||||
|  | ||||
| def get_cover_on_failure(use_generic_cover): | ||||
|     if use_generic_cover: | ||||
|         try: | ||||
|             return send_from_directory(_STATIC_DIR, "generic_cover.jpg") | ||||
|         except PermissionError: | ||||
|             log.error("No permission to access generic_cover.jpg file.") | ||||
|             abort(403) | ||||
|     abort(404) | ||||
| def get_cover_on_failure(): | ||||
|     try: | ||||
|         return send_from_directory(_STATIC_DIR, "generic_cover.jpg") | ||||
|     except PermissionError: | ||||
|         log.error("No permission to access generic_cover.jpg file.") | ||||
|         abort(403) | ||||
|  | ||||
|  | ||||
| def get_book_cover(book_id, resolution=None): | ||||
|     book = calibre_db.get_filtered_book(book_id, allow_show_archived=True) | ||||
|     return get_book_cover_internal(book, use_generic_cover_on_failure=True, resolution=resolution) | ||||
|     return get_book_cover_internal(book, resolution=resolution) | ||||
|  | ||||
|  | ||||
| # Called only by kobo sync -> cover not found should be answered with 404 and not with default cover | ||||
| def get_book_cover_with_uuid(book_uuid, resolution=None): | ||||
|     book = calibre_db.get_book_by_uuid(book_uuid) | ||||
|     return get_book_cover_internal(book, use_generic_cover_on_failure=False, resolution=resolution) | ||||
|     if not book: | ||||
|         return  # allows kobo.HandleCoverImageRequest to proxy request | ||||
|     return get_book_cover_internal(book, resolution=resolution) | ||||
|  | ||||
|  | ||||
| def get_book_cover_internal(book, use_generic_cover_on_failure, resolution=None): | ||||
| def get_book_cover_internal(book, resolution=None): | ||||
|     if book and book.has_cover: | ||||
|  | ||||
|         # Send the book cover thumbnail if it exists in cache | ||||
| @@ -769,16 +768,16 @@ def get_book_cover_internal(book, use_generic_cover_on_failure, resolution=None) | ||||
|         if config.config_use_google_drive: | ||||
|             try: | ||||
|                 if not gd.is_gdrive_ready(): | ||||
|                     return get_cover_on_failure(use_generic_cover_on_failure) | ||||
|                     return get_cover_on_failure() | ||||
|                 path = gd.get_cover_via_gdrive(book.path) | ||||
|                 if path: | ||||
|                     return redirect(path) | ||||
|                 else: | ||||
|                     log.error('{}/cover.jpg not found on Google Drive'.format(book.path)) | ||||
|                     return get_cover_on_failure(use_generic_cover_on_failure) | ||||
|                     return get_cover_on_failure() | ||||
|             except Exception as ex: | ||||
|                 log.error_or_exception(ex) | ||||
|                 return get_cover_on_failure(use_generic_cover_on_failure) | ||||
|                 return get_cover_on_failure() | ||||
|  | ||||
|         # Send the book cover from the Calibre directory | ||||
|         else: | ||||
| @@ -786,9 +785,9 @@ def get_book_cover_internal(book, use_generic_cover_on_failure, resolution=None) | ||||
|             if os.path.isfile(os.path.join(cover_file_path, "cover.jpg")): | ||||
|                 return send_from_directory(cover_file_path, "cover.jpg") | ||||
|             else: | ||||
|                 return get_cover_on_failure(use_generic_cover_on_failure) | ||||
|                 return get_cover_on_failure() | ||||
|     else: | ||||
|         return get_cover_on_failure(use_generic_cover_on_failure) | ||||
|         return get_cover_on_failure() | ||||
|  | ||||
|  | ||||
| def get_book_cover_thumbnail(book, resolution): | ||||
| @@ -811,7 +810,7 @@ def get_series_thumbnail_on_failure(series_id, resolution): | ||||
|         .filter(db.Books.has_cover == 1) \ | ||||
|         .first() | ||||
|  | ||||
|     return get_book_cover_internal(book, use_generic_cover_on_failure=True, resolution=resolution) | ||||
|     return get_book_cover_internal(book, resolution=resolution) | ||||
|  | ||||
|  | ||||
| def get_series_cover_thumbnail(series_id, resolution=None): | ||||
|   | ||||
							
								
								
									
										34
									
								
								cps/kobo.py
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								cps/kobo.py
									
									
									
									
									
								
							| @@ -930,20 +930,26 @@ def get_current_bookmark_response(current_bookmark): | ||||
| @kobo.route("/<book_uuid>/<width>/<height>/<Quality>/<isGreyscale>/image.jpg") | ||||
| @requires_kobo_auth | ||||
| def HandleCoverImageRequest(book_uuid, width, height, Quality, isGreyscale): | ||||
|     book_cover = helper.get_book_cover_with_uuid(book_uuid, resolution=COVER_THUMBNAIL_SMALL) | ||||
|     if not book_cover: | ||||
|         if config.config_kobo_proxy: | ||||
|             log.debug("Cover for unknown book: %s proxied to kobo" % book_uuid) | ||||
|             return redirect(KOBO_IMAGEHOST_URL + | ||||
|                             "/{book_uuid}/{width}/{height}/false/image.jpg".format(book_uuid=book_uuid, | ||||
|                                                                                    width=width, | ||||
|                                                                                    height=height), 307) | ||||
|         else: | ||||
|             log.debug("Cover for unknown book: %s requested" % book_uuid) | ||||
|             # additional proxy request make no sense, -> direct return | ||||
|             return make_response(jsonify({})) | ||||
|     log.debug("Cover request received for book %s" % book_uuid) | ||||
|     return book_cover | ||||
|     try: | ||||
|         resolution = None if int(height) > 1000 else COVER_THUMBNAIL_SMALL | ||||
|     except ValueError: | ||||
|         log.error("Requested height %s of book %s is invalid" % (book_uuid, height)) | ||||
|         resolution = COVER_THUMBNAIL_SMALL | ||||
|     book_cover = helper.get_book_cover_with_uuid(book_uuid, resolution=resolution) | ||||
|     if book_cover: | ||||
|         log.debug("Serving local cover image of book %s" % book_uuid) | ||||
|         return book_cover | ||||
|  | ||||
|     if not config.config_kobo_proxy: | ||||
|         log.debug("Returning 404 for cover image of unknown book %s" % book_uuid) | ||||
|         # additional proxy request make no sense, -> direct return | ||||
|         return abort(404) | ||||
|  | ||||
|     log.debug("Redirecting request for cover image of unknown book %s to Kobo" % book_uuid) | ||||
|     return redirect(KOBO_IMAGEHOST_URL + | ||||
|                     "/{book_uuid}/{width}/{height}/false/image.jpg".format(book_uuid=book_uuid, | ||||
|                                                                             width=width, | ||||
|                                                                             height=height), 307) | ||||
|  | ||||
|  | ||||
| @kobo.route("") | ||||
|   | ||||
| @@ -98,7 +98,7 @@ class Amazon(Metadata): | ||||
|                     try: | ||||
|                         match.authors = [next( | ||||
|                             filter(lambda i: i != " " and i != "\n" and not i.startswith("{"), | ||||
|                                    x.findAll(text=True))).strip() | ||||
|                                    x.findAll(string=True))).strip() | ||||
|                                         for x in soup2.findAll("span", attrs={"class": "author"})] | ||||
|                     except (AttributeError, TypeError, StopIteration): | ||||
|                         match.authors = "" | ||||
|   | ||||
| @@ -102,7 +102,7 @@ class LubimyCzytac(Metadata): | ||||
|     PUBLISH_DATE = "//dt[contains(@title,'Data pierwszego wydania" | ||||
|     FIRST_PUBLISH_DATE = f"{DETAILS}{PUBLISH_DATE} oryginalnego')]{SIBLINGS}[1]/text()" | ||||
|     FIRST_PUBLISH_DATE_PL = f"{DETAILS}{PUBLISH_DATE} polskiego')]{SIBLINGS}[1]/text()" | ||||
|     TAGS = "//nav[@aria-label='breadcrumb']//a[contains(@href,'/ksiazki/k/')]/text()" | ||||
|     TAGS = "//nav[@aria-label='breadcrumbs']//a[contains(@href,'/ksiazki/k/')]/span/text()" | ||||
|  | ||||
|     RATING = "//meta[@property='books:rating:value']/@content" | ||||
|     COVER = "//meta[@property='og:image']/@content" | ||||
|   | ||||
| @@ -7279,6 +7279,11 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. | ||||
|         float: right | ||||
|     } | ||||
|  | ||||
|     body.blur #main-nav + #scnd-nav .create-shelf, body.blur #main-nav + .col-sm-2 #scnd-nav .create-shelf { | ||||
|       float: none; | ||||
|       margin: 5px 0 10px -10px; | ||||
|     } | ||||
|  | ||||
|     #main-nav + #scnd-nav .nav-head.hidden-xs { | ||||
|         display: list-item !important; | ||||
|         width: 225px | ||||
|   | ||||
| @@ -40,6 +40,7 @@ $(".sendbtn-form").click(function() { | ||||
|     $.ajax({ | ||||
|         method: 'post', | ||||
|         url: $(this).data('href'), | ||||
|         data: {csrf_token: $("input[name='csrf_token']").val()}, | ||||
|         success: function (data) { | ||||
|             handleResponse(data) | ||||
|         } | ||||
|   | ||||
| @@ -71,6 +71,7 @@ var settings = { | ||||
|     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 | ||||
| }; | ||||
| @@ -131,7 +132,7 @@ var createURLFromArray = function(array, mimeType) { | ||||
|     } | ||||
|  | ||||
|     if ((typeof URL !== "function" && typeof URL !== "object") || | ||||
|       typeof URL.createObjectURL !== "function") { | ||||
|         typeof URL.createObjectURL !== "function") { | ||||
|         throw "Browser support for Object URLs is missing"; | ||||
|     } | ||||
|  | ||||
| @@ -206,7 +207,7 @@ function initProgressClick() { | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function loadFromArrayBuffer(ab) { | ||||
| function loadFromArrayBuffer(ab, lastCompletion = 0) { | ||||
|     const collator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' }); | ||||
|     loadArchiveFormats(['rar', 'zip', 'tar'], function() { | ||||
|         // Open the file as an archive | ||||
| @@ -246,7 +247,7 @@ function loadFromArrayBuffer(ab) { | ||||
|                                         } else { | ||||
|                                             $("#left").show(); | ||||
|                                         } | ||||
|                                         updatePage(); | ||||
|                                         updatePage(lastCompletion); | ||||
|                                     } | ||||
|                                 } else { | ||||
|                                     totalImages--; | ||||
| @@ -261,17 +262,6 @@ function loadFromArrayBuffer(ab) { | ||||
| } | ||||
|  | ||||
| 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 | ||||
| @@ -279,12 +269,31 @@ function scrollTocToActive() { | ||||
| } | ||||
|  | ||||
| function updatePage() { | ||||
|     $(".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(); | ||||
|     scrollCurrentImageIntoView(); | ||||
|     updateProgress(); | ||||
|     pageDisplayUpdate(); | ||||
|     setTheme(); | ||||
|  | ||||
|     if (imageFiles[currentImage]) { | ||||
|         setImage(imageFiles[currentImage].dataURI); | ||||
|     } else { | ||||
|         setImage("loading"); | ||||
|     } | ||||
|  | ||||
|     $("body").toggleClass("dark-theme", settings.theme === "dark"); | ||||
|     $("#mainContent").toggleClass("disabled-scrollbar", settings.scrollbar === 0); | ||||
|  | ||||
|     kthoom.setSettings(); | ||||
|     kthoom.saveSettings(); | ||||
| } | ||||
| @@ -359,6 +368,7 @@ function setImage(url, _canvas) { | ||||
|         img.onerror = function() { | ||||
|             canvas.width = innerWidth - 100; | ||||
|             canvas.height = 300; | ||||
|             updateScale(true);             | ||||
|             x.fillStyle = "black"; | ||||
|             x.font = "50px sans-serif"; | ||||
|             x.strokeStyle = "black"; | ||||
| @@ -412,6 +422,8 @@ function setImage(url, _canvas) { | ||||
|             scrollTo(0, 0); | ||||
|             x.drawImage(img, 0, 0); | ||||
|  | ||||
|             updateScale(false); | ||||
|  | ||||
|             canvas.style.display = ""; | ||||
|             $("body").css("overflowY", ""); | ||||
|             x.restore(); | ||||
| @@ -450,6 +462,9 @@ function showPrevPage() { | ||||
|         currentImage++; | ||||
|     } else { | ||||
|         updatePage(); | ||||
|         if (settings.nextPage === 0) { | ||||
|             $("#mainContent").scrollTop(0); | ||||
|         } | ||||
|     } | ||||
|     updateDirectionButtons(); | ||||
| } | ||||
| @@ -461,6 +476,9 @@ function showNextPage() { | ||||
|         currentImage--; | ||||
|     } else { | ||||
|         updatePage(); | ||||
|         if (settings.nextPage === 0) { | ||||
|             $("#mainContent").scrollTop(0); | ||||
|         } | ||||
|     } | ||||
|     updateDirectionButtons(); | ||||
| } | ||||
| @@ -477,7 +495,7 @@ function scrollCurrentImageIntoView() { | ||||
|     } | ||||
| } | ||||
|  | ||||
| function updateScale() { | ||||
| function updateScale(clear) { | ||||
|     var canvasArray = $("#mainContent > canvas"); | ||||
|     var maxheight = innerHeight - 50; | ||||
|      | ||||
| @@ -486,7 +504,7 @@ function updateScale() { | ||||
|     canvasArray.css("maxWidth", ""); | ||||
|     canvasArray.css("maxHeight", ""); | ||||
|  | ||||
|     if(settings.pageDisplay === 0) { | ||||
|     if(!clear) { | ||||
|         canvasArray.addClass("hide"); | ||||
|         pageDisplayUpdate(); | ||||
|     } | ||||
| @@ -653,7 +671,7 @@ function init(filename) { | ||||
|     request.responseType = "arraybuffer"; | ||||
|     request.addEventListener("load", function() { | ||||
|         if (request.status >= 200 && request.status < 300) { | ||||
|             loadFromArrayBuffer(request.response); | ||||
|             loadFromArrayBuffer(request.response, currentImage); | ||||
|         } else { | ||||
|             console.warn(request.statusText, request.responseText); | ||||
|         } | ||||
| @@ -708,7 +726,7 @@ function init(filename) { | ||||
|         } | ||||
|          | ||||
|         updatePage(); | ||||
|         updateScale(); | ||||
|         updateScale(false); | ||||
|     }); | ||||
|  | ||||
|     // Close modal | ||||
| @@ -721,6 +739,9 @@ function init(filename) { | ||||
|     $("#thumbnails").on("click", "a", function() { | ||||
|         currentImage = $(this).data("page") - 1; | ||||
|         updatePage(); | ||||
|         if (settings.nextPage === 0) { | ||||
|             $("#mainContent").scrollTop(0); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     // Fullscreen mode | ||||
|   | ||||
| @@ -43,30 +43,30 @@ | ||||
|                                 {% endif %} | ||||
|                             </div> | ||||
|                         {% endif %} | ||||
|                     {% endif %} | ||||
|                     {% if current_user.kindle_mail and entry.email_share_list %} | ||||
|                         <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> | ||||
|                         {% if entry.email_share_list.__len__() == 1 %} | ||||
|                             <div class="btn-group" role="group"> | ||||
|                                 <button id="sendbtn" class="btn btn-primary sendbtn-form" data-href="{{url_for('web.send_to_ereader', book_id=entry.id, book_format=entry.email_share_list[0]['format'], convert=entry.email_share_list[0]['convert'])}}"> | ||||
|                                     <span class="glyphicon glyphicon-send"></span> {{entry.email_share_list[0]['text']}} | ||||
|                                 </button> | ||||
|                             </div> | ||||
|                         {% else %} | ||||
|                             <div class="btn-group" role="group"> | ||||
|                                 <button id="sendbtn2" type="button" class="btn btn-primary dropdown-toggle" | ||||
|                                         data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> | ||||
|                                     <span class="glyphicon glyphicon-send"></span>{{ _('Send to eReader') }} | ||||
|                                     <span class="caret"></span> | ||||
|                                 </button> | ||||
|                                 <ul class="dropdown-menu" aria-labelledby="send-to-ereader"> | ||||
|                                     {% for format in entry.email_share_list %} | ||||
|                                         <li> | ||||
|                                             <a class="sendbtn-form" data-href="{{url_for('web.send_to_ereader', book_id=entry.id, book_format=format['format'], convert=format['convert'])}}">{{ format['text'] }}</a> | ||||
|                                         </li> | ||||
|                                     {% endfor %} | ||||
|                                 </ul> | ||||
|                             </div> | ||||
|                         {% if current_user.kindle_mail and entry.email_share_list %} | ||||
|                             <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> | ||||
|                             {% if entry.email_share_list.__len__() == 1 %} | ||||
|                                 <div class="btn-group" role="group"> | ||||
|                                     <button id="sendbtn" class="btn btn-primary sendbtn-form" data-href="{{url_for('web.send_to_ereader', book_id=entry.id, book_format=entry.email_share_list[0]['format'], convert=entry.email_share_list[0]['convert'])}}"> | ||||
|                                         <span class="glyphicon glyphicon-send"></span> {{entry.email_share_list[0]['text']}} | ||||
|                                     </button> | ||||
|                                 </div> | ||||
|                             {% else %} | ||||
|                                 <div class="btn-group" role="group"> | ||||
|                                     <button id="sendbtn2" type="button" class="btn btn-primary dropdown-toggle" | ||||
|                                             data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> | ||||
|                                         <span class="glyphicon glyphicon-send"></span>{{ _('Send to eReader') }} | ||||
|                                         <span class="caret"></span> | ||||
|                                     </button> | ||||
|                                     <ul class="dropdown-menu" aria-labelledby="send-to-ereader"> | ||||
|                                         {% for format in entry.email_share_list %} | ||||
|                                             <li> | ||||
|                                                 <a class="sendbtn-form" data-href="{{url_for('web.send_to_ereader', book_id=entry.id, book_format=format['format'], convert=format['convert'])}}">{{ format['text'] }}</a> | ||||
|                                             </li> | ||||
|                                         {% endfor %} | ||||
|                                     </ul> | ||||
|                                 </div> | ||||
|                             {% endif %} | ||||
|                         {% endif %} | ||||
|                     {% endif %} | ||||
|                     {% if entry.reader_list and current_user.role_viewer() %} | ||||
|   | ||||
| @@ -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" style="display:none" onclick="showLeftPage()">‹</div> | ||||
|   <div id="right" class="arrow" style="display:none" onclick="showRightPage()">›</div> | ||||
|   <div id="left" class="arrow" style="display:none" onclick="showLeftPage(); setBookmark();">‹</div> | ||||
|   <div id="right" class="arrow" style="display:none" onclick="showRightPage(); setBookmark();">›</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,118 @@ | ||||
|                 <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> | ||||
|     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("←"); | ||||
|       } | ||||
|     }; | ||||
|   </script> | ||||
|   <script> | ||||
|     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); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     window.calibre = { | ||||
|       filePath: "{{ url_for('static', filename='js/libs/') }}", | ||||
|       cssPath: "{{ url_for('static', filename='css/') }}", | ||||
|       bookUrl: "{{ url_for('static', filename=comicfile) }}/", | ||||
|       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 }}" | ||||
|     }; | ||||
|     if (calibre.useBookmarks) { | ||||
|       currentImage = eval(calibre.bookmark); | ||||
|       if (typeof currentImage !== 'number'){ | ||||
|         currentImage = 0; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     document.onreadystatechange = function () { | ||||
|       if (document.readyState == "complete") { | ||||
|         init("{{ url_for('web.serve_book', book_id=comicfile, book_format=extension) }}"); | ||||
|         updateArrows(); | ||||
|       } | ||||
|     } | ||||
|   </script> | ||||
|   <script> | ||||
|     $('input[name="direction"]').change(function() { | ||||
|         updateArrows(); | ||||
|       }); | ||||
|     </script> | ||||
| </body> | ||||
| </html> | ||||
|   | ||||
| @@ -88,7 +88,7 @@ def process(tmp_file_path, original_file_name, original_file_extension, rar_exec | ||||
|         log.warning('cannot parse metadata, using default: %s', ex) | ||||
|  | ||||
|     if not meta.title.strip(): | ||||
|         meta = original_file_name | ||||
|         meta = meta._replace(title=original_file_name) | ||||
|     if not meta.author.strip() or meta.author.lower() == 'unknown': | ||||
|         meta = meta._replace(author=_('Unknown')) | ||||
|     return meta | ||||
|   | ||||
| @@ -1561,7 +1561,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
	 Ozzie Isaacs
					Ozzie Isaacs