Files
bladwin-RP/lua/bladw_announcement/client/admin_menu/cl_menu.lua
T
nocode 490d5a2194 feat(admin_menu): add recipient selection dropdown to announcement admin panel
Add a new dropdown menu for selecting announcement recipients with three options:
- "Tout le monde" (Everyone): broadcasts to all players
- "Groupe" (Group): allows selecting specific user groups
- "Joueurs" (Players): allows selecting individual players

Implement an animated scrollable list panel that displays checkboxes for group or player selection based on the chosen scope. The panel height animates smoothly between collapsed and expanded states.

Extract the frequently used outline color (255, 255, 255, 60) into a COL_LINE constant. Add a DrawArrow helper function to render dropdown indicators. Update the send button validation logic to ensure at least one recipient is selected before sending.

The panel remains open after sending so the admin can keep their text and reuse it if needed.
2026-07-20 15:41:03 +02:00

368 lines
14 KiB
Lua

local rndx = include("lib/rndx.lua")
local logo = Material("nocode/announcement/logo.png")
BladW = BladW or {}
BladW.Announcement = BladW.Announcement or {}
-- mêmes couleurs que les annonces
local COL_BG = Color(0, 0, 0, 200)
local COL_PANEL = Color(0, 0, 0, 160)
local COL_ACCENT = Color(76, 119, 200)
local COL_ACCENT_DIM = Color(60, 90, 150)
local COL_TEXT = Color(255, 255, 255)
local COL_RED = Color(255, 80, 80)
local COL_GRAY = Color(54, 54, 54, 255)
local COL_MUTED = Color(160, 160, 160)
local COL_WARN = Color(255, 200, 50)
local COL_LINE = Color(255, 255, 255, 60)
-- nb de caractères d'un champ (UTF-8, un accent = 1)
local function EntryLen(entry)
local v = entry:GetValue()
return utf8.len(v) or #v
end
-- petit triangle warning dessiné à la main (comme ça pas besoin de font emoji)
local function DrawWarning(x, y, size)
draw.NoTexture()
surface.SetDrawColor(COL_WARN)
surface.DrawPoly({
{ x = x + size * 0.5, y = y },
{ x = x + size, y = y + size },
{ x = x, y = y + size },
})
draw.SimpleText("!", "DermaDefaultBold", x + size * 0.5, y + size * 0.62, Color(30, 30, 30), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
end
-- mon style pour les champs de texte
local function StyleEntry(entry, placeholder, default)
entry:SetFont("bladw_text_Medium")
entry:SetText(default or "")
entry:SetPlaceholderText(placeholder or "")
entry:SetTextColor(COL_TEXT)
entry:SetPaintBackground(false)
entry.Paint = function(self, w, h)
rndx.Draw(10, 0, 0, w, h, COL_PANEL, rndx.SHAPE_FIGMA)
local outline = self:IsEditing() and COL_ACCENT or COL_LINE
rndx.DrawOutlined(10, 0, 0, w, h, outline, 2, rndx.SHAPE_FIGMA)
self:DrawTextEntryText(COL_TEXT, COL_ACCENT, COL_TEXT)
end
end
-- petite flèche bas
local function DrawArrow(w, h)
draw.NoTexture()
surface.SetDrawColor(COL_MUTED)
local ax = w - 24
surface.DrawPoly({
{ x = ax, y = h * 0.5 - 2 },
{ x = ax + 10, y = h * 0.5 - 2 },
{ x = ax + 5, y = h * 0.5 + 4 },
})
end
local LIST_H = 130 -- hauteur du panneau de cases une fois ouvert
local LIST_Y = 440 -- haut du panneau
local function OpenAAdminMenu()
-- si le menu est déjà ouvert je le vire
if IsValid(BladW_AdminMenu) then BladW_AdminMenu:Remove() end
local selectedPreset = ""
local maxLen = (BladW.Announcement.Conf or {}).announcement_max_length or 120
-- destinataires : scope + sélection multiple
local scope, scopeLabel = "all", "Tout le monde"
local selGroups, selPlayers = {}, {}
local expand = 0 -- 0 = replié, 1 = ouvert (animé)
local frame = vgui.Create("DFrame")
BladW_AdminMenu = frame
frame:SetSize(500, 640) -- taille max -> sert au centrage, on démarre replié
frame:Center()
frame:SetTall(498)
frame:SetTitle("")
frame:ShowCloseButton(false)
frame:MakePopup()
frame.Paint = function(self, w, h)
rndx.Draw(20, 0, 0, w, h, nil, rndx.SHAPE_FIGMA + rndx.BLUR)
rndx.Draw(20, 0, 0, w, h, COL_BG, rndx.SHAPE_FIGMA)
rndx.DrawOutlined(20, 0, 0, w, h, COL_TEXT, 3, rndx.SHAPE_FIGMA)
rndx.DrawMaterial(0, w * 0.5 - 30, 20, 60, 60, COL_TEXT, logo)
draw.SimpleText("Envoyer une annonce", "bladw_text", w * 0.5, 94, COL_TEXT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
draw.SimpleText("Presets", "bladw_text_Medium", 40, 118, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
draw.SimpleText("Destinataires", "bladw_text_Medium", 40, 372, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
end
-- bouton fermer
local close = vgui.Create("DButton", frame)
close:SetSize(30, 30)
close:SetPos(frame:GetWide() - 42, 14)
close:SetText("")
close.Paint = function(self, w, h)
rndx.Draw(8, 0, 0, w, h, self:IsHovered() and COL_RED or COL_GRAY, rndx.SHAPE_FIGMA)
draw.SimpleText("X", "bladw_text_Medium", w * 0.5, h * 0.5, COL_TEXT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
end
close.DoClick = function() frame:Remove() end
-- champs
local titleEntry = vgui.Create("DTextEntry", frame)
titleEntry:SetPos(40, 240)
titleEntry:SetSize(420, 40)
StyleEntry(titleEntry, "Titre de l'annonce", "Annonce")
local msgEntry = vgui.Create("DTextEntry", frame)
msgEntry:SetPos(40, 288)
msgEntry:SetSize(420, 40)
StyleEntry(msgEntry, "Message de l'annonce")
msgEntry.Paint = function(self, w, h)
local over = EntryLen(self) > maxLen
rndx.Draw(10, 0, 0, w, h, COL_PANEL, rndx.SHAPE_FIGMA)
local outline = over and COL_RED or (self:IsEditing() and COL_ACCENT or COL_LINE)
rndx.DrawOutlined(10, 0, 0, w, h, outline, over and 3 or 2, rndx.SHAPE_FIGMA)
self:DrawTextEntryText(COL_TEXT, COL_ACCENT, COL_TEXT)
end
-- ligne du bas : warning à gauche, compteur à droite
local status = vgui.Create("DPanel", frame)
status:SetPos(40, 332)
status:SetSize(420, 26)
status:SetPaintBackground(false)
status.Paint = function(self, w, h)
local len = EntryLen(msgEntry)
local over = len > maxLen
if frame.sendFeedback and (frame.sendFeedbackUntil or 0) > SysTime() then
DrawWarning(0, h * 0.5 - 8, 16)
draw.SimpleText(frame.sendFeedback, "bladw_text_Medium", 24, h * 0.5, COL_WARN, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
elseif over then
DrawWarning(0, h * 0.5 - 8, 16)
draw.SimpleText("Trop de caractères", "bladw_text_Medium", 24, h * 0.5, COL_RED, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
end
draw.SimpleText(len .. " / " .. maxLen, "bladw_text_Medium", w, h * 0.5,
over and COL_RED or COL_MUTED, TEXT_ALIGN_RIGHT, TEXT_ALIGN_CENTER)
end
-- panneau de cases (groupes ou joueurs), hauteur animée
local list = vgui.Create("DScrollPanel", frame)
list:SetPos(40, LIST_Y)
list:SetSize(420, 1)
list.Paint = function(self, w, h)
rndx.Draw(8, 0, 0, w, h, Color(0, 0, 0, 90), rndx.SHAPE_FIGMA)
end
local vbar = list:GetVBar()
vbar:SetWide(8)
vbar.Paint = function() end
vbar.btnUp.Paint = function() end
vbar.btnDown.Paint = function() end
vbar.btnGrip.Paint = function(self, w, h)
rndx.Draw(4, 0, 0, w, h, COL_LINE, rndx.SHAPE_FIGMA)
end
local function AddRow(label, isChecked, onToggle)
local row = list:Add("DButton")
row:Dock(TOP)
row:DockMargin(4, 4, 8, 0)
row:SetTall(28)
row:SetText("")
row.Paint = function(self, w, h)
rndx.Draw(6, 0, 0, w, h, COL_PANEL, rndx.SHAPE_FIGMA)
local bs, bx = 18, 8
local by = (h - bs) * 0.5
rndx.DrawOutlined(4, bx, by, bs, bs, isChecked() and COL_ACCENT or COL_LINE, 2, rndx.SHAPE_FIGMA)
if isChecked() then
rndx.Draw(3, bx + 4, by + 4, bs - 8, bs - 8, COL_ACCENT, rndx.SHAPE_FIGMA)
end
draw.SimpleText(label, "bladw_text_Medium", bx + bs + 10, h * 0.5, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
end
row.DoClick = function() onToggle() end
end
local function RebuildList()
list:Clear()
if scope == "group" then
local seen = {}
for _, p in ipairs(player.GetAll()) do
local g = p:GetUserGroup()
if g and g ~= "" and not seen[g] then
seen[g] = true
AddRow(g, function() return selGroups[g] end, function()
selGroups[g] = (not selGroups[g]) or nil
end)
end
end
elseif scope == "players" then
for _, p in ipairs(player.GetAll()) do
AddRow(p:Nick(), function() return selPlayers[p] end, function()
selPlayers[p] = (not selPlayers[p]) or nil
end)
end
end
end
-- dropdown du scope
local scopeBtn = vgui.Create("DButton", frame)
scopeBtn:SetPos(40, 392)
scopeBtn:SetSize(420, 40)
scopeBtn:SetText("")
scopeBtn.Paint = function(self, w, h)
rndx.Draw(10, 0, 0, w, h, COL_PANEL, rndx.SHAPE_FIGMA)
rndx.DrawOutlined(10, 0, 0, w, h, self:IsHovered() and COL_ACCENT or COL_LINE, 2, rndx.SHAPE_FIGMA)
draw.SimpleText(scopeLabel, "bladw_text_Medium", 12, h * 0.5, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
DrawArrow(w, h)
end
scopeBtn.DoClick = function(self)
local menu = DermaMenu()
menu:SetMinimumWidth(self:GetWide())
menu.Paint = function(s, w, h)
rndx.Draw(8, 0, 0, w, h, Color(18, 18, 18, 252), rndx.SHAPE_FIGMA)
rndx.DrawOutlined(8, 0, 0, w, h, COL_LINE, 1, rndx.SHAPE_FIGMA)
end
local function opt(label, id)
local o = menu:AddOption(label, function()
scope, scopeLabel = id, label
RebuildList()
end)
o:SetTall(30)
o:SetText("")
o.Paint = function(s, w, h)
if s:IsHovered() then rndx.Draw(6, 3, 1, w - 6, h - 2, COL_ACCENT, rndx.SHAPE_FIGMA) end
draw.SimpleText(label, "bladw_text_Medium", 12, h * 0.5, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
end
end
opt("Tout le monde", "all")
opt("Groupe", "group")
opt("Joueurs", "players")
local mx, my = self:LocalToScreen(0, self:GetTall() + 2)
menu:Open(mx, my)
end
-- bouton envoyer (position gérée par le Think selon l'ouverture)
local send = vgui.Create("DButton", frame)
send:SetPos(40, LIST_Y)
send:SetSize(420, 44)
send:SetText("")
send.Paint = function(self, w, h)
local over = EntryLen(msgEntry) > maxLen
local col = over and COL_GRAY or (self:IsHovered() and COL_ACCENT or COL_ACCENT_DIM)
rndx.Draw(12, 0, 0, w, h, col, rndx.SHAPE_FIGMA)
draw.SimpleText("Envoyer", "bladw_text", w * 0.5, h * 0.5, over and COL_MUTED or COL_TEXT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
end
send.DoClick = function()
local function fail(msg)
frame.sendFeedback = msg
frame.sendFeedbackUntil = SysTime() + 4
surface.PlaySound("buttons/button10.wav")
end
if EntryLen(msgEntry) > maxLen then return fail("Trop de caractères") end
if msgEntry:GetValue() == "" and selectedPreset == "" then return fail("Message vide") end
local groups, players = {}, {}
if scope == "group" then
for g in pairs(selGroups) do groups[#groups + 1] = g end
if #groups == 0 then return fail("Choisis au moins un groupe") end
elseif scope == "players" then
for p in pairs(selPlayers) do if IsValid(p) then players[#players + 1] = p end end
if #players == 0 then return fail("Choisis au moins un joueur") end
end
net.Start("bladWSendAnnouncement")
net.WriteString(selectedPreset)
net.WriteString(titleEntry:GetValue())
net.WriteString(msgEntry:GetValue())
net.WriteString(scope)
if scope == "group" then
net.WriteUInt(#groups, 8)
for _, g in ipairs(groups) do net.WriteString(g) end
elseif scope == "players" then
net.WriteUInt(#players, 8)
for _, p in ipairs(players) do net.WriteEntity(p) end
end
net.SendToServer()
end
-- ESC ferme le menu (sans menu pause) + animation d'ouverture du panneau
frame.Think = function(self)
if gui.IsGameUIVisible() then
gui.HideGameUI()
self:Remove()
return
end
local target = (scope == "all") and 0 or 1
expand = Lerp(FrameTime() * 12, expand, target)
if math.abs(expand - target) < 0.002 then expand = target end
local lh = math.Round(expand * LIST_H)
list:SetVisible(lh > 2)
list:SetTall(math.max(lh, 1))
local sy = LIST_Y + lh + (lh > 0 and 12 or 0)
send:SetPos(40, sy)
self:SetTall(sy + send:GetTall() + 14)
end
-- boutons preset : remplissent les champs, retiennent le preset et règlent les destinataires
local presets = vgui.Create("DIconLayout", frame)
presets:SetPos(40, 134)
presets:SetSize(420, 90)
presets:SetSpaceX(8)
presets:SetSpaceY(8)
for key, preset in SortedPairs(BladW.Announcement.Presets or {}) do
local b = presets:Add("DButton")
b:SetSize(134, 34)
b:SetText("")
b.Paint = function(self, w, h)
local on = (selectedPreset == key) or self:IsHovered()
rndx.Draw(8, 0, 0, w, h, on and COL_ACCENT or COL_GRAY, rndx.SHAPE_FIGMA)
draw.SimpleText(preset.title or key, "bladw_text_Medium", w * 0.5, h * 0.5, COL_TEXT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
end
b.DoClick = function()
selectedPreset = key
titleEntry:SetText(preset.title or "")
msgEntry:SetText(preset.text or "")
for k in pairs(selGroups) do selGroups[k] = nil end
for k in pairs(selPlayers) do selPlayers[k] = nil end
local t = preset.target
if not t or t == "" or t == "all" then
scope, scopeLabel = "all", "Tout le monde"
else
scope, scopeLabel = "group", "Groupe"
selGroups[t] = true
end
RebuildList()
end
end
RebuildList()
end
-- réponse du serveur après un envoi : publié -> on ferme, refusé -> on garde le menu + on dit pourquoi
net.Receive("bladWAnnouncementResult", function()
local ok = net.ReadBool()
local left = net.ReadUInt(8)
if not IsValid(BladW_AdminMenu) then return end
if ok then
BladW_AdminMenu:Remove()
else
BladW_AdminMenu.sendFeedback = left > 0 and ("Annonce déjà en cours (" .. left .. "s)") or "Envoi impossible"
BladW_AdminMenu.sendFeedbackUntil = SysTime() + 5
end
end)
return OpenAAdminMenu