Merge Reader improvements
This commit is contained in:
21
cps.py
21
cps.py
@@ -42,9 +42,26 @@ def hide_console_windows():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
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()
|
hide_console_windows()
|
||||||
main()
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
# Allow stopping the server with Ctrl+C from an interactive console, after reboot of calibre-web
|
||||||
|
# (initally the SIGINT signal CTRL+C is catched in server.py via the signal callback).
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -216,6 +216,12 @@ class WebServer(object):
|
|||||||
try:
|
try:
|
||||||
sock, output = self._make_gevent_listener()
|
sock, output = self._make_gevent_listener()
|
||||||
log.info('Starting Gevent server on %s', output)
|
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,
|
self.wsgiserver = WSGIServer(sock, self.app, log=self.access_logger, handler_class=MyWSGIHandler,
|
||||||
error_log=log,
|
error_log=log,
|
||||||
spawn=Pool(), **ssl_args)
|
spawn=Pool(), **ssl_args)
|
||||||
@@ -266,6 +272,12 @@ class WebServer(object):
|
|||||||
output = _readable_listen_address(self.listen_address, self.listen_port)
|
output = _readable_listen_address(self.listen_address, self.listen_port)
|
||||||
http_server.listen(self.listen_port, self.listen_address)
|
http_server.listen(self.listen_port, self.listen_address)
|
||||||
log.info('Starting Tornado server on %s', output)
|
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 = IOLoop.current()
|
||||||
self.wsgiserver.start()
|
self.wsgiserver.start()
|
||||||
|
|||||||
@@ -13,6 +13,11 @@
|
|||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.amberTheme {
|
||||||
|
background: #e8bf5e;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
.blackTheme {
|
.blackTheme {
|
||||||
background: #000;
|
background: #000;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'fontello';
|
font-family: "fontello";
|
||||||
src: url('fonts/fontello.eot?60518104');
|
src: url("fonts/fontello.eot?60518104");
|
||||||
src:
|
src: url("fonts/fontello.eot?60518104#iefix") format("embedded-opentype"),
|
||||||
url('fonts/fontello.eot?60518104#iefix') format('embedded-opentype'),
|
url("fonts/fontello.woff?60518104") format("woff"),
|
||||||
url('fonts/fontello.woff?60518104') format('woff'),
|
url("fonts/fontello.ttf?60518104") format("truetype"),
|
||||||
url('fonts/fontello.ttf?60518104') format('truetype'),
|
url("fonts/fontello.svg?60518104#fontello") format("svg");
|
||||||
url('fonts/fontello.svg?60518104#fontello') format('svg');
|
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
@@ -16,7 +15,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.myselect {
|
.myselect {
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#main {
|
#main {
|
||||||
@@ -48,7 +47,7 @@ body {
|
|||||||
-webkit-transition: opacity 0.5s;
|
-webkit-transition: opacity 0.5s;
|
||||||
-moz-transition: opacity 0.5s;
|
-moz-transition: opacity 0.5s;
|
||||||
transition: opacity 0.5s;
|
transition: opacity 0.5s;
|
||||||
z-index: 10;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
#titlebar:hover {
|
#titlebar:hover {
|
||||||
@@ -93,14 +92,18 @@ body {
|
|||||||
box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
|
box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
#book-title { font-weight: 600; }
|
#book-title {
|
||||||
#title-seperator { display: none; }
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
#title-seperator {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
#viewer {
|
#viewer {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
height: 80%;
|
height: 80%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
z-index: 2;
|
z-index: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -136,6 +139,7 @@ body {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
padding-top: 160px;
|
padding-top: 160px;
|
||||||
padding-bottom: 160px;
|
padding-bottom: 160px;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow:hover {
|
.arrow:hover {
|
||||||
@@ -200,10 +204,16 @@ body {
|
|||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#title-controls { float: right; }
|
#title-controls {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
#panels a::before { visibility: visible; }
|
#panels a::before {
|
||||||
#panels a:hover { color: #aaa; }
|
visibility: visible;
|
||||||
|
}
|
||||||
|
#panels a:hover {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
.list_item.currentChapter > a,
|
.list_item.currentChapter > a,
|
||||||
.list_item a:hover {
|
.list_item a:hover {
|
||||||
@@ -237,8 +247,12 @@ body {
|
|||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input::-webkit-input-placeholder { color: #454545; }
|
input::-webkit-input-placeholder {
|
||||||
input:-moz-placeholder { color: #454545; }
|
color: #454545;
|
||||||
|
}
|
||||||
|
input:-moz-placeholder {
|
||||||
|
color: #454545;
|
||||||
|
}
|
||||||
|
|
||||||
#divider {
|
#divider {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -378,14 +392,14 @@ input:-moz-placeholder { color: #454545; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toc_toggle::before {
|
.toc_toggle::before {
|
||||||
content: '▸';
|
content: "▸";
|
||||||
color: #fff;
|
color: #fff;
|
||||||
margin-right: -4px;
|
margin-right: -4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.currentChapter > .toc_toggle::before,
|
.currentChapter > .toc_toggle::before,
|
||||||
.openChapter > .toc_toggle::before {
|
.openChapter > .toc_toggle::before {
|
||||||
content: '▾';
|
content: "▾";
|
||||||
}
|
}
|
||||||
|
|
||||||
.view {
|
.view {
|
||||||
@@ -499,21 +513,33 @@ input:-moz-placeholder { color: #454545; }
|
|||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#settingsPanel .xsmall { font-size: x-small; }
|
#settingsPanel .xsmall {
|
||||||
#settingsPanel .small { font-size: small; }
|
font-size: x-small;
|
||||||
#settingsPanel .medium { font-size: medium; }
|
}
|
||||||
#settingsPanel .large { font-size: large; }
|
#settingsPanel .small {
|
||||||
#settingsPanel .xlarge { font-size: x-large; }
|
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 {
|
.modal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
width: 100vw;
|
width: 97vw;
|
||||||
height: auto;
|
height: 90dvh;
|
||||||
max-width: 630px;
|
max-width: 630px;
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
@@ -539,7 +565,7 @@ input:-moz-placeholder { color: #454545; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
.md-show ~ .overlay {
|
.md-show ~ .overlay {
|
||||||
opacity: 1;
|
/* opacity: 1; */
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,7 +580,7 @@ input:-moz-placeholder { color: #454545; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
.md-content > div {
|
.md-content > div {
|
||||||
padding: 15px 40px 30px;
|
padding: 10px 20px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -581,6 +607,7 @@ input:-moz-placeholder { color: #454545; }
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.md-content .themes button.lightTheme,
|
||||||
.md-content .themes button.whiteTheme {
|
.md-content .themes button.whiteTheme {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
color: black;
|
color: black;
|
||||||
@@ -591,6 +618,11 @@ input:-moz-placeholder { color: #454545; }
|
|||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.md-content .themes button.amberTheme {
|
||||||
|
background-color: #e8bf5e;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
.md-content .themes button.blackTheme {
|
.md-content .themes button.blackTheme {
|
||||||
background-color: black;
|
background-color: black;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -638,31 +670,47 @@ input:-moz-placeholder { color: #454545; }
|
|||||||
|
|
||||||
@media only screen and (max-width: 900px) {
|
@media only screen and (max-width: 900px) {
|
||||||
#viewer {
|
#viewer {
|
||||||
width: 60%;
|
width: 100%;
|
||||||
margin-left: 20%;
|
height: calc(100dvh - 99px);
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#viewer iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#prev {
|
#prev {
|
||||||
left: 20px;
|
left: 20px;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
#next {
|
#next {
|
||||||
right: 20px;
|
right: 20px;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 550px) {
|
@media only screen and (max-width: 550px) {
|
||||||
#viewer {
|
#viewer {
|
||||||
width: 80%;
|
width: 100%;
|
||||||
margin-left: 10%;
|
height: calc(100dvh - 99px);
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#viewer iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#prev {
|
#prev {
|
||||||
left: 0;
|
left: 0;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
#next {
|
#next {
|
||||||
right: 0;
|
right: 0;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow {
|
.arrow {
|
||||||
@@ -712,11 +760,7 @@ input:-moz-placeholder { color: #454545; }
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen
|
@media only screen and (min-device-width: 768px) and (max-device-width: 1024px) and (orientation: landscape) /* and (-webkit-min-device-pixel-ratio: 2) */ {
|
||||||
and (min-device-width: 768px)
|
|
||||||
and (max-device-width: 1024px)
|
|
||||||
and (orientation: landscape)
|
|
||||||
/* and (-webkit-min-device-pixel-ratio: 2)*/ {
|
|
||||||
#viewer {
|
#viewer {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
margin-left: 10%;
|
margin-left: 10%;
|
||||||
@@ -739,47 +783,52 @@ input:-moz-placeholder { color: #454545; }
|
|||||||
/* For iPhone 6\6s portrait layouts only */
|
/* For iPhone 6\6s portrait layouts only */
|
||||||
@media only screen and (min-device-width: 375px) and (max-device-width: 667px) and (orientation: portrait) {
|
@media only screen and (min-device-width: 375px) and (max-device-width: 667px) and (orientation: portrait) {
|
||||||
#viewer {
|
#viewer {
|
||||||
width: 300px;
|
width: 100%;
|
||||||
height: 480px;
|
height: calc(100dvh - 99px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#viewer iframe {
|
#viewer iframe {
|
||||||
width: 300px;
|
width: 100%;
|
||||||
height: 480px;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* For iPhone 6\6s landscape layouts only */
|
/* For iPhone 6\6s landscape layouts only */
|
||||||
@media only screen and (min-device-width: 375px) and (max-device-width: 667px) and (orientation: landscape) {
|
@media only screen and (min-device-width: 375px) and (max-device-width: 667px) and (orientation: landscape) {
|
||||||
#viewer {
|
#viewer {
|
||||||
width: 450px;
|
width: 100%;
|
||||||
height: 300px;
|
height: calc(100dvh - 99px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#viewer iframe {
|
#viewer iframe {
|
||||||
width: 450px;
|
width: 100%;
|
||||||
height: 300px;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* For iPhone portrait layouts only */
|
/* For iPhone portrait layouts only */
|
||||||
@media only screen and (max-device-width: 374px) and (orientation: portrait) {
|
@media only screen and (max-device-width: 374px) and (orientation: portrait) {
|
||||||
#viewer {
|
#viewer {
|
||||||
width: 256px;
|
width: 100%;
|
||||||
height: 432px;
|
height: calc(100dvh - 99px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#viewer iframe {
|
#viewer iframe {
|
||||||
width: 256px;
|
width: 100%;
|
||||||
height: 432px;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* For iPhone landscape layouts only */
|
/* For iPhone landscape layouts only */
|
||||||
@media only screen and (max-device-width: 374px) and (orientation: landscape) {
|
@media only screen and (max-device-width: 374px) and (orientation: landscape) {
|
||||||
|
#viewer {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100dvh - 99px);
|
||||||
|
}
|
||||||
|
|
||||||
#viewer iframe {
|
#viewer iframe {
|
||||||
width: 256px;
|
width: 100%;
|
||||||
height: 124px;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -803,32 +852,76 @@ input:-moz-placeholder { color: #454545; }
|
|||||||
font-size: 112%;
|
font-size: 112%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-search::before { content: '\e807'; } /* '' */
|
.icon-search::before {
|
||||||
.icon-resize-full-1::before { content: '\e804'; } /* '' */
|
content: "\e807";
|
||||||
.icon-cancel-circled2::before { content: '\e80f'; } /* '' */
|
} /* '' */
|
||||||
.icon-link::before { content: '\e80d'; } /* '' */
|
.icon-resize-full-1::before {
|
||||||
.icon-bookmark::before { content: '\e805'; } /* '' */
|
content: "\e804";
|
||||||
.icon-bookmark-empty::before { content: '\e806'; } /* '' */
|
} /* '' */
|
||||||
.icon-download-cloud::before { content: '\e811'; } /* '' */
|
.icon-cancel-circled2::before {
|
||||||
.icon-edit::before { content: '\e814'; } /* '' */
|
content: "\e80f";
|
||||||
.icon-menu::before { content: '\e802'; } /* '' */
|
} /* '' */
|
||||||
.icon-cog::before { content: '\e813'; } /* '' */
|
.icon-link::before {
|
||||||
.icon-resize-full::before { content: '\e812'; } /* '' */
|
content: "\e80d";
|
||||||
.icon-cancel-circled::before { content: '\e80e'; } /* '' */
|
} /* '' */
|
||||||
.icon-up-dir::before { content: '\e80c'; } /* '' */
|
.icon-bookmark::before {
|
||||||
.icon-right-dir::before { content: '\e80b'; } /* '' */
|
content: "\e805";
|
||||||
.icon-angle-right::before { content: '\e809'; } /* '' */
|
} /* '' */
|
||||||
.icon-angle-down::before { content: '\e80a'; } /* '' */
|
.icon-bookmark-empty::before {
|
||||||
.icon-right::before { content: '\e815'; } /* '' */
|
content: "\e806";
|
||||||
.icon-list-1::before { content: '\e803'; } /* '' */
|
} /* '' */
|
||||||
.icon-list-numbered::before { content: '\e801'; } /* '' */
|
.icon-download-cloud::before {
|
||||||
.icon-columns::before { content: '\e810'; } /* '' */
|
content: "\e811";
|
||||||
.icon-list::before { content: '\e800'; } /* '' */
|
} /* '' */
|
||||||
.icon-resize-small::before { content: '\e808'; } /* '' */
|
.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{
|
#progress {
|
||||||
right: 4rem;
|
right: 4rem;
|
||||||
bottom: 4rem;
|
bottom: 2px;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,74 @@
|
|||||||
.fontSizeWrapper {
|
.fontSizeWrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.slider {
|
.slider {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translate(0,-50%);
|
transform: translate(0, -50%);
|
||||||
width: 90%;
|
width: 90%;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-shadow: 0px 15px 40px #7E6D5766;
|
box-shadow: 0px 15px 40px #7e6d5766;
|
||||||
}
|
}
|
||||||
.slider label {
|
.slider label {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-family: Open Sans;
|
font-family: Open Sans;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.slider input[type="range"] {
|
.slider input[type="range"] {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
height: 5px;
|
height: 5px;
|
||||||
background: black;
|
background: black;
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
font-size: 20px;
|
font-size: 14px;
|
||||||
font-weight: 400;
|
font-weight: 600;
|
||||||
font-family: Open Sans;
|
font-family: Open Sans;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item~button {
|
.item ~ button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border: none;
|
border: none;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
margin-top: 5%;
|
margin-top: 5%;
|
||||||
margin-right: 1%;
|
margin-right: 1%;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#main,
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|||||||
2
cps/static/js/libs/pickr/pickr.min.css
vendored
Normal file
2
cps/static/js/libs/pickr/pickr.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
3
cps/static/js/libs/pickr/pickr.min.js
vendored
Normal file
3
cps/static/js/libs/pickr/pickr.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
var reader;
|
var reader;
|
||||||
|
|
||||||
(function() {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
EPUBJS.filePath = calibre.filePath;
|
EPUBJS.filePath = calibre.filePath;
|
||||||
@@ -10,7 +10,7 @@ var reader;
|
|||||||
|
|
||||||
reader = ePubReader(calibre.bookUrl, {
|
reader = ePubReader(calibre.bookUrl, {
|
||||||
restore: true,
|
restore: true,
|
||||||
bookmarks: calibre.bookmark ? [calibre.bookmark] : []
|
bookmarks: calibre.bookmark ? [calibre.bookmark] : [],
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.keys(themes).forEach(function (theme) {
|
Object.keys(themes).forEach(function (theme) {
|
||||||
@@ -54,29 +54,92 @@ var reader;
|
|||||||
|
|
||||||
// Update progress percentage
|
// Update progress percentage
|
||||||
let progressDiv = document.getElementById("progress");
|
let progressDiv = document.getElementById("progress");
|
||||||
reader.book.ready.then((()=>{
|
// Pages counter (virtual pages via EPUB locations)
|
||||||
let locations_key = reader.book.key()+'-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";
|
||||||
|
// 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 stored_locations = localStorage.getItem(locations_key);
|
||||||
let make_locations, save_locations;
|
let make_locations, save_locations;
|
||||||
if (stored_locations) {
|
if (stored_locations) {
|
||||||
make_locations = Promise.resolve(reader.book.locations.load(stored_locations));
|
make_locations = Promise.resolve(
|
||||||
|
reader.book.locations.load(stored_locations)
|
||||||
|
);
|
||||||
// No-op because locations are already saved
|
// No-op because locations are already saved
|
||||||
save_locations = ()=>{};
|
save_locations = () => {};
|
||||||
} else {
|
} else {
|
||||||
make_locations = reader.book.locations.generate();
|
make_locations = reader.book.locations.generate();
|
||||||
save_locations = ()=>{
|
save_locations = () => {
|
||||||
localStorage.setItem(locations_key, reader.book.locations.save());
|
localStorage.setItem(
|
||||||
|
locations_key,
|
||||||
|
reader.book.locations.save()
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
make_locations.then(()=>{
|
make_locations
|
||||||
reader.rendition.on('relocated', (location)=>{
|
.then(() => {
|
||||||
let percentage = Math.round(location.end.percentage*100);
|
// Try to restore last position (CFI) from localStorage if present
|
||||||
progressDiv.textContent=percentage+"%";
|
try {
|
||||||
});
|
var _savedPos = localStorage.getItem(position_key);
|
||||||
reader.rendition.reportLocation();
|
if (_savedPos) {
|
||||||
progressDiv.style.visibility = "visible";
|
try {
|
||||||
}).then(save_locations);
|
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 + "%";
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
if (total > 0) {
|
||||||
|
pagesDiv.textContent = current + "/" + total;
|
||||||
|
pagesDiv.style.visibility = "visible";
|
||||||
|
} else {
|
||||||
|
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";
|
||||||
|
})
|
||||||
|
.then(save_locations);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} action - Add or remove bookmark
|
* @param {string} action - Add or remove bookmark
|
||||||
@@ -85,26 +148,43 @@ var reader;
|
|||||||
function updateBookmark(action, location) {
|
function updateBookmark(action, location) {
|
||||||
// Remove other bookmarks (there can only be one)
|
// Remove other bookmarks (there can only be one)
|
||||||
if (action === "add") {
|
if (action === "add") {
|
||||||
this.settings.bookmarks.filter(function (bookmark) {
|
this.settings.bookmarks
|
||||||
return bookmark && bookmark !== location;
|
.filter(function (bookmark) {
|
||||||
}).map(function (bookmark) {
|
return bookmark && bookmark !== location;
|
||||||
this.removeBookmark(bookmark);
|
})
|
||||||
}.bind(this));
|
.map(
|
||||||
|
function (bookmark) {
|
||||||
|
this.removeBookmark(bookmark);
|
||||||
|
}.bind(this)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var csrftoken = $("input[name='csrf_token']").val();
|
var csrftoken = $("input[name='csrf_token']").val();
|
||||||
|
|
||||||
// Save to database
|
// Save to database
|
||||||
$.ajax(calibre.bookmarkUrl, {
|
$.ajax(calibre.bookmarkUrl, {
|
||||||
method: "post",
|
method: "post",
|
||||||
data: { bookmark: location || "" },
|
data: { bookmark: location || "" },
|
||||||
headers: { "X-CSRFToken": csrftoken }
|
headers: { "X-CSRFToken": csrftoken },
|
||||||
}).fail(function (xhr, status, error) {
|
}).fail(function (xhr, status, error) {
|
||||||
alert(error);
|
alert(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default settings load
|
// Default settings load
|
||||||
const theme = localStorage.getItem("calibre.reader.theme") ?? "lightTheme";
|
const theme = localStorage.getItem("calibre.reader.theme") ?? "lightTheme";
|
||||||
selectTheme(theme);
|
selectTheme(theme);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
<!--a id="show-Search" class="show_view icon-search" data-view="Search">Search</a-->
|
<!--a id="show-Search" class="show_view icon-search" data-view="Search">Search</a-->
|
||||||
<a id="show-Toc" class="show_view icon-list-1 active" data-view="Toc">TOC</a>
|
<a id="show-Toc" class="show_view icon-list-1 active" data-view="Toc">TOC</a>
|
||||||
<a id="show-Bookmarks" class="show_view icon-bookmark" data-view="Bookmarks">Bookmarks</a>
|
<a id="show-Bookmarks" class="show_view icon-bookmark" data-view="Bookmarks">Bookmarks</a>
|
||||||
|
<a id="back-button" data-view="Back to Books" href="{{ url_for('web.index') }}">Books</a>
|
||||||
<!--a id="show-Notes" class="show_view icon-edit" data-view="Notes">Notes</a-->
|
<!--a id="show-Notes" class="show_view icon-edit" data-view="Notes">Notes</a-->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -49,7 +50,6 @@
|
|||||||
</div-->
|
</div-->
|
||||||
</div>
|
</div>
|
||||||
<div id="main">
|
<div id="main">
|
||||||
|
|
||||||
<div id="titlebar">
|
<div id="titlebar">
|
||||||
<div id="opener">
|
<div id="opener">
|
||||||
<a id="slider" class="icon-menu">Menu</a>
|
<a id="slider" class="icon-menu">Menu</a>
|
||||||
@@ -70,37 +70,58 @@
|
|||||||
<div id="prev" class="arrow">‹</div>
|
<div id="prev" class="arrow">‹</div>
|
||||||
<div id="viewer"></div>
|
<div id="viewer"></div>
|
||||||
<div id="next" class="arrow">›</div>
|
<div id="next" class="arrow">›</div>
|
||||||
<div id="progress">0%</div>
|
<div class="read-footer">
|
||||||
<div id="loader"><img src="{{ url_for('static', filename='img/loader.gif') }}"></div>
|
<div id="pages-count"></div>
|
||||||
</div>
|
<div id="progress">0%</div>
|
||||||
|
</div>
|
||||||
|
<div id="loader">
|
||||||
|
<img src="{{ url_for('static', filename='img/loader.gif') }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="modal md-effect-1" id="settings-modal">
|
<div class="modal md-effect-1" id="settings-modal">
|
||||||
<div class="md-content">
|
<div class="md-content">
|
||||||
<h3>{{_('Settings')}}</h3>
|
<h3>{{_('Settings')}}</h3>
|
||||||
<div class="form-group themes" id="themes">
|
<div class="form-group themes" id="themes">
|
||||||
{{_('Choose a theme below:')}}}<br />
|
{{_('Choose a theme below:')}}}<br />
|
||||||
|
|
||||||
<!-- Hardcoded a tick in the light theme button because it is the "default" theme. Need to find a way to do this dynamically on startup-->
|
<!-- Hardcoded a tick in the light theme button because it is the "default" theme. Need to find a way to do this dynamically on startup-->
|
||||||
<button type="button" id="lightTheme" class="lightTheme" onclick="selectTheme(this.id)"><span
|
<button type="button" id="lightTheme" class="lightTheme" onclick="selectTheme(this.id)"><span
|
||||||
id="lightSelected">✓</span>{{_('Light')}}</button>
|
id="lightSelected">✓</span>{{_('Light')}}</button>
|
||||||
<button type="button" id="darkTheme" class="darkTheme" onclick="selectTheme(this.id)"><span
|
<button type="button" id="darkTheme" class="darkTheme" onclick="selectTheme(this.id)"><span
|
||||||
id="darkSelected"> </span>{{_('Dark')}}</button>
|
id="darkSelected"> </span>{{_('Dark')}}</button>
|
||||||
<button type="button" id="sepiaTheme" class="sepiaTheme" onclick="selectTheme(this.id)"><span
|
<button type="button" id="sepiaTheme" class="sepiaTheme" onclick="selectTheme(this.id)"><span
|
||||||
id="sepiaSelected"> </span>{{_('Sepia')}}</button>
|
id="sepiaSelected"> </span>{{_('Sepia')}}</button>
|
||||||
|
<button type="button" id="amberTheme" class="amberTheme" onclick="selectTheme(this.id)"><span
|
||||||
|
id="amberSelected"> </span>{{_('Amber')}}</button>
|
||||||
<button type="button" id="blackTheme" class="blackTheme" onclick="selectTheme(this.id)"><span
|
<button type="button" id="blackTheme" class="blackTheme" onclick="selectTheme(this.id)"><span
|
||||||
id="blackSelected"> </span>{{_('Black')}}</button>
|
id="blackSelected"> </span>{{_('Black')}}</button>
|
||||||
</div>
|
|
||||||
|
<!-- Custom theme: color input + pickr -->
|
||||||
|
<div id="customThemeWrapper" style="display: inline-flex; align-items: center;gap: 8px;margin: 5px 8px;">Custom:
|
||||||
|
<!-- Picker-only UI: swatch acts as the picker button, and a small span shows selection tick -->
|
||||||
|
<div id="customThemeSwatch" title="Pick color" style="width: 30px; height: 30px; border: 1px solid #ccc; border-radius: 3px;cursor: pointer;"></div>
|
||||||
|
<span id="customSelected"> </span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<p>
|
<p>
|
||||||
<input type="checkbox" id="sidebarReflow"
|
<input type="checkbox" id="sidebarReflow"
|
||||||
name="sidebarReflow">{{_('Reflow text when sidebars are open.')}}
|
name="sidebarReflow">{{_('Reflow text when sidebars are open.')}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group fontSizeWrapper">
|
<div class="form-group">
|
||||||
<div class="slider">
|
<p>
|
||||||
<label for="fader">{{ _('Font Sizes') }}</label>
|
<input type="checkbox" id="showPagesCount" name="showPagesCount"/>{{_('Show pages count')}}
|
||||||
<input type="range" min="75" max="200" value="100" id="fontSizeFader" step="25">
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="form-group fontSizeWrapper">
|
||||||
|
<label>{{ _('Font Size') }}</label>
|
||||||
|
<div class="font-size-controls" style="display: flex;align-items: center;gap: 10px;margin-top: 2px;">
|
||||||
|
<button type="button" id="fontSizeDecrease" style="padding: 8px 15px; font-size: 18px; cursor: pointer">−</button>
|
||||||
|
<span id="fontSizeDisplay" style="min-width: 60px;text-align: center;font-size: 14px;font-weight: bold;">100%</span>
|
||||||
|
<button type="button" id="fontSizeIncrease" style="padding: 8px 15px; font-size: 18px; cursor: pointer">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="font" id="font">
|
<div class="font" id="font">
|
||||||
<label class="item">{{_('Font')}}:</label>
|
<label class="item">{{_('Font')}}:</label>
|
||||||
<button type="button" id="default" onclick="selectFont(this.id)"><span>✓</span>{{_('Default')}}</button>
|
<button type="button" id="default" onclick="selectFont(this.id)"><span>✓</span>{{_('Default')}}</button>
|
||||||
@@ -119,8 +140,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="overlay"></div>
|
<div class="overlay"></div>
|
||||||
<script src="{{ url_for('static', filename='js/libs/jquery.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/libs/jquery.min.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/compress/jszip_epub.min.js') }}">
|
<script src="{{ url_for('static', filename='js/compress/jszip_epub.min.js') }}"></script>
|
||||||
</script>
|
|
||||||
<script src="{{ url_for('static', filename='js/libs/epub.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/libs/epub.min.js') }}"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
window.calibre = {
|
window.calibre = {
|
||||||
@@ -132,6 +152,14 @@
|
|||||||
useBookmarks: "{{ current_user.is_authenticated | tojson }}"
|
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 = {
|
window.themes = {
|
||||||
"darkTheme": {
|
"darkTheme": {
|
||||||
"bgColor": "#202124",
|
"bgColor": "#202124",
|
||||||
@@ -148,6 +176,11 @@
|
|||||||
"css_path": "{{ url_for('static', filename='css/epub_themes.css') }}",
|
"css_path": "{{ url_for('static', filename='css/epub_themes.css') }}",
|
||||||
"title-color": "#4f4f4f"
|
"title-color": "#4f4f4f"
|
||||||
},
|
},
|
||||||
|
"amberTheme": {
|
||||||
|
"bgColor": "#e8bf5e",
|
||||||
|
"css_path": "{{ url_for('static', filename='css/epub_themes.css') }}",
|
||||||
|
"title-color": "#2f2f2f"
|
||||||
|
},
|
||||||
"blackTheme": {
|
"blackTheme": {
|
||||||
"bgColor": "black",
|
"bgColor": "black",
|
||||||
"css_path": "{{ url_for('static', filename='css/epub_themes.css') }}",
|
"css_path": "{{ url_for('static', filename='css/epub_themes.css') }}",
|
||||||
@@ -157,52 +190,112 @@
|
|||||||
|
|
||||||
function selectTheme (id) {
|
function selectTheme (id) {
|
||||||
let tickSpans = document.getElementById("themes").querySelectorAll("span");
|
let tickSpans = document.getElementById("themes").querySelectorAll("span");
|
||||||
|
|
||||||
// Remove tick mark from all theme buttons
|
|
||||||
tickSpans.forEach(function (tickSpan) {
|
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
|
// If the theme button exists, set its inner span to a tick. Otherwise set any span with the matching id.
|
||||||
document.getElementById(id).querySelector("span").textContent = "✓";
|
let el = document.getElementById(id);
|
||||||
|
if (el) {
|
||||||
|
let sp = el.querySelector("span");
|
||||||
|
if (sp) sp.textContent = "✓";
|
||||||
|
} else {
|
||||||
|
let spById = document.getElementById(id + "Selected") || document.getElementById("customSelected");
|
||||||
|
if (spById) spById.textContent = "✓";
|
||||||
|
}
|
||||||
|
|
||||||
// Saving theme to local storage
|
// Saving theme to local storage
|
||||||
localStorage.setItem("calibre.reader.theme", id);
|
localStorage.setItem("calibre.reader.theme", id);
|
||||||
|
|
||||||
// Apply theme to epubjs iframe
|
|
||||||
reader.rendition.themes.select(id);
|
|
||||||
|
|
||||||
// Apply theme to rest of the page.
|
// If selecting custom theme, ensure epubjs theme is registered with chosen bg color
|
||||||
|
if (id === "customTheme") {
|
||||||
|
let 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 = themes[id]["bgColor"];
|
document.getElementById("main").style.backgroundColor = themes[id]["bgColor"];
|
||||||
document.getElementById("titlebar").style.color = themes[id]["title-color"] || "#fff";
|
document.getElementById("titlebar").style.color = themes[id]["title-color"] || "#fff";
|
||||||
document.getElementById("progress").style.color = themes[id]["title-color"] || "#fff";
|
document.getElementById("progress").style.color = themes[id]["title-color"] || "#fff";
|
||||||
}
|
}
|
||||||
|
|
||||||
// font size settings logic
|
// font size settings logic
|
||||||
let fontSizeFader = document.getElementById('fontSizeFader');
|
let currentFontSize = 100; // default 100%
|
||||||
fontSizeFader.addEventListener("change", function () {
|
const minFontSize = 50;
|
||||||
reader.rendition.themes.fontSize(`${this.value}%`);
|
const maxFontSize = 300;
|
||||||
|
const stepSize = 5;
|
||||||
|
|
||||||
|
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;
|
let defaultFont;
|
||||||
|
|
||||||
function selectFont(id) {
|
window.selectFont = function (id) {
|
||||||
if (!defaultFont) {
|
if (!defaultFont) {
|
||||||
defaultFont = reader.rendition.getContents()[0]?.css('font-family');
|
defaultFont = reader.rendition.getContents()[0]?.css('font-family');
|
||||||
}
|
}
|
||||||
|
|
||||||
spans = document.getElementById("font").querySelectorAll("span");
|
spans = document.getElementById("font").querySelectorAll("span");
|
||||||
for(var i = 0; i < spans.length; i++) {
|
for(var i = 0; i < spans.length; i++) {
|
||||||
spans[i].textContent = "";
|
spans[i].textContent = "";
|
||||||
}
|
}
|
||||||
document.getElementById(id).querySelector("span").textContent = "✓";
|
document.getElementById(id).querySelector("span").textContent = "✓";
|
||||||
|
|
||||||
if (id == 'default') {
|
// Save font selection to localStorage
|
||||||
reader.rendition.themes.font(defaultFont);
|
localStorage.setItem("calibre.reader.font", id);
|
||||||
return;
|
|
||||||
|
if (id == "default") {
|
||||||
|
reader.rendition.themes.font(defaultFont);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
reader.rendition.themes.font(id);
|
reader.rendition.themes.font(id);
|
||||||
}
|
};
|
||||||
|
|
||||||
function spread(id) {
|
function spread(id) {
|
||||||
spans = document.getElementById("layout").querySelectorAll("span");
|
spans = document.getElementById("layout").querySelectorAll("span");
|
||||||
@@ -210,8 +303,190 @@
|
|||||||
spans[i].textContent = "";
|
spans[i].textContent = "";
|
||||||
}
|
}
|
||||||
document.getElementById(id).querySelector("span").textContent = "✓";
|
document.getElementById(id).querySelector("span").textContent = "✓";
|
||||||
reader.rendition.spread(id==='spread'?true:'none');
|
reader.rendition.spread(id==="spread" ? true : "none");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pages counter visibility setting
|
||||||
|
(function () {
|
||||||
|
var checkbox = document.getElementById("showPagesCount");
|
||||||
|
var pagesEl = document.getElementById("pages-count");
|
||||||
|
var key = "calibre.reader.showPages";
|
||||||
|
var saved = localStorage.getItem(key);
|
||||||
|
var show = saved === null ? true : saved === "true";
|
||||||
|
if (checkbox) checkbox.checked = show;
|
||||||
|
if (pagesEl) pagesEl.style.display = show ? "" : "none";
|
||||||
|
if (checkbox) {
|
||||||
|
checkbox.addEventListener("change", function () {
|
||||||
|
var val = checkbox.checked;
|
||||||
|
localStorage.setItem(key, String(val));
|
||||||
|
var target = document.getElementById("pages-count");
|
||||||
|
if (target) target.style.visibility = val ? "visible" : "hidden";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
// Initialize custom theme UI and Pickr once Pickr is available
|
||||||
|
(function () {
|
||||||
|
var swatch = document.getElementById("customThemeSwatch");
|
||||||
|
var saved =
|
||||||
|
window.themes &&
|
||||||
|
window.themes.customTheme &&
|
||||||
|
window.themes.customTheme.bgColor
|
||||||
|
? window.themes.customTheme.bgColor
|
||||||
|
: "#ffffff";
|
||||||
|
if (swatch) swatch.style.background = saved;
|
||||||
|
|
||||||
|
function _hexToRgb(hex) {
|
||||||
|
hex = hex.replace("#", "");
|
||||||
|
if (hex.length === 3) {
|
||||||
|
hex = hex
|
||||||
|
.split("")
|
||||||
|
.map(function (h) {
|
||||||
|
return h + h;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
var bigint = parseInt(hex, 16);
|
||||||
|
return {
|
||||||
|
r: (bigint >> 16) & 255,
|
||||||
|
g: (bigint >> 8) & 255,
|
||||||
|
b: bigint & 255,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Better contrast decision using WCAG relative luminance and contrast ratio
|
||||||
|
// Returns true if black text is the better choice (i.e. background is light)
|
||||||
|
function _isLight(hex) {
|
||||||
|
try {
|
||||||
|
var rgb = _hexToRgb(hex);
|
||||||
|
// convert 0-255 to 0-1
|
||||||
|
var srgb = { r: rgb.r / 255, g: rgb.g / 255, b: rgb.b / 255 };
|
||||||
|
function lin(c) {
|
||||||
|
return c <= 0.03928
|
||||||
|
? c / 12.92
|
||||||
|
: Math.pow((c + 0.055) / 1.055, 2.4);
|
||||||
|
}
|
||||||
|
var R = lin(srgb.r),
|
||||||
|
G = lin(srgb.g),
|
||||||
|
B = lin(srgb.b);
|
||||||
|
var L = 0.2126 * R + 0.7152 * G + 0.0722 * B; // relative luminance
|
||||||
|
|
||||||
|
// contrast ratios against black (L=0) and white (L=1)
|
||||||
|
var contrastWithBlack = (L + 0.05) / (0.0 + 0.05);
|
||||||
|
var contrastWithWhite = (1.0 + 0.05) / (L + 0.05);
|
||||||
|
|
||||||
|
// pick the text color that gives higher contrast. If black gives >= contrast, treat bg as light
|
||||||
|
return contrastWithBlack >= contrastWithWhite;
|
||||||
|
} catch (e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCustomColor(hex) {
|
||||||
|
if (!hex) return;
|
||||||
|
if (hex[0] !== "#") hex = "#" + hex;
|
||||||
|
window.themes.customTheme.bgColor = hex;
|
||||||
|
try {
|
||||||
|
localStorage.setItem("calibre.reader.customTheme", hex);
|
||||||
|
} catch (e) {}
|
||||||
|
if (swatch) swatch.style.background = hex;
|
||||||
|
|
||||||
|
// Mark theme as selected
|
||||||
|
try {
|
||||||
|
localStorage.setItem("calibre.reader.theme", "customTheme");
|
||||||
|
} catch (e) {}
|
||||||
|
// Clear ticks and set custom tick
|
||||||
|
var tickSpans = document
|
||||||
|
.getElementById("themes")
|
||||||
|
.querySelectorAll("span");
|
||||||
|
tickSpans.forEach(function (ts) {
|
||||||
|
ts.textContent = "";
|
||||||
|
});
|
||||||
|
var customTick = document.getElementById("customSelected");
|
||||||
|
if (customTick) customTick.textContent = "✓";
|
||||||
|
|
||||||
|
// Compute title/text color based on contrast (black or white)
|
||||||
|
var titleColor = _isLight(hex) ? "#000000" : "#ffffff";
|
||||||
|
try {
|
||||||
|
document.getElementById("main").style.backgroundColor = hex;
|
||||||
|
document.getElementById("titlebar").style.color = titleColor;
|
||||||
|
document.getElementById("progress").style.color = titleColor;
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
// Persist title-color in themes map so selectTheme reads a consistent value
|
||||||
|
try {
|
||||||
|
window.themes.customTheme["title-color"] = titleColor;
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
// Register and select theme in epub rendition (include text color)
|
||||||
|
try {
|
||||||
|
if (reader && reader.rendition && reader.rendition.themes) {
|
||||||
|
reader.rendition.themes.register("customTheme", {
|
||||||
|
body: { background: hex, color: titleColor },
|
||||||
|
});
|
||||||
|
reader.rendition.themes.select("customTheme");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to apply custom theme to reader", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delay init until Pickr is available
|
||||||
|
function ensurePickrAndInit() {
|
||||||
|
if (window.Pickr) {
|
||||||
|
try {
|
||||||
|
var pickr = Pickr.create({
|
||||||
|
el: "#customThemeSwatch",
|
||||||
|
theme: "classic",
|
||||||
|
default: saved,
|
||||||
|
components: {
|
||||||
|
preview: true,
|
||||||
|
opacity: false,
|
||||||
|
hue: true,
|
||||||
|
interaction: {
|
||||||
|
hex: true,
|
||||||
|
input: true,
|
||||||
|
save: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Immediate apply when color changes in the picker
|
||||||
|
pickr.on("change", function (color, instance) {
|
||||||
|
try {
|
||||||
|
var hex = color.toHEXA().toString();
|
||||||
|
if (swatch) swatch.style.background = hex;
|
||||||
|
applyCustomColor(hex);
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also respond to save (some pickr configs use save)
|
||||||
|
pickr.on("save", function (color, instance) {
|
||||||
|
try {
|
||||||
|
var hex = color.toHEXA().toString();
|
||||||
|
if (swatch) swatch.style.background = hex;
|
||||||
|
applyCustomColor(hex);
|
||||||
|
pickr.hide();
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clicking swatch opens pickr
|
||||||
|
if (swatch)
|
||||||
|
swatch.addEventListener("click", function () {
|
||||||
|
pickr.show();
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Pickr init failed", e);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// wait a bit
|
||||||
|
setTimeout(ensurePickrAndInit, 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
ensurePickrAndInit();
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script src="{{ url_for('static', filename='js/libs/screenfull.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/libs/screenfull.min.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/libs/reader.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/libs/reader.min.js') }}"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user