Files
bladwin-RP/lua/bladw_warn/client/cl_menu.lua
T
nocode 9ab34fe802 feat(client): add main warning menu interface with player list and history
Implement comprehensive client-side menu for the warning system including:
- Responsive player list with search functionality
- Warning history display with details
- Modal dialog for applying warnings with reason selection
- Custom styling with color palette and rounded corners
- Font Awesome icon integration
- Dynamic font scaling based on screen resolution
- Network communication for warning actions
2026-07-20 23:29:27 +02:00

530 lines
20 KiB
Lua

local rndx = include("lib/rndx.lua")
local logo = Material("nocode/announcement/logo.png")
BladW = BladW or {}
BladW.Warn = BladW.Warn or {}
BladW.Warn.List = BladW.Warn.List or {}
-- palette
local COL_BG = Color(24, 24, 28)
local COL_PANEL = Color(38, 38, 44)
local COL_INPUT = Color(30, 30, 35)
local COL_LINE = Color(255, 255, 255, 22)
local COL_TEXT = Color(236, 236, 240)
local COL_MUTED = Color(150, 150, 160)
local COL_BLUE = Color(74, 108, 224)
local COL_RED = Color(226, 76, 76)
local COL_GREEN = Color(45, 170, 96)
local COL_ORANGE = Color(232, 168, 32)
local COL_SEL = Color(50, 50, 60)
local COL_BAR = Color(92, 92, 104) -- petit bout à gauche des joueurs (non sélectionné)
local COL_BADGE = Color(22, 22, 26) -- fond du badge "warns actifs" (gris foncé)
local function Lighten(c, a)
a = a or 22
return Color(math.min(c.r + a, 255), math.min(c.g + a, 255), math.min(c.b + a, 255), c.a)
end
-- ============================================================
-- FontAwesome (vrais glyphes)
-- Le .ttf doit être chargé côté client : dépose un fa-solid-900.ttf
-- dans resource/fonts/. Change FA_FONT si ta police a un autre nom
-- de famille, et les codepoints si ta version de FA diffère.
-- ============================================================
local FA_FONT = "Font Awesome 6 Free Solid" -- nom de famille Windows (name ID 1), pas le typo "Font Awesome 6 Free"
local FA = {
search = 0xf002, -- magnifying-glass
shield = 0xf3ed, -- shield-halved (shield-alt en FA5)
card = 0xf2c2, -- id-card
copy = 0xf0c5, -- copy
trash = 0xf1f8, -- trash
}
local function FAIcon(cp, font, x, y, col, ax, ay)
draw.SimpleText(utf8.char(cp), font or "bw_fa", x, y, col, ax or TEXT_ALIGN_CENTER, ay or TEXT_ALIGN_CENTER)
end
-- fonts responsives
local FS = 1
local function BuildFonts()
FS = math.Clamp(ScrH() / 1080, 0.7, 1.5)
surface.CreateFont("bw_title", { font = "Poppins-Bold", size = math.Round(24 * FS), weight = 700, antialias = true })
surface.CreateFont("bw_big", { font = "Poppins-SemiBold", size = math.Round(19 * FS), weight = 600, antialias = true })
surface.CreateFont("bw_med", { font = "Poppins-Medium", size = math.Round(15 * FS), weight = 500, antialias = true })
surface.CreateFont("bw_bold", { font = "Poppins-Bold", size = math.Round(15 * FS), weight = 700, antialias = true })
surface.CreateFont("bw_small", { font = "Poppins-Regular", size = math.Round(13 * FS), weight = 400, antialias = true })
surface.CreateFont("bw_fa", { font = FA_FONT, size = math.Round(15 * FS), weight = 900, extended = true })
surface.CreateFont("bw_fa_lg", { font = FA_FONT, size = math.Round(19 * FS), weight = 900, extended = true })
end
BuildFonts()
hook.Add("OnScreenSizeChanged", "bw_warn_fonts", BuildFonts)
-- temps relatif ("2 mois", "3 j"...)
local function RelTime(t)
local d = os.time() - (t or 0)
if d < 60 then return "à l'instant"
elseif d < 3600 then return math.floor(d / 60) .. " min"
elseif d < 86400 then return math.floor(d / 3600) .. " h"
elseif d < 2592000 then return math.floor(d / 86400) .. " j"
elseif d < 31536000 then return math.floor(d / 2592000) .. " mois"
else return math.floor(d / 31536000) .. " an(s)" end
end
local function Btn(parent, label, col, font)
local b = vgui.Create("DButton", parent)
b:SetText("")
b.Paint = function(self, w, h)
rndx.Draw(8, 0, 0, w, h, self:IsHovered() and Lighten(col) or col, rndx.SHAPE_FIGMA)
draw.SimpleText(label, font or "bw_med", w * 0.5, h * 0.5, COL_TEXT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
end
return b
end
local function StyleEntry(e, ph, multiline)
e:SetFont("bw_med")
e:SetTextColor(COL_TEXT)
e:SetPlaceholderText(ph or "")
e:SetPaintBackground(false)
if multiline then e:SetMultiline(true) end
e.Paint = function(self, w, h)
rndx.Draw(8, 0, 0, w, h, COL_INPUT, rndx.SHAPE_FIGMA)
self:DrawTextEntryText(COL_TEXT, COL_BLUE, COL_TEXT)
end
end
local function StyleScroll(sp)
local bar = sp:GetVBar()
bar:SetWide(8)
bar.Paint = function() end
bar.btnUp.Paint = function() end
bar.btnDown.Paint = function() end
bar.btnGrip.Paint = function(self, w, h) rndx.Draw(4, 0, 0, w, h, COL_LINE, rndx.SHAPE_FIGMA) end
end
local function GetEntry(sid)
for _, e in ipairs(BladW.Warn.List) do
if e.sid == sid then return e end
end
end
-- ============================================================
-- Modal « Appliquer un warn »
-- ============================================================
local function OpenWarnModal(sid)
local reasons = (BladW.Warn.Conf or {}).reasons or {}
local chosen, chosenLabel = nil, "Choisissez une raison..."
local m = vgui.Create("DFrame")
m:SetSize(math.min(ScrW() * 0.42, 580), math.min(ScrH() * 0.5, 400))
m:Center()
m:SetTitle("")
m:ShowCloseButton(false)
m:MakePopup()
m:DockPadding(30, 66, 30, 26)
m.Paint = function(self, w, h)
rndx.Draw(16, 0, 0, w, h, COL_PANEL, rndx.SHAPE_FIGMA)
rndx.DrawOutlined(16, 0, 0, w, h, COL_LINE, 2, rndx.SHAPE_FIGMA)
draw.SimpleText("Appliquer un warn", "bw_title", w * 0.5, 36, COL_TEXT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
end
local mclose = vgui.Create("DButton", m)
mclose:SetSize(32, 32)
mclose:SetText("")
mclose.Paint = function(self, w, h)
rndx.Draw(8, 0, 0, w, h, self:IsHovered() and COL_RED or COL_BG, rndx.SHAPE_FIGMA)
draw.SimpleText("X", "bw_med", w * 0.5, h * 0.5, COL_RED, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
end
mclose.DoClick = function() m:Remove() end
m.PerformLayout = function(self, w, h) mclose:SetPos(w - 42, 14) end
-- dropdown des raisons
local rdd = vgui.Create("DButton", m)
rdd:Dock(TOP)
rdd:SetTall(46)
rdd:SetText("")
rdd.Paint = function(self, w, h)
rndx.Draw(8, 0, 0, w, h, COL_INPUT, rndx.SHAPE_FIGMA)
rndx.DrawOutlined(8, 0, 0, w, h, COL_LINE, 2, rndx.SHAPE_FIGMA)
draw.SimpleText(chosenLabel, "bw_med", 14, h * 0.5, chosen and COL_TEXT or COL_MUTED, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
draw.NoTexture()
surface.SetDrawColor(COL_MUTED)
local ax = w - 26
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
rdd.DoClick = function(self)
local menu = DermaMenu()
menu:SetMinimumWidth(self:GetWide())
menu.Paint = function(s, w, h)
rndx.Draw(8, 0, 0, w, h, Color(20, 20, 24, 252), rndx.SHAPE_FIGMA)
rndx.DrawOutlined(8, 0, 0, w, h, COL_LINE, 1, rndx.SHAPE_FIGMA)
end
for _, r in ipairs(reasons) do
local o = menu:AddOption(r, function() chosen, chosenLabel = r, r 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_BLUE, rndx.SHAPE_FIGMA) end
draw.SimpleText(r, "bw_med", 12, h * 0.5, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
end
end
local mx, my = self:LocalToScreen(0, self:GetTall() + 2)
menu:Open(mx, my)
end
-- commentaire du staff
local clabel = vgui.Create("DLabel", m)
clabel:Dock(TOP)
clabel:DockMargin(0, 18, 0, 6)
clabel:SetTall(20)
clabel:SetFont("bw_med")
clabel:SetTextColor(COL_TEXT)
clabel:SetText("Commentaires du staff :")
local comment = vgui.Create("DTextEntry", m)
comment:Dock(TOP)
comment:SetTall(90)
StyleEntry(comment, "Joueur a surveiller...", true)
-- bouton Avertir
local warnBtn = Btn(m, "Avertir", COL_GREEN, "bw_big")
warnBtn:Dock(BOTTOM)
warnBtn:SetTall(46)
warnBtn:DockMargin(90, 16, 90, 0)
warnBtn.DoClick = function()
if not chosen then
surface.PlaySound("buttons/button10.wav")
return
end
net.Start("bladw_warn_add")
net.WriteString(sid)
net.WriteString(chosen)
net.WriteString(comment:GetValue() or "")
net.SendToServer()
m:Remove()
end
end
-- ============================================================
-- Menu principal
-- ============================================================
function BladW.Warn.Open()
if IsValid(BladW.Warn.Frame) then BladW.Warn.Frame:Remove() end
local W = math.min(ScrW() * 0.72, 1180)
local H = math.min(ScrH() * 0.80, 750)
local frame = vgui.Create("DFrame")
BladW.Warn.Frame = frame
frame:SetSize(W, H)
frame:Center()
frame:SetTitle("")
frame:ShowCloseButton(false)
frame:MakePopup()
frame:DockPadding(16, 16, 16, 16)
frame.Paint = function(self, w, h)
local lw = w * 0.3 + 24 -- fin de la zone gauche (alignée sur la colonne joueurs)
-- côté droit : blur + voile (assez sombre mais on voit le fond)
rndx.Draw(18, 0, 0, w, h, nil, rndx.SHAPE_FIGMA + rndx.BLUR)
rndx.Draw(18, 0, 0, w, h, Color(24, 24, 28, 200), rndx.SHAPE_FIGMA)
-- côté gauche : couleur pleine (coins arrondis à gauche, carré à droite)
rndx.Draw(18, 0, 0, lw, h, COL_BG, rndx.SHAPE_FIGMA + rndx.NO_TR + rndx.NO_BR)
-- bordure blanche (comme les annonces)
rndx.DrawOutlined(18, 0, 0, w, h, Color(255, 255, 255), 3, rndx.SHAPE_FIGMA)
end
local selectedSID
-- ===== colonne gauche =====
local left = vgui.Create("DPanel", frame)
left:Dock(LEFT)
left:SetWide(W * 0.3)
left:DockMargin(0, 0, 16, 0)
left:SetPaintBackground(false)
local lhead = vgui.Create("DPanel", left)
lhead:Dock(TOP)
lhead:SetTall(46)
lhead:SetPaintBackground(false)
lhead.Paint = function(self, w, h)
rndx.DrawMaterial(0, 0, (h - 42) * 0.5, 42, 42, color_white, logo)
draw.SimpleText("Joueurs", "bw_title", 54, h * 0.5, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
end
local searchBox = vgui.Create("DPanel", left)
searchBox:Dock(TOP)
searchBox:DockMargin(0, 8, 0, 10)
searchBox:SetTall(40)
searchBox:SetPaintBackground(false)
searchBox.Paint = function(self, w, h)
rndx.Draw(8, 0, 0, w, h, COL_INPUT, rndx.SHAPE_FIGMA)
FAIcon(FA.search, "bw_fa", 22, h * 0.5, COL_MUTED)
end
local search = vgui.Create("DTextEntry", searchBox)
search:Dock(FILL)
search:DockMargin(38, 0, 10, 0)
search:SetFont("bw_med")
search:SetTextColor(COL_TEXT)
search:SetPlaceholderText("Rechercher un joueur")
search:SetPaintBackground(false)
search.Paint = function(self, w, h)
self:DrawTextEntryText(COL_TEXT, COL_BLUE, COL_TEXT)
end
local plist = vgui.Create("DScrollPanel", left)
plist:Dock(FILL)
StyleScroll(plist)
-- ===== colonne droite =====
local right = vgui.Create("DPanel", frame)
right:Dock(FILL)
right:SetPaintBackground(false)
local rhead = vgui.Create("DPanel", right)
rhead:Dock(TOP)
rhead:SetTall(58)
rhead:SetPaintBackground(false)
rhead.Paint = function(self, w, h)
draw.SimpleText("Historique", "bw_title", w * 0.5, 16, COL_TEXT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
FAIcon(FA.card, "bw_fa_lg", w * 0.5, 44, COL_TEXT)
end
local close = vgui.Create("DButton", rhead)
close:Dock(RIGHT)
close:SetWide(40)
close:SetText("")
close.Paint = function(self, w, h)
local y = (h - 34) * 0.5
rndx.Draw(8, w - 34, y, 34, 34, self:IsHovered() and COL_RED or COL_PANEL, rndx.SHAPE_FIGMA)
draw.SimpleText("X", "bw_big", w - 17, h * 0.5, COL_RED, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
end
close.DoClick = function() frame:Remove() end
local content = vgui.Create("DPanel", right)
content:Dock(FILL)
content:DockMargin(0, 8, 0, 0)
content:SetPaintBackground(false)
-- ===== refresh =====
local RefreshRight
local function RefreshPlayers()
plist:Clear()
local filter = string.lower(search:GetValue() or "")
for _, e in ipairs(BladW.Warn.List) do
if filter == "" or string.find(string.lower(e.name), filter, 1, true) then
local entry = e
local row = plist:Add("DButton")
row:Dock(TOP)
row:DockMargin(0, 0, 8, 8)
row:SetTall(52)
row:SetText("")
row.Paint = function(self, w, h)
local sel = selectedSID == entry.sid
rndx.Draw(8, 0, 0, w, h, sel and COL_SEL or COL_PANEL, rndx.SHAPE_FIGMA)
-- petit bout à gauche : bleu si sélectionné, gris clair sinon (coins gauche arrondis)
rndx.Draw(8, 0, 0, 5, h, sel and COL_BLUE or COL_BAR, rndx.SHAPE_FIGMA + rndx.NO_TR + rndx.NO_BR)
draw.SimpleText(entry.name, "bw_bold", 60, h * 0.5, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
if entry.staff then
FAIcon(FA.shield, "bw_fa", w - 28, h * 0.5, COL_ORANGE)
end
end
row.DoClick = function() selectedSID = entry.sid RefreshRight() end
local av = vgui.Create("AvatarImage", row)
av:SetSize(34, 34)
av:SetPos(14, 9)
av:SetSteamID(entry.sid, 64)
av:SetMouseInputEnabled(false)
end
end
end
function RefreshRight()
content:Clear()
local e = selectedSID and GetEntry(selectedSID)
if not e then
local ph = vgui.Create("DPanel", content)
ph:Dock(FILL)
ph:SetPaintBackground(false)
ph.Paint = function(self, w, h)
draw.SimpleText("Sélectionne un joueur à gauche", "bw_med", w * 0.5, h * 0.4, COL_MUTED, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
end
return
end
-- carte joueur
local card = vgui.Create("DPanel", content)
card:Dock(TOP)
card:SetTall(90)
card:SetPaintBackground(false)
card.Paint = function(self, w, h)
rndx.Draw(12, 0, 0, w, h, COL_PANEL, rndx.SHAPE_FIGMA)
draw.SimpleText(e.name, "bw_big", 96, 30, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
draw.SimpleText("Steam ID :", "bw_small", 96, 56, COL_MUTED, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
local n = #e.warns
local txt = n .. (n > 1 and " Warns actifs" or " Warn actif")
surface.SetFont("bw_bold")
local bw = surface.GetTextSize(txt) + 30
rndx.Draw(6, w * 0.5 - bw * 0.5, h * 0.5 - 16, bw, 32, COL_BADGE, rndx.SHAPE_FIGMA)
draw.SimpleText(txt, "bw_bold", w * 0.5, h * 0.5, COL_RED, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
end
local av = vgui.Create("AvatarImage", card)
av:SetSize(56, 56)
av:SetPos(22, 17)
av:SetSteamID(e.sid, 64)
av:SetMouseInputEnabled(false)
-- bouton copie du SteamID
local copyBtn = vgui.Create("DButton", card)
copyBtn:SetText("")
copyBtn:SetSize(16, 16)
copyBtn:SetTooltip("Copier le SteamID")
copyBtn.Paint = function(self, w, h)
FAIcon(FA.copy, "bw_fa", w * 0.5, h * 0.5, self:IsHovered() and COL_TEXT or COL_MUTED)
end
copyBtn.DoClick = function() SetClipboardText(e.sid) end
card.PerformLayout = function(self, w, h)
surface.SetFont("bw_small")
local tw = surface.GetTextSize("Steam ID :")
copyBtn:SetPos(96 + tw + 8, 49)
end
-- ligne « Ajouter un warn »
local addRow = vgui.Create("DPanel", content)
addRow:Dock(TOP)
addRow:SetTall(64)
addRow:DockMargin(0, 10, 0, 4)
addRow:SetPaintBackground(false)
addRow.Paint = function(self, w, h)
surface.SetDrawColor(COL_LINE)
surface.DrawRect(0, h * 0.5, w * 0.5 - 100, 1)
surface.DrawRect(w * 0.5 + 100, h * 0.5, w * 0.5 - 100, 1)
end
local addBtn = Btn(addRow, "Ajouter un warn", COL_BLUE, "bw_big")
addBtn:SetSize(190, 42)
addRow.PerformLayout = function(self, w, h) addBtn:SetPos(w * 0.5 - 95, h * 0.5 - 21) end
addBtn.DoClick = function() OpenWarnModal(e.sid) end
-- ligne « Vider le casier » (en bas)
local clearRow = vgui.Create("DPanel", content)
clearRow:Dock(BOTTOM)
clearRow:SetTall(54)
clearRow:SetPaintBackground(false)
local clearBtn = Btn(clearRow, "Vider le casier", COL_ORANGE, "bw_med")
clearBtn:SetSize(190, 40)
clearBtn.Paint = function(self, w, h)
rndx.Draw(8, 0, 0, w, h, self:IsHovered() and Lighten(COL_ORANGE) or COL_ORANGE, rndx.SHAPE_FIGMA)
FAIcon(FA.trash, "bw_fa", 32, h * 0.5, COL_TEXT)
draw.SimpleText("Vider le casier", "bw_med", w * 0.5 + 14, h * 0.5, COL_TEXT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
end
clearRow.PerformLayout = function(self, w, h) clearBtn:SetPos(w - 190, h - 46) end
clearBtn.DoClick = function()
if #e.warns == 0 then return end
net.Start("bladw_warn_clear")
net.WriteString(e.sid)
net.SendToServer()
end
-- liste des warns
local warns = vgui.Create("DScrollPanel", content)
warns:Dock(FILL)
warns:DockMargin(0, 6, 0, 6)
StyleScroll(warns)
for i, w in ipairs(e.warns) do
local idx, warn = i, w
local wc = warns:Add("DPanel")
wc:Dock(TOP)
wc:DockMargin(0, 0, 8, 10)
wc:SetTall(116)
wc:SetPaintBackground(false)
wc.Paint = function(self, cw, ch)
rndx.Draw(10, 0, 0, cw, ch, COL_PANEL, rndx.SHAPE_FIGMA)
draw.SimpleText("Warn émis par :", "bw_med", 16, 16, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP)
draw.SimpleText(warn.issuer ~= "" and warn.issuer or "?", "bw_med", 168, 16, COL_MUTED, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP)
draw.SimpleText("Date :", "bw_med", 16, 40, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP)
if warn.time and warn.time > 0 then
local d = ("(%s) %s"):format(RelTime(warn.time), os.date("%d/%m/%Y, %I:%M %p", warn.time))
draw.SimpleText(d, "bw_med", 168, 40, COL_MUTED, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP)
end
draw.SimpleText("Raison :", "bw_med", 16, 64, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP)
rndx.Draw(6, 14, ch - 40, cw - 28, 30, COL_INPUT, rndx.SHAPE_FIGMA)
draw.SimpleText(warn.reason or "", "bw_small", 26, ch - 25, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
end
local del = Btn(wc, "Supprimer", COL_RED, "bw_med")
del:SetSize(150, 36)
wc.PerformLayout = function(self, cw, ch) del:SetPos(cw - 164, 14) end
del.DoClick = function()
net.Start("bladw_warn_remove")
net.WriteString(e.sid)
net.WriteUInt(idx, 8)
net.SendToServer()
end
end
end
frame.RefreshAll = function()
RefreshPlayers()
RefreshRight()
end
search.OnChange = function() RefreshPlayers() end
RefreshPlayers()
RefreshRight()
end
-- ============================================================
-- Réseau
-- ============================================================
net.Receive("bladw_warn_open", function()
BladW.Warn.Open()
end)
-- annonce d'un warn dans le chat de base
net.Receive("bladw_warn_chat", function()
local target = net.ReadString()
local admin = net.ReadString()
local reason = net.ReadString()
chat.AddText(
COL_ORANGE, "[Warn] ",
COL_TEXT, target,
COL_MUTED, " a été averti par ",
COL_TEXT, admin,
COL_MUTED, "",
COL_RED, reason
)
end)
net.Receive("bladw_warn_sync", function()
local list = {}
local n = net.ReadUInt(8)
for i = 1, n do
local e = { name = net.ReadString(), sid = net.ReadString(), staff = net.ReadBool(), warns = {} }
local wn = net.ReadUInt(8)
for j = 1, wn do
e.warns[j] = {
issuer = net.ReadString(),
time = net.ReadUInt(32),
reason = net.ReadString(),
comment = net.ReadString(),
}
end
list[i] = e
end
BladW.Warn.List = list
if IsValid(BladW.Warn.Frame) and BladW.Warn.Frame.RefreshAll then
BladW.Warn.Frame.RefreshAll()
end
end)