diff --git a/lua/bladw_announcement/client/admin_menu/cl_menu.lua b/lua/bladw_announcement/client/admin_menu/cl_menu.lua index 3992c3f..779648c 100644 --- a/lua/bladw_announcement/client/admin_menu/cl_menu.lua +++ b/lua/bladw_announcement/client/admin_menu/cl_menu.lua @@ -14,6 +14,7 @@ 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) @@ -42,12 +43,27 @@ local function StyleEntry(entry, placeholder, default) 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 Color(255, 255, 255, 60) + 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 @@ -56,36 +72,32 @@ local function OpenAAdminMenu() 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, 460) + 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) - -- fond flou + sombre + contour blanc (comme la box d'annonce) 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) - -- logo + titre 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 - -- ESC ferme mon menu sans laisser le menu pause s'afficher. - -- le moteur ouvre le menu pause sur ESC avant que je puisse l'intercepter, donc je le recache direct et je ferme. - frame.Think = function(self) - if gui.IsGameUIVisible() then - gui.HideGameUI() - self:Remove() - end - end - - -- bouton pour fermer + -- bouton fermer local close = vgui.Create("DButton", frame) close:SetSize(30, 30) close:SetPos(frame:GetWide() - 42, 14) @@ -96,7 +108,7 @@ local function OpenAAdminMenu() end close.DoClick = function() frame:Remove() end - -- les champs (créés avant les boutons preset pour être captés dans leurs closures) + -- champs local titleEntry = vgui.Create("DTextEntry", frame) titleEntry:SetPos(40, 240) titleEntry:SetSize(420, 40) @@ -106,11 +118,10 @@ local function OpenAAdminMenu() msgEntry:SetPos(40, 288) msgEntry:SetSize(420, 40) StyleEntry(msgEntry, "Message de l'annonce") - -- bord rouge quand ça dépasse la limite 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 Color(255, 255, 255, 60)) + 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 @@ -124,7 +135,6 @@ local function OpenAAdminMenu() local len = EntryLen(msgEntry) local over = len > maxLen - -- retour du serveur (ex : annonce déjà en cours) sinon le warning de dépassement 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) @@ -137,7 +147,171 @@ local function OpenAAdminMenu() over and COL_RED or COL_MUTED, TEXT_ALIGN_RIGHT, TEXT_ALIGN_CENTER) end - -- boutons preset : remplissent les champs et retiennent le preset choisi + -- 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) @@ -157,38 +331,21 @@ local function OpenAAdminMenu() 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 - -- bouton envoyer (grisé + bloqué si ça dépasse) - local send = vgui.Create("DButton", frame) - send:SetPos(40, 372) - send:SetSize(420, 50) - 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() - if EntryLen(msgEntry) > maxLen then - surface.PlaySound("buttons/button10.wav") - return - end - if msgEntry:GetValue() == "" and selectedPreset == "" then - surface.PlaySound("buttons/button10.wav") - return - end - - -- on envoie mais on NE ferme PAS : c'est la réponse serveur qui fermera si c'est publié, - -- comme ça si c'est refusé l'admin garde son texte - net.Start("bladWSendAnnouncement") - net.WriteString(selectedPreset) - net.WriteString(titleEntry:GetValue()) - net.WriteString(msgEntry:GetValue()) - net.SendToServer() - end + RebuildList() end