diff --git a/docs/theme.md b/docs/theme.md new file mode 100644 index 0000000..4126c77 --- /dev/null +++ b/docs/theme.md @@ -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 + +* `` is the scheme in effect. Set as early as possible + (an inline script in base.html's `
`, 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. diff --git a/static/css/app.css b/static/css/app.css index 36e4a4b..5689138 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -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; } diff --git a/static/js/app.js b/static/js/app.js index e7ac60e..1dad5a3 100644 --- a/static/js/app.js +++ b/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() { + diff --git a/static/js/home.js b/static/js/home.js index e50cf7f..712ad66 100644 --- a/static/js/home.js +++ b/static/js/home.js @@ -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); diff --git a/static/js/sw.js b/static/js/sw.js index 2113fd9..32bd9f3 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -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', diff --git a/templates/base.html b/templates/base.html index 64f4baa..9d26a8a 100644 --- a/templates/base.html +++ b/templates/base.html @@ -11,6 +11,50 @@ {% if user.is_authenticated %}{% endif %} +