Hell und Dunkel (docs/theme.md), Vorlese-Schnittstelle für die Playerleiste; sw v102
- Farbvariablen für beide Schemata in app.css (--bg --fg --muted --card --panel --hover --raised --border --border-strong --accent --on-accent --scrim --shadow), Hell als html[data-theme=light]. Umschalter im Kopf jeder Seite (base.html, vor dem ersten Zeichnen): Dunkel / Hell / Automatisch, ☾☀◐ in der Navigation; __dioraTheme / __dioraSetTheme / Ereignis diora-theme. - Reader: neues Standard-Theme "Wie die App" (Dunkel/Hell folgt der App). - ◑ (Kontrast zum Hintergrundbild) nur noch mit Hintergrundbild. - home.js auf die Variablen. - window.__dioraTts (state/toggle/stop/next/prev) und Ereignis diora-tts für die Playerleiste; mit __dioraTtsDock keine eigene Vorlese-Leiste mehr. - Reader-Hinweise über der Playerleiste, wenn sie im Reader steht. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
080cdbde5a
commit
884a18e8ef
6 changed files with 298 additions and 52 deletions
44
docs/theme.md
Normal file
44
docs/theme.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# Light and dark
|
||||
|
||||
diora has two colour schemes, **dark** (the old look) and **light**, chosen by the reader:
|
||||
"Dunkel", "Hell" or "Automatisch" (as the operating system says). This file is the contract
|
||||
between diora-web (app.css, home.js, templates), diora-android (native.css, native-*.js,
|
||||
podcast-ui.css, feeds.js) and diora-desktop (desktop.css).
|
||||
|
||||
## Switching
|
||||
|
||||
* `<html data-theme="dark|light">` is the scheme in effect. Set as early as possible
|
||||
(an inline script in base.html's `<head>`, before any CSS paints) so nothing flashes.
|
||||
* The choice is `localStorage.diora_theme` = `"dark"` (default) | `"light"` | `"auto"`.
|
||||
* `window.__dioraTheme()` → the choice; `window.__dioraSetTheme(choice)` sets and applies it;
|
||||
`window.__dioraThemeChanged` (event `diora-theme` on `window`, `detail` = the scheme in
|
||||
effect) lets native code follow (Android status/navigation bars, Electron's window colour).
|
||||
* "auto" follows `prefers-color-scheme` live.
|
||||
* E-ink mode (`html.nui-eink`) keeps its own black-on-white rules and ignores this.
|
||||
* The reader has its own themes; its default is now "Wie die App" (`readerSettings.theme = 'app'`):
|
||||
dark → reader dark, light → reader bright. Sepia, Dunkel and Hell stay choosable.
|
||||
* A wallpaper (the radio page's background image) keeps its own contrast handling
|
||||
(`body.bright-bg`, the ◑ button); without a wallpaper the scheme decides.
|
||||
|
||||
## Colours: only through these variables
|
||||
|
||||
| variable | dark | light | for |
|
||||
|---|---|---|---|
|
||||
| `--bg` | `#000` | `#f6f5f1` | the page |
|
||||
| `--fg` | `#fff` | `#17171a` | text |
|
||||
| `--muted` | `#9a9a9a` | `#66666b` | secondary text, icons |
|
||||
| `--card` | `#0b0b0b` | `#ffffff` | cards, rows that stand out |
|
||||
| `--panel` | `#111` | `#ffffff` | sheets, menus, docks, bars |
|
||||
| `--hover` | `#141414` | `#ecebe6` | hover / pressed |
|
||||
| `--raised` | `#1a1a1a` | `#e4e2dc` | inputs, tracks of sliders, chips |
|
||||
| `--border` | `#2a2a2a` | `#d9d6ce` | lines |
|
||||
| `--border-strong` | `#444` | `#b4b0a6` | lines that must show (inputs, focus off) |
|
||||
| `--accent` | `#e63946` | `#d0263a` | the red: brand, active, progress |
|
||||
| `--on-accent` | `#fff` | `#fff` | text on the red |
|
||||
| `--scrim` | `rgba(0,0,0,.6)` | `rgba(20,20,20,.35)` | behind sheets |
|
||||
| `--shadow` | `none` | `0 1px 3px rgba(0,0,0,.08)` | cards in light (no lines needed in dark) |
|
||||
|
||||
Old names stay as aliases: `--nui-muted: var(--muted)`, `--nui-panel: var(--panel)`,
|
||||
`--text: var(--fg)`, `--text-muted: var(--muted)`, `--bg-card: var(--card)`.
|
||||
Images, album art, covers and Ambient's own picture keep their colours. Status colours
|
||||
(`--green`, `--yellow`, `--red`) may stay as they are.
|
||||
|
|
@ -47,13 +47,13 @@
|
|||
html, body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
scrollbar-color: #333 #000;
|
||||
scrollbar-color: var(--raised) var(--bg);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||
::-webkit-scrollbar-track { background: #000; }
|
||||
::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #555; }
|
||||
::-webkit-scrollbar-track { background: var(--bg); }
|
||||
::-webkit-scrollbar-thumb { background: var(--raised); border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: var(--border-strong); }
|
||||
|
||||
:root {
|
||||
--bg: #000;
|
||||
|
|
@ -77,6 +77,42 @@ html, body {
|
|||
--outline: #000;
|
||||
--text: var(--fg);
|
||||
--text-muted: var(--fg);
|
||||
|
||||
/* The colour scheme (docs/theme.md): dark here, light below. Use only these. */
|
||||
--muted: #9a9a9a;
|
||||
--card: #0b0b0b;
|
||||
--panel: #111;
|
||||
--hover: #141414;
|
||||
--raised: #1a1a1a;
|
||||
--border-strong: #444;
|
||||
--on-accent: #fff;
|
||||
--scrim: rgba(0, 0, 0, 0.6);
|
||||
--shadow: none;
|
||||
--surface: var(--panel);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
html[data-theme="light"] {
|
||||
--bg: #f6f5f1;
|
||||
--fg: #17171a;
|
||||
--fg-muted: #17171a;
|
||||
--muted: #66666b;
|
||||
--card: #fff;
|
||||
--panel: #fff;
|
||||
--hover: #ecebe6;
|
||||
--raised: #e4e2dc;
|
||||
--border: #d9d6ce;
|
||||
--border-strong: #b4b0a6;
|
||||
--accent: #d0263a;
|
||||
--accent-hover: #b51f31;
|
||||
--on-accent: #fff;
|
||||
--scrim: rgba(20, 20, 20, 0.35);
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
--bg-card: var(--card);
|
||||
--bg-alt: var(--hover);
|
||||
/* Outlined text is for wallpapers (body.bright-bg / a dark picture); on plain light, none. */
|
||||
--outline: transparent;
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
/* inverted: black on white */
|
||||
|
|
@ -395,7 +431,7 @@ a:hover {
|
|||
width: 100%;
|
||||
height: 70vh;
|
||||
border: 0;
|
||||
background: #000;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.sub-tabs {
|
||||
|
|
@ -477,7 +513,7 @@ a:hover {
|
|||
|
||||
.data-table tbody tr:nth-child(odd) { background: transparent; }
|
||||
.data-table tbody tr:nth-child(even) { background: transparent; }
|
||||
.data-table tbody tr:hover { background: rgba(255, 255, 255, 0.05); }
|
||||
.data-table tbody tr:hover { background: var(--hover); }
|
||||
|
||||
.empty-msg {
|
||||
color: var(--text-muted);
|
||||
|
|
@ -508,14 +544,14 @@ a:hover {
|
|||
white-space: nowrap;
|
||||
}
|
||||
.btn:hover {
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
border-color: #444;
|
||||
background: var(--hover);
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
|
||||
.btn-play {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #fff;
|
||||
color: var(--on-accent);
|
||||
font-size: 0.9rem;
|
||||
padding: 0.4rem 1rem;
|
||||
}
|
||||
|
|
@ -524,14 +560,14 @@ a:hover {
|
|||
border-color: var(--accent-hover);
|
||||
}
|
||||
.btn-play.playing {
|
||||
background: #333;
|
||||
border-color: #555;
|
||||
background: var(--raised);
|
||||
border-color: var(--border-strong);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
color: var(--yellow);
|
||||
border-color: #444;
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
.btn-save:hover {
|
||||
background: #2a2a00;
|
||||
|
|
@ -546,7 +582,7 @@ a:hover {
|
|||
.btn-primary {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #fff;
|
||||
color: var(--on-accent);
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-hover);
|
||||
|
|
@ -562,6 +598,9 @@ a:hover {
|
|||
border-color: #a00;
|
||||
color: #fff;
|
||||
}
|
||||
html[data-theme="light"] .btn-danger { background: #fdecee; border-color: #e7a3aa; color: #a3172a; }
|
||||
html[data-theme="light"] .btn-danger:hover { background: #a3172a; border-color: #a3172a; color: #fff; }
|
||||
html[data-theme="light"] .btn-save:hover { background: #fbf3d3; }
|
||||
|
||||
.btn-lastfm {
|
||||
background: #d51007;
|
||||
|
|
@ -688,6 +727,7 @@ a:hover {
|
|||
border-radius: var(--radius);
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
html[data-theme="light"] .form-errors { background: #fdecee; border-color: #e7a3aa; }
|
||||
|
||||
.field-help {
|
||||
color: var(--text-muted);
|
||||
|
|
@ -929,7 +969,7 @@ a:hover {
|
|||
|
||||
.recommendations-section {
|
||||
padding: 12px 16px 4px;
|
||||
border-bottom: 1px solid #222;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.recommendations-context {
|
||||
font-size: 0.75rem;
|
||||
|
|
@ -1009,7 +1049,7 @@ a:hover {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid #1a1a1a;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
|
|
@ -1108,13 +1148,13 @@ body.dnd-mode .timer-display {
|
|||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid #1a1a1a;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.mood-chip {
|
||||
background: transparent;
|
||||
border: 1px solid #333;
|
||||
color: #ccc;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--fg);
|
||||
border-radius: 999px;
|
||||
padding: 3px 12px;
|
||||
font-size: 0.78rem;
|
||||
|
|
@ -1123,8 +1163,8 @@ body.dnd-mode .timer-display {
|
|||
}
|
||||
|
||||
.mood-chip:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: #555;
|
||||
background: var(--hover);
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
|
||||
.focus-today {
|
||||
|
|
@ -1274,7 +1314,7 @@ body.dnd-mode .timer-display {
|
|||
.sidebar-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
background: var(--scrim);
|
||||
z-index: 299;
|
||||
}
|
||||
|
||||
|
|
@ -1334,7 +1374,7 @@ body.dnd-mode .timer-display {
|
|||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
background: var(--scrim);
|
||||
z-index: 500;
|
||||
}
|
||||
|
||||
|
|
@ -1417,7 +1457,7 @@ body.dnd-mode .timer-display {
|
|||
.book-drop-zone:hover,
|
||||
.book-drop-zone.drag-over {
|
||||
border-color: var(--accent);
|
||||
background: rgba(255,255,255,0.04);
|
||||
background: var(--hover);
|
||||
}
|
||||
.book-drop-zone label {
|
||||
color: var(--accent);
|
||||
|
|
@ -1791,13 +1831,13 @@ body.reader-immersive.reader-show-bottom .reader-overlay { bottom: var(--bar-h)
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(20, 20, 20, 0.85);
|
||||
background: var(--scrim);
|
||||
z-index: 200;
|
||||
}
|
||||
.pdf-loading-spinner {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 3px solid #333;
|
||||
border: 3px solid var(--raised);
|
||||
border-top-color: var(--accent, #e63946);
|
||||
border-radius: 50%;
|
||||
animation: pdf-spin 0.7s linear infinite;
|
||||
|
|
@ -1814,7 +1854,7 @@ body.reader-immersive.reader-show-bottom .reader-overlay { bottom: var(--bar-h)
|
|||
/* Highlight popover */
|
||||
.highlight-popover { position:fixed; z-index:500; display:flex; gap:6px; background:var(--bg-card,#1a1a1a); border:1px solid var(--border); border-radius:var(--radius); padding:6px 8px; box-shadow:0 4px 16px rgba(0,0,0,.5); }
|
||||
.hl-color-btn { width:24px; height:24px; border-radius:50%; border:2px solid transparent; cursor:pointer; font-weight:700; font-size:12px; color:#000; line-height:1; }
|
||||
.hl-color-btn:hover { border-color:#fff; }
|
||||
.hl-color-btn:hover { border-color:var(--fg); }
|
||||
.hl-note-btn { background:none; border:1px solid var(--border); color:var(--fg); padding:2px 6px; border-radius:var(--radius); cursor:pointer; }
|
||||
|
||||
/* Footnote popover */
|
||||
|
|
@ -1908,7 +1948,7 @@ body.reader-immersive.reader-show-bottom .reader-overlay { bottom: var(--bar-h)
|
|||
here. */
|
||||
.note-bottom-sheet { position:fixed; inset:0; z-index:650; }
|
||||
.note-bottom-sheet-backdrop {
|
||||
position:absolute; inset:0; background:rgba(0,0,0,.5);
|
||||
position:absolute; inset:0; background:var(--scrim);
|
||||
opacity:0; transition:opacity .2s ease; pointer-events:none;
|
||||
}
|
||||
.note-bottom-sheet.open .note-bottom-sheet-backdrop { opacity:1; pointer-events:auto; }
|
||||
|
|
@ -1951,7 +1991,9 @@ mark.tts-current { background:rgba(230,57,70,.55); color:inherit; border-radius:
|
|||
@keyframes toast-fade { 0%,70%{opacity:1} 100%{opacity:0} }
|
||||
|
||||
.reader-toast-action { display:flex; align-items:center; gap:10px; padding:8px 8px 8px 14px; animation:toast-slide-up 0.2s ease forwards; pointer-events:auto; }
|
||||
.reader-toast-btn { background:var(--accent,#e63946); color:#fff; border:none; border-radius:calc(var(--radius) - 2px); padding:5px 12px; font-family:var(--font); font-size:13px; font-weight:600; cursor:pointer; white-space:nowrap; }
|
||||
/* The apps: the player bar sits in the reader, above its footer; notices go above both. */
|
||||
html.nui-reading .reader-toast { bottom:calc(var(--nui-dock-h, 0px) + 40px); }
|
||||
.reader-toast-btn { background:var(--accent,#e63946); color:var(--on-accent); border:none; border-radius:calc(var(--radius) - 2px); padding:5px 12px; font-family:var(--font); font-size:13px; font-weight:600; cursor:pointer; white-space:nowrap; }
|
||||
.reader-toast-btn:hover { filter:brightness(1.1); }
|
||||
@keyframes toast-slide-up { 0%{opacity:0; transform:translate(-50%, 8px);} 100%{opacity:1; transform:translate(-50%, 0);} }
|
||||
|
||||
|
|
@ -1960,7 +2002,7 @@ mark.tts-current { background:rgba(230,57,70,.55); color:inherit; border-radius:
|
|||
bottom: 4px;
|
||||
right: 6px;
|
||||
font-size: 0.65rem;
|
||||
color: #444;
|
||||
color: var(--border-strong);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
|
|
|||
133
static/js/app.js
133
static/js/app.js
|
|
@ -1629,6 +1629,12 @@ function applyAccent(entry) {
|
|||
root.style.setProperty('--accent-hover', entry.hover);
|
||||
}
|
||||
|
||||
// Light and dark: the switch is in base.html (every page has it). The reader's
|
||||
// "Wie die App" follows it.
|
||||
window.addEventListener('diora-theme', () => {
|
||||
if (currentBookId) applyReaderSettings(!!currentPdfDoc);
|
||||
});
|
||||
|
||||
function setScheme(bright) {
|
||||
document.body.classList.toggle('bright-bg', bright);
|
||||
const btn = document.getElementById('contrast-toggle');
|
||||
|
|
@ -1736,6 +1742,8 @@ async function applyEncryptedBackground() {
|
|||
const blob = new Blob([plain], {type: ENCRYPTED_BG.mime || 'image/jpeg'});
|
||||
const url = URL.createObjectURL(blob);
|
||||
document.body.style.backgroundImage = `url('${url}')`;
|
||||
const contrastBtn = document.getElementById('contrast-toggle');
|
||||
if (contrastBtn) contrastBtn.hidden = false; // only with a picture behind the page
|
||||
document.body.style.backgroundSize = 'cover';
|
||||
document.body.style.backgroundPosition = 'center';
|
||||
document.body.style.backgroundAttachment = 'fixed';
|
||||
|
|
@ -3770,6 +3778,8 @@ let ttsAudio = null;
|
|||
let ttsCurrentMark = null;
|
||||
let ttsBarEl = null;
|
||||
let ttsLang = localStorage.getItem('diora_tts_lang') || 'de';
|
||||
let _ttsBlocks = null; // the book's blocks while reading aloud
|
||||
let _ttsCur = null; // what's being read: {bi, si, sentence, block}
|
||||
|
||||
function setTtsLang(lang) {
|
||||
ttsLang = lang;
|
||||
|
|
@ -3862,15 +3872,19 @@ function _ttsPlayBlob(blob) {
|
|||
});
|
||||
}
|
||||
|
||||
// Walks blocks/sentences from startBlockIndex onward, fetching one sentence
|
||||
// ahead while the current one plays so there's no gap between them.
|
||||
async function _ttsPlayLoop(blocks, startBlockIndex, runToken) {
|
||||
// Walks blocks/sentences from startBlockIndex (and its sentence startSentence)
|
||||
// onward, fetching one sentence ahead while the current one plays so there's
|
||||
// no gap between them.
|
||||
async function _ttsPlayLoop(blocks, startBlockIndex, runToken, startSentence = 0) {
|
||||
function* sentenceStream() {
|
||||
for (let bi = startBlockIndex; bi < blocks.length; bi++) {
|
||||
const block = blocks[bi];
|
||||
const text = (block.textContent || '').trim();
|
||||
if (!text) continue;
|
||||
for (const sentence of _ttsSplitSentences(text)) yield {block, sentence};
|
||||
const sentences = _ttsSplitSentences(text);
|
||||
for (let si = bi === startBlockIndex ? startSentence : 0; si < sentences.length; si++) {
|
||||
yield {block, sentence: sentences[si], bi, si};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3896,8 +3910,14 @@ async function _ttsPlayLoop(blocks, startBlockIndex, runToken) {
|
|||
}
|
||||
if (runToken !== ttsRunToken) return;
|
||||
|
||||
if (block !== lastBlock) { lastBlock = block; blockSearchOffset = 0; }
|
||||
if (block !== lastBlock) {
|
||||
lastBlock = block;
|
||||
// Started in the middle of a block (previous/next sentence): find it from there.
|
||||
blockSearchOffset = _ttsOffsetOf(block, cur.value.si);
|
||||
}
|
||||
_ttsCur = {bi: cur.value.bi, si: cur.value.si, sentence, block};
|
||||
blockSearchOffset = _ttsHighlightSentence(block, sentence, blockSearchOffset);
|
||||
_ttsNotify();
|
||||
|
||||
await _ttsPlayBlob(audioBlob);
|
||||
if (runToken !== ttsRunToken) return;
|
||||
|
|
@ -3915,8 +3935,90 @@ function _ttsTogglePause() {
|
|||
else ttsAudio.play().catch(() => {});
|
||||
}
|
||||
_ttsUpdateBar();
|
||||
_ttsNotify();
|
||||
}
|
||||
|
||||
// ---- read-aloud for a player bar (the shared dock in native-podcast.js) ----------
|
||||
// window.__dioraTts: state(), toggle(), stop(), next(), prev(); a 'diora-tts'
|
||||
// event on window after every change. A dock that shows it sets
|
||||
// window.__dioraTtsDock = true, and the reader's own little bar stays away.
|
||||
|
||||
function _ttsState() {
|
||||
const meta = (currentBookId && bookMetaCache[currentBookId]) || {};
|
||||
const cur = _ttsCur;
|
||||
const chapterEl = cur && cur.block.closest('[data-epub-src]');
|
||||
const blocks = _ttsBlocks || [];
|
||||
return {
|
||||
active: ttsActive,
|
||||
paused: ttsPaused,
|
||||
bookId: currentBookId,
|
||||
title: meta.title || '',
|
||||
author: meta.author || '',
|
||||
chapter: chapterEl ? _tocTitleForChapterSrc(chapterEl.dataset.epubSrc) : '',
|
||||
sentence: cur ? cur.sentence : '',
|
||||
progress: cur && blocks.length ? cur.bi / blocks.length : 0,
|
||||
};
|
||||
}
|
||||
|
||||
function _ttsNotify() {
|
||||
try { window.dispatchEvent(new CustomEvent('diora-tts', {detail: _ttsState()})); } catch (e) {}
|
||||
}
|
||||
|
||||
// Read from another sentence on: the one before/after the current one.
|
||||
function _ttsRestartAt(bi, si) {
|
||||
const blocks = _ttsBlocks;
|
||||
if (!ttsActive || !blocks) return;
|
||||
ttsRunToken++;
|
||||
const runToken = ttsRunToken;
|
||||
if (ttsAudio) {
|
||||
try { ttsAudio.pause(); ttsAudio.src = ''; } catch (e) {}
|
||||
ttsAudio = null;
|
||||
}
|
||||
_ttsPlayLoop(blocks, bi, runToken, si);
|
||||
}
|
||||
|
||||
// Where sentence si of a block starts in its text (for the highlight).
|
||||
function _ttsOffsetOf(block, si) {
|
||||
const full = block.textContent || '';
|
||||
let off = 0;
|
||||
_ttsSplitSentences(full.trim()).slice(0, si).forEach(s => {
|
||||
const k = full.indexOf(s, off);
|
||||
if (k >= 0) off = k + s.length;
|
||||
});
|
||||
return off;
|
||||
}
|
||||
|
||||
function _ttsSentenceCount(block) {
|
||||
const text = (block.textContent || '').trim();
|
||||
return text ? _ttsSplitSentences(text).length : 0;
|
||||
}
|
||||
|
||||
function _ttsStep(dir) {
|
||||
const cur = _ttsCur;
|
||||
const blocks = _ttsBlocks;
|
||||
if (!cur || !blocks) return;
|
||||
if (dir > 0) {
|
||||
if (cur.si + 1 < _ttsSentenceCount(cur.block)) return _ttsRestartAt(cur.bi, cur.si + 1);
|
||||
for (let bi = cur.bi + 1; bi < blocks.length; bi++) if (_ttsSentenceCount(blocks[bi])) return _ttsRestartAt(bi, 0);
|
||||
stopReadAloud();
|
||||
} else {
|
||||
if (cur.si > 0) return _ttsRestartAt(cur.bi, cur.si - 1);
|
||||
for (let bi = cur.bi - 1; bi >= 0; bi--) {
|
||||
const n = _ttsSentenceCount(blocks[bi]);
|
||||
if (n) return _ttsRestartAt(bi, n - 1);
|
||||
}
|
||||
_ttsRestartAt(cur.bi, 0);
|
||||
}
|
||||
}
|
||||
|
||||
window.__dioraTts = {
|
||||
state: _ttsState,
|
||||
toggle: () => (ttsActive ? _ttsTogglePause() : startReadAloud()),
|
||||
stop: () => stopReadAloud(),
|
||||
next: () => _ttsStep(1),
|
||||
prev: () => _ttsStep(-1),
|
||||
};
|
||||
|
||||
function _ttsUpdateBar() {
|
||||
if (!ttsBarEl) return;
|
||||
const btn = ttsBarEl.querySelector('.tts-pause-btn');
|
||||
|
|
@ -3925,6 +4027,7 @@ function _ttsUpdateBar() {
|
|||
|
||||
function _ttsShowBar() {
|
||||
_ttsRemoveBar();
|
||||
if (window.__dioraTtsDock) return; // the player bar shows it
|
||||
const bar = document.createElement('div');
|
||||
bar.className = 'tts-bar';
|
||||
bar.innerHTML = `
|
||||
|
|
@ -3968,10 +4071,13 @@ function startReadAloud() {
|
|||
ttsPaused = false;
|
||||
ttsRunToken++;
|
||||
const runToken = ttsRunToken;
|
||||
_ttsBlocks = blocks;
|
||||
_ttsCur = null;
|
||||
|
||||
const btn = $('reader-tts-btn');
|
||||
if (btn) { btn.classList.add('active'); btn.title = 'Vorlesen beenden'; }
|
||||
_ttsShowBar();
|
||||
_ttsNotify();
|
||||
|
||||
_ttsPlayLoop(blocks, startIndex, runToken);
|
||||
}
|
||||
|
|
@ -3986,8 +4092,11 @@ function stopReadAloud() {
|
|||
}
|
||||
_ttsClearHighlight();
|
||||
_ttsRemoveBar();
|
||||
_ttsBlocks = null;
|
||||
_ttsCur = null;
|
||||
const btn = $('reader-tts-btn');
|
||||
if (btn) { btn.classList.remove('active'); btn.title = 'Vorlesen'; }
|
||||
_ttsNotify();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -4647,7 +4756,7 @@ function _readerTypeDefaults() {
|
|||
|
||||
function loadReaderSettings(bookId) {
|
||||
// Reset to defaults, then apply per-book overrides
|
||||
Object.assign(readerSettings, { theme: 'dark', fontFamily: 'serif', noBold: false,
|
||||
Object.assign(readerSettings, { theme: 'app', fontFamily: 'serif', noBold: false,
|
||||
pdfZoom: 100, pdfInverted: false, pdfPaginated: false, pdfSpread: false },
|
||||
_readerTypeDefaults());
|
||||
try {
|
||||
|
|
@ -4659,6 +4768,8 @@ function loadReaderSettings(bookId) {
|
|||
if (saved[key] === old) delete saved[key];
|
||||
}
|
||||
}
|
||||
// Before v3 the reader's theme was always stored, "dark" by default: that now means "Wie die App".
|
||||
if ((saved.v || 0) < 3 && saved.theme === 'dark') delete saved.theme;
|
||||
delete saved.v;
|
||||
Object.assign(readerSettings, saved);
|
||||
if (saved.pdfPaginated === undefined) {
|
||||
|
|
@ -4669,7 +4780,7 @@ function loadReaderSettings(bookId) {
|
|||
|
||||
function saveReaderSettings() {
|
||||
if (currentBookId) {
|
||||
localStorage.setItem(`diora_reader_settings_${currentBookId}`, JSON.stringify({ ...readerSettings, v: 2 }));
|
||||
localStorage.setItem(`diora_reader_settings_${currentBookId}`, JSON.stringify({ ...readerSettings, v: 3 }));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4708,8 +4819,11 @@ function applyReaderSettings(isPdf) {
|
|||
|
||||
// Theme
|
||||
overlay.classList.remove('reader-theme-sepia', 'reader-theme-bright');
|
||||
if (readerSettings.theme === 'sepia') overlay.classList.add('reader-theme-sepia');
|
||||
else if (readerSettings.theme === 'bright') overlay.classList.add('reader-theme-bright');
|
||||
const readerTheme = readerSettings.theme === 'app'
|
||||
? (document.documentElement.getAttribute('data-theme') === 'light' ? 'bright' : 'dark')
|
||||
: readerSettings.theme;
|
||||
if (readerTheme === 'sepia') overlay.classList.add('reader-theme-sepia');
|
||||
else if (readerTheme === 'bright') overlay.classList.add('reader-theme-bright');
|
||||
|
||||
// PDF invert
|
||||
if (isPdf && readerSettings.pdfInverted) overlay.classList.add('pdf-inverted');
|
||||
|
|
@ -4767,6 +4881,7 @@ function toggleSettingsPanel() {
|
|||
<button class="btn btn-sm ${readerSettings.fontFamily === 'humanist' ? 'active' : ''}" data-rs-font="humanist">Verdana</button>
|
||||
<button class="btn btn-sm ${readerSettings.fontFamily === 'mono' ? 'active' : ''}" data-rs-font="mono">Mono</button>
|
||||
<button class="btn btn-sm ${readerSettings.noBold ? 'active' : ''}" id="rs-no-bold">Kein Fett</button>
|
||||
<button class="btn btn-sm ${readerSettings.theme === 'app' ? 'active' : ''}" data-rs-theme="app">Wie die App</button>
|
||||
<button class="btn btn-sm ${readerSettings.theme === 'dark' ? 'active' : ''}" data-rs-theme="dark">Dunkel</button>
|
||||
<button class="btn btn-sm ${readerSettings.theme === 'sepia' ? 'active' : ''}" data-rs-theme="sepia">Sepia</button>
|
||||
<button class="btn btn-sm ${readerSettings.theme === 'bright' ? 'active' : ''}" data-rs-theme="bright">Hell</button>
|
||||
|
|
|
|||
|
|
@ -331,27 +331,27 @@
|
|||
'#tab-home .home-brand { font-size: 26px; font-weight: 700; color: var(--accent, #e63946); margin: 4px 0 18px; letter-spacing: 0.05em; }',
|
||||
'#tab-home .home-section { margin-bottom: 22px; }',
|
||||
'#tab-home .home-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 8px; }',
|
||||
'#tab-home .home-head h2 { margin: 0; font-size: 12px; font-weight: 400; text-transform: uppercase; letter-spacing: 0.08em; color: var(--nui-muted, #9a9a9a); }',
|
||||
'#tab-home .home-more { background: none; border: none; color: var(--nui-muted, #9a9a9a); font: inherit; font-size: 12px; cursor: pointer; padding: 4px 0; }',
|
||||
'#tab-home .home-card { display: flex; align-items: center; gap: 12px; width: 100%; box-sizing: border-box; padding: 14px; border: 1px solid var(--border, #2a2a2a); border-radius: 6px; background: #0b0b0b; color: inherit; font: inherit; text-align: left; }',
|
||||
'#tab-home .home-head h2 { margin: 0; font-size: 12px; font-weight: 400; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted, #9a9a9a); }',
|
||||
'#tab-home .home-more { background: none; border: none; color: var(--muted, #9a9a9a); font: inherit; font-size: 12px; cursor: pointer; padding: 4px 0; }',
|
||||
'#tab-home .home-card { display: flex; align-items: center; gap: 12px; width: 100%; box-sizing: border-box; padding: 14px; border: 1px solid var(--border, #2a2a2a); border-radius: 6px; background: var(--card, #0b0b0b); box-shadow: var(--shadow, none); color: inherit; font: inherit; text-align: left; }',
|
||||
'#tab-home button.home-card, #tab-home .home-pod-text, #tab-home .home-row, #tab-home .home-station { cursor: pointer; }',
|
||||
'#tab-home .home-main { flex-wrap: nowrap; }',
|
||||
'#tab-home button.home-main { flex-direction: column; align-items: flex-start; gap: 3px; }',
|
||||
'#tab-home .home-empty { color: var(--nui-muted, #9a9a9a); justify-content: space-between; }',
|
||||
'#tab-home .home-empty { color: var(--muted, #9a9a9a); justify-content: space-between; }',
|
||||
'#tab-home .home-kicker { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--accent, #e63946); }',
|
||||
'#tab-home .home-title { font-size: 16px; font-weight: 700; line-height: 1.3; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }',
|
||||
'#tab-home .home-sub { font-size: 13px; color: var(--nui-muted, #9a9a9a); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }',
|
||||
'#tab-home .home-state { display: flex; align-items: center; gap: 8px; margin-top: 6px; font-size: 12px; color: var(--nui-muted, #9a9a9a); }',
|
||||
'#tab-home .home-bar { display: inline-block; width: 160px; max-width: 40vw; height: 4px; border-radius: 2px; background: #2a2a2a; overflow: hidden; }',
|
||||
'#tab-home .home-sub { font-size: 13px; color: var(--muted, #9a9a9a); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }',
|
||||
'#tab-home .home-state { display: flex; align-items: center; gap: 8px; margin-top: 6px; font-size: 12px; color: var(--muted, #9a9a9a); }',
|
||||
'#tab-home .home-bar { display: inline-block; width: 160px; max-width: 40vw; height: 4px; border-radius: 2px; background: var(--raised, #2a2a2a); overflow: hidden; }',
|
||||
'#tab-home .home-bar > span { display: block; height: 100%; background: var(--accent, #e63946); }',
|
||||
'#tab-home .home-pod-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }',
|
||||
'#tab-home .home-art { width: 56px; height: 56px; flex: none; border-radius: 4px; object-fit: cover; background: #1a1a1a; }',
|
||||
'#tab-home .home-art { width: 56px; height: 56px; flex: none; border-radius: 4px; object-fit: cover; background: var(--raised, #1a1a1a); }',
|
||||
'#tab-home .home-amb-icon { width: 56px; height: 56px; flex: none; border-radius: 4px; background: #14102a; color: #b9a7ff; display: inline-flex; align-items: center; justify-content: center; font-size: 26px; }',
|
||||
'#tab-home .home-play { flex: none; min-height: 36px; padding: 0 14px; }',
|
||||
'#tab-home .home-list { margin-top: 6px; }',
|
||||
'#tab-home .home-row { display: flex; justify-content: space-between; gap: 12px; width: 100%; padding: 10px 14px; background: none; border: none; border-bottom: 1px solid var(--border, #2a2a2a); color: inherit; font: inherit; font-size: 14px; text-align: left; }',
|
||||
'#tab-home .home-row-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
|
||||
'#tab-home .home-row-side { flex: none; color: var(--nui-muted, #9a9a9a); font-size: 12px; }',
|
||||
'#tab-home .home-row-side { flex: none; color: var(--muted, #9a9a9a); font-size: 12px; }',
|
||||
'#tab-home .home-news { flex-direction: column; align-items: stretch; gap: 0; padding: 4px 0; }',
|
||||
'#tab-home .home-story { align-items: baseline; }',
|
||||
'#tab-home .home-news .home-story:last-child { border-bottom: none; }',
|
||||
|
|
@ -360,12 +360,12 @@
|
|||
'#tab-home .home-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }',
|
||||
'#tab-home .home-chip { padding: 6px 12px; border: 1px solid var(--border, #2a2a2a); border-radius: 16px; background: none; color: inherit; font: inherit; font-size: 13px; cursor: pointer; }',
|
||||
'#tab-home .home-stations { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 8px; }',
|
||||
'#tab-home .home-station { display: flex; align-items: center; gap: 10px; min-height: 48px; padding: 8px 12px; border: 1px solid var(--border, #2a2a2a); border-radius: 6px; background: #0b0b0b; color: inherit; font: inherit; font-size: 14px; text-align: left; }',
|
||||
'#tab-home .home-station { display: flex; align-items: center; gap: 10px; min-height: 48px; padding: 8px 12px; border: 1px solid var(--border, #2a2a2a); border-radius: 6px; background: var(--card, #0b0b0b); box-shadow: var(--shadow, none); color: inherit; font: inherit; font-size: 14px; text-align: left; }',
|
||||
'#tab-home .home-station-icon { color: var(--accent, #e63946); flex: none; }',
|
||||
'#tab-home .home-station-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
|
||||
'#tab-home .home-on { border-color: var(--accent, #e63946); }',
|
||||
'#tab-home .home-note { margin-top: 8px; font-size: 13px; color: var(--nui-muted, #9a9a9a); }',
|
||||
'@media (hover: hover) { #tab-home .home-card:hover, #tab-home .home-station:hover, #tab-home .home-row:hover { background: #141414; } }',
|
||||
'#tab-home .home-note { margin-top: 8px; font-size: 13px; color: var(--muted, #9a9a9a); }',
|
||||
'@media (hover: hover) { #tab-home .home-card:hover, #tab-home .home-station:hover, #tab-home .home-row:hover { background: var(--hover, #141414); } }',
|
||||
'.reader-theme-bright #tab-home, body.eink #tab-home .home-card { background: #fff; }',
|
||||
].join('\n');
|
||||
document.head.appendChild(style);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* diora service worker — caches the app shell for offline use.
|
||||
*/
|
||||
|
||||
const CACHE = 'diora-v101';
|
||||
const CACHE = 'diora-v102';
|
||||
const SHELL = [
|
||||
'/static/css/app.css',
|
||||
'/static/js/app.js',
|
||||
|
|
|
|||
|
|
@ -11,6 +11,50 @@
|
|||
{% if user.is_authenticated %}<meta name="username" content="{{ user.username }}">{% endif %}
|
||||
<link rel="manifest" href="/static/manifest.json">
|
||||
<link rel="apple-touch-icon" href="/static/icon-192.png">
|
||||
<script>
|
||||
// Light and dark (docs/theme.md): the choice is "dark", "light" or "auto" (as the
|
||||
// system says). Set before anything paints; every page has it, app.js or not.
|
||||
// Plain ES5: old e-reader WebViews run this too.
|
||||
(function () {
|
||||
var KEY = 'diora_theme';
|
||||
var LABELS = {dark: 'Dunkel', light: 'Hell', auto: 'Automatisch'};
|
||||
var mq = window.matchMedia ? window.matchMedia('(prefers-color-scheme: light)') : null;
|
||||
function choice() {
|
||||
var v = null;
|
||||
try { v = localStorage.getItem(KEY); } catch (e) {}
|
||||
return v === 'light' || v === 'auto' ? v : 'dark';
|
||||
}
|
||||
function scheme() {
|
||||
var c = choice();
|
||||
return c === 'light' || (c === 'auto' && mq && mq.matches) ? 'light' : 'dark';
|
||||
}
|
||||
function apply() {
|
||||
var s = scheme();
|
||||
document.documentElement.setAttribute('data-theme', s);
|
||||
var meta = document.querySelector('meta[name="theme-color"]');
|
||||
if (meta) meta.setAttribute('content', s === 'light' ? '#f6f5f1' : '#000000');
|
||||
var b = document.getElementById('theme-toggle');
|
||||
if (b) {
|
||||
var c = choice();
|
||||
b.textContent = c === 'light' ? '\u2600' : c === 'auto' ? '\u25D0' : '\u263E';
|
||||
b.title = 'Erscheinungsbild: ' + LABELS[c];
|
||||
}
|
||||
try { window.dispatchEvent(new CustomEvent('diora-theme', {detail: s})); } catch (e) {}
|
||||
}
|
||||
window.__dioraTheme = choice;
|
||||
window.__dioraSetTheme = function (c) {
|
||||
try { localStorage.setItem(KEY, LABELS[c] ? c : 'dark'); } catch (e) {}
|
||||
apply();
|
||||
};
|
||||
window.cycleTheme = function () {
|
||||
var order = ['dark', 'light', 'auto'];
|
||||
window.__dioraSetTheme(order[(order.indexOf(choice()) + 1) % order.length]);
|
||||
};
|
||||
if (mq && mq.addEventListener) mq.addEventListener('change', function () { if (choice() === 'auto') apply(); });
|
||||
document.addEventListener('DOMContentLoaded', apply);
|
||||
document.documentElement.setAttribute('data-theme', scheme());
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="/static/css/app.css">
|
||||
<title>{% block title %}diora{% endblock %}</title>
|
||||
<script>const DIORA_CONFIG = { ebookMaxBytes: {{ EBOOK_MAX_BYTES }}, bgMaxBytes: {{ BG_MAX_BYTES }}, podcastInboxPageSize: {{ PODCAST_INBOX_PAGE_SIZE }} };</script>
|
||||
|
|
@ -22,7 +66,8 @@
|
|||
<nav class="navbar">
|
||||
<a href="/" class="navbar-brand">diora</a>
|
||||
<div class="navbar-links">
|
||||
<button class="btn-icon contrast-toggle" id="contrast-toggle" onclick="toggleContrast()" title="Kontrastmodus an/aus">◑</button>
|
||||
<button class="btn-icon theme-toggle" id="theme-toggle" onclick="cycleTheme()" title="Erscheinungsbild">☾</button>
|
||||
<button class="btn-icon contrast-toggle" id="contrast-toggle" onclick="toggleContrast()" title="Kontrast zum Hintergrundbild" hidden>◑</button>
|
||||
{% if user.is_authenticated %}
|
||||
<span class="navbar-user">{{ user.username }}</span>
|
||||
<a href="{% url 'settings' %}">Einstellungen</a>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue