From 1ce1aee35e40b5b48cd993eaf10acb0f34c09c3b Mon Sep 17 00:00:00 2001 From: IgorKurkov Date: Sun, 26 Oct 2025 18:23:07 +0100 Subject: [PATCH 1/9] fix(js reader): adjust height to dynamic viewport height, add amber theme --- .gitignore | 2 + cps/static/css/epub_themes.css | 23 +- cps/static/css/main.css | 243 +++++++++++------ cps/templates/read.html | 462 ++++++++++++++++++++------------- 4 files changed, 462 insertions(+), 268 deletions(-) diff --git a/.gitignore b/.gitignore index ff08f874b..f3e0ae8b7 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ gdrive_credentials client_secrets.json gmail.json /.key +calibre-volume +calibre-web-env diff --git a/cps/static/css/epub_themes.css b/cps/static/css/epub_themes.css index b9cbcedbe..0a90a07fe 100644 --- a/cps/static/css/epub_themes.css +++ b/cps/static/css/epub_themes.css @@ -1,19 +1,24 @@ .lightTheme { - background: #fff; - color: #000; + background: #fff; + color: #000; } .darkTheme { - background: #202124; - color: #fff + background: #202124; + color: #fff; } .sepiaTheme { - background: #ece1ca; - color: #000; + background: #ece1ca; + color: #000; +} + +.amberTheme { + background: #e8bf5e; + color: #000; } .blackTheme { - background: #000; - color: #fff; -} \ No newline at end of file + background: #000; + color: #fff; +} diff --git a/cps/static/css/main.css b/cps/static/css/main.css index 571f546c9..fcb00eed6 100644 --- a/cps/static/css/main.css +++ b/cps/static/css/main.css @@ -1,11 +1,10 @@ @font-face { - font-family: 'fontello'; - src: url('fonts/fontello.eot?60518104'); - src: - url('fonts/fontello.eot?60518104#iefix') format('embedded-opentype'), - url('fonts/fontello.woff?60518104') format('woff'), - url('fonts/fontello.ttf?60518104') format('truetype'), - url('fonts/fontello.svg?60518104#fontello') format('svg'); + font-family: "fontello"; + src: url("fonts/fontello.eot?60518104"); + src: url("fonts/fontello.eot?60518104#iefix") format("embedded-opentype"), + url("fonts/fontello.woff?60518104") format("woff"), + url("fonts/fontello.ttf?60518104") format("truetype"), + url("fonts/fontello.svg?60518104#fontello") format("svg"); font-weight: normal; font-style: normal; } @@ -16,7 +15,7 @@ body { } .myselect { - overflow: visible !important; + overflow: visible !important; } #main { @@ -48,7 +47,7 @@ body { -webkit-transition: opacity 0.5s; -moz-transition: opacity 0.5s; transition: opacity 0.5s; - z-index: 10; + z-index: 100; } #titlebar:hover { @@ -93,14 +92,18 @@ body { box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8); } -#book-title { font-weight: 600; } -#title-seperator { display: none; } +#book-title { + font-weight: 600; +} +#title-seperator { + display: none; +} #viewer { width: 80%; height: 80%; margin: 0 auto; - z-index: 2; + z-index: 0; position: relative; overflow: hidden; } @@ -136,6 +139,7 @@ body { user-select: none; padding-top: 160px; padding-bottom: 160px; + z-index: 10; } .arrow:hover { @@ -200,10 +204,16 @@ body { max-width: 80%; } -#title-controls { float: right; } +#title-controls { + float: right; +} -#panels a::before { visibility: visible; } -#panels a:hover { color: #aaa; } +#panels a::before { + visibility: visible; +} +#panels a:hover { + color: #aaa; +} .list_item.currentChapter > a, .list_item a:hover { @@ -237,8 +247,12 @@ body { margin-top: -1px; } -input::-webkit-input-placeholder { color: #454545; } -input:-moz-placeholder { color: #454545; } +input::-webkit-input-placeholder { + color: #454545; +} +input:-moz-placeholder { + color: #454545; +} #divider { position: absolute; @@ -378,14 +392,14 @@ input:-moz-placeholder { color: #454545; } } .toc_toggle::before { - content: '▸'; + content: "▸"; color: #fff; margin-right: -4px; } .currentChapter > .toc_toggle::before, .openChapter > .toc_toggle::before { - content: '▾'; + content: "▾"; } .view { @@ -499,13 +513,25 @@ input:-moz-placeholder { color: #454545; } list-style-type: none; } -#settingsPanel .xsmall { font-size: x-small; } -#settingsPanel .small { font-size: small; } -#settingsPanel .medium { font-size: medium; } -#settingsPanel .large { font-size: large; } -#settingsPanel .xlarge { font-size: x-large; } +#settingsPanel .xsmall { + font-size: x-small; +} +#settingsPanel .small { + font-size: small; +} +#settingsPanel .medium { + font-size: medium; +} +#settingsPanel .large { + font-size: large; +} +#settingsPanel .xlarge { + font-size: x-large; +} -.highlight { background-color: yellow; } +.highlight { + background-color: yellow; +} .modal { position: fixed; @@ -581,6 +607,7 @@ input:-moz-placeholder { color: #454545; } color: white; } +.md-content .themes button.lightTheme, .md-content .themes button.whiteTheme { background-color: white; color: black; @@ -591,6 +618,11 @@ input:-moz-placeholder { color: #454545; } color: black; } +.md-content .themes button.amberTheme { + background-color: #e8bf5e; + color: black; +} + .md-content .themes button.blackTheme { background-color: black; color: white; @@ -638,31 +670,47 @@ input:-moz-placeholder { color: #454545; } @media only screen and (max-width: 900px) { #viewer { - width: 60%; - margin-left: 20%; + width: 100%; + height: calc(100dvh - 99px); + margin-left: 0; + } + + #viewer iframe { + width: 100%; + height: 100%; } #prev { left: 20px; + z-index: 10; } #next { right: 20px; + z-index: 10; } } @media only screen and (max-width: 550px) { #viewer { - width: 80%; - margin-left: 10%; + width: 100%; + height: calc(100dvh - 99px); + margin-left: 0; + } + + #viewer iframe { + width: 100%; + height: 100%; } #prev { left: 0; + z-index: 10; } #next { right: 0; + z-index: 10; } .arrow { @@ -712,11 +760,7 @@ input:-moz-placeholder { color: #454545; } } } -@media only screen - and (min-device-width: 768px) - and (max-device-width: 1024px) - and (orientation: landscape) - /* and (-webkit-min-device-pixel-ratio: 2)*/ { +@media only screen and (min-device-width: 768px) and (max-device-width: 1024px) and (orientation: landscape) /* and (-webkit-min-device-pixel-ratio: 2) */ { #viewer { width: 80%; margin-left: 10%; @@ -739,47 +783,52 @@ input:-moz-placeholder { color: #454545; } /* For iPhone 6\6s portrait layouts only */ @media only screen and (min-device-width: 375px) and (max-device-width: 667px) and (orientation: portrait) { #viewer { - width: 300px; - height: 480px; + width: 100%; + height: calc(100dvh - 99px); } #viewer iframe { - width: 300px; - height: 480px; + width: 100%; + height: 100%; } } /* For iPhone 6\6s landscape layouts only */ @media only screen and (min-device-width: 375px) and (max-device-width: 667px) and (orientation: landscape) { #viewer { - width: 450px; - height: 300px; + width: 100%; + height: calc(100dvh - 99px); } #viewer iframe { - width: 450px; - height: 300px; + width: 100%; + height: 100%; } } /* For iPhone portrait layouts only */ @media only screen and (max-device-width: 374px) and (orientation: portrait) { #viewer { - width: 256px; - height: 432px; + width: 100%; + height: calc(100dvh - 99px); } #viewer iframe { - width: 256px; - height: 432px; + width: 100%; + height: 100%; } } /* For iPhone landscape layouts only */ @media only screen and (max-device-width: 374px) and (orientation: landscape) { + #viewer { + width: 100%; + height: calc(100dvh - 99px); + } + #viewer iframe { - width: 256px; - height: 124px; + width: 100%; + height: 100%; } } @@ -803,32 +852,76 @@ input:-moz-placeholder { color: #454545; } font-size: 112%; } -.icon-search::before { content: '\e807'; } /* '' */ -.icon-resize-full-1::before { content: '\e804'; } /* '' */ -.icon-cancel-circled2::before { content: '\e80f'; } /* '' */ -.icon-link::before { content: '\e80d'; } /* '' */ -.icon-bookmark::before { content: '\e805'; } /* '' */ -.icon-bookmark-empty::before { content: '\e806'; } /* '' */ -.icon-download-cloud::before { content: '\e811'; } /* '' */ -.icon-edit::before { content: '\e814'; } /* '' */ -.icon-menu::before { content: '\e802'; } /* '' */ -.icon-cog::before { content: '\e813'; } /* '' */ -.icon-resize-full::before { content: '\e812'; } /* '' */ -.icon-cancel-circled::before { content: '\e80e'; } /* '' */ -.icon-up-dir::before { content: '\e80c'; } /* '' */ -.icon-right-dir::before { content: '\e80b'; } /* '' */ -.icon-angle-right::before { content: '\e809'; } /* '' */ -.icon-angle-down::before { content: '\e80a'; } /* '' */ -.icon-right::before { content: '\e815'; } /* '' */ -.icon-list-1::before { content: '\e803'; } /* '' */ -.icon-list-numbered::before { content: '\e801'; } /* '' */ -.icon-columns::before { content: '\e810'; } /* '' */ -.icon-list::before { content: '\e800'; } /* '' */ -.icon-resize-small::before { content: '\e808'; } /* '' */ +.icon-search::before { + content: "\e807"; +} /* '' */ +.icon-resize-full-1::before { + content: "\e804"; +} /* '' */ +.icon-cancel-circled2::before { + content: "\e80f"; +} /* '' */ +.icon-link::before { + content: "\e80d"; +} /* '' */ +.icon-bookmark::before { + content: "\e805"; +} /* '' */ +.icon-bookmark-empty::before { + content: "\e806"; +} /* '' */ +.icon-download-cloud::before { + content: "\e811"; +} /* '' */ +.icon-edit::before { + content: "\e814"; +} /* '' */ +.icon-menu::before { + content: "\e802"; +} /* '' */ +.icon-cog::before { + content: "\e813"; +} /* '' */ +.icon-resize-full::before { + content: "\e812"; +} /* '' */ +.icon-cancel-circled::before { + content: "\e80e"; +} /* '' */ +.icon-up-dir::before { + content: "\e80c"; +} /* '' */ +.icon-right-dir::before { + content: "\e80b"; +} /* '' */ +.icon-angle-right::before { + content: "\e809"; +} /* '' */ +.icon-angle-down::before { + content: "\e80a"; +} /* '' */ +.icon-right::before { + content: "\e815"; +} /* '' */ +.icon-list-1::before { + content: "\e803"; +} /* '' */ +.icon-list-numbered::before { + content: "\e801"; +} /* '' */ +.icon-columns::before { + content: "\e810"; +} /* '' */ +.icon-list::before { + content: "\e800"; +} /* '' */ +.icon-resize-small::before { + content: "\e808"; +} /* '' */ -#progress{ - right: 4rem; - bottom: 4rem; - width: fit-content; - position: absolute; +#progress { + right: 4rem; + bottom: 2px; + width: fit-content; + position: absolute; } diff --git a/cps/templates/read.html b/cps/templates/read.html index 9c6e4bcac..7166d86a6 100644 --- a/cps/templates/read.html +++ b/cps/templates/read.html @@ -1,46 +1,72 @@ - - - - + + + {{_('epub Reader')}} | {{title}} - - - + + + {% if g.google_site_verification|length > 0 %} - + {% endif %} - - + + - - - - - + + + + + - +
- -
-
- Menu -
-
- -   –   - -
- -
- -
- -
- -
0%
-
-
-
-
- - + +
+ + 100% +
@@ -273,13 +297,40 @@ } // font size settings logic - let fontSizeFader = document.getElementById("fontSizeFader"); + let currentFontSize = 100; // default 100% + const minFontSize = 50; + const maxFontSize = 300; + const stepSize = 5; - // Save font size when changed - fontSizeFader.addEventListener("change", function () { - const fontSize = this.value; - localStorage.setItem("calibre.reader.fontSize", fontSize); - reader.rendition.themes.fontSize(`${fontSize}%`); + const fontSizeDisplay = document.getElementById("fontSizeDisplay"); + const fontSizeDecrease = document.getElementById("fontSizeDecrease"); + const fontSizeIncrease = document.getElementById("fontSizeIncrease"); + + function updateFontSize(newSize) { + if (newSize < minFontSize) newSize = minFontSize; + if (newSize > maxFontSize) newSize = maxFontSize; + + currentFontSize = newSize; + fontSizeDisplay.textContent = newSize + "%"; + localStorage.setItem("calibre.reader.fontSize", newSize); + if (reader && reader.rendition) { + reader.rendition.themes.fontSize(`${newSize}%`); + } + } + + // Restore saved font size on load + const savedFontSize = localStorage.getItem("calibre.reader.fontSize"); + if (savedFontSize) { + currentFontSize = parseInt(savedFontSize); + fontSizeDisplay.textContent = savedFontSize + "%"; + } + + fontSizeDecrease.addEventListener("click", function () { + updateFontSize(currentFontSize - stepSize); + }); + + fontSizeIncrease.addEventListener("click", function () { + updateFontSize(currentFontSize + stepSize); }); let defaultFont; From 902368efa7b6c765fc4e0aeadc69bd23eeb37bb3 Mon Sep 17 00:00:00 2001 From: IgorKurkov Date: Sun, 26 Oct 2025 18:47:56 +0100 Subject: [PATCH 4/9] feat(epub.js): add saving for settings of the font family to localstorage --- cps/static/js/reading/epub.js | 7 ++++++- cps/templates/read.html | 7 +++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/cps/static/js/reading/epub.js b/cps/static/js/reading/epub.js index 21663a6ab..0a907ed2a 100644 --- a/cps/static/js/reading/epub.js +++ b/cps/static/js/reading/epub.js @@ -119,11 +119,16 @@ var reader; const theme = localStorage.getItem("calibre.reader.theme") ?? "lightTheme"; selectTheme(theme); - // Restore saved font size after reader is ready + // Restore saved font and font size after reader is ready reader.book.ready.then(() => { const savedFontSize = localStorage.getItem("calibre.reader.fontSize"); if (savedFontSize) { reader.rendition.themes.fontSize(`${savedFontSize}%`); } + + const savedFont = localStorage.getItem("calibre.reader.font"); + if (savedFont && window.selectFont) { + window.selectFont(savedFont); + } }); })(); diff --git a/cps/templates/read.html b/cps/templates/read.html index f31b97141..6262f403b 100644 --- a/cps/templates/read.html +++ b/cps/templates/read.html @@ -335,7 +335,7 @@ let defaultFont; - function selectFont(id) { + window.selectFont = function (id) { if (!defaultFont) { defaultFont = reader.rendition.getContents()[0]?.css("font-family"); } @@ -346,12 +346,15 @@ } document.getElementById(id).querySelector("span").textContent = "✓"; + // Save font selection to localStorage + localStorage.setItem("calibre.reader.font", id); + if (id == "default") { reader.rendition.themes.font(defaultFont); return; } reader.rendition.themes.font(id); - } + }; function spread(id) { spans = document.getElementById("layout").querySelectorAll("span"); From d8fbf967b364532bae5c3c4f5f2c68f782bbc04e Mon Sep 17 00:00:00 2001 From: IgorKurkov Date: Sun, 26 Oct 2025 21:28:58 +0100 Subject: [PATCH 5/9] add console process interruption by ctrl C --- cps.py | 20 ++++++++++++++++++-- cps/server.py | 12 ++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/cps.py b/cps.py index 039c2a5e1..97863504f 100755 --- a/cps.py +++ b/cps.py @@ -42,9 +42,25 @@ def hide_console_windows(): if __name__ == '__main__': - if os.name == "nt": + # Only hide the console window on Windows when there is no interactive + # console attached. This preserves the ability to use Ctrl+C from a + # terminal (stdin is a tty) to stop the server. + if os.name == "nt" and not sys.stdin.isatty(): hide_console_windows() - main() + + try: + main() + except KeyboardInterrupt: + # Allow stopping the server with Ctrl+C from an interactive console. + try: + # Try to perform a graceful stop if web_server is available. + # Import here to avoid import-time side-effects when not needed. + from cps import web_server + web_server.stop() + except Exception: + pass + print('\nCalibre-Web: received interrupt, shutting down') + sys.exit(0) diff --git a/cps/server.py b/cps/server.py index dca407a7e..bf53a3d46 100644 --- a/cps/server.py +++ b/cps/server.py @@ -216,6 +216,12 @@ class WebServer(object): try: sock, output = self._make_gevent_listener() log.info('Starting Gevent server on %s', output) + # Also print to stdout so interactive terminals show a clear success message + try: + print(f"Calibre-Web: server started on {output}") + except Exception: + print(f"Calibre-Web: error {output}") + pass self.wsgiserver = WSGIServer(sock, self.app, log=self.access_logger, handler_class=MyWSGIHandler, error_log=log, spawn=Pool(), **ssl_args) @@ -266,6 +272,12 @@ class WebServer(object): output = _readable_listen_address(self.listen_address, self.listen_port) http_server.listen(self.listen_port, self.listen_address) log.info('Starting Tornado server on %s', output) + # Also print to stdout so interactive terminals show a clear success message + try: + print(f"Calibre-Web: server started on {output}") + except Exception: + print(f"Calibre-Web: error {output}") + pass self.wsgiserver = IOLoop.current() self.wsgiserver.start() From 560e8e132d0db9cc1d622ccba2c313c36e39bcc4 Mon Sep 17 00:00:00 2001 From: IgorKurkov Date: Wed, 29 Oct 2025 20:37:15 +0100 Subject: [PATCH 6/9] add swipes by gestures in settings as an option, hammer.js for mobile --- cps/static/css/reader.css | 75 +- cps/static/js/libs/hammer.min.js | 1255 ++++++++++++++++++++++++++++++ cps/static/js/reading/epub.js | 129 ++- cps/templates/read.html | 30 + 4 files changed, 1430 insertions(+), 59 deletions(-) create mode 100644 cps/static/js/libs/hammer.min.js diff --git a/cps/static/css/reader.css b/cps/static/css/reader.css index 7229a6e3b..ac943460f 100644 --- a/cps/static/css/reader.css +++ b/cps/static/css/reader.css @@ -1,47 +1,52 @@ .fontSizeWrapper { - position: relative; + position: relative; } .slider { - position: absolute; - top: 50%; - transform: translate(0,-50%); - width: 90%; - height: 60px; - background: transparent; - border-radius: 20px; - display: flex; - align-items: center; - box-shadow: 0px 15px 40px #7E6D5766; + position: absolute; + top: 50%; + transform: translate(0, -50%); + width: 90%; + height: 60px; + background: transparent; + border-radius: 20px; + display: flex; + align-items: center; + box-shadow: 0px 15px 40px #7e6d5766; } .slider label { - font-size: 20px; - font-weight: 400; - font-family: Open Sans; - padding-right: 10px; - color: white; + font-size: 20px; + font-weight: 400; + font-family: Open Sans; + padding-right: 10px; + color: white; } .slider input[type="range"] { - width: 80%; - height: 5px; - background: black; - border: none; - outline: none; + width: 80%; + height: 5px; + background: black; + border: none; + outline: none; } .item { - font-size: 20px; - font-weight: 400; - font-family: Open Sans; - padding-right: 10px; - color: white; + font-size: 20px; + font-weight: 400; + font-family: Open Sans; + padding-right: 10px; + color: white; } -.item~button { - display: inline-block; - border: none; - text-align: center; - text-decoration: none; - margin-top: 5%; - margin-right: 1%; - font-size: 16px; -} \ No newline at end of file +.item ~ button { + display: inline-block; + border: none; + text-align: center; + text-decoration: none; + margin-top: 5%; + margin-right: 1%; + font-size: 16px; +} + +#main, +#viewer { + touch-action: pan-y; +} diff --git a/cps/static/js/libs/hammer.min.js b/cps/static/js/libs/hammer.min.js new file mode 100644 index 000000000..4e9b56f93 --- /dev/null +++ b/cps/static/js/libs/hammer.min.js @@ -0,0 +1,1255 @@ +/*! Hammer.JS - v2.0.8 - 2016-04-23 + * http://hammerjs.github.io/ + * + * Copyright (c) 2016 Jorik Tangelder; + * Licensed under the MIT license */ +!(function (a, b, c, d) { + "use strict"; + function e(a, b, c) { + return setTimeout(j(a, c), b); + } + function f(a, b, c) { + return Array.isArray(a) ? (g(a, c[b], c), !0) : !1; + } + function g(a, b, c) { + var e; + if (a) + if (a.forEach) a.forEach(b, c); + else if (a.length !== d) + for (e = 0; e < a.length; ) b.call(c, a[e], e, a), e++; + else for (e in a) a.hasOwnProperty(e) && b.call(c, a[e], e, a); + } + function h(b, c, d) { + var e = "DEPRECATED METHOD: " + c + "\n" + d + " AT \n"; + return function () { + var c = new Error("get-stack-trace"), + d = + c && c.stack + ? c.stack + .replace(/^[^\(]+?[\n$]/gm, "") + .replace(/^\s+at\s+/gm, "") + .replace( + /^Object.\s*\(/gm, + "{anonymous}()@" + ) + : "Unknown Stack Trace", + f = a.console && (a.console.warn || a.console.log); + return f && f.call(a.console, e, d), b.apply(this, arguments); + }; + } + function i(a, b, c) { + var d, + e = b.prototype; + (d = a.prototype = Object.create(e)), + (d.constructor = a), + (d._super = e), + c && la(d, c); + } + function j(a, b) { + return function () { + return a.apply(b, arguments); + }; + } + function k(a, b) { + return typeof a == oa ? a.apply(b ? b[0] || d : d, b) : a; + } + function l(a, b) { + return a === d ? b : a; + } + function m(a, b, c) { + g(q(b), function (b) { + a.addEventListener(b, c, !1); + }); + } + function n(a, b, c) { + g(q(b), function (b) { + a.removeEventListener(b, c, !1); + }); + } + function o(a, b) { + for (; a; ) { + if (a == b) return !0; + a = a.parentNode; + } + return !1; + } + function p(a, b) { + return a.indexOf(b) > -1; + } + function q(a) { + return a.trim().split(/\s+/g); + } + function r(a, b, c) { + if (a.indexOf && !c) return a.indexOf(b); + for (var d = 0; d < a.length; ) { + if ((c && a[d][c] == b) || (!c && a[d] === b)) return d; + d++; + } + return -1; + } + function s(a) { + return Array.prototype.slice.call(a, 0); + } + function t(a, b, c) { + for (var d = [], e = [], f = 0; f < a.length; ) { + var g = b ? a[f][b] : a[f]; + r(e, g) < 0 && d.push(a[f]), (e[f] = g), f++; + } + return ( + c && + (d = b + ? d.sort(function (a, c) { + return a[b] > c[b]; + }) + : d.sort()), + d + ); + } + function u(a, b) { + for ( + var c, e, f = b[0].toUpperCase() + b.slice(1), g = 0; + g < ma.length; + + ) { + if (((c = ma[g]), (e = c ? c + f : b), e in a)) return e; + g++; + } + return d; + } + function v() { + return ua++; + } + function w(b) { + var c = b.ownerDocument || b; + return c.defaultView || c.parentWindow || a; + } + function x(a, b) { + var c = this; + (this.manager = a), + (this.callback = b), + (this.element = a.element), + (this.target = a.options.inputTarget), + (this.domHandler = function (b) { + k(a.options.enable, [a]) && c.handler(b); + }), + this.init(); + } + function y(a) { + var b, + c = a.options.inputClass; + return new (b = c ? c : xa ? M : ya ? P : wa ? R : L)(a, z); + } + function z(a, b, c) { + var d = c.pointers.length, + e = c.changedPointers.length, + f = b & Ea && d - e === 0, + g = b & (Ga | Ha) && d - e === 0; + (c.isFirst = !!f), + (c.isFinal = !!g), + f && (a.session = {}), + (c.eventType = b), + A(a, c), + a.emit("hammer.input", c), + a.recognize(c), + (a.session.prevInput = c); + } + function A(a, b) { + var c = a.session, + d = b.pointers, + e = d.length; + c.firstInput || (c.firstInput = D(b)), + e > 1 && !c.firstMultiple + ? (c.firstMultiple = D(b)) + : 1 === e && (c.firstMultiple = !1); + var f = c.firstInput, + g = c.firstMultiple, + h = g ? g.center : f.center, + i = (b.center = E(d)); + (b.timeStamp = ra()), + (b.deltaTime = b.timeStamp - f.timeStamp), + (b.angle = I(h, i)), + (b.distance = H(h, i)), + B(c, b), + (b.offsetDirection = G(b.deltaX, b.deltaY)); + var j = F(b.deltaTime, b.deltaX, b.deltaY); + (b.overallVelocityX = j.x), + (b.overallVelocityY = j.y), + (b.overallVelocity = qa(j.x) > qa(j.y) ? j.x : j.y), + (b.scale = g ? K(g.pointers, d) : 1), + (b.rotation = g ? J(g.pointers, d) : 0), + (b.maxPointers = c.prevInput + ? b.pointers.length > c.prevInput.maxPointers + ? b.pointers.length + : c.prevInput.maxPointers + : b.pointers.length), + C(c, b); + var k = a.element; + o(b.srcEvent.target, k) && (k = b.srcEvent.target), (b.target = k); + } + function B(a, b) { + var c = b.center, + d = a.offsetDelta || {}, + e = a.prevDelta || {}, + f = a.prevInput || {}; + (b.eventType !== Ea && f.eventType !== Ga) || + ((e = a.prevDelta = { x: f.deltaX || 0, y: f.deltaY || 0 }), + (d = a.offsetDelta = { x: c.x, y: c.y })), + (b.deltaX = e.x + (c.x - d.x)), + (b.deltaY = e.y + (c.y - d.y)); + } + function C(a, b) { + var c, + e, + f, + g, + h = a.lastInterval || b, + i = b.timeStamp - h.timeStamp; + if (b.eventType != Ha && (i > Da || h.velocity === d)) { + var j = b.deltaX - h.deltaX, + k = b.deltaY - h.deltaY, + l = F(i, j, k); + (e = l.x), + (f = l.y), + (c = qa(l.x) > qa(l.y) ? l.x : l.y), + (g = G(j, k)), + (a.lastInterval = b); + } else + (c = h.velocity), + (e = h.velocityX), + (f = h.velocityY), + (g = h.direction); + (b.velocity = c), + (b.velocityX = e), + (b.velocityY = f), + (b.direction = g); + } + function D(a) { + for (var b = [], c = 0; c < a.pointers.length; ) + (b[c] = { + clientX: pa(a.pointers[c].clientX), + clientY: pa(a.pointers[c].clientY), + }), + c++; + return { + timeStamp: ra(), + pointers: b, + center: E(b), + deltaX: a.deltaX, + deltaY: a.deltaY, + }; + } + function E(a) { + var b = a.length; + if (1 === b) return { x: pa(a[0].clientX), y: pa(a[0].clientY) }; + for (var c = 0, d = 0, e = 0; b > e; ) + (c += a[e].clientX), (d += a[e].clientY), e++; + return { x: pa(c / b), y: pa(d / b) }; + } + function F(a, b, c) { + return { x: b / a || 0, y: c / a || 0 }; + } + function G(a, b) { + return a === b + ? Ia + : qa(a) >= qa(b) + ? 0 > a + ? Ja + : Ka + : 0 > b + ? La + : Ma; + } + function H(a, b, c) { + c || (c = Qa); + var d = b[c[0]] - a[c[0]], + e = b[c[1]] - a[c[1]]; + return Math.sqrt(d * d + e * e); + } + function I(a, b, c) { + c || (c = Qa); + var d = b[c[0]] - a[c[0]], + e = b[c[1]] - a[c[1]]; + return (180 * Math.atan2(e, d)) / Math.PI; + } + function J(a, b) { + return I(b[1], b[0], Ra) + I(a[1], a[0], Ra); + } + function K(a, b) { + return H(b[0], b[1], Ra) / H(a[0], a[1], Ra); + } + function L() { + (this.evEl = Ta), + (this.evWin = Ua), + (this.pressed = !1), + x.apply(this, arguments); + } + function M() { + (this.evEl = Xa), + (this.evWin = Ya), + x.apply(this, arguments), + (this.store = this.manager.session.pointerEvents = []); + } + function N() { + (this.evTarget = $a), + (this.evWin = _a), + (this.started = !1), + x.apply(this, arguments); + } + function O(a, b) { + var c = s(a.touches), + d = s(a.changedTouches); + return b & (Ga | Ha) && (c = t(c.concat(d), "identifier", !0)), [c, d]; + } + function P() { + (this.evTarget = bb), (this.targetIds = {}), x.apply(this, arguments); + } + function Q(a, b) { + var c = s(a.touches), + d = this.targetIds; + if (b & (Ea | Fa) && 1 === c.length) + return (d[c[0].identifier] = !0), [c, c]; + var e, + f, + g = s(a.changedTouches), + h = [], + i = this.target; + if ( + ((f = c.filter(function (a) { + return o(a.target, i); + })), + b === Ea) + ) + for (e = 0; e < f.length; ) (d[f[e].identifier] = !0), e++; + for (e = 0; e < g.length; ) + d[g[e].identifier] && h.push(g[e]), + b & (Ga | Ha) && delete d[g[e].identifier], + e++; + return h.length ? [t(f.concat(h), "identifier", !0), h] : void 0; + } + function R() { + x.apply(this, arguments); + var a = j(this.handler, this); + (this.touch = new P(this.manager, a)), + (this.mouse = new L(this.manager, a)), + (this.primaryTouch = null), + (this.lastTouches = []); + } + function S(a, b) { + a & Ea + ? ((this.primaryTouch = b.changedPointers[0].identifier), + T.call(this, b)) + : a & (Ga | Ha) && T.call(this, b); + } + function T(a) { + var b = a.changedPointers[0]; + if (b.identifier === this.primaryTouch) { + var c = { x: b.clientX, y: b.clientY }; + this.lastTouches.push(c); + var d = this.lastTouches, + e = function () { + var a = d.indexOf(c); + a > -1 && d.splice(a, 1); + }; + setTimeout(e, cb); + } + } + function U(a) { + for ( + var b = a.srcEvent.clientX, c = a.srcEvent.clientY, d = 0; + d < this.lastTouches.length; + d++ + ) { + var e = this.lastTouches[d], + f = Math.abs(b - e.x), + g = Math.abs(c - e.y); + if (db >= f && db >= g) return !0; + } + return !1; + } + function V(a, b) { + (this.manager = a), this.set(b); + } + function W(a) { + if (p(a, jb)) return jb; + var b = p(a, kb), + c = p(a, lb); + return b && c ? jb : b || c ? (b ? kb : lb) : p(a, ib) ? ib : hb; + } + function X() { + if (!fb) return !1; + var b = {}, + c = a.CSS && a.CSS.supports; + return ( + [ + "auto", + "manipulation", + "pan-y", + "pan-x", + "pan-x pan-y", + "none", + ].forEach(function (d) { + b[d] = c ? a.CSS.supports("touch-action", d) : !0; + }), + b + ); + } + function Y(a) { + (this.options = la({}, this.defaults, a || {})), + (this.id = v()), + (this.manager = null), + (this.options.enable = l(this.options.enable, !0)), + (this.state = nb), + (this.simultaneous = {}), + (this.requireFail = []); + } + function Z(a) { + return a & sb + ? "cancel" + : a & qb + ? "end" + : a & pb + ? "move" + : a & ob + ? "start" + : ""; + } + function $(a) { + return a == Ma + ? "down" + : a == La + ? "up" + : a == Ja + ? "left" + : a == Ka + ? "right" + : ""; + } + function _(a, b) { + var c = b.manager; + return c ? c.get(a) : a; + } + function aa() { + Y.apply(this, arguments); + } + function ba() { + aa.apply(this, arguments), (this.pX = null), (this.pY = null); + } + function ca() { + aa.apply(this, arguments); + } + function da() { + Y.apply(this, arguments), (this._timer = null), (this._input = null); + } + function ea() { + aa.apply(this, arguments); + } + function fa() { + aa.apply(this, arguments); + } + function ga() { + Y.apply(this, arguments), + (this.pTime = !1), + (this.pCenter = !1), + (this._timer = null), + (this._input = null), + (this.count = 0); + } + function ha(a, b) { + return ( + (b = b || {}), + (b.recognizers = l(b.recognizers, ha.defaults.preset)), + new ia(a, b) + ); + } + function ia(a, b) { + (this.options = la({}, ha.defaults, b || {})), + (this.options.inputTarget = this.options.inputTarget || a), + (this.handlers = {}), + (this.session = {}), + (this.recognizers = []), + (this.oldCssProps = {}), + (this.element = a), + (this.input = y(this)), + (this.touchAction = new V(this, this.options.touchAction)), + ja(this, !0), + g( + this.options.recognizers, + function (a) { + var b = this.add(new a[0](a[1])); + a[2] && b.recognizeWith(a[2]), + a[3] && b.requireFailure(a[3]); + }, + this + ); + } + function ja(a, b) { + var c = a.element; + if (c.style) { + var d; + g(a.options.cssProps, function (e, f) { + (d = u(c.style, f)), + b + ? ((a.oldCssProps[d] = c.style[d]), (c.style[d] = e)) + : (c.style[d] = a.oldCssProps[d] || ""); + }), + b || (a.oldCssProps = {}); + } + } + function ka(a, c) { + var d = b.createEvent("Event"); + d.initEvent(a, !0, !0), (d.gesture = c), c.target.dispatchEvent(d); + } + var la, + ma = ["", "webkit", "Moz", "MS", "ms", "o"], + na = b.createElement("div"), + oa = "function", + pa = Math.round, + qa = Math.abs, + ra = Date.now; + la = + "function" != typeof Object.assign + ? function (a) { + if (a === d || null === a) + throw new TypeError( + "Cannot convert undefined or null to object" + ); + for (var b = Object(a), c = 1; c < arguments.length; c++) { + var e = arguments[c]; + if (e !== d && null !== e) + for (var f in e) e.hasOwnProperty(f) && (b[f] = e[f]); + } + return b; + } + : Object.assign; + var sa = h( + function (a, b, c) { + for (var e = Object.keys(b), f = 0; f < e.length; ) + (!c || (c && a[e[f]] === d)) && (a[e[f]] = b[e[f]]), f++; + return a; + }, + "extend", + "Use `assign`." + ), + ta = h( + function (a, b) { + return sa(a, b, !0); + }, + "merge", + "Use `assign`." + ), + ua = 1, + va = /mobile|tablet|ip(ad|hone|od)|android/i, + wa = "ontouchstart" in a, + xa = u(a, "PointerEvent") !== d, + ya = wa && va.test(navigator.userAgent), + za = "touch", + Aa = "pen", + Ba = "mouse", + Ca = "kinect", + Da = 25, + Ea = 1, + Fa = 2, + Ga = 4, + Ha = 8, + Ia = 1, + Ja = 2, + Ka = 4, + La = 8, + Ma = 16, + Na = Ja | Ka, + Oa = La | Ma, + Pa = Na | Oa, + Qa = ["x", "y"], + Ra = ["clientX", "clientY"]; + x.prototype = { + handler: function () {}, + init: function () { + this.evEl && m(this.element, this.evEl, this.domHandler), + this.evTarget && m(this.target, this.evTarget, this.domHandler), + this.evWin && m(w(this.element), this.evWin, this.domHandler); + }, + destroy: function () { + this.evEl && n(this.element, this.evEl, this.domHandler), + this.evTarget && n(this.target, this.evTarget, this.domHandler), + this.evWin && n(w(this.element), this.evWin, this.domHandler); + }, + }; + var Sa = { mousedown: Ea, mousemove: Fa, mouseup: Ga }, + Ta = "mousedown", + Ua = "mousemove mouseup"; + i(L, x, { + handler: function (a) { + var b = Sa[a.type]; + b & Ea && 0 === a.button && (this.pressed = !0), + b & Fa && 1 !== a.which && (b = Ga), + this.pressed && + (b & Ga && (this.pressed = !1), + this.callback(this.manager, b, { + pointers: [a], + changedPointers: [a], + pointerType: Ba, + srcEvent: a, + })); + }, + }); + var Va = { + pointerdown: Ea, + pointermove: Fa, + pointerup: Ga, + pointercancel: Ha, + pointerout: Ha, + }, + Wa = { 2: za, 3: Aa, 4: Ba, 5: Ca }, + Xa = "pointerdown", + Ya = "pointermove pointerup pointercancel"; + a.MSPointerEvent && + !a.PointerEvent && + ((Xa = "MSPointerDown"), + (Ya = "MSPointerMove MSPointerUp MSPointerCancel")), + i(M, x, { + handler: function (a) { + var b = this.store, + c = !1, + d = a.type.toLowerCase().replace("ms", ""), + e = Va[d], + f = Wa[a.pointerType] || a.pointerType, + g = f == za, + h = r(b, a.pointerId, "pointerId"); + e & Ea && (0 === a.button || g) + ? 0 > h && (b.push(a), (h = b.length - 1)) + : e & (Ga | Ha) && (c = !0), + 0 > h || + ((b[h] = a), + this.callback(this.manager, e, { + pointers: b, + changedPointers: [a], + pointerType: f, + srcEvent: a, + }), + c && b.splice(h, 1)); + }, + }); + var Za = { touchstart: Ea, touchmove: Fa, touchend: Ga, touchcancel: Ha }, + $a = "touchstart", + _a = "touchstart touchmove touchend touchcancel"; + i(N, x, { + handler: function (a) { + var b = Za[a.type]; + if ((b === Ea && (this.started = !0), this.started)) { + var c = O.call(this, a, b); + b & (Ga | Ha) && + c[0].length - c[1].length === 0 && + (this.started = !1), + this.callback(this.manager, b, { + pointers: c[0], + changedPointers: c[1], + pointerType: za, + srcEvent: a, + }); + } + }, + }); + var ab = { touchstart: Ea, touchmove: Fa, touchend: Ga, touchcancel: Ha }, + bb = "touchstart touchmove touchend touchcancel"; + i(P, x, { + handler: function (a) { + var b = ab[a.type], + c = Q.call(this, a, b); + c && + this.callback(this.manager, b, { + pointers: c[0], + changedPointers: c[1], + pointerType: za, + srcEvent: a, + }); + }, + }); + var cb = 2500, + db = 25; + i(R, x, { + handler: function (a, b, c) { + var d = c.pointerType == za, + e = c.pointerType == Ba; + if ( + !( + e && + c.sourceCapabilities && + c.sourceCapabilities.firesTouchEvents + ) + ) { + if (d) S.call(this, b, c); + else if (e && U.call(this, c)) return; + this.callback(a, b, c); + } + }, + destroy: function () { + this.touch.destroy(), this.mouse.destroy(); + }, + }); + var eb = u(na.style, "touchAction"), + fb = eb !== d, + gb = "compute", + hb = "auto", + ib = "manipulation", + jb = "none", + kb = "pan-x", + lb = "pan-y", + mb = X(); + V.prototype = { + set: function (a) { + a == gb && (a = this.compute()), + fb && + this.manager.element.style && + mb[a] && + (this.manager.element.style[eb] = a), + (this.actions = a.toLowerCase().trim()); + }, + update: function () { + this.set(this.manager.options.touchAction); + }, + compute: function () { + var a = []; + return ( + g(this.manager.recognizers, function (b) { + k(b.options.enable, [b]) && + (a = a.concat(b.getTouchAction())); + }), + W(a.join(" ")) + ); + }, + preventDefaults: function (a) { + var b = a.srcEvent, + c = a.offsetDirection; + if (this.manager.session.prevented) return void b.preventDefault(); + var d = this.actions, + e = p(d, jb) && !mb[jb], + f = p(d, lb) && !mb[lb], + g = p(d, kb) && !mb[kb]; + if (e) { + var h = 1 === a.pointers.length, + i = a.distance < 2, + j = a.deltaTime < 250; + if (h && i && j) return; + } + return g && f + ? void 0 + : e || (f && c & Na) || (g && c & Oa) + ? this.preventSrc(b) + : void 0; + }, + preventSrc: function (a) { + (this.manager.session.prevented = !0), a.preventDefault(); + }, + }; + var nb = 1, + ob = 2, + pb = 4, + qb = 8, + rb = qb, + sb = 16, + tb = 32; + (Y.prototype = { + defaults: {}, + set: function (a) { + return ( + la(this.options, a), + this.manager && this.manager.touchAction.update(), + this + ); + }, + recognizeWith: function (a) { + if (f(a, "recognizeWith", this)) return this; + var b = this.simultaneous; + return ( + (a = _(a, this)), + b[a.id] || ((b[a.id] = a), a.recognizeWith(this)), + this + ); + }, + dropRecognizeWith: function (a) { + return f(a, "dropRecognizeWith", this) + ? this + : ((a = _(a, this)), delete this.simultaneous[a.id], this); + }, + requireFailure: function (a) { + if (f(a, "requireFailure", this)) return this; + var b = this.requireFail; + return ( + (a = _(a, this)), + -1 === r(b, a) && (b.push(a), a.requireFailure(this)), + this + ); + }, + dropRequireFailure: function (a) { + if (f(a, "dropRequireFailure", this)) return this; + a = _(a, this); + var b = r(this.requireFail, a); + return b > -1 && this.requireFail.splice(b, 1), this; + }, + hasRequireFailures: function () { + return this.requireFail.length > 0; + }, + canRecognizeWith: function (a) { + return !!this.simultaneous[a.id]; + }, + emit: function (a) { + function b(b) { + c.manager.emit(b, a); + } + var c = this, + d = this.state; + qb > d && b(c.options.event + Z(d)), + b(c.options.event), + a.additionalEvent && b(a.additionalEvent), + d >= qb && b(c.options.event + Z(d)); + }, + tryEmit: function (a) { + return this.canEmit() ? this.emit(a) : void (this.state = tb); + }, + canEmit: function () { + for (var a = 0; a < this.requireFail.length; ) { + if (!(this.requireFail[a].state & (tb | nb))) return !1; + a++; + } + return !0; + }, + recognize: function (a) { + var b = la({}, a); + return k(this.options.enable, [this, b]) + ? (this.state & (rb | sb | tb) && (this.state = nb), + (this.state = this.process(b)), + void (this.state & (ob | pb | qb | sb) && this.tryEmit(b))) + : (this.reset(), void (this.state = tb)); + }, + process: function (a) {}, + getTouchAction: function () {}, + reset: function () {}, + }), + i(aa, Y, { + defaults: { pointers: 1 }, + attrTest: function (a) { + var b = this.options.pointers; + return 0 === b || a.pointers.length === b; + }, + process: function (a) { + var b = this.state, + c = a.eventType, + d = b & (ob | pb), + e = this.attrTest(a); + return d && (c & Ha || !e) + ? b | sb + : d || e + ? c & Ga + ? b | qb + : b & ob + ? b | pb + : ob + : tb; + }, + }), + i(ba, aa, { + defaults: { + event: "pan", + threshold: 10, + pointers: 1, + direction: Pa, + }, + getTouchAction: function () { + var a = this.options.direction, + b = []; + return a & Na && b.push(lb), a & Oa && b.push(kb), b; + }, + directionTest: function (a) { + var b = this.options, + c = !0, + d = a.distance, + e = a.direction, + f = a.deltaX, + g = a.deltaY; + return ( + e & b.direction || + (b.direction & Na + ? ((e = 0 === f ? Ia : 0 > f ? Ja : Ka), + (c = f != this.pX), + (d = Math.abs(a.deltaX))) + : ((e = 0 === g ? Ia : 0 > g ? La : Ma), + (c = g != this.pY), + (d = Math.abs(a.deltaY)))), + (a.direction = e), + c && d > b.threshold && e & b.direction + ); + }, + attrTest: function (a) { + return ( + aa.prototype.attrTest.call(this, a) && + (this.state & ob || + (!(this.state & ob) && this.directionTest(a))) + ); + }, + emit: function (a) { + (this.pX = a.deltaX), (this.pY = a.deltaY); + var b = $(a.direction); + b && (a.additionalEvent = this.options.event + b), + this._super.emit.call(this, a); + }, + }), + i(ca, aa, { + defaults: { event: "pinch", threshold: 0, pointers: 2 }, + getTouchAction: function () { + return [jb]; + }, + attrTest: function (a) { + return ( + this._super.attrTest.call(this, a) && + (Math.abs(a.scale - 1) > this.options.threshold || + this.state & ob) + ); + }, + emit: function (a) { + if (1 !== a.scale) { + var b = a.scale < 1 ? "in" : "out"; + a.additionalEvent = this.options.event + b; + } + this._super.emit.call(this, a); + }, + }), + i(da, Y, { + defaults: { event: "press", pointers: 1, time: 251, threshold: 9 }, + getTouchAction: function () { + return [hb]; + }, + process: function (a) { + var b = this.options, + c = a.pointers.length === b.pointers, + d = a.distance < b.threshold, + f = a.deltaTime > b.time; + if ( + ((this._input = a), + !d || !c || (a.eventType & (Ga | Ha) && !f)) + ) + this.reset(); + else if (a.eventType & Ea) + this.reset(), + (this._timer = e( + function () { + (this.state = rb), this.tryEmit(); + }, + b.time, + this + )); + else if (a.eventType & Ga) return rb; + return tb; + }, + reset: function () { + clearTimeout(this._timer); + }, + emit: function (a) { + this.state === rb && + (a && a.eventType & Ga + ? this.manager.emit(this.options.event + "up", a) + : ((this._input.timeStamp = ra()), + this.manager.emit(this.options.event, this._input))); + }, + }), + i(ea, aa, { + defaults: { event: "rotate", threshold: 0, pointers: 2 }, + getTouchAction: function () { + return [jb]; + }, + attrTest: function (a) { + return ( + this._super.attrTest.call(this, a) && + (Math.abs(a.rotation) > this.options.threshold || + this.state & ob) + ); + }, + }), + i(fa, aa, { + defaults: { + event: "swipe", + threshold: 10, + velocity: 0.3, + direction: Na | Oa, + pointers: 1, + }, + getTouchAction: function () { + return ba.prototype.getTouchAction.call(this); + }, + attrTest: function (a) { + var b, + c = this.options.direction; + return ( + c & (Na | Oa) + ? (b = a.overallVelocity) + : c & Na + ? (b = a.overallVelocityX) + : c & Oa && (b = a.overallVelocityY), + this._super.attrTest.call(this, a) && + c & a.offsetDirection && + a.distance > this.options.threshold && + a.maxPointers == this.options.pointers && + qa(b) > this.options.velocity && + a.eventType & Ga + ); + }, + emit: function (a) { + var b = $(a.offsetDirection); + b && this.manager.emit(this.options.event + b, a), + this.manager.emit(this.options.event, a); + }, + }), + i(ga, Y, { + defaults: { + event: "tap", + pointers: 1, + taps: 1, + interval: 300, + time: 250, + threshold: 9, + posThreshold: 10, + }, + getTouchAction: function () { + return [ib]; + }, + process: function (a) { + var b = this.options, + c = a.pointers.length === b.pointers, + d = a.distance < b.threshold, + f = a.deltaTime < b.time; + if ((this.reset(), a.eventType & Ea && 0 === this.count)) + return this.failTimeout(); + if (d && f && c) { + if (a.eventType != Ga) return this.failTimeout(); + var g = this.pTime + ? a.timeStamp - this.pTime < b.interval + : !0, + h = + !this.pCenter || + H(this.pCenter, a.center) < b.posThreshold; + (this.pTime = a.timeStamp), + (this.pCenter = a.center), + h && g ? (this.count += 1) : (this.count = 1), + (this._input = a); + var i = this.count % b.taps; + if (0 === i) + return this.hasRequireFailures() + ? ((this._timer = e( + function () { + (this.state = rb), this.tryEmit(); + }, + b.interval, + this + )), + ob) + : rb; + } + return tb; + }, + failTimeout: function () { + return ( + (this._timer = e( + function () { + this.state = tb; + }, + this.options.interval, + this + )), + tb + ); + }, + reset: function () { + clearTimeout(this._timer); + }, + emit: function () { + this.state == rb && + ((this._input.tapCount = this.count), + this.manager.emit(this.options.event, this._input)); + }, + }), + (ha.VERSION = "2.0.8"), + (ha.defaults = { + domEvents: !1, + touchAction: gb, + enable: !0, + inputTarget: null, + inputClass: null, + preset: [ + [ea, { enable: !1 }], + [ca, { enable: !1 }, ["rotate"]], + [fa, { direction: Na }], + [ba, { direction: Na }, ["swipe"]], + [ga], + [ga, { event: "doubletap", taps: 2 }, ["tap"]], + [da], + ], + cssProps: { + userSelect: "none", + touchSelect: "none", + touchCallout: "none", + contentZooming: "none", + userDrag: "none", + tapHighlightColor: "rgba(0,0,0,0)", + }, + }); + var ub = 1, + vb = 2; + (ia.prototype = { + set: function (a) { + return ( + la(this.options, a), + a.touchAction && this.touchAction.update(), + a.inputTarget && + (this.input.destroy(), + (this.input.target = a.inputTarget), + this.input.init()), + this + ); + }, + stop: function (a) { + this.session.stopped = a ? vb : ub; + }, + recognize: function (a) { + var b = this.session; + if (!b.stopped) { + this.touchAction.preventDefaults(a); + var c, + d = this.recognizers, + e = b.curRecognizer; + (!e || (e && e.state & rb)) && (e = b.curRecognizer = null); + for (var f = 0; f < d.length; ) + (c = d[f]), + b.stopped === vb || + (e && c != e && !c.canRecognizeWith(e)) + ? c.reset() + : c.recognize(a), + !e && + c.state & (ob | pb | qb) && + (e = b.curRecognizer = c), + f++; + } + }, + get: function (a) { + if (a instanceof Y) return a; + for (var b = this.recognizers, c = 0; c < b.length; c++) + if (b[c].options.event == a) return b[c]; + return null; + }, + add: function (a) { + if (f(a, "add", this)) return this; + var b = this.get(a.options.event); + return ( + b && this.remove(b), + this.recognizers.push(a), + (a.manager = this), + this.touchAction.update(), + a + ); + }, + remove: function (a) { + if (f(a, "remove", this)) return this; + if ((a = this.get(a))) { + var b = this.recognizers, + c = r(b, a); + -1 !== c && (b.splice(c, 1), this.touchAction.update()); + } + return this; + }, + on: function (a, b) { + if (a !== d && b !== d) { + var c = this.handlers; + return ( + g(q(a), function (a) { + (c[a] = c[a] || []), c[a].push(b); + }), + this + ); + } + }, + off: function (a, b) { + if (a !== d) { + var c = this.handlers; + return ( + g(q(a), function (a) { + b ? c[a] && c[a].splice(r(c[a], b), 1) : delete c[a]; + }), + this + ); + } + }, + emit: function (a, b) { + this.options.domEvents && ka(a, b); + var c = this.handlers[a] && this.handlers[a].slice(); + if (c && c.length) { + (b.type = a), + (b.preventDefault = function () { + b.srcEvent.preventDefault(); + }); + for (var d = 0; d < c.length; ) c[d](b), d++; + } + }, + destroy: function () { + this.element && ja(this, !1), + (this.handlers = {}), + (this.session = {}), + this.input.destroy(), + (this.element = null); + }, + }), + la(ha, { + INPUT_START: Ea, + INPUT_MOVE: Fa, + INPUT_END: Ga, + INPUT_CANCEL: Ha, + STATE_POSSIBLE: nb, + STATE_BEGAN: ob, + STATE_CHANGED: pb, + STATE_ENDED: qb, + STATE_RECOGNIZED: rb, + STATE_CANCELLED: sb, + STATE_FAILED: tb, + DIRECTION_NONE: Ia, + DIRECTION_LEFT: Ja, + DIRECTION_RIGHT: Ka, + DIRECTION_UP: La, + DIRECTION_DOWN: Ma, + DIRECTION_HORIZONTAL: Na, + DIRECTION_VERTICAL: Oa, + DIRECTION_ALL: Pa, + Manager: ia, + Input: x, + TouchAction: V, + TouchInput: P, + MouseInput: L, + PointerEventInput: M, + TouchMouseInput: R, + SingleTouchInput: N, + Recognizer: Y, + AttrRecognizer: aa, + Tap: ga, + Pan: ba, + Swipe: fa, + Pinch: ca, + Rotate: ea, + Press: da, + on: m, + off: n, + each: g, + merge: ta, + extend: sa, + assign: la, + inherit: i, + bindFn: j, + prefixed: u, + }); + var wb = + "undefined" != typeof a ? a : "undefined" != typeof self ? self : {}; + (wb.Hammer = ha), + "function" == typeof define && define.amd + ? define(function () { + return ha; + }) + : "undefined" != typeof module && module.exports + ? (module.exports = ha) + : (a[c] = ha); +})(window, document, "Hammer"); +//# sourceMappingURL=hammer.min.js.map diff --git a/cps/static/js/reading/epub.js b/cps/static/js/reading/epub.js index 0a907ed2a..46c9c1da9 100644 --- a/cps/static/js/reading/epub.js +++ b/cps/static/js/reading/epub.js @@ -24,33 +24,114 @@ var reader; $("#bookmark, #show-Bookmarks").remove(); } - // Enable swipe support - // I have no idea why swiperRight/swiperLeft from plugins is not working, events just don't get fired - var touchStart = 0; - var touchEnd = 0; + // Navigation mode: 'sides' or 'gestures' + var hammerManagers = []; - reader.rendition.on("touchstart", function (event) { - touchStart = event.changedTouches[0].screenX; - }); - reader.rendition.on("touchend", function (event) { - touchEnd = event.changedTouches[0].screenX; - if (touchStart < touchEnd) { - if (reader.book.package.metadata.direction === "rtl") { - reader.rendition.next(); - } else { - reader.rendition.prev(); - } - // Swiped Right + function hasActiveSelection(win) { + try { + var sel = win.getSelection && win.getSelection(); + return sel && sel.type === "Range" && sel.toString().length > 0; + } catch (e) { + return false; } - if (touchStart > touchEnd) { - if (reader.book.package.metadata.direction === "rtl") { - reader.rendition.prev(); - } else { - reader.rendition.next(); - } - // Swiped Left + } + + function bindHammer(target, isIframeDoc) { + if (typeof Hammer === "undefined") { + return; } - }); + var mc = new Hammer(target); + mc.get("swipe").set({ + direction: Hammer.DIRECTION_HORIZONTAL, + threshold: 25, + velocity: 0.3, + }); + mc.on("swipeleft swiperight", function (ev) { + if (!window.cwGesturesEnabled) return; + if (ev.pointers && ev.pointers.length > 1) return; // ignore multi-touch + var win = isIframeDoc ? target.defaultView : window; + if (hasActiveSelection(win)) return; // do not navigate when selecting text + // Mapping per requirement: L->R = PREV, R->L = NEXT (independent of RTL) + if (ev.type === "swipeleft") reader.rendition.next(); + else reader.rendition.prev(); + }); + hammerManagers.push(mc); + } + + function destroyHammers() { + while (hammerManagers.length) { + var mc = hammerManagers.pop(); + try { + mc.destroy(); + } catch (e) {} + } + } + + function enableSideClicks() { + var prevBtn = document.getElementById("prev"); + var nextBtn = document.getElementById("next"); + if (prevBtn) { + prevBtn.style.display = ""; + prevBtn.onclick = function () { + reader.rendition.prev(); + }; + } + if (nextBtn) { + nextBtn.style.display = ""; + nextBtn.onclick = function () { + reader.rendition.next(); + }; + } + } + + function disableSideClicks() { + var prevBtn = document.getElementById("prev"); + var nextBtn = document.getElementById("next"); + if (prevBtn) { + prevBtn.onclick = null; + prevBtn.style.display = "none"; + } + if (nextBtn) { + nextBtn.onclick = null; + nextBtn.style.display = "none"; + } + } + + function enableGestures() { + // Bind to outer container + if (reader && reader.rendition && reader.rendition.container) { + bindHammer(reader.rendition.container, false); + } + // Bind to inner iframes when rendered + reader.rendition.on("rendered", function (section, contents) { + var docEl = contents.document; + if (docEl && docEl.documentElement && docEl.documentElement.style) { + docEl.documentElement.style.touchAction = "pan-y"; + } + bindHammer(docEl, true); + }); + } + + window.applyNavigationMode = function (mode) { + if (!window.cwInitialized) { + destroyHammers(); + enableGestures(); + window.cwInitialized = true; + } + + if (mode === "sides") { + enableSideClicks(); + window.cwGesturesEnabled = false; + } else { + disableSideClicks(); + window.cwGesturesEnabled = true; + } + }; + + // Apply saved or default mode on load + var savedMode = + localStorage.getItem("calibre.reader.navMode") || "gestures"; + window.applyNavigationMode(savedMode); // Update progress percentage let progressDiv = document.getElementById("progress"); diff --git a/cps/templates/read.html b/cps/templates/read.html index 6262f403b..690d60a56 100644 --- a/cps/templates/read.html +++ b/cps/templates/read.html @@ -157,6 +157,19 @@ />{{_('Reflow text when sidebars are open.')}}

