feat(read.html, epub): add custom theme selection with color picker, used pickr.js
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user