Sync text zoom preference through the account instead of local settings

Local user://settings.cfg meant zoom level didn't follow a student
across devices/browsers. Question bank and CCS text scale now live on
the user's study profile in Mongo (QuestionTextScale/CcsTextScale),
synced through a new PUT /api/profile/me/text-scale endpoint and the
existing preferences GET/cache already used for study preferences.

Both question_player.gd and ccs_service.gd read the synced value at
startup and react to AccountProfileService.profile_changed so a value
pulled down after login (or changed from another session) applies
without a restart. Backend validates the 0.85-1.35 range and updates
only the field(s) provided via a targeted $set, so it can't clobber
the other scale or the unrelated study-profile preferences.
This commit is contained in:
hz
2026-07-23 03:59:02 -05:00
parent b88f38f29b
commit d69a1fd0e3
10 changed files with 124 additions and 36 deletions
+19 -2
View File
@@ -84,6 +84,20 @@ func update_preferences(target_exam_date: String, preparation_stage: String, wee
_request_profile("update_preferences", HTTPClient.METHOD_PUT, "/me/preferences", preferences)
func update_text_scale(question_text_scale: Variant = null, ccs_text_scale: Variant = null) -> void:
var payload := {}
if question_text_scale != null:
payload["questionTextScale"] = question_text_scale
preferences["questionTextScale"] = question_text_scale
if ccs_text_scale != null:
payload["ccsTextScale"] = ccs_text_scale
preferences["ccsTextScale"] = ccs_text_scale
if payload.is_empty():
return
_save_cache(preferences_pending)
_request_profile("update_text_scale", HTTPClient.METHOD_PUT, "/me/text-scale", payload)
func change_password(current_password: String, new_password: String) -> void:
_request_auth("change_password", HTTPClient.METHOD_POST, "/me/change-password", {
"currentPassword": current_password, "newPassword": new_password
@@ -192,7 +206,7 @@ func _on_profile_completed(result: int, response_code: int, _headers: PackedStri
if typeof(parsed) != TYPE_DICTIONARY:
request_failed.emit("The profile service returned invalid data.")
return
if operation in ["preferences", "update_preferences"]:
if operation in ["preferences", "update_preferences", "update_text_scale"]:
preferences = parsed
preferences_pending = false
_save_cache(false)
@@ -345,4 +359,7 @@ func _error_message(body: PackedByteArray, fallback: String) -> String:
static func _default_preferences() -> Dictionary:
return {"targetExamDate": null, "preparationStage": "pre_dedicated", "weeklyGoalHours": 10, "preferredStudyMode": "mixed"}
return {
"targetExamDate": null, "preparationStage": "pre_dedicated", "weeklyGoalHours": 10, "preferredStudyMode": "mixed",
"questionTextScale": 1.0, "ccsTextScale": 1.0
}
+17 -17
View File
@@ -13,13 +13,11 @@ signal attempt_abandoned
signal report_submitted(report_id: String)
signal report_failed(message: String)
signal request_failed(message: String)
signal text_scale_changed
const TEXT_SCALE_MIN := 0.85
const TEXT_SCALE_MAX := 1.35
const TEXT_SCALE_STEP := 0.1
const TEXT_SCALE_SETTINGS_PATH := "user://settings.cfg"
const TEXT_SCALE_SETTINGS_SECTION := "study"
const TEXT_SCALE_SETTINGS_KEY := "ccs_text_scale"
var api_url := DEFAULT_API_URL
var request: HTTPRequest
@@ -40,11 +38,16 @@ var request_access_token := ""
func _ready() -> void:
api_url = EndpointConfig.resolve(str(ProjectSettings.get_setting("step3/ccs_api_url", DEFAULT_API_URL)), "/api/ccs")
text_scale = _load_text_scale_preference()
request = HTTPRequest.new()
request.request_completed.connect(_on_request_completed)
add_child(request)
AuthState.auth_changed.connect(_on_auth_changed)
call_deferred("_connect_account_profile_service")
func _connect_account_profile_service() -> void:
text_scale = _synced_text_scale()
AccountProfileService.profile_changed.connect(_on_account_profile_changed)
func load_cases() -> void:
@@ -104,23 +107,20 @@ func submit_report(attempt_id: String, category: String, details: String) -> voi
func adjust_text_scale(delta: float) -> void:
text_scale = clampf(text_scale + delta, TEXT_SCALE_MIN, TEXT_SCALE_MAX)
_save_text_scale_preference(text_scale)
AccountProfileService.update_text_scale(null, text_scale)
func _load_text_scale_preference() -> float:
var config := ConfigFile.new()
if config.load(TEXT_SCALE_SETTINGS_PATH) == OK:
var saved: float = config.get_value(TEXT_SCALE_SETTINGS_SECTION, TEXT_SCALE_SETTINGS_KEY, 1.0)
return clampf(saved, TEXT_SCALE_MIN, TEXT_SCALE_MAX)
return 1.0
func _synced_text_scale() -> float:
var saved: float = AccountProfileService.preferences.get("ccsTextScale", 1.0)
return clampf(saved, TEXT_SCALE_MIN, TEXT_SCALE_MAX)
func _save_text_scale_preference(value: float) -> void:
var config := ConfigFile.new()
config.load(TEXT_SCALE_SETTINGS_PATH)
config.set_value(TEXT_SCALE_SETTINGS_SECTION, TEXT_SCALE_SETTINGS_KEY, value)
if config.save(TEXT_SCALE_SETTINGS_PATH) != OK:
push_warning("Unable to save CCS text scale preference.")
func _on_account_profile_changed() -> void:
var synced := _synced_text_scale()
if is_equal_approx(synced, text_scale):
return
text_scale = synced
text_scale_changed.emit()
func toggle_highlight(attempt_id: String, region_id: String, start: int, end: int) -> void:
+2
View File
@@ -58,6 +58,8 @@ func _ready() -> void:
resized.connect(_update_responsive_layout)
if not theme_service.theme_changed.is_connected(_on_theme_changed):
theme_service.theme_changed.connect(_on_theme_changed)
if not CCSService.text_scale_changed.is_connected(_apply_text_scale):
CCSService.text_scale_changed.connect(_apply_text_scale)
_apply_theme()
call_deferred("_update_responsive_layout")
+1
View File
@@ -40,6 +40,7 @@ func _ready() -> void:
status = Label.new(); Style.set_label(status, 14, Style.RED); row.add_child(status)
CCSService.request_failed.connect(_request_failed)
if not theme_service.theme_changed.is_connected(_on_theme_changed): theme_service.theme_changed.connect(_on_theme_changed)
if not CCSService.text_scale_changed.is_connected(_apply_text_scale): CCSService.text_scale_changed.connect(_apply_text_scale)
func _study_label(region: String) -> RichTextLabel:
var label := RichTextLabel.new(); label.bbcode_enabled = true; label.fit_content = true; label.selection_enabled = true; label.set_meta("ccs_region", region); label.gui_input.connect(_text_input.bind(label)); return label
+15 -17
View File
@@ -18,9 +18,6 @@ const CORRECT_ADVANCE_DELAY_SECONDS := 1.25
const TEXT_SCALE_MIN := 0.85
const TEXT_SCALE_MAX := 1.35
const TEXT_SCALE_STEP := 0.1
const TEXT_SCALE_SETTINGS_PATH := "user://settings.cfg"
const TEXT_SCALE_SETTINGS_SECTION := "study"
const TEXT_SCALE_SETTINGS_KEY := "question_text_scale"
const REPORT_DETAILS_MIN := 10
const REPORT_DETAILS_MAX := 2000
const MEDIA_ZOOM_MIN := 0.25
@@ -149,7 +146,9 @@ var shake_origin := Vector2.ZERO
func _ready() -> void:
theme_service = get_node("/root/UIThemeService")
question_text_scale = _load_text_scale_preference()
question_text_scale = _synced_question_text_scale()
if not AccountProfileService.profile_changed.is_connected(_on_account_profile_changed):
AccountProfileService.profile_changed.connect(_on_account_profile_changed)
set_anchors_preset(Control.PRESET_FULL_RECT)
add_theme_constant_override("separation", 10)
if not QuestionBankService.choice_percentages_updated.is_connected(_on_choice_percentages_updated):
@@ -2125,26 +2124,25 @@ func _adjust_question_text_scale(delta: float) -> void:
if is_equal_approx(next_scale, question_text_scale):
return
question_text_scale = next_scale
_save_text_scale_preference(next_scale)
AccountProfileService.update_text_scale(next_scale)
_refresh_scaled_fonts(self)
_render_stem()
_apply_saved_choice_state()
func _load_text_scale_preference() -> float:
var config := ConfigFile.new()
if config.load(TEXT_SCALE_SETTINGS_PATH) == OK:
var saved: float = config.get_value(TEXT_SCALE_SETTINGS_SECTION, TEXT_SCALE_SETTINGS_KEY, 1.0)
return clamp(saved, TEXT_SCALE_MIN, TEXT_SCALE_MAX)
return 1.0
func _synced_question_text_scale() -> float:
var saved: float = AccountProfileService.preferences.get("questionTextScale", 1.0)
return clamp(saved, TEXT_SCALE_MIN, TEXT_SCALE_MAX)
func _save_text_scale_preference(value: float) -> void:
var config := ConfigFile.new()
config.load(TEXT_SCALE_SETTINGS_PATH)
config.set_value(TEXT_SCALE_SETTINGS_SECTION, TEXT_SCALE_SETTINGS_KEY, value)
if config.save(TEXT_SCALE_SETTINGS_PATH) != OK:
push_warning("Unable to save question text scale preference.")
func _on_account_profile_changed() -> void:
var synced := _synced_question_text_scale()
if is_equal_approx(synced, question_text_scale):
return
question_text_scale = synced
_refresh_scaled_fonts(self)
_render_stem()
_apply_saved_choice_state()
func _set_scaled_label(label: Label, base_size: int, color: Color = Style.TEXT) -> void: