feat(read.html, epub): add custom theme selection with color picker, used pickr.js

This commit is contained in:
IgorKurkov
2025-10-29 22:10:08 +01:00
parent 6d6d5ac097
commit 458d0d1e3d
5 changed files with 257 additions and 14 deletions

View File

@@ -538,7 +538,7 @@ input:-moz-placeholder {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100vw;
width: 97vw;
height: 90dvh;
max-width: 630px;
z-index: 2000;
@@ -565,7 +565,7 @@ input:-moz-placeholder {
}
.md-show ~ .overlay {
opacity: 1;
/* opacity: 1; */
visibility: visible;
}

View File

@@ -29,8 +29,8 @@
}
.item {
font-size: 20px;
font-weight: 400;
font-size: 14px;
font-weight: 600;
font-family: Open Sans;
padding-right: 10px;
color: white;

File diff suppressed because one or more lines are too long

3
cps/static/js/libs/pickr/pickr.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -157,6 +157,31 @@
>
<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>
@@ -177,7 +202,7 @@
id="navGestures"
onclick="selectNavMode('gestures')"
>
<span id="navGesturesTick"> </span>{{_('Gestures (for mobile)')}}
<span id="navGesturesTick"> </span>{{_('Gestures (mobile)')}}
</button>
</div>
<div class="form-group">
@@ -272,6 +297,14 @@
useBookmarks: "{{ current_user.is_authenticated | tojson }}",
};
// load custom theme color from localStorage (if any)
var _savedCustomThemeColor = null;
try {
_savedCustomThemeColor = localStorage.getItem(
"calibre.reader.customTheme"
);
} catch (e) {}
window.themes = {
darkTheme: {
bgColor: "#202124",
@@ -291,33 +324,67 @@
amberTheme: {
bgColor: "#e8bf5e",
css_path: "{{ url_for('static', filename='css/epub_themes.css') }}",
"title-color": "#4f4f4f",
"title-color": "#2f2f2f",
},
blackTheme: {
bgColor: "black",
css_path: "{{ url_for('static', filename='css/epub_themes.css') }}",
"title-color": "#fff",
},
customTheme: {
bgColor: _savedCustomThemeColor || "#ffffff",
css_path: "{{ url_for('static', filename='css/epub_themes.css') }}",
"title-color": "#4f4f4f",
},
};
function selectTheme(id) {
let tickSpans = document
// Collect all span tick markers inside themes and clear them safely
var tickSpans = document
.getElementById("themes")
.querySelectorAll("span");
// Remove tick mark from all theme buttons
tickSpans.forEach(function (tickSpan) {
document.getElementById(tickSpan.id).textContent = "";
try {
tickSpan.textContent = "";
} catch (e) {}
});
// Add tick mark to the button corresponding to the currently selected theme
document.getElementById(id).querySelector("span").textContent = "✓";
// If the theme button exists, set its inner span to a tick. Otherwise set any span with the matching id.
var el = document.getElementById(id);
if (el) {
var sp = el.querySelector("span");
if (sp) sp.textContent = "✓";
} else {
var spById =
document.getElementById(id + "Selected") ||
document.getElementById("customSelected");
if (spById) spById.textContent = "✓";
}
// Saving theme to local storage
localStorage.setItem("calibre.reader.theme", id);
// Apply theme to epubjs iframe
reader.rendition.themes.select(id);
// If selecting custom theme, ensure epubjs theme is registered with chosen bg color
if (id === "customTheme") {
var customColor = window.themes.customTheme.bgColor || "#ffffff";
try {
if (reader && reader.rendition && reader.rendition.themes) {
reader.rendition.themes.register("customTheme", {
body: {
background: customColor,
},
});
reader.rendition.themes.select("customTheme");
}
} catch (e) {
console.error("Failed to register/select customTheme", e);
}
} else {
// Apply theme to epubjs iframe
try {
reader.rendition.themes.select(id);
} catch (e) {}
}
// Apply theme to rest of the page.
document.getElementById("main").style.backgroundColor =
@@ -432,6 +499,177 @@
}
})();
</script>
<!-- Pickr color picker (local loader / fallback) -->
<link
rel="stylesheet"
href="{{ url_for('static', filename='js/libs/pickr/pickr.min.css') }}"
/>
<script src="{{ url_for('static', filename='js/libs/pickr/pickr.min.js') }}"></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/hammer.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/reader.min.js') }}"></script>