1068 lines
38 KiB
GDScript
1068 lines
38 KiB
GDScript
extends Panel
|
|
## Server Admin Panel — Management for Users and Leaderboards.
|
|
## Features: User BAN/DELETE/ROLE management, Leaderboard score modification.
|
|
|
|
signal closed
|
|
|
|
# -- UI refs --
|
|
@onready var tabs := %Tabs as TabContainer
|
|
@onready var title_label := %Title as Label
|
|
@onready var count_label := %CountLabel as Label
|
|
@onready var refresh_btn := %RefreshBtn as Button
|
|
@onready var close_btn := %CloseBtn as Button
|
|
@onready var status_label := %StatusLabel as Label
|
|
|
|
# Tab: Users
|
|
@onready var user_tree := %UserTree as Tree
|
|
@onready var select_all_btn := %SelectAllBtn as Button
|
|
@onready var deselect_btn := %DeselectBtn as Button
|
|
@onready var selected_label := %SelectedLabel as Label
|
|
@onready var ban_btn := %BanBtn as Button
|
|
@onready var unban_btn := %UnbanBtn as Button
|
|
@onready var delete_btn := %DeleteBtn as Button
|
|
|
|
# Tab: Leaderboards
|
|
@onready var lb_tree := %LeaderboardTree as Tree
|
|
@onready var sync_lb_btn := %SyncLeaderboardBtn as Button
|
|
@onready var reset_lb_btn := %ResetLBBtn as Button
|
|
|
|
# Tab: Daily Rewards
|
|
@onready var month_option_btn := %MonthOptionBtn as OptionButton
|
|
@onready var days_grid := %DaysGrid as GridContainer
|
|
@onready var day_config_template := %DayConfigTemplate as PanelContainer
|
|
@onready var load_dr_btn := %LoadDRConfigBtn as Button
|
|
@onready var save_dr_btn := %SaveDRConfigBtn as Button
|
|
|
|
var _daily_reward_config_data: Dictionary = {}
|
|
var _current_dr_month: String = ""
|
|
|
|
# Tab: Announcements
|
|
@onready var target_user_edit := %TargetUserEdit as LineEdit
|
|
@onready var find_user_btn := %FindUserBtn as Button
|
|
@onready var resolved_id_label := %ResolvedIdLabel as Label
|
|
@onready var title_edit := %TitleEdit as LineEdit
|
|
@onready var content_edit := %ContentEdit as TextEdit
|
|
@onready var start_date_edit := %StartDatePicker as Button
|
|
@onready var end_date_edit := %EndDatePicker as Button
|
|
@onready var add_reward_btn := %AddRewardBtn as Button
|
|
@onready var rewards_list := %RewardsList as VBoxContainer
|
|
@onready var reward_row_template := %RewardRowTemplate as HBoxContainer
|
|
@onready var send_mail_btn := %SendMailBtn as Button
|
|
|
|
var _resolved_user_id: String = ""
|
|
|
|
# Tab: Mail Manager
|
|
@onready var mail_tree := %MailTree as Tree
|
|
@onready var refresh_mail_btn := %RefreshMailBtn as Button
|
|
@onready var edit_mail_btn := %EditMailBtn as Button
|
|
@onready var end_mail_btn := %EndMailBtn as Button
|
|
@onready var delete_mail_server_btn := %DeleteMailServerBtn as Button
|
|
|
|
var _mail_root: TreeItem
|
|
var _all_server_mails: Array = []
|
|
|
|
const MONTH_NAMES = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
|
|
|
|
# -- Data --
|
|
var all_users: Array = []
|
|
var lb_data: Array = []
|
|
var _user_root: TreeItem
|
|
var _lb_root: TreeItem
|
|
|
|
# -- Colors --
|
|
const CLR_BG := Color(0.1, 0.1, 0.12)
|
|
const CLR_HEADER := Color(0.85, 0.55, 0.2)
|
|
const CLR_TITLE := Color(0.95, 0.35, 0.35)
|
|
const CLR_TEXT := Color(0.82, 0.82, 0.82)
|
|
const CLR_DIM := Color(0.5, 0.5, 0.5)
|
|
const CLR_BANNED := Color(1.0, 0.3, 0.3)
|
|
const CLR_ADMIN := Color(0.4, 0.75, 1.0)
|
|
const CLR_MOD := Color(0.5, 0.9, 0.4)
|
|
const CLR_BTN_BAN := Color(1.0, 0.6, 0.2)
|
|
const CLR_BTN_UNBAN := Color(0.4, 0.8, 0.4)
|
|
const CLR_BTN_DEL := Color(1.0, 0.3, 0.3)
|
|
const CLR_STATUS_OK := Color(0.4, 0.9, 0.4)
|
|
const CLR_STATUS_ERR := Color(1.0, 0.3, 0.3)
|
|
|
|
func _ready() -> void:
|
|
visible = false
|
|
_apply_plain_style()
|
|
_setup_columns()
|
|
_connect_signals()
|
|
|
|
# =============================================================================
|
|
# Plain style (overrides any inherited theme)
|
|
# =============================================================================
|
|
func _apply_plain_style() -> void:
|
|
title_label.add_theme_color_override("font_color", CLR_TITLE)
|
|
title_label.add_theme_font_size_override("font_size", 22)
|
|
count_label.add_theme_color_override("font_color", CLR_HEADER)
|
|
status_label.add_theme_color_override("font_color", CLR_DIM)
|
|
|
|
for btn: Button in [refresh_btn, close_btn, select_all_btn, deselect_btn, load_dr_btn, save_dr_btn]:
|
|
_style_button(btn, Color(0.2, 0.2, 0.24), CLR_TEXT)
|
|
_style_button(ban_btn, Color(0.3, 0.2, 0.1), CLR_BTN_BAN)
|
|
_style_button(unban_btn, Color(0.1, 0.22, 0.1), CLR_BTN_UNBAN)
|
|
_style_button(delete_btn, Color(0.28, 0.1, 0.1), CLR_BTN_DEL)
|
|
_style_button(sync_lb_btn, Color(0.2, 0.2, 0.3), CLR_ADMIN)
|
|
|
|
var tree_bg := StyleBoxFlat.new()
|
|
tree_bg.bg_color = Color(0.12, 0.12, 0.14)
|
|
tree_bg.set_corner_radius_all(4)
|
|
|
|
for t: Tree in [user_tree, lb_tree]:
|
|
t.add_theme_stylebox_override("panel", tree_bg)
|
|
t.add_theme_color_override("font_color", CLR_TEXT)
|
|
t.add_theme_color_override("title_button_color", CLR_HEADER)
|
|
|
|
func _style_button(btn: Button, bg_color: Color, text_color: Color) -> void:
|
|
var sb := StyleBoxFlat.new()
|
|
sb.bg_color = bg_color
|
|
sb.set_corner_radius_all(4)
|
|
sb.set_content_margin_all(6)
|
|
btn.add_theme_stylebox_override("normal", sb)
|
|
var sb_hover := sb.duplicate()
|
|
sb_hover.bg_color = bg_color.lightened(0.15)
|
|
btn.add_theme_stylebox_override("hover", sb_hover)
|
|
btn.add_theme_color_override("font_color", text_color)
|
|
|
|
# =============================================================================
|
|
# Columns Setup
|
|
# =============================================================================
|
|
func _setup_columns() -> void:
|
|
# Users
|
|
user_tree.set_column_title(0, "☑")
|
|
user_tree.set_column_title(1, "User ID")
|
|
user_tree.set_column_title(2, "Username")
|
|
user_tree.set_column_title(3, "Display Name")
|
|
user_tree.set_column_title(4, "Created")
|
|
user_tree.set_column_title(5, "Edit")
|
|
user_tree.set_column_custom_minimum_width(0, 40)
|
|
user_tree.set_column_expand(0, false)
|
|
_user_root = user_tree.create_item()
|
|
|
|
# Leaderboards
|
|
lb_tree.set_column_title(0, "Rank")
|
|
lb_tree.set_column_title(1, "Player")
|
|
lb_tree.set_column_title(2, "High Score")
|
|
lb_tree.set_column_title(3, "Wins")
|
|
lb_tree.set_column_title(4, "Games")
|
|
lb_tree.set_column_title(5, "Edit")
|
|
lb_tree.set_column_expand(0, false)
|
|
lb_tree.set_column_custom_minimum_width(0, 60)
|
|
lb_tree.set_column_expand(5, false)
|
|
lb_tree.set_column_custom_minimum_width(5, 60)
|
|
_lb_root = lb_tree.create_item()
|
|
|
|
# Mail Manager
|
|
mail_tree.set_column_title(0, "Type")
|
|
mail_tree.set_column_title(1, "Title")
|
|
mail_tree.set_column_title(2, "Sender")
|
|
mail_tree.set_column_title(3, "Start")
|
|
mail_tree.set_column_title(4, "Expires")
|
|
mail_tree.set_column_title(5, "Status")
|
|
mail_tree.set_column_custom_minimum_width(0, 100)
|
|
mail_tree.set_column_expand(0, false)
|
|
mail_tree.set_column_custom_minimum_width(5, 80)
|
|
mail_tree.set_column_expand(5, false)
|
|
_mail_root = mail_tree.create_item()
|
|
|
|
func _connect_signals() -> void:
|
|
close_btn.pressed.connect(_on_close)
|
|
refresh_btn.pressed.connect(_on_refresh)
|
|
tabs.tab_changed.connect(_on_tab_changed)
|
|
|
|
# User actions
|
|
select_all_btn.pressed.connect(_select_all)
|
|
deselect_btn.pressed.connect(_deselect_all)
|
|
ban_btn.pressed.connect(_on_ban)
|
|
unban_btn.pressed.connect(_on_unban)
|
|
delete_btn.pressed.connect(_on_delete)
|
|
user_tree.item_edited.connect(_on_user_tree_item_edited)
|
|
user_tree.button_clicked.connect(_on_user_tree_button_clicked)
|
|
|
|
# LB actions
|
|
lb_tree.button_clicked.connect(_on_lb_tree_button_clicked)
|
|
sync_lb_btn.pressed.connect(_on_sync_leaderboard)
|
|
|
|
# DR actions
|
|
load_dr_btn.pressed.connect(_load_daily_rewards_config)
|
|
save_dr_btn.pressed.connect(_save_daily_rewards_config)
|
|
month_option_btn.item_selected.connect(_on_dr_month_selected)
|
|
|
|
# Announcement actions
|
|
send_mail_btn.pressed.connect(_on_send_mail)
|
|
add_reward_btn.pressed.connect(_on_add_reward_pressed)
|
|
find_user_btn.pressed.connect(_on_find_user)
|
|
target_user_edit.text_changed.connect(func(_t): _resolved_user_id = ""; resolved_id_label.text = "")
|
|
|
|
# Mail Manager actions
|
|
refresh_mail_btn.pressed.connect(func(): await _load_mail())
|
|
mail_tree.item_selected.connect(_on_mail_item_selected)
|
|
edit_mail_btn.pressed.connect(_on_edit_mail_pressed)
|
|
end_mail_btn.pressed.connect(_on_end_mail_pressed)
|
|
delete_mail_server_btn.pressed.connect(_on_delete_mail_server_pressed)
|
|
_update_mail_action_btns(null)
|
|
|
|
# =============================================================================
|
|
# Core Panel Logic
|
|
# =============================================================================
|
|
func show_panel() -> void:
|
|
visible = true
|
|
_on_tab_changed(tabs.current_tab)
|
|
|
|
func _on_close() -> void:
|
|
visible = false
|
|
emit_signal("closed")
|
|
|
|
func _on_refresh() -> void:
|
|
_on_tab_changed(tabs.current_tab)
|
|
|
|
func _on_tab_changed(tab_index: int) -> void:
|
|
_set_status("")
|
|
if tab_index == 0:
|
|
await _load_users()
|
|
elif tab_index == 1:
|
|
await _load_leaderboard()
|
|
elif tab_index == 2:
|
|
await _load_daily_rewards_config()
|
|
elif tab_index == 4:
|
|
await _load_mail()
|
|
|
|
# =============================================================================
|
|
# RPC Helper
|
|
# =============================================================================
|
|
func _rpc(rpc_name: String, payload: Dictionary) -> Dictionary:
|
|
if not NakamaManager.client or not NakamaManager.session:
|
|
return {"error": "Not connected"}
|
|
var result = await NakamaManager.client.rpc_async(
|
|
NakamaManager.session, rpc_name, JSON.stringify(payload)
|
|
)
|
|
if result.is_exception():
|
|
var err: String = result.get_exception().message
|
|
_set_status(err, CLR_STATUS_ERR)
|
|
return {"error": err}
|
|
if result.payload:
|
|
return JSON.parse_string(result.payload)
|
|
return {}
|
|
|
|
func _set_status(msg: String, color: Color = CLR_DIM) -> void:
|
|
status_label.text = msg
|
|
status_label.add_theme_color_override("font_color", color)
|
|
|
|
# =============================================================================
|
|
# TAB 1: USER MANAGEMENT
|
|
# =============================================================================
|
|
func _load_users() -> void:
|
|
_clear_tree(user_tree, _user_root)
|
|
_set_status("Loading users...")
|
|
|
|
var res := await _rpc("admin_list_users", {})
|
|
if res.has("error"):
|
|
_set_status("Failed: " + str(res.error), CLR_STATUS_ERR)
|
|
return
|
|
|
|
all_users = res.get("users", [])
|
|
count_label.text = "%d users" % all_users.size()
|
|
|
|
for user in all_users:
|
|
var uid: String = user.get("user_id", "")
|
|
var uname: String = user.get("username", "")
|
|
var dname: String = user.get("display_name", uname)
|
|
var role: String = user.get("role", "player")
|
|
var banned: bool = user.get("banned", false)
|
|
var created: String = str(user.get("create_time", ""))
|
|
|
|
var item := _user_root.create_child()
|
|
item.set_cell_mode(0, TreeItem.CELL_MODE_CHECK)
|
|
item.set_editable(0, true)
|
|
|
|
var short_id := uid.substr(0, 8) + "..."
|
|
item.set_text(1, short_id)
|
|
item.set_tooltip_text(1, uid)
|
|
|
|
var uname_display := uname
|
|
if role != "player": uname_display += " [%s]" % role.to_upper()
|
|
if banned: uname_display += " (BANNED)"
|
|
item.set_text(2, uname_display)
|
|
item.set_text(3, dname)
|
|
item.set_text(4, created.substr(0, 10) if created.length() > 10 else created)
|
|
item.add_button(5, _get_edit_icon(), 0, false, "Edit User")
|
|
|
|
if banned: item.set_custom_color(2, CLR_BANNED)
|
|
elif role in ["admin", "owner"]: item.set_custom_color(2, CLR_ADMIN)
|
|
|
|
item.set_metadata(0, user)
|
|
|
|
_update_selection_count()
|
|
_set_status("")
|
|
|
|
func _on_user_tree_item_edited() -> void:
|
|
_update_selection_count()
|
|
|
|
func _update_selection_count() -> void:
|
|
var count := _get_checked_items().size()
|
|
selected_label.text = "%d selected" % count
|
|
ban_btn.disabled = count == 0
|
|
unban_btn.disabled = count == 0
|
|
delete_btn.disabled = count == 0
|
|
|
|
func _on_user_tree_button_clicked(item: TreeItem, _col: int, _id: int, _mouse: int) -> void:
|
|
_show_edit_user_dialog(item.get_metadata(0))
|
|
|
|
func _show_edit_user_dialog(user: Dictionary) -> void:
|
|
var uid: String = user.get("user_id", "")
|
|
var uname: String = user.get("username", "")
|
|
var role: String = user.get("role", "player")
|
|
var banned: bool = user.get("banned", false)
|
|
|
|
var dialog := AcceptDialog.new()
|
|
dialog.title = "Edit User: " + uname
|
|
dialog.min_size = Vector2i(380, 260)
|
|
var vbox := VBoxContainer.new()
|
|
vbox.add_theme_constant_override("separation", 8)
|
|
|
|
var id_lbl := Label.new()
|
|
id_lbl.text = "User ID: " + uid
|
|
id_lbl.add_theme_color_override("font_color", CLR_DIM)
|
|
vbox.add_child(id_lbl)
|
|
|
|
var role_hbox := HBoxContainer.new()
|
|
var role_lbl := Label.new()
|
|
role_lbl.text = "Role: "
|
|
role_hbox.add_child(role_lbl)
|
|
var role_option := OptionButton.new()
|
|
var roles := ["player", "moderator", "admin"]
|
|
for r in roles: role_option.add_item(r)
|
|
role_option.selected = roles.find(role)
|
|
role_hbox.add_child(role_option)
|
|
vbox.add_child(role_hbox)
|
|
|
|
var ban_check := CheckButton.new()
|
|
ban_check.text = "Banned"
|
|
ban_check.button_pressed = banned
|
|
vbox.add_child(ban_check)
|
|
|
|
var reason_input := LineEdit.new()
|
|
reason_input.placeholder_text = "Ban reason..."
|
|
reason_input.text = user.get("ban_reason", "")
|
|
reason_input.visible = banned
|
|
vbox.add_child(reason_input)
|
|
ban_check.toggled.connect(func(on): reason_input.visible = on)
|
|
|
|
var save_btn := Button.new()
|
|
save_btn.text = "Save Changes"
|
|
save_btn.custom_minimum_size.y = 36
|
|
vbox.add_child(save_btn)
|
|
|
|
dialog.add_child(vbox)
|
|
add_child(dialog)
|
|
dialog.popup_centered()
|
|
|
|
save_btn.pressed.connect(func():
|
|
var new_role: String = roles[role_option.selected]
|
|
await _save_user_edit(uid, uname, new_role, ban_check.button_pressed, reason_input.text)
|
|
dialog.queue_free()
|
|
)
|
|
|
|
func _save_user_edit(uid: String, uname: String, new_role: String, new_banned: bool, reason: String) -> void:
|
|
_set_status("Saving...")
|
|
await _rpc("admin_set_user_role", {"user_id": uid, "role": new_role})
|
|
if new_banned:
|
|
await _rpc("admin_ban_player", {"user_id": uid, "reason": reason, "duration_hours": 0})
|
|
else:
|
|
await _rpc("admin_unban_player", {"user_id": uid})
|
|
_set_status("Saved: " + uname, CLR_STATUS_OK)
|
|
await _load_users()
|
|
|
|
# =============================================================================
|
|
# TAB 1: USER SELECTION BATCH ACTIONS
|
|
# =============================================================================
|
|
func _select_all() -> void:
|
|
if not _user_root: return
|
|
var item := _user_root.get_first_child()
|
|
while item:
|
|
item.set_checked(0, true)
|
|
item = item.get_next()
|
|
_update_selection_count()
|
|
|
|
func _deselect_all() -> void:
|
|
if not _user_root: return
|
|
var item := _user_root.get_first_child()
|
|
while item:
|
|
item.set_checked(0, false)
|
|
item = item.get_next()
|
|
_update_selection_count()
|
|
|
|
func _get_checked_items() -> Array[TreeItem]:
|
|
var items: Array[TreeItem] = []
|
|
if not _user_root: return items
|
|
var item := _user_root.get_first_child()
|
|
while item:
|
|
if item.is_checked(0): items.append(item)
|
|
item = item.get_next()
|
|
return items
|
|
|
|
func _get_checked_user_data() -> Array:
|
|
var data: Array = []
|
|
for item in _get_checked_items():
|
|
data.append(item.get_metadata(0))
|
|
return data
|
|
|
|
func _on_ban() -> void:
|
|
var users := _get_checked_user_data()
|
|
if users.is_empty(): return
|
|
var to_ban: Array = []
|
|
for u in users:
|
|
if u.get("banned", false): continue
|
|
if u.get("role", "") in ["admin", "owner"]: continue
|
|
to_ban.append(u)
|
|
if to_ban.is_empty():
|
|
_set_status("No eligible users to ban", CLR_STATUS_ERR)
|
|
return
|
|
|
|
var dialog := ConfirmationDialog.new()
|
|
dialog.title = "Ban %d user(s)" % to_ban.size()
|
|
var vbox := VBoxContainer.new()
|
|
vbox.add_theme_constant_override("separation", 8)
|
|
var list_names: Array = []
|
|
for i in range(min(to_ban.size(), 10)): list_names.append(to_ban[i].get("username", ""))
|
|
var name_str := ", ".join(list_names)
|
|
if to_ban.size() > 10: name_str += "... and %d others" % (to_ban.size() - 10)
|
|
var lbl := Label.new()
|
|
lbl.text = "Ban following users:\n" + name_str
|
|
lbl.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
lbl.custom_minimum_size = Vector2(350, 0)
|
|
vbox.add_child(lbl)
|
|
var reason_input := LineEdit.new()
|
|
reason_input.placeholder_text = "Ban reason"
|
|
vbox.add_child(reason_input)
|
|
var dur := SpinBox.new()
|
|
dur.min_value = 0; dur.max_value = 8760; dur.value = 24; dur.prefix = "Hours: "
|
|
vbox.add_child(dur)
|
|
dialog.add_child(vbox)
|
|
add_child(dialog)
|
|
dialog.popup_centered()
|
|
dialog.confirmed.connect(func():
|
|
var ok := 0
|
|
for u in to_ban:
|
|
var r := await _rpc("admin_ban_player", {"user_id": u.user_id, "reason": reason_input.text, "duration_hours": int(dur.value)})
|
|
if r.get("success", false): ok += 1
|
|
_set_status("Banned %d/%d" % [ok, to_ban.size()], CLR_STATUS_OK)
|
|
await _load_users()
|
|
dialog.queue_free()
|
|
)
|
|
|
|
func _on_unban() -> void:
|
|
var users := _get_checked_user_data()
|
|
var to_unban := []; for u in users: if u.get("banned", false): to_unban.append(u)
|
|
if to_unban.is_empty(): return
|
|
var ok := 0
|
|
for u in to_unban:
|
|
var r := await _rpc("admin_unban_player", {"user_id": u.user_id})
|
|
if r.get("success", false): ok += 1
|
|
_set_status("Unbanned %d/%d" % [ok, to_unban.size()], CLR_STATUS_OK)
|
|
await _load_users()
|
|
|
|
func _on_delete() -> void:
|
|
var users := _get_checked_user_data()
|
|
if users.is_empty(): return
|
|
var ids := []; var names := []
|
|
for u in users:
|
|
if u.get("role", "") in ["admin", "owner"]: continue
|
|
ids.append(u.user_id); names.append(u.username)
|
|
if ids.is_empty(): return
|
|
var dialog := ConfirmationDialog.new()
|
|
dialog.title = "DELETE %d account(s)" % ids.size()
|
|
var vbox := VBoxContainer.new(); vbox.add_theme_constant_override("separation", 12)
|
|
var list_names = []; for i in range(min(names.size(), 10)): list_names.append(names[i])
|
|
var name_str = ", ".join(list_names); if names.size() > 10: name_str += "... and %d others" % (names.size() - 10)
|
|
var text_lbl = Label.new(); text_lbl.text = "PERMANENTLY DELETE:\n" + name_str + "\n\nThis CANNOT be undone!"; text_lbl.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART; text_lbl.custom_minimum_size = Vector2(400, 0)
|
|
vbox.add_child(text_lbl); dialog.add_child(vbox); add_child(dialog); dialog.popup_centered()
|
|
dialog.confirmed.connect(func():
|
|
var r := await _rpc("admin_delete_users", {"user_ids": ids})
|
|
_set_status("Deleted %d accounts" % r.get("deleted", []).size(), CLR_STATUS_OK)
|
|
await _load_users()
|
|
dialog.queue_free()
|
|
)
|
|
|
|
# =============================================================================
|
|
# TAB 2: LEADERBOARD MANAGEMENT
|
|
# =============================================================================
|
|
func _load_leaderboard() -> void:
|
|
_clear_tree(lb_tree, _lb_root)
|
|
_set_status("Loading leaderboards...")
|
|
|
|
var res := await _rpc("get_leaderboard_stats", {})
|
|
if res.has("error"):
|
|
_set_status("Failed to load scores", CLR_STATUS_ERR)
|
|
return
|
|
|
|
lb_data = res.get("leaderboard", [])
|
|
count_label.text = "%d records" % lb_data.size()
|
|
lb_data.sort_custom(func(a, b): return a.get("high_score", 0) > b.get("high_score", 0))
|
|
|
|
var rank = 1
|
|
for entry in lb_data:
|
|
var item := _lb_root.create_child()
|
|
item.set_text(0, "#%d" % rank)
|
|
item.set_text(1, entry.get("display_name", "Unknown"))
|
|
item.set_text(2, str(entry.get("high_score", 0)))
|
|
item.set_text(3, str(entry.get("games_won", 0)))
|
|
item.set_text(4, str(entry.get("games_played", 0)))
|
|
item.add_button(5, _get_edit_icon(), 0, false, "Edit Score")
|
|
item.set_metadata(0, entry)
|
|
rank += 1
|
|
_set_status("")
|
|
|
|
func _on_lb_tree_button_clicked(item: TreeItem, _col: int, _id: int, _mouse: int) -> void:
|
|
_show_edit_score_dialog(item.get_metadata(0))
|
|
|
|
func _show_edit_score_dialog(entry: Dictionary) -> void:
|
|
var uid: String = entry.get("user_id", "")
|
|
var name: String = entry.get("display_name", "Unknown")
|
|
var dialog := AcceptDialog.new(); dialog.title = "Edit Score: " + name; dialog.min_size = Vector2i(350, 280)
|
|
var vbox := VBoxContainer.new(); vbox.add_theme_constant_override("separation", 10)
|
|
var info_lbl := Label.new(); info_lbl.text = "Manage player stats for: " + name; info_lbl.add_theme_color_override("font_color", CLR_HEADER); vbox.add_child(info_lbl)
|
|
var grid := GridContainer.new(); grid.columns = 2
|
|
var fields = {"high_score": SpinBox.new(), "games_won": SpinBox.new(), "games_played": SpinBox.new()}
|
|
for key in fields:
|
|
var lbl = Label.new()
|
|
lbl.text = key.capitalize() + ":"
|
|
grid.add_child(lbl)
|
|
var spin = fields[key] as SpinBox; spin.max_value = 1000000; spin.value = entry.get(key, 0); spin.custom_minimum_size.x = 120; grid.add_child(spin)
|
|
vbox.add_child(grid); var btn_vbox = VBoxContainer.new(); btn_vbox.add_theme_constant_override("separation", 5)
|
|
var save_btn = Button.new(); save_btn.text = "UPDATE SCORE"; save_btn.custom_minimum_size.y = 40; btn_vbox.add_child(save_btn)
|
|
var reset_btn = Button.new(); reset_btn.text = "RESET / DELETE RECORD"; reset_btn.add_theme_color_override("font_color", CLR_BTN_DEL); btn_vbox.add_child(reset_btn)
|
|
vbox.add_child(btn_vbox); dialog.add_child(vbox); add_child(dialog); dialog.popup_centered()
|
|
save_btn.pressed.connect(func():
|
|
var stats = {}; for k in fields: stats[k] = int(fields[k].value)
|
|
await _save_score_edit(uid, stats); dialog.queue_free()
|
|
)
|
|
reset_btn.pressed.connect(func(): _confirm_reset_score(uid, name); dialog.queue_free())
|
|
|
|
func _confirm_reset_score(uid: String, name: String) -> void:
|
|
var confirm := ConfirmationDialog.new(); confirm.title = "Reset Score?"; confirm.dialog_text = "Are you sure?"
|
|
add_child(confirm); confirm.popup_centered(); confirm.confirmed.connect(func():
|
|
var res := await _rpc("admin_delete_stats", {"user_id": uid})
|
|
if res.has("success"): await _load_leaderboard()
|
|
)
|
|
|
|
func _save_score_edit(uid: String, stats: Dictionary) -> void:
|
|
_set_status("Updating score...")
|
|
var res := await _rpc("admin_update_stats", {"user_id": uid, "stats": stats})
|
|
if res.has("success"): _set_status("Updated", CLR_STATUS_OK); await _load_leaderboard()
|
|
|
|
func _on_sync_leaderboard() -> void:
|
|
_set_status("Syncing all records to native leaderboard...", CLR_HEADER)
|
|
var res := await _rpc("admin_sync_leaderboard", {})
|
|
if res.has("success"):
|
|
var count = res.get("synced", 0)
|
|
_set_status("Successfully synced %d records!" % count, CLR_STATUS_OK)
|
|
await _load_leaderboard()
|
|
else:
|
|
_set_status("Sync failed: " + str(res.get("error", "Unknown")), CLR_STATUS_ERR)
|
|
|
|
# =============================================================================
|
|
# Common Helpers
|
|
# =============================================================================
|
|
func _clear_tree(tree: Tree, root: TreeItem) -> void:
|
|
if root:
|
|
var child := root.get_first_child()
|
|
while child:
|
|
var next := child.get_next()
|
|
root.remove_child(child)
|
|
child = next
|
|
|
|
func _get_edit_icon() -> Texture2D:
|
|
var img := Image.create(16, 16, false, Image.FORMAT_RGBA8)
|
|
img.fill(Color(0, 0, 0, 0))
|
|
for i in range(4, 12):
|
|
img.set_pixel(i, 11 - i + 4, CLR_TEXT)
|
|
img.set_pixel(i, 12 - i + 4, CLR_TEXT)
|
|
return ImageTexture.create_from_image(img)
|
|
|
|
# =============================================================================
|
|
# TAB 3: DAILY REWARDS
|
|
# =============================================================================
|
|
func _load_daily_rewards_config() -> void:
|
|
_set_status("Loading Daily Rewards Config...")
|
|
var res := await _rpc("get_daily_reward_config_admin", {})
|
|
if res.has("error"):
|
|
_set_status("Failed to load DR config", CLR_STATUS_ERR)
|
|
return
|
|
|
|
var config = res.get("config", {})
|
|
if config.is_empty():
|
|
for m in range(1, 13):
|
|
var m_str = "%02d" % m
|
|
var arr = []
|
|
for d in range(30):
|
|
arr.append({"type": "star", "amount": min(10 + d*5, 100)})
|
|
config[m_str] = arr
|
|
|
|
_daily_reward_config_data = config
|
|
|
|
month_option_btn.clear()
|
|
for i in range(1, 13):
|
|
month_option_btn.add_item(MONTH_NAMES[i - 1])
|
|
month_option_btn.set_item_metadata(i - 1, "%02d" % i)
|
|
|
|
if not _daily_reward_config_data.is_empty():
|
|
_current_dr_month = "01"
|
|
month_option_btn.select(0)
|
|
_build_dr_grid()
|
|
|
|
_set_status("Config Loaded", CLR_STATUS_OK)
|
|
|
|
func _on_dr_month_selected(index: int) -> void:
|
|
# Save current grid values into the dictionary before switching
|
|
_save_current_grid_to_dict()
|
|
|
|
_current_dr_month = month_option_btn.get_item_metadata(index)
|
|
_build_dr_grid()
|
|
|
|
func _save_current_grid_to_dict() -> void:
|
|
if _current_dr_month.is_empty(): return
|
|
var arr = []
|
|
for child in days_grid.get_children():
|
|
if child != day_config_template:
|
|
var opt = child.get_node("Margin/VBox/TypeOptionBtn") as OptionButton
|
|
var spin = child.get_node("Margin/VBox/AmountSpinBox") as SpinBox
|
|
var item_type = opt.get_item_text(opt.selected) if opt.selected >= 0 else "star"
|
|
arr.append({"type": item_type, "amount": int(spin.value)})
|
|
if not arr.is_empty():
|
|
_daily_reward_config_data[_current_dr_month] = arr
|
|
|
|
func _build_dr_grid() -> void:
|
|
for child in days_grid.get_children():
|
|
if child != day_config_template:
|
|
child.queue_free()
|
|
|
|
var arr = _daily_reward_config_data.get(_current_dr_month, [])
|
|
for i in range(arr.size()):
|
|
var slot = day_config_template.duplicate()
|
|
slot.visible = true
|
|
days_grid.add_child(slot)
|
|
|
|
var lbl = slot.get_node("Margin/VBox/DayLabel") as Label
|
|
var opt = slot.get_node("Margin/VBox/TypeOptionBtn") as OptionButton
|
|
var spin = slot.get_node("Margin/VBox/AmountSpinBox") as SpinBox
|
|
|
|
lbl.text = "Day " + str(i + 1)
|
|
var rdata = arr[i]
|
|
if typeof(rdata) == TYPE_DICTIONARY:
|
|
spin.value = rdata.get("amount", 0)
|
|
var type_str = rdata.get("type", "star")
|
|
var found = false
|
|
for j in range(opt.item_count):
|
|
if opt.get_item_text(j) == type_str:
|
|
opt.select(j)
|
|
found = true
|
|
break
|
|
if not found:
|
|
opt.add_item(type_str)
|
|
opt.select(opt.item_count - 1)
|
|
else:
|
|
# Fallback for old int format
|
|
spin.value = int(rdata)
|
|
opt.select(0)
|
|
|
|
func _save_daily_rewards_config() -> void:
|
|
_save_current_grid_to_dict()
|
|
_set_status("Saving config...")
|
|
var req = { "config": _daily_reward_config_data }
|
|
var res = await _rpc("set_daily_reward_config", req)
|
|
if res.has("error"):
|
|
_set_status("Save failed: " + res.get("error"), CLR_STATUS_ERR)
|
|
else:
|
|
_set_status("Config saved successfully!", CLR_STATUS_OK)
|
|
|
|
# =============================================================================
|
|
# TAB 4: ANNOUNCEMENTS
|
|
# =============================================================================
|
|
func _on_add_reward_pressed() -> void:
|
|
var row = reward_row_template.duplicate()
|
|
row.visible = true
|
|
rewards_list.add_child(row)
|
|
var remove_btn = row.get_node("RemoveBtn") as Button
|
|
remove_btn.pressed.connect(func(): row.queue_free())
|
|
|
|
func _on_find_user() -> void:
|
|
var input = target_user_edit.text.strip_edges()
|
|
if input.is_empty():
|
|
_resolved_user_id = ""
|
|
resolved_id_label.text = "(Global — all users)"
|
|
return
|
|
|
|
_set_status("Looking up user...")
|
|
var uid = await _resolve_target_user_id(input)
|
|
if uid.is_empty():
|
|
resolved_id_label.text = "NOT FOUND"
|
|
resolved_id_label.add_theme_color_override("font_color", CLR_STATUS_ERR)
|
|
_set_status("User not found: " + input, CLR_STATUS_ERR)
|
|
else:
|
|
_resolved_user_id = uid
|
|
resolved_id_label.text = "ID: " + uid.substr(0, 12) + "..."
|
|
resolved_id_label.add_theme_color_override("font_color", CLR_STATUS_OK)
|
|
_set_status("Found user: " + uid, CLR_STATUS_OK)
|
|
|
|
func _resolve_target_user_id(input: String) -> String:
|
|
"""Resolve a username, display_name, or user_id to a user_id.
|
|
Returns empty string if not found."""
|
|
if input.is_empty():
|
|
return ""
|
|
|
|
# If it looks like a UUID already, return as-is
|
|
if input.length() >= 32 and "-" in input:
|
|
return input
|
|
|
|
# Search in cached all_users first
|
|
for u in all_users:
|
|
var uname: String = u.get("username", "")
|
|
var dname: String = u.get("display_name", "")
|
|
var uid: String = u.get("user_id", "")
|
|
if uname.to_lower() == input.to_lower() or dname.to_lower() == input.to_lower():
|
|
return uid
|
|
|
|
# Cache miss — fetch from server
|
|
var res := await _rpc("admin_list_users", {})
|
|
if res.has("error"):
|
|
return ""
|
|
all_users = res.get("users", [])
|
|
|
|
for u in all_users:
|
|
var uname: String = u.get("username", "")
|
|
var dname: String = u.get("display_name", "")
|
|
var uid: String = u.get("user_id", "")
|
|
if uname.to_lower() == input.to_lower() or dname.to_lower() == input.to_lower():
|
|
return uid
|
|
|
|
return ""
|
|
|
|
func _on_send_mail() -> void:
|
|
var input = target_user_edit.text.strip_edges()
|
|
var title = title_edit.text.strip_edges()
|
|
var content = content_edit.text.strip_edges()
|
|
var start_date = start_date_edit.get_date_iso()
|
|
var end_date = end_date_edit.get_date_iso()
|
|
|
|
if title.is_empty() or content.is_empty():
|
|
_set_status("Title and content cannot be empty", CLR_STATUS_ERR)
|
|
return
|
|
|
|
# Resolve target
|
|
var target_uid := ""
|
|
if not input.is_empty():
|
|
if not _resolved_user_id.is_empty():
|
|
target_uid = _resolved_user_id
|
|
else:
|
|
_set_status("Resolving user...")
|
|
target_uid = await _resolve_target_user_id(input)
|
|
if target_uid.is_empty():
|
|
_set_status("User not found: " + input + ". Use Find button.", CLR_STATUS_ERR)
|
|
return
|
|
_resolved_user_id = target_uid
|
|
resolved_id_label.text = "ID: " + target_uid.substr(0, 12) + "..."
|
|
resolved_id_label.add_theme_color_override("font_color", CLR_STATUS_OK)
|
|
|
|
var rewards_arr = []
|
|
for child in rewards_list.get_children():
|
|
if not child.visible: continue # skip template
|
|
var type_opt = child.get_node("TypeOption") as OptionButton
|
|
var id_edit = child.get_node("IdEdit") as LineEdit
|
|
var amount_spin = child.get_node("AmountSpin") as SpinBox
|
|
|
|
var type_str = type_opt.get_item_text(type_opt.selected)
|
|
var r_id = id_edit.text.strip_edges()
|
|
rewards_arr.append({
|
|
"type": type_str,
|
|
"id": r_id,
|
|
"amount": int(amount_spin.value)
|
|
})
|
|
|
|
send_mail_btn.disabled = true
|
|
_set_status("Sending mail...")
|
|
|
|
var payload = {
|
|
"target_user_id": target_uid,
|
|
"title": title,
|
|
"content": content,
|
|
"start_date": start_date,
|
|
"end_date": end_date,
|
|
"rewards": rewards_arr
|
|
}
|
|
|
|
var res = await _rpc("admin_send_mail", payload)
|
|
send_mail_btn.disabled = false
|
|
|
|
if res.has("error"):
|
|
_set_status("Failed to send mail: " + res.get("error", "Unknown"), CLR_STATUS_ERR)
|
|
else:
|
|
_set_status("Mail sent successfully!", CLR_STATUS_OK)
|
|
target_user_edit.text = ""
|
|
_resolved_user_id = ""
|
|
resolved_id_label.text = ""
|
|
title_edit.text = ""
|
|
content_edit.text = ""
|
|
if start_date_edit.has_method("clear_date"):
|
|
start_date_edit.clear_date()
|
|
if end_date_edit.has_method("clear_date"):
|
|
end_date_edit.clear_date()
|
|
for child in rewards_list.get_children():
|
|
if child.visible: child.queue_free()
|
|
|
|
# =============================================================================
|
|
# TAB 5: MAIL MANAGER
|
|
# =============================================================================
|
|
func _load_mail() -> void:
|
|
_clear_tree(mail_tree, _mail_root)
|
|
_set_status("Loading mails...")
|
|
|
|
var res := await _rpc("admin_list_mail", {})
|
|
if res.has("error"):
|
|
_set_status("Failed: " + str(res.error), CLR_STATUS_ERR)
|
|
return
|
|
|
|
_all_server_mails = res.get("mails", [])
|
|
count_label.text = "%d mails" % _all_server_mails.size()
|
|
|
|
var now_str = Time.get_datetime_string_from_system(true)
|
|
|
|
for mail in _all_server_mails:
|
|
var item := _mail_root.create_child()
|
|
var mail_type: String = mail.get("type", "global")
|
|
var mail_title: String = mail.get("title", "No Title")
|
|
var sender: String = mail.get("sender", "SYSTEM")
|
|
var start_date: String = mail.get("start_date", "")
|
|
var expiry: String = mail.get("expiry_date", "")
|
|
|
|
# Type column
|
|
item.set_text(0, mail_type.to_upper())
|
|
if mail_type == "personal":
|
|
item.set_custom_color(0, CLR_ADMIN)
|
|
else:
|
|
item.set_custom_color(0, CLR_MOD)
|
|
|
|
# Title
|
|
item.set_text(1, mail_title)
|
|
|
|
# Sender
|
|
item.set_text(2, sender)
|
|
|
|
# Start date
|
|
item.set_text(3, start_date.substr(0, 10) if start_date.length() >= 10 else start_date)
|
|
|
|
# Expiry
|
|
var expiry_short = expiry.substr(0, 10) if expiry.length() >= 10 else expiry
|
|
item.set_text(4, expiry_short)
|
|
|
|
# Status
|
|
var end_date: String = mail.get("end_date", "")
|
|
var status := "ACTIVE"
|
|
if not end_date.is_empty() and now_str > end_date:
|
|
status = "ENDED"
|
|
item.set_custom_color(5, CLR_BTN_DEL)
|
|
elif not expiry.is_empty() and now_str > expiry:
|
|
status = "EXPIRED"
|
|
item.set_custom_color(5, CLR_BTN_DEL)
|
|
else:
|
|
item.set_custom_color(5, CLR_STATUS_OK)
|
|
item.set_text(5, status)
|
|
|
|
item.set_metadata(0, mail)
|
|
|
|
_update_mail_action_btns(null)
|
|
_set_status("")
|
|
|
|
func _on_mail_item_selected() -> void:
|
|
var item = mail_tree.get_selected()
|
|
_update_mail_action_btns(item)
|
|
|
|
func _update_mail_action_btns(item) -> void:
|
|
var has_sel = item != null
|
|
edit_mail_btn.disabled = not has_sel
|
|
end_mail_btn.disabled = not has_sel
|
|
delete_mail_server_btn.disabled = not has_sel
|
|
|
|
func _get_selected_mail() -> Dictionary:
|
|
var item = mail_tree.get_selected()
|
|
if item:
|
|
return item.get_metadata(0)
|
|
return {}
|
|
|
|
func _on_edit_mail_pressed() -> void:
|
|
var mail = _get_selected_mail()
|
|
if mail.is_empty(): return
|
|
_show_edit_mail_dialog(mail)
|
|
|
|
func _show_edit_mail_dialog(mail: Dictionary) -> void:
|
|
var mail_id: String = mail.get("id", "")
|
|
var mail_type: String = mail.get("type", "global")
|
|
var target_uid: String = mail.get("target_user_id", "")
|
|
|
|
var dialog := AcceptDialog.new()
|
|
dialog.title = "Edit Mail: " + mail.get("title", "")
|
|
dialog.min_size = Vector2i(480, 360)
|
|
var vbox := VBoxContainer.new()
|
|
vbox.add_theme_constant_override("separation", 10)
|
|
|
|
var id_lbl := Label.new()
|
|
id_lbl.text = "ID: " + mail_id + " | Type: " + mail_type
|
|
id_lbl.add_theme_color_override("font_color", CLR_DIM)
|
|
vbox.add_child(id_lbl)
|
|
|
|
var grid := GridContainer.new()
|
|
grid.columns = 2
|
|
grid.add_theme_constant_override("h_separation", 8)
|
|
grid.add_theme_constant_override("v_separation", 8)
|
|
|
|
var title_lbl := Label.new(); title_lbl.text = "Title:"; grid.add_child(title_lbl)
|
|
var title_input := LineEdit.new(); title_input.text = mail.get("title", ""); title_input.custom_minimum_size.x = 300; grid.add_child(title_input)
|
|
|
|
var content_lbl := Label.new(); content_lbl.text = "Content:"; grid.add_child(content_lbl)
|
|
var content_input := TextEdit.new(); content_input.text = mail.get("content", ""); content_input.custom_minimum_size = Vector2(300, 100); grid.add_child(content_input)
|
|
|
|
var end_lbl := Label.new(); end_lbl.text = "End Date:"; grid.add_child(end_lbl)
|
|
var end_picker: Button = load("res://scenes/ui/date_picker.tscn").instantiate()
|
|
end_picker.custom_minimum_size.x = 200
|
|
grid.add_child(end_picker)
|
|
# Pre-populate if existing end_date
|
|
var existing_end: String = mail.get("end_date", "")
|
|
if not existing_end.is_empty():
|
|
var parts = existing_end.substr(0, 10).split("-")
|
|
if parts.size() == 3:
|
|
end_picker.current_date = {"year": int(parts[0]), "month": int(parts[1]), "day": int(parts[2])}
|
|
end_picker.view_date = end_picker.current_date.duplicate()
|
|
end_picker.text = existing_end.substr(0, 10)
|
|
|
|
# Recipient row
|
|
var recip_lbl := Label.new(); recip_lbl.text = "Recipient:"; grid.add_child(recip_lbl)
|
|
var recip_hbox := HBoxContainer.new()
|
|
recip_hbox.add_theme_constant_override("separation", 6)
|
|
var recip_input := LineEdit.new()
|
|
recip_input.custom_minimum_size.x = 180
|
|
recip_input.placeholder_text = "Username or ID (empty = global)"
|
|
if mail_type == "personal" and not target_uid.is_empty():
|
|
recip_input.text = target_uid.substr(0, 12) + "..."
|
|
recip_input.tooltip_text = target_uid
|
|
recip_hbox.add_child(recip_input)
|
|
var recip_find_btn := Button.new()
|
|
recip_find_btn.text = "Find"
|
|
recip_find_btn.custom_minimum_size.x = 60
|
|
recip_hbox.add_child(recip_find_btn)
|
|
var recip_resolved_lbl := Label.new()
|
|
recip_resolved_lbl.add_theme_color_override("font_color", CLR_STATUS_OK)
|
|
recip_hbox.add_child(recip_resolved_lbl)
|
|
grid.add_child(recip_hbox)
|
|
|
|
var _edit_resolved_uid := target_uid
|
|
recip_find_btn.pressed.connect(func():
|
|
var inp = recip_input.text.strip_edges()
|
|
if inp.is_empty():
|
|
_edit_resolved_uid = ""
|
|
recip_resolved_lbl.text = "(Global)"
|
|
recip_resolved_lbl.add_theme_color_override("font_color", CLR_STATUS_OK)
|
|
return
|
|
var uid = await _resolve_target_user_id(inp)
|
|
if uid.is_empty():
|
|
recip_resolved_lbl.text = "NOT FOUND"
|
|
recip_resolved_lbl.add_theme_color_override("font_color", CLR_STATUS_ERR)
|
|
else:
|
|
_edit_resolved_uid = uid
|
|
recip_resolved_lbl.text = uid.substr(0, 12) + "..."
|
|
recip_resolved_lbl.add_theme_color_override("font_color", CLR_STATUS_OK)
|
|
)
|
|
|
|
vbox.add_child(grid)
|
|
|
|
var save_btn := Button.new()
|
|
save_btn.text = "Save Changes"
|
|
save_btn.custom_minimum_size.y = 40
|
|
vbox.add_child(save_btn)
|
|
|
|
dialog.add_child(vbox)
|
|
add_child(dialog)
|
|
dialog.popup_centered()
|
|
|
|
save_btn.pressed.connect(func():
|
|
# If user typed something but didn't click Find, auto-resolve
|
|
var recip_text = recip_input.text.strip_edges()
|
|
var final_target = _edit_resolved_uid
|
|
if not recip_text.is_empty() and _edit_resolved_uid == target_uid:
|
|
# Text changed but not resolved yet
|
|
var resolved = await _resolve_target_user_id(recip_text)
|
|
if not resolved.is_empty():
|
|
final_target = resolved
|
|
elif recip_text.is_empty():
|
|
final_target = ""
|
|
|
|
_set_status("Updating mail...")
|
|
var payload = {
|
|
"mail_id": mail_id,
|
|
"type": mail_type,
|
|
"target_user_id": target_uid,
|
|
"new_target_user_id": final_target,
|
|
"title": title_input.text,
|
|
"content": content_input.text,
|
|
"end_date": end_picker.get_date_iso()
|
|
}
|
|
var res = await _rpc("admin_update_mail", payload)
|
|
if res.has("success"):
|
|
_set_status("Mail updated!", CLR_STATUS_OK)
|
|
await _load_mail()
|
|
dialog.queue_free()
|
|
)
|
|
|
|
func _on_end_mail_pressed() -> void:
|
|
var mail = _get_selected_mail()
|
|
if mail.is_empty(): return
|
|
|
|
var mail_id: String = mail.get("id", "")
|
|
var mail_type: String = mail.get("type", "global")
|
|
var target_uid: String = mail.get("target_user_id", "")
|
|
|
|
var confirm := ConfirmationDialog.new()
|
|
confirm.title = "End Mail Now?"
|
|
confirm.dialog_text = "Set end_date to NOW for:\n" + mail.get("title", "Unknown")
|
|
add_child(confirm)
|
|
confirm.popup_centered()
|
|
confirm.confirmed.connect(func():
|
|
var now_iso = Time.get_datetime_string_from_system(true)
|
|
var res = await _rpc("admin_update_mail", {
|
|
"mail_id": mail_id,
|
|
"type": mail_type,
|
|
"target_user_id": target_uid,
|
|
"end_date": now_iso
|
|
})
|
|
if res.has("success"):
|
|
_set_status("Mail ended", CLR_STATUS_OK)
|
|
await _load_mail()
|
|
confirm.queue_free()
|
|
)
|
|
|
|
func _on_delete_mail_server_pressed() -> void:
|
|
var mail = _get_selected_mail()
|
|
if mail.is_empty(): return
|
|
|
|
var mail_id: String = mail.get("id", "")
|
|
var mail_type: String = mail.get("type", "global")
|
|
var target_uid: String = mail.get("target_user_id", "")
|
|
|
|
var confirm := ConfirmationDialog.new()
|
|
confirm.title = "PERMANENTLY Delete Mail?"
|
|
confirm.dialog_text = "Remove from server storage:\n" + mail.get("title", "Unknown") + "\n\nThis cannot be undone!"
|
|
add_child(confirm)
|
|
confirm.popup_centered()
|
|
confirm.confirmed.connect(func():
|
|
var res = await _rpc("admin_delete_mail_server", {
|
|
"mail_id": mail_id,
|
|
"type": mail_type,
|
|
"target_user_id": target_uid
|
|
})
|
|
if res.has("success"):
|
|
_set_status("Mail deleted from server", CLR_STATUS_OK)
|
|
await _load_mail()
|
|
confirm.queue_free()
|
|
)
|