feat(reader): add light/dark mode toggle for reading themes
Add explicit light/dark mode toggle switch to the reading theme settings. The reading mode defaults based on the chrome theme (dark chrome themes like tokyo-night default to dark reading mode). - Add readingMode property to readerShell Alpine component - Add toggleReadingMode() method that toggles dark class on body - Add detectChromeDarkMode() to infer default from chrome theme - Update applyTheme() to add/remove dark class and persist reading_mode - Add toggle switch UI in settings panel (blue pill style, next to Reading Theme heading) - Add reading_mode to default settings in settings-manager
This commit is contained in:
+15
-1
@@ -186,7 +186,21 @@ templ ReaderSettingsPanel() {
|
|||||||
</div>
|
</div>
|
||||||
<!-- Reading Theme (reflowable only) -->
|
<!-- Reading Theme (reflowable only) -->
|
||||||
<div class="mb-6" x-show="!isFixedLayout">
|
<div class="mb-6" x-show="!isFixedLayout">
|
||||||
<h3 class="font-semibold mb-2">Reading Theme</h3>
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<h3 class="font-semibold">Reading Theme</h3>
|
||||||
|
<button
|
||||||
|
@click="toggleReadingMode()"
|
||||||
|
class="relative w-12 h-6 rounded-full transition-colors duration-200"
|
||||||
|
:class="readingMode === 'dark' ? 'bg-blue-500' : 'bg-gray-400'"
|
||||||
|
type="button"
|
||||||
|
:aria-label="readingMode === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform duration-200"
|
||||||
|
:class="readingMode === 'dark' ? 'translate-x-6' : 'translate-x-0'"
|
||||||
|
></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<label class="block mb-2">
|
<label class="block mb-2">
|
||||||
<select name="reading_theme" x-model="readingTheme" @change="applyTheme()" class="w-full mt-1 px-3 py-2 rounded border">
|
<select name="reading_theme" x-model="readingTheme" @change="applyTheme()" class="w-full mt-1 px-3 py-2 rounded border">
|
||||||
<optgroup label="📖 Classic Reading">
|
<optgroup label="📖 Classic Reading">
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ func ReaderSettingsPanel() templ.Component {
|
|||||||
templ_7745c5c3_Var7 = templ.NopComponent
|
templ_7745c5c3_Var7 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"dockable-panel\" data-panel=\"settings\" data-side=\"left\" x-ref=\"settingsPanel\"><div class=\"panel-header flex items-center justify-between p-3 cursor-pointer\" @click=\"toggleWindowShade($refs.settingsPanel)\"><h3 class=\"panel-title font-semibold\">⚙️ Settings</h3><div class=\"panel-controls flex items-center gap-2\"><button class=\"window-shade-toggle\">─</button></div></div><div class=\"panel-content p-4 overflow-y-auto\"><!-- Display settings --><div class=\"mb-6\"><h3 class=\"font-semibold mb-2\">Display</h3><label class=\"block mb-2\">Chrome Behavior <select name=\"chrome_behavior\" class=\"w-full mt-1 px-3 py-2 rounded border\"><option value=\"auto-hide\">Auto Hide</option> <option value=\"always-visible\">Always Visible</option> <option value=\"hide-on-scroll\">Hide on Scroll</option></select></label> <label class=\"block mb-2\">Progress Mode <select name=\"progress_mode\" class=\"w-full mt-1 px-3 py-2 rounded border\"><option value=\"pages\">Pages</option> <option value=\"chapter\">Chapter</option> <option value=\"percentage\">Percentage</option> <option value=\"time-left\">Time Left</option></select></label></div><!-- Reading Theme (reflowable only) --><div class=\"mb-6\" x-show=\"!isFixedLayout\"><h3 class=\"font-semibold mb-2\">Reading Theme</h3><label class=\"block mb-2\"><select name=\"reading_theme\" x-model=\"readingTheme\" @change=\"applyTheme()\" class=\"w-full mt-1 px-3 py-2 rounded border\"><optgroup label=\"📖 Classic Reading\"><option value=\"light\">Light</option> <option value=\"paper\">Paper</option> <option value=\"sepia\">Sepia</option> <option value=\"parchment\">Parchment</option> <option value=\"warm\">Warm</option> <option value=\"candlelight\">Candlelight</option></optgroup> <optgroup label=\"🌤️ Sky & Atmosphere\"><option value=\"azure\">Azure</option> <option value=\"sky\">Sky</option> <option value=\"arctic\">Arctic</option> <option value=\"frost\">Frost</option></optgroup> <optgroup label=\"🌅 Sunset & Warmth\"><option value=\"dusk\">Dusk</option> <option value=\"sunset\">Sunset</option> <option value=\"twilight\">Twilight</option></optgroup> <optgroup label=\"🌲 Nature & Earth\"><option value=\"forest\">Forest</option> <option value=\"moss\">Moss</option> <option value=\"slate\">Slate</option></optgroup> <optgroup label=\"⚡ High Performance\"><option value=\"oled\">OLED</option> <option value=\"solarized\">Solarized</option></optgroup></select></label></div><!-- Typography (reflowable only) --><div class=\"mb-6\" x-show=\"!isFixedLayout\"><h3 class=\"font-semibold mb-2\">Typography</h3><label class=\"block mb-2\">Reading Font <select name=\"reading_font\" x-model=\"readingFont\" @change=\"applyFont()\" class=\"w-full mt-1 px-3 py-2 rounded border\"><option value=\"literata\">Literata (Default)</option> <option value=\"crimson\">Crimson Text</option> <option value=\"source-serif\">Source Serif 4</option> <option value=\"eb-garamond\">EB Garamond</option> <option value=\"libertinus\">Libertinus Serif</option> <option value=\"noto-serif\">Noto Serif</option> <option value=\"charis-sil\">Charis SIL</option> <option value=\"ibm-plex\">IBM Plex Serif</option></select></label> <label class=\"block mb-2\">Font Size <input type=\"range\" name=\"font_size\" x-model.number=\"fontSize\" @change=\"applyFont()\" min=\"12\" max=\"24\" class=\"w-full\"> <span class=\"text-xs\" x-text=\"fontSize + 'px'\" style=\"color: var(--text-secondary)\">16px</span></label> <label class=\"block mb-2\">Line Height <input type=\"range\" name=\"line_height\" x-model.number=\"lineHeight\" @change=\"applyFont()\" min=\"1.0\" max=\"2.5\" step=\"0.1\" class=\"w-full\"> <span class=\"text-xs\" x-text=\"lineHeight\" style=\"color: var(--text-secondary)\">1.6</span></label></div><!-- Navigation --><div class=\"mb-6\" x-show=\"isFixedLayout\"><h3 class=\"font-semibold mb-2\">Navigation</h3><label class=\"flex items-center mb-2\"><input type=\"checkbox\" name=\"double_page_spread\" class=\"mr-2\"> Double Page Spread</label></div><button @click=\"toggleSettings()\" class=\"w-full py-2 bg-blue-600 text-white rounded hover:bg-blue-700\">Done</button></div></div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"dockable-panel\" data-panel=\"settings\" data-side=\"left\" x-ref=\"settingsPanel\"><div class=\"panel-header flex items-center justify-between p-3 cursor-pointer\" @click=\"toggleWindowShade($refs.settingsPanel)\"><h3 class=\"panel-title font-semibold\">⚙️ Settings</h3><div class=\"panel-controls flex items-center gap-2\"><button class=\"window-shade-toggle\">─</button></div></div><div class=\"panel-content p-4 overflow-y-auto\"><!-- Display settings --><div class=\"mb-6\"><h3 class=\"font-semibold mb-2\">Display</h3><label class=\"block mb-2\">Chrome Behavior <select name=\"chrome_behavior\" class=\"w-full mt-1 px-3 py-2 rounded border\"><option value=\"auto-hide\">Auto Hide</option> <option value=\"always-visible\">Always Visible</option> <option value=\"hide-on-scroll\">Hide on Scroll</option></select></label> <label class=\"block mb-2\">Progress Mode <select name=\"progress_mode\" class=\"w-full mt-1 px-3 py-2 rounded border\"><option value=\"pages\">Pages</option> <option value=\"chapter\">Chapter</option> <option value=\"percentage\">Percentage</option> <option value=\"time-left\">Time Left</option></select></label></div><!-- Reading Theme (reflowable only) --><div class=\"mb-6\" x-show=\"!isFixedLayout\"><div class=\"flex items-center justify-between mb-2\"><h3 class=\"font-semibold\">Reading Theme</h3><button @click=\"toggleReadingMode()\" class=\"relative w-12 h-6 rounded-full transition-colors duration-200\" :class=\"readingMode === 'dark' ? 'bg-blue-500' : 'bg-gray-400'\" type=\"button\" :aria-label=\"readingMode === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'\"><span class=\"absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform duration-200\" :class=\"readingMode === 'dark' ? 'translate-x-6' : 'translate-x-0'\"></span></button></div><label class=\"block mb-2\"><select name=\"reading_theme\" x-model=\"readingTheme\" @change=\"applyTheme()\" class=\"w-full mt-1 px-3 py-2 rounded border\"><optgroup label=\"📖 Classic Reading\"><option value=\"light\">Light</option> <option value=\"paper\">Paper</option> <option value=\"sepia\">Sepia</option> <option value=\"parchment\">Parchment</option> <option value=\"warm\">Warm</option> <option value=\"candlelight\">Candlelight</option></optgroup> <optgroup label=\"🌤️ Sky & Atmosphere\"><option value=\"azure\">Azure</option> <option value=\"sky\">Sky</option> <option value=\"arctic\">Arctic</option> <option value=\"frost\">Frost</option></optgroup> <optgroup label=\"🌅 Sunset & Warmth\"><option value=\"dusk\">Dusk</option> <option value=\"sunset\">Sunset</option> <option value=\"twilight\">Twilight</option></optgroup> <optgroup label=\"🌲 Nature & Earth\"><option value=\"forest\">Forest</option> <option value=\"moss\">Moss</option> <option value=\"slate\">Slate</option></optgroup> <optgroup label=\"⚡ High Performance\"><option value=\"oled\">OLED</option> <option value=\"solarized\">Solarized</option></optgroup></select></label></div><!-- Typography (reflowable only) --><div class=\"mb-6\" x-show=\"!isFixedLayout\"><h3 class=\"font-semibold mb-2\">Typography</h3><label class=\"block mb-2\">Reading Font <select name=\"reading_font\" x-model=\"readingFont\" @change=\"applyFont()\" class=\"w-full mt-1 px-3 py-2 rounded border\"><option value=\"literata\">Literata (Default)</option> <option value=\"crimson\">Crimson Text</option> <option value=\"source-serif\">Source Serif 4</option> <option value=\"eb-garamond\">EB Garamond</option> <option value=\"libertinus\">Libertinus Serif</option> <option value=\"noto-serif\">Noto Serif</option> <option value=\"charis-sil\">Charis SIL</option> <option value=\"ibm-plex\">IBM Plex Serif</option></select></label> <label class=\"block mb-2\">Font Size <input type=\"range\" name=\"font_size\" x-model.number=\"fontSize\" @change=\"applyFont()\" min=\"12\" max=\"24\" class=\"w-full\"> <span class=\"text-xs\" x-text=\"fontSize + 'px'\" style=\"color: var(--text-secondary)\">16px</span></label> <label class=\"block mb-2\">Line Height <input type=\"range\" name=\"line_height\" x-model.number=\"lineHeight\" @change=\"applyFont()\" min=\"1.0\" max=\"2.5\" step=\"0.1\" class=\"w-full\"> <span class=\"text-xs\" x-text=\"lineHeight\" style=\"color: var(--text-secondary)\">1.6</span></label></div><!-- Navigation --><div class=\"mb-6\" x-show=\"isFixedLayout\"><h3 class=\"font-semibold mb-2\">Navigation</h3><label class=\"flex items-center mb-2\"><input type=\"checkbox\" name=\"double_page_spread\" class=\"mr-2\"> Double Page Spread</label></div><button @click=\"toggleSettings()\" class=\"w-full py-2 bg-blue-600 text-white rounded hover:bg-blue-700\">Done</button></div></div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -293,7 +293,7 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component {
|
|||||||
var templ_7745c5c3_Var11 string
|
var templ_7745c5c3_Var11 string
|
||||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.CfiPosition)
|
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.CfiPosition)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 334, Col: 38}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 348, Col: 38}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -306,7 +306,7 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component {
|
|||||||
var templ_7745c5c3_Var12 string
|
var templ_7745c5c3_Var12 string
|
||||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.Title)
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.Title)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 337, Col: 49}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 351, Col: 49}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -319,7 +319,7 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component {
|
|||||||
var templ_7745c5c3_Var13 string
|
var templ_7745c5c3_Var13 string
|
||||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.Position)
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.Position)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 339, Col: 27}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 353, Col: 27}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
|
|||||||
@@ -84,7 +84,8 @@ document.addEventListener("alpine:init", () => {
|
|||||||
bookmarksOpen: false,
|
bookmarksOpen: false,
|
||||||
navigatorOpen: false,
|
navigatorOpen: false,
|
||||||
tocItems: [] as any[],
|
tocItems: [] as any[],
|
||||||
readingTheme: "dark" as string,
|
readingTheme: "light" as string,
|
||||||
|
readingMode: "light" as string,
|
||||||
readingFont: "literata" as string,
|
readingFont: "literata" as string,
|
||||||
fontSize: 16 as number,
|
fontSize: 16 as number,
|
||||||
lineHeight: 1.6 as number,
|
lineHeight: 1.6 as number,
|
||||||
@@ -97,13 +98,21 @@ document.addEventListener("alpine:init", () => {
|
|||||||
}) {
|
}) {
|
||||||
this.settings = await loadSettings();
|
this.settings = await loadSettings();
|
||||||
if (this.settings) {
|
if (this.settings) {
|
||||||
this.readingTheme = this.settings.reading_theme || "dark";
|
this.readingTheme = this.settings.reading_theme || "light";
|
||||||
this.readingFont = this.settings.reading_font || "literata";
|
this.readingFont = this.settings.reading_font || "literata";
|
||||||
this.fontSize = this.settings.font_size || 16;
|
this.fontSize = this.settings.font_size || 16;
|
||||||
this.lineHeight = this.settings.line_height || 1.6;
|
this.lineHeight = this.settings.line_height || 1.6;
|
||||||
if (this.settings.reading_theme) {
|
if (this.settings.reading_theme) {
|
||||||
document.body.classList.add(`theme-${this.settings.reading_theme}`);
|
document.body.classList.add(`theme-${this.settings.reading_theme}`);
|
||||||
}
|
}
|
||||||
|
if (this.settings.reading_mode) {
|
||||||
|
this.readingMode = this.settings.reading_mode;
|
||||||
|
} else {
|
||||||
|
this.readingMode = this.detectChromeDarkMode() ? "dark" : "light";
|
||||||
|
}
|
||||||
|
if (this.readingMode === "dark") {
|
||||||
|
document.body.classList.add("dark");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.view = document.getElementById("reader-view") as any;
|
this.view = document.getElementById("reader-view") as any;
|
||||||
const resp = await fetch(config.fileUrl, {
|
const resp = await fetch(config.fileUrl, {
|
||||||
@@ -258,10 +267,29 @@ document.addEventListener("alpine:init", () => {
|
|||||||
applyTheme() {
|
applyTheme() {
|
||||||
document.body.className = document.body.className
|
document.body.className = document.body.className
|
||||||
.replace(/theme-(?!tokyo|dracula|nord|solarized|monokai|one-dark|material|catppuccin)[\w-]+/g, "")
|
.replace(/theme-(?!tokyo|dracula|nord|solarized|monokai|one-dark|material|catppuccin)[\w-]+/g, "")
|
||||||
|
.replace(/\bdark\b/g, "")
|
||||||
.trim();
|
.trim();
|
||||||
document.body.classList.add(`theme-${this.readingTheme}`);
|
document.body.classList.add(`theme-${this.readingTheme}`);
|
||||||
|
if (this.readingMode === "dark") {
|
||||||
|
document.body.classList.add("dark");
|
||||||
|
}
|
||||||
this.applyStyles();
|
this.applyStyles();
|
||||||
saveSettings({ reading_theme: this.readingTheme });
|
saveSettings({
|
||||||
|
reading_theme: this.readingTheme,
|
||||||
|
reading_mode: this.readingMode,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
toggleReadingMode() {
|
||||||
|
this.readingMode = this.readingMode === "dark" ? "light" : "dark";
|
||||||
|
this.applyTheme();
|
||||||
|
},
|
||||||
|
detectChromeDarkMode(): boolean {
|
||||||
|
const darkChromeThemes = [
|
||||||
|
"theme-tokyo-night", "theme-dracula", "theme-nord",
|
||||||
|
"theme-monokai", "theme-one-dark", "theme-material",
|
||||||
|
"theme-catppuccin",
|
||||||
|
];
|
||||||
|
return darkChromeThemes.some((t) => document.body.classList.contains(t));
|
||||||
},
|
},
|
||||||
applyFont() {
|
applyFont() {
|
||||||
this.applyStyles();
|
this.applyStyles();
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ export function getDefaultSettings(): ReaderSettings {
|
|||||||
chrome_behavior: "auto-hide",
|
chrome_behavior: "auto-hide",
|
||||||
progress_mode: "pages",
|
progress_mode: "pages",
|
||||||
chrome_theme: "tokyo-night",
|
chrome_theme: "tokyo-night",
|
||||||
reading_theme: "dark",
|
reading_theme: "light",
|
||||||
|
reading_mode: "dark",
|
||||||
reading_font: "literata",
|
reading_font: "literata",
|
||||||
tap_zone_size: 30,
|
tap_zone_size: 30,
|
||||||
auto_scroll: false,
|
auto_scroll: false,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user