feat(reader): implement full appearance customization panel with themes, fonts, margins, line-height, and localStorage persistence

This commit is contained in:
2026-10-05 16:15:20 +03:00
parent c0fdcd776b
commit 84e9be614e
4 changed files with 589 additions and 37 deletions
+27 -5
View File
@@ -98,7 +98,29 @@
---
### 5. 🔌 Пул соединений HTTP (`httpx.AsyncClient` с Keep-Alive)
---
### 5. 🔤 Кастомизация оформления читалки (Типографика, темы, отступы, LocalStorage)
- [x] **Панель настроек оформления текста (`Aa`)**:
- Кнопка вызова настроек в тулбаре читалки: поповер на ПК и выдвижная шторка (Bottom Sheet) на смартфонах.
- [x] **Настройка размера шрифта и степпер**:
- Степпер `A-` / `A+` с индикатором точного размера (от 14px до 34px).
- [x] **Семейство шрифтов (Font Family)**:
- Переключение в 1 клик между `Sans` (системный), `Serif` (книжный с засечками), `Mono` (моноширинный).
- [x] **Межстрочный интервал (Line Height)**:
- Компактный (1.35), Оптимальный (1.65), Широкий (1.95).
- [x] **Поля и ширина колонки (Margins & Max Width)**:
- Узкие поля (940px), Стандарт (760px), Книжная колонка (600px).
- [x] **Цветовые темы читалки (Themes)**:
- 🌙 Тёмная (Dark Slate), 📜 Сепия (Тёплый пергамент), ☀️ Светлая (Чистый лист), 🖤 OLED (Абсолютный чёрный).
- [x] **Выравнивание текста (Text Alignment)**:
- По левому краю / По ширине (Justified).
- [x] **Автосохранение и персистентность**:
- Мгновенное реактивное применение стилей через CSS-переменные без перезагрузки и сохранение настроек в `localStorage`.
---
### 6. 🔌 Пул соединений HTTP (`httpx.AsyncClient` с Keep-Alive)
- [ ] В [`app/dictionary.py`](file:///home/ai-projects/services/study.qpid.pro/app/dictionary.py) создать синглтон-клиент `get_http_client()` с пулом соединений:
```python
limits = httpx.Limits(max_keepalive_connections=10, max_connections=20)
@@ -108,7 +130,7 @@
---
### 6. 🗄️ Тюнинг SQLite PRAGMA и составных индексов
### 7. 🗄️ Тюнинг SQLite PRAGMA и составных индексов
- [ ] В [`app/database.py`](file:///home/ai-projects/services/study.qpid.pro/app/database.py):
- Вынести `PRAGMA journal_mode=WAL;` в `init_db()` (убрать избыточный вызов при каждом запросе).
- Включить параметры производительности памяти Linux:
@@ -121,13 +143,13 @@
---
### 7. ✂️ Де-блоатинг внешних парсеров (YAGNI)
### 8. ✂️ Де-блоатинг внешних парсеров (YAGNI)
- [ ] Удалить лишний сетевой опрос Datamuse (`fetch_datamuse`) — он дублирует определения и перегружает ответ нерелевантными рифмами.
- [ ] Упростить хрупкий regex-парсинг разметки Wikitext, опираясь на стабильные REST JSON эндпоинты.
---
### 8. 📚 Локальный оффлайн-словарь (99%+ покрытия со словоформами, SQLite 10–20 МБ)
### 9. 📚 Локальный оффлайн-словарь (99%+ покрытия со словоформами, SQLite 10–20 МБ)
- [ ] Создать в SQLite таблицу:
```sql
CREATE TABLE IF NOT EXISTS offline_dictionary (
@@ -149,7 +171,7 @@
---
### 9. 🧩 Модульность фронтенда на нативных ES-модулях (без Webpack/Vite)
### 10. 🧩 Модульность фронтенда на нативных ES-модулях (без Webpack/Vite)
- [ ] Перевести [`app/static/js/app.js`](file:///home/ai-projects/services/study.qpid.pro/app/static/js/app.js) на нативный браузерный синтаксис `<script type="module">`.
- [ ] Выделить модули с нативным `import/export`:
- `modules/reader.js` — читалку, блок-парсер и навигацию по главам.
+281 -9
View File
@@ -2822,6 +2822,22 @@ body {
animation: slideUpSheet 0.22s cubic-bezier(0.16, 1, 0.3, 1) !important;
}
.reader-settings-panel {
position: fixed !important;
top: auto !important;
bottom: calc(58px + env(safe-area-inset-bottom, 0px)) !important;
left: 8px !important;
right: 8px !important;
width: auto !important;
max-height: calc(85dvh - 60px) !important;
overflow-y: auto !important;
-webkit-overflow-scrolling: touch !important;
border-radius: 20px !important;
box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.55) !important;
z-index: 10006 !important;
animation: slideUpSheet 0.22s cubic-bezier(0.16, 1, 0.3, 1) !important;
}
@keyframes slideUpSheet {
from {
transform: translateY(100%);
@@ -3127,22 +3143,278 @@ body {
display: flex;
align-items: center;
gap: 4px;
}
.reader-scroll-area {
padding: 36px 24px 60px;
max-width: 780px;
margin: 0 auto;
--reader-font-size: 18px;
--reader-line-height: 1.65;
--reader-font-family: 'Literata', 'Merriweather', 'Georgia', 'Cambria', serif;
--reader-max-width: 760px;
--reader-padding-h: 24px;
--reader-text-align: left;
--reader-bg: var(--bg-surface);
--reader-text: var(--text-main);
background-color: var(--reader-bg);
min-height: 60vh;
padding: 0;
transition: background-color 0.2s ease, color 0.2s ease;
}
.reader-content-body {
font-family: Georgia, 'Times New Roman', serif;
font-size: 19px;
line-height: 1.85;
color: var(--text-main);
max-width: var(--reader-max-width);
margin: 0 auto;
padding: 32px var(--reader-padding-h) 80px;
font-family: var(--reader-font-family);
font-size: var(--reader-font-size);
line-height: var(--reader-line-height);
text-align: var(--reader-text-align);
color: var(--reader-text);
word-break: break-word;
user-select: text;
transition: font-size 0.15s ease, line-height 0.15s ease, max-width 0.2s ease, padding 0.2s ease;
}
/* Reader Appearance Themes */
.reader-scroll-area.reader-theme-sepia {
--reader-bg: #fbf0d9;
--reader-text: #433422;
}
.reader-scroll-area.reader-theme-sepia .reader-word:hover {
background-color: rgba(217, 119, 6, 0.25);
color: #92400e;
}
.reader-scroll-area.reader-theme-sepia blockquote {
background-color: #f4e4c1;
border-color: #d6bc97;
color: #5f4b32;
}
.reader-scroll-area.reader-theme-sepia .reader-inline-code,
.reader-scroll-area.reader-theme-sepia .reader-code-wrap pre {
background-color: #efe0bc;
color: #433422;
border-color: #dec89f;
}
.reader-scroll-area.reader-theme-sepia .reader-table th,
.reader-scroll-area.reader-theme-sepia .reader-table td {
border-color: #dec89f;
}
.reader-scroll-area.reader-theme-light {
--reader-bg: #ffffff;
--reader-text: #1e293b;
}
.reader-scroll-area.reader-theme-light blockquote {
background-color: #f8fafc;
border-color: #cbd5e1;
color: #334155;
}
.reader-scroll-area.reader-theme-light .reader-inline-code,
.reader-scroll-area.reader-theme-light .reader-code-wrap pre {
background-color: #f1f5f9;
color: #0f172a;
border-color: #e2e8f0;
}
.reader-scroll-area.reader-theme-light .reader-word:hover {
background-color: rgba(2, 132, 199, 0.15);
color: #0284c7;
}
.reader-scroll-area.reader-theme-oled {
--reader-bg: #000000;
--reader-text: #d8e2ec;
}
.reader-scroll-area.reader-theme-oled blockquote {
background-color: #0a0e17;
border-color: #1e293b;
}
.reader-scroll-area.reader-theme-oled .reader-inline-code,
.reader-scroll-area.reader-theme-oled .reader-code-wrap pre {
background-color: #0a0e17;
border-color: #1e293b;
}
.reader-scroll-area.reader-theme-dark {
--reader-bg: var(--bg-surface);
--reader-text: var(--text-main);
}
/* Reader Font Families */
.reader-scroll-area.reader-font-sans {
--reader-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
.reader-scroll-area.reader-font-serif {
--reader-font-family: 'Literata', 'Merriweather', 'Georgia', 'Cambria', 'Times New Roman', serif;
}
.reader-scroll-area.reader-font-mono {
--reader-font-family: 'JetBrains Mono', 'Fira Code', 'Menlo', 'Consolas', monospace;
}
/* Reader Margin & Width Variants */
.reader-scroll-area.reader-margins-narrow {
--reader-max-width: 940px;
--reader-padding-h: 12px;
}
.reader-scroll-area.reader-margins-normal {
--reader-max-width: 760px;
--reader-padding-h: 24px;
}
.reader-scroll-area.reader-margins-wide {
--reader-max-width: 600px;
--reader-padding-h: 36px;
}
/* Reader Text Alignment */
.reader-scroll-area.reader-align-left {
--reader-text-align: left;
}
.reader-scroll-area.reader-align-justify {
--reader-text-align: justify;
hyphens: auto;
}
/* Reader Appearance Settings Panel / Sheet */
.reader-settings-panel {
position: fixed;
top: 64px;
right: 28px;
width: 320px;
background-color: var(--bg-surface);
border: 1px solid var(--border-focus);
border-radius: var(--radius-lg);
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.45);
z-index: 10002;
padding: 16px;
animation: fadeIn 0.15s ease-out;
}
.reader-settings-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border-color);
}
.reader-settings-header h4 {
font-size: 15px;
font-weight: 700;
color: var(--text-main);
}
.reader-settings-body {
display: flex;
flex-direction: column;
gap: 12px;
}
.setting-row {
display: flex;
flex-direction: column;
gap: 6px;
}
.setting-label {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-muted);
}
.setting-segmented-control {
display: flex;
background-color: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 2px;
gap: 2px;
}
.seg-btn {
flex: 1;
padding: 6px 8px;
font-size: 12px;
font-weight: 600;
background: transparent;
border: none;
border-radius: 4px;
color: var(--text-muted);
cursor: pointer;
transition: var(--transition);
text-align: center;
}
.seg-btn:hover {
color: var(--text-main);
}
.seg-btn.active {
background-color: var(--primary);
color: #0f172a;
}
.setting-stepper {
display: flex;
align-items: center;
justify-content: space-between;
background-color: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 4px 6px;
}
.step-btn {
background: var(--bg-surface-hover);
border: 1px solid var(--border-color);
border-radius: 4px;
color: var(--text-main);
font-size: 13px;
font-weight: 700;
width: 38px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.step-val {
font-size: 14px;
font-weight: 700;
color: var(--primary);
}
.theme-palette {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.theme-chip {
padding: 7px 10px;
font-size: 12px;
font-weight: 600;
border-radius: var(--radius-sm);
border: 2px solid transparent;
cursor: pointer;
text-align: center;
transition: var(--transition);
}
.theme-chip-dark { background: #1e293b; color: #f8fafc; border-color: #334155; }
.theme-chip-sepia { background: #fbf0d9; color: #433422; border-color: #d6bc97; }
.theme-chip-light { background: #ffffff; color: #0f172a; border-color: #cbd5e1; }
.theme-chip-oled { background: #000000; color: #e2e8f0; border-color: #334155; }
.theme-chip.active {
border-color: var(--primary) !important;
box-shadow: 0 0 0 2px var(--primary-dim);
}
.reader-settings-btn {
display: inline-flex;
align-items: center;
gap: 4px;
}
.reader-content-body h1,
+76 -6
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Notes — Персональный тренажер и словарь английского языка</title>
<meta name="description" content="Защищённый персональный тренажёр и словарь английского языка с интервальными повторениями SRS SM-2, контекстным поиском и озвучкой">
<link rel="stylesheet" href="/static/css/style.css?v=3.1">
<link rel="stylesheet" href="/static/css/style.css?v=3.2">
<!-- PWA meta and icons -->
<link rel="manifest" href="/manifest.json">
@@ -567,10 +567,10 @@
</div>
<div class="reader-toolbar-right">
<div class="font-controls">
<button id="reader-font-dec-btn" class="btn btn-xs btn-outline" title="Уменьшить шрифт">A-</button>
<button id="reader-font-inc-btn" class="btn btn-xs btn-outline" title="Увеличить шрифт">A+</button>
</div>
<button id="reader-settings-btn" class="btn btn-xs btn-outline reader-settings-btn" title="Настройки оформления текста (шрифт, поля, тема)">
<span style="font-weight: 700; font-family: serif; font-size: 13px;">Aa</span>
<span class="btn-subtext-inline">Шрифт</span>
</button>
<button id="reader-toggle-highlight-btn" class="btn btn-xs btn-outline" title="Вкл/Выкл подсветку изучаемых слов">✨ Подсветка</button>
<button id="reader-train-btn" class="btn btn-xs btn-trainer" title="Тренировать слова из этой книги">🎓 Слова книги</button>
</div>
@@ -952,6 +952,76 @@
</div>
</div>
<!-- Reader Appearance Settings Popover / Bottom Sheet -->
<div id="reader-settings-panel" class="reader-settings-panel hidden">
<div class="sheet-drag-handle mobile-only"></div>
<div class="reader-settings-header">
<h4>🔤 Настройки текста</h4>
<button id="reader-settings-close-btn" class="popup-close-btn">✕</button>
</div>
<div class="reader-settings-body">
<!-- Font Family -->
<div class="setting-row">
<span class="setting-label">Шрифт</span>
<div class="setting-segmented-control" id="reader-font-family-group">
<button class="seg-btn" data-font="sans" style="font-family: var(--font-family);">Sans</button>
<button class="seg-btn active" data-font="serif" style="font-family: Georgia, serif;">Serif</button>
<button class="seg-btn" data-font="mono" style="font-family: monospace;">Mono</button>
</div>
</div>
<!-- Font Size -->
<div class="setting-row">
<span class="setting-label">Размер шрифта</span>
<div class="setting-stepper">
<button id="setting-font-dec-btn" class="step-btn">A−</button>
<span id="setting-font-size-val" class="step-val">18 px</span>
<button id="setting-font-inc-btn" class="step-btn">A+</button>
</div>
</div>
<!-- Line Height -->
<div class="setting-row">
<span class="setting-label">Межстрочный интервал</span>
<div class="setting-segmented-control" id="reader-line-height-group">
<button class="seg-btn" data-lh="1.35">Компакт</button>
<button class="seg-btn active" data-lh="1.65">Оптимум</button>
<button class="seg-btn" data-lh="1.95">Широкий</button>
</div>
</div>
<!-- Margins / Width -->
<div class="setting-row">
<span class="setting-label">Ширина и поля страницы</span>
<div class="setting-segmented-control" id="reader-margins-group">
<button class="seg-btn" data-margin="narrow">Узкие</button>
<button class="seg-btn active" data-margin="normal">Стандарт</button>
<button class="seg-btn" data-margin="wide">Книга</button>
</div>
</div>
<!-- Reading Theme -->
<div class="setting-row">
<span class="setting-label">Цветовая тема читалки</span>
<div class="theme-palette" id="reader-theme-group">
<button class="theme-chip theme-chip-dark active" data-theme="dark" title="Тёмная (Dark Slate)">🌙 Тёмная</button>
<button class="theme-chip theme-chip-sepia" data-theme="sepia" title="Сепия (Тёплый пергамент)">📜 Сепия</button>
<button class="theme-chip theme-chip-light" data-theme="light" title="Светлая (Чистый лист)">☀️ Светлая</button>
<button class="theme-chip theme-chip-oled" data-theme="oled" title="OLED (Абсолютный чёрный)">🖤 OLED</button>
</div>
</div>
<!-- Text Alignment -->
<div class="setting-row">
<span class="setting-label">Выравнивание текста</span>
<div class="setting-segmented-control" id="reader-align-group">
<button class="seg-btn active" data-align="left">По левому краю</button>
<button class="seg-btn" data-align="justify">По ширине</button>
</div>
</div>
</div>
</div>
<!-- Interactive Click-to-Translate Popup (Mounted at body root to prevent scroll clipping) -->
<div id="reader-word-popup" class="reader-word-popup hidden">
<div class="sheet-drag-handle mobile-only"></div>
@@ -981,7 +1051,7 @@
<!-- Audio Element for native streaming -->
<audio id="global-audio-player" preload="none"></audio>
<script src="/static/js/app.js?v=3.1"></script>
<script src="/static/js/app.js?v=3.2"></script>
</body>
</html>
+202 -14
View File
@@ -102,8 +102,18 @@ document.addEventListener('DOMContentLoaded', () => {
const readerChapterSelect = document.getElementById('reader-chapter-select');
const readerPrevChBtn = document.getElementById('reader-prev-ch-btn');
const readerNextChBtn = document.getElementById('reader-next-ch-btn');
const readerFontDecBtn = document.getElementById('reader-font-dec-btn');
const readerFontIncBtn = document.getElementById('reader-font-inc-btn');
const readerSettingsBtn = document.getElementById('reader-settings-btn');
const readerSettingsPanel = document.getElementById('reader-settings-panel');
const readerSettingsCloseBtn = document.getElementById('reader-settings-close-btn');
const settingFontDecBtn = document.getElementById('setting-font-dec-btn');
const settingFontIncBtn = document.getElementById('setting-font-inc-btn');
const settingFontSizeVal = document.getElementById('setting-font-size-val');
const readerFontFamilyGroup = document.getElementById('reader-font-family-group');
const readerLineHeightGroup = document.getElementById('reader-line-height-group');
const readerMarginsGroup = document.getElementById('reader-margins-group');
const readerThemeGroup = document.getElementById('reader-theme-group');
const readerAlignGroup = document.getElementById('reader-align-group');
const readerToggleHighlightBtn = document.getElementById('reader-toggle-highlight-btn');
const readerTrainBtn = document.getElementById('reader-train-btn');
const readerContentBody = document.getElementById('reader-content-body');
@@ -123,10 +133,18 @@ document.addEventListener('DOMContentLoaded', () => {
const popupSavedBadge = document.getElementById('popup-saved-badge');
const popupAddContextBtn = document.getElementById('popup-add-context-btn');
// Reader State
// Reader State & Appearance Settings
const DEFAULT_READER_SETTINGS = {
fontSize: 18,
fontFamily: 'serif',
lineHeight: 1.65,
marginWidth: 'normal',
theme: 'dark',
textAlign: 'left'
};
let readerSettings = { ...DEFAULT_READER_SETTINGS };
let currentBook = null;
let currentChapterIndex = 0;
let readerFontSize = 19;
let isHighlightEnabled = false;
let popupCurrentData = null;
@@ -1989,6 +2007,7 @@ document.addEventListener('DOMContentLoaded', () => {
bookshelfContainer.classList.add('hidden');
readerInterface.classList.remove('hidden');
applyReaderSettings();
renderChapter(currentChapterIndex);
} catch (e) {
@@ -2826,24 +2845,193 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
if (readerFontIncBtn) {
readerFontIncBtn.addEventListener('click', () => {
if (readerFontSize < 28) {
readerFontSize += 2;
readerContentBody.style.fontSize = `${readerFontSize}px`;
function loadSavedReaderSettings() {
try {
const saved = localStorage.getItem('notes_reader_settings');
if (saved) {
readerSettings = Object.assign({}, DEFAULT_READER_SETTINGS, JSON.parse(saved));
}
} catch (e) {}
}
function saveReaderSettings() {
try {
localStorage.setItem('notes_reader_settings', JSON.stringify(readerSettings));
} catch (e) {}
}
function applyReaderSettings() {
if (!readerScrollArea) return;
// 1. Font size
readerScrollArea.style.setProperty('--reader-font-size', `${readerSettings.fontSize}px`);
if (settingFontSizeVal) settingFontSizeVal.textContent = `${readerSettings.fontSize} px`;
// 2. Line height
readerScrollArea.style.setProperty('--reader-line-height', readerSettings.lineHeight);
// 3. Theme
readerScrollArea.classList.remove('reader-theme-dark', 'reader-theme-sepia', 'reader-theme-light', 'reader-theme-oled');
readerScrollArea.classList.add(`reader-theme-${readerSettings.theme}`);
// 4. Font family
readerScrollArea.classList.remove('reader-font-sans', 'reader-font-serif', 'reader-font-mono');
readerScrollArea.classList.add(`reader-font-${readerSettings.fontFamily}`);
// 5. Margin width
readerScrollArea.classList.remove('reader-margins-narrow', 'reader-margins-normal', 'reader-margins-wide');
readerScrollArea.classList.add(`reader-margins-${readerSettings.marginWidth}`);
// 6. Text align
readerScrollArea.classList.remove('reader-align-left', 'reader-align-justify');
readerScrollArea.classList.add(`reader-align-${readerSettings.textAlign}`);
updateReaderSettingsUI();
}
function updateReaderSettingsUI() {
if (!readerSettingsPanel) return;
if (settingFontSizeVal) settingFontSizeVal.textContent = `${readerSettings.fontSize} px`;
if (readerFontFamilyGroup) {
readerFontFamilyGroup.querySelectorAll('.seg-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.font === readerSettings.fontFamily);
});
}
if (readerLineHeightGroup) {
readerLineHeightGroup.querySelectorAll('.seg-btn').forEach(btn => {
btn.classList.toggle('active', Math.abs(parseFloat(btn.dataset.lh) - parseFloat(readerSettings.lineHeight)) < 0.05);
});
}
if (readerMarginsGroup) {
readerMarginsGroup.querySelectorAll('.seg-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.margin === readerSettings.marginWidth);
});
}
if (readerThemeGroup) {
readerThemeGroup.querySelectorAll('.theme-chip').forEach(btn => {
btn.classList.toggle('active', btn.dataset.theme === readerSettings.theme);
});
}
if (readerAlignGroup) {
readerAlignGroup.querySelectorAll('.seg-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.align === readerSettings.textAlign);
});
}
}
function initReaderSettingsPanel() {
loadSavedReaderSettings();
applyReaderSettings();
if (readerSettingsBtn) {
readerSettingsBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (readerSettingsPanel) {
const isHidden = readerSettingsPanel.classList.contains('hidden');
readerSettingsPanel.classList.toggle('hidden', !isHidden);
if (isHidden) updateReaderSettingsUI();
}
});
}
if (readerFontDecBtn) {
readerFontDecBtn.addEventListener('click', () => {
if (readerFontSize > 14) {
readerFontSize -= 2;
readerContentBody.style.fontSize = `${readerFontSize}px`;
if (readerSettingsCloseBtn) {
readerSettingsCloseBtn.addEventListener('click', () => {
if (readerSettingsPanel) readerSettingsPanel.classList.add('hidden');
});
}
if (settingFontDecBtn) {
settingFontDecBtn.addEventListener('click', () => {
if (readerSettings.fontSize > 13) {
readerSettings.fontSize -= 1;
applyReaderSettings();
saveReaderSettings();
}
});
}
if (settingFontIncBtn) {
settingFontIncBtn.addEventListener('click', () => {
if (readerSettings.fontSize < 34) {
readerSettings.fontSize += 1;
applyReaderSettings();
saveReaderSettings();
}
});
}
if (readerFontFamilyGroup) {
readerFontFamilyGroup.addEventListener('click', (e) => {
const btn = e.target.closest('.seg-btn');
if (btn && btn.dataset.font) {
readerSettings.fontFamily = btn.dataset.font;
applyReaderSettings();
saveReaderSettings();
}
});
}
if (readerLineHeightGroup) {
readerLineHeightGroup.addEventListener('click', (e) => {
const btn = e.target.closest('.seg-btn');
if (btn && btn.dataset.lh) {
readerSettings.lineHeight = parseFloat(btn.dataset.lh);
applyReaderSettings();
saveReaderSettings();
}
});
}
if (readerMarginsGroup) {
readerMarginsGroup.addEventListener('click', (e) => {
const btn = e.target.closest('.seg-btn');
if (btn && btn.dataset.margin) {
readerSettings.marginWidth = btn.dataset.margin;
applyReaderSettings();
saveReaderSettings();
}
});
}
if (readerThemeGroup) {
readerThemeGroup.addEventListener('click', (e) => {
const btn = e.target.closest('.theme-chip');
if (btn && btn.dataset.theme) {
readerSettings.theme = btn.dataset.theme;
applyReaderSettings();
saveReaderSettings();
}
});
}
if (readerAlignGroup) {
readerAlignGroup.addEventListener('click', (e) => {
const btn = e.target.closest('.seg-btn');
if (btn && btn.dataset.align) {
readerSettings.textAlign = btn.dataset.align;
applyReaderSettings();
saveReaderSettings();
}
});
}
document.addEventListener('click', (e) => {
if (readerSettingsPanel && !readerSettingsPanel.classList.contains('hidden')) {
if (!readerSettingsPanel.contains(e.target) && e.target !== readerSettingsBtn && !readerSettingsBtn.contains(e.target)) {
readerSettingsPanel.classList.add('hidden');
}
}
});
}
initReaderSettingsPanel();
if (readerToggleHighlightBtn) {
readerToggleHighlightBtn.addEventListener('click', () => {
isHighlightEnabled = !isHighlightEnabled;