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