Rebuild flashcard study screen from mockup, with real backend-backed stats

Study screen now matches the provided mockup's layout: a "Current
Deck" panel with a circular mastery-progress ring, and a "Spaced
Repetition" panel (New/Due/Learning/Review counts, streak, daily
goal) on either side of the card. Kept the app's real global sidebar
rather than the mockup's own nav, per direction.

Mastery/Streak/Daily Goal weren't tracked anywhere, so this adds real
backend support rather than showing fabricated numbers:
- Mastered = graduated to review state AND FSRS stability >= 21 days
  (FlashcardScheduleStore.MasteryStabilityDaysThreshold), stricter
  than the plain Review-state count
- CurrentStreakDays = consecutive UTC days with a review, computed
  from review-log timestamps in the new pure FlashcardScheduling
  .ComputeStreakDays (unit tested, habit-streak semantics: doesn't
  reset to 0 the instant a new day starts before today's first review)
- DailyGoal/ReviewsToday reuse the account's existing ReviewsPerDay
  setting rather than inventing a new one
Also removes the exit "X" button in favor of the same Escape/pause
menu the question-player flow uses (main.gd's shared app_menu, with
an "Exit Study Session" action), and adds a new circular_progress.gd
reusable ring control plus a real "Options" menu (bury/suspend the
current card) wired to schedule endpoints that had no UI before.
This commit is contained in:
hz
2026-07-21 23:20:14 -05:00
parent 72fb6e4776
commit 70aa425fde
10 changed files with 600 additions and 23 deletions
+32
View File
@@ -106,6 +106,38 @@ func _run() -> void:
_assert(not study.compact_layout, "a wide viewport should restore the study screen's desktop layout")
_assert(is_equal_approx(study.card_panel.custom_minimum_size.x, 720.0),
"a wide viewport should restore the study card's full 720px width")
_assert(study.deck_panel.visible and study.stats_panel.visible,
"a wide viewport should show the current-deck and spaced-repetition side panels")
study.size = Vector2(360, 720)
study._update_responsive_layout()
_assert(not study.deck_panel.visible and not study.stats_panel.visible,
"a phone-width viewport should hide the side panels rather than squeeze a 3-column layout into no room")
study.deck_id = "deck-1"
study.decks = [
{"deck": {"id": "deck-1", "name": "Nephrology"}, "total": 150, "mastered": 48},
{"deck": {"id": "deck-2", "name": "Cardiology"}, "total": 40, "mastered": 40},
]
study._refresh_deck_panel()
_assert(study.deck_name_label.text == "Nephrology", "the deck panel should show the currently-studied deck's name, not just the first one")
_assert(study.deck_mastered_label.text == "48 / 150 cards mastered", "the deck panel should report mastered/total for the current deck only")
study.deck_id = ""
study._refresh_deck_panel()
_assert(study.deck_name_label.text == "All decks", "studying with no deck filter should label the panel \"All decks\"")
_assert(study.deck_mastered_label.text == "88 / 190 cards mastered", "\"All decks\" should sum mastered/total across every deck")
study.latest_counts = {
"due": 5, "newAvailable": 15, "learning": 48, "review": 132,
"currentStreakDays": 1, "reviewsToday": 25, "dailyGoal": 50
}
study._refresh_stats_panel()
_assert(study.stats_due_label.text == "5" and study.stats_learning_label.text == "48" and study.stats_review_label.text == "132",
"the spaced-repetition panel should reflect the latest due/learning/review counts")
_assert(study.streak_label.text == "1 day", "a streak of exactly one day should use the singular form")
_assert(study.daily_goal_label.text == "25 / 50" and is_equal_approx(study.daily_goal_bar.value, 25.0),
"the daily goal panel should show today's review count against the account's configured goal")
study.free()
host.queue_free()