Стильный интерфейс для работы с шаблонами ответов, быстрая смена префиксов, предпросмотр ответов
// ==UserScript==
// @name BR Forum Assistant
// @namespace http://tampermonkey.net/
// @version 3.0
// @description Стильный интерфейс для работы с шаблонами ответов, быстрая смена префиксов, предпросмотр ответов
// @author Valik
// @match https://forum.blackrussia.online/threads/*
// @icon https://i.postimg.cc/bJQpMWfY/image.png
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// ========== Префиксы ==========
const PREFIX = {
NONE: 0,
ACCEPT: 8,
UNACCEPT: 4,
PIN: 2,
SPECADM: 11,
GA: 12,
CLOSE: 7,
TEXY: 13,
REALIZOVANO: 5,
VAJNO: 1,
OJIDANIE: 14,
RASSMOTRENO: 9,
KACHESTVO: 15,
};
// ========== БАЗА ВСЕХ ШАБЛОНОВ (полная, из вашего скрипта) ==========
const rawButtons = [
{ title: 'Приветсвие', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>текст <br><br>Закрыто. [/FONT][/SIZE]' },
{ title: 'Ссылку на вк', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Приложите в следующей теме ссылку на вашу VK Страницу. <br><br>Закрыто. [/FONT][/SIZE]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'Свяжитесь со мной', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Свяжитесь со мной вконтакте: https://vk.com/77oleinik77<br><br>На рассмотрении.[/FONT]', prefix: PREFIX.PIN, sticky: true },
{ title: 'ᅠ ᅠ ᅠ ᅠ ᅠ ᅠ ᅠ ᅠ ᅠ ᅠ ᅠᅠ|-(--(-(-> Жалобы на администрацию <-)-)--)-|ᅠ ᅠ ᅠ ᅠ ᅠᅠ ᅠ ᅠ ᅠ ᅠ ᅠ ᅠᅠ' },
{ title: 'На рассмотрении', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Ваша жалоба принята на рассмотрение. Ожидайте ответа. Просим не создавать дубликаты темы.<br><br>Ожидайте ответа.[/FONT][/SIZE]', prefix: PREFIX.PIN, sticky: true },
{ title: 'Ссылку на тему', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Приложите, пожалуйста, ссылку на тему в следующем обращении.<br><br>Отказано. Закрыто.[/FONT][/SIZE]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'Жалоба составлена не по форме', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Ваша жалоба составлена не по форме. Ознакомьтесь с правилами подачи жалобы: [URL=\'https://forum.blackrussia.online/index.php?threads/Правила-подачи-жалоб-на-администрацию.3429349/\']*Клик*[/URL]<br><br>Название темы: Nick_Name администратора | Суть жалобы<br><br>Форма:<br>[ICODE]1. Ваш Nick_Name:<br>2. Nick_Name администратора:<br>3. Дата:<br>4. Суть жалобы:<br>5. Доказательства:[/ICODE]<br><br>Отказано. Закрыто.[/FONT][/SIZE]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'Запрос доказательств', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>У администратора были запрошены доказательства. Ожидайте, пожалуйста, ответа.<br><br>На рассмотрении.[/FONT][/SIZE]', prefix: PREFIX.PIN, sticky: true },
{ title: 'Наказание выдано верно', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Доказательства предоставлены. Наказание выдано верно.<br><br>Закрыто.[/FONT][/SIZE]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'Отсутствуют доказательства', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Приложите доказательства в следующей теме.<br><br>Закрыто.[/FONT][/SIZE]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'Нерабочие доказательства', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Вы приложили нерабочие доказательства.<br>В следующей теме приложите рабочие доказательства в виде ссылки на фотохостинг.<br><br>Закрыто.[/FONT][/SIZE]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'Ошибка администратора', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Администратор допустил ошибку. Приносим извинения. Наказание снято.<br><br>Закрыто.[/FONT][/SIZE]', prefix: PREFIX.ACCEPT, sticky: false },
{ title: 'Беседа с администратором', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>С администратором будет проведена профилактическая беседа.<br><br>Решено.[/FONT][/SIZE]', prefix: PREFIX.ACCEPT, sticky: false },
{ title: 'Передача Главному Администратору', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Ваша жалоба передана Главному Администратору.<br><br>Ожидайте ответа.[/FONT][/SIZE]', prefix: PREFIX.GA, sticky: true },
{ title: 'Передача Спец. Администрации', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Ваша жалоба передана Специальной Администрации.<br><br>Ожидайте ответа.[/FONT][/SIZE]', prefix: PREFIX.SPECADM, sticky: true },
{ title: 'В тех раздел', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Ваша тема не относится к жалобам на администрацию. Пожалуйста, обратитесь в технический раздел форума.<br><br>Закрыто.[/FONT][/SIZE]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'Жалоба от третьего лица', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Жалоба подана от третьего лица и не подлежит рассмотрению.<br><br>Закрыто.[/FONT][/SIZE]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'Отсутствует /time', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>В представленном доказательстве отсутствует /time.<br><br>Закрыто.[/FONT][/SIZE]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'Срок подачи жалобы истёк', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>С момента выдачи наказания прошло более 48 часов. Жалоба не подлежит рассмотрению.<br><br>Закрыто.[/FONT][/SIZE]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'Обжалование наказаний', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Ваша жалоба относится к обжалованию наказания. Обратитесь, пожалуйста, в соответствующий раздел.<br><br>Закрыто.[/FONT][/SIZE]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'Недостаточно доказательств', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Недостаточно доказательств нарушения со стороны администратора.<br><br>Закрыто.[/FONT][/SIZE]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'ᅠ ᅠ ᅠ ᅠ ᅠ ᅠ ᅠ ᅠ ᅠ ᅠ ᅠ ᅠᅠ|-(--(-(-> Обжалование наказаний <-)-)--)-| ᅠ ᅠ ᅠ ᅠ ᅠ ᅠ ᅠ ᅠ ᅠᅠ ᅠ ᅠᅠᅠ' },
{ title: 'Обжалование на рассмотрении', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Ваше обжалование взято на рассмотрение. Не нужно создавать копии этой темы.<br><br>Ожидайте ответа.[/FONT]', prefix: PREFIX.GA, sticky: true },
{ title: 'Передача ГА', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Ваше обжалование передано Главному Администратору.<br><br>Ожидайте ответа.[/FONT][/SIZE]', prefix: PREFIX.GA, sticky: true },
{ title: 'Передача СА', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Ваше обжалование передано Специальной Администрации.<br><br>Ожидайте ответа.[/FONT][/SIZE]', prefix: PREFIX.SPECADM, sticky: true },
{ title: 'В жалобы на админов', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Переношу вашу тему в раздел жалоб на администрацию.<br><br>Отказано.[/FONT][/SIZE]', prefix: PREFIX.OJIDANIE, sticky: false },
{ title: 'Взлом', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Приложите скриншоты привязок аккаунта в следующей теме.<br><br>Отказано.[/FONT][/SIZE]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'В жалобы на тех', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Вам было выдано наказание от технического специалиста, обратитесь в раздел "Жалобы на технических специалистов" нашего сервера.<br><br>Закрыто.[/FONT]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'Жалобы от 3-его лица', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Обжалование составлено от 3-го лица.<br><br>Закрыто.[/FONT]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'Присутвуют редактирования', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Доказательства должны быть в первоначальном виде, без присутствия редактирования с помощью сторонних программ.<br><br>Закрыто.[/FONT]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'Нет окна блокировки', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Без окна блокировки тема не подлежит рассмотрению. Пожалуйста, создайте новую тему и приложите окно блокировки с фотохостинга.<br>[URL="https://yapx.ru/"]yapx.ru[/URL],<br>[URL="https://imgur.com/"]imgur.com[/URL],<br>[URL="https://www.youtube.com/"]youtube.com[/URL],<br>[URL="https://imgbb.com"]ImgBB.com[/URL]<br>(все кликабельно).<br><br>Закрыто.[/FONT][/SIZE]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'Обжалование не по форме', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Ваше обжалование составлено не по форме. Пожалуйста создайте новую тему, соблюдая форму подачи:<br>Ваш никнейм и причина блокировки, пример:<br>Bruce_Banner | Массовый DM.<br>и форму обжалований:<br>[ICODE]1. Ваш Nick_Name:<br>2. Nick_Name администратора:<br>3. Дата выдачи/получения наказания:<br>4. Суть заявки:<br>5. Доказательство:[/ICODE]<br><br>Закрыто.[/FONT]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'Доква с соц сетей', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Доказательства с соц.сетей, не принимаются.<br>Загрузите их на фото-хостинг [URL="https://yapx.ru/"]yapx.ru[/URL], [URL="https://imgur.com/"]imgur.com[/URL], [URL="https://www.youtube.com/"]youtube.com[/URL],[URL="https://imgbb.com"]ImgBB.com[/URL](все кликабетильно).<br><br>Закрыто.[/FONT]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'В другой раздел', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Созданная тема никоим образом не относится к данному разделу.<br><br>Закрыто.[/FONT]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'Бан айпи', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Для решения проблемы, пожалуйста, воспользуйтесь VPN или смените сеть Wi-Fi.<br><br>Закрыто.[/FONT]', prefix: PREFIX.CLOSE, sticky: false },
{ title: 'Дубликат', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Ранее вам уже был дан ответ в подобной теме.<br>Если вы продолжите создавать дубликаты, ваш форумный аккаунт будет заблокирован.<br><br>Закрыто.[/FONT]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'НонРП Обман (свяжитесь)', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Если вы готовы возместить ущерб игроку, свяжитесь с игроком для возврата имущества, затем он должен оформить обжалование.<br><br>Закрыто.[/FONT]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'НонРП Обман (срок вышел)', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>У вас было 24 часа на проведение сделки и возмещение ущерба, а также предоставление записи (Fraps).<br>Срок истёк, однако фрапс так и не был предоставлен.<br>В связи с этим аккаунт будет заблокирован.<br><br>Закрыто.[/FONT]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'НонРП Обман (даю 24 часа)', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Аккаунт разблокирован.<br>У вас есть 24 часа для проведения сделки и возмещения ущерба.<br>После этого обязательно предоставьте запись (Fraps) в эту тему.<br><br>Закрыто.[/FONT]', prefix: PREFIX.GA, sticky: true },
{ title: 'Наказание снято', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Рассмотрев ваше обжалование, было принято решение о снятии вашего наказания.<br><br>Одобрено, закрыто.[/FONT][/SIZE]', prefix: PREFIX.ACCEPT, sticky: false },
{ title: 'ЧС Сервера', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Вы были вынесены из Чёрного Списка Сервера.<br><br>Одобрено.[/FONT][/SIZE]', prefix: PREFIX.ACCEPT, sticky: false },
{ title: 'ОЧСА', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Общий Чёрный Список Администрации снят.<br><br>Одобрено.[/FONT][/SIZE]', prefix: PREFIX.ACCEPT, sticky: false },
{ title: 'ОЧСП', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Общий Чёрный Список Проекта снят.<br><br>Одобрено.[/FONT][/SIZE]', prefix: PREFIX.ACCEPT, sticky: false },
{ title: 'ЧСДП', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Глобальная блокировка снята.<br><br>Одобрено.[/FONT][/SIZE]', prefix: PREFIX.ACCEPT, sticky: false },
{ title: 'Блокировка аккаунта', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Блокировка аккаунта снята.<br><br>Одобрено.[/FONT][/SIZE]', prefix: PREFIX.ACCEPT, sticky: false },
{ title: 'ранее был обжалован', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Вам уже было одобрено обжалование.<br><br>Закрыто.[/FONT][/SIZE]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'Обжалованию не подлежит', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Ваше обжалование не будет рассматриваться и будет закрыто, так как ваше наказание соответствует причинам, которые обжалованию не подлежат: различные формы слива, продажа игровой валюты, махинации, целенаправленный багоюз, продажа, передача аккаунта, сокрытие ошибок, багов системы, использование стороннего программного обеспечения, распространение конфиденциальной информации, обман администрации.<br><br>Отказано.[/FONT][/SIZE]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'Отказано', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>В обжаловании отказано.<br><br>Закрыто.[/FONT][/SIZE]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'Наказание сокращено', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Ваше обжалование было рассмотрено и принято решение о сокращении вашего наказания.<br><br>Одобрено.[/FONT][/SIZE]', prefix: PREFIX.ACCEPT, sticky: false },
{ title: 'Server', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Спасибо за обращение, блокировка снята.<br><br>Закрыто.[/FONT][/SIZE]', prefix: PREFIX.RASSMOTRENO, sticky: false },
{ title: 'Слив админки (бан)', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Спасибо за обращение, администратор был снят с поста и занесён в Общий чёрный список проекта. Блокировка снята. <br><br>Закрыто.[/FONT][/SIZE]', prefix: PREFIX.RASSMOTRENO, sticky: false },
{ title: 'Слив админки (мут)', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Спасибо за обращение, администратор был снят с поста и занесён в Общий чёрный список проекта. Блокировка чата снята. <br><br>Закрыто.[/FONT][/SIZE]', prefix: PREFIX.RASSMOTRENO, sticky: false },
{ title: 'Слив админки (варн)', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Спасибо за обращение, администратор был снят с поста и занесён в Общий чёрный список проекта. Предупреждение снято. <br><br>Закрыто.[/FONT][/SIZE]', prefix: PREFIX.RASSMOTRENO, sticky: false },
{ title: 'Смена ника', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Аккаунт разблокирован. У вас есть 24 часа на смену игрового никнейма. После смены, пожалуйста, предоставьте скриншот в этой теме. Тема остаётся открытой.<br><br>На рассмотрении.[/FONT][/SIZE]', prefix: PREFIX.GA, sticky: true },
{ title: 'Не сменил ник', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Ваш аккаунт был заблокирован. У вас было 24 часа на смену игрового никнейма, а также необходимо было приложить скриншот, подтверждающий смену никнейма, в данную тему.<br><br>Закрыто.[/FONT][/SIZE]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'ᅠ ᅠ ᅠ ᅠ ᅠ ᅠ ᅠ ᅠ ᅠᅠ ᅠ ᅠ ᅠᅠ|-(--(-(-> Жалобы на игроков <-)-)--)-| ᅠ ᅠ ᅠ ᅠ ᅠ ᅠ ᅠ ᅠ ᅠᅠ ᅠᅠ ᅠ ᅠᅠᅠ' },
{ title: 'Игрок будет наказан', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Игрок получит соответсвующее наказание.<br><br>Закрыто.[/FONT][/SIZE]', prefix: PREFIX.ACCEPT, sticky: false },
{ title: 'срок 72 часа', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Срок написания жалобы составляет три дня (72 часа) с момента совершенного нарушения со стороны игрока сервера. В случае истечения срока жалоба рассмотрению не подлежит.<br><br>Закрыто.[/FONT][/SIZE]', prefix: PREFIX.ACCEPT, sticky: false },
{ title: 'Нужен фрапс', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br>Для рассмотрения данного нарушения требуется предоставить видеозапись, на которой чётко зафиксировано нарушение со стороны игрока.<br><br>Закрыто.[/FONT][/SIZE]', prefix: PREFIX.UNACCEPT, sticky: false },
{ title: 'Не по форме', content: '[SIZE=4][FONT=Verdana]Здравствуйте.<br><br> Ваша жалоба составлена не по форме. Пожалуйста создайте новую тему соблюдая форму подачи:<br>Ваш никнейм и никнейм нарушителя, пример:<br>Timofei_Oleinik | OOC Обман.<br>и форму жалоб на игроков:<br>[ICODE]1. Ваш Nick_Name:<br>2. Nick_Name игрока:<br>3. Суть жалобы:<br>4. Доказательство:[/ICODE]<br><br>Закрыто.[/FONT]', prefix: PREFIX.UNACCEPT, sticky: false },
];
// ========== Группировка по категориям ==========
function buildTemplateGroups() {
const groups = [];
let currentCategory = 'Основные';
let currentItems = [];
for (const btn of rawButtons) {
if (btn.title.includes('|-(--(-(->') || (btn.title.includes('ᅠ') && btn.title.length > 20)) {
if (currentItems.length) groups.push({ category: currentCategory, items: [...currentItems] });
currentItems = [];
const match = btn.title.match(/->\s*(.+?)\s*<-/);
currentCategory = match ? match[1].trim() : btn.title.replace(/[ᅠ\s\-|()]+/g, ' ').trim();
continue;
}
currentItems.push({
title: btn.title,
icon: getIconForTitle(btn.title),
statusBadge: getStatusBadge(btn.prefix),
content: btn.content,
prefix: btn.prefix,
sticky: btn.sticky || false,
});
}
if (currentItems.length) groups.push({ category: currentCategory, items: currentItems });
return groups;
}
function getIconForTitle(title) {
if (title.includes('жалоб') || title.includes('Жалоб')) return '⚠️';
if (title.includes('Обжалование')) return '⚖️';
if (title.includes('Привет')) return '👋';
if (title.includes('Свяжитесь')) return '📞';
if (title.includes('Ссылку')) return '🔗';
if (title.includes('Наказание')) return '🔨';
if (title.includes('Ошибка')) return '❌';
if (title.includes('Слив')) return '💧';
if (title.includes('Смена')) return '✏️';
return '📌';
}
function getStatusBadge(prefix) {
if (prefix === PREFIX.ACCEPT) return { text: 'Одобрено', color: '#2e7d32' };
if (prefix === PREFIX.UNACCEPT) return { text: 'Отказано', color: '#c62828' };
if (prefix === PREFIX.PIN) return { text: 'На рассмотрении', color: '#ed6c02' };
if (prefix === PREFIX.GA || prefix === PREFIX.SPECADM) return { text: 'Передано', color: '#0288d1' };
if (prefix === PREFIX.CLOSE) return { text: 'Закрыто', color: '#6c757d' };
if (prefix === PREFIX.REALIZOVANO) return { text: 'Реализовано', color: '#0097a7' };
if (prefix === PREFIX.RASSMOTRENO) return { text: 'Рассмотрено', color: '#558b2f' };
return null;
}
let templateGroups = [];
// ========== Вспомогательные функции ==========
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>]/g, (m) => (m === '&' ? '&' : m === '<' ? '<' : '>'));
}
function getThreadData() {
const authorID = $('a.username').first().data('user-id') || $('a.username').attr('data-user-id');
const authorName = $('a.username').first().text().trim();
const hours = new Date().getHours();
let greeting = 'Доброй ночи';
if (hours > 4 && hours <= 11) greeting = 'Доброе утро';
else if (hours > 11 && hours <= 15) greeting = 'Добрый день';
else if (hours > 15 && hours <= 21) greeting = 'Добрый вечер';
return { user: { id: authorID, name: authorName, mention: `[USER=${authorID}]${authorName}[/USER]` }, greeting };
}
async function postReply(content) {
return new Promise((resolve, reject) => {
const $form = $('form[data-xf-post="quick-reply"]');
if (!$form.length) return reject('Форма быстрого ответа не найдена');
$form.find('textarea[name=message_html], .fr-element').html(content);
if (window.XFEditor && window.XFEditor.setContent) window.XFEditor.setContent(content);
else $form.find('textarea[name=message]').val(content);
const formData = new FormData($form[0]);
formData.append('_xfToken', XF.config.csrf);
formData.append('_xfResponseType', 'json');
fetch($form.attr('action'), { method: 'POST', body: formData, credentials: 'same-origin' })
.then(r => r.json())
.then(data => data.status === 'ok' ? resolve(data) : reject(data.errors || 'Ошибка отправки'))
.catch(reject);
});
}
function editThreadData(prefix, pin = false) {
const threadTitle = $('.p-title-value').last().text().trim();
const formData = new FormData();
formData.append('prefix_id', prefix);
formData.append('title', threadTitle);
if (pin) formData.append('sticky', 1);
formData.append('_xfToken', XF.config.csrf);
formData.append('_xfRequestUri', document.URL.split(XF.config.url.fullBase)[1]);
formData.append('_xfWithData', 1);
formData.append('_xfResponseType', 'json');
fetch(`${document.URL}edit`, { method: 'POST', body: formData }).then(() => location.reload());
}
async function applyTemplateById(groupIdx, itemIdx, closeModal = true) {
const item = templateGroups[groupIdx]?.items[itemIdx];
if (!item) return;
const compiled = Handlebars.compile(item.content);
const content = compiled(getThreadData());
$('.fr-element.fr-view').html(content);
$('span.fr-placeholder').empty();
await postReply(content);
if (item.prefix !== undefined) editThreadData(item.prefix, item.sticky || false);
else location.reload();
if (closeModal) {
$('#tp-modal').hide();
$('#tp-overlay').hide();
}
}
// ========== Построение модального окна ==========
function buildModalHTML() {
const groupsHtml = templateGroups.map((g, gi) => {
const itemsHtml = g.items.map((item, ii) => {
const statusHtml = item.statusBadge ? `<span class="tp-card-status" style="background:${item.statusBadge.color}">${item.statusBadge.text}</span>` : '';
return `
<div class="tp-card" data-group="${gi}" data-item="${ii}">
<div class="tp-card-icon">${item.icon || '📌'}</div>
<div class="tp-card-title">${escapeHtml(item.title)}</div>
${statusHtml}
<div class="tp-card-buttons">
<button class="tp-card-preview-btn">👁️ Просмотр</button>
<button class="tp-card-send-btn">📤 Отправить</button>
</div>
</div>
`;
}).join('');
return `<div class="tp-group"><div class="tp-group-header">${escapeHtml(g.category)}</div><div class="tp-card-grid">${itemsHtml}</div></div>`;
}).join('');
return `
<div id="tp-overlay"></div>
<div id="tp-modal">
<div id="tp-header">
<div class="tp-header-title">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#1e293b" stroke-width="1.5"><path d="M4 4H20V20H4V4Z M8 8H16V10H8V8Z M8 12H16V14H8V12Z M8 16H12V18H8V16Z"/></svg>
<span>Шаблоны ответов</span>
</div>
<button id="tp-close" class="tp-close-btn">✕</button>
</div>
<div id="tp-search-wrap">
<svg class="tp-search-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#64748b"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input type="text" id="tp-search" placeholder="Поиск...">
</div>
<div id="tp-groups-container">${groupsHtml}</div>
</div>
`;
}
// ========== ПРЕДПРОСМОТР (исправленный, гарантированно работающий) ==========
let currentPreview = null;
function showPreview(groupIdx, itemIdx) {
if (currentPreview) closePreview();
const item = templateGroups[groupIdx]?.items[itemIdx];
if (!item) {
console.warn('Шаблон не найден', groupIdx, itemIdx);
return;
}
try {
const compiled = Handlebars.compile(item.content);
const rawContent = compiled(getThreadData());
const displayContent = rawContent.replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>');
const $preview = $(`
<div class="tp-preview-overlay">
<div class="tp-preview-card">
<div class="tp-preview-header">
<span>📄 Предпросмотр</span>
<button class="tp-preview-close">✕</button>
</div>
<div class="tp-preview-content">${displayContent}</div>
<div class="tp-preview-actions">
<button class="tp-preview-apply" data-group="${groupIdx}" data-item="${itemIdx}">Применить</button>
<button class="tp-preview-apply-close" data-group="${groupIdx}" data-item="${itemIdx}">Вставить и закрыть</button>
</div>
</div>
</div>
`);
$('body').append($preview);
currentPreview = $preview;
// Центрирование
$preview.css({ position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)' });
// Закрытие по крестику или клику на фон
$preview.find('.tp-preview-close').on('click', closePreview);
$preview.on('click', (e) => {
if (e.target === $preview[0]) closePreview();
});
// Кнопки внутри превью
$preview.find('.tp-preview-apply, .tp-preview-apply-close').on('click', async function (e) {
e.stopPropagation();
const g = $(this).data('group');
const i = $(this).data('item');
await applyTemplateById(g, i, true);
closePreview();
$('#tp-modal').hide();
$('#tp-overlay').hide();
});
} catch (err) {
console.error('Ошибка при создании предпросмотра:', err);
}
}
function closePreview() {
if (currentPreview) {
currentPreview.remove();
currentPreview = null;
}
}
// ========== Стили (минимализм, белый фон, анимации) ==========
const style = document.createElement('style');
style.textContent = `
:root {
--bg-white: #ffffff;
--bg-light: #f8fafc;
--border-light: #e2e8f0;
--text-dark: #0f172a;
--accent: #3b82f6;
--btn-bg: #4b5563;
--btn-hover: #374151;
}
@keyframes modalFadeIn {
from { opacity: 0; transform: translate(-50%, -48%) scale(0.96); }
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
@keyframes cardFadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
#tp-overlay {
display: none; position: fixed; top:0; left:0; width:100%; height:100%;
background: rgba(0,0,0,0.4);
backdrop-filter: blur(3px);
z-index: 9998;
animation: fadeIn 0.2s ease;
}
#tp-modal {
display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: 85%; max-width: 750px; max-height: 85vh;
background: var(--bg-white);
border-radius: 28px;
box-shadow: 0 25px 40px -12px rgba(0,0,0,0.25);
z-index: 9999;
padding: 20px 24px;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
border: 1px solid var(--border-light);
overflow: hidden;
animation: modalFadeIn 0.2s ease;
}
#tp-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 18px; }
.tp-header-title { display: flex; align-items: center; gap: 8px; font-size: 18px; font-weight: 600; color: var(--text-dark); }
.tp-close-btn { background: none; border: none; color: #ef4444; font-size: 22px; cursor: pointer; transition: opacity 0.2s; }
.tp-close-btn:hover { opacity: 0.7; }
#tp-search-wrap { position: relative; margin-bottom: 20px; }
.tp-search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); pointer-events: none; }
#tp-search {
width: 100%; padding: 10px 12px 10px 36px;
background: var(--bg-light);
border: 1px solid var(--border-light);
border-radius: 40px;
font-size: 13px;
outline: none;
transition: 0.2s;
}
#tp-search:focus { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(59,130,246,0.2); }
#tp-groups-container { overflow-y: auto; max-height: calc(85vh - 120px); padding-right: 4px; }
.tp-group { margin-bottom: 24px; animation: cardFadeIn 0.2s ease; }
.tp-group-header {
font-size: 14px; font-weight: 600; margin-bottom: 12px; padding-bottom: 4px;
border-bottom: 1.5px solid var(--border-light);
color: var(--accent);
text-transform: uppercase;
letter-spacing: 0.3px;
}
.tp-card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 12px; }
.tp-card {
background: var(--bg-light);
border: 1px solid var(--border-light);
border-radius: 20px;
padding: 14px;
transition: all 0.25s cubic-bezier(0.2, 0, 0, 1);
cursor: default;
}
.tp-card:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(0,0,0,0.08); border-color: var(--accent); }
.tp-card-icon { font-size: 24px; margin-bottom: 8px; }
.tp-card-title { font-weight: 500; font-size: 13px; margin-bottom: 10px; line-height: 1.4; color: var(--text-dark); }
.tp-card-status { display: inline-block; padding: 2px 10px; border-radius: 30px; font-size: 10px; font-weight: 600; color: white; margin-bottom: 12px; }
.tp-card-buttons { display: flex; gap: 8px; margin-top: 4px; }
.tp-card-preview-btn, .tp-card-send-btn {
flex: 1;
background: transparent;
border: 1px solid var(--border-light);
border-radius: 40px;
padding: 6px 0;
font-size: 11px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
text-align: center;
}
.tp-card-preview-btn { color: var(--accent); border-color: var(--accent); }
.tp-card-preview-btn:hover { background: var(--accent); color: white; }
.tp-card-send-btn { color: white; background: var(--btn-bg); border: none; }
.tp-card-send-btn:hover { background: var(--btn-hover); transform: translateY(-1px); }
.tp-preview-overlay {
position: fixed; top:0; left:0; width:100%; height:100%;
background: rgba(0,0,0,0.5); backdrop-filter: blur(4px); z-index: 10000;
animation: fadeIn 0.15s ease;
}
.tp-preview-card {
background: var(--bg-white); border-radius: 24px; width: 380px; max-height: 80vh;
display: flex; flex-direction: column; box-shadow: 0 25px 40px -12px black;
border: 1px solid var(--border-light); overflow: hidden;
animation: modalFadeIn 0.15s ease;
}
.tp-preview-header {
display: flex; justify-content: space-between; padding: 12px 18px;
background: var(--bg-light); font-weight: 600; border-bottom: 1px solid var(--border-light);
color: var(--text-dark);
}
.tp-preview-close { background: none; border: none; color: #ef4444; font-size: 20px; cursor: pointer; }
.tp-preview-content {
padding: 16px; overflow-y: auto; font-family: monospace; font-size: 12px;
white-space: pre-wrap; word-wrap: break-word; background: #ffffff;
color: #1e293b !important;
max-height: 60vh;
}
.tp-preview-content * {
color: #1e293b !important;
}
white-space: pre-wrap; word-wrap: break-word; background: white; max-height: 60vh;
}
.tp-preview-actions { padding: 12px 16px; display: flex; gap: 10px; border-top: 1px solid var(--border-light); background: var(--bg-light); }
.tp-preview-actions button { flex: 1; padding: 6px 0; border-radius: 40px; border: none; font-weight: 500; font-size: 13px; cursor: pointer; transition: 0.2s; }
.tp-preview-apply { background: var(--accent); color: white; }
.tp-preview-apply-close { background: #f1f5f9; color: #1e293b; border: 1px solid #cbd5e1; }
.tp-preview-apply:hover { background: #2563eb; }
.tp-preview-apply-close:hover { background: #e2e8f0; }
.pf-prefix-btn {
background: #4b5563 !important;
border: none !important;
border-radius: 30px !important;
padding: 4px 12px !important;
font-size: 12px !important;
font-weight: 500 !important;
color: white !important;
transition: all 0.2s !important;
margin: 2px 3px !important;
display: inline-flex !important;
align-items: center !important;
gap: 6px !important;
}
.pf-prefix-btn:hover {
background: #374151 !important;
transform: translateY(-1px);
}
#tp-open-btn {
background: #4b5563 !important;
border: none !important;
border-radius: 30px !important;
padding: 4px 12px !important;
font-size: 12px !important;
color: white !important;
transition: 0.2s !important;
}
#tp-open-btn:hover {
background: #374151 !important;
transform: translateY(-1px);
}
`;
document.head.appendChild(style);
// ========== ИНИЦИАЛИЗАЦИЯ ==========
$(document).ready(async () => {
// Убедимся, что Handlebars загружен
if (!window.Handlebars) {
$('body').append('<script src="https://cdn.jsdelivr.net/npm/handlebars@latest/dist/handlebars.js"></script>');
await new Promise(resolve => {
const check = setInterval(() => {
if (window.Handlebars) {
clearInterval(check);
resolve();
}
}, 100);
});
}
// Построение групп шаблонов
templateGroups = buildTemplateGroups();
// Кнопки быстрых префиксов
const btnConfig = [
{ text: '⏳ На рассмотрение', prefix: PREFIX.PIN, pin: true },
{ text: '❌ Отказано', prefix: PREFIX.UNACCEPT, pin: false },
{ text: '✅ Одобрено', prefix: PREFIX.ACCEPT, pin: false },
{ text: '🔥 Спецадмин', prefix: PREFIX.SPECADM, pin: true },
{ text: '🛠️ Тех.спец', prefix: PREFIX.TEXY, pin: false },
{ text: '👑 Гл.админ', prefix: PREFIX.GA, pin: true },
{ text: '🔒 Закрыто', prefix: PREFIX.CLOSE, pin: false },
{ text: '🎯 Реализовано', prefix: PREFIX.REALIZOVANO, pin: false },
{ text: '❗ Важно', prefix: PREFIX.VAJNO, pin: false },
{ text: '⏰ Ожидание', prefix: PREFIX.OJIDANIE, pin: false },
{ text: '✔️ Рассмотрено', prefix: PREFIX.RASSMOTRENO, pin: false },
{ text: '⚪ Без префикса', prefix: PREFIX.NONE, pin: false },
{ text: '🔍 Контроль качества', prefix: PREFIX.KACHESTVO, pin: false },
];
btnConfig.forEach(cfg => {
$('.button--icon--reply').before(
`<button type="button" class="button rippleButton pf-prefix-btn" data-prefix="${cfg.prefix}" data-pin="${cfg.pin}" style="margin:2px;">${cfg.text}</button>`
);
});
// Кнопка открытия модального окна
$('.button--icon--reply').before('<button type="button" class="button rippleButton" id="tp-open-btn" style="margin:2px;">📋 Шаблоны</button>');
$('body').append(buildModalHTML());
const $modal = $('#tp-modal');
const $overlay = $('#tp-overlay');
const $search = $('#tp-search');
const $groupsContainer = $('#tp-groups-container');
$('#tp-open-btn').click(() => {
$modal.show();
$overlay.show();
$search.val('').trigger('input');
closePreview();
});
$('#tp-close, #tp-overlay').click(() => {
$modal.hide();
$overlay.hide();
closePreview();
});
$search.on('input', function () {
const q = this.value.trim().toLowerCase();
$groupsContainer.find('.tp-group').each(function () {
let visible = false;
$(this).find('.tp-card').each(function () {
const txt = $(this).find('.tp-card-title').text().toLowerCase();
const match = txt.includes(q);
$(this).toggle(match);
if (match) visible = true;
});
$(this).toggle(visible);
});
});
// Просмотр
$(document).on('click', '.tp-card-preview-btn', function (e) {
e.stopPropagation();
const $card = $(this).closest('.tp-card');
const g = $card.data('group');
const i = $card.data('item');
if (g !== undefined && i !== undefined) {
showPreview(g, i);
} else {
console.warn('data-group или data-item не найдены', $card);
}
});
// Отправить
$(document).on('click', '.tp-card-send-btn', async function (e) {
e.stopPropagation();
const $card = $(this).closest('.tp-card');
const g = $card.data('group');
const i = $card.data('item');
if (g !== undefined && i !== undefined) {
await applyTemplateById(g, i, true);
$modal.hide();
$overlay.hide();
}
});
// Кнопки префиксов
$(document).on('click', '.pf-prefix-btn', function () {
const prefix = parseInt($(this).data('prefix'));
const pin = $(this).data('pin') === 'true';
editThreadData(prefix, pin);
});
});
})();