+
+ From 6d6d5ac097e146d83d29ba5348dcf8122cb05b78 Mon Sep 17 00:00:00 2001 From: IgorKurkov Date: Wed, 29 Oct 2025 22:06:09 +0100 Subject: [PATCH 7/9] add books button, add page count on bottom and in settings, add swipe for closing sidebar, fix settings modal, fix few styles --- cps/static/css/main.css | 4 +-- cps/static/css/reader.css | 24 +++++++++++++++++- cps/static/js/reading/epub.js | 48 +++++++++++++++++++++++++++++++++-- cps/templates/read.html | 44 +++++++++++++++++++++++++++++--- 4 files changed, 112 insertions(+), 8 deletions(-) diff --git a/cps/static/css/main.css b/cps/static/css/main.css index fcb00eed6..feb105244 100644 --- a/cps/static/css/main.css +++ b/cps/static/css/main.css @@ -539,7 +539,7 @@ input:-moz-placeholder { left: 50%; transform: translate(-50%, -50%); width: 100vw; - height: auto; + height: 90dvh; max-width: 630px; z-index: 2000; visibility: hidden; @@ -580,7 +580,7 @@ input:-moz-placeholder { } .md-content > div { - padding: 15px 40px 30px; + padding: 10px 20px; margin: 0; font-weight: 300; font-size: 14px; diff --git a/cps/static/css/reader.css b/cps/static/css/reader.css index ac943460f..6294353d6 100644 --- a/cps/static/css/reader.css +++ b/cps/static/css/reader.css @@ -47,6 +47,28 @@ } #main, -#viewer { +#viewer, +#sidebar, +#tocView { touch-action: pan-y; } + +.read-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 12px; +} +.read-footer #pages-count { + font-size: 12px; + + position: absolute; + left: 12px; + bottom: 2px; +} + +#back-button { + width: 50px !important; + right: 50px; + position: absolute; +} diff --git a/cps/static/js/reading/epub.js b/cps/static/js/reading/epub.js index 46c9c1da9..9f906674e 100644 --- a/cps/static/js/reading/epub.js +++ b/cps/static/js/reading/epub.js @@ -102,6 +102,24 @@ var reader; if (reader && reader.rendition && reader.rendition.container) { bindHammer(reader.rendition.container, false); } + // Bind swipe on sidebar to close/open main + try { + var sidebarEl = document.getElementById("tocView"); + if (sidebarEl && typeof Hammer !== "undefined") { + var sidebarMc = new Hammer(sidebarEl); + sidebarMc.get("swipe").set({ + direction: Hammer.DIRECTION_HORIZONTAL, + threshold: 25, + velocity: 0.3, + }); + sidebarMc.on("swipeleft swiperight", function (ev) { + if ($("#sidebar").hasClass("open")) { + $("#slider").click(); + } + }); + hammerManagers.push(sidebarMc); + } + } catch (e) {} // Bind to inner iframes when rendered reader.rendition.on("rendered", function (section, contents) { var docEl = contents.document; @@ -129,12 +147,23 @@ var reader; }; // Apply saved or default mode on load - var savedMode = - localStorage.getItem("calibre.reader.navMode") || "gestures"; + var savedMode = localStorage.getItem("calibre.reader.navMode") || "sizes"; window.applyNavigationMode(savedMode); // Update progress percentage let progressDiv = document.getElementById("progress"); + // Pages counter (virtual pages via EPUB locations) + let pagesDiv = document.getElementById("pages-count"); + // Honor saved visibility preference for pages counter + (function () { + try { + var pref = localStorage.getItem("calibre.reader.showPages"); + var show = pref === null ? true : pref === "true"; + if (pagesDiv) + pagesDiv.style.visibility = show ? "visible" : "hidden"; + } catch (e) {} + })(); + reader.book.ready.then(() => { let locations_key = reader.book.key() + "-locations"; let stored_locations = localStorage.getItem(locations_key); @@ -159,6 +188,21 @@ var reader; reader.rendition.on("relocated", (location) => { let percentage = Math.round(location.end.percentage * 100); progressDiv.textContent = percentage + "%"; + + // Pages based on generated EPUB locations (CFI positions) + const cfi = location.start.cfi; + const current = + reader.book.locations.locationFromCfi(cfi) || 0; // 1-based index typically + const total = reader.book.locations.length() || 0; + const remaining = Math.max(total - current, 0); + + if (total > 0) { + pagesDiv.textContent = current + "/" + total; + pagesDiv.style.visibility = "visible"; + } else { + pagesDiv.textContent = ""; + pagesDiv.style.visibility = "hidden"; + } }); reader.rendition.reportLocation(); progressDiv.style.visibility = "visible"; diff --git a/cps/templates/read.html b/cps/templates/read.html index 690d60a56..5d21feb12 100644 --- a/cps/templates/read.html +++ b/cps/templates/read.html @@ -57,6 +57,13 @@ data-view="Bookmarks" >Bookmarks + + Books +
@@ -95,7 +102,10 @@
-
0%
+
@@ -167,9 +177,18 @@ id="navGestures" onclick="selectNavMode('gestures')" > - {{_('Gestures')}} + {{_('Gestures (for mobile)')}}
+
+

+ {{_('Show pages count')}} +

+
From 458d0d1e3db9aa03231046277e5f9749e7fbf7e3 Mon Sep 17 00:00:00 2001 From: IgorKurkov Date: Wed, 29 Oct 2025 22:10:08 +0100 Subject: [PATCH 8/9] feat(read.html, epub): add custom theme selection with color picker, used pickr.js --- cps/static/css/main.css | 4 +- cps/static/css/reader.css | 4 +- cps/static/js/libs/pickr/pickr.min.css | 2 + cps/static/js/libs/pickr/pickr.min.js | 3 + cps/templates/read.html | 258 ++++++++++++++++++++++++- 5 files changed, 257 insertions(+), 14 deletions(-) create mode 100644 cps/static/js/libs/pickr/pickr.min.css create mode 100644 cps/static/js/libs/pickr/pickr.min.js diff --git a/cps/static/css/main.css b/cps/static/css/main.css index feb105244..f590eda3a 100644 --- a/cps/static/css/main.css +++ b/cps/static/css/main.css @@ -538,7 +538,7 @@ input:-moz-placeholder { top: 50%; left: 50%; transform: translate(-50%, -50%); - width: 100vw; + width: 97vw; height: 90dvh; max-width: 630px; z-index: 2000; @@ -565,7 +565,7 @@ input:-moz-placeholder { } .md-show ~ .overlay { - opacity: 1; + /* opacity: 1; */ visibility: visible; } diff --git a/cps/static/css/reader.css b/cps/static/css/reader.css index 6294353d6..4db589e9d 100644 --- a/cps/static/css/reader.css +++ b/cps/static/css/reader.css @@ -29,8 +29,8 @@ } .item { - font-size: 20px; - font-weight: 400; + font-size: 14px; + font-weight: 600; font-family: Open Sans; padding-right: 10px; color: white; diff --git a/cps/static/js/libs/pickr/pickr.min.css b/cps/static/js/libs/pickr/pickr.min.css new file mode 100644 index 000000000..e4ccfd28f --- /dev/null +++ b/cps/static/js/libs/pickr/pickr.min.css @@ -0,0 +1,2 @@ +/*! Pickr 1.9.1 MIT | https://github.com/Simonwep/pickr */ +.pickr{position:relative;overflow:visible;transform:translateY(0)}.pickr *{box-sizing:border-box;outline:none;border:none;-webkit-appearance:none}.pickr .pcr-button{position:relative;height:2em;width:2em;padding:.5em;cursor:pointer;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Helvetica Neue",Arial,sans-serif;border-radius:.15em;background:url("data:image/svg+xml;utf8, ") no-repeat center;background-size:0;transition:all .3s}.pickr .pcr-button::before{position:absolute;content:"";top:0;left:0;width:100%;height:100%;background:url("data:image/svg+xml;utf8, ");background-size:.5em;border-radius:.15em;z-index:-1}.pickr .pcr-button::before{z-index:initial}.pickr .pcr-button::after{position:absolute;content:"";top:0;left:0;height:100%;width:100%;transition:background .3s;background:var(--pcr-color);border-radius:.15em}.pickr .pcr-button.clear{background-size:70%}.pickr .pcr-button.clear::before{opacity:0}.pickr .pcr-button.clear:focus{box-shadow:0 0 0 1px rgba(255,255,255,.85),0 0 0 3px var(--pcr-color)}.pickr .pcr-button.disabled{cursor:not-allowed}.pickr *,.pcr-app *{box-sizing:border-box;outline:none;border:none;-webkit-appearance:none}.pickr input:focus,.pickr input.pcr-active,.pickr button:focus,.pickr button.pcr-active,.pcr-app input:focus,.pcr-app input.pcr-active,.pcr-app button:focus,.pcr-app button.pcr-active{box-shadow:0 0 0 1px rgba(255,255,255,.85),0 0 0 3px var(--pcr-color)}.pickr .pcr-palette,.pickr .pcr-slider,.pcr-app .pcr-palette,.pcr-app .pcr-slider{transition:box-shadow .3s}.pickr .pcr-palette:focus,.pickr .pcr-slider:focus,.pcr-app .pcr-palette:focus,.pcr-app .pcr-slider:focus{box-shadow:0 0 0 1px rgba(255,255,255,.85),0 0 0 3px rgba(0,0,0,.25)}.pcr-app{position:fixed;display:flex;flex-direction:column;z-index:10000;border-radius:.1em;background:#fff;opacity:0;visibility:hidden;transition:opacity .3s,visibility 0s .3s;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Helvetica Neue",Arial,sans-serif;box-shadow:0 .15em 1.5em 0 rgba(0,0,0,.1),0 0 1em 0 rgba(0,0,0,.03);left:0;top:0}.pcr-app.visible{transition:opacity .3s;visibility:visible;opacity:1}.pcr-app .pcr-swatches{display:flex;flex-wrap:wrap;margin-top:.75em}.pcr-app .pcr-swatches.pcr-last{margin:0}@supports(display: grid){.pcr-app .pcr-swatches{display:grid;align-items:center;grid-template-columns:repeat(auto-fit, 1.75em)}}.pcr-app .pcr-swatches>button{font-size:1em;position:relative;width:calc(1.75em - 5px);height:calc(1.75em - 5px);border-radius:.15em;cursor:pointer;margin:2.5px;flex-shrink:0;justify-self:center;transition:all .15s;overflow:hidden;background:rgba(0,0,0,0);z-index:1}.pcr-app .pcr-swatches>button::before{position:absolute;content:"";top:0;left:0;width:100%;height:100%;background:url("data:image/svg+xml;utf8, ");background-size:6px;border-radius:.15em;z-index:-1}.pcr-app .pcr-swatches>button::after{content:"";position:absolute;top:0;left:0;width:100%;height:100%;background:var(--pcr-color);border:1px solid rgba(0,0,0,.05);border-radius:.15em;box-sizing:border-box}.pcr-app .pcr-swatches>button:hover{filter:brightness(1.05)}.pcr-app .pcr-swatches>button:not(.pcr-active){box-shadow:none}.pcr-app .pcr-interaction{display:flex;flex-wrap:wrap;align-items:center;margin:0 -0.2em 0 -0.2em}.pcr-app .pcr-interaction>*{margin:0 .2em}.pcr-app .pcr-interaction input{letter-spacing:.07em;font-size:.75em;text-align:center;cursor:pointer;color:#75797e;background:#f1f3f4;border-radius:.15em;transition:all .15s;padding:.45em .5em;margin-top:.75em}.pcr-app .pcr-interaction input:hover{filter:brightness(0.975)}.pcr-app .pcr-interaction input:focus{box-shadow:0 0 0 1px rgba(255,255,255,.85),0 0 0 3px rgba(66,133,244,.75)}.pcr-app .pcr-interaction .pcr-result{color:#75797e;text-align:left;flex:1 1 8em;min-width:8em;transition:all .2s;border-radius:.15em;background:#f1f3f4;cursor:text}.pcr-app .pcr-interaction .pcr-result::-moz-selection{background:#4285f4;color:#fff}.pcr-app .pcr-interaction .pcr-result::selection{background:#4285f4;color:#fff}.pcr-app .pcr-interaction .pcr-type.active{color:#fff;background:#4285f4}.pcr-app .pcr-interaction .pcr-save,.pcr-app .pcr-interaction .pcr-cancel,.pcr-app .pcr-interaction .pcr-clear{color:#fff;width:auto}.pcr-app .pcr-interaction .pcr-save,.pcr-app .pcr-interaction .pcr-cancel,.pcr-app .pcr-interaction .pcr-clear{color:#fff}.pcr-app .pcr-interaction .pcr-save:hover,.pcr-app .pcr-interaction .pcr-cancel:hover,.pcr-app .pcr-interaction .pcr-clear:hover{filter:brightness(0.925)}.pcr-app .pcr-interaction .pcr-save{background:#4285f4}.pcr-app .pcr-interaction .pcr-clear,.pcr-app .pcr-interaction .pcr-cancel{background:#f44250}.pcr-app .pcr-interaction .pcr-clear:focus,.pcr-app .pcr-interaction .pcr-cancel:focus{box-shadow:0 0 0 1px rgba(255,255,255,.85),0 0 0 3px rgba(244,66,80,.75)}.pcr-app .pcr-selection .pcr-picker{position:absolute;height:18px;width:18px;border:2px solid #fff;border-radius:100%;-webkit-user-select:none;-moz-user-select:none;user-select:none}.pcr-app .pcr-selection .pcr-color-palette,.pcr-app .pcr-selection .pcr-color-chooser,.pcr-app .pcr-selection .pcr-color-opacity{position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none;display:flex;flex-direction:column;cursor:grab;cursor:-webkit-grab}.pcr-app .pcr-selection .pcr-color-palette:active,.pcr-app .pcr-selection .pcr-color-chooser:active,.pcr-app .pcr-selection .pcr-color-opacity:active{cursor:grabbing;cursor:-webkit-grabbing}.pcr-app[data-theme=classic]{width:28.5em;max-width:95vw;padding:.8em}.pcr-app[data-theme=classic] .pcr-selection{display:flex;justify-content:space-between;flex-grow:1}.pcr-app[data-theme=classic] .pcr-selection .pcr-color-preview{position:relative;z-index:1;width:2em;display:flex;flex-direction:column;justify-content:space-between;margin-right:.75em}.pcr-app[data-theme=classic] .pcr-selection .pcr-color-preview::before{position:absolute;content:"";top:0;left:0;width:100%;height:100%;background:url("data:image/svg+xml;utf8, ");background-size:.5em;border-radius:.15em;z-index:-1}.pcr-app[data-theme=classic] .pcr-selection .pcr-color-preview .pcr-last-color{cursor:pointer;border-radius:.15em .15em 0 0;z-index:2}.pcr-app[data-theme=classic] .pcr-selection .pcr-color-preview .pcr-current-color{border-radius:0 0 .15em .15em}.pcr-app[data-theme=classic] .pcr-selection .pcr-color-preview .pcr-last-color,.pcr-app[data-theme=classic] .pcr-selection .pcr-color-preview .pcr-current-color{background:var(--pcr-color);width:100%;height:50%}.pcr-app[data-theme=classic] .pcr-selection .pcr-color-palette{width:100%;height:8em;z-index:1}.pcr-app[data-theme=classic] .pcr-selection .pcr-color-palette .pcr-palette{flex-grow:1;border-radius:.15em}.pcr-app[data-theme=classic] .pcr-selection .pcr-color-palette .pcr-palette::before{position:absolute;content:"";top:0;left:0;width:100%;height:100%;background:url("data:image/svg+xml;utf8, ");background-size:.5em;border-radius:.15em;z-index:-1}.pcr-app[data-theme=classic] .pcr-selection .pcr-color-chooser,.pcr-app[data-theme=classic] .pcr-selection .pcr-color-opacity{margin-left:.75em}.pcr-app[data-theme=classic] .pcr-selection .pcr-color-chooser .pcr-picker,.pcr-app[data-theme=classic] .pcr-selection .pcr-color-opacity .pcr-picker{left:50%;transform:translateX(-50%)}.pcr-app[data-theme=classic] .pcr-selection .pcr-color-chooser .pcr-slider,.pcr-app[data-theme=classic] .pcr-selection .pcr-color-opacity .pcr-slider{width:8px;flex-grow:1;border-radius:50em}.pcr-app[data-theme=classic] .pcr-selection .pcr-color-chooser .pcr-slider{background:linear-gradient(to bottom, hsl(0, 100%, 50%), hsl(60, 100%, 50%), hsl(120, 100%, 50%), hsl(180, 100%, 50%), hsl(240, 100%, 50%), hsl(300, 100%, 50%), hsl(0, 100%, 50%))}.pcr-app[data-theme=classic] .pcr-selection .pcr-color-opacity .pcr-slider{background:linear-gradient(to bottom, transparent, black),url("data:image/svg+xml;utf8, ");background-size:100%,50%} diff --git a/cps/static/js/libs/pickr/pickr.min.js b/cps/static/js/libs/pickr/pickr.min.js new file mode 100644 index 000000000..5fa877d29 --- /dev/null +++ b/cps/static/js/libs/pickr/pickr.min.js @@ -0,0 +1,3 @@ +/*! Pickr 1.9.1 MIT | https://github.com/Simonwep/pickr */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.Pickr=e():t.Pickr=e()}(self,(()=>(()=>{"use strict";var t={d:(e,o)=>{for(var n in o)t.o(o,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:o[n]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};t.d(e,{default:()=>E});var o={};function n(t,e,o,n,i={}){e instanceof HTMLCollection||e instanceof NodeList?e=Array.from(e):Array.isArray(e)||(e=[e]),Array.isArray(o)||(o=[o]);for(const s of e)for(const e of o)s[t](e,n,{capture:!1,...i});return Array.prototype.slice.call(arguments,1)}t.r(o),t.d(o,{adjustableInputNumbers:()=>p,createElementFromString:()=>r,createFromTemplate:()=>a,eventPath:()=>l,off:()=>s,on:()=>i,resolveElement:()=>c});const i=n.bind(null,"addEventListener"),s=n.bind(null,"removeEventListener");function r(t){const e=document.createElement("div");return e.innerHTML=t.trim(),e.firstElementChild}function a(t){const e=(t,e)=>{const o=t.getAttribute(e);return t.removeAttribute(e),o},o=(t,n={})=>{const i=e(t,":obj"),s=e(t,":ref"),r=i?n[i]={}:n;s&&(n[s]=t);for(const n of Array.from(t.children)){const t=e(n,":arr"),i=o(n,t?{}:r);t&&(r[t]||(r[t]=[])).push(Object.keys(i).length?i:n)}return n};return o(r(t))}function l(t){let e=t.path||t.composedPath&&t.composedPath();if(e)return e;let o=t.target.parentElement;for(e=[t.target,o];o=o.parentElement;)e.push(o);return e.push(document,window),e}function c(t){return t instanceof Element?t:"string"==typeof t?t.split(/>>/g).reduce(((t,e,o,n)=>(t=t.querySelector(e),ot)){function o(o){const n=[.001,.01,.1][Number(o.shiftKey||2*o.ctrlKey)]*(o.deltaY<0?1:-1);let i=0,s=t.selectionStart;t.value=t.value.replace(/[\d.]+/g,((t,o)=>o<=s&&o+t.length>=s?(s=o,e(Number(t),n,i)):(i++,t))),t.focus(),t.setSelectionRange(s,s),o.preventDefault(),t.dispatchEvent(new Event("input"))}i(t,"focus",(()=>i(window,"wheel",o,{passive:!1}))),i(t,"blur",(()=>s(window,"wheel",o)))}const{min:u,max:h,floor:d,round:m}=Math;function f(t,e,o){e/=100,o/=100;const n=d(t=t/360*6),i=t-n,s=o*(1-e),r=o*(1-i*e),a=o*(1-(1-i)*e),l=n%6;return[255*[o,r,s,s,a,o][l],255*[a,o,o,r,s,s][l],255*[s,s,a,o,o,r][l]]}function v(t,e,o){const n=(2-(e/=100))*(o/=100)/2;return 0!==n&&(e=1===n?0:n<.5?e*o/(2*n):e*o/(2-2*n)),[t,100*e,100*n]}function b(t,e,o){const n=u(t/=255,e/=255,o/=255),i=h(t,e,o),s=i-n;let r,a;if(0===s)r=a=0;else{a=s/i;const n=((i-t)/6+s/2)/s,l=((i-e)/6+s/2)/s,c=((i-o)/6+s/2)/s;t===i?r=c-l:e===i?r=1/3+n-c:o===i&&(r=2/3+l-n),r<0?r+=1:r>1&&(r-=1)}return[360*r,100*a,100*i]}function y(t,e,o,n){e/=100,o/=100;return[...b(255*(1-u(1,(t/=100)*(1-(n/=100))+n)),255*(1-u(1,e*(1-n)+n)),255*(1-u(1,o*(1-n)+n)))]}function g(t,e,o){e/=100;const n=2*(e*=(o/=100)<.5?o:1-o)/(o+e)*100,i=100*(o+e);return[t,isNaN(n)?0:n,i]}function _(t){return b(...t.match(/.{2}/g).map((t=>parseInt(t,16))))}function w(t){t=t.match(/^[a-zA-Z]+$/)?function(t){if("black"===t.toLowerCase())return"#000";const e=document.createElement("canvas").getContext("2d");return e.fillStyle=t,"#000"===e.fillStyle?null:e.fillStyle}(t):t;const e={cmyk:/^cmyk\D+([\d.]+)\D+([\d.]+)\D+([\d.]+)\D+([\d.]+)/i,rgba:/^rgba?\D+([\d.]+)(%?)\D+([\d.]+)(%?)\D+([\d.]+)(%?)\D*?(([\d.]+)(%?)|$)/i,hsla:/^hsla?\D+([\d.]+)\D+([\d.]+)\D+([\d.]+)\D*?(([\d.]+)(%?)|$)/i,hsva:/^hsva?\D+([\d.]+)\D+([\d.]+)\D+([\d.]+)\D*?(([\d.]+)(%?)|$)/i,hexa:/^#?(([\dA-Fa-f]{3,4})|([\dA-Fa-f]{6})|([\dA-Fa-f]{8}))$/i},o=t=>t.map((t=>/^(|\d+)\.\d+|\d+$/.test(t)?Number(t):void 0));let n;t:for(const i in e)if(n=e[i].exec(t))switch(i){case"cmyk":{const[,t,e,s,r]=o(n);if(t>100||e>100||s>100||r>100)break t;return{values:y(t,e,s,r),type:i}}case"rgba":{let[,t,,e,,s,,,r]=o(n);if(t="%"===n[2]?t/100*255:t,e="%"===n[4]?e/100*255:e,s="%"===n[6]?s/100*255:s,r="%"===n[9]?r/100:r,t>255||e>255||s>255||r<0||r>1)break t;return{values:[...b(t,e,s),r],a:r,type:i}}case"hexa":{let[,t]=n;4!==t.length&&3!==t.length||(t=t.split("").map((t=>t+t)).join(""));const e=t.substring(0,6);let o=t.substring(6);return o=o?parseInt(o,16)/255:void 0,{values:[..._(e),o],a:o,type:i}}case"hsla":{let[,t,e,s,,r]=o(n);if(r="%"===n[6]?r/100:r,t>360||e>100||s>100||r<0||r>1)break t;return{values:[...g(t,e,s),r],a:r,type:i}}case"hsva":{let[,t,e,s,,r]=o(n);if(r="%"===n[6]?r/100:r,t>360||e>100||s>100||r<0||r>1)break t;return{values:[t,e,s,r],a:r,type:i}}}return{values:null,type:null}}function A(t=0,e=0,o=0,n=1){const i=(t,e)=>(o=-1)=>e(~o?t.map((t=>Number(t.toFixed(o)))):t),s={h:t,s:e,v:o,a:n,toHSVA(){const t=[s.h,s.s,s.v,s.a];return t.toString=i(t,(t=>`hsva(${t[0]}, ${t[1]}%, ${t[2]}%, ${s.a})`)),t},toHSLA(){const t=[...v(s.h,s.s,s.v),s.a];return t.toString=i(t,(t=>`hsla(${t[0]}, ${t[1]}%, ${t[2]}%, ${s.a})`)),t},toRGBA(){const t=[...f(s.h,s.s,s.v),s.a];return t.toString=i(t,(t=>`rgba(${t[0]}, ${t[1]}, ${t[2]}, ${s.a})`)),t},toCMYK(){const t=function(t,e,o){const n=f(t,e,o),i=n[0]/255,s=n[1]/255,r=n[2]/255,a=u(1-i,1-s,1-r);return[100*(1===a?0:(1-i-a)/(1-a)),100*(1===a?0:(1-s-a)/(1-a)),100*(1===a?0:(1-r-a)/(1-a)),100*a]}(s.h,s.s,s.v);return t.toString=i(t,(t=>`cmyk(${t[0]}%, ${t[1]}%, ${t[2]}%, ${t[3]}%)`)),t},toHEXA(){const t=function(t,e,o){return f(t,e,o).map((t=>m(t).toString(16).padStart(2,"0")))}(s.h,s.s,s.v),e=s.a>=1?"":Number((255*s.a).toFixed(0)).toString(16).toUpperCase().padStart(2,"0");return e&&t.push(e),t.toString=()=>`#${t.join("").toUpperCase()}`,t},clone:()=>A(s.h,s.s,s.v,s.a)};return s}const $=t=>Math.max(Math.min(t,1),0);function C(t){const e={options:Object.assign({lock:null,onchange:()=>0,onstop:()=>0},t),_keyboard(t){const{options:o}=e,{type:n,key:i}=t;if(document.activeElement===o.wrapper){const{lock:o}=e.options,s="ArrowUp"===i,r="ArrowRight"===i,a="ArrowDown"===i,l="ArrowLeft"===i;if("keydown"===n&&(s||r||a||l)){let n=0,i=0;"v"===o?n=s||r?1:-1:"h"===o?n=s||r?-1:1:(i=s?-1:a?1:0,n=l?-1:r?1:0),e.update($(e.cache.x+.01*n),$(e.cache.y+.01*i)),t.preventDefault()}else i.startsWith("Arrow")&&(e.options.onstop(),t.preventDefault())}},_tapstart(t){i(document,["mouseup","touchend","touchcancel"],e._tapstop),i(document,["mousemove","touchmove"],e._tapmove),t.cancelable&&t.preventDefault(),e._tapmove(t)},_tapmove(t){const{options:o,cache:n}=e,{lock:i,element:s,wrapper:r}=o,a=r.getBoundingClientRect();let l=0,c=0;if(t){const e=t&&t.touches&&t.touches[0];l=t?(e||t).clientX:0,c=t?(e||t).clientY:0,la.left+a.width&&(l=a.left+a.width),ca.top+a.height&&(c=a.top+a.height),l-=a.left,c-=a.top}else n&&(l=n.x*a.width,c=n.y*a.height);"h"!==i&&(s.style.left=`calc(${l/a.width*100}% - ${s.offsetWidth/2}px)`),"v"!==i&&(s.style.top=`calc(${c/a.height*100}% - ${s.offsetHeight/2}px)`),e.cache={x:l/a.width,y:c/a.height};const p=$(l/a.width),u=$(c/a.height);switch(i){case"v":return o.onchange(p);case"h":return o.onchange(u);default:return o.onchange(p,u)}},_tapstop(){e.options.onstop(),s(document,["mouseup","touchend","touchcancel"],e._tapstop),s(document,["mousemove","touchmove"],e._tapmove)},trigger(){e._tapmove()},update(t=0,o=0){const{left:n,top:i,width:s,height:r}=e.options.wrapper.getBoundingClientRect();"h"===e.options.lock&&(o=t),e._tapmove({clientX:n+s*t,clientY:i+r*o})},destroy(){const{options:t,_tapstart:o,_keyboard:n}=e;s(document,["keydown","keyup"],n),s([t.wrapper,t.element],"mousedown",o),s([t.wrapper,t.element],"touchstart",o,{passive:!1})}},{options:o,_tapstart:n,_keyboard:r}=e;return i([o.wrapper,o.element],"mousedown",n),i([o.wrapper,o.element],"touchstart",n,{passive:!1}),i(document,["keydown","keyup"],r),e}function k(t={}){t=Object.assign({onchange:()=>0,className:"",elements:[]},t);const e=i(t.elements,"click",(e=>{t.elements.forEach((o=>o.classList[e.target===o?"add":"remove"](t.className))),t.onchange(e),e.stopPropagation()}));return{destroy:()=>s(...e)}}const S={variantFlipOrder:{start:"sme",middle:"mse",end:"ems"},positionFlipOrder:{top:"tbrl",right:"rltb",bottom:"btrl",left:"lrbt"},position:"bottom",margin:8,padding:0},O=(t,e,o)=>{const n="object"!=typeof t||t instanceof HTMLElement?{reference:t,popper:e,...o}:t;return{update(t=n){const{reference:e,popper:o}=Object.assign(n,t);if(!o||!e)throw new Error("Popper- or reference-element missing.");return((t,e,o)=>{const{container:n,arrow:i,margin:s,padding:r,position:a,variantFlipOrder:l,positionFlipOrder:c}={container:document.documentElement.getBoundingClientRect(),...S,...o},{left:p,top:u}=e.style;e.style.left="0",e.style.top="0";const h=t.getBoundingClientRect(),d=e.getBoundingClientRect(),m={t:h.top-d.height-s,b:h.bottom+s,r:h.right+s,l:h.left-d.width-s},f={vs:h.left,vm:h.left+h.width/2-d.width/2,ve:h.left+h.width-d.width,hs:h.top,hm:h.bottom-h.height/2-d.height/2,he:h.bottom-d.height},[v,b="middle"]=a.split("-"),y=c[v],g=l[b],{top:_,left:w,bottom:A,right:$}=n;for(const t of y){const o="t"===t||"b"===t;let n=m[t];const[s,a]=o?["top","left"]:["left","top"],[l,c]=o?[d.height,d.width]:[d.width,d.height],[p,u]=o?[A,$]:[$,A],[v,b]=o?[_,w]:[w,_];if(!(np))for(const p of g){let m=f[(o?"v":"h")+p];if(!(mu)){if(m-=d[a],n-=d[s],e.style[a]=`${m}px`,e.style[s]=`${n}px`,i){const e=o?h.width/2:h.height/2,r=c/2,u=e>r,d=m+{s:u?r:e,m:r,e:u?r:c-e}[p],f=n+{t:l,b:0,r:0,l}[t];i.style[a]=`${d}px`,i.style[s]=`${f}px`}return t+p}}}return e.style.left=p,e.style.top=u,null})(e,o,n)}}};class E{static utils=o;static version="1.9.1";static I18N_DEFAULTS={"ui:dialog":"color picker dialog","btn:toggle":"toggle color picker dialog","btn:swatch":"color swatch","btn:last-color":"use previous color","btn:save":"Save","btn:cancel":"Cancel","btn:clear":"Clear","aria:btn:save":"save and close","aria:btn:cancel":"cancel and close","aria:btn:clear":"clear and close","aria:input":"color input field","aria:palette":"color selection area","aria:hue":"hue selection slider","aria:opacity":"selection slider"};static DEFAULT_OPTIONS={appClass:null,theme:"classic",useAsButton:!1,padding:8,disabled:!1,comparison:!0,closeOnScroll:!1,outputPrecision:0,lockOpacity:!1,autoReposition:!0,container:"body",components:{interaction:{}},i18n:{},swatches:null,inline:!1,sliders:null,default:"#42445a",defaultRepresentation:null,position:"bottom-middle",adjustableNumbers:!0,showAlways:!1,closeWithKey:"Escape"};_initializingActive=!0;_recalc=!0;_nanopop=null;_root=null;_color=A();_lastColor=A();_swatchColors=[];_setupAnimationFrame=null;_eventListener={init:[],save:[],hide:[],show:[],clear:[],change:[],changestop:[],cancel:[],swatchselect:[]};constructor(t){this.options=t=Object.assign({...E.DEFAULT_OPTIONS},t);const{swatches:e,components:o,theme:n,sliders:i,lockOpacity:s,padding:r}=t;["nano","monolith"].includes(n)&&!i&&(t.sliders="h"),o.interaction||(o.interaction={});const{preview:a,opacity:l,hue:c,palette:p}=o;o.opacity=!s&&l,o.palette=p||a||l||c,this._preBuild(),this._buildComponents(),this._bindEvents(),this._finalBuild(),e&&e.length&&e.forEach((t=>this.addSwatch(t)));const{button:u,app:h}=this._root;this._nanopop=O(u,h,{margin:r}),u.setAttribute("role","button"),u.setAttribute("aria-label",this._t("btn:toggle"));const d=this;this._setupAnimationFrame=requestAnimationFrame((function e(){if(!h.offsetWidth)return requestAnimationFrame(e);d.setColor(t.default),d._rePositioningPicker(),t.defaultRepresentation&&(d._representation=t.defaultRepresentation,d.setColorRepresentation(d._representation)),t.showAlways&&d.show(),d._initializingActive=!1,d._emit("init")}))}static create=t=>new E(t);_preBuild(){const{options:t}=this;for(const e of["el","container"])t[e]=c(t[e]);this._root=(t=>{const{components:e,useAsButton:o,inline:n,appClass:i,theme:s,lockOpacity:r}=t.options,l=t=>t?"":'style="display:none" hidden',c=e=>t._t(e),p=a(`\n
\n\n ${o?"":''}\n\n
\n
\n
\n \n
\n
\n\n
\n
\n
\n
\n\n
\n
\n
\n
\n\n
\n
\n
\n
\n
\n\n
\n\n
\n \n\n \n \n \n \n \n\n \n \n \n
\n
\n
\n `),u=p.interaction;return u.options.find((t=>!t.hidden&&!t.classList.add("active"))),u.type=()=>u.options.find((t=>t.classList.contains("active"))),p})(this),t.useAsButton&&(this._root.button=t.el),t.container.appendChild(this._root.root)}_finalBuild(){const t=this.options,e=this._root;if(t.container.removeChild(e.root),t.inline){const o=t.el.parentElement;t.el.nextSibling?o.insertBefore(e.app,t.el.nextSibling):o.appendChild(e.app)}else t.container.appendChild(e.app);t.useAsButton?t.inline&&t.el.remove():t.el.parentNode.replaceChild(e.root,t.el),t.disabled&&this.disable(),t.comparison||(e.button.style.transition="none",t.useAsButton||(e.preview.lastColor.style.transition="none")),this.hide()}_buildComponents(){const t=this,e=this.options.components,o=(t.options.sliders||"v").repeat(2),[n,i]=o.match(/^[vh]+$/g)?o:[],s=()=>this._color||(this._color=this._lastColor.clone()),r={palette:C({element:t._root.palette.picker,wrapper:t._root.palette.palette,onstop:()=>t._emit("changestop","slider",t),onchange(o,n){if(!e.palette)return;const i=s(),{_root:r,options:a}=t,{lastColor:l,currentColor:c}=r.preview;t._recalc&&(i.s=100*o,i.v=100-100*n,i.v<0&&(i.v=0),t._updateOutput("slider"));const p=i.toRGBA().toString(0);this.element.style.background=p,this.wrapper.style.background=`\n linear-gradient(to top, rgba(0, 0, 0, ${i.a}), transparent),\n linear-gradient(to left, hsla(${i.h}, 100%, 50%, ${i.a}), rgba(255, 255, 255, ${i.a}))\n `,a.comparison?a.useAsButton||t._lastColor||l.style.setProperty("--pcr-color",p):(r.button.style.setProperty("--pcr-color",p),r.button.classList.remove("clear"));const u=i.toHEXA().toString();for(const{el:e,color:o}of t._swatchColors)e.classList[u===o.toHEXA().toString()?"add":"remove"]("pcr-active");c.style.setProperty("--pcr-color",p)}}),hue:C({lock:"v"===i?"h":"v",element:t._root.hue.picker,wrapper:t._root.hue.slider,onstop:()=>t._emit("changestop","slider",t),onchange(o){if(!e.hue||!e.palette)return;const n=s();t._recalc&&(n.h=360*o),this.element.style.backgroundColor=`hsl(${n.h}, 100%, 50%)`,r.palette.trigger()}}),opacity:C({lock:"v"===n?"h":"v",element:t._root.opacity.picker,wrapper:t._root.opacity.slider,onstop:()=>t._emit("changestop","slider",t),onchange(o){if(!e.opacity||!e.palette)return;const n=s();t._recalc&&(n.a=Math.round(100*o)/100),this.element.style.background=`rgba(0, 0, 0, ${n.a})`,r.palette.trigger()}}),selectable:k({elements:t._root.interaction.options,className:"active",onchange(e){t._representation=e.target.getAttribute("data-type").toUpperCase(),t._recalc&&t._updateOutput("swatch")}})};this._components=r}_bindEvents(){const{_root:t,options:e}=this,o=[i(t.interaction.clear,"click",(()=>this._clearColor())),i([t.interaction.cancel,t.preview.lastColor],"click",(()=>{this.setHSVA(...(this._lastColor||this._color).toHSVA(),!0),this._emit("cancel")})),i(t.interaction.save,"click",(()=>{!this.applyColor()&&!e.showAlways&&this.hide()})),i(t.interaction.result,["keyup","input"],(t=>{this.setColor(t.target.value,!0)&&!this._initializingActive&&(this._emit("change",this._color,"input",this),this._emit("changestop","input",this)),t.stopImmediatePropagation()})),i(t.interaction.result,["focus","blur"],(t=>{this._recalc="blur"===t.type,this._recalc&&this._updateOutput(null)})),i([t.palette.palette,t.palette.picker,t.hue.slider,t.hue.picker,t.opacity.slider,t.opacity.picker],["mousedown","touchstart"],(()=>this._recalc=!0),{passive:!0})];if(!e.showAlways){const n=e.closeWithKey;o.push(i(t.button,"click",(()=>this.isOpen()?this.hide():this.show())),i(document,"keyup",(t=>this.isOpen()&&(t.key===n||t.code===n)&&this.hide())),i(document,["touchstart","mousedown"],(e=>{this.isOpen()&&!l(e).some((e=>e===t.app||e===t.button))&&this.hide()}),{capture:!0}))}if(e.adjustableNumbers){const e={rgba:[255,255,255,1],hsva:[360,100,100,1],hsla:[360,100,100,1],cmyk:[100,100,100,100]};p(t.interaction.result,((t,o,n)=>{const i=e[this.getColorRepresentation().toLowerCase()];if(i){const e=i[n],s=t+(e>=100?1e3*o:o);return s<=0?0:Number((s{n.isOpen()&&(e.closeOnScroll&&n.hide(),null===t?(t=setTimeout((()=>t=null),100),requestAnimationFrame((function e(){n._rePositioningPicker(),null!==t&&requestAnimationFrame(e)}))):(clearTimeout(t),t=setTimeout((()=>t=null),100)))}),{capture:!0}))}this._eventBindings=o}_rePositioningPicker(){const{options:t}=this;if(!t.inline){if(!this._nanopop.update({container:document.body.getBoundingClientRect(),position:t.position})){const t=this._root.app,e=t.getBoundingClientRect();t.style.top=(window.innerHeight-e.height)/2+"px",t.style.left=(window.innerWidth-e.width)/2+"px"}}}_updateOutput(t){const{_root:e,_color:o,options:n}=this;if(e.interaction.type()){const t=`to${e.interaction.type().getAttribute("data-type")}`;e.interaction.result.value="function"==typeof o[t]?o[t]().toString(n.outputPrecision):""}!this._initializingActive&&this._recalc&&this._emit("change",o,t,this)}_clearColor(t=!1){const{_root:e,options:o}=this;o.useAsButton||e.button.style.setProperty("--pcr-color","rgba(0, 0, 0, 0.15)"),e.button.classList.add("clear"),o.showAlways||this.hide(),this._lastColor=null,this._initializingActive||t||(this._emit("save",null),this._emit("clear"))}_parseLocalColor(t){const{values:e,type:o,a:n}=w(t),{lockOpacity:i}=this.options,s=void 0!==n&&1!==n;return e&&3===e.length&&(e[3]=void 0),{values:!e||i&&s?null:e,type:o}}_t(t){return this.options.i18n[t]||E.I18N_DEFAULTS[t]}_emit(t,...e){this._eventListener[t].forEach((t=>t(...e,this)))}on(t,e){return this._eventListener[t].push(e),this}off(t,e){const o=this._eventListener[t]||[],n=o.indexOf(e);return~n&&o.splice(n,1),this}addSwatch(t){const{values:e}=this._parseLocalColor(t);if(e){const{_swatchColors:t,_root:o}=this,n=A(...e),s=r(` + +
+ Custom: + +
+ +

@@ -177,7 +202,7 @@ id="navGestures" onclick="selectNavMode('gestures')" > - {{_('Gestures (for mobile)')}} + {{_('Gestures (mobile)')}}

@@ -272,6 +297,14 @@ useBookmarks: "{{ current_user.is_authenticated | tojson }}", }; + // load custom theme color from localStorage (if any) + var _savedCustomThemeColor = null; + try { + _savedCustomThemeColor = localStorage.getItem( + "calibre.reader.customTheme" + ); + } catch (e) {} + window.themes = { darkTheme: { bgColor: "#202124", @@ -291,33 +324,67 @@ amberTheme: { bgColor: "#e8bf5e", css_path: "{{ url_for('static', filename='css/epub_themes.css') }}", - "title-color": "#4f4f4f", + "title-color": "#2f2f2f", }, blackTheme: { bgColor: "black", css_path: "{{ url_for('static', filename='css/epub_themes.css') }}", "title-color": "#fff", }, + customTheme: { + bgColor: _savedCustomThemeColor || "#ffffff", + css_path: "{{ url_for('static', filename='css/epub_themes.css') }}", + "title-color": "#4f4f4f", + }, }; function selectTheme(id) { - let tickSpans = document + // Collect all span tick markers inside themes and clear them safely + var tickSpans = document .getElementById("themes") .querySelectorAll("span"); - - // Remove tick mark from all theme buttons tickSpans.forEach(function (tickSpan) { - document.getElementById(tickSpan.id).textContent = ""; + try { + tickSpan.textContent = ""; + } catch (e) {} }); - // Add tick mark to the button corresponding to the currently selected theme - document.getElementById(id).querySelector("span").textContent = "✓"; + // If the theme button exists, set its inner span to a tick. Otherwise set any span with the matching id. + var el = document.getElementById(id); + if (el) { + var sp = el.querySelector("span"); + if (sp) sp.textContent = "✓"; + } else { + var spById = + document.getElementById(id + "Selected") || + document.getElementById("customSelected"); + if (spById) spById.textContent = "✓"; + } // Saving theme to local storage localStorage.setItem("calibre.reader.theme", id); - // Apply theme to epubjs iframe - reader.rendition.themes.select(id); + // If selecting custom theme, ensure epubjs theme is registered with chosen bg color + if (id === "customTheme") { + var customColor = window.themes.customTheme.bgColor || "#ffffff"; + try { + if (reader && reader.rendition && reader.rendition.themes) { + reader.rendition.themes.register("customTheme", { + body: { + background: customColor, + }, + }); + reader.rendition.themes.select("customTheme"); + } + } catch (e) { + console.error("Failed to register/select customTheme", e); + } + } else { + // Apply theme to epubjs iframe + try { + reader.rendition.themes.select(id); + } catch (e) {} + } // Apply theme to rest of the page. document.getElementById("main").style.backgroundColor = @@ -432,6 +499,177 @@ } })(); + + + + + + From bba7812a07ac2b756365e40706de62135eafd67b Mon Sep 17 00:00:00 2001 From: IgorKurkov Date: Wed, 29 Oct 2025 22:20:08 +0100 Subject: [PATCH 9/9] feat(epub.js): add autosaving of current book reading position in localStorage to open this book on the last read point --- cps/static/js/reading/epub.js | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/cps/static/js/reading/epub.js b/cps/static/js/reading/epub.js index 9f906674e..d24a5e7c2 100644 --- a/cps/static/js/reading/epub.js +++ b/cps/static/js/reading/epub.js @@ -166,6 +166,8 @@ var reader; reader.book.ready.then(() => { let locations_key = reader.book.key() + "-locations"; + // Key to persist last-read position for this book in localStorage + let position_key = "calibre.reader.position." + reader.book.key(); let stored_locations = localStorage.getItem(locations_key); let make_locations, save_locations; if (stored_locations) { @@ -185,6 +187,22 @@ var reader; } make_locations .then(() => { + // Try to restore last position (CFI) from localStorage if present + try { + var _savedPos = localStorage.getItem(position_key); + if (_savedPos) { + try { + var _posObj = JSON.parse(_savedPos); + if (_posObj && _posObj.cfi) { + // Display the saved CFI location + try { + reader.rendition.display(_posObj.cfi); + } catch (e) {} + } + } catch (e) {} + } + } catch (e) {} + reader.rendition.on("relocated", (location) => { let percentage = Math.round(location.end.percentage * 100); progressDiv.textContent = percentage + "%"; @@ -194,7 +212,6 @@ var reader; const current = reader.book.locations.locationFromCfi(cfi) || 0; // 1-based index typically const total = reader.book.locations.length() || 0; - const remaining = Math.max(total - current, 0); if (total > 0) { pagesDiv.textContent = current + "/" + total; @@ -203,6 +220,18 @@ var reader; pagesDiv.textContent = ""; pagesDiv.style.visibility = "hidden"; } + + // Persist last position (CFI + percentage) to localStorage so reader can restore on next open + try { + var posObj = { + cfi: location.start.cfi, + percentage: location.start.percentage, + }; + localStorage.setItem( + position_key, + JSON.stringify(posObj) + ); + } catch (e) {} }); reader.rendition.reportLocation(); progressDiv.style.visibility = "visible";