Merge Reader improvements
This commit is contained in:
@@ -29,6 +29,7 @@
|
||||
<!--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>
|
||||
@@ -49,7 +50,6 @@
|
||||
</div-->
|
||||
</div>
|
||||
<div id="main">
|
||||
|
||||
<div id="titlebar">
|
||||
<div id="opener">
|
||||
<a id="slider" class="icon-menu">Menu</a>
|
||||
@@ -70,37 +70,58 @@
|
||||
<div id="prev" class="arrow">‹</div>
|
||||
<div id="viewer"></div>
|
||||
<div id="next" class="arrow">›</div>
|
||||
<div id="progress">0%</div>
|
||||
<div id="loader"><img src="{{ url_for('static', filename='img/loader.gif') }}"></div>
|
||||
</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-->
|
||||
<!-- 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>
|
||||
</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">
|
||||
<p>
|
||||
<input type="checkbox" id="sidebarReflow"
|
||||
name="sidebarReflow">{{_('Reflow text when sidebars are open.')}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group fontSizeWrapper">
|
||||
<div class="slider">
|
||||
<label for="fader">{{ _('Font Sizes') }}</label>
|
||||
<input type="range" min="75" max="200" value="100" id="fontSizeFader" step="25">
|
||||
</div>
|
||||
</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>
|
||||
@@ -119,8 +140,7 @@
|
||||
</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/compress/jszip_epub.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/libs/epub.min.js') }}"></script>
|
||||
<script type="text/javascript">
|
||||
window.calibre = {
|
||||
@@ -132,6 +152,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",
|
||||
@@ -148,6 +176,11 @@
|
||||
"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') }}",
|
||||
@@ -157,52 +190,112 @@
|
||||
|
||||
function selectTheme (id) {
|
||||
let 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.
|
||||
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);
|
||||
|
||||
// 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("titlebar").style.color = themes[id]["title-color"] || "#fff";
|
||||
document.getElementById("progress").style.color = themes[id]["title-color"] || "#fff";
|
||||
}
|
||||
|
||||
// font size settings logic
|
||||
let fontSizeFader = document.getElementById('fontSizeFader');
|
||||
fontSizeFader.addEventListener("change", function () {
|
||||
reader.rendition.themes.fontSize(`${this.value}%`);
|
||||
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;
|
||||
|
||||
function selectFont(id) {
|
||||
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 = "";
|
||||
spans[i].textContent = "";
|
||||
}
|
||||
document.getElementById(id).querySelector("span").textContent = "✓";
|
||||
|
||||
if (id == 'default') {
|
||||
reader.rendition.themes.font(defaultFont);
|
||||
return;
|
||||
|
||||
// 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");
|
||||
@@ -210,8 +303,190 @@
|
||||
spans[i].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 src="{{ url_for('static', filename='js/libs/screenfull.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/libs/reader.min.js') }}"></script>
|
||||
|
||||
Reference in New Issue
Block a user