Add real session-history navigation (Previous/Next/Shuffle), compact
side panels, and a segmented progress row to match the mockup layout.
Flashcards now opens straight into Study, with library management
moved into the Options menu ("Manage Flashcards") and Export hidden
from the UI (Import stays). Shuffle is backed by a real, due-before-new
randomized pick on the backend, not a decorative toggle.
153 lines
7.4 KiB
GDScript
153 lines
7.4 KiB
GDScript
extends SceneTree
|
|
|
|
# Flashcards previously had no coverage for theme switching or narrow-viewport layout: the
|
|
# library screen's `_refresh_status()` and the study screen's `refresh_theme_controls()` were
|
|
# both no-op stubs, and neither screen reacted to its own resize. This drives both screens
|
|
# through a dark/light toggle and a phone-width resize with no network calls (mirrors
|
|
# flashcard_study_render_test.gd: content is injected directly rather than fetched).
|
|
|
|
const Brand := preload("res://scripts/ui/medmind_style.gd")
|
|
|
|
var failures := 0
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
var host := Control.new()
|
|
host.size = Vector2(1280, 800)
|
|
root.add_child(host)
|
|
|
|
var theme_service = root.get_node("UIThemeService")
|
|
var prior_dark: bool = theme_service.is_dark_mode()
|
|
|
|
var Hub := load("res://scripts/ui/flashcards_hub.gd")
|
|
var hub = Hub.new()
|
|
hub.size = host.size
|
|
host.add_child(hub)
|
|
await process_frame
|
|
|
|
hub.notes = [{"id": "note-1", "type": "basic", "prompt": "2 + 2", "answer": "4", "tags": []}]
|
|
|
|
theme_service.set_dark_mode(true)
|
|
hub._render_notes()
|
|
await process_frame
|
|
var dark_list_box := hub.list_card.get_theme_stylebox("panel") as StyleBoxFlat
|
|
_assert(dark_list_box.bg_color.is_equal_approx(Color("081426")),
|
|
"the flashcards list card should use the dark surface while dark mode is active")
|
|
var dark_row := hub.list_container.get_child(0) as BoxContainer
|
|
var dark_icon_box := (dark_row.get_child(0) as PanelContainer).get_theme_stylebox("panel") as StyleBoxFlat
|
|
_assert(dark_icon_box.bg_color.is_equal_approx(Brand.icon_circle_box(true, Brand.ROYAL_BLUE, 20).bg_color),
|
|
"a basic card's type icon should use its dark accent styling")
|
|
|
|
theme_service.set_dark_mode(false)
|
|
await process_frame
|
|
var light_list_box := hub.list_card.get_theme_stylebox("panel") as StyleBoxFlat
|
|
_assert(light_list_box.bg_color.is_equal_approx(Color.WHITE),
|
|
"toggling the app theme should rebuild the flashcards list card with the light surface (the library screen's theme_changed handler was previously a no-op)")
|
|
var light_row := hub.list_container.get_child(0) as BoxContainer
|
|
var light_icon_box := (light_row.get_child(0) as PanelContainer).get_theme_stylebox("panel") as StyleBoxFlat
|
|
_assert(light_icon_box.bg_color.is_equal_approx(Brand.icon_circle_box(false, Brand.ROYAL_BLUE, 20).bg_color),
|
|
"a basic card's type icon should use its light accent styling")
|
|
var light_menu_box := hub.import_menu.get_theme_stylebox("panel") as StyleBoxFlat
|
|
_assert(light_menu_box.bg_color.is_equal_approx(Brand.panel_box(false).bg_color),
|
|
"the import menu panel should also restyle to the light surface after a theme toggle")
|
|
|
|
theme_service.set_dark_mode(prior_dark)
|
|
|
|
hub.size = Vector2(360, 800)
|
|
hub._update_responsive_layout()
|
|
await process_frame
|
|
_assert(hub.compact_layout, "the flashcards library should detect a phone-width viewport")
|
|
var compact_row := hub.list_container.get_child(0) as BoxContainer
|
|
_assert(compact_row.vertical, "phone-width flashcard rows should stack their content above the edit/archive actions instead of clipping")
|
|
_assert(compact_row.get_combined_minimum_size().x <= hub.size.x,
|
|
"a phone-width flashcard row should fit inside the viewport (minimum %.1f px)" % compact_row.get_combined_minimum_size().x)
|
|
|
|
hub.size = Vector2(1280, 800)
|
|
hub._update_responsive_layout()
|
|
await process_frame
|
|
_assert(not hub.compact_layout, "a wide viewport should restore the flashcards library's desktop row layout")
|
|
var wide_row := hub.list_container.get_child(0) as BoxContainer
|
|
_assert(not wide_row.vertical, "a wide viewport should lay flashcard row content out horizontally again")
|
|
|
|
hub.free()
|
|
|
|
var Study := load("res://scripts/ui/flashcards_study.gd")
|
|
var study = Study.new()
|
|
study._build()
|
|
|
|
theme_service.set_dark_mode(true)
|
|
study.refresh_theme_controls()
|
|
var dark_study_box := study.card_panel.get_theme_stylebox("panel") as StyleBoxFlat
|
|
_assert(dark_study_box.bg_color.is_equal_approx(Brand.content_card_box(true, true, Brand.ROYAL_BLUE).bg_color),
|
|
"the study card should use the dark active-card surface while dark mode is active")
|
|
|
|
theme_service.set_dark_mode(false)
|
|
study.refresh_theme_controls()
|
|
var light_study_box := study.card_panel.get_theme_stylebox("panel") as StyleBoxFlat
|
|
_assert(light_study_box.bg_color.is_equal_approx(Brand.content_card_box(false, true, Brand.ROYAL_BLUE).bg_color),
|
|
"toggling the app theme should restyle the study card to the light surface (refresh_theme_controls was previously a no-op)")
|
|
_assert(light_study_box.border_color.is_equal_approx(Brand.ROYAL_BLUE),
|
|
"the study card should keep its accent border after a theme refresh")
|
|
|
|
theme_service.set_dark_mode(prior_dark)
|
|
|
|
study.size = Vector2(360, 720)
|
|
study._update_responsive_layout()
|
|
_assert(study.compact_layout, "the study screen should detect a phone-width viewport")
|
|
_assert(study.card_panel.custom_minimum_size.x <= study.size.x,
|
|
"the study card should shrink to fit a phone-width viewport instead of overflowing (minimum %.1f px on a %.1f px viewport)" % [study.card_panel.custom_minimum_size.x, study.size.x])
|
|
|
|
study.size = Vector2(1280, 720)
|
|
study._update_responsive_layout()
|
|
_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()
|
|
await process_frame
|
|
quit(1 if failures > 0 else 0)
|
|
|
|
|
|
func _assert(condition: bool, message: String) -> void:
|
|
if condition:
|
|
return
|
|
failures += 1
|
|
push_error(message)
|