script_name("SmartDialogs") local imgui = require 'mimgui' local ffi = require 'ffi' local encoding = require 'encoding' local sampev = require 'lib.samp.events' encoding.default = 'CP1251' local u8 = encoding.UTF8 local configPath = getWorkingDirectory() .. '\\config\\SmartDialogs.json' local dialogsData = {} local showMainMenu = imgui.new.bool(false) local showSuggestions = imgui.new.bool(false) local currentDialog = { rawText = "", cleanText = "", style = -1 } local currentSuggestions = {} local tempCaptureSuggestions = {} local currentAutoSubmit = false -- Состояние авто-отправки для текущего диалога -- Буферы для многострочного ввода local captureMultilineBuffer = imgui.new.char[8192]("") local presetMultilineBuffer = imgui.new.char[8192]("") local autoSubmitCapture = imgui.new.bool(false) -- Переменные для модального окна local requestAddLinesModal = false local activePresetIndex = 0 -- Система уведомлений local sysNotifText = nil local sysNotifTimer = 0 local sysNotifType = 0 -- Оптимизация сохранения local needSave = false local saveTimer = 0 local function requestSave() needSave = true saveTimer = os.clock() + 0.5 -- Откладываем сохранение на полсекунды end local function showSystemNotification(text, nType) sysNotifText = text sysNotifType = nType or 3 sysNotifTimer = os.clock() + 3.0 end local function stripColors(text) if not text then return "" end return text:gsub("{%x%x%x%x%x%x}", "") end function loadConfig() local f = io.open(configPath, "r") if f then local data = f:read("*a") f:close() local status, parsed = pcall(decodeJson, data) if status and type(parsed) == "table" then dialogsData = parsed end end end function saveConfig() local f = io.open(configPath, "w") if f then f:write(encodeJson(dialogsData)) f:close() end end function main() if not isSampLoaded() or not isSampfuncsLoaded() then return end while not isSampAvailable() do wait(100) end loadConfig() sampRegisterChatCommand("sdmenu", function() showMainMenu[0] = not showMainMenu[0] end) while true do wait(0) -- Скрытие окна, если диалог закрыт if not sampIsDialogActive() then showSuggestions[0] = false end -- Отложенное сохранение файла if needSave and os.clock() > saveTimer then saveConfig() needSave = false end end end function sampev.onShowDialog(dialogId, style, title, button1, button2, text) if style == 1 or style == 3 then currentDialog.rawText = text currentDialog.cleanText = stripColors(text) currentDialog.style = style showSuggestions[0] = false currentSuggestions = {} tempCaptureSuggestions = {} captureMultilineBuffer[0] = 0 currentAutoSubmit = false for _, dlg in ipairs(dialogsData) do if currentDialog.cleanText == dlg.matchText then currentSuggestions = dlg.suggestions currentAutoSubmit = dlg.autoSubmit or false showSuggestions[0] = true break end end else currentDialog.style = -1 showSuggestions[0] = false end end local function ApplyStyle() imgui.PushStyleColor(imgui.Col.WindowBg, imgui.ImVec4(0.08, 0.08, 0.08, 0.95)) imgui.PushStyleColor(imgui.Col.PopupBg, imgui.ImVec4(0.06, 0.06, 0.06, 0.98)) imgui.PushStyleColor(imgui.Col.Button, imgui.ImVec4(0.18, 0.35, 0.58, 1.0)) imgui.PushStyleColor(imgui.Col.ButtonHovered, imgui.ImVec4(0.28, 0.45, 0.68, 1.0)) imgui.PushStyleColor(imgui.Col.ButtonActive, imgui.ImVec4(0.08, 0.25, 0.48, 1.0)) imgui.PushStyleColor(imgui.Col.Header, imgui.ImVec4(0.18, 0.35, 0.58, 1.0)) imgui.PushStyleColor(imgui.Col.HeaderHovered, imgui.ImVec4(0.28, 0.45, 0.68, 0.8)) imgui.PushStyleColor(imgui.Col.HeaderActive, imgui.ImVec4(0.08, 0.25, 0.48, 1.0)) imgui.PushStyleColor(imgui.Col.FrameBg, imgui.ImVec4(0.15, 0.15, 0.15, 1.0)) imgui.PushStyleColor(imgui.Col.FrameBgHovered, imgui.ImVec4(0.22, 0.22, 0.22, 0.8)) imgui.PushStyleColor(imgui.Col.FrameBgActive, imgui.ImVec4(0.15, 0.15, 0.15, 1.0)) imgui.PushStyleColor(imgui.Col.TitleBg, imgui.ImVec4(0.10, 0.10, 0.10, 1.0)) imgui.PushStyleColor(imgui.Col.TitleBgActive, imgui.ImVec4(0.18, 0.35, 0.58, 1.0)) imgui.PushStyleColor(imgui.Col.ModalWindowDimBg, imgui.ImVec4(0.0, 0.0, 0.0, 0.6)) imgui.PushStyleVarFloat(imgui.StyleVar.FrameRounding, 6.0) imgui.PushStyleVarFloat(imgui.StyleVar.ChildRounding, 6.0) imgui.PushStyleVarFloat(imgui.StyleVar.PopupRounding, 6.0) imgui.PushStyleVarFloat(imgui.StyleVar.WindowRounding, 8.0) end local function PopStyle() imgui.PopStyleColor(14) imgui.PopStyleVar(4) end -- === РЕНДЕР УВЕДОМЛЕНИЙ === local notifFrame = imgui.OnFrame( function() return sysNotifText and os.clock() < sysNotifTimer end, function(player) ApplyStyle() local resX, resY = getScreenResolution() imgui.SetNextWindowPos(imgui.ImVec2(resX - 20, resY - 20), imgui.Cond.Always, imgui.ImVec2(1.0, 1.0)) local borderColor = imgui.ImVec4(0.18, 0.35, 0.58, 1.0) if sysNotifType == 1 then borderColor = imgui.ImVec4(0.20, 0.80, 0.20, 0.80) end if sysNotifType == 2 then borderColor = imgui.ImVec4(0.80, 0.20, 0.20, 0.80) end imgui.PushStyleColor(imgui.Col.WindowBg, imgui.ImVec4(0.12, 0.12, 0.12, 0.95)) imgui.PushStyleColor(imgui.Col.Border, borderColor) imgui.PushStyleVarFloat(imgui.StyleVar.WindowBorderSize, 2.0) imgui.PushStyleVarVec2(imgui.StyleVar.WindowPadding, imgui.ImVec2(15, 12)) local flags = bit.bor(imgui.WindowFlags.NoTitleBar, imgui.WindowFlags.NoResize, imgui.WindowFlags.NoMove, imgui.WindowFlags.AlwaysAutoResize, imgui.WindowFlags.NoFocusOnAppearing) imgui.Begin("##SysNotif", nil, flags) imgui.Text(u8(sysNotifText)) imgui.End() imgui.PopStyleVar(2) imgui.PopStyleColor(2) PopStyle() end ) notifFrame.HideCursor = true -- === БОКОВОЕ МЕНЮ ПОДСКАЗОК === local sugFrame = imgui.OnFrame( function() return showSuggestions[0] and sampIsDialogActive() end, function(player) ApplyStyle() local resX, resY = getScreenResolution() imgui.SetNextWindowPos(imgui.ImVec2(resX / 2, resY - 50), imgui.Cond.Always, imgui.ImVec2(0.5, 1.0)) local flags = bit.bor(imgui.WindowFlags.NoTitleBar, imgui.WindowFlags.AlwaysAutoResize, imgui.WindowFlags.NoMove, imgui.WindowFlags.NoSavedSettings) imgui.Begin("SuggestionsOverlay", showSuggestions, flags) for i, sug in ipairs(currentSuggestions) do if imgui.Button(u8(sug)) then sampSetCurrentDialogEditboxText(sug) if currentAutoSubmit then -- Отправляем диалог, нажимая левую кнопку (1) sampCloseCurrentDialogWithButton(1) end end end imgui.End() PopStyle() end ) -- === ГЛАВНОЕ МЕНЮ (/sdmenu) === local menuFrame = imgui.OnFrame( function() return showMainMenu[0] end, function(player) ApplyStyle() imgui.SetNextWindowPos(imgui.ImVec2(500, 300), imgui.Cond.FirstUseEver, imgui.ImVec2(0.5, 0.5)) imgui.SetNextWindowSize(imgui.ImVec2(550, 450), imgui.Cond.FirstUseEver) imgui.Begin(u8"SmartDialogs | Меню", showMainMenu) -- МОДАЛЬНОЕ ОКНО ДОБАВЛЕНИЯ СТРОК if requestAddLinesModal then imgui.OpenPopup("AddLinesModal") requestAddLinesModal = false end if imgui.BeginPopupModal("AddLinesModal", nil, bit.bor(imgui.WindowFlags.AlwaysAutoResize, imgui.WindowFlags.NoTitleBar)) then imgui.Text(u8"Массовое добавление фраз") imgui.Spacing() imgui.TextDisabled(u8"Каждая новая строка будет добавлена как отдельная кнопка.") imgui.InputTextMultiline("##LinesInput", presetMultilineBuffer, ffi.sizeof(presetMultilineBuffer), imgui.ImVec2(450, 200)) imgui.Spacing() imgui.Separator() imgui.Spacing() local availWidth = imgui.GetContentRegionAvail().x local btnW = (availWidth - imgui.GetStyle().ItemSpacing.x) / 2 if imgui.Button(u8"Сохранить", imgui.ImVec2(btnW, 0)) then local str = ffi.string(presetMultilineBuffer) if str ~= "" and dialogsData[activePresetIndex] then local added = 0 for line in str:gmatch("[^\r\n]+") do local cleanLine = line:match("^%s*(.-)%s*$") if cleanLine and cleanLine ~= "" then table.insert(dialogsData[activePresetIndex].suggestions, u8:decode(cleanLine)) added = added + 1 end end if added > 0 then requestSave() if showSuggestions[0] then currentSuggestions = dialogsData[activePresetIndex].suggestions end showSystemNotification("Добавлено фраз: " .. added, 1) end end imgui.CloseCurrentPopup() end imgui.SameLine() if imgui.Button(u8"Отмена", imgui.ImVec2(btnW, 0)) then imgui.CloseCurrentPopup() end imgui.EndPopup() end -- ЗАХВАТ ДИАЛОГА if sampIsDialogActive() and (currentDialog.style == 1 or currentDialog.style == 3) then imgui.TextColored(imgui.ImVec4(0.4, 0.8, 0.4, 1.0), u8"Текущий открытый диалог:") local dispText = currentDialog.cleanText:gsub("\n", " ") if string.len(dispText) > 60 then dispText = string.sub(dispText, 1, 60) .. "..." end imgui.TextDisabled(u8"Текст привязки: " .. u8(dispText)) imgui.Spacing() for i, sug in ipairs(tempCaptureSuggestions) do if imgui.Button("X##cap"..i) then table.remove(tempCaptureSuggestions, i) end imgui.SameLine() imgui.Text(u8(sug)) end imgui.Spacing() imgui.TextDisabled(u8"Вставьте сюда список фраз (каждая с новой строки):") imgui.InputTextMultiline("##capMultiline", captureMultilineBuffer, ffi.sizeof(captureMultilineBuffer), imgui.ImVec2(-1, 80)) if imgui.Button(u8"+ Добавить список", imgui.ImVec2(-1, 0)) then local str = ffi.string(captureMultilineBuffer) if str ~= "" then for line in str:gmatch("[^\r\n]+") do local cleanLine = line:match("^%s*(.-)%s*$") if cleanLine and cleanLine ~= "" then table.insert(tempCaptureSuggestions, u8:decode(cleanLine)) end end captureMultilineBuffer[0] = 0 end end imgui.Spacing() imgui.Checkbox(u8"Автоматически нажимать 'Далее' (Enter)", autoSubmitCapture) imgui.Spacing() if imgui.Button(u8"Сохранить пресет в базу", imgui.ImVec2(-1, 35)) then if #tempCaptureSuggestions > 0 then local dlg = { matchText = currentDialog.cleanText, suggestions = {}, autoSubmit = autoSubmitCapture[0] } for _, s in ipairs(tempCaptureSuggestions) do table.insert(dlg.suggestions, s) end table.insert(dialogsData, dlg) requestSave() tempCaptureSuggestions = {} autoSubmitCapture[0] = false showSystemNotification("Пресет успешно сохранен!", 1) else showSystemNotification("Ошибка: добавьте хотя бы 1 фразу!", 2) end end imgui.Separator() end -- СПИСОК ПРЕСЕТОВ imgui.Text(u8"Сохраненные пресеты:") if #dialogsData == 0 then imgui.TextDisabled(u8"Список пуст.") end for i, dlg in ipairs(dialogsData) do local shortText = dlg.matchText:gsub("\n", " ") if string.len(shortText) > 40 then shortText = string.sub(shortText, 1, 40) .. "..." end local dispModeText = "Текст: " .. shortText if imgui.TreeNodeStr(u8(dispModeText) .. "##" .. i) then if imgui.Button(u8"Удалить весь пресет##del"..i) then table.remove(dialogsData, i) requestSave() showSystemNotification("Пресет удален!", 3) end imgui.SameLine() if imgui.Button(u8"+ Добавить строки##add"..i) then activePresetIndex = i presetMultilineBuffer[0] = 0 requestAddLinesModal = true end imgui.Spacing() -- Чекбокс для редактирования авто-отправки уже существующего пресета local autoPtr = imgui.new.bool(dlg.autoSubmit or false) if imgui.Checkbox(u8"Авто-отправка (Enter)##auto"..i, autoPtr) then dlg.autoSubmit = autoPtr[0] requestSave() if showSuggestions[0] then currentAutoSubmit = dlg.autoSubmit end end imgui.Spacing() for j, sug in ipairs(dlg.suggestions) do if imgui.Button("X##delsug"..i.."_"..j) then table.remove(dlg.suggestions, j) requestSave() if showSuggestions[0] then currentSuggestions = dlg.suggestions end end imgui.SameLine() imgui.Text(u8(sug)) end imgui.Spacing() imgui.TreePop() end end imgui.End() PopStyle() end )