feat(reader): implement FEAT-01 Focus Zen Mode (100% vh/vw, auto-hiding HUD, hotkeys F/Esc, native fullscreen)
This commit is contained in:
@@ -514,7 +514,7 @@
|
|||||||
|
|
||||||
## ⏳ Очередь новых функций (Feature Backlog)
|
## ⏳ Очередь новых функций (Feature Backlog)
|
||||||
|
|
||||||
- [ ] **FEAT-01: 🧘 Фокус-режим в читалке (Focus Mode / Zen Reader — 100% vh & 100% vw)**:
|
- [x] **FEAT-01: 🧘 Фокус-режим в читалке (Focus Mode / Zen Reader — 100% vh & 100% vw)**:
|
||||||
- **Концепция**: чистый дзен-режим чтения на весь экран без отвлекающих панелей с сохранением 100% интерактивного перевода.
|
- **Концепция**: чистый дзен-режим чтения на весь экран без отвлекающих панелей с сохранением 100% интерактивного перевода.
|
||||||
- **Интерфейс и верстка**:
|
- **Интерфейс и верстка**:
|
||||||
- Скрытие шапки сайта (`navbar`), тулбара читалки, мобильной панели вкладок (`bottom tab bar`) и рамок.
|
- Скрытие шапки сайта (`navbar`), тулбара читалки, мобильной панели вкладок (`bottom tab bar`) и рамок.
|
||||||
|
|||||||
@@ -331,6 +331,8 @@ def _migrate(conn: sqlite3.Connection):
|
|||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
subtitle TEXT DEFAULT '',
|
subtitle TEXT DEFAULT '',
|
||||||
golden_formula TEXT NOT NULL,
|
golden_formula TEXT NOT NULL,
|
||||||
|
cheatsheet_table TEXT DEFAULT '',
|
||||||
|
detailed_notes TEXT DEFAULT '',
|
||||||
contrast_explanation_json TEXT NOT NULL,
|
contrast_explanation_json TEXT NOT NULL,
|
||||||
journaling_prompt TEXT NOT NULL,
|
journaling_prompt TEXT NOT NULL,
|
||||||
exercises_json TEXT NOT NULL,
|
exercises_json TEXT NOT NULL,
|
||||||
@@ -340,6 +342,18 @@ def _migrate(conn: sqlite3.Connection):
|
|||||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_grammar_units_num ON grammar_units(unit_num);")
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_grammar_units_num ON grammar_units(unit_num);")
|
||||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_grammar_units_level ON grammar_units(level);")
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_grammar_units_level ON grammar_units(level);")
|
||||||
|
|
||||||
|
# Safe migration: add cheatsheet_table and detailed_notes columns if not present
|
||||||
|
existing_grammar_cols = {
|
||||||
|
row[1] for row in conn.execute("PRAGMA table_info(grammar_units)").fetchall()
|
||||||
|
}
|
||||||
|
grammar_migrations = [
|
||||||
|
("cheatsheet_table", "TEXT DEFAULT ''"),
|
||||||
|
("detailed_notes", "TEXT DEFAULT ''"),
|
||||||
|
]
|
||||||
|
for col, definition in grammar_migrations:
|
||||||
|
if col not in existing_grammar_cols:
|
||||||
|
conn.execute(f"ALTER TABLE grammar_units ADD COLUMN {col} {definition}")
|
||||||
|
|
||||||
conn.execute("""
|
conn.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS grammar_progress (
|
CREATE TABLE IF NOT EXISTS grammar_progress (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
|||||||
+48
-14
@@ -51,7 +51,7 @@ CURATED_MURPHY_UNITS = _LazyUnitsList()
|
|||||||
def seed_grammar_units(conn: sqlite3.Connection) -> int:
|
def seed_grammar_units(conn: sqlite3.Connection) -> int:
|
||||||
"""
|
"""
|
||||||
Populates grammar_units table with curated Murphy units.
|
Populates grammar_units table with curated Murphy units.
|
||||||
Safe and idempotent (INSERT OR IGNORE).
|
Safe, idempotent upsert (ON CONFLICT DO UPDATE).
|
||||||
"""
|
"""
|
||||||
inserted = 0
|
inserted = 0
|
||||||
for u in CURATED_MURPHY_UNITS:
|
for u in CURATED_MURPHY_UNITS:
|
||||||
@@ -59,10 +59,22 @@ def seed_grammar_units(conn: sqlite3.Connection) -> int:
|
|||||||
exercises_json = json.dumps(u["exercises"], ensure_ascii=False)
|
exercises_json = json.dumps(u["exercises"], ensure_ascii=False)
|
||||||
cursor = conn.execute(
|
cursor = conn.execute(
|
||||||
"""
|
"""
|
||||||
INSERT OR IGNORE INTO grammar_units (
|
INSERT INTO grammar_units (
|
||||||
unit_num, level, category, title, subtitle, golden_formula,
|
unit_num, level, category, title, subtitle, golden_formula,
|
||||||
|
cheatsheet_table, detailed_notes,
|
||||||
contrast_explanation_json, journaling_prompt, exercises_json
|
contrast_explanation_json, journaling_prompt, exercises_json
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(unit_num) DO UPDATE SET
|
||||||
|
level = excluded.level,
|
||||||
|
category = excluded.category,
|
||||||
|
title = excluded.title,
|
||||||
|
subtitle = excluded.subtitle,
|
||||||
|
golden_formula = excluded.golden_formula,
|
||||||
|
cheatsheet_table = excluded.cheatsheet_table,
|
||||||
|
detailed_notes = excluded.detailed_notes,
|
||||||
|
contrast_explanation_json = excluded.contrast_explanation_json,
|
||||||
|
journaling_prompt = excluded.journaling_prompt,
|
||||||
|
exercises_json = excluded.exercises_json
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
u["unit_num"],
|
u["unit_num"],
|
||||||
@@ -71,6 +83,8 @@ def seed_grammar_units(conn: sqlite3.Connection) -> int:
|
|||||||
u["title"],
|
u["title"],
|
||||||
u.get("subtitle", ""),
|
u.get("subtitle", ""),
|
||||||
u["golden_formula"],
|
u["golden_formula"],
|
||||||
|
u.get("cheatsheet_table", ""),
|
||||||
|
u.get("detailed_notes", ""),
|
||||||
contrast_json,
|
contrast_json,
|
||||||
u["journaling_prompt"],
|
u["journaling_prompt"],
|
||||||
exercises_json,
|
exercises_json,
|
||||||
@@ -117,18 +131,22 @@ def get_grammar_units_list(conn: sqlite3.Connection, user_id: int) -> List[Dict[
|
|||||||
def get_grammar_unit_detail(conn: sqlite3.Connection, user_id: int, unit_id: int) -> Optional[Dict[str, Any]]:
|
def get_grammar_unit_detail(conn: sqlite3.Connection, user_id: int, unit_id: int) -> Optional[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Returns full content of a unit for lesson mode.
|
Returns full content of a unit for lesson mode.
|
||||||
|
Supports looking up by database PK or unit_num.
|
||||||
"""
|
"""
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
u.id, u.unit_num, u.level, u.category, u.title, u.subtitle,
|
u.id, u.unit_num, u.level, u.category, u.title, u.subtitle,
|
||||||
u.golden_formula, u.contrast_explanation_json, u.journaling_prompt, u.exercises_json,
|
u.golden_formula, u.cheatsheet_table, u.detailed_notes,
|
||||||
|
u.contrast_explanation_json, u.journaling_prompt, u.exercises_json,
|
||||||
p.is_completed, p.score, p.handwritten_logged, p.completed_at
|
p.is_completed, p.score, p.handwritten_logged, p.completed_at
|
||||||
FROM grammar_units u
|
FROM grammar_units u
|
||||||
LEFT JOIN grammar_progress p ON u.id = p.unit_id AND p.user_id = ?
|
LEFT JOIN grammar_progress p ON u.id = p.unit_id AND p.user_id = ?
|
||||||
WHERE u.id = ?
|
WHERE u.id = ? OR u.unit_num = ?
|
||||||
|
ORDER BY (u.id = ?) DESC
|
||||||
|
LIMIT 1
|
||||||
""",
|
""",
|
||||||
(user_id, unit_id),
|
(user_id, unit_id, unit_id, unit_id),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
|
|
||||||
if not row:
|
if not row:
|
||||||
@@ -162,9 +180,12 @@ def get_grammar_unit_detail(conn: sqlite3.Connection, user_id: int, unit_id: int
|
|||||||
"level": row["level"],
|
"level": row["level"],
|
||||||
"category": row["category"],
|
"category": row["category"],
|
||||||
"title": row["title"],
|
"title": row["title"],
|
||||||
"subtitle": row["subtitle"],
|
"subtitle": row["subtitle"] or "",
|
||||||
"golden_formula": row["golden_formula"],
|
"golden_formula": row["golden_formula"],
|
||||||
|
"cheatsheet_table": row["cheatsheet_table"] or "",
|
||||||
|
"detailed_notes": row["detailed_notes"] or "",
|
||||||
"contrasts": contrasts,
|
"contrasts": contrasts,
|
||||||
|
"contrast_explanation": contrasts,
|
||||||
"journaling_prompt": row["journaling_prompt"],
|
"journaling_prompt": row["journaling_prompt"],
|
||||||
"exercises": exercises,
|
"exercises": exercises,
|
||||||
"is_completed": bool(row["is_completed"]),
|
"is_completed": bool(row["is_completed"]),
|
||||||
@@ -187,7 +208,8 @@ def process_grammar_answer(
|
|||||||
with tags 'grammar, murphy-unit-{unit_num}' and srs_state='learning'.
|
with tags 'grammar, murphy-unit-{unit_num}' and srs_state='learning'.
|
||||||
"""
|
"""
|
||||||
unit_row = conn.execute(
|
unit_row = conn.execute(
|
||||||
"SELECT * FROM grammar_units WHERE id = ?", (unit_id,)
|
"SELECT * FROM grammar_units WHERE id = ? OR unit_num = ? ORDER BY (id = ?) DESC LIMIT 1",
|
||||||
|
(unit_id, unit_id, unit_id),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
|
|
||||||
if not unit_row:
|
if not unit_row:
|
||||||
@@ -293,9 +315,15 @@ def record_grammar_completion(
|
|||||||
"""
|
"""
|
||||||
Saves unit completion score and handwritten journaling status.
|
Saves unit completion score and handwritten journaling status.
|
||||||
"""
|
"""
|
||||||
|
u_row = conn.execute(
|
||||||
|
"SELECT id FROM grammar_units WHERE id = ? OR unit_num = ? ORDER BY (id = ?) DESC LIMIT 1",
|
||||||
|
(unit_id, unit_id, unit_id),
|
||||||
|
).fetchone()
|
||||||
|
real_unit_id = u_row["id"] if u_row else unit_id
|
||||||
|
|
||||||
prog_row = conn.execute(
|
prog_row = conn.execute(
|
||||||
"SELECT id FROM grammar_progress WHERE user_id = ? AND unit_id = ?",
|
"SELECT id FROM grammar_progress WHERE user_id = ? AND unit_id = ?",
|
||||||
(user_id, unit_id),
|
(user_id, real_unit_id),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
|
|
||||||
hw_val = 1 if handwritten_logged else 0
|
hw_val = 1 if handwritten_logged else 0
|
||||||
@@ -320,10 +348,10 @@ def record_grammar_completion(
|
|||||||
user_id, unit_id, is_completed, score, handwritten_logged, handwritten_at, completed_at
|
user_id, unit_id, is_completed, score, handwritten_logged, handwritten_at, completed_at
|
||||||
) VALUES (?, ?, 1, ?, ?, CASE WHEN ? = 1 THEN CURRENT_TIMESTAMP ELSE NULL END, CURRENT_TIMESTAMP)
|
) VALUES (?, ?, 1, ?, ?, CASE WHEN ? = 1 THEN CURRENT_TIMESTAMP ELSE NULL END, CURRENT_TIMESTAMP)
|
||||||
""",
|
""",
|
||||||
(user_id, unit_id, score, hw_val, hw_val),
|
(user_id, real_unit_id, score, hw_val, hw_val),
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"status": "ok", "unit_id": unit_id, "score": score, "handwritten_logged": handwritten_logged}
|
return {"status": "ok", "unit_id": real_unit_id, "score": score, "handwritten_logged": handwritten_logged}
|
||||||
|
|
||||||
|
|
||||||
def update_handwritten_status(
|
def update_handwritten_status(
|
||||||
@@ -335,10 +363,16 @@ def update_handwritten_status(
|
|||||||
"""
|
"""
|
||||||
Toggles handwritten paper notebook checkbox for a unit.
|
Toggles handwritten paper notebook checkbox for a unit.
|
||||||
"""
|
"""
|
||||||
|
u_row = conn.execute(
|
||||||
|
"SELECT id FROM grammar_units WHERE id = ? OR unit_num = ? ORDER BY (id = ?) DESC LIMIT 1",
|
||||||
|
(unit_id, unit_id, unit_id),
|
||||||
|
).fetchone()
|
||||||
|
real_unit_id = u_row["id"] if u_row else unit_id
|
||||||
|
|
||||||
hw_val = 1 if handwritten else 0
|
hw_val = 1 if handwritten else 0
|
||||||
prog_row = conn.execute(
|
prog_row = conn.execute(
|
||||||
"SELECT id FROM grammar_progress WHERE user_id = ? AND unit_id = ?",
|
"SELECT id FROM grammar_progress WHERE user_id = ? AND unit_id = ?",
|
||||||
(user_id, unit_id),
|
(user_id, real_unit_id),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
|
|
||||||
if prog_row:
|
if prog_row:
|
||||||
@@ -358,7 +392,7 @@ def update_handwritten_status(
|
|||||||
user_id, unit_id, is_completed, score, handwritten_logged, handwritten_at
|
user_id, unit_id, is_completed, score, handwritten_logged, handwritten_at
|
||||||
) VALUES (?, ?, 0, 0, ?, CASE WHEN ? = 1 THEN CURRENT_TIMESTAMP ELSE NULL END)
|
) VALUES (?, ?, 0, 0, ?, CASE WHEN ? = 1 THEN CURRENT_TIMESTAMP ELSE NULL END)
|
||||||
""",
|
""",
|
||||||
(user_id, unit_id, hw_val, hw_val),
|
(user_id, real_unit_id, hw_val, hw_val),
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"status": "ok", "unit_id": unit_id, "handwritten_logged": handwritten}
|
return {"status": "ok", "unit_id": real_unit_id, "handwritten_logged": handwritten}
|
||||||
|
|||||||
@@ -1688,3 +1688,151 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
FEAT-02: Murphy Cheatsheet Tables & Rich Detailed Notes
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.grammar-table-wrap {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background: var(--bg-card);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
margin: 12px 0 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grammar-cheatsheet-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 13.5px;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grammar-cheatsheet-table thead th {
|
||||||
|
background: var(--bg-surface-hover);
|
||||||
|
color: var(--text-main);
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-bottom: 2px solid var(--border-color);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grammar-cheatsheet-table tbody td {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grammar-cheatsheet-table tbody tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grammar-cheatsheet-table tbody tr:hover {
|
||||||
|
background: rgba(99, 102, 241, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grammar-inline-code {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
font-size: 12.5px;
|
||||||
|
background: var(--bg-input);
|
||||||
|
color: var(--color-primary);
|
||||||
|
padding: 2px 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grammar-notes-box {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 20px 24px;
|
||||||
|
color: var(--text-main);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.65;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grammar-notes-box .grammar-note-h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--text-main);
|
||||||
|
margin: 16px 0 8px 0;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grammar-notes-box .grammar-note-h4 {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-primary);
|
||||||
|
margin: 16px 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grammar-notes-box .grammar-note-h5 {
|
||||||
|
font-size: 13.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin: 12px 0 6px 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grammar-notes-box .grammar-note-p {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grammar-notes-box .grammar-note-quote {
|
||||||
|
border-left: 3px solid var(--color-primary);
|
||||||
|
background: rgba(99, 102, 241, 0.06);
|
||||||
|
padding: 8px 14px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grammar-notes-list {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grammar-notes-list li {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collapsible Practice Cheatsheet Drawer */
|
||||||
|
.practice-cheatsheet-toggle-wrap {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.practice-cheatsheet-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.practice-cheatsheet-btn:hover {
|
||||||
|
color: var(--text-main);
|
||||||
|
background: var(--bg-surface-hover);
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.practice-cheatsheet-drawer {
|
||||||
|
margin-top: 10px;
|
||||||
|
animation: fadeIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+187
-2
@@ -2061,6 +2061,191 @@
|
|||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
line-height: 1.65;
|
line-height: 1.65;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
FEAT-01: Focus Mode in Reader (Zen Reader — 100% vh & 100% vw)
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.reader-zen-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-main);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-zen-btn:hover {
|
||||||
|
border-color: var(--primary);
|
||||||
|
color: var(--primary);
|
||||||
|
background-color: var(--primary-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fullscreen / Zen Viewport Override */
|
||||||
|
body.zen-mode-active {
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.zen-mode-active #app-header,
|
||||||
|
body.zen-mode-active #bottom-tab-bar,
|
||||||
|
body.zen-mode-active .reader-toolbar {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.zen-mode-active .reader-view {
|
||||||
|
max-width: 100% !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.zen-mode-active #reader-interface {
|
||||||
|
position: fixed !important;
|
||||||
|
inset: 0 !important;
|
||||||
|
width: 100vw !important;
|
||||||
|
height: 100dvh !important;
|
||||||
|
z-index: 9999 !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
background-color: var(--reader-bg) !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.zen-mode-active #reader-scroll-area {
|
||||||
|
width: 100vw !important;
|
||||||
|
height: 100dvh !important;
|
||||||
|
padding-top: max(24px, env(safe-area-inset-top)) !important;
|
||||||
|
padding-bottom: max(60px, env(safe-area-inset-bottom)) !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Floating Minimal HUD in Zen Mode */
|
||||||
|
.reader-zen-hud {
|
||||||
|
position: fixed;
|
||||||
|
top: max(16px, env(safe-area-inset-top));
|
||||||
|
right: max(20px, env(safe-area-inset-right));
|
||||||
|
z-index: 10002;
|
||||||
|
transition: opacity 0.35s cubic-bezier(0.16, 1, 0.3, 1), transform 0.35s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-zen-hud.hud-idle {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translateY(-8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-hud-content {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: rgba(15, 23, 42, 0.82);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
-webkit-backdrop-filter: blur(14px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
padding: 6px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #e2e8f0;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-hud-title {
|
||||||
|
max-width: 160px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-hud-sep {
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-hud-chapter {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-hud-progress {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #38bdf8;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-hud-exit-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
color: #cbd5e1;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
padding: 0;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-hud-exit-btn:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.35);
|
||||||
|
border-color: rgba(239, 68, 68, 0.6);
|
||||||
|
color: #fca5a5;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme-adaptive HUD styling for Sepia & Light themes */
|
||||||
|
.reader-scroll-area.reader-theme-sepia ~ #reader-zen-hud .zen-hud-content {
|
||||||
|
background: rgba(247, 237, 214, 0.88);
|
||||||
|
border-color: rgba(146, 64, 14, 0.25);
|
||||||
|
color: #453422;
|
||||||
|
box-shadow: 0 4px 20px rgba(70, 50, 30, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-scroll-area.reader-theme-sepia ~ #reader-zen-hud .zen-hud-title {
|
||||||
|
color: #291e14;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-scroll-area.reader-theme-sepia ~ #reader-zen-hud .zen-hud-progress {
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-scroll-area.reader-theme-sepia ~ #reader-zen-hud .zen-hud-exit-btn {
|
||||||
|
background: rgba(146, 64, 14, 0.12);
|
||||||
|
border-color: rgba(146, 64, 14, 0.2);
|
||||||
|
color: #453422;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-scroll-area.reader-theme-light ~ #reader-zen-hud .zen-hud-content {
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border-color: rgba(0, 0, 0, 0.12);
|
||||||
|
color: #1e293b;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-scroll-area.reader-theme-light ~ #reader-zen-hud .zen-hud-title {
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-scroll-area.reader-theme-light ~ #reader-zen-hud .zen-hud-progress {
|
||||||
|
color: #0284c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-scroll-area.reader-theme-light ~ #reader-zen-hud .zen-hud-exit-btn {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
border-color: rgba(0, 0, 0, 0.15);
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -768,6 +768,10 @@
|
|||||||
<span style="font-weight: 700; font-family: serif; font-size: 13px;">Aa</span>
|
<span style="font-weight: 700; font-family: serif; font-size: 13px;">Aa</span>
|
||||||
<span class="btn-subtext-inline">Шрифт</span>
|
<span class="btn-subtext-inline">Шрифт</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button id="reader-zen-btn" class="btn btn-xs btn-outline reader-zen-btn" title="Фокус-режим чтения (Zen Mode, клавиша F)">
|
||||||
|
<span>🧘</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-toggle-highlight-btn" class="btn btn-xs btn-outline" title="Вкл/Выкл подсветку изучаемых слов">✨ Подсветка</button>
|
||||||
<button id="reader-train-btn" class="btn btn-xs btn-trainer" title="Тренировать слова из этой книги">🎓 Слова книги</button>
|
<button id="reader-train-btn" class="btn btn-xs btn-trainer" title="Тренировать слова из этой книги">🎓 Слова книги</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -777,6 +781,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<!-- Floating Zen HUD (Discreet, auto-fading, fixed in top-right) -->
|
||||||
|
<div id="reader-zen-hud" class="reader-zen-hud hidden">
|
||||||
|
<div class="zen-hud-content">
|
||||||
|
<span id="zen-hud-title" class="zen-hud-title">Книга</span>
|
||||||
|
<span class="zen-hud-sep">•</span>
|
||||||
|
<span id="zen-hud-chapter" class="zen-hud-chapter">Глава 1</span>
|
||||||
|
<span class="zen-hud-sep">•</span>
|
||||||
|
<span id="zen-hud-progress" class="zen-hud-progress">0%</span>
|
||||||
|
<button type="button" id="zen-hud-exit-btn" class="zen-hud-exit-btn" title="Выйти из фокус-режима (Esc или F)">✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Reading Container -->
|
<!-- Reading Container -->
|
||||||
<div class="reader-scroll-area" id="reader-scroll-area">
|
<div class="reader-scroll-area" id="reader-scroll-area">
|
||||||
<div class="reader-content-body" id="reader-content-body">
|
<div class="reader-content-body" id="reader-content-body">
|
||||||
@@ -946,6 +962,18 @@
|
|||||||
<p id="grammar-lesson-sub" class="lesson-sub-title">Unit Subtitle</p>
|
<p id="grammar-lesson-sub" class="lesson-sub-title">Unit Subtitle</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Cheatsheet Markdown Table -->
|
||||||
|
<div id="grammar-cheatsheet-section" class="grammar-section-block">
|
||||||
|
<h3 class="section-title"><span class="title-icon">📊</span> Наглядная шпаргалка и правила (Таблица)</h3>
|
||||||
|
<div id="grammar-cheatsheet-content"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Detailed Murphy Notes -->
|
||||||
|
<div id="grammar-detailed-notes-section" class="grammar-section-block">
|
||||||
|
<h3 class="section-title"><span class="title-icon">📖</span> Подробный конспект темы (Raymond Murphy)</h3>
|
||||||
|
<div id="grammar-detailed-notes-content"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Contrast Explanation Cards -->
|
<!-- Contrast Explanation Cards -->
|
||||||
<div class="grammar-section-block">
|
<div class="grammar-section-block">
|
||||||
<h3 class="section-title"><span class="title-icon">⚖️</span> Метод контрастов (В чём суть и когда применять)</h3>
|
<h3 class="section-title"><span class="title-icon">⚖️</span> Метод контрастов (В чём суть и когда применять)</h3>
|
||||||
@@ -999,6 +1027,15 @@
|
|||||||
<div id="grammar-practice-dots" class="practice-dots"></div>
|
<div id="grammar-practice-dots" class="practice-dots"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Practice Mode Cheatsheet Toggle & Drawer -->
|
||||||
|
<div class="practice-cheatsheet-toggle-wrap">
|
||||||
|
<button type="button" id="grammar-practice-cheatsheet-toggle" class="btn btn-sm btn-ghost practice-cheatsheet-btn">
|
||||||
|
<span>📋 Шпаргалка по теме</span>
|
||||||
|
<i class="ph-caret-down"></i>
|
||||||
|
</button>
|
||||||
|
<div id="grammar-practice-cheatsheet-drawer" class="practice-cheatsheet-drawer hidden"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grammar-cloze-box">
|
<div class="grammar-cloze-box">
|
||||||
<div id="grammar-cloze-sentence" class="cloze-sentence-display"></div>
|
<div id="grammar-cloze-sentence" class="cloze-sentence-display"></div>
|
||||||
<div id="grammar-cloze-clue" class="cloze-clue-display"></div>
|
<div id="grammar-cloze-clue" class="cloze-clue-display"></div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
// =======================================================================
|
// =======================================================================
|
||||||
|
|
||||||
import { api } from './api.js';
|
import { api } from './api.js';
|
||||||
import { showToast, escapeHtml } from './ui.js';
|
import { showToast, escapeHtml, renderMarkdownTable, renderMarkdownNotes } from './ui.js';
|
||||||
|
|
||||||
let allUnits = [];
|
let allUnits = [];
|
||||||
let currentFilter = 'all';
|
let currentFilter = 'all';
|
||||||
@@ -29,6 +29,18 @@ export function initGrammar() {
|
|||||||
const sumUnitsBtn = document.getElementById('grammar-sum-units-btn');
|
const sumUnitsBtn = document.getElementById('grammar-sum-units-btn');
|
||||||
const sumSrsBtn = document.getElementById('grammar-sum-srs-btn');
|
const sumSrsBtn = document.getElementById('grammar-sum-srs-btn');
|
||||||
const handwrittenToggle = document.getElementById('grammar-handwritten-toggle');
|
const handwrittenToggle = document.getElementById('grammar-handwritten-toggle');
|
||||||
|
const cheatsheetToggle = document.getElementById('grammar-practice-cheatsheet-toggle');
|
||||||
|
const cheatsheetDrawer = document.getElementById('grammar-practice-cheatsheet-drawer');
|
||||||
|
|
||||||
|
if (cheatsheetToggle && cheatsheetDrawer) {
|
||||||
|
cheatsheetToggle.addEventListener('click', () => {
|
||||||
|
const isNowHidden = !cheatsheetDrawer.classList.contains('hidden');
|
||||||
|
cheatsheetDrawer.classList.toggle('hidden', isNowHidden);
|
||||||
|
cheatsheetToggle.innerHTML = isNowHidden
|
||||||
|
? '<span>📋 Шпаргалка по теме</span><i class="ph-caret-down"></i>'
|
||||||
|
: '<span>📋 Скрыть шпаргалку</span><i class="ph-caret-up"></i>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', () => {
|
backBtn.addEventListener('click', () => {
|
||||||
@@ -314,6 +326,50 @@ function renderUnitTheory() {
|
|||||||
handwrittenToggle.checked = Boolean(currentUnit.handwritten_logged);
|
handwrittenToggle.checked = Boolean(currentUnit.handwritten_logged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cheatsheet Markdown Table
|
||||||
|
const cheatsheetSec = document.getElementById('grammar-cheatsheet-section');
|
||||||
|
const cheatsheetBox = document.getElementById('grammar-cheatsheet-content');
|
||||||
|
if (cheatsheetSec && cheatsheetBox) {
|
||||||
|
if (currentUnit.cheatsheet_table && currentUnit.cheatsheet_table.trim()) {
|
||||||
|
cheatsheetBox.innerHTML = renderMarkdownTable(currentUnit.cheatsheet_table);
|
||||||
|
cheatsheetSec.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
cheatsheetBox.innerHTML = '';
|
||||||
|
cheatsheetSec.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detailed Murphy Notes
|
||||||
|
const notesSec = document.getElementById('grammar-detailed-notes-section');
|
||||||
|
const notesBox = document.getElementById('grammar-detailed-notes-content');
|
||||||
|
if (notesSec && notesBox) {
|
||||||
|
if (currentUnit.detailed_notes && currentUnit.detailed_notes.trim()) {
|
||||||
|
notesBox.innerHTML = renderMarkdownNotes(currentUnit.detailed_notes);
|
||||||
|
notesSec.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
notesBox.innerHTML = '';
|
||||||
|
notesSec.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Practice Cheatsheet Drawer content
|
||||||
|
const practiceDrawer = document.getElementById('grammar-practice-cheatsheet-drawer');
|
||||||
|
const practiceToggleWrap = document.querySelector('.practice-cheatsheet-toggle-wrap');
|
||||||
|
if (practiceDrawer && practiceToggleWrap) {
|
||||||
|
if (currentUnit.cheatsheet_table && currentUnit.cheatsheet_table.trim()) {
|
||||||
|
practiceDrawer.innerHTML = renderMarkdownTable(currentUnit.cheatsheet_table);
|
||||||
|
practiceToggleWrap.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
practiceDrawer.innerHTML = '';
|
||||||
|
practiceToggleWrap.classList.add('hidden');
|
||||||
|
}
|
||||||
|
practiceDrawer.classList.add('hidden');
|
||||||
|
const practiceToggle = document.getElementById('grammar-practice-cheatsheet-toggle');
|
||||||
|
if (practiceToggle) {
|
||||||
|
practiceToggle.innerHTML = '<span>📋 Шпаргалка по теме</span><i class="ph-caret-down"></i>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Golden formula
|
// Golden formula
|
||||||
const formulaEl = document.getElementById('grammar-golden-formula-text');
|
const formulaEl = document.getElementById('grammar-golden-formula-text');
|
||||||
if (formulaEl) {
|
if (formulaEl) {
|
||||||
@@ -368,6 +424,12 @@ function startUnitPractice() {
|
|||||||
sessionCorrect = 0;
|
sessionCorrect = 0;
|
||||||
sessionIngestedCards = [];
|
sessionIngestedCards = [];
|
||||||
|
|
||||||
|
// Ensure cheatsheet drawer is collapsed at practice start
|
||||||
|
const practiceDrawer = document.getElementById('grammar-practice-cheatsheet-drawer');
|
||||||
|
const practiceToggle = document.getElementById('grammar-practice-cheatsheet-toggle');
|
||||||
|
if (practiceDrawer) practiceDrawer.classList.add('hidden');
|
||||||
|
if (practiceToggle) practiceToggle.innerHTML = '<span>📋 Шпаргалка по теме</span><i class="ph-caret-down"></i>';
|
||||||
|
|
||||||
renderExercise();
|
renderExercise();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+103
-20
@@ -4,7 +4,7 @@
|
|||||||
// =======================================================================
|
// =======================================================================
|
||||||
|
|
||||||
import { api } from './api.js';
|
import { api } from './api.js';
|
||||||
import { ui, escapeHtml, showToast, switchView, shuffleArray, escapeRegex, playAudio, playTextSpeech, cleanPromptTranslation } from './ui.js';
|
import { ui, escapeHtml, showToast, switchView, shuffleArray, escapeRegex, playAudio, playTextSpeech, cleanPromptTranslation, renderMarkdownTable, renderMarkdownNotes } from './ui.js';
|
||||||
|
|
||||||
let presets = [];
|
let presets = [];
|
||||||
let currentPreset = null;
|
let currentPreset = null;
|
||||||
@@ -1332,6 +1332,18 @@ async function runGrammarReviewStep(step, container) {
|
|||||||
<span class="badge-formula">${escapeHtml(unitDetail.golden_formula || '')}</span>
|
<span class="badge-formula">${escapeHtml(unitDetail.golden_formula || '')}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
${unitDetail.cheatsheet_table ? `
|
||||||
|
<div class="practice-cheatsheet-toggle-wrap mt-2">
|
||||||
|
<button type="button" id="review-cheatsheet-toggle-btn" class="btn btn-sm btn-ghost practice-cheatsheet-btn">
|
||||||
|
<span>📋 Шпаргалка по теме</span>
|
||||||
|
<i class="ph-caret-down"></i>
|
||||||
|
</button>
|
||||||
|
<div id="review-cheatsheet-drawer" class="practice-cheatsheet-drawer hidden">
|
||||||
|
${renderMarkdownTable(unitDetail.cheatsheet_table)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
<div class="grammar-question-box mt-3">
|
<div class="grammar-question-box mt-3">
|
||||||
<p class="grammar-sentence-prompt">${escapeHtml(ex.sentence)}</p>
|
<p class="grammar-sentence-prompt">${escapeHtml(ex.sentence)}</p>
|
||||||
${ex.clue ? `<div class="clue-tag">💡 Подсказка: ${escapeHtml(ex.clue)}</div>` : ''}
|
${ex.clue ? `<div class="clue-tag">💡 Подсказка: ${escapeHtml(ex.clue)}</div>` : ''}
|
||||||
@@ -1350,6 +1362,18 @@ async function runGrammarReviewStep(step, container) {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const reviewCsBtn = document.getElementById('review-cheatsheet-toggle-btn');
|
||||||
|
const reviewCsDrawer = document.getElementById('review-cheatsheet-drawer');
|
||||||
|
if (reviewCsBtn && reviewCsDrawer) {
|
||||||
|
reviewCsBtn.addEventListener('click', () => {
|
||||||
|
const isHidden = reviewCsDrawer.classList.contains('hidden');
|
||||||
|
reviewCsDrawer.classList.toggle('hidden', !isHidden);
|
||||||
|
reviewCsBtn.innerHTML = !isHidden
|
||||||
|
? '<span>📋 Шпаргалка по теме</span><i class="ph-caret-down"></i>'
|
||||||
|
: '<span>📋 Скрыть шпаргалку</span><i class="ph-caret-up"></i>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
container.querySelectorAll('.grammar-opt-btn').forEach(btn => {
|
container.querySelectorAll('.grammar-opt-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
// Disable all options
|
// Disable all options
|
||||||
@@ -1432,7 +1456,11 @@ async function runGrammarNewStep(step, container) {
|
|||||||
let stage = 'theory'; // theory -> exercises
|
let stage = 'theory'; // theory -> exercises
|
||||||
|
|
||||||
function renderTheory() {
|
function renderTheory() {
|
||||||
const contrasts = unitDetail.contrast_explanation || [];
|
const contrasts = unitDetail.contrasts || unitDetail.contrast_explanation || [];
|
||||||
|
const hasCheatsheet = Boolean(unitDetail.cheatsheet_table && unitDetail.cheatsheet_table.trim());
|
||||||
|
const hasNotes = Boolean(unitDetail.detailed_notes && unitDetail.detailed_notes.trim());
|
||||||
|
const hasPrompt = Boolean(unitDetail.journaling_prompt && unitDetail.journaling_prompt.trim());
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="step-card-box">
|
<div class="step-card-box">
|
||||||
<div class="step-mini-header">
|
<div class="step-mini-header">
|
||||||
@@ -1449,29 +1477,60 @@ async function runGrammarNewStep(step, container) {
|
|||||||
<pre class="formula-code">${escapeHtml(unitDetail.golden_formula)}</pre>
|
<pre class="formula-code">${escapeHtml(unitDetail.golden_formula)}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Contrast Situations -->
|
<!-- Cheatsheet Markdown Table -->
|
||||||
${contrasts.map(c => `
|
${hasCheatsheet ? `
|
||||||
<div class="contrast-card mt-3">
|
<div class="lesson-grammar-section mt-4">
|
||||||
<div class="contrast-row">
|
<div class="section-title"><span class="title-icon">📊</span> Наглядная шпаргалка и правила (Таблица):</div>
|
||||||
<div class="contrast-col a">
|
${renderMarkdownTable(unitDetail.cheatsheet_table)}
|
||||||
<div class="col-title">${escapeHtml(c.situation_a)}</div>
|
|
||||||
<div class="col-example">${escapeHtml(c.example_a)}</div>
|
|
||||||
<div class="col-trans">${escapeHtml(c.trans_a)}</div>
|
|
||||||
</div>
|
|
||||||
<div class="contrast-col b">
|
|
||||||
<div class="col-title">${escapeHtml(c.situation_b)}</div>
|
|
||||||
<div class="col-example">${escapeHtml(c.example_b)}</div>
|
|
||||||
<div class="col-trans">${escapeHtml(c.trans_b)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="contrast-diff mt-2">💡 <strong>Ключевая разница:</strong> ${escapeHtml(c.key_difference)}</div>
|
|
||||||
</div>
|
</div>
|
||||||
`).join('')}
|
` : ''}
|
||||||
|
|
||||||
|
<!-- Detailed Notes -->
|
||||||
|
${hasNotes ? `
|
||||||
|
<div class="lesson-grammar-section mt-4">
|
||||||
|
<div class="section-title"><span class="title-icon">📖</span> Подробный конспект темы (Raymond Murphy):</div>
|
||||||
|
${renderMarkdownNotes(unitDetail.detailed_notes)}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<!-- Contrast Situations -->
|
||||||
|
${contrasts.length > 0 ? `
|
||||||
|
<div class="lesson-grammar-section mt-4">
|
||||||
|
<div class="section-title"><span class="title-icon">⚖️</span> Метод контрастов (В чём суть и когда применять):</div>
|
||||||
|
${contrasts.map(c => `
|
||||||
|
<div class="contrast-card mt-2">
|
||||||
|
<div class="contrast-row">
|
||||||
|
<div class="contrast-col a">
|
||||||
|
<div class="col-title">${escapeHtml(c.situation_a)}</div>
|
||||||
|
<div class="col-example">${escapeHtml(c.example_a)}</div>
|
||||||
|
<div class="col-trans">${escapeHtml(c.trans_a)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="contrast-col b">
|
||||||
|
<div class="col-title">${escapeHtml(c.situation_b)}</div>
|
||||||
|
<div class="col-example">${escapeHtml(c.example_b)}</div>
|
||||||
|
<div class="col-trans">${escapeHtml(c.trans_b)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="contrast-diff mt-2">💡 <strong>Ключевая разница:</strong> ${escapeHtml(c.key_difference)}</div>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<!-- Handwritten Journaling Prompt -->
|
||||||
|
${hasPrompt ? `
|
||||||
|
<div class="grammar-journaling-card mt-4">
|
||||||
|
<div class="journaling-prompt-box">
|
||||||
|
<div class="formula-label">📓 Персональный дневник от руки (Motor Memory):</div>
|
||||||
|
<p class="journaling-prompt-text mt-1">${escapeHtml(unitDetail.journaling_prompt)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
<!-- Handwritten Journaling Checkbox -->
|
<!-- Handwritten Journaling Checkbox -->
|
||||||
<div class="journaling-box mt-4">
|
<div class="journaling-box mt-4">
|
||||||
<label class="checkbox-container">
|
<label class="checkbox-container">
|
||||||
<input type="checkbox" id="hw-checkbox">
|
<input type="checkbox" id="hw-checkbox" ${unitDetail.handwritten_logged ? 'checked' : ''}>
|
||||||
<span class="checkmark"></span>
|
<span class="checkmark"></span>
|
||||||
<span class="hw-text">✍️ Я записал формулу и пример в бумажную тетрадь</span>
|
<span class="hw-text">✍️ Я записал формулу и пример в бумажную тетрадь</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -1541,6 +1600,18 @@ async function runGrammarNewStep(step, container) {
|
|||||||
<span class="badge">Практика темы (${exIdx + 1} из ${selectedExercises.length})</span>
|
<span class="badge">Практика темы (${exIdx + 1} из ${selectedExercises.length})</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
${unitDetail.cheatsheet_table ? `
|
||||||
|
<div class="practice-cheatsheet-toggle-wrap mt-2">
|
||||||
|
<button type="button" id="lesson-cheatsheet-toggle-btn" class="btn btn-sm btn-ghost practice-cheatsheet-btn">
|
||||||
|
<span>📋 Шпаргалка по теме</span>
|
||||||
|
<i class="ph-caret-down"></i>
|
||||||
|
</button>
|
||||||
|
<div id="lesson-cheatsheet-drawer" class="practice-cheatsheet-drawer hidden">
|
||||||
|
${renderMarkdownTable(unitDetail.cheatsheet_table)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
<div class="grammar-question-box mt-3">
|
<div class="grammar-question-box mt-3">
|
||||||
<p class="grammar-sentence-prompt">${escapeHtml(ex.sentence)}</p>
|
<p class="grammar-sentence-prompt">${escapeHtml(ex.sentence)}</p>
|
||||||
${ex.clue ? `<div class="clue-tag">💡 Подсказка: ${escapeHtml(ex.clue)}</div>` : ''}
|
${ex.clue ? `<div class="clue-tag">💡 Подсказка: ${escapeHtml(ex.clue)}</div>` : ''}
|
||||||
@@ -1559,6 +1630,18 @@ async function runGrammarNewStep(step, container) {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const lessonCsBtn = document.getElementById('lesson-cheatsheet-toggle-btn');
|
||||||
|
const lessonCsDrawer = document.getElementById('lesson-cheatsheet-drawer');
|
||||||
|
if (lessonCsBtn && lessonCsDrawer) {
|
||||||
|
lessonCsBtn.addEventListener('click', () => {
|
||||||
|
const isHidden = lessonCsDrawer.classList.contains('hidden');
|
||||||
|
lessonCsDrawer.classList.toggle('hidden', !isHidden);
|
||||||
|
lessonCsBtn.innerHTML = !isHidden
|
||||||
|
? '<span>📋 Шпаргалка по теме</span><i class="ph-caret-down"></i>'
|
||||||
|
: '<span>📋 Скрыть шпаргалку</span><i class="ph-caret-up"></i>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
container.querySelectorAll('.grammar-opt-btn').forEach(btn => {
|
container.querySelectorAll('.grammar-opt-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
container.querySelectorAll('.grammar-opt-btn').forEach(b => b.disabled = true);
|
container.querySelectorAll('.grammar-opt-btn').forEach(b => b.disabled = true);
|
||||||
|
|||||||
@@ -28,6 +28,13 @@ let popupCurrentData = null;
|
|||||||
let readerScrollSaveTimer = null;
|
let readerScrollSaveTimer = null;
|
||||||
let popupScrollRaf = null;
|
let popupScrollRaf = null;
|
||||||
|
|
||||||
|
let isZenMode = false;
|
||||||
|
let zenHudIdleTimer = null;
|
||||||
|
|
||||||
|
export function getIsZenMode() {
|
||||||
|
return isZenMode;
|
||||||
|
}
|
||||||
|
|
||||||
export function getCurrentBook() {
|
export function getCurrentBook() {
|
||||||
return currentBook;
|
return currentBook;
|
||||||
}
|
}
|
||||||
@@ -134,6 +141,7 @@ function createBookCard(b) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function loadBookshelf() {
|
export async function loadBookshelf() {
|
||||||
|
if (isZenMode) toggleZenMode(false);
|
||||||
const bookshelfContainer = document.getElementById('bookshelf-container');
|
const bookshelfContainer = document.getElementById('bookshelf-container');
|
||||||
const readerInterface = document.getElementById('reader-interface');
|
const readerInterface = document.getElementById('reader-interface');
|
||||||
const bookshelfGrid = document.getElementById('bookshelf-grid');
|
const bookshelfGrid = document.getElementById('bookshelf-grid');
|
||||||
@@ -1610,6 +1618,26 @@ export function updateReaderProgressBar() {
|
|||||||
} else if (readerScrollPctBadge) {
|
} else if (readerScrollPctBadge) {
|
||||||
readerScrollPctBadge.textContent = `${pct}%`;
|
readerScrollPctBadge.textContent = `${pct}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update Zen HUD elements if present
|
||||||
|
const zenHudTitle = document.getElementById('zen-hud-title');
|
||||||
|
const zenHudChapter = document.getElementById('zen-hud-chapter');
|
||||||
|
const zenHudProgress = document.getElementById('zen-hud-progress');
|
||||||
|
|
||||||
|
if (zenHudTitle && currentBook) {
|
||||||
|
zenHudTitle.textContent = currentBook.title || 'Книга';
|
||||||
|
zenHudTitle.title = currentBook.title || 'Книга';
|
||||||
|
}
|
||||||
|
if (zenHudChapter && currentBook && currentBook.chapters) {
|
||||||
|
const totalCh = currentBook.chapters.length;
|
||||||
|
const ch = currentBook.chapters[currentChapterIndex];
|
||||||
|
const chTitle = ch ? (ch.title || `Глава ${currentChapterIndex + 1}`) : `Глава ${currentChapterIndex + 1}`;
|
||||||
|
zenHudChapter.textContent = totalCh > 1 ? `Гл. ${currentChapterIndex + 1}/${totalCh}` : chTitle;
|
||||||
|
zenHudChapter.title = chTitle;
|
||||||
|
}
|
||||||
|
if (zenHudProgress) {
|
||||||
|
zenHudProgress.textContent = `${pct}%`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleReaderScroll() {
|
export function handleReaderScroll() {
|
||||||
@@ -1836,6 +1864,62 @@ export function positionSettingsPanel() {
|
|||||||
readerSettingsPanel.style.maxHeight = `${maxH}px`;
|
readerSettingsPanel.style.maxHeight = `${maxH}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Zen Focus Mode
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
export function wakeZenHud() {
|
||||||
|
if (!isZenMode) return;
|
||||||
|
const hud = document.getElementById('reader-zen-hud');
|
||||||
|
if (!hud) return;
|
||||||
|
hud.classList.remove('hud-idle');
|
||||||
|
if (zenHudIdleTimer) {
|
||||||
|
clearTimeout(zenHudIdleTimer);
|
||||||
|
}
|
||||||
|
zenHudIdleTimer = setTimeout(() => {
|
||||||
|
if (isZenMode && hud) {
|
||||||
|
hud.classList.add('hud-idle');
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleZenMode(forceState = null) {
|
||||||
|
const newState = (forceState !== null) ? Boolean(forceState) : !isZenMode;
|
||||||
|
if (isZenMode === newState) return;
|
||||||
|
isZenMode = newState;
|
||||||
|
|
||||||
|
const hud = document.getElementById('reader-zen-hud');
|
||||||
|
const zenBtn = document.getElementById('reader-zen-btn');
|
||||||
|
const readerSettingsPanel = document.getElementById('reader-settings-panel');
|
||||||
|
|
||||||
|
if (isZenMode) {
|
||||||
|
document.body.classList.add('zen-mode-active');
|
||||||
|
if (zenBtn) zenBtn.classList.add('active');
|
||||||
|
if (readerSettingsPanel) readerSettingsPanel.classList.add('hidden');
|
||||||
|
if (hud) {
|
||||||
|
hud.classList.remove('hidden');
|
||||||
|
hud.classList.remove('hud-idle');
|
||||||
|
wakeZenHud();
|
||||||
|
}
|
||||||
|
if (!document.fullscreenElement && document.documentElement.requestFullscreen) {
|
||||||
|
document.documentElement.requestFullscreen().catch(() => {});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('zen-mode-active');
|
||||||
|
if (zenBtn) zenBtn.classList.remove('active');
|
||||||
|
if (hud) {
|
||||||
|
hud.classList.add('hidden');
|
||||||
|
hud.classList.remove('hud-idle');
|
||||||
|
}
|
||||||
|
if (zenHudIdleTimer) {
|
||||||
|
clearTimeout(zenHudIdleTimer);
|
||||||
|
zenHudIdleTimer = null;
|
||||||
|
}
|
||||||
|
if (document.fullscreenElement && document.exitFullscreen) {
|
||||||
|
document.exitFullscreen().catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Reader Module Initialization
|
// Reader Module Initialization
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
@@ -1865,6 +1949,62 @@ export function initReader() {
|
|||||||
const readerBackBtn = document.getElementById('reader-back-btn');
|
const readerBackBtn = document.getElementById('reader-back-btn');
|
||||||
const readerToggleHighlightBtn = document.getElementById('reader-toggle-highlight-btn');
|
const readerToggleHighlightBtn = document.getElementById('reader-toggle-highlight-btn');
|
||||||
const readerTrainBtn = document.getElementById('reader-train-btn');
|
const readerTrainBtn = document.getElementById('reader-train-btn');
|
||||||
|
const readerZenBtn = document.getElementById('reader-zen-btn');
|
||||||
|
const zenHudExitBtn = document.getElementById('zen-hud-exit-btn');
|
||||||
|
|
||||||
|
if (readerZenBtn) {
|
||||||
|
readerZenBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleZenMode();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zenHudExitBtn) {
|
||||||
|
zenHudExitBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleZenMode(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wake HUD on user activity during zen mode
|
||||||
|
['mousemove', 'touchstart', 'scroll'].forEach(evt => {
|
||||||
|
window.addEventListener(evt, () => {
|
||||||
|
if (isZenMode) wakeZenHud();
|
||||||
|
}, { passive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keyboard shortcut for Zen Mode (F / f / а / А) and exit on Escape
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
const tag = (e.target && e.target.tagName) ? e.target.tagName.toLowerCase() : '';
|
||||||
|
if (tag === 'input' || tag === 'textarea' || (e.target && e.target.isContentEditable)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const readerView = document.getElementById('reader-view');
|
||||||
|
const readerInterface = document.getElementById('reader-interface');
|
||||||
|
const isReaderActive = readerView && !readerView.classList.contains('hidden') &&
|
||||||
|
readerInterface && !readerInterface.classList.contains('hidden');
|
||||||
|
|
||||||
|
if (!isReaderActive) return;
|
||||||
|
|
||||||
|
if (e.key === 'Escape' && isZenMode) {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleZenMode(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((e.key === 'f' || e.key === 'F' || e.key === 'а' || e.key === 'А') && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleZenMode();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Native fullscreen sync
|
||||||
|
document.addEventListener('fullscreenchange', () => {
|
||||||
|
if (!document.fullscreenElement && isZenMode) {
|
||||||
|
toggleZenMode(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const popupCloseBtn = document.getElementById('popup-close-btn');
|
const popupCloseBtn = document.getElementById('popup-close-btn');
|
||||||
const popupSaveBtn = document.getElementById('popup-save-btn');
|
const popupSaveBtn = document.getElementById('popup-save-btn');
|
||||||
@@ -2450,6 +2590,7 @@ export function initReader() {
|
|||||||
|
|
||||||
if (readerBackBtn) {
|
if (readerBackBtn) {
|
||||||
readerBackBtn.addEventListener('click', () => {
|
readerBackBtn.addEventListener('click', () => {
|
||||||
|
if (isZenMode) toggleZenMode(false);
|
||||||
window.location.hash = '#reader';
|
window.location.hash = '#reader';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,111 @@ export function formatTranslationWithSpoiler(text) {
|
|||||||
return `${escapeHtml(cleanText)} <span class="grammar-hint-badge" title="Нажмите, чтобы показать форму" onclick="this.classList.toggle('active')"><span class="badge-label">ℹ️ форма</span><span class="badge-body">${escapeHtml(noteText)}</span></span>`;
|
return `${escapeHtml(cleanText)} <span class="grammar-hint-badge" title="Нажмите, чтобы показать форму" onclick="this.classList.toggle('active')"><span class="badge-label">ℹ️ форма</span><span class="badge-body">${escapeHtml(noteText)}</span></span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Lightweight Native Markdown Parsers (Tables & Notes for Murphy Grammar)
|
||||||
|
// Zero-dependency, safe escaping first, pure ES6
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
export function renderMarkdownInline(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
let str = escapeHtml(text);
|
||||||
|
// Bold: **text**
|
||||||
|
str = str.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
||||||
|
// Inline code: `text`
|
||||||
|
str = str.replace(/`([^`]+)`/g, '<code class="grammar-inline-code">$1</code>');
|
||||||
|
// Italic: *text* (excluding already formatted strong)
|
||||||
|
str = str.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<em>$1</em>');
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderMarkdownTable(mdText) {
|
||||||
|
if (!mdText || typeof mdText !== 'string' || !mdText.trim()) return '';
|
||||||
|
const rawLines = mdText.trim().split(/\r?\n/).map(l => l.trim()).filter(l => l.length > 0);
|
||||||
|
const tableLines = rawLines.filter(l => l.startsWith('|') && l.endsWith('|'));
|
||||||
|
if (tableLines.length < 2) {
|
||||||
|
return `<div class="grammar-table-raw"><pre>${escapeHtml(mdText)}</pre></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const splitRow = (rowStr) => {
|
||||||
|
const inner = rowStr.replace(/^\|/, '').replace(/\|$/, '');
|
||||||
|
return inner.split(/(?<!\\)\|/).map(c => c.trim().replace(/\\\|/g, '|'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const headers = splitRow(tableLines[0]);
|
||||||
|
const alignRow = splitRow(tableLines[1]);
|
||||||
|
const alignments = alignRow.map(col => {
|
||||||
|
const tc = col.trim();
|
||||||
|
if (tc.startsWith(':') && tc.endsWith(':')) return 'center';
|
||||||
|
if (tc.endsWith(':')) return 'right';
|
||||||
|
return 'left';
|
||||||
|
});
|
||||||
|
|
||||||
|
const rows = [];
|
||||||
|
for (let r = 2; r < tableLines.length; r++) {
|
||||||
|
rows.push(splitRow(tableLines[r]));
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '<div class="grammar-table-wrap"><table class="grammar-cheatsheet-table"><thead><tr>';
|
||||||
|
headers.forEach((h, idx) => {
|
||||||
|
const align = alignments[idx] || 'left';
|
||||||
|
html += `<th style="text-align: ${align}">${renderMarkdownInline(h)}</th>`;
|
||||||
|
});
|
||||||
|
html += '</tr></thead><tbody>';
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
html += '<tr>';
|
||||||
|
row.forEach((cell, idx) => {
|
||||||
|
const align = alignments[idx] || 'left';
|
||||||
|
html += `<td style="text-align: ${align}">${renderMarkdownInline(cell)}</td>`;
|
||||||
|
});
|
||||||
|
html += '</tr>';
|
||||||
|
});
|
||||||
|
html += '</tbody></table></div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderMarkdownNotes(mdText) {
|
||||||
|
if (!mdText || typeof mdText !== 'string' || !mdText.trim()) return '';
|
||||||
|
const lines = mdText.split(/\r?\n/);
|
||||||
|
const out = [];
|
||||||
|
let inList = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const trimmed = lines[i].trim();
|
||||||
|
|
||||||
|
if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
|
||||||
|
if (!inList) {
|
||||||
|
out.push('<ul class="grammar-notes-list">');
|
||||||
|
inList = true;
|
||||||
|
}
|
||||||
|
const itemText = trimmed.replace(/^[-*]\s+/, '');
|
||||||
|
out.push(`<li>${renderMarkdownInline(itemText)}</li>`);
|
||||||
|
continue;
|
||||||
|
} else if (inList) {
|
||||||
|
out.push('</ul>');
|
||||||
|
inList = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.startsWith('#### ')) {
|
||||||
|
out.push(`<h5 class="grammar-note-h5">${renderMarkdownInline(trimmed.replace(/^####\s+/, ''))}</h5>`);
|
||||||
|
} else if (trimmed.startsWith('### ')) {
|
||||||
|
out.push(`<h4 class="grammar-note-h4">${renderMarkdownInline(trimmed.replace(/^###\s+/, ''))}</h4>`);
|
||||||
|
} else if (trimmed.startsWith('## ')) {
|
||||||
|
out.push(`<h3 class="grammar-note-h3">${renderMarkdownInline(trimmed.replace(/^##\s+/, ''))}</h3>`);
|
||||||
|
} else if (trimmed.startsWith('> ')) {
|
||||||
|
out.push(`<blockquote class="grammar-note-quote">${renderMarkdownInline(trimmed.replace(/^>\s+/, ''))}</blockquote>`);
|
||||||
|
} else if (trimmed === '') {
|
||||||
|
// blank line separator
|
||||||
|
} else {
|
||||||
|
out.push(`<p class="grammar-note-p">${renderMarkdownInline(trimmed)}</p>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inList) {
|
||||||
|
out.push('</ul>');
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<div class="grammar-notes-box">${out.join('\n')}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Toast Notifications
|
// Toast Notifications
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
@@ -168,6 +273,11 @@ export function closeModal(modalElem) {
|
|||||||
// View Switcher Helper
|
// View Switcher Helper
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
export function switchView(targetViewId) {
|
export function switchView(targetViewId) {
|
||||||
|
if (targetViewId !== 'reader-view') {
|
||||||
|
document.body.classList.remove('zen-mode-active');
|
||||||
|
const hud = document.getElementById('reader-zen-hud');
|
||||||
|
if (hud) hud.classList.add('hidden');
|
||||||
|
}
|
||||||
const allViews = [
|
const allViews = [
|
||||||
'dictionary-view',
|
'dictionary-view',
|
||||||
'srs-view',
|
'srs-view',
|
||||||
@@ -286,6 +396,9 @@ export const ui = {
|
|||||||
playTextSpeech,
|
playTextSpeech,
|
||||||
cleanPromptTranslation,
|
cleanPromptTranslation,
|
||||||
formatTranslationWithSpoiler,
|
formatTranslationWithSpoiler,
|
||||||
|
renderMarkdownInline,
|
||||||
|
renderMarkdownTable,
|
||||||
|
renderMarkdownNotes,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ui;
|
export default ui;
|
||||||
|
|||||||
+829
-754
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1511,8 +1511,18 @@ def test_grammar_engine_and_handwritten_journaling():
|
|||||||
assert len(u_data["contrasts"]) >= 1
|
assert len(u_data["contrasts"]) >= 1
|
||||||
assert "journaling_prompt" in u_data
|
assert "journaling_prompt" in u_data
|
||||||
assert "тетрадь" in u_data["journaling_prompt"].lower()
|
assert "тетрадь" in u_data["journaling_prompt"].lower()
|
||||||
|
assert "cheatsheet_table" in u_data
|
||||||
|
assert "|" in u_data["cheatsheet_table"]
|
||||||
|
assert "detailed_notes" in u_data
|
||||||
|
assert len(u_data["detailed_notes"]) > 100
|
||||||
|
assert "###" in u_data["detailed_notes"]
|
||||||
assert len(u_data["exercises"]) == 5
|
assert len(u_data["exercises"]) == 5
|
||||||
|
|
||||||
|
# Check lookup by unit_num directly
|
||||||
|
num_res = client.get("/api/grammar/units/1")
|
||||||
|
assert num_res.status_code == 200
|
||||||
|
assert num_res.json()["id"] == unit_1_id
|
||||||
|
|
||||||
ex0 = u_data["exercises"][0]
|
ex0 = u_data["exercises"][0]
|
||||||
assert len(ex0["options"]) == 4
|
assert len(ex0["options"]) == 4
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user