extends Control class_name ProfileUI ## Profile Page UI. ## ## Displays: ## - User avatar representation, username, and bio ## - Favorite experiences (worlds) list ## - Public/mutual friends list ## - Respects server-enforced privacy controls and blocked-user restricted view ## - Allows authenticated user to edit their own bio and privacy settings signal back_requested @onready var back_button: Button = $VBoxContainer/HeaderHBox/BackButton @onready var title_label: Label = $VBoxContainer/HeaderHBox/TitleLabel @onready var refresh_button: Button = $VBoxContainer/HeaderHBox/RefreshButton @onready var status_label: Label = $VBoxContainer/StatusMessageLabel # Lookup controls @onready var user_id_edit: LineEdit = $VBoxContainer/LookupHBox/UserIdEdit @onready var view_user_btn: Button = $VBoxContainer/LookupHBox/ViewUserButton @onready var view_my_profile_btn: Button = $VBoxContainer/LookupHBox/ViewMyProfileButton # Profile Card details @onready var username_label: Label = $VBoxContainer/Scroll/ContentVBox/CardPanel/Margin/HBox/DetailsVBox/UsernameLabel @onready var user_id_label: Label = $VBoxContainer/Scroll/ContentVBox/CardPanel/Margin/HBox/DetailsVBox/UserIdLabel @onready var avatar_preview_label: Label = $VBoxContainer/Scroll/ContentVBox/CardPanel/Margin/HBox/AvatarBox/AvatarLabel @onready var bio_edit: TextEdit = $VBoxContainer/Scroll/ContentVBox/CardPanel/Margin/HBox/DetailsVBox/BioEdit @onready var save_bio_btn: Button = $VBoxContainer/Scroll/ContentVBox/CardPanel/Margin/HBox/DetailsVBox/SaveBioButton # Privacy Controls (visible only for own profile) @onready var privacy_vbox: VBoxContainer = $VBoxContainer/Scroll/ContentVBox/PrivacyVBox @onready var profile_vis_opt: OptionButton = $VBoxContainer/Scroll/ContentVBox/PrivacyVBox/Grid/ProfileVisOption @onready var friends_vis_opt: OptionButton = $VBoxContainer/Scroll/ContentVBox/PrivacyVBox/Grid/FriendsVisOption @onready var favorites_vis_opt: OptionButton = $VBoxContainer/Scroll/ContentVBox/PrivacyVBox/Grid/FavoritesVisOption @onready var save_privacy_btn: Button = $VBoxContainer/Scroll/ContentVBox/PrivacyVBox/SavePrivacyButton # Favorites list @onready var favorites_container: VBoxContainer = $VBoxContainer/Scroll/ContentVBox/FavoritesVBox/FavoritesList @onready var favorites_status_label: Label = $VBoxContainer/Scroll/ContentVBox/FavoritesVBox/FavoritesStatusLabel # Friends list @onready var friends_container: VBoxContainer = $VBoxContainer/Scroll/ContentVBox/FriendsVBox/FriendsList @onready var friends_status_label: Label = $VBoxContainer/Scroll/ContentVBox/FriendsVBox/FriendsStatusLabel var _api_client: Node = null var _viewing_user_id: int = 0 var _my_user_id: int = 0 var _is_own_profile: bool = false func _ready() -> void: _api_client = _get_api() _setup_privacy_options() _wire_signals() if _api_client != null and _api_client.has_token(): _init_my_profile() func _get_api() -> Node: if has_node("/root/ApiClient"): return get_node("/root/ApiClient") return null func _setup_privacy_options() -> void: var visibilities := ["public", "friends_only", "private"] for opt in [profile_vis_opt, friends_vis_opt, favorites_vis_opt]: if opt: opt.clear() for v in visibilities: opt.add_item(v.capitalize()) func _wire_signals() -> void: if back_button: back_button.pressed.connect(_on_back_pressed) if refresh_button: refresh_button.pressed.connect(_refresh_current) if view_user_btn: view_user_btn.pressed.connect(_on_view_user_pressed) if view_my_profile_btn: view_my_profile_btn.pressed.connect(_init_my_profile) if save_bio_btn: save_bio_btn.pressed.connect(_on_save_bio_pressed) if save_privacy_btn: save_privacy_btn.pressed.connect(_on_save_privacy_pressed) func _on_back_pressed() -> void: back_requested.emit() if get_tree().current_scene == self: get_tree().change_scene_to_file("res://scenes/main_menu.tscn") func show_message(text: String, is_error: bool = false) -> void: if status_label: status_label.text = text status_label.modulate = Color(1.0, 0.4, 0.4) if is_error else Color(0.4, 1.0, 0.4) func _init_my_profile() -> void: if _api_client == null or not _api_client.has_token(): show_message("Log in to view profile.", true) return var me_res: Dictionary = await _api_client.get_me() if me_res.ok and me_res.data is Dictionary: _my_user_id = int(me_res.data.get("id", 0)) load_profile(_my_user_id, true) func _on_view_user_pressed() -> void: var uid_str := user_id_edit.text.strip_edges() if not uid_str.is_valid_int(): show_message("Enter a valid integer user ID.", true) return var uid := int(uid_str) load_profile(uid, uid == _my_user_id) func _refresh_current() -> void: if _viewing_user_id > 0: load_profile(_viewing_user_id, _is_own_profile) ## Load profile for user_id. func load_profile(user_id: int, is_own: bool = false) -> void: _viewing_user_id = user_id _is_own_profile = is_own if _is_own_profile: await _load_own_profile() else: await _load_public_profile(user_id) func _load_own_profile() -> void: title_label.text = "My Profile" if privacy_vbox: privacy_vbox.visible = true if save_bio_btn: save_bio_btn.visible = true if bio_edit: bio_edit.editable = true var res: Dictionary = await _api_client.get_my_profile() if not res.ok: show_message("Failed to load own profile: " + res.detail, true) return var data: Dictionary = res.data if res.data is Dictionary else {} _populate_card(_my_user_id, "You", str(data.get("bio", "")), data.get("avatar_asset_id")) # Privacy if data.has("privacy_json") and data["privacy_json"] is Dictionary: var priv: Dictionary = data["privacy_json"] _set_option_val(profile_vis_opt, priv.get("profile_visibility", "public")) _set_option_val(friends_vis_opt, priv.get("friends_list_visibility", "public")) _set_option_val(favorites_vis_opt, priv.get("favorites_visibility", "public")) # Load own favorites var fav_res: Dictionary = await _api_client.get_my_favorites() _populate_favorites(fav_res.data.get("world_ids", []) if fav_res.ok and fav_res.data is Dictionary else []) # Load own friends var fr_res: Dictionary = await _api_client.get_friends() _populate_friends(fr_res.data.get("friends", []) if fr_res.ok and fr_res.data is Dictionary else []) func _load_public_profile(user_id: int) -> void: title_label.text = "User Profile" if privacy_vbox: privacy_vbox.visible = false if save_bio_btn: save_bio_btn.visible = false if bio_edit: bio_edit.editable = false var res: Dictionary = await _api_client.get_public_profile(user_id) if not res.ok: show_message("Failed to load profile: " + res.detail, true) return var data: Dictionary = res.data if res.data is Dictionary else {} var uname: String = str(data.get("username", "User #%d" % user_id)) var bio: String = str(data.get("bio", "")) if data.get("bio") != null else "[Bio hidden or empty]" _populate_card(user_id, uname, bio, data.get("avatar_asset_id")) # Favorites if data.has("favorites") and data["favorites"] != null: _populate_favorites(data["favorites"]) else: _clear_children(favorites_container) if favorites_status_label: favorites_status_label.text = "Favorites are hidden by privacy settings." favorites_status_label.visible = true # Friends if data.has("friends") and data["friends"] != null: _populate_friends(data["friends"]) else: _clear_children(friends_container) if friends_status_label: friends_status_label.text = "Friends list is hidden by privacy settings." friends_status_label.visible = true func _populate_card(uid: int, uname: String, bio: String, avatar_asset_id: Variant) -> void: if username_label: username_label.text = uname if user_id_label: user_id_label.text = "User ID: %d" % uid if bio_edit: bio_edit.text = bio if avatar_preview_label: avatar_preview_label.text = "Avatar #%s" % str(avatar_asset_id) if avatar_asset_id != null else "Default Avatar" func _populate_favorites(favorites: Array) -> void: _clear_children(favorites_container) if favorites_status_label: favorites_status_label.visible = favorites.is_empty() if favorites.is_empty(): favorites_status_label.text = "No favorite experiences yet." for item in favorites: var hbox := HBoxContainer.new() hbox.add_theme_constant_override("separation", 8) var lbl := Label.new() if item is Dictionary: lbl.text = "★ %s (World #%s)" % [str(item.get("name", "World")), str(item.get("id", ""))] else: lbl.text = "★ World #%s" % str(item) hbox.add_child(lbl) favorites_container.add_child(hbox) func _populate_friends(friends: Array) -> void: _clear_children(friends_container) if friends_status_label: friends_status_label.visible = friends.is_empty() if friends.is_empty(): friends_status_label.text = "No friends listed." for friend in friends: var hbox := HBoxContainer.new() hbox.add_theme_constant_override("separation", 8) var lbl := Label.new() if friend is Dictionary: var fid: int = int(friend.get("id", 0)) var fname: String = str(friend.get("username", "User")) lbl.text = "👤 %s (ID: %d)" % [fname, fid] hbox.add_child(lbl) var view_btn := Button.new() view_btn.text = "View" view_btn.pressed.connect(func(): load_profile(fid, fid == _my_user_id)) hbox.add_child(view_btn) else: lbl.text = "👤 User #%s" % str(friend) hbox.add_child(lbl) friends_container.add_child(hbox) func _on_save_bio_pressed() -> void: if _api_client == null or not _is_own_profile: return var new_bio := bio_edit.text.strip_edges() if bio_edit else "" var res: Dictionary = await _api_client.update_my_profile(new_bio) if res.ok: show_message("Bio updated successfully!") else: show_message("Failed to update bio: " + res.detail, true) func _on_save_privacy_pressed() -> void: if _api_client == null or not _is_own_profile: return var privacy := { "profile_visibility": _get_option_val(profile_vis_opt), "friends_list_visibility": _get_option_val(friends_vis_opt), "favorites_visibility": _get_option_val(favorites_vis_opt), } var res: Dictionary = await _api_client.update_my_profile(null, null, privacy) if res.ok: show_message("Privacy settings updated!") else: show_message("Failed to update privacy: " + res.detail, true) func _set_option_val(opt: OptionButton, val: String) -> void: if opt == null: return for i in range(opt.item_count): if opt.get_item_text(i).to_lower() == val.to_lower(): opt.selected = i return func _get_option_val(opt: OptionButton) -> String: if opt == null or opt.selected < 0: return "public" return opt.get_item_text(opt.selected).to_lower().replace(" ", "_") func _clear_children(node: Node) -> void: if node == null: return for c in node.get_children(): c.queue_free()