feat(read.html, epub): add custom theme selection with color picker, used pickr.js
This commit is contained in:
@@ -538,7 +538,7 @@ input:-moz-placeholder {
|
|||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
width: 100vw;
|
width: 97vw;
|
||||||
height: 90dvh;
|
height: 90dvh;
|
||||||
max-width: 630px;
|
max-width: 630px;
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
@@ -565,7 +565,7 @@ input:-moz-placeholder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.md-show ~ .overlay {
|
.md-show ~ .overlay {
|
||||||
opacity: 1;
|
/* opacity: 1; */
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,8 +29,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
font-size: 20px;
|
font-size: 14px;
|
||||||
font-weight: 400;
|
font-weight: 600;
|
||||||
font-family: Open Sans;
|
font-family: Open Sans;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
color: white;
|
color: white;
|
||||||
|
|||||||
2
cps/static/js/libs/pickr/pickr.min.css
vendored
Normal file
2
cps/static/js/libs/pickr/pickr.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
3
cps/static/js/libs/pickr/pickr.min.js
vendored
Normal file
3
cps/static/js/libs/pickr/pickr.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -157,6 +157,31 @@
|
|||||||
>
|
>
|
||||||
<span id="blackSelected"> </span>{{_('Black')}}
|
<span id="blackSelected"> </span>{{_('Black')}}
|
||||||
</button>
|
</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>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<p>
|
<p>
|
||||||
@@ -177,7 +202,7 @@
|
|||||||
id="navGestures"
|
id="navGestures"
|
||||||
onclick="selectNavMode('gestures')"
|
onclick="selectNavMode('gestures')"
|
||||||
>
|
>
|
||||||
<span id="navGesturesTick"> </span>{{_('Gestures (for mobile)')}}
|
<span id="navGesturesTick"> </span>{{_('Gestures (mobile)')}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -272,6 +297,14 @@
|
|||||||
useBookmarks: "{{ current_user.is_authenticated | tojson }}",
|
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 = {
|
window.themes = {
|
||||||
darkTheme: {
|
darkTheme: {
|
||||||
bgColor: "#202124",
|
bgColor: "#202124",
|
||||||
@@ -291,33 +324,67 @@
|
|||||||
amberTheme: {
|
amberTheme: {
|
||||||
bgColor: "#e8bf5e",
|
bgColor: "#e8bf5e",
|
||||||
css_path: "{{ url_for('static', filename='css/epub_themes.css') }}",
|
css_path: "{{ url_for('static', filename='css/epub_themes.css') }}",
|
||||||
"title-color": "#4f4f4f",
|
"title-color": "#2f2f2f",
|
||||||
},
|
},
|
||||||
blackTheme: {
|
blackTheme: {
|
||||||
bgColor: "black",
|
bgColor: "black",
|
||||||
css_path: "{{ url_for('static', filename='css/epub_themes.css') }}",
|
css_path: "{{ url_for('static', filename='css/epub_themes.css') }}",
|
||||||
"title-color": "#fff",
|
"title-color": "#fff",
|
||||||
},
|
},
|
||||||
|
customTheme: {
|
||||||
|
bgColor: _savedCustomThemeColor || "#ffffff",
|
||||||
|
css_path: "{{ url_for('static', filename='css/epub_themes.css') }}",
|
||||||
|
"title-color": "#4f4f4f",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function selectTheme(id) {
|
function selectTheme(id) {
|
||||||
let tickSpans = document
|
// Collect all span tick markers inside themes and clear them safely
|
||||||
|
var tickSpans = document
|
||||||
.getElementById("themes")
|
.getElementById("themes")
|
||||||
.querySelectorAll("span");
|
.querySelectorAll("span");
|
||||||
|
|
||||||
// Remove tick mark from all theme buttons
|
|
||||||
tickSpans.forEach(function (tickSpan) {
|
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
|
// If the theme button exists, set its inner span to a tick. Otherwise set any span with the matching id.
|
||||||
document.getElementById(id).querySelector("span").textContent = "✓";
|
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
|
// Saving theme to local storage
|
||||||
localStorage.setItem("calibre.reader.theme", id);
|
localStorage.setItem("calibre.reader.theme", id);
|
||||||
|
|
||||||
// Apply theme to epubjs iframe
|
// If selecting custom theme, ensure epubjs theme is registered with chosen bg color
|
||||||
reader.rendition.themes.select(id);
|
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.
|
// Apply theme to rest of the page.
|
||||||
document.getElementById("main").style.backgroundColor =
|
document.getElementById("main").style.backgroundColor =
|
||||||
@@ -432,6 +499,177 @@
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</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/screenfull.min.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/libs/hammer.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>
|
<script src="{{ url_for('static', filename='js/libs/reader.min.js') }}"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user