f9773bd8ba
Introduce OuterCol() function to dynamically set the outer border color based on the announcement_theme configuration. The border will be white when theme is set to "white", otherwise it defaults to the accent color. This allows for better visual customization of the admin menu appearance.
373 lines
14 KiB
Lua
373 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)
|
|
|
|
-- couleur des contours extérieurs selon la config ("blue" ou "white")
|
|
local function OuterCol()
|
|
return (BladW.Announcement.Conf or {}).announcement_theme == "white" and COL_TEXT or COL_ACCENT
|
|
end
|
|
|
|
-- 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, OuterCol(), 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
|