Implement Godot profile page UI with bio editing, privacy controls, favorites, and friends (task 121de83d3be8)
This commit is contained in:
@@ -0,0 +1,215 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/profile_ui.gd" id="1_pu"]
|
||||||
|
|
||||||
|
[node name="ProfileUI" type="Control"]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
script = ExtResource("1_pu")
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = 32.0
|
||||||
|
offset_top = 32.0
|
||||||
|
offset_right = -32.0
|
||||||
|
offset_bottom = -32.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
theme_override_constants/separation = 12
|
||||||
|
|
||||||
|
[node name="HeaderHBox" type="HBoxContainer" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 16
|
||||||
|
|
||||||
|
[node name="BackButton" type="Button" parent="VBoxContainer/HeaderHBox"]
|
||||||
|
custom_minimum_size = Vector2(100, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "< Back"
|
||||||
|
|
||||||
|
[node name="TitleLabel" type="Label" parent="VBoxContainer/HeaderHBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_override_font_sizes/font_size = 26
|
||||||
|
text = "Profile"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="RefreshButton" type="Button" parent="VBoxContainer/HeaderHBox"]
|
||||||
|
custom_minimum_size = Vector2(100, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Refresh"
|
||||||
|
|
||||||
|
[node name="LookupHBox" type="HBoxContainer" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="LookupPrefix" type="Label" parent="VBoxContainer/LookupHBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Lookup User ID:"
|
||||||
|
|
||||||
|
[node name="UserIdEdit" type="LineEdit" parent="VBoxContainer/LookupHBox"]
|
||||||
|
custom_minimum_size = Vector2(120, 32)
|
||||||
|
layout_mode = 2
|
||||||
|
placeholder_text = "e.g. 2"
|
||||||
|
|
||||||
|
[node name="ViewUserButton" type="Button" parent="VBoxContainer/LookupHBox"]
|
||||||
|
custom_minimum_size = Vector2(100, 32)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "View"
|
||||||
|
|
||||||
|
[node name="Spacer" type="Control" parent="VBoxContainer/LookupHBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="ViewMyProfileButton" type="Button" parent="VBoxContainer/LookupHBox"]
|
||||||
|
custom_minimum_size = Vector2(140, 32)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "My Own Profile"
|
||||||
|
|
||||||
|
[node name="StatusMessageLabel" type="Label" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="Scroll" type="ScrollContainer" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="ContentVBox" type="VBoxContainer" parent="VBoxContainer/Scroll"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_override_constants/separation = 16
|
||||||
|
|
||||||
|
[node name="CardPanel" type="PanelContainer" parent="VBoxContainer/Scroll/ContentVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Margin" type="MarginContainer" parent="VBoxContainer/Scroll/ContentVBox/CardPanel"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/margin_left = 16
|
||||||
|
theme_override_constants/margin_top = 16
|
||||||
|
theme_override_constants/margin_right = 16
|
||||||
|
theme_override_constants/margin_bottom = 16
|
||||||
|
|
||||||
|
[node name="HBox" type="HBoxContainer" parent="VBoxContainer/Scroll/ContentVBox/CardPanel/Margin"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 20
|
||||||
|
|
||||||
|
[node name="AvatarBox" type="VBoxContainer" parent="VBoxContainer/Scroll/ContentVBox/CardPanel/Margin/HBox"]
|
||||||
|
custom_minimum_size = Vector2(120, 120)
|
||||||
|
layout_mode = 2
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
[node name="AvatarLabel" type="Label" parent="VBoxContainer/Scroll/ContentVBox/CardPanel/Margin/HBox/AvatarBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Avatar"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="DetailsVBox" type="VBoxContainer" parent="VBoxContainer/Scroll/ContentVBox/CardPanel/Margin/HBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_override_constants/separation = 6
|
||||||
|
|
||||||
|
[node name="UsernameLabel" type="Label" parent="VBoxContainer/Scroll/ContentVBox/CardPanel/Margin/HBox/DetailsVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 22
|
||||||
|
text = "Username"
|
||||||
|
|
||||||
|
[node name="UserIdLabel" type="Label" parent="VBoxContainer/Scroll/ContentVBox/CardPanel/Margin/HBox/DetailsVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "User ID: --"
|
||||||
|
|
||||||
|
[node name="BioPrompt" type="Label" parent="VBoxContainer/Scroll/ContentVBox/CardPanel/Margin/HBox/DetailsVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Bio:"
|
||||||
|
|
||||||
|
[node name="BioEdit" type="TextEdit" parent="VBoxContainer/Scroll/ContentVBox/CardPanel/Margin/HBox/DetailsVBox"]
|
||||||
|
custom_minimum_size = Vector2(0, 80)
|
||||||
|
layout_mode = 2
|
||||||
|
placeholder_text = "Tell the world about yourself..."
|
||||||
|
|
||||||
|
[node name="SaveBioButton" type="Button" parent="VBoxContainer/Scroll/ContentVBox/CardPanel/Margin/HBox/DetailsVBox"]
|
||||||
|
custom_minimum_size = Vector2(100, 32)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
text = "Save Bio"
|
||||||
|
|
||||||
|
[node name="PrivacyVBox" type="VBoxContainer" parent="VBoxContainer/Scroll/ContentVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="PrivacyHeader" type="Label" parent="VBoxContainer/Scroll/ContentVBox/PrivacyVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 18
|
||||||
|
text = "Privacy Settings"
|
||||||
|
|
||||||
|
[node name="Grid" type="GridContainer" parent="VBoxContainer/Scroll/ContentVBox/PrivacyVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
columns = 2
|
||||||
|
theme_override_constants/h_separation = 16
|
||||||
|
theme_override_constants/v_separation = 8
|
||||||
|
|
||||||
|
[node name="ProfileVisLabel" type="Label" parent="VBoxContainer/Scroll/ContentVBox/PrivacyVBox/Grid"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Profile Visibility:"
|
||||||
|
|
||||||
|
[node name="ProfileVisOption" type="OptionButton" parent="VBoxContainer/Scroll/ContentVBox/PrivacyVBox/Grid"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="FriendsVisLabel" type="Label" parent="VBoxContainer/Scroll/ContentVBox/PrivacyVBox/Grid"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Friends List Visibility:"
|
||||||
|
|
||||||
|
[node name="FriendsVisOption" type="OptionButton" parent="VBoxContainer/Scroll/ContentVBox/PrivacyVBox/Grid"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="FavoritesVisLabel" type="Label" parent="VBoxContainer/Scroll/ContentVBox/PrivacyVBox/Grid"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Favorites Visibility:"
|
||||||
|
|
||||||
|
[node name="FavoritesVisOption" type="OptionButton" parent="VBoxContainer/Scroll/ContentVBox/PrivacyVBox/Grid"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="SavePrivacyButton" type="Button" parent="VBoxContainer/Scroll/ContentVBox/PrivacyVBox"]
|
||||||
|
custom_minimum_size = Vector2(140, 32)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
text = "Save Privacy"
|
||||||
|
|
||||||
|
[node name="FavoritesVBox" type="VBoxContainer" parent="VBoxContainer/Scroll/ContentVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="FavHeader" type="Label" parent="VBoxContainer/Scroll/ContentVBox/FavoritesVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 18
|
||||||
|
text = "Favorite Experiences"
|
||||||
|
|
||||||
|
[node name="FavoritesStatusLabel" type="Label" parent="VBoxContainer/Scroll/ContentVBox/FavoritesVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "No favorite experiences yet."
|
||||||
|
|
||||||
|
[node name="FavoritesList" type="VBoxContainer" parent="VBoxContainer/Scroll/ContentVBox/FavoritesVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 4
|
||||||
|
|
||||||
|
[node name="FriendsVBox" type="VBoxContainer" parent="VBoxContainer/Scroll/ContentVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="FriendsHeader" type="Label" parent="VBoxContainer/Scroll/ContentVBox/FriendsVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 18
|
||||||
|
text = "Friends"
|
||||||
|
|
||||||
|
[node name="FriendsStatusLabel" type="Label" parent="VBoxContainer/Scroll/ContentVBox/FriendsVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "No friends listed."
|
||||||
|
|
||||||
|
[node name="FriendsList" type="VBoxContainer" parent="VBoxContainer/Scroll/ContentVBox/FriendsVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 4
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
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()
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## Headless smoke test for ProfileUI scene and script.
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
_run()
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
print("SMOKE: Starting Profile UI verification...")
|
||||||
|
var scene_res := load("res://scenes/profile_ui.tscn")
|
||||||
|
if scene_res == null:
|
||||||
|
printerr("FAIL: Could not load res://scenes/profile_ui.tscn")
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
var ui: Control = scene_res.instantiate()
|
||||||
|
if ui == null:
|
||||||
|
printerr("FAIL: Could not instantiate ProfileUI")
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
root.add_child(ui)
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
# Verify essential nodes
|
||||||
|
if ui.username_label == null or ui.bio_edit == null:
|
||||||
|
printerr("FAIL: username_label or bio_edit is null")
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
if ui.privacy_vbox == null or ui.favorites_container == null or ui.friends_container == null:
|
||||||
|
printerr("FAIL: container nodes are null")
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Verify signal declaration
|
||||||
|
if not ui.has_signal("back_requested"):
|
||||||
|
printerr("FAIL: back_requested signal missing")
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Test card population
|
||||||
|
ui._populate_card(99, "TestExplorer", "Just another world builder", 12)
|
||||||
|
if ui.username_label.text != "TestExplorer" or ui.bio_edit.text != "Just another world builder":
|
||||||
|
printerr("FAIL: Card population failed")
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
print("PASS: Profile card population verified.")
|
||||||
|
|
||||||
|
# Test favorites population
|
||||||
|
var mock_favs := [
|
||||||
|
{"id": 1, "name": "Sunset Plains"},
|
||||||
|
{"id": 2, "name": "Neon City Rooftops"}
|
||||||
|
]
|
||||||
|
ui._populate_favorites(mock_favs)
|
||||||
|
if ui.favorites_container.get_child_count() != 2:
|
||||||
|
printerr("FAIL: Favorites count mismatch")
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
print("PASS: Favorites list population verified.")
|
||||||
|
|
||||||
|
# Test friends population
|
||||||
|
var mock_friends := [
|
||||||
|
{"id": 10, "username": "Alice"},
|
||||||
|
{"id": 20, "username": "Bob"}
|
||||||
|
]
|
||||||
|
ui._populate_friends(mock_friends)
|
||||||
|
if ui.friends_container.get_child_count() != 2:
|
||||||
|
printerr("FAIL: Friends list count mismatch")
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
print("PASS: Friends list population verified.")
|
||||||
|
|
||||||
|
ui.queue_free()
|
||||||
|
print("SMOKE RESULT: PASS")
|
||||||
|
quit(0)
|
||||||
Reference in New Issue
Block a user