extends Control class_name GroupsUI ## Groups & Communities UI. ## ## Features: ## - Browse groups with pagination and "My Groups" filter ## - Create new group (creator becomes owner) ## - Detailed group page: description, member count, member list with roles ## - Join and Leave group actions ## - Role management for owner (promote to admin, demote to member, transfer ownership) ## - Delete group (owner only) 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 @onready var tab_container: TabContainer = $VBoxContainer/TabContainer # Browse controls @onready var my_groups_check: CheckBox = $VBoxContainer/TabContainer/Browse/FilterHBox/MyGroupsCheck @onready var groups_list_container: VBoxContainer = $VBoxContainer/TabContainer/Browse/Scroll/GroupsList # Create controls @onready var new_group_name_edit: LineEdit = $VBoxContainer/TabContainer/Create/FormVBox/NameEdit @onready var new_group_desc_edit: TextEdit = $VBoxContainer/TabContainer/Create/FormVBox/DescEdit @onready var create_group_btn: Button = $VBoxContainer/TabContainer/Create/FormVBox/CreateButton # Group Page controls @onready var group_page_vbox: VBoxContainer = $VBoxContainer/TabContainer/GroupDetails/Scroll/DetailsVBox @onready var group_name_label: Label = $VBoxContainer/TabContainer/GroupDetails/Scroll/DetailsVBox/HeaderHBox/GroupNameLabel @onready var member_count_label: Label = $VBoxContainer/TabContainer/GroupDetails/Scroll/DetailsVBox/HeaderHBox/MemberCountLabel @onready var group_desc_label: Label = $VBoxContainer/TabContainer/GroupDetails/Scroll/DetailsVBox/GroupDescLabel @onready var join_btn: Button = $VBoxContainer/TabContainer/GroupDetails/Scroll/DetailsVBox/ActionsHBox/JoinButton @onready var leave_btn: Button = $VBoxContainer/TabContainer/GroupDetails/Scroll/DetailsVBox/ActionsHBox/LeaveButton @onready var delete_btn: Button = $VBoxContainer/TabContainer/GroupDetails/Scroll/DetailsVBox/ActionsHBox/DeleteButton @onready var members_list_container: VBoxContainer = $VBoxContainer/TabContainer/GroupDetails/Scroll/DetailsVBox/MembersVBox/MembersList var _api_client: Node = null var _current_user_id: int = 0 var _active_group_id: int = 0 var _active_group_data: Dictionary = {} func _ready() -> void: _api_client = _get_api() _wire_signals() if _api_client != null and _api_client.has_token(): _init_user() refresh_groups() func _get_api() -> Node: if has_node("/root/ApiClient"): return get_node("/root/ApiClient") return null func _init_user() -> void: if _api_client == null: return var me_res: Dictionary = await _api_client.get_me() if me_res.ok and me_res.data is Dictionary: _current_user_id = int(me_res.data.get("id", 0)) func _wire_signals() -> void: if back_button: back_button.pressed.connect(_on_back_pressed) if refresh_button: refresh_button.pressed.connect(refresh_groups) if my_groups_check: my_groups_check.toggled.connect(func(_t): refresh_groups()) if create_group_btn: create_group_btn.pressed.connect(_on_create_group_pressed) if join_btn: join_btn.pressed.connect(_on_join_pressed) if leave_btn: leave_btn.pressed.connect(_on_leave_pressed) if delete_btn: delete_btn.pressed.connect(_on_delete_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) # ── Browse Groups ── func refresh_groups() -> void: if _api_client == null or not _api_client.has_token(): return var mine: bool = my_groups_check.button_pressed if my_groups_check else false var res: Dictionary = await _api_client.list_groups(1, 50, mine) _clear_children(groups_list_container) if not res.ok: show_message("Failed to load groups: " + res.detail, true) return var groups: Array = [] if res.data is Dictionary and res.data.has("groups"): groups = res.data["groups"] elif res.data is Array: groups = res.data if groups.is_empty(): var empty_lbl := Label.new() empty_lbl.text = "No groups found. Create one in the Create Group tab!" empty_lbl.modulate = Color(0.6, 0.6, 0.6) groups_list_container.add_child(empty_lbl) return for g in groups: var gid: int = int(g.get("id", 0)) var gname: String = str(g.get("name", "Group")) var gdesc: String = str(g.get("description", "")) var count: int = int(g.get("member_count", 0)) _build_group_card(gid, gname, gdesc, count) func _build_group_card(gid: int, gname: String, gdesc: String, count: int) -> void: var panel := PanelContainer.new() var margin := MarginContainer.new() margin.add_theme_constant_override("margin_left", 12) margin.add_theme_constant_override("margin_top", 12) margin.add_theme_constant_override("margin_right", 12) margin.add_theme_constant_override("margin_bottom", 12) var hbox := HBoxContainer.new() hbox.add_theme_constant_override("separation", 16) var info_vbox := VBoxContainer.new() info_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL var name_lbl := Label.new() name_lbl.text = gname name_lbl.add_theme_font_size_override("font_size", 18) info_vbox.add_child(name_lbl) if gdesc != "": var desc_lbl := Label.new() desc_lbl.text = gdesc.left(100) desc_lbl.modulate = Color(0.8, 0.8, 0.8) info_vbox.add_child(desc_lbl) var count_lbl := Label.new() count_lbl.text = "%d member%s" % [count, "" if count == 1 else "s"] count_lbl.modulate = Color(0.6, 0.8, 1.0) info_vbox.add_child(count_lbl) hbox.add_child(info_vbox) var view_btn := Button.new() view_btn.text = "View Group" view_btn.custom_minimum_size = Vector2(110, 36) view_btn.size_flags_vertical = Control.SIZE_SHRINK_CENTER view_btn.pressed.connect(func(): open_group_page(gid)) hbox.add_child(view_btn) margin.add_child(hbox) panel.add_child(margin) groups_list_container.add_child(panel) # ── Create Group ── func _on_create_group_pressed() -> void: if _api_client == null: return var gname := new_group_name_edit.text.strip_edges() var gdesc := new_group_desc_edit.text.strip_edges() if gname == "": show_message("Group name cannot be empty.", true) return var res: Dictionary = await _api_client.create_group(gname, gdesc) if res.ok and res.data is Dictionary: var gid: int = int(res.data.get("id", 0)) new_group_name_edit.text = "" new_group_desc_edit.text = "" show_message("Group '%s' created!" % gname) await refresh_groups() open_group_page(gid) else: show_message("Failed to create group: " + res.detail, true) # ── Group Page Details ── func open_group_page(group_id: int) -> void: _active_group_id = group_id if tab_container: tab_container.current_tab = 2 # Details tab await refresh_group_page(group_id) func refresh_group_page(group_id: int) -> void: if _api_client == null or group_id <= 0: return var res: Dictionary = await _api_client.get_group(group_id) if not res.ok: show_message("Failed to load group: " + res.detail, true) return _active_group_data = res.data if res.data is Dictionary else {} var gname: String = str(_active_group_data.get("name", "Group")) var gdesc: String = str(_active_group_data.get("description", "No description provided.")) var members: Array = _active_group_data.get("members", []) var count: int = int(_active_group_data.get("member_count", members.size())) var owner_id: int = int(_active_group_data.get("owner_id", 0)) if group_name_label: group_name_label.text = gname if group_desc_label: group_desc_label.text = gdesc if member_count_label: member_count_label.text = "%d Members" % count # Check membership of current user var is_member := false var my_role := "" for m in members: var uid: int = int(m.get("user_id", 0)) if uid == _current_user_id: is_member = true my_role = str(m.get("role", "member")) break var is_owner := (_current_user_id == owner_id) or my_role == "owner" if join_btn: join_btn.visible = not is_member if leave_btn: leave_btn.visible = is_member and not is_owner if delete_btn: delete_btn.visible = is_owner _populate_members_list(members, is_owner) func _populate_members_list(members: Array, can_manage_roles: bool) -> void: _clear_children(members_list_container) for m in members: var uid: int = int(m.get("user_id", 0)) var uname: String = "User #%d" % uid if m.has("user") and m["user"] is Dictionary: uname = str(m["user"].get("username", uname)) var role: String = str(m.get("role", "member")) var hbox := HBoxContainer.new() hbox.add_theme_constant_override("separation", 12) var name_lbl := Label.new() name_lbl.text = "%s (ID: %d)" % [uname, uid] name_lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL hbox.add_child(name_lbl) var role_badge := Label.new() role_badge.text = "[%s]" % role.to_upper() if role == "owner": role_badge.modulate = Color(1.0, 0.8, 0.2) elif role == "admin": role_badge.modulate = Color(0.4, 0.8, 1.0) else: role_badge.modulate = Color(0.7, 0.7, 0.7) hbox.add_child(role_badge) # Role selector for owner (excluding self/owner) if can_manage_roles and role != "owner": var opt := OptionButton.new() opt.add_item("Member") opt.add_item("Admin") opt.selected = 1 if role == "admin" else 0 opt.item_selected.connect(func(idx: int): _on_role_changed(uid, "admin" if idx == 1 else "member")) hbox.add_child(opt) var xfer_btn := Button.new() xfer_btn.text = "Make Owner" xfer_btn.pressed.connect(func(): _on_transfer_ownership(uid)) hbox.add_child(xfer_btn) members_list_container.add_child(hbox) func _on_join_pressed() -> void: if _api_client == null or _active_group_id <= 0: return var res: Dictionary = await _api_client.join_group(_active_group_id) if res.ok: show_message("Joined group!") refresh_group_page(_active_group_id) else: show_message("Failed to join group: " + res.detail, true) func _on_leave_pressed() -> void: if _api_client == null or _active_group_id <= 0: return var res: Dictionary = await _api_client.leave_group(_active_group_id) if res.ok: show_message("Left group.") refresh_group_page(_active_group_id) else: show_message("Failed to leave group: " + res.detail, true) func _on_delete_pressed() -> void: if _api_client == null or _active_group_id <= 0: return var res: Dictionary = await _api_client.delete_group(_active_group_id) if res.ok: show_message("Group deleted.") tab_container.current_tab = 0 refresh_groups() else: show_message("Failed to delete group: " + res.detail, true) func _on_role_changed(uid: int, new_role: String) -> void: if _api_client == null or _active_group_id <= 0: return var res: Dictionary = await _api_client.set_group_member_role(_active_group_id, uid, new_role) if res.ok: show_message("Role updated to %s." % new_role) refresh_group_page(_active_group_id) else: show_message("Failed to change role: " + res.detail, true) func _on_transfer_ownership(new_owner_id: int) -> void: if _api_client == null or _active_group_id <= 0: return var res: Dictionary = await _api_client.transfer_group_ownership(_active_group_id, new_owner_id) if res.ok: show_message("Ownership transferred to User #%d!" % new_owner_id) refresh_group_page(_active_group_id) else: show_message("Failed to transfer ownership: " + res.detail, true) func _clear_children(node: Node) -> void: if node == null: return for c in node.get_children(): c.queue_free()