From 9ab34fe802bc34867e6e8dc870660c0ebdf8cb94 Mon Sep 17 00:00:00 2001 From: nocode Date: Mon, 20 Jul 2026 23:29:27 +0200 Subject: [PATCH] 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 --- lua/bladw_warn/client/cl_menu.lua | 529 ++++++++++++++++++++++++++++++ 1 file changed, 529 insertions(+) create mode 100644 lua/bladw_warn/client/cl_menu.lua diff --git a/lua/bladw_warn/client/cl_menu.lua b/lua/bladw_warn/client/cl_menu.lua new file mode 100644 index 0000000..617a195 --- /dev/null +++ b/lua/bladw_warn/client/cl_menu.lua @@ -0,0 +1,529 @@ +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)