402 lines
15 KiB
Lua
402 lines
15 KiB
Lua
local PLUGIN = PLUGIN
|
||
|
||
-- Цветовая схема
|
||
local COLOR_BG_DARK = Color(3, 5, 4)
|
||
local COLOR_BG_MEDIUM = Color(8, 12, 10)
|
||
local COLOR_BG_LIGHT = Color(12, 18, 14)
|
||
local COLOR_PRIMARY = Color(27, 94, 32)
|
||
local COLOR_PRIMARY_DARK = Color(15, 60, 18)
|
||
local COLOR_ACCENT = Color(56, 102, 35)
|
||
local COLOR_TEXT_PRIMARY = Color(165, 214, 167)
|
||
local COLOR_TEXT_SECONDARY = Color(102, 187, 106)
|
||
local COLOR_BORDER = Color(15, 60, 18, 60)
|
||
|
||
-- Локальное хранилище логов
|
||
local clientLogs = {}
|
||
local isLoadingLogs = false
|
||
|
||
-- Сетевой получатель для открытия меню (из команды Helix)
|
||
net.Receive("ixServerLogs_OpenMenu", function()
|
||
PLUGIN:OpenLogsMenu()
|
||
end)
|
||
|
||
-- Получение логов с сервера
|
||
function PLUGIN:RequestLogs(filters)
|
||
if isLoadingLogs then return end
|
||
|
||
isLoadingLogs = true
|
||
clientLogs = {}
|
||
|
||
net.Start("ixServerLogs_Request")
|
||
net.WriteTable(filters or {})
|
||
net.SendToServer()
|
||
end
|
||
|
||
-- Получение ответа с логами
|
||
net.Receive("ixServerLogs_Response", function()
|
||
local isFirst = net.ReadUInt(2) == 1
|
||
local logs = net.ReadTable()
|
||
|
||
if isFirst then
|
||
clientLogs = {}
|
||
end
|
||
|
||
for _, log in ipairs(logs) do
|
||
table.insert(clientLogs, log)
|
||
end
|
||
|
||
isLoadingLogs = false
|
||
|
||
-- Обновляем UI если открыто
|
||
if IsValid(PLUGIN.logsFrame) and IsValid(PLUGIN.logsList) then
|
||
PLUGIN:UpdateLogsList()
|
||
end
|
||
end)
|
||
|
||
-- Главное меню логов
|
||
function PLUGIN:OpenLogsMenu()
|
||
if IsValid(self.logsFrame) then
|
||
self.logsFrame:Remove()
|
||
end
|
||
|
||
local scrW, scrH = ScrW(), ScrH()
|
||
local frame = vgui.Create("DFrame")
|
||
frame:SetSize(scrW * 0.9, scrH * 0.85)
|
||
frame:Center()
|
||
frame:SetTitle("")
|
||
frame:SetDraggable(true)
|
||
frame:ShowCloseButton(false)
|
||
frame:MakePopup()
|
||
|
||
frame.Paint = function(s, w, h)
|
||
-- Основной фон
|
||
draw.RoundedBox(8, 0, 0, w, h, COLOR_BG_DARK)
|
||
|
||
-- Верхняя панель
|
||
draw.RoundedBoxEx(8, 0, 0, w, 60, COLOR_BG_MEDIUM, true, true, false, false)
|
||
|
||
-- Акцентная линия
|
||
surface.SetDrawColor(COLOR_PRIMARY)
|
||
surface.DrawRect(0, 60, w, 3)
|
||
|
||
-- Заголовок
|
||
draw.SimpleText("◆ ЛОГИ СЕРВЕРА ◆", "ixMenuButtonFont", w/2, 20, COLOR_ACCENT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||
draw.SimpleText("СИСТЕМА МОНИТОРИНГА СОБЫТИЙ", "ixSmallFont", w/2, 43, COLOR_TEXT_SECONDARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||
|
||
-- Рамка
|
||
surface.SetDrawColor(COLOR_BORDER)
|
||
surface.DrawOutlinedRect(0, 0, w, h, 2)
|
||
end
|
||
|
||
-- Кнопка закрытия
|
||
local closeBtn = vgui.Create("DButton", frame)
|
||
closeBtn:SetSize(40, 40)
|
||
closeBtn:SetPos(frame:GetWide() - 50, 10)
|
||
closeBtn:SetText("")
|
||
closeBtn.Paint = function(s, w, h)
|
||
local col = s:IsHovered() and Color(200, 50, 50) or Color(150, 50, 50)
|
||
draw.RoundedBox(4, 0, 0, w, h, col)
|
||
|
||
surface.SetDrawColor(255, 255, 255)
|
||
surface.DrawLine(w*0.3, h*0.3, w*0.7, h*0.7)
|
||
surface.DrawLine(w*0.7, h*0.3, w*0.3, h*0.7)
|
||
end
|
||
closeBtn.DoClick = function()
|
||
frame:Close()
|
||
end
|
||
|
||
-- Панель фильтров
|
||
local filterPanel = vgui.Create("DPanel", frame)
|
||
filterPanel:SetSize(250, frame:GetTall() - 80)
|
||
filterPanel:SetPos(10, 70)
|
||
filterPanel.Paint = function(s, w, h)
|
||
draw.RoundedBox(6, 0, 0, w, h, COLOR_BG_MEDIUM)
|
||
surface.SetDrawColor(COLOR_BORDER)
|
||
surface.DrawOutlinedRect(0, 0, w, h, 1)
|
||
end
|
||
|
||
-- Заголовок фильтров
|
||
local filterHeader = vgui.Create("DLabel", filterPanel)
|
||
filterHeader:SetPos(10, 10)
|
||
filterHeader:SetFont("ixSmallFont")
|
||
filterHeader:SetText("⚙ ФИЛЬТРЫ")
|
||
filterHeader:SetTextColor(COLOR_TEXT_PRIMARY)
|
||
filterHeader:SizeToContents()
|
||
|
||
-- Категории
|
||
local categoryLabel = vgui.Create("DLabel", filterPanel)
|
||
categoryLabel:SetPos(10, 40)
|
||
categoryLabel:SetFont("DermaDefault")
|
||
categoryLabel:SetText("Категория:")
|
||
categoryLabel:SetTextColor(COLOR_TEXT_SECONDARY)
|
||
categoryLabel:SizeToContents()
|
||
|
||
local categoryBox = vgui.Create("DComboBox", filterPanel)
|
||
categoryBox:SetPos(10, 60)
|
||
categoryBox:SetSize(230, 30)
|
||
categoryBox:SetValue("Все категории")
|
||
categoryBox:AddChoice("Все категории")
|
||
|
||
for catID, cat in SortedPairs(self.LOG_CATEGORIES) do
|
||
categoryBox:AddChoice(cat.icon .. " " .. cat.name, catID)
|
||
end
|
||
|
||
categoryBox.Paint = function(s, w, h)
|
||
draw.RoundedBox(4, 0, 0, w, h, COLOR_BG_LIGHT)
|
||
surface.SetDrawColor(COLOR_PRIMARY)
|
||
surface.DrawOutlinedRect(0, 0, w, h, 1)
|
||
end
|
||
|
||
-- Поиск
|
||
local searchLabel = vgui.Create("DLabel", filterPanel)
|
||
searchLabel:SetPos(10, 100)
|
||
searchLabel:SetFont("DermaDefault")
|
||
searchLabel:SetText("Поиск:")
|
||
searchLabel:SetTextColor(COLOR_TEXT_SECONDARY)
|
||
searchLabel:SizeToContents()
|
||
|
||
local searchBox = vgui.Create("DTextEntry", filterPanel)
|
||
searchBox:SetPos(10, 120)
|
||
searchBox:SetSize(230, 30)
|
||
searchBox:SetPlaceholderText("Введите текст для поиска...")
|
||
searchBox.Paint = function(s, w, h)
|
||
draw.RoundedBox(4, 0, 0, w, h, COLOR_BG_LIGHT)
|
||
surface.SetDrawColor(COLOR_PRIMARY)
|
||
surface.DrawOutlinedRect(0, 0, w, h, 1)
|
||
s:DrawTextEntryText(COLOR_TEXT_PRIMARY, COLOR_ACCENT, COLOR_TEXT_PRIMARY)
|
||
end
|
||
|
||
-- Лимит
|
||
local limitLabel = vgui.Create("DLabel", filterPanel)
|
||
limitLabel:SetPos(10, 160)
|
||
limitLabel:SetFont("DermaDefault")
|
||
limitLabel:SetText("Количество записей:")
|
||
limitLabel:SetTextColor(COLOR_TEXT_SECONDARY)
|
||
limitLabel:SizeToContents()
|
||
|
||
local limitSlider = vgui.Create("DNumSlider", filterPanel)
|
||
limitSlider:SetPos(10, 180)
|
||
limitSlider:SetSize(230, 30)
|
||
limitSlider:SetMin(10)
|
||
limitSlider:SetMax(500)
|
||
limitSlider:SetDecimals(0)
|
||
limitSlider:SetValue(100)
|
||
limitSlider:SetText("")
|
||
|
||
-- Кнопка применить фильтры
|
||
local applyBtn = vgui.Create("DButton", filterPanel)
|
||
applyBtn:SetPos(10, 230)
|
||
applyBtn:SetSize(230, 40)
|
||
applyBtn:SetText("")
|
||
applyBtn.Paint = function(s, w, h)
|
||
local col = s:IsHovered() and COLOR_ACCENT or COLOR_PRIMARY
|
||
draw.RoundedBox(4, 0, 0, w, h, col)
|
||
surface.SetDrawColor(COLOR_BORDER)
|
||
surface.DrawOutlinedRect(0, 0, w, h, 1)
|
||
draw.SimpleText("🔍 ПРИМЕНИТЬ", "ixSmallFont", w/2, h/2, COLOR_TEXT_PRIMARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||
end
|
||
applyBtn.DoClick = function()
|
||
local filters = {
|
||
limit = limitSlider:GetValue()
|
||
}
|
||
|
||
local _, catID = categoryBox:GetSelected()
|
||
if catID then
|
||
filters.category = catID
|
||
end
|
||
|
||
local searchText = searchBox:GetValue()
|
||
if searchText and searchText ~= "" then
|
||
filters.search = searchText
|
||
end
|
||
|
||
self:RequestLogs(filters)
|
||
end
|
||
|
||
-- Кнопка очистить логи
|
||
local clearBtn = vgui.Create("DButton", filterPanel)
|
||
clearBtn:SetPos(10, 280)
|
||
clearBtn:SetSize(230, 40)
|
||
clearBtn:SetText("")
|
||
clearBtn.Paint = function(s, w, h)
|
||
local col = s:IsHovered() and Color(200, 50, 50) or Color(150, 50, 50)
|
||
draw.RoundedBox(4, 0, 0, w, h, col)
|
||
surface.SetDrawColor(COLOR_BORDER)
|
||
surface.DrawOutlinedRect(0, 0, w, h, 1)
|
||
draw.SimpleText("🗑 ОЧИСТИТЬ ЛОГИ", "ixSmallFont", w/2, h/2, COLOR_TEXT_PRIMARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||
end
|
||
clearBtn.DoClick = function()
|
||
Derma_Query(
|
||
"Вы уверены что хотите очистить все логи?",
|
||
"Подтверждение",
|
||
"Да",
|
||
function()
|
||
net.Start("ixServerLogs_Clear")
|
||
net.SendToServer()
|
||
clientLogs = {}
|
||
self:UpdateLogsList()
|
||
end,
|
||
"Нет"
|
||
)
|
||
end
|
||
|
||
-- Панель логов
|
||
local logsPanel = vgui.Create("DPanel", frame)
|
||
logsPanel:SetSize(frame:GetWide() - 280, frame:GetTall() - 80)
|
||
logsPanel:SetPos(270, 70)
|
||
logsPanel.Paint = function(s, w, h)
|
||
draw.RoundedBox(6, 0, 0, w, h, COLOR_BG_MEDIUM)
|
||
surface.SetDrawColor(COLOR_BORDER)
|
||
surface.DrawOutlinedRect(0, 0, w, h, 1)
|
||
end
|
||
|
||
-- Заголовок логов
|
||
local logsHeader = vgui.Create("DPanel", logsPanel)
|
||
logsHeader:SetSize(logsPanel:GetWide(), 40)
|
||
logsHeader:Dock(TOP)
|
||
logsHeader.Paint = function(s, w, h)
|
||
draw.RoundedBoxEx(6, 0, 0, w, h, COLOR_PRIMARY_DARK, true, true, false, false)
|
||
|
||
draw.SimpleText("📋 ЗАПИСИ ЛОГОВ", "ixSmallFont", 10, h/2, COLOR_TEXT_PRIMARY, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||
draw.SimpleText("Всего: " .. #clientLogs, "DermaDefault", w - 10, h/2, COLOR_TEXT_SECONDARY, TEXT_ALIGN_RIGHT, TEXT_ALIGN_CENTER)
|
||
end
|
||
|
||
-- Список логов
|
||
local logsList = vgui.Create("DScrollPanel", logsPanel)
|
||
logsList:Dock(FILL)
|
||
logsList:DockMargin(5, 5, 5, 5)
|
||
|
||
local sbar = logsList:GetVBar()
|
||
sbar:SetHideButtons(true)
|
||
function sbar:Paint(w, h)
|
||
draw.RoundedBox(4, 0, 0, w, h, COLOR_BG_DARK)
|
||
end
|
||
function sbar.btnGrip:Paint(w, h)
|
||
draw.RoundedBox(4, 0, 0, w, h, COLOR_PRIMARY)
|
||
end
|
||
|
||
self.logsFrame = frame
|
||
self.logsList = logsList
|
||
self.logsHeader = logsHeader
|
||
|
||
-- Загружаем логи
|
||
self:RequestLogs({limit = 100})
|
||
end
|
||
|
||
-- Обновление списка логов
|
||
function PLUGIN:UpdateLogsList()
|
||
if not IsValid(self.logsList) then return end
|
||
|
||
self.logsList:Clear()
|
||
|
||
if #clientLogs == 0 then
|
||
local noLogs = vgui.Create("DLabel", self.logsList)
|
||
noLogs:SetText("Логи не найдены")
|
||
noLogs:SetFont("ixSmallFont")
|
||
noLogs:SetTextColor(COLOR_TEXT_SECONDARY)
|
||
noLogs:SizeToContents()
|
||
noLogs:Dock(TOP)
|
||
noLogs:DockMargin(10, 10, 10, 0)
|
||
return
|
||
end
|
||
|
||
-- Обновляем заголовок
|
||
if IsValid(self.logsHeader) then
|
||
self.logsHeader.Paint = function(s, w, h)
|
||
draw.RoundedBoxEx(6, 0, 0, w, h, COLOR_PRIMARY_DARK, true, true, false, false)
|
||
|
||
draw.SimpleText("📋 ЗАПИСИ ЛОГОВ", "ixSmallFont", 10, h/2, COLOR_TEXT_PRIMARY, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||
draw.SimpleText("Всего: " .. #clientLogs, "DermaDefault", w - 10, h/2, COLOR_TEXT_SECONDARY, TEXT_ALIGN_RIGHT, TEXT_ALIGN_CENTER)
|
||
end
|
||
end
|
||
|
||
for _, log in ipairs(clientLogs) do
|
||
local logEntry = vgui.Create("DButton", self.logsList)
|
||
logEntry:SetTall(66)
|
||
logEntry:Dock(TOP)
|
||
logEntry:DockMargin(5, 5, 5, 0)
|
||
logEntry:SetText("")
|
||
|
||
local category = self.LOG_CATEGORIES[log.category]
|
||
local categoryColor = category and category.color or color_white
|
||
|
||
logEntry.Paint = function(s, w, h)
|
||
local isHovered = s:IsHovered() or s:IsChildHovered()
|
||
local bgCol = isHovered and Color(18, 25, 20) or COLOR_BG_LIGHT
|
||
|
||
draw.RoundedBox(4, 0, 0, w, h, bgCol)
|
||
|
||
-- Боковая полоска категории
|
||
draw.RoundedBoxEx(4, 0, 0, 5, h, categoryColor, true, false, true, false)
|
||
|
||
-- Рамка
|
||
surface.SetDrawColor(isHovered and COLOR_PRIMARY or COLOR_BORDER)
|
||
surface.DrawOutlinedRect(0, 0, w, h, 1)
|
||
|
||
-- Время
|
||
draw.SimpleText(log.timeString, "DermaDefault", 15, 8, COLOR_TEXT_SECONDARY, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP)
|
||
|
||
-- Категория
|
||
local catText = category and (category.icon .. " " .. category.name) or log.category
|
||
draw.SimpleText(catText, "DermaDefault", 15, 25, categoryColor, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP)
|
||
|
||
-- Сообщение
|
||
draw.SimpleText(log.message, "ixSmallFont", 15, 43, COLOR_TEXT_PRIMARY, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP)
|
||
end
|
||
|
||
-- Поиск SteamID в тексте сообщения
|
||
local function findSteamID(text)
|
||
if not text then return nil end
|
||
return string.match(text, "STEAM_[0-5]:[0-9]:[0-9]+")
|
||
end
|
||
|
||
local extractedID = findSteamID(log.message)
|
||
|
||
logEntry.DoRightClick = function(s)
|
||
local menu = DermaMenu()
|
||
|
||
-- Опция копирования основного сообщения
|
||
menu:AddOption("Копировать сообщение", function()
|
||
SetClipboardText(log.message)
|
||
ix.util.Notify("Сообщение скопировано")
|
||
end):SetIcon("icon16/page_copy.png")
|
||
|
||
-- Копирование SteamID цели (если есть в метаданных)
|
||
if log.target and log.target.steamid and log.target.steamid ~= "N/A" then
|
||
menu:AddOption("Копировать SteamID (" .. log.target.name .. ")", function()
|
||
SetClipboardText(log.target.steamid)
|
||
ix.util.Notify("SteamID скопирован: " .. log.target.steamid)
|
||
end):SetIcon("icon16/user_edit.png")
|
||
-- Если нет в метаданных, но нашли в тексте (для архива)
|
||
elseif extractedID then
|
||
menu:AddOption("Копировать найденный SteamID (" .. extractedID .. ")", function()
|
||
SetClipboardText(extractedID)
|
||
ix.util.Notify("SteamID скопирован: " .. extractedID)
|
||
end):SetIcon("icon16/user_magnify.png")
|
||
end
|
||
|
||
-- Копирование SteamID атакующего (для убийств)
|
||
if log.data and log.data.attacker and log.data.attacker ~= "world" then
|
||
menu:AddOption("Копировать SteamID Атакующего", function()
|
||
SetClipboardText(log.data.attacker)
|
||
ix.util.Notify("SteamID атакующего скопирован: " .. log.data.attacker)
|
||
end):SetIcon("icon16/user_delete.png")
|
||
end
|
||
|
||
menu:AddSpacer()
|
||
|
||
menu:AddOption("Копировать время (" .. log.timeString .. ")", function()
|
||
SetClipboardText(log.timeString)
|
||
end):SetIcon("icon16/time.png")
|
||
|
||
menu:AddOption("Копировать полную строку", function()
|
||
local fullLog = string.format("[%s][%s] %s", log.timeString, category and category.name or log.category, log.message)
|
||
SetClipboardText(fullLog)
|
||
ix.util.Notify("Полная строка скопирована")
|
||
end):SetIcon("icon16/page_white_copy.png")
|
||
|
||
menu:Open()
|
||
end
|
||
end
|
||
end
|