extends Control const Brand := preload("res://scripts/ui/medmind_style.gd") const FeatureFlags := preload("res://scripts/config/feature_flags.gd") var main: Control var scroll: ScrollContainer var page: VBoxContainer var columns: GridContainer var profile_name: Label var profile_identity: Label var profile_meta: Label var identity_values: Dictionary = {} var metric_values: Dictionary = {} var systems_list: VBoxContainer var status_label: Label var outcome_score: SpinBox var outcome_date: LineEdit var outcome_consent: CheckBox var outcome_status: Label var outcome_delete: Button var connected_accounts_list: VBoxContainer var return_screen := "" var back_to_session_button: Button var progression_content: VBoxContainer func _ready() -> void: set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) AccountProfileService.profile_changed.connect(_render) AccountProfileService.request_failed.connect(_show_error) AccountProfileService.preferences_saved.connect(func(): _show_saved("Study identity saved.")) AccountProfileService.archive_saved.connect(func(path: String): _show_saved("Archive saved: %s" % path)) AccountProfileService.external_link_started.connect(func(provider: String): _show_saved("Complete %s connection in the opened window." % provider.capitalize())) AccountProfileService.external_linked.connect(func(provider: String): _show_saved("%s account connected." % provider.capitalize())) DashboardService.dashboard_changed.connect(_render) DashboardService.dashboard_failed.connect(_show_error) ExamSimulationService.outcome_changed.connect(_render_outcome) ExamSimulationService.request_failed.connect(_show_error) UIThemeService.theme_changed.connect(_rebuild) resized.connect(_update_responsive) _build() func on_show(payload: Variant = null) -> void: var wrapper: Dictionary = payload if typeof(payload) == TYPE_DICTIONARY else {} return_screen = str(wrapper.get("return_screen", "")) _refresh_back_to_session_visibility() _render() AccountProfileService.refresh() DashboardService.refresh() CCSService.load_rating() ExamSimulationService.load_outcome() func _build() -> void: for child in get_children(): remove_child(child) child.free() identity_values.clear() metric_values.clear() scroll = ScrollContainer.new() scroll.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED add_child(scroll) page = VBoxContainer.new() page.size_flags_horizontal = Control.SIZE_EXPAND_FILL page.add_theme_constant_override("separation", 14) scroll.add_child(page) status_label = _label("", 13, Brand.muted_color(_dark())) status_label.visible = false page.add_child(status_label) back_to_session_button = _button("Back to session", "", true) back_to_session_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN UISoundService.set_button_sound(back_to_session_button, "back") back_to_session_button.pressed.connect(_return_to_session) page.add_child(back_to_session_button) columns = GridContainer.new() columns.columns = 2 columns.size_flags_horizontal = Control.SIZE_EXPAND_FILL columns.add_theme_constant_override("h_separation", 14) columns.add_theme_constant_override("v_separation", 14) page.add_child(columns) var left := VBoxContainer.new() left.size_flags_horizontal = Control.SIZE_EXPAND_FILL left.add_theme_constant_override("separation", 14) columns.add_child(left) left.add_child(_profile_summary_card()) left.add_child(_study_identity_card()) left.add_child(_performance_card()) left.add_child(_systems_card()) var right := VBoxContainer.new() right.custom_minimum_size.x = 410 right.size_flags_horizontal = Control.SIZE_EXPAND_FILL right.add_theme_constant_override("separation", 14) columns.add_child(right) right.add_child(_outcome_card()) progression_content = null if FeatureFlags.progression_enabled(): right.add_child(_progression_card()) right.add_child(_connected_accounts_card()) right.add_child(_account_actions_card()) _refresh_back_to_session_visibility() _update_responsive() _render() func _profile_summary_card() -> PanelContainer: var body := VBoxContainer.new() body.add_theme_constant_override("separation", 14) body.add_child(_section_header("Profile Summary", "Your RanaZe account")) var row := HBoxContainer.new() row.add_theme_constant_override("separation", 18) var avatar := PanelContainer.new() avatar.custom_minimum_size = Vector2(74, 74) avatar.add_theme_stylebox_override("panel", Brand.icon_circle_box(_dark(), Brand.PURPLE, 37, true)) var avatar_icon := TextureRect.new() avatar_icon.texture = load("res://assets/icons/lucide/user-round.svg") avatar_icon.custom_minimum_size = Vector2(34, 34) avatar_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE avatar_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED avatar.add_child(avatar_icon) row.add_child(avatar) var copy := VBoxContainer.new() copy.size_flags_horizontal = Control.SIZE_EXPAND_FILL profile_name = _label("Loading profile...", 22, Brand.text_color(_dark()), true) copy.add_child(profile_name) profile_identity = _label("", 14, Brand.muted_color(_dark())) copy.add_child(profile_identity) profile_meta = _label("", 13, Brand.muted_color(_dark())) copy.add_child(profile_meta) row.add_child(copy) var edit := _button("Edit profile", "pencil", false) edit.size_flags_vertical = Control.SIZE_SHRINK_CENTER edit.pressed.connect(_open_profile_editor) row.add_child(edit) body.add_child(row) return _card(body) func _study_identity_card() -> PanelContainer: var body := VBoxContainer.new() body.add_theme_constant_override("separation", 12) var header := _header_with_action("Study Identity", "Your preparation plan", "Edit", _open_study_editor) body.add_child(header) var grid := GridContainer.new() grid.columns = 2 grid.add_theme_constant_override("h_separation", 20) grid.add_theme_constant_override("v_separation", 12) for item in [ ["exam", "Target exam date"], ["stage", "Current stage"], ["goal", "Weekly study goal"], ["mode", "Preferred study mode"] ]: var box := VBoxContainer.new() box.size_flags_horizontal = Control.SIZE_EXPAND_FILL box.add_child(_label(item[1], 12, Brand.muted_color(_dark()))) var value := _label("--", 15, Brand.text_color(_dark()), true) identity_values[item[0]] = value box.add_child(value) grid.add_child(box) body.add_child(grid) return _card(body) func _performance_card() -> PanelContainer: var body := VBoxContainer.new() body.add_theme_constant_override("separation", 12) body.add_child(_section_header("Performance Summary", "Your cumulative RanaZe activity")) var grid := GridContainer.new() grid.columns = 3 grid.add_theme_constant_override("h_separation", 10) grid.add_theme_constant_override("v_separation", 10) for item in [ ["qbank", "Question Bank rating", Brand.ROYAL_BLUE], ["ccs", "CCS rating", Brand.PURPLE], ["accuracy", "First-attempt accuracy", Brand.TEAL], ["questions", "Questions completed", Brand.ROYAL_BLUE], ["cases", "CCS cases completed", Brand.PURPLE], ["streak", "Current streak", Brand.AMBER] ]: var metric := VBoxContainer.new() metric.size_flags_horizontal = Control.SIZE_EXPAND_FILL metric.add_child(_label(item[1], 11, Brand.muted_color(_dark()))) var value := _label("--", 23, item[2], true) metric_values[item[0]] = value metric.add_child(value) grid.add_child(metric) body.add_child(grid) return _card(body) func _systems_card() -> PanelContainer: var body := VBoxContainer.new() body.add_theme_constant_override("separation", 10) var header := _header_with_action("System Performance", "Accuracy by Step 3 system", "View analytics", func(): main.show_screen("dashboard")) body.add_child(header) systems_list = VBoxContainer.new() systems_list.add_theme_constant_override("separation", 8) body.add_child(systems_list) return _card(body) func _outcome_card() -> PanelContainer: var body := VBoxContainer.new() body.add_theme_constant_override("separation", 10) body.add_child(_section_header("Official Step 3 Outcome", "Optionally help calibrate future predictions")) outcome_score = SpinBox.new() outcome_score.min_value = 1 outcome_score.max_value = 300 outcome_score.value = 200 outcome_score.custom_minimum_size.y = 44 body.add_child(_field("Official score", outcome_score)) outcome_date = LineEdit.new() outcome_date.placeholder_text = "YYYY-MM-DD" _style_field(outcome_date) body.add_child(_field("Exam date", outcome_date)) outcome_consent = CheckBox.new() outcome_consent.text = "I consent to de-identified calibration use" body.add_child(outcome_consent) var row := HBoxContainer.new() row.add_theme_constant_override("separation", 8) var save := _button("Save outcome", "circle-check", true) save.size_flags_horizontal = Control.SIZE_EXPAND_FILL save.pressed.connect(_save_outcome) row.add_child(save) outcome_delete = _button("Delete", "x", false) outcome_delete.pressed.connect(ExamSimulationService.delete_outcome) row.add_child(outcome_delete) body.add_child(row) outcome_status = _label("No official outcome is stored.", 12, Brand.muted_color(_dark())) outcome_status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART body.add_child(outcome_status) return _card(body) func _progression_card() -> PanelContainer: progression_content = VBoxContainer.new() progression_content.add_theme_constant_override("separation", 10) progression_content.add_child(_section_header("Clinical Progression", "Breadth, challenge, and retention")) return _card(progression_content) func _account_actions_card() -> PanelContainer: var body := VBoxContainer.new() body.add_theme_constant_override("separation", 9) body.add_child(_section_header("Account Actions", "Security and data controls")) var password := _action_button("Change password", "settings") password.pressed.connect(_open_password_dialog) body.add_child(password) var export := _action_button("Export my data", "copy") export.pressed.connect(func(): AccountProfileService.export_archive()) body.add_child(export) var delete := _action_button("Delete account permanently", "x", Brand.MAGENTA) delete.pressed.connect(_open_delete_dialog) body.add_child(delete) return _card(body) func _connected_accounts_card() -> PanelContainer: var body := VBoxContainer.new() body.add_theme_constant_override("separation", 9) body.add_child(_section_header("Connected Accounts", "Use another trusted identity to sign in")) connected_accounts_list = VBoxContainer.new() connected_accounts_list.add_theme_constant_override("separation", 8) body.add_child(connected_accounts_list) return _card(body) func _render() -> void: if profile_name == null: return var account: Dictionary = AccountProfileService.account var prefs: Dictionary = AccountProfileService.preferences var username := string_field(account, "username", AuthState.username) var display_name := string_field(account, "displayName").strip_edges() profile_name.text = username if display_name.is_empty() else display_name var email := string_field(account, "email").strip_edges() profile_identity.text = "@%s" % username if email.is_empty() else "@%s | %s" % [username, email] var role := string_field(account, "role", AuthState.role).capitalize() var created := _date_only(string_field(account, "createdUtc")) profile_meta.text = role if created.is_empty() else "%s | Member since %s" % [role, created] _render_connected_accounts(account.get("connectedProviders", []), account.get("availableProviders", [])) (identity_values["exam"] as Label).text = _date_only(string_field(prefs, "targetExamDate"), "Not scheduled") (identity_values["stage"] as Label).text = _friendly(string_field(prefs, "preparationStage", "pre_dedicated")) (identity_values["goal"] as Label).text = "%d hours / week" % int(prefs.get("weeklyGoalHours", 10)) (identity_values["mode"] as Label).text = _friendly(string_field(prefs, "preferredStudyMode", "mixed")) var data: Dictionary = DashboardService.data _set_metric("qbank", str(int(data.get("questionBankRating", QuestionBankService.overall_elo)))) _set_metric("ccs", str(int(data.get("ccsRating", CCSService.rating.get("elo", 1000))))) _set_metric("accuracy", "%d%%" % int(data.get("firstAttemptAccuracy", 0))) _set_metric("questions", str(int(data.get("questionsCompleted", 0)))) _set_metric("cases", str(int(data.get("ccsCasesCompleted", 0)))) _set_metric("streak", "%d days" % int(data.get("currentStreak", 0))) _render_systems(data.get("systems", [])) if FeatureFlags.progression_enabled(): _render_progression(data.get("progression", {})) _render_outcome() func _render_progression(value: Variant) -> void: if progression_content == null: return _clear(progression_content) progression_content.add_child(_section_header("Clinical Progression", "Breadth, challenge, and retention")) var progression: Dictionary = value if typeof(value) == TYPE_DICTIONARY else {} if progression.is_empty(): progression_content.add_child(_label("Loading progression...", 13, Brand.muted_color(_dark()))) return progression_content.add_child(_label(str(progression.get("displayName", "Case Scout III")), 24, Brand.PURPLE, true)) var badges: Array = progression.get("earnedBadges", []) progression_content.add_child(_label("Earned: %s" % ", ".join(PackedStringArray(badges)), 12, Brand.muted_color(_dark()))) var ladder := GridContainer.new() ladder.columns = 3 ladder.add_theme_constant_override("h_separation", 7) ladder.add_theme_constant_override("v_separation", 7) var rank_names := ["Case Scout", "Pattern Hunter", "Diagnostic Tactician", "Management Strategist", "Clinical Vanguard"] var divisions := ["III", "II", "I"] var earned_ordinal := int(progression.get("rankOrdinal", 0)) var earned_dates: Dictionary = progression.get("earnedRanksUtc", {}) var ladder_data: Array = progression.get("ladder", []) for ordinal in 15: var earned := ordinal <= earned_ordinal var caption := "%s %s" % [rank_names[floori(float(ordinal) / 3.0)], divisions[ordinal % 3]] var date := str(earned_dates.get(str(ordinal), "")).left(10) if earned and not date.is_empty(): caption += "\n%s" % date var cell := _label(caption, 10, Brand.SUCCESS if earned else Brand.muted_color(_dark()), earned) cell.custom_minimum_size = Vector2(105, 38) cell.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER cell.vertical_alignment = VERTICAL_ALIGNMENT_CENTER if ordinal < ladder_data.size() and typeof(ladder_data[ordinal]) == TYPE_DICTIONARY: cell.tooltip_text = str((ladder_data[ordinal] as Dictionary).get("requirementSummary", "")) ladder.add_child(cell) progression_content.add_child(ladder) var next_name = progression.get("nextRankName") if next_name != null: progression_content.add_child(_label("Next: %s %s" % [str(next_name), str(progression.get("nextDivision", ""))], 14, Brand.text_color(_dark()), true)) for requirement_value in progression.get("nextRequirements", []): var requirement: Dictionary = requirement_value var row := HBoxContainer.new() var text := _label(str(requirement.get("label", "Requirement")), 11, Brand.text_color(_dark())) text.size_flags_horizontal = Control.SIZE_EXPAND_FILL row.add_child(text) row.add_child(_label("%d / %d" % [int(requirement.get("current", 0)), int(requirement.get("required", 0))], 11, Brand.SUCCESS if bool(requirement.get("complete", false)) else Brand.ROYAL_BLUE, true)) progression_content.add_child(row) progression_content.add_child(HSeparator.new()) progression_content.add_child(_label("Personal records", 15, Brand.text_color(_dark()), true)) for record_value in progression.get("records", []): var record: Dictionary = record_value var suffix := "%d%%" % int(record.get("value", 0)) if str(record.get("key", "")) == "timed_block" else str(int(record.get("value", 0))) var title := _label("%s %s" % [str(record.get("label", "Record")), suffix], 12, Brand.text_color(_dark()), true) title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART progression_content.add_child(title) var definition := str(record.get("definition", "")) if bool(record.get("trackingFromLaunch", false)): definition += " Tracking since %s." % str(progression.get("trackingStartedUtc", "")).left(10) var detail := _label(definition, 10, Brand.muted_color(_dark())) detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART progression_content.add_child(detail) func _render_connected_accounts(provider_values: Variant, available_values: Variant) -> void: if connected_accounts_list == null: return _clear(connected_accounts_list) var connected: Dictionary = {} var available: Dictionary = {} if typeof(provider_values) == TYPE_ARRAY: for value in provider_values: connected[str(value).to_lower()] = true if typeof(available_values) == TYPE_ARRAY: for value in available_values: available[str(value).to_lower()] = true for provider in [["steam", "Steam"], ["google", "Google"]]: var provider_id := str(provider[0]) var provider_name := str(provider[1]) if not connected.has(provider_id) and not available.has(provider_id): continue var row := HBoxContainer.new() row.add_theme_constant_override("separation", 10) var copy := VBoxContainer.new() copy.size_flags_horizontal = Control.SIZE_EXPAND_FILL copy.add_child(_label(provider_name, 14, Brand.text_color(_dark()), true)) copy.add_child(_label("Connected" if connected.has(provider_id) else "Not connected", 12, Brand.SUCCESS if connected.has(provider_id) else Brand.muted_color(_dark()))) row.add_child(copy) if not connected.has(provider_id): var connect_button := _button("Connect", "link", false) connect_button.pressed.connect(AccountProfileService.connect_provider.bind(provider_id, provider_name)) row.add_child(connect_button) connected_accounts_list.add_child(row) func _render_systems(systems: Array = DashboardService.data.get("systems", [])) -> void: if systems_list == null: return _clear(systems_list) if systems.is_empty(): systems_list.add_child(_label("Complete questions to build your system breakdown.", 13, Brand.muted_color(_dark()))) return var sorted := systems.duplicate(true) sorted.sort_custom(func(a: Dictionary, b: Dictionary): return int(a.get("total", 0)) > int(b.get("total", 0))) for item in sorted.slice(0, mini(8, sorted.size())): var row := HBoxContainer.new() row.add_theme_constant_override("separation", 10) var title := _label(str(item.get("system", "General")), 13, Brand.text_color(_dark())) title.custom_minimum_size.x = 210 title.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS row.add_child(title) var bar := ProgressBar.new() bar.show_percentage = false bar.value = int(item.get("accuracy", 0)) bar.custom_minimum_size = Vector2(160, 7) bar.size_flags_horizontal = Control.SIZE_EXPAND_FILL bar.size_flags_vertical = Control.SIZE_SHRINK_CENTER _style_progress(bar, Brand.ROYAL_BLUE) row.add_child(bar) var detail := _label("%d%% %d / %d" % [int(item.get("accuracy", 0)), int(item.get("correct", 0)), int(item.get("total", 0))], 12, Brand.muted_color(_dark())) detail.custom_minimum_size.x = 110 row.add_child(detail) systems_list.add_child(row) func _render_outcome() -> void: if outcome_status == null: return var outcome: Dictionary = ExamSimulationService.outcome if outcome.is_empty(): outcome_status.text = "No official outcome is stored." outcome_delete.visible = false return outcome_score.value = int(outcome.get("officialScore", 200)) outcome_date.text = _date_only(string_field(outcome, "examDate")) outcome_consent.button_pressed = true outcome_status.text = "Saved score %d for %s." % [int(outcome_score.value), outcome_date.text] outcome_delete.visible = true func _open_profile_editor() -> void: var dialog := _dialog("Edit profile") var display := LineEdit.new() display.placeholder_text = "Display name" display.text = string_field(AccountProfileService.account, "displayName") _style_field(display) dialog.add_field(display) var email := LineEdit.new() email.placeholder_text = "Email address" email.text = string_field(AccountProfileService.account, "email") _style_field(email) dialog.add_field(email) dialog.confirmed.connect(func(): AccountProfileService.update_account(display.text, email.text)) add_child(dialog) dialog.popup_centered(Vector2i(520, 260)) func _open_study_editor() -> void: var prefs: Dictionary = AccountProfileService.preferences var dialog := _dialog("Edit study identity") var exam := LineEdit.new() exam.placeholder_text = "Target exam date: YYYY-MM-DD" exam.text = _date_only(string_field(prefs, "targetExamDate")) _style_field(exam) dialog.add_field(exam) var stage := OptionButton.new() _add_options(stage, [["Pre-dedicated", "pre_dedicated"], ["Dedicated", "dedicated"], ["Final review", "final_review"]], string_field(prefs, "preparationStage", "pre_dedicated")) dialog.add_field(stage) var goal := OptionButton.new() for hours in [5, 10, 15, 20, 25]: goal.add_item("%d hours per week" % hours, hours) _select_id(goal, int(prefs.get("weeklyGoalHours", 10))) dialog.add_field(goal) var mode := OptionButton.new() _add_options(mode, [["Mixed", "mixed"], ["Question Bank", "question_bank"], ["CCS", "ccs"], ["Exam simulation", "exam_simulation"]], string_field(prefs, "preferredStudyMode", "mixed")) dialog.add_field(mode) dialog.confirmed.connect(func(): AccountProfileService.update_preferences(exam.text, str(stage.get_item_metadata(stage.selected)), goal.get_selected_id(), str(mode.get_item_metadata(mode.selected)))) add_child(dialog) dialog.popup_centered(Vector2i(540, 390)) func _open_password_dialog() -> void: var dialog := _dialog("Change password") var current := _password_field("Current password") var replacement := _password_field("New password (8+ characters)") var confirmation := _password_field("Confirm new password") dialog.add_field(current) dialog.add_field(replacement) dialog.add_field(confirmation) dialog.confirmed.connect(func(): if replacement.text != confirmation.text or replacement.text.length() < 8: _show_error("New passwords must match and contain at least 8 characters.") return AccountProfileService.change_password(current.text, replacement.text)) add_child(dialog) dialog.popup_centered(Vector2i(520, 330)) func _open_delete_dialog() -> void: var dialog := _dialog("Permanently delete account") var warning := _label("This permanently removes your account and all RanaZe study data. This cannot be undone.", 13, Brand.MAGENTA) warning.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART dialog.add_field(warning) var password := _password_field("Current password") dialog.add_field(password) var confirmation := LineEdit.new() confirmation.placeholder_text = "Type DELETE" _style_field(confirmation) dialog.add_field(confirmation) dialog.confirmed.connect(func(): AccountProfileService.delete_account(password.text, confirmation.text)) add_child(dialog) dialog.popup_centered(Vector2i(540, 330)) func _save_outcome() -> void: var error := validate_outcome(int(outcome_score.value), outcome_date.text, outcome_consent.button_pressed) if not error.is_empty(): _show_error(error) return ExamSimulationService.save_outcome(int(outcome_score.value), outcome_date.text.strip_edges()) static func validate_outcome(score: int, exam_date: String, consent: bool) -> String: if score < 1 or score > 300: return "Enter an official score from 1 through 300." if RegEx.create_from_string("^\\d{4}-\\d{2}-\\d{2}$").search(exam_date.strip_edges()) == null: return "Enter the exam date as YYYY-MM-DD." if not consent: return "Consent is required before saving an outcome." return "" func _update_responsive() -> void: if columns != null: columns.columns = 1 if size.x < 1050.0 else 2 func _rebuild(_is_dark: bool) -> void: _build() func _show_error(message: String) -> void: status_label.text = message status_label.add_theme_color_override("font_color", Brand.MAGENTA) status_label.visible = true func _show_saved(message: String) -> void: status_label.text = message status_label.add_theme_color_override("font_color", Brand.SUCCESS) status_label.visible = true get_tree().create_timer(3.0).timeout.connect(func(): if is_instance_valid(status_label): status_label.visible = false) func _card(child: Control) -> PanelContainer: var panel := PanelContainer.new() panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL var box := Brand.content_card_box(_dark(), false) box.content_margin_left = 20 box.content_margin_right = 20 box.content_margin_top = 18 box.content_margin_bottom = 18 panel.add_theme_stylebox_override("panel", box) panel.add_child(child) return panel func _section_header(title: String, subtitle: String) -> VBoxContainer: var box := VBoxContainer.new() box.add_child(_label(title, 18, Brand.text_color(_dark()), true)) box.add_child(_label(subtitle, 12, Brand.muted_color(_dark()))) return box func _header_with_action(title: String, subtitle: String, action: String, callback: Callable) -> HBoxContainer: var row := HBoxContainer.new() var heading := _section_header(title, subtitle) heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL row.add_child(heading) var button := _button(action, "pencil", false) button.pressed.connect(callback) row.add_child(button) return row func _button(text: String, icon_name: String = "", primary: bool = false) -> Button: var button := Button.new() button.text = text button.custom_minimum_size = Vector2(0, 42) button.add_theme_font_override("font", Brand.font) button.add_theme_font_size_override("font_size", 13) if not icon_name.is_empty(): button.icon = load("res://assets/icons/lucide/%s.svg" % icon_name) button.add_theme_constant_override("icon_max_width", 17) if primary: button.add_theme_stylebox_override("normal", Brand.gradient_button_box()) button.add_theme_color_override("font_color", Color.WHITE) else: button.add_theme_stylebox_override("normal", Brand.secondary_button_box(_dark())) return button func _refresh_back_to_session_visibility() -> void: if back_to_session_button != null: back_to_session_button.visible = not return_screen.is_empty() func _return_to_session() -> void: if return_screen.is_empty(): return main.show_screen(return_screen) func _action_button(text: String, icon_name: String, color: Color = Color.TRANSPARENT) -> Button: var button := _button(text, icon_name, false) button.alignment = HORIZONTAL_ALIGNMENT_LEFT if color != Color.TRANSPARENT: button.add_theme_color_override("font_color", color) button.add_theme_color_override("icon_normal_color", color) return button func _label(text: String, font_size: int, color: Color, _bold: bool = false) -> Label: var label := Label.new() label.text = text label.add_theme_font_override("font", Brand.font) label.add_theme_font_size_override("font_size", font_size) label.add_theme_color_override("font_color", color) return label func _field(caption: String, control: Control) -> VBoxContainer: var box := VBoxContainer.new() box.add_child(_label(caption, 12, Brand.muted_color(_dark()))) box.add_child(control) return box func _dialog(title: String) -> ProfileDialog: var dialog := ProfileDialog.new() dialog.title = title dialog.ok_button_text = "Save" dialog.add_theme_font_override("font", Brand.font) dialog.fields.custom_minimum_size.x = 450 return dialog func _password_field(placeholder: String) -> LineEdit: var field := LineEdit.new() field.placeholder_text = placeholder field.secret = true field.secret_character = "•" _style_field(field) return field func _style_field(field: LineEdit) -> void: field.custom_minimum_size.y = 46 field.add_theme_font_override("font", Brand.font) field.add_theme_stylebox_override("normal", Brand.field_box(_dark())) field.add_theme_stylebox_override("focus", Brand.field_box(_dark(), true)) func _style_progress(bar: ProgressBar, accent: Color) -> void: var background := StyleBoxFlat.new() background.bg_color = Color("26324a") if _dark() else Color("e2e8f0") background.set_corner_radius_all(4) var fill := StyleBoxFlat.new() fill.bg_color = accent fill.set_corner_radius_all(4) bar.add_theme_stylebox_override("background", background) bar.add_theme_stylebox_override("fill", fill) func _set_metric(key: String, value: String) -> void: if metric_values.has(key): (metric_values[key] as Label).text = value func _clear(container: Container) -> void: for child in container.get_children(): container.remove_child(child) child.free() static func string_field(source: Dictionary, key: String, fallback: String = "") -> String: var value = source.get(key) return fallback if value == null else str(value) func _date_only(value: String, fallback: String = "") -> String: if value.is_empty(): return fallback return value.left(10) func _friendly(value: String) -> String: return value.replace("_", " ").capitalize() func _dark() -> bool: return UIThemeService.is_dark_mode() func _add_options(control: OptionButton, options: Array, selected_value: String) -> void: for option in options: control.add_item(str(option[0])) control.set_item_metadata(control.item_count - 1, option[1]) if str(option[1]) == selected_value: control.select(control.item_count - 1) func _select_id(control: OptionButton, id: int) -> void: for index in control.item_count: if control.get_item_id(index) == id: control.select(index) class ProfileDialog extends AcceptDialog: var fields: VBoxContainer func _init() -> void: fields = VBoxContainer.new() fields.add_theme_constant_override("separation", 10) add_child(fields) func add_field(node: Control) -> void: fields.add_child(node)