Files

319 lines
11 KiB
GDScript

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 _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("ui_cancel"):
get_viewport().set_input_as_handled()
_on_back_pressed()
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()