Merge Reader improvements

This commit is contained in:
Ozzie Isaacs
2025-12-07 13:38:54 +01:00
9 changed files with 689 additions and 175 deletions

View File

@@ -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>