358 lines
12 KiB
GDScript
358 lines
12 KiB
GDScript
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 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()
|