mirror of
https://github.com/janeczku/calibre-web
synced 2026-01-28 13:51:23 +00:00
496 lines
21 KiB
HTML
496 lines
21 KiB
HTML
<!DOCTYPE html>
|
||
<html class="no-js">
|
||
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||
<title>{{_('epub Reader')}} | {{title}}</title>
|
||
<meta name="description" content="">
|
||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||
<meta name="mobile-web-app-capable" content="yes">
|
||
{% if g.google_site_verification|length > 0 %}
|
||
<meta name="google-site-verification" content="{{g.google_site_verification}}">
|
||
{% endif %}
|
||
<link rel="apple-touch-icon" sizes="140x140" href="{{ url_for('static', filename='favicon.ico') }}">
|
||
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/libs/normalize.css') }}">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/popup.css') }}">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/reader.css') }}">
|
||
</head>
|
||
|
||
<body>
|
||
<div id="sidebar">
|
||
<div id="panels">
|
||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||
<!--input id="searchBox" placeholder="search" type="search"-->
|
||
|
||
<!--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-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-->
|
||
|
||
</div>
|
||
<div id="tocView" class="view">
|
||
</div>
|
||
<!--div id="searchView" class="view">
|
||
<ul id="searchResults"></ul>
|
||
</div-->
|
||
<div id="bookmarksView" class="view">
|
||
<ul id="bookmarks"></ul>
|
||
</div>
|
||
<!--div id="notesView" class="view">
|
||
<div id="new-note">
|
||
<textarea id="note-text"></textarea>
|
||
<button id="note-anchor">Anchor</button>
|
||
</div>
|
||
<ol id="notes"></ol>
|
||
</div-->
|
||
</div>
|
||
<div id="main">
|
||
<div id="titlebar">
|
||
<div id="opener">
|
||
<a id="slider" class="icon-menu">Menu</a>
|
||
</div>
|
||
<div id="metainfo">
|
||
<span id="book-title"></span>
|
||
<span id="title-seperator"> – </span>
|
||
<span id="chapter-title"></span>
|
||
</div>
|
||
<div id="title-controls">
|
||
<a id="bookmark" class="icon-bookmark-empty">Bookmark</a>
|
||
<a id="setting" class="icon-cog">Settings</a>
|
||
<a id="fullscreen" class="icon-resize-full">Fullscreen</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="divider"></div>
|
||
<div id="prev" class="arrow">‹</div>
|
||
<div id="viewer"></div>
|
||
<div id="next" class="arrow">›</div>
|
||
<div class="read-footer">
|
||
<div id="pages-count"></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="md-content">
|
||
<h3>{{_('Settings')}}</h3>
|
||
<div class="form-group themes" id="themes">
|
||
{{_('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-->
|
||
<button type="button" id="lightTheme" class="lightTheme" onclick="selectTheme(this.id)"><span
|
||
id="lightSelected">✓</span>{{_('Light')}}</button>
|
||
<button type="button" id="darkTheme" class="darkTheme" onclick="selectTheme(this.id)"><span
|
||
id="darkSelected"> </span>{{_('Dark')}}</button>
|
||
<button type="button" id="sepiaTheme" class="sepiaTheme" onclick="selectTheme(this.id)"><span
|
||
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
|
||
id="blackSelected"> </span>{{_('Black')}}</button>
|
||
|
||
<!-- 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">
|
||
<p>
|
||
<input type="checkbox" id="sidebarReflow"
|
||
name="sidebarReflow">{{_('Reflow text when sidebars are open.')}}
|
||
</p>
|
||
</div>
|
||
<div class="form-group">
|
||
<p>
|
||
<input type="checkbox" id="showPagesCount" name="showPagesCount"/>{{_('Show pages count')}}
|
||
</p>
|
||
</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">
|
||
<label class="item">{{_('Font')}}:</label>
|
||
<button type="button" id="default" onclick="selectFont(this.id)"><span>✓</span>{{_('Default')}}</button>
|
||
<button type="button" id="Yahei" onclick="selectFont(this.id)"><span></span>{{_('Yahei')}}</button>
|
||
<button type="button" id="SimSun" onclick="selectFont(this.id)"><span></span>{{_('SimSun')}}</button>
|
||
<button type="button" id="KaiTi" onclick="selectFont(this.id)"><span></span>{{_('KaiTi')}}</button>
|
||
<button type="button" id="Arial" onclick="selectFont(this.id)"><span></span>{{_('Arial')}}</button>
|
||
</div>
|
||
<div class="layou" id="layout">
|
||
<label class="item">{{ _('Spread') }}:</label>
|
||
<button type="button" id="spread" onclick="spread(this.id)"><span>✓</span>{{_('Two columns')}}</button>
|
||
<button type="button" id="nonespread" onclick="spread(this.id)"><span></span>{{_('One column')}}</button>
|
||
</div>
|
||
<div class="closer icon-cancel-circled"></div>
|
||
</div>
|
||
</div>
|
||
<div class="overlay"></div>
|
||
<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>
|
||
<script src="{{ url_for('static', filename='js/libs/epub.min.js') }}"></script>
|
||
<script type="text/javascript">
|
||
window.calibre = {
|
||
filePath: "{{ url_for('static', filename='js/libs/') }}",
|
||
cssPath: "{{ url_for('static', filename='css/') }}",
|
||
bookmarkUrl: "{{ url_for('web.set_bookmark', book_id=bookid, book_format=book_format) }}",
|
||
bookUrl: "{{ url_for('web.serve_book', book_id=bookid, book_format=book_format, anyname='file.epub') }}",
|
||
bookmark: "{{ bookmark.bookmark_key if bookmark != None }}",
|
||
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",
|
||
"css_path": "{{ url_for('static', filename='css/epub_themes.css') }}",
|
||
"title-color": "#fff"
|
||
},
|
||
"lightTheme": {
|
||
"bgColor": "white",
|
||
"css_path": "{{ url_for('static', filename='css/epub_themes.css') }}",
|
||
"title-color": "#4f4f4f"
|
||
},
|
||
"sepiaTheme": {
|
||
"bgColor": "#ece1ca",
|
||
"css_path": "{{ url_for('static', filename='css/epub_themes.css') }}",
|
||
"title-color": "#4f4f4f"
|
||
},
|
||
"amberTheme": {
|
||
"bgColor": "#e8bf5e",
|
||
"css_path": "{{ url_for('static', filename='css/epub_themes.css') }}",
|
||
"title-color": "#2f2f2f"
|
||
},
|
||
"blackTheme": {
|
||
"bgColor": "black",
|
||
"css_path": "{{ url_for('static', filename='css/epub_themes.css') }}",
|
||
"title-color": "#fff"
|
||
},
|
||
};
|
||
|
||
function selectTheme (id) {
|
||
let tickSpans = document.getElementById("themes").querySelectorAll("span");
|
||
tickSpans.forEach(function (tickSpan) {
|
||
try {
|
||
tickSpan.textContent = "";
|
||
} catch (e) {}
|
||
});
|
||
|
||
// If the theme button exists, set its inner span to a tick. Otherwise set any span with the matching id.
|
||
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
|
||
localStorage.setItem("calibre.reader.theme", id);
|
||
|
||
// 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("titlebar").style.color = themes[id]["title-color"] || "#fff";
|
||
document.getElementById("progress").style.color = themes[id]["title-color"] || "#fff";
|
||
}
|
||
|
||
// font size settings logic
|
||
let currentFontSize = 100; // default 100%
|
||
const minFontSize = 50;
|
||
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;
|
||
|
||
window.selectFont = function (id) {
|
||
if (!defaultFont) {
|
||
defaultFont = reader.rendition.getContents()[0]?.css('font-family');
|
||
}
|
||
|
||
spans = document.getElementById("font").querySelectorAll("span");
|
||
for(var i = 0; i < spans.length; i++) {
|
||
spans[i].textContent = "";
|
||
}
|
||
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");
|
||
for(var i = 0; i < spans.length; i++) {
|
||
spans[i].textContent = "";
|
||
}
|
||
document.getElementById(id).querySelector("span").textContent = "✓";
|
||
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 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/reading/epub.js') }}"></script>
|
||
</body>
|
||
</html>
|