feat(reader): implement full appearance customization panel with themes, fonts, margins, line-height, and localStorage persistence
This commit is contained in:
1 parent
c0fdcd776b
commit
84e9be614e
4 files changed
+589
-37
No files matched your search
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
+205
-17
@@ -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,23 +2845,192 @@ 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 (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');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (readerFontDecBtn) {
|
||||
readerFontDecBtn.addEventListener('click', () => {
|
||||
if (readerFontSize > 14) {
|
||||
readerFontSize -= 2;
|
||||
readerContentBody.style.fontSize = `${readerFontSize}px`;
|
||||
}
|
||||
});
|
||||
}
|
||||
initReaderSettingsPanel();
|
||||
|
||||
if (readerToggleHighlightBtn) {
|
||||
readerToggleHighlightBtn.addEventListener('click', () => {
|
||||
|
||||
Reference in new issue
Block a user