From 489b9c3d7c841efb135871d4ef2fca0cd1c0b81c Mon Sep 17 00:00:00 2001 From: hz Date: Thu, 23 Jul 2026 04:37:45 -0500 Subject: [PATCH] Fix text zoom not actually syncing, and expose it in Settings Two real gaps meant the account-synced zoom (from the previous "sync through the account" change) never reached the screen: _load_cache() updated AccountProfileService.preferences but never emitted profile_changed, so question_player.gd/ccs_service.gd (already built and listening before login even happens) never found out a cached value had loaded. And nothing forced a server refresh right after login - it only fired if the user happened to open the profile/progress screen, so a fresh device/session had no path to the true synced value at all. main.gd now calls AccountProfileService.refresh() once per login (not on every silent token refresh) so the real preferences GET always runs right after signing in, on any device. Also added Question bank/CCS case text size steppers to the Settings screen next to dark mode, per user request, reusing the same AccountProfileService/CCSService plumbing as the in-screen A-/A+ buttons. --- scripts/autoload/account_profile_service.gd | 2 + scripts/ui/main.gd | 7 +++ scripts/ui/settings_screen.gd | 70 +++++++++++++++++++++ tests/settings_screen_test.gd | 69 ++++++++++++++++++++ tests/settings_screen_test.gd.uid | 1 + 5 files changed, 149 insertions(+) create mode 100644 tests/settings_screen_test.gd create mode 100644 tests/settings_screen_test.gd.uid diff --git a/scripts/autoload/account_profile_service.gd b/scripts/autoload/account_profile_service.gd index 88756b0..6d27b9f 100644 --- a/scripts/autoload/account_profile_service.gd +++ b/scripts/autoload/account_profile_service.gd @@ -318,6 +318,7 @@ func _on_auth_changed() -> void: else: account = {} preferences = _default_preferences() + profile_changed.emit() func _cache_path() -> String: @@ -331,6 +332,7 @@ func _load_cache() -> void: if typeof(parsed) == TYPE_DICTIONARY: preferences = parsed.get("preferences", _default_preferences()) preferences_pending = bool(parsed.get("pending", false)) + profile_changed.emit() func _save_cache(pending: bool) -> void: diff --git a/scripts/ui/main.gd b/scripts/ui/main.gd index 33a6a58..369c05d 100644 --- a/scripts/ui/main.gd +++ b/scripts/ui/main.gd @@ -5,6 +5,7 @@ const Brand := preload("res://scripts/ui/medmind_style.gd") const NavigationRoutes := preload("res://scripts/ui/navigation_routes.gd") var screens := {} +var _account_profile_synced := false var header_panel: PanelContainer var header_title: Label var environment_badge: Label @@ -755,6 +756,12 @@ func open_legal_document(kind: String) -> void: func _on_auth_changed() -> void: _refresh_header_controls() + if AuthState.is_logged_in: + if not _account_profile_synced: + _account_profile_synced = true + AccountProfileService.refresh() + else: + _account_profile_synced = false if AuthState.is_logged_in and AuthState.requires_legal_acceptance and current_screen_name not in ["login", "legal_consent"]: show_screen("legal_consent") diff --git a/scripts/ui/settings_screen.gd b/scripts/ui/settings_screen.gd index 607f545..7de9f93 100644 --- a/scripts/ui/settings_screen.gd +++ b/scripts/ui/settings_screen.gd @@ -1,10 +1,15 @@ extends VBoxContainer const Brand := preload("res://scripts/ui/medmind_style.gd") +const QUESTION_TEXT_SCALE_MIN := 0.85 +const QUESTION_TEXT_SCALE_MAX := 1.35 +const QUESTION_TEXT_SCALE_STEP := 0.1 var main: Control var theme_toggle: CheckButton var sound_toggle: CheckButton +var question_scale_value_label: Label +var ccs_scale_value_label: Label func _ready() -> void: @@ -52,6 +57,9 @@ func _ready() -> void: sound_toggle.toggled.connect(UISoundService.set_sounds_enabled) content.add_child(sound_toggle) + content.add_child(_build_text_scale_row("Question bank text size", "question")) + content.add_child(_build_text_scale_row("CCS case text size", "ccs")) + var legal_panel := PanelContainer.new() legal_panel.custom_minimum_size = Vector2(520, 0) legal_panel.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN @@ -74,11 +82,73 @@ func _ready() -> void: legal_content.add_child(button) UIThemeService.theme_changed.connect(_on_theme_changed) + AccountProfileService.profile_changed.connect(_refresh_text_scale_labels) + CCSService.text_scale_changed.connect(_refresh_text_scale_labels) + _refresh_text_scale_labels() func on_show(_payload: Variant = null) -> void: theme_toggle.set_pressed_no_signal(UIThemeService.is_dark_mode()) sound_toggle.set_pressed_no_signal(UISoundService.are_sounds_enabled()) + _refresh_text_scale_labels() + + +func _build_text_scale_row(label_text: String, kind: String) -> HBoxContainer: + var row := HBoxContainer.new() + row.custom_minimum_size = Vector2(0, 52) + row.add_theme_constant_override("separation", 12) + + var label := Label.new() + label.text = label_text + label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + row.add_child(label) + + var smaller := Button.new() + smaller.text = "A-" + smaller.tooltip_text = "Decrease text size" + smaller.custom_minimum_size = Vector2(44, 44) + row.add_child(smaller) + + var value_label := Label.new() + value_label.custom_minimum_size = Vector2(56, 0) + value_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + row.add_child(value_label) + + var larger := Button.new() + larger.text = "A+" + larger.tooltip_text = "Increase text size" + larger.custom_minimum_size = Vector2(44, 44) + row.add_child(larger) + + if kind == "question": + question_scale_value_label = value_label + smaller.pressed.connect(_adjust_question_text_scale.bind(-QUESTION_TEXT_SCALE_STEP)) + larger.pressed.connect(_adjust_question_text_scale.bind(QUESTION_TEXT_SCALE_STEP)) + else: + ccs_scale_value_label = value_label + smaller.pressed.connect(_adjust_ccs_text_scale.bind(-CCSService.TEXT_SCALE_STEP)) + larger.pressed.connect(_adjust_ccs_text_scale.bind(CCSService.TEXT_SCALE_STEP)) + return row + + +func _adjust_question_text_scale(delta: float) -> void: + var current: float = AccountProfileService.preferences.get("questionTextScale", 1.0) + var next_scale: float = clamp(current + delta, QUESTION_TEXT_SCALE_MIN, QUESTION_TEXT_SCALE_MAX) + AccountProfileService.update_text_scale(next_scale) + _refresh_text_scale_labels() + + +func _adjust_ccs_text_scale(delta: float) -> void: + CCSService.adjust_text_scale(delta) + _refresh_text_scale_labels() + + +func _refresh_text_scale_labels() -> void: + if question_scale_value_label != null: + var question_scale: float = AccountProfileService.preferences.get("questionTextScale", 1.0) + question_scale_value_label.text = "%d%%" % round(question_scale * 100) + if ccs_scale_value_label != null: + ccs_scale_value_label.text = "%d%%" % round(CCSService.text_scale * 100) func _on_theme_toggled(enabled: bool) -> void: diff --git a/tests/settings_screen_test.gd b/tests/settings_screen_test.gd new file mode 100644 index 0000000..95c178f --- /dev/null +++ b/tests/settings_screen_test.gd @@ -0,0 +1,69 @@ +extends SceneTree + +var failures := 0 + + +func _initialize() -> void: + call_deferred("_run") + + +func _run() -> void: + var account_profile = root.get_node("AccountProfileService") + var ccs_service = root.get_node("CCSService") + var prior_preferences: Dictionary = account_profile.preferences.duplicate(true) + var prior_ccs_scale: float = ccs_service.text_scale + account_profile.preferences["questionTextScale"] = 1.0 + ccs_service.text_scale = 1.0 + + var host := Control.new() + root.add_child(host) + var screen = load("res://scripts/ui/settings_screen.gd").new() + screen.main = host + host.add_child(screen) + await process_frame + + _assert(screen.question_scale_value_label != null, "settings should expose a question bank text size control") + _assert(screen.ccs_scale_value_label != null, "settings should expose a CCS case text size control") + _assert(screen.question_scale_value_label.text == "100%", "question bank text size should start at 100%") + _assert(screen.ccs_scale_value_label.text == "100%", "CCS case text size should start at 100%") + + _assert(_find_button(screen, "A+", 0) != null, "question bank row should have an A+ button") + _assert(_find_button(screen, "A+", 1) != null, "CCS row should have an A+ button") + + var prior_profile_api_url: String = ProjectSettings.get_setting("step3/profile_api_url", "") + ProjectSettings.set_setting("step3/profile_api_url", "http://127.0.0.1:1") + + # Call the handlers directly rather than emitting Button.pressed - the real + # signal path also fires UISoundService's click sound, whose OGG playback + # doesn't release cleanly before this short-lived headless process exits. + screen._adjust_question_text_scale(0.1) + _assert(is_equal_approx(float(account_profile.preferences.get("questionTextScale", 1.0)), 1.1), "question bank A+ should increase the synced scale") + _assert(screen.question_scale_value_label.text == "110%", "question bank text size label should reflect the new scale") + + screen._adjust_ccs_text_scale(0.1) + _assert(is_equal_approx(ccs_service.text_scale, 1.1), "CCS A+ should increase the shared CCS text scale") + _assert(screen.ccs_scale_value_label.text == "110%", "CCS text size label should reflect the new scale") + + ProjectSettings.set_setting("step3/profile_api_url", prior_profile_api_url) + host.queue_free() + await process_frame + account_profile.preferences = prior_preferences + ccs_service.text_scale = prior_ccs_scale + quit(1 if failures > 0 else 0) + + +func _find_button(root_node: Node, text: String, occurrence: int) -> Button: + var found := 0 + for node in root_node.find_children("*", "Button", true, false): + if node.text == text: + if found == occurrence: + return node + found += 1 + return null + + +func _assert(condition: bool, message: String) -> void: + if condition: + return + failures += 1 + push_error(message) diff --git a/tests/settings_screen_test.gd.uid b/tests/settings_screen_test.gd.uid new file mode 100644 index 0000000..8aedcff --- /dev/null +++ b/tests/settings_screen_test.gd.uid @@ -0,0 +1 @@ +uid://dysbx2e58xwm8