Files
modbison/scripts/groups_ui.gd
T

364 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 _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()