diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,1237 +1,988 @@ - - - - - -GéoRévision — Tectonique des Plaques - - - - -
- -
-
-

GÉORÉVISION

- Méthode Ebbinghaus + Testing Effect -
-
Tectonique des Plaques — Chapitres 3, 4, 5 & Synthèse · 76 notions
-
- - -
-
76Notions
-
0Maîtrisées
-
0En cours
-
0À revoir
-
Score Quiz
-
- - -
- - - - -
- - -
- -
- - - - - - -
- -
-
- 0 / 0 -
- -
-
-
- -
-
-
-
CLIQUER POUR RÉVÉLER ↗
-
-
- -
-
-
← RETOURNER
-
-
-
- - -
- - - -
- -
- - -
- -
- - - - - -
- -
-
-
- - -
-
-
-
-
- -
- -
-
-
score final
-
- - -
- -
- - -
-
-
Maîtrisé
-
Hésitant
-
À revoir
-
Non vu
-
-
-

Cliquez sur un numéro pour aller directement à la carte correspondante.

-
- - -
-
-
- -
- - - - \ No newline at end of file +STATUS_LABELS = {None: "Non vu", 0: "À revoir", 1: "Hésitant", 2: "Maîtrisé"} + +WRONG_POOL = [ + "Cristallisation fractionnée", "Plan de Wadati-Benioff", "Décompression adiabatique", + "Fusion partielle", "Serpentinisation", "Dorsale médio-atlantique", "Glaucophane", + "Faille inverse", "Faille normale", "Basalte en coussin", "Jadéite", "Grenat", + "Hornblende", "Chlorite", "Actinote", "Gabbro", "Andésite", "Diorite", + "Schiste bleu", "Schiste vert", "Éclogite", "Slab pull", "Coin mantellique", + "Chambre magmatique", "Isotherme 1300°C", "Solidus", "Géotherme", + "Arc insulaire", "Cordillère", "Nuée ardente", "5 à 7 km", "10 à 16 cm/an", + "1 à 5 cm/an", "700 km de profondeur", "50 km de profondeur", + "Fumeurs noirs", "Micaschiste", "Gneiss", "Péridotite", "Pyroxène", + "Plagioclase", "Serpentine", "Nappe de charriage", "Racine crustale", +] + +# ══════════════════════════════════════════════════════════ +# SAUVEGARDE +# ══════════════════════════════════════════════════════════ +def load_progress(): + if os.path.exists(SAVE_FILE): + try: + with open(SAVE_FILE) as f: + data = json.load(f) + return {int(k): v for k, v in data.items()} + except Exception: + pass + return {} + +def save_progress(status): + try: + with open(SAVE_FILE, "w") as f: + json.dump(status, f) + except Exception: + pass + +# ══════════════════════════════════════════════════════════ +# UTILITAIRES CURSES +# ══════════════════════════════════════════════════════════ +def init_colors(): + curses.start_color() + curses.use_default_colors() + # Paires : (numéro, fg, bg) + curses.init_pair(1, curses.COLOR_WHITE, -1) # normal + curses.init_pair(2, curses.COLOR_CYAN, -1) # titre/accent + curses.init_pair(3, curses.COLOR_GREEN, -1) # maîtrisé + curses.init_pair(4, curses.COLOR_YELLOW, -1) # hésitant / ch.3 + curses.init_pair(5, curses.COLOR_RED, -1) # à revoir + curses.init_pair(6, curses.COLOR_MAGENTA, -1) # ch.4 / hint + curses.init_pair(7, curses.COLOR_BLUE, -1) # ch.5 + curses.init_pair(8, curses.COLOR_WHITE, curses.COLOR_BLUE) # sélectionné + curses.init_pair(9, curses.COLOR_BLACK, curses.COLOR_GREEN) # correct + curses.init_pair(10, curses.COLOR_WHITE, curses.COLOR_RED) # faux + curses.init_pair(11, curses.COLOR_CYAN, curses.COLOR_BLACK) # header bg + curses.init_pair(12, curses.COLOR_WHITE, curses.COLOR_CYAN) # bouton actif + +C_NORMAL = lambda: curses.color_pair(1) +C_TITLE = lambda: curses.color_pair(2) | curses.A_BOLD +C_GREEN = lambda: curses.color_pair(3) | curses.A_BOLD +C_YELLOW = lambda: curses.color_pair(4) | curses.A_BOLD +C_RED = lambda: curses.color_pair(5) | curses.A_BOLD +C_MAGENTA = lambda: curses.color_pair(6) +C_BLUE = lambda: curses.color_pair(7) +C_SEL = lambda: curses.color_pair(8) | curses.A_BOLD +C_OK = lambda: curses.color_pair(9) | curses.A_BOLD +C_FAIL = lambda: curses.color_pair(10) | curses.A_BOLD +C_HEADER = lambda: curses.color_pair(11) | curses.A_BOLD +C_BTN = lambda: curses.color_pair(12) | curses.A_BOLD + +def ch_color(ch): + return { + "3": C_YELLOW(), + "4": C_MAGENTA(), + "5": C_BLUE(), + "synth": C_GREEN(), + }.get(ch, C_NORMAL()) + +def safe_addstr(win, y, x, text, attr=0): + h, w = win.getmaxyx() + if y < 0 or y >= h or x < 0: + return + max_len = w - x - 1 + if max_len <= 0: + return + try: + win.addstr(y, x, text[:max_len], attr) + except curses.error: + pass + +def draw_box(win, y, x, h, w, title="", color=None): + attr = color or C_NORMAL() + h2, w2 = win.getmaxyx() + # top + try: + safe_addstr(win, y, x, "╔" + "═" * (w-2) + "╗", attr) + for i in range(1, h-1): + if y+i < h2: + safe_addstr(win, y+i, x, "║", attr) + safe_addstr(win, y+i, x+w-1, "║", attr) + safe_addstr(win, y+h-1, x, "╚" + "═" * (w-2) + "╝", attr) + if title: + t = f" {title} " + tx = x + (w - len(t)) // 2 + safe_addstr(win, y, tx, t, attr | curses.A_BOLD) + except curses.error: + pass + +def draw_header(win, title): + h, w = win.getmaxyx() + try: + win.attron(C_HEADER()) + win.addstr(0, 0, " " * w) + safe_addstr(win, 0, 0, f" GÉO-RÉVISION │ {title}", C_HEADER()) + win.attroff(C_HEADER()) + except curses.error: + pass + +def wrap_text(text, width): + lines = [] + for paragraph in text.split("\n"): + if len(paragraph) <= width: + lines.append(paragraph) + else: + wrapped = textwrap.wrap(paragraph, width) + lines.extend(wrapped if wrapped else [""]) + return lines + +def get_stats(status): + good = sum(1 for v in status.values() if v == 2) + ok = sum(1 for v in status.values() if v == 1) + bad = sum(1 for v in status.values() if v == 0) + unseen = 76 - good - ok - bad + return good, ok, bad, unseen + +def draw_stats_bar(win, y, status): + good, ok, bad, unseen = get_stats(status) + h, w = win.getmaxyx() + bar = ( + f" ✓ Maîtrisés:{good:3} " + f"~ Hésitants:{ok:3} " + f"✗ À revoir:{bad:3} " + f"? Non vus:{unseen:3} " + f"Total: 76" + ) + safe_addstr(win, y, 0, bar.ljust(w), C_NORMAL()) + +# ══════════════════════════════════════════════════════════ +# ÉCRAN MENU PRINCIPAL +# ══════════════════════════════════════════════════════════ +def menu_screen(stdscr, status): + options = [ + ("1", "Flashcards", "Révision active par carte — toutes chapitres"), + ("2", "Flashcards CH.3", "Chapitre 3 : Mobilité horizontale"), + ("3", "Flashcards CH.4", "Chapitre 4 : Zones de divergence"), + ("4", "Flashcards CH.5", "Chapitre 5 : Zones de convergence"), + ("5", "Flashcards Synthèse", "Cycle lithosphérique complet"), + ("6", "Flashcards À revoir", "Seulement les cartes non maîtrisées"), + ("7", "Quiz QCM", "Entraînement avec 4 propositions"), + ("8", "Récapitulatif", "Toutes les 76 notions en lecture"), + ("9", "Carte de progression", "Visualisation de l'avancement"), + ("0", "Réinitialiser", "Effacer la progression"), + ("Q", "Quitter", ""), + ] + sel = 0 + while True: + stdscr.clear() + h, w = stdscr.getmaxyx() + draw_header(stdscr, "MENU PRINCIPAL") + + # Logo + logo = [ + "╔═══════════════════════════════════════════════════╗", + "║ GÉO-RÉVISION — Tectonique des Plaques ║", + "║ 76 notions · CH.3 · CH.4 · CH.5 · Synthèse ��", + "║ Méthode : Ebbinghaus + Testing Effect ║", + "╚═══════════════════════════════════════════════════╝", + ] + for i, line in enumerate(logo): + safe_addstr(stdscr, 2+i, max(0,(w-len(line))//2), line, C_TITLE()) + + # Stats + draw_stats_bar(stdscr, 8, status) + + # Options + for i, (key, name, desc) in enumerate(options): + y = 10 + i + attr = C_SEL() if i == sel else C_NORMAL() + prefix = "▶ " if i == sel else " " + line = f"{prefix}[{key}] {name:<30} {desc}" + safe_addstr(stdscr, y, 2, line[:w-4], attr) + + safe_addstr(stdscr, h-2, 2, "↑↓ pour naviguer · ENTRÉE pour sélectionner", C_MAGENTA()) + + k = stdscr.getch() + if k == curses.KEY_UP: + sel = (sel - 1) % len(options) + elif k == curses.KEY_DOWN: + sel = (sel + 1) % len(options) + elif k in (curses.KEY_ENTER, 10, 13): + key = options[sel][0] + return key + elif k in range(256): + c = chr(k).upper() + for i, (key, _, _) in enumerate(options): + if c == key.upper(): + return key + +# ══════════════════════════════════════════════════════════ +# FLASHCARD +# ══════════════════════════════════════════════════════════ +def flashcard_screen(stdscr, status, cards, title="Flashcards"): + if not cards: + stdscr.clear() + draw_header(stdscr, title) + safe_addstr(stdscr, 5, 4, "Aucune carte dans ce filtre.", C_YELLOW()) + safe_addstr(stdscr, 7, 4, "Appuie sur une touche pour revenir.", C_NORMAL()) + stdscr.getch() + return status + + deck = cards[:] + random.shuffle(deck) + idx = 0 + flipped = False + + while True: + card = deck[idx] + stdscr.clear() + h, w = stdscr.getmaxyx() + draw_header(stdscr, f"{title} [{idx+1}/{len(deck)}]") + draw_stats_bar(stdscr, 1, status) + + # Barre progression + pct = (idx + 1) / len(deck) + bar_w = min(w - 4, 60) + filled = int(pct * bar_w) + bar = "█" * filled + "░" * (bar_w - filled) + safe_addstr(stdscr, 2, 2, f"[{bar}] {int(pct*100)}%", C_CYAN() if hasattr(curses, 'COLOR_CYAN') else C_NORMAL()) + + # Encart chapitre + safe_addstr(stdscr, 4, 2, f"■ {card['section']}", ch_color(card["ch"])) + + # Statut de la carte + s = status.get(card["id"]) + s_colors = {None: C_NORMAL(), 0: C_RED(), 1: C_YELLOW(), 2: C_GREEN()} + s_label = STATUS_LABELS.get(s, "Non vu") + safe_addstr(stdscr, 4, w-20, f"[#{card['id']:02d}] {s_label}", s_colors.get(s, C_NORMAL())) + + if not flipped: + # Recto : question + draw_box(stdscr, 5, 1, min(12, h-14), w-2, "QUESTION", C_TITLE()) + q_lines = wrap_text(card["q"], w-6) + for i, line in enumerate(q_lines[:6]): + safe_addstr(stdscr, 7+i, 4, line, C_NORMAL() | curses.A_BOLD) + if card.get("hint"): + safe_addstr(stdscr, 13, 4, f"💡 {card['hint']}", C_MAGENTA()) + safe_addstr(stdscr, 15, 4, "[ ESPACE ] Révéler la réponse", C_TITLE()) + safe_addstr(stdscr, 16, 4, "[ ← / → ] Carte précédente / suivante [ M ] Mélanger [ Q ] Menu", C_MAGENTA()) + else: + # Verso : réponse + draw_box(stdscr, 5, 1, 3, w-2, "QUESTION", C_YELLOW()) + q_short = card["q"][:w-6] + safe_addstr(stdscr, 6, 4, q_short, C_YELLOW()) + + draw_box(stdscr, 8, 1, min(11, h-16), w-2, "RÉPONSE", C_GREEN()) + a_lines = wrap_text(card["a"], w-6) + for i, line in enumerate(a_lines[:7]): + safe_addstr(stdscr, 10+i, 4, line, C_NORMAL()) + + # Boutons notation + y_btn = h - 6 + safe_addstr(stdscr, y_btn, 2, "[ 1 ] ✗ À REVOIR", C_RED()) + safe_addstr(stdscr, y_btn, 25, "[ 2 ] ~ HÉSITANT", C_YELLOW()) + safe_addstr(stdscr, y_btn, 50, "[ 3 ] ✓ MAÎTRISÉ", C_GREEN()) + safe_addstr(stdscr, y_btn+1, 2, "[ ← / → ] Naviguer [ ESPACE ] Retourner [ Q ] Menu", C_MAGENTA()) + + k = stdscr.getch() + + if k == ord(' '): + flipped = not flipped + elif k == curses.KEY_RIGHT or k == ord('n') or k == ord('N'): + idx = (idx + 1) % len(deck) + flipped = False + elif k == curses.KEY_LEFT or k == ord('p') or k == ord('P'): + idx = (idx - 1) % len(deck) + flipped = False + elif k == ord('m') or k == ord('M'): + random.shuffle(deck) + idx = 0 + flipped = False + elif k == ord('1') and flipped: + status[card["id"]] = 0 + save_progress(status) + flipped = False + idx = (idx + 1) % len(deck) + elif k == ord('2') and flipped: + status[card["id"]] = 1 + save_progress(status) + flipped = False + idx = (idx + 1) % len(deck) + elif k == ord('3') and flipped: + status[card["id"]] = 2 + save_progress(status) + flipped = False + idx = (idx + 1) % len(deck) + elif k == ord('q') or k == ord('Q'): + break + + return status + +def C_CYAN(): + return curses.color_pair(2) + +# ══════════════════════════════════════════════════════════ +# QUIZ QCM +# ══════════════════════════════════════════════════════════ +def build_quiz_question(card): + """Crée une question QCM à partir d'une carte.""" + first_line = card["a"].split("\n")[0].strip() + # Nettoie les annotations type "1. " + correct = re.sub(r"^\d+\.\s*", "", first_line).strip() + if len(correct) > 70: + correct = correct[:70] + "..." + + # Génère 3 mauvaises réponses différentes + pool = [w for w in WRONG_POOL if w.lower() not in correct.lower()] + random.shuffle(pool) + wrongs = pool[:3] + + options = wrongs + [correct] + random.shuffle(options) + return { + "question": card["q"], + "correct": correct, + "options": options, + "explanation": card["a"], + "section": card["section"], + "ch": card["ch"], + } + +def quiz_screen(stdscr, status, cards=None): + if cards is None: + cards = CARDS[:] + random.shuffle(cards) + score = 0 + total = len(cards) + wrong_ids = [] + + for qi, card in enumerate(cards): + qdata = build_quiz_question(card) + sel = 0 + answered = False + chosen = None + + while True: + stdscr.clear() + h, w = stdscr.getmaxyx() + draw_header(stdscr, f"QUIZ [{qi+1}/{total}] Score: {score}/{qi}") + draw_stats_bar(stdscr, 1, status) + + # Section + safe_addstr(stdscr, 3, 2, f"■ {qdata['section']}", ch_color(qdata["ch"])) + + # Question + draw_box(stdscr, 4, 1, 4, w-2, "QUESTION", C_TITLE()) + q_lines = wrap_text(qdata["question"], w-6) + for i, line in enumerate(q_lines[:2]): + safe_addstr(stdscr, 5+i, 4, line, C_NORMAL() | curses.A_BOLD) + + # Options + for i, opt in enumerate(qdata["options"]): + y = 9 + i * 2 + prefix = " " + if not answered: + attr = C_SEL() if i == sel else C_NORMAL() + prefix = "▶ " if i == sel else " " + else: + if opt == qdata["correct"]: + attr = C_GREEN() + prefix = "✓ " + elif opt == chosen and chosen != qdata["correct"]: + attr = C_RED() + prefix = "✗ " + else: + attr = C_NORMAL() + prefix = " " + + opt_short = opt[:w-10] + safe_addstr(stdscr, y, 2, f"{prefix}[{'ABCD'[i]}] {opt_short}", attr) + + if answered: + # Explication + exp_y = 18 + if chosen == qdata["correct"]: + safe_addstr(stdscr, exp_y, 2, "✓ BONNE RÉPONSE !", C_GREEN()) + else: + safe_addstr(stdscr, exp_y, 2, f"✗ Réponse correcte : {qdata['correct'][:w-30]}", C_RED()) + exp_lines = wrap_text(qdata["explanation"], w-6) + for i, line in enumerate(exp_lines[:3]): + safe_addstr(stdscr, exp_y+1+i, 4, line, C_MAGENTA()) + safe_addstr(stdscr, h-2, 2, "[ ENTRÉE ou ESPACE ] Question suivante [ Q ] Arrêter", C_TITLE()) + else: + safe_addstr(stdscr, h-2, 2, "[ ↑↓ ] Naviguer [ ENTRÉE ] Valider [ Q ] Arrêter", C_TITLE()) + + k = stdscr.getch() + + if not answered: + if k == curses.KEY_UP: + sel = (sel - 1) % 4 + elif k == curses.KEY_DOWN: + sel = (sel + 1) % 4 + elif k in range(ord('a'), ord('e')): + sel = k - ord('a') + elif k in range(ord('A'), ord('E')): + sel = k - ord('A') + elif k in (curses.KEY_ENTER, 10, 13, ord(' ')): + answered = True + chosen = qdata["options"][sel] + if chosen == qdata["correct"]: + score += 1 + status[card["id"]] = max(status.get(card["id"], 0) or 0, 2) + else: + wrong_ids.append(card["id"]) + status[card["id"]] = 0 + save_progress(status) + elif k == ord('q') or k == ord('Q'): + break + else: + if k in (curses.KEY_ENTER, 10, 13, ord(' ')): + break + elif k == ord('q') or k == ord('Q'): + # Montre résultat intermédiaire + qi = total + break + + if k == ord('q') or k == ord('Q'): + break + + # Résultat final + stdscr.clear() + h, w = stdscr.getmaxyx() + draw_header(stdscr, "RÉSULTAT DU QUIZ") + + pct = int(score / total * 100) if total > 0 else 0 + color = C_GREEN() if pct >= 80 else C_YELLOW() if pct >= 50 else C_RED() + + draw_box(stdscr, 3, w//2-20, 12, 40, "SCORE FINAL", C_TITLE()) + score_str = f"{pct}%" + safe_addstr(stdscr, 6, w//2 - len(score_str)//2, score_str, color | curses.A_BOLD) + detail = f"{score} bonnes réponses sur {total}" + safe_addstr(stdscr, 8, w//2 - len(detail)//2, detail, C_NORMAL()) + + msg = ("🏆 Excellent ! Chapitres maîtrisés !" if pct >= 80 else + "👍 Bien ! Quelques révisions restent utiles." if pct >= 60 else + "⚠ Des lacunes — retourne aux flashcards !" if pct >= 40 else + "📚 Reprends les flashcards et révise tout !") + safe_addstr(stdscr, 10, w//2 - len(msg)//2, msg[:w-4], C_TITLE()) + + if wrong_ids: + safe_addstr(stdscr, 16, 2, f"Cartes à revoir : {', '.join(f'#{i}' for i in wrong_ids[:15])}", C_RED()) + + safe_addstr(stdscr, h-2, 2, "Appuie sur une touche pour revenir au menu.", C_MAGENTA()) + stdscr.getch() + return status + +# ══════════════════════════════════════════════════════════ +# RÉCAPITULATIF +# ══════════════════════════════════════════════════════════ +def recap_screen(stdscr, status): + lines = [] + groups = [ + ("3", "═══ CHAPITRE 3 — Mobilité horizontale des plaques ═══"), + ("4", "═══ CHAPITRE 4 — Dynamique des zones de divergence ═══"), + ("5", "═══ CHAPITRE 5 — Dynamique des zones de convergence ═══"), + ("synth", "═══ SYNTHÈSE — Cycle de la lithosphère océanique ═══"), + ] + for ch, title in groups: + lines.append(("title", ch, title)) + for c in CARDS: + if c["ch"] == ch: + lines.append(("section", ch, f" ─ {c['section']}")) + lines.append(("question", ch, f" #{c['id']:02d} Q: {c['q']}")) + for al in c["a"].split("\n"): + lines.append(("answer", ch, f" → {al}")) + lines.append(("sep", ch, "")) + + scroll = 0 + while True: + stdscr.clear() + h, w = stdscr.getmaxyx() + draw_header(stdscr, "RÉCAPITULATIF COMPLET — 76 notions") + draw_stats_bar(stdscr, 1, status) + + visible = h - 5 + for i, (ltype, ch, text) in enumerate(lines[scroll:scroll+visible]): + y = 3 + i + text_trunc = text[:w-2] + if ltype == "title": + safe_addstr(stdscr, y, 0, text_trunc.ljust(w-1), ch_color(ch) | curses.A_BOLD) + elif ltype == "section": + safe_addstr(stdscr, y, 0, text_trunc, C_CYAN()) + elif ltype == "question": + safe_addstr(stdscr, y, 0, text_trunc, C_NORMAL() | curses.A_BOLD) + elif ltype == "answer": + safe_addstr(stdscr, y, 0, text_trunc, C_MAGENTA()) + else: + pass + + pct_scroll = int(scroll / max(1, len(lines) - visible) * 100) + safe_addstr(stdscr, h-1, 2, f"↑↓/PgUp/PgDn : défiler ({pct_scroll}%) Q : menu", C_TITLE()) + + k = stdscr.getch() + if k == curses.KEY_UP: + scroll = max(0, scroll - 1) + elif k == curses.KEY_DOWN: + scroll = min(len(lines) - visible, scroll + 1) + elif k == curses.KEY_PPAGE: + scroll = max(0, scroll - visible) + elif k == curses.KEY_NPAGE: + scroll = min(len(lines) - visible, scroll + visible) + elif k == ord('q') or k == ord('Q'): + break + +# ══════════════════════════════════════════════════════════ +# CARTE DE PROGRESSION +# ══════════════════════════════════════════════════════════ +def map_screen(stdscr, status): + while True: + stdscr.clear() + h, w = stdscr.getmaxyx() + draw_header(stdscr, "CARTE DE PROGRESSION") + draw_stats_bar(stdscr, 1, status) + + safe_addstr(stdscr, 3, 2, "Légende :", C_NORMAL() | curses.A_BOLD) + safe_addstr(stdscr, 3, 14, "[██] Non vu", C_NORMAL()) + safe_addstr(stdscr, 3, 28, "[██] À revoir", C_RED()) + safe_addstr(stdscr, 3, 44, "[~~] Hésitant", C_YELLOW()) + safe_addstr(stdscr, 3, 60, "[OK] Maîtrisé", C_GREEN()) + + # Grille + cols = min(19, (w - 4) // 5) + for c in CARDS: + idx = c["id"] - 1 + row = idx // cols + col = idx % cols + y = 5 + row * 2 + x = 2 + col * 5 + if y >= h - 3: + break + s = status.get(c["id"]) + if s == 2: + attr = C_GREEN() + label = f"OK " + elif s == 1: + attr = C_YELLOW() + label = f"~~ " + elif s == 0: + attr = C_RED() + label = f"✗ " + else: + attr = C_NORMAL() + label = f" " + safe_addstr(stdscr, y, x, f"{c['id']:02d}", attr | curses.A_BOLD) + safe_addstr(stdscr, y+1, x, label, attr) + + # Résumé par chapitre + y_sum = h - 8 + safe_addstr(stdscr, y_sum, 2, "Par chapitre :", C_TITLE()) + for ch in ["3", "4", "5", "synth"]: + ch_cards = [c for c in CARDS if c["ch"] == ch] + ch_good = sum(1 for c in ch_cards if status.get(c["id"]) == 2) + ch_ok = sum(1 for c in ch_cards if status.get(c["id"]) == 1) + line = f"{CH_LABELS[ch]:20s} ✓{ch_good:3d} ~{ch_ok:3d} /{len(ch_cards)}" + safe_addstr(stdscr, y_sum+1 + list(["3","4","5","synth"]).index(ch), + 4, line, ch_color(ch)) + + safe_addstr(stdscr, h-1, 2, "Q : revenir au menu", C_MAGENTA()) + + k = stdscr.getch() + if k == ord('q') or k == ord('Q'): + break + +# ══════════════════════════════════════════════════════════ +# RÉINITIALISATION +# ══════════════════════════════════════════════════════════ +def confirm_reset(stdscr, status): + stdscr.clear() + h, w = stdscr.getmaxyx() + draw_header(stdscr, "RÉINITIALISATION") + safe_addstr(stdscr, 5, 4, "⚠ Effacer toute la progression ?", C_RED()) + safe_addstr(stdscr, 7, 4, "[ O ] Oui, effacer [ N ] Annuler", C_NORMAL()) + k = stdscr.getch() + if k in (ord('o'), ord('O')): + status.clear() + if os.path.exists(SAVE_FILE): + os.remove(SAVE_FILE) + safe_addstr(stdscr, 9, 4, "Progression effacée.", C_GREEN()) + stdscr.refresh() + curses.napms(1000) + return status + +# ══════════════════════════════════════════════════════════ +# MAIN +# ══════════════════════════════════════════════════════════ +def main(stdscr): + curses.curs_set(0) + stdscr.keypad(True) + init_colors() + status = load_progress() + + while True: + choice = menu_screen(stdscr, status) + + if choice == "1": + status = flashcard_screen(stdscr, status, CARDS[:], "Flashcards — Tout") + elif choice == "2": + cards = [c for c in CARDS if c["ch"] == "3"] + status = flashcard_screen(stdscr, status, cards, "Flashcards — CH.3") + elif choice == "3": + cards = [c for c in CARDS if c["ch"] == "4"] + status = flashcard_screen(stdscr, status, cards, "Flashcards — CH.4") + elif choice == "4": + cards = [c for c in CARDS if c["ch"] == "5"] + status = flashcard_screen(stdscr, status, cards, "Flashcards — CH.5") + elif choice == "5": + cards = [c for c in CARDS if c["ch"] == "synth"] + status = flashcard_screen(stdscr, status, cards, "Flashcards — Synthèse") + elif choice == "6": + cards = [c for c in CARDS if status.get(c["id"]) in (None, 0)] + if not cards: + cards = CARDS[:] + status = flashcard_screen(stdscr, status, cards, "Flashcards — À revoir") + elif choice == "7": + status = quiz_screen(stdscr, status) + elif choice == "8": + recap_screen(stdscr, status) + elif choice == "9": + map_screen(stdscr, status) + elif choice == "0": + status = confirm_reset(stdscr, status) + elif choice in ("Q", "q"): + break + + save_progress(status) + +if __name__ == "__main__": + curses.wrapper(main) \ No newline at end of file