feat: 2.3.1

This commit is contained in:
2026-05-12 17:55:53 +08:00
parent 13f3c3d591
commit 7ca11c6534
28 changed files with 1328 additions and 578 deletions
+49 -4
View File
@@ -90,7 +90,52 @@ func delete_mail(mail_id: String) -> bool:
func mark_as_read(mail_id: String) -> void:
if mail_id in read_ids: return
# Since there's no specific RPC for just marking as read, we can just do a claim with 0 rewards if needed,
# or add an RPC for it. For now we just let claim or delete handle the server-side state.
# Let's add an empty claim or just handle it purely local until claimed/deleted if it has no rewards.
pass
read_ids.append(mail_id)
_update_unread_count()
# Persist read state to server via delete_mail RPC pattern (just saves state)
_save_inbox_state()
func _save_inbox_state() -> void:
if not NakamaManager.session: return
var state_payload = {
"claimed_ids": claimed_ids,
"deleted_ids": [],
"read_ids": read_ids
}
# We use storage write via a lightweight RPC or direct storage
var r = await NakamaManager.client.rpc_async(
NakamaManager.session, "save_mail_state",
JSON.stringify(state_payload)
)
if r.is_exception():
push_warning("[MailManager] Could not save mail state: " + r.get_exception().message)
func read_all_and_claim_all() -> void:
"""Mark all mails as read and claim all unclaimed rewards."""
if mails.is_empty(): return
# Mark all as read
for mail in mails:
var mid = mail.get("id", "")
if mid not in read_ids:
read_ids.append(mid)
# Claim all unclaimed rewards
var to_claim: Array = []
for mail in mails:
var mid = mail.get("id", "")
if mid in claimed_ids: continue
var rewards = mail.get("rewards", [])
var has_rewards = false
if typeof(rewards) == TYPE_DICTIONARY:
has_rewards = rewards.get("star", 0) > 0 or rewards.get("gold", 0) > 0
elif typeof(rewards) == TYPE_ARRAY:
has_rewards = rewards.size() > 0
if has_rewards:
to_claim.append(mid)
for mid in to_claim:
await claim_reward(mid)
_update_unread_count()
mail_updated.emit()
+365 -2
View File
@@ -38,6 +38,8 @@ 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
@@ -47,6 +49,18 @@ var _current_dr_month: String = ""
@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 --
@@ -140,6 +154,19 @@ func _setup_columns() -> void:
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)
@@ -166,6 +193,16 @@ func _connect_signals() -> void:
# 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
@@ -189,6 +226,8 @@ func _on_tab_changed(tab_index: int) -> void:
await _load_leaderboard()
elif tab_index == 2:
await _load_daily_rewards_config()
elif tab_index == 4:
await _load_mail()
# =============================================================================
# RPC Helper
@@ -649,8 +688,60 @@ func _on_add_reward_pressed() -> void:
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 target = target_user_edit.text.strip_edges()
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()
@@ -659,6 +750,21 @@ func _on_send_mail() -> void:
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():
@@ -679,7 +785,7 @@ func _on_send_mail() -> void:
_set_status("Sending mail...")
var payload = {
"target_user_id": target,
"target_user_id": target_uid,
"title": title,
"content": content,
"start_date": start_date,
@@ -694,6 +800,9 @@ func _on_send_mail() -> void:
_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"):
@@ -702,3 +811,257 @@ func _on_send_mail() -> void:
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()
)
+53 -27
View File
@@ -6,11 +6,13 @@ signal closed
@onready var mail_list_vbox := %MailListVBox as VBoxContainer
@onready var mail_title_lbl := %MailTitleLbl as Label
@onready var mail_content_text := %MailContentText as RichTextLabel
@onready var sender_lbl := %SenderLbl as Label
@onready var dynamic_rewards_container := %DynamicRewardsContainer as HBoxContainer
@onready var reward_hbox_template := %RewardHBoxTemplate as HBoxContainer
@onready var sender_lbl = get_node_or_null("%SenderLbl")
@onready var dynamic_rewards_container := %DynamicRewardsContainer as VBoxContainer
@onready var reward_hbox_template = %RewardHBoxTemplate
@onready var action_btn := %ActionBtn as Button
@onready var read_all_btn := %ReadAllBtn as Button
@onready var empty_state_lbl := %EmptyStateLbl as Label
@onready var mail_btn_template := %MailBtnTemplate as Button
var _current_mail: Dictionary = {}
@@ -18,6 +20,7 @@ func _ready() -> void:
visible = false
close_btn.pressed.connect(hide_panel)
action_btn.pressed.connect(_on_action_pressed)
read_all_btn.pressed.connect(_on_read_all_pressed)
if MailManager:
MailManager.mail_updated.connect(_refresh_ui)
@@ -25,6 +28,7 @@ func show_panel() -> void:
visible = true
_clear_details()
if MailManager:
await MailManager.read_all_and_claim_all()
MailManager.fetch_mails()
_refresh_ui()
@@ -57,16 +61,15 @@ func _refresh_ui() -> void:
if _current_mail.is_empty() and mails.size() > 0:
_on_mail_selected(mails[0])
@onready var mail_btn_template := %MailBtnTemplate as Button
func _create_mail_button(mail: Dictionary) -> Button:
var btn = mail_btn_template.duplicate()
btn.visible = true
var title = btn.get_node("VBox/Title") as Label
title.text = mail.get("title", "No Title")
var title_lbl = btn.get_node("Margin/VBox/Title") as Label
if title_lbl:
title_lbl.text = mail.get("title", "No Title")
var date_lbl = btn.get_node("VBox/HBox/DateLbl") as Label
var date_lbl = btn.get_node("Margin/VBox/HBox/DateLbl") as Label
var date_str = mail.get("date", "")
var expiry_str = mail.get("expiry_date", "")
@@ -85,19 +88,21 @@ func _create_mail_button(mail: Dictionary) -> Button:
else:
label_text += " (Expired)"
date_lbl.text = label_text
if date_lbl:
date_lbl.text = label_text
var status_lbl = btn.get_node("VBox/HBox/StatusLbl") as Label
var status_lbl = btn.get_node("Margin/VBox/HBox/StatusLbl") as Label
var mail_id = mail.get("id", "")
if mail_id in MailManager.claimed_ids:
status_lbl.text = "CLAIMED"
status_lbl.add_theme_color_override("font_color", Color.GREEN)
elif mail_id in MailManager.read_ids:
status_lbl.text = "READ"
status_lbl.add_theme_color_override("font_color", Color.GRAY)
else:
status_lbl.text = "NEW"
status_lbl.add_theme_color_override("font_color", Color.YELLOW)
if status_lbl:
if mail_id in MailManager.claimed_ids:
status_lbl.text = "CLAIMED"
status_lbl.add_theme_color_override("font_color", Color.GREEN)
elif mail_id in MailManager.read_ids:
status_lbl.text = "READ"
status_lbl.add_theme_color_override("font_color", Color.GRAY)
else:
status_lbl.text = "NEW"
status_lbl.add_theme_color_override("font_color", Color.YELLOW)
return btn
@@ -113,7 +118,7 @@ func _on_mail_selected(mail: Dictionary) -> void:
func _clear_details() -> void:
mail_title_lbl.text = ""
mail_content_text.text = ""
sender_lbl.text = ""
if sender_lbl: sender_lbl.text = ""
for child in dynamic_rewards_container.get_children():
if child.visible: child.queue_free()
action_btn.hide()
@@ -121,7 +126,7 @@ func _clear_details() -> void:
func _update_details(mail: Dictionary) -> void:
mail_title_lbl.text = mail.get("title", "No Title")
mail_content_text.text = mail.get("content", "")
sender_lbl.text = "SENDER:\n" + mail.get("sender", "SYSTEM")
if sender_lbl: sender_lbl.text = "SENDER:\n" + mail.get("sender", "SYSTEM")
for child in dynamic_rewards_container.get_children():
if child.visible: child.queue_free()
@@ -141,16 +146,29 @@ func _update_details(mail: Dictionary) -> void:
var row = reward_hbox_template.duplicate()
row.visible = true
dynamic_rewards_container.add_child(row)
var amt_lbl = row.get_node("AmountLbl") as Label
var amt_lbl = row.get_node("Margin/HBox/VBox/AmountLbl") as Label
var type_lbl = row.get_node("Margin/HBox/VBox/TypeLbl") as Label
var t = r.get("type", "star")
var amt = r.get("amount", 0)
var id = r.get("id", "")
var rid = r.get("id", "")
if t == "star" or t == "gold":
amt_lbl.text = str(amt) + " " + t.to_upper()
else:
amt_lbl.text = str(amt) + " " + id
if amt_lbl: amt_lbl.text = "x" + str(amt)
if type_lbl: type_lbl.text = t.to_upper() if (t == "star" or t == "gold") else rid.to_upper()
# Fill empty slots up to 4
var added = rewards.size()
while added < 4:
added += 1
var empty_row = reward_hbox_template.duplicate()
empty_row.visible = true
dynamic_rewards_container.add_child(empty_row)
var icon_bg = empty_row.get_node_or_null("Margin/HBox/IconBg")
if icon_bg: icon_bg.color = Color(0, 0, 0, 0)
var t_lbl = empty_row.get_node_or_null("Margin/HBox/VBox/TypeLbl")
if t_lbl: t_lbl.text = ""
var a_lbl = empty_row.get_node_or_null("Margin/HBox/VBox/AmountLbl")
if a_lbl: a_lbl.text = ""
action_btn.show()
var mail_id = mail.get("id", "")
@@ -178,3 +196,11 @@ func _on_action_pressed() -> void:
if ok:
_current_mail = {}
_clear_details()
func _on_read_all_pressed() -> void:
if not MailManager: return
for mail in MailManager.mails:
var mid = mail.get("id", "")
if mid not in MailManager.read_ids:
MailManager.mark_as_read(mid)
_refresh_ui()
+5
View File
@@ -83,6 +83,11 @@ func _show_tab(tab: String) -> void:
_friends_view.visible = tab == "friends"
_dm_view.visible = tab == "dm"
_search_tab_btn.set_pressed_no_signal(tab == "search")
_friends_tab_btn.set_pressed_no_signal(tab == "friends")
if _dm_tab_btn:
_dm_tab_btn.set_pressed_no_signal(tab == "dm")
# Auto-load search results on first open
if tab == "search" and _search_results_list.get_child_count() == 0:
_on_search_pressed()