feat(reader): implement FEAT-01 Focus Zen Mode (100% vh/vw, auto-hiding HUD, hotkeys F/Esc, native fullscreen)

This commit is contained in:
2026-10-08 10:45:09 +03:00
parent 9bbc9473de
commit f10965eff5
13 changed files with 3157 additions and 792 deletions
+48 -14
View File
@@ -51,7 +51,7 @@ CURATED_MURPHY_UNITS = _LazyUnitsList()
def seed_grammar_units(conn: sqlite3.Connection) -> int:
"""
Populates grammar_units table with curated Murphy units.
Safe and idempotent (INSERT OR IGNORE).
Safe, idempotent upsert (ON CONFLICT DO UPDATE).
"""
inserted = 0
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)
cursor = conn.execute(
"""
INSERT OR IGNORE INTO grammar_units (
INSERT INTO grammar_units (
unit_num, level, category, title, subtitle, golden_formula,
cheatsheet_table, detailed_notes,
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"],
@@ -71,6 +83,8 @@ def seed_grammar_units(conn: sqlite3.Connection) -> int:
u["title"],
u.get("subtitle", ""),
u["golden_formula"],
u.get("cheatsheet_table", ""),
u.get("detailed_notes", ""),
contrast_json,
u["journaling_prompt"],
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]]:
"""
Returns full content of a unit for lesson mode.
Supports looking up by database PK or unit_num.
"""
row = conn.execute(
"""
SELECT
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
FROM grammar_units u
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()
if not row:
@@ -162,9 +180,12 @@ def get_grammar_unit_detail(conn: sqlite3.Connection, user_id: int, unit_id: int
"level": row["level"],
"category": row["category"],
"title": row["title"],
"subtitle": row["subtitle"],
"subtitle": row["subtitle"] or "",
"golden_formula": row["golden_formula"],
"cheatsheet_table": row["cheatsheet_table"] or "",
"detailed_notes": row["detailed_notes"] or "",
"contrasts": contrasts,
"contrast_explanation": contrasts,
"journaling_prompt": row["journaling_prompt"],
"exercises": exercises,
"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'.
"""
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()
if not unit_row:
@@ -293,9 +315,15 @@ def record_grammar_completion(
"""
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(
"SELECT id FROM grammar_progress WHERE user_id = ? AND unit_id = ?",
(user_id, unit_id),
(user_id, real_unit_id),
).fetchone()
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
) 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(
@@ -335,10 +363,16 @@ def update_handwritten_status(
"""
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
prog_row = conn.execute(
"SELECT id FROM grammar_progress WHERE user_id = ? AND unit_id = ?",
(user_id, unit_id),
(user_id, real_unit_id),
).fetchone()
if prog_row:
@@ -358,7 +392,7 @@ def update_handwritten_status(
user_id, unit_id, is_completed, score, handwritten_logged, handwritten_at
) 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}