Панель куратора BR — автоник, свайп, редактор шаблонов, фото, змейка, поддержка
// ==UserScript==
// @name BR Admin Panel Pro v5
// @namespace https://vk.ru/club237051164
// @version 5.1.0
// @description Панель куратора BR — автоник, свайп, редактор шаблонов, фото, змейка, поддержка
// @author Akzholch1k | vk.ru/brunoverona
// @match https://forum.blackrussia.online/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @connect api.telegram.org
// @connect api.imgbb.com
// @connect *
// @license MIT
// ==/UserScript==
(function () {
'use strict';
/* ══════════════════════════════════════
КОНФИГ
══════════════════════════════════════ */
const TG_TOKEN = '8572813058:AAEdG21L9oRpvP_nbuiT8l8xDMufJt2s-bo';
const TG_CID = '6564808901';
const SERVER = 'https://ВАШ_ПРОЕКТ.railway.app'; // ← заменить на свой Railway URL
const SECRET = 'brpanel2024';
const CR_LINK = 'cr-mp://connect/blackrussia.online:7777';
const PREFIXES = [
{ id:0, name:'— Без префикса', color:'#666' },
{ id:2, name:'📌 Закреплено', color:'#f0c040' },
{ id:4, name:'❌ Отказано', color:'#ff4f4f' },
{ id:5, name:'⏳ На рассмотрении', color:'#ec7c26' },
{ id:6, name:'✅ Решено', color:'#2ed47a' },
{ id:7, name:'🔒 Закрыто', color:'#888' },
{ id:8, name:'✔️ Одобрено', color:'#4caf50' },
{ id:9, name:'👁 Рассмотрено', color:'#2196f3' },
{ id:10, name:'📢 Команде проекта', color:'#9c27b0' },
{ id:12, name:'🏛 ГА', color:'#ff9800' },
{ id:13, name:'🔧 Тех. специалисту', color:'#00bcd4' },
{ id:14, name:'⌛ Ожидание', color:'#607d8b' },
];
/* ══════════════════════════════════════
ХРАНИЛИЩЕ
══════════════════════════════════════ */
const S = {
get: (k,d) => { try { const v=GM_getValue(k,null); return v?JSON.parse(v):d; } catch { return d; } },
set: (k,v) => GM_setValue(k, JSON.stringify(v)),
};
const DEFAULT_TEMPLATES = [
{ id:1, name:'На рассмотрении', prefix:5, autopin:true, color:'#ec7c26',
content:'[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{greeting}}, уважаемый(ая) игрок [USER={{uid}}]{{username}}[/USER][/ICODE].[/CENTER][/I][/SIZE][/FONT][/COLOR]\n\n[CENTER][url=https://postimages.org/][img]https://i.postimg.cc/cCG97p5p/Pics-Art-07-12-03-23-18-1.png[/img][/url][/CENTER]\n\n[B][CENTER][FONT=times new roman][COLOR=#ffffff][ICODE]Ваша заявка принята и находится на рассмотрении.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n\n[B][CENTER][FONT=times new roman][COLOR=#ec7c26][ICODE]На рассмотрении.[/ICODE][/COLOR][/CENTER][/FONT][/B]' },
{ id:2, name:'Одобрено', prefix:8, autopin:true, color:'#2ed47a',
content:'[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{greeting}}, уважаемый(ая) игрок [USER={{uid}}]{{username}}[/USER][/ICODE].[/CENTER][/I][/SIZE][/FONT][/COLOR]\n\n[CENTER][url=https://postimages.org/][img]https://i.postimg.cc/cCG97p5p/Pics-Art-07-12-03-23-18-1.png[/img][/url][/CENTER]\n\n[B][CENTER][FONT=times new roman][COLOR=#00FF00][ICODE]Ваша заявка одобрена. Поздравляем![/ICODE][/COLOR][/CENTER][/FONT][/B]\n\n[B][CENTER][FONT=times new roman][COLOR=#00FF00][ICODE]Одобрено.[/ICODE][/COLOR][/CENTER][/FONT][/B]' },
{ id:3, name:'Отказано', prefix:4, autopin:true, color:'#ff4f4f',
content:'[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{greeting}}, уважаемый(ая) игрок [USER={{uid}}]{{username}}[/USER][/ICODE].[/CENTER][/I][/SIZE][/FONT][/COLOR]\n\n[B][CENTER][FONT=times new roman][COLOR=#FF4444][ICODE]К сожалению, ваша заявка отклонена.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n\n[B][CENTER][FONT=times new roman][COLOR=#FF4444][ICODE]Отказано.[/ICODE][/COLOR][/CENTER][/FONT][/B]' },
{ id:4, name:'Закрыто', prefix:7, autopin:false, color:'#888',
content:'[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{greeting}}, уважаемый(ая) игрок [USER={{uid}}]{{username}}[/USER][/ICODE].[/CENTER][/I][/SIZE][/FONT][/COLOR]\n\n[B][CENTER][FONT=times new roman][COLOR=#aaaaaa][ICODE]Тема закрыта куратором.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n\n[B][CENTER][FONT=times new roman][COLOR=#aaaaaa][ICODE]Закрыто.[/ICODE][/COLOR][/CENTER][/FONT][/B]' },
{ id:5, name:'Нет доказательств', prefix:4, autopin:false, color:'#ff4f4f',
content:'[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{greeting}}, уважаемый(ая) игрок [USER={{uid}}]{{username}}[/USER][/ICODE].[/CENTER][/I][/SIZE][/FONT][/COLOR]\n\n[B][CENTER][FONT=times new roman][COLOR=#FF4444][ICODE]В вашей жалобе отсутствуют доказательства.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n\n[B][CENTER][FONT=times new roman][COLOR=#aaaaaa][ICODE]Закрыто.[/ICODE][/COLOR][/CENTER][/FONT][/B]' },
];
let nickname = S.get('brp_nick', null);
let templates = S.get('brp_tpls', DEFAULT_TEMPLATES);
let logs = S.get('brp_logs', []);
let users = S.get('brp_users', 0);
let panelOpen = false;
let activeTab = 'tpl';
let currentPage = 0;
let unread = 0;
let forumUser = { name: null, uid: null };
// FIX: глобальные таймеры — чтобы не плодить бесконечные intervals
let _pollTimer = null;
let _pingTimer = null;
let _urlTimer = null;
// FIX: RAF для частиц логин-экрана — храним ID чтобы остановить
let _loginRaf = null;
// FIX: таймер змейки — храним чтобы остановить при выходе
let _snakeTimer = null;
// FIX: обработчик клавиш змейки — храним чтобы удалить
let _snakeKeyHandler = null;
function saveTpls() { S.set('brp_tpls', templates); }
function saveLogs() { S.set('brp_logs', logs.slice(-300)); }
// Проверка: настроен ли сервер (не заглушка)
function serverOk() { return SERVER && !SERVER.includes('ВАШ_ПРОЕКТ'); }
/* ══════════════════════════════════════
АВТО-НИК С ФОРУМА
══════════════════════════════════════ */
function detectForumUser() {
try {
// Свой ник — ищем текст в навбаре
const nameEl = document.querySelector('.p-navgroup-link--user .p-navgroup-linkText');
if (nameEl) {
const n = nameEl.textContent.trim();
if (n) { nickname = n; S.set('brp_nick', nickname); }
}
// Автор первого поста в теме
const authorLink = document.querySelector('article.message--post:first-of-type a.username[data-user-id]')
|| document.querySelector('a.username[data-user-id]');
if (authorLink) {
forumUser.name = authorLink.textContent.trim();
forumUser.uid = authorLink.getAttribute('data-user-id') || '';
}
} catch {}
}
/* ══════════════════════════════════════
УТИЛИТЫ
══════════════════════════════════════ */
function greeting() {
const h = new Date().getHours();
if (h>=5 && h<12) return 'Доброе утро';
if (h>=12 && h<17) return 'Добрый день';
if (h>=17 && h<22) return 'Добрый вечер';
return 'Доброй ночи';
}
function pfx(id) { return PREFIXES.find(p=>p.id===id) || PREFIXES[0]; }
function addLog(a, d) {
const entry = { t:new Date().toLocaleString('ru-RU'), u:nickname||'?', a, d };
logs.push(entry); saveLogs();
tg(`📋 <b>${a}</b>\n👤 ${nickname||'?'}\n📝 ${d}\n🕐 ${entry.t}`);
}
function tg(text, chatId) {
GM_xmlhttpRequest({
method:'POST',
url:`https://api.telegram.org/bot${TG_TOKEN}/sendMessage`,
headers:{'Content-Type':'application/json'},
data: JSON.stringify({ chat_id: chatId||TG_CID, text, parse_mode:'HTML' }),
});
}
function toast(msg, ms=3000) {
const el = document.createElement('div');
el.className = 'brv5-toast'; el.textContent = msg;
document.body.appendChild(el);
requestAnimationFrame(() => el.classList.add('in'));
setTimeout(() => { el.classList.remove('in'); setTimeout(() => el.remove(), 350); }, ms);
}
function applyMacros(text) {
return text
.replace(/{{greeting}}/g, greeting())
.replace(/{{username}}/g, forumUser.name || 'Игрок')
.replace(/{{uid}}/g, forumUser.uid || '0');
}
/* ══════════════════════════════════════
РЕДАКТОР ФОРУМА (XenForo / Froala)
══════════════════════════════════════ */
function findEditor() {
for (const f of document.querySelectorAll('iframe')) {
try {
const b = (f.contentDocument||f.contentWindow.document).body;
if (b?.contentEditable==='true') return {t:'ce', el:b, doc:f.contentDocument||f.contentWindow.document};
} catch {}
}
const ce = document.querySelector('.fr-element[contenteditable="true"],[contenteditable="true"][role="textbox"]');
if (ce) return {t:'ce', el:ce, doc:document};
const ta = document.querySelector('textarea[name="message"],#ctrl_body');
if (ta) return {t:'ta', el:ta};
return null;
}
function insertText(text) {
const ed = findEditor();
if (!ed) { toast('❌ Открой поле ответа на форуме'); return false; }
if (ed.t==='ta') {
ed.el.value = text;
ed.el.dispatchEvent(new Event('input',{bubbles:true}));
} else {
ed.el.focus();
try { ed.doc.execCommand('selectAll',false,null); ed.doc.execCommand('insertText',false,text); }
catch { ed.el.innerText = text; }
ed.el.dispatchEvent(new Event('input',{bubbles:true}));
}
return true;
}
function applyPrefix(id) {
if (!id) return;
const el = document.querySelector('select[name="prefix_id"],#ctrl_prefix_id,select.prefixMenu');
if (!el) return;
el.value = String(id);
el.dispatchEvent(new Event('change',{bubbles:true}));
}
function submitForm() {
const btn = document.querySelector('button[data-handler="submit"],.js-submit-button,button.button--primary');
if (btn) { btn.click(); return true; }
return false;
}
// FIX: правильный endpoint для редактирования темы XenForo
// Оригинал делал fetch(location.href + 'edit') — это неверно,
// нужно /threads/{id}/edit
function editThread(prefix, sticky=false, open=true) {
const m = location.pathname.match(/threads\/[^/]+\.(\d+)\//);
if (!m) return; // не страница темы — выходим
const threadId = m[1];
const titleEl = document.querySelector('.p-title-value');
const title = titleEl ? (titleEl.lastChild?.textContent || titleEl.textContent).trim() : '';
const token = (typeof XF !== 'undefined' && XF.config?.csrf)
? XF.config.csrf
: (document.querySelector('input[name="_xfToken"]')?.value || '');
const body = new FormData();
body.append('prefix_id', prefix);
if (title) body.append('title', title);
if (sticky) body.append('sticky', '1');
body.append('discussion_open', open ? '1' : '0');
if (token) {
body.append('_xfToken', token);
body.append('_xfRequestUri', location.pathname);
body.append('_xfWithData', '1');
body.append('_xfResponseType', 'json');
}
fetch(`/threads/${threadId}/edit`, { method:'POST', body })
.then(r => { if (r.ok) location.reload(); })
.catch(() => {});
}
function applyTemplate(tpl) {
detectForumUser(); // обновляем данные автора прямо перед вставкой
const text = applyMacros(tpl.content);
if (!insertText(text)) return;
if (tpl.prefix) {
applyPrefix(tpl.prefix);
editThread(tpl.prefix, tpl.autopin, !tpl.autopin);
}
addLog('Шаблон', `${tpl.name} → ${forumUser.name||'?'}`);
toast(`✅ "${tpl.name}" вставлен`);
setTimeout(() => { if (!submitForm()) toast('⚠️ Нажми "Ответить" вручную'); }, 900);
}
/* ══════════════════════════════════════
ЗАГРУЗКА ФОТО (imgbb)
══════════════════════════════════════ */
function uploadPhoto(file, cb) {
const key = S.get('brp_imgbb','');
if (!key) { cb(null, 'Нет ключа imgbb. Добавьте ключ в настройках (страница 2).'); return; }
const fd = new FormData();
fd.append('image', file);
GM_xmlhttpRequest({
method:'POST',
url:`https://api.imgbb.com/1/upload?key=${key}`,
data: fd,
onload: r => {
try {
const d = JSON.parse(r.responseText);
if (d.success) cb(d.data.url, null);
else cb(null, 'Ошибка imgbb: ' + (d.error?.message||'?'));
} catch { cb(null,'parse error'); }
},
onerror: () => cb(null,'network error'),
});
}
/* ══════════════════════════════════════
API СЕРВЕР
FIX: все вызовы api() тихо завершаются если SERVER не настроен
══════════════════════════════════════ */
function api(path, body, cb) {
if (!serverOk()) { cb('no_server', null); return; }
GM_xmlhttpRequest({
method:'POST', url:SERVER+path,
headers:{'Content-Type':'application/json'},
data: JSON.stringify({secret:SECRET,...body}),
onload: r => { try { cb(null,JSON.parse(r.responseText)); } catch { cb('parse',null); } },
onerror: () => cb('net',null),
});
}
function sendToSupport(text, cb) {
const info = { url:location.href, topic:document.querySelector('.p-title-value')?.textContent?.trim()||'' };
api('/send',{nick:nickname,text,...info},cb);
}
function pollMessages(cb) {
api('/poll',{nick:nickname},(e,d) => { if(!e && d?.messages?.length) cb(d.messages); });
}
function loadHistory(cb) {
api('/history',{nick:nickname},(e,d) => cb(e ? [] : (d?.messages||[])));
}
/* ══════════════════════════════════════
CSS
══════════════════════════════════════ */
document.head.appendChild(Object.assign(document.createElement('style'), { textContent:`
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&family=Rajdhani:wght@500;700&display=swap');
:root{
--bg:#080510;--card:rgba(16,10,28,.96);--pri:#8a2be2;--pril:#b066ff;--prid:#5f27cd;
--txt:#fff;--mut:#777;--inp:rgba(255,255,255,.07);--brd:rgba(138,43,226,.2);
--grn:#2ed47a;--red:#ff4f4f;--r:14px;--sh:0 20px 60px rgba(0,0,0,.8);
}
.brv5-toast{
position:fixed;bottom:90px;left:50%;transform:translateX(-50%) translateY(10px);
background:var(--card);border:1px solid var(--brd);border-radius:30px;
padding:10px 22px;font:13px 'Inter',sans-serif;color:var(--txt);
z-index:9999999;opacity:0;transition:.35s;pointer-events:none;
white-space:nowrap;backdrop-filter:blur(14px);box-shadow:var(--sh);
}
.brv5-toast.in{opacity:1;transform:translateX(-50%) translateY(0)}
/* ── ЛОГИН ── */
#brv5-login{position:fixed;inset:0;z-index:999998;background:var(--bg);display:flex;align-items:center;justify-content:center;overflow:hidden}
#brv5-canvas{position:absolute;inset:0;pointer-events:none}
.brv5-login-bg{position:absolute;inset:0;background:radial-gradient(circle at 70% 20%,rgba(138,43,226,.35),transparent 55%),radial-gradient(circle at 20% 80%,rgba(45,13,80,.6),transparent 50%)}
.brv5-auth{position:relative;z-index:2;background:var(--card);width:min(400px,92vw);padding:46px 34px;border-radius:22px;border:1px solid var(--brd);backdrop-filter:blur(20px);text-align:center;animation:brv5Pop .5s cubic-bezier(.34,1.56,.64,1);box-shadow:0 0 80px rgba(138,43,226,.15),var(--sh)}
.brv5-logo{font:700 2.2rem 'Rajdhani',sans-serif;color:var(--txt);letter-spacing:3px;margin-bottom:6px}
.brv5-logo span{color:var(--pril)}
.brv5-sub{color:var(--mut);font-size:.82rem;margin-bottom:28px;line-height:1.5}
.brv5-field{position:relative;margin-bottom:14px;text-align:left}
.brv5-field i{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:var(--pril);font-size:1rem;pointer-events:none}
.brv5-field input{width:100%;box-sizing:border-box;padding:13px 14px 13px 44px;background:var(--inp);border:1px solid rgba(255,255,255,.1);border-radius:10px;color:var(--txt);font-size:.9rem;font-family:'Inter',sans-serif;outline:none;transition:.3s}
.brv5-field input:focus{border-color:var(--pri);background:rgba(138,43,226,.12)}
.brv5-detect{font-size:11px;color:var(--grn);margin-bottom:12px;min-height:16px}
.brv5-btn{width:100%;padding:14px;border:none;border-radius:11px;background:linear-gradient(90deg,var(--pri),var(--prid));color:#fff;font-weight:700;font-size:1rem;font-family:'Inter',sans-serif;cursor:pointer;transition:.3s;letter-spacing:.5px}
.brv5-btn:hover{filter:brightness(1.2);box-shadow:0 10px 28px rgba(138,43,226,.4)}
.brv5-foot{margin-top:18px;color:var(--mut);font-size:.8rem}
.brv5-foot a{color:var(--txt);font-weight:600;text-decoration:none}
/* ── FAB ── */
#brv5-fab{position:fixed;bottom:22px;right:22px;z-index:999996;width:52px;height:52px;border-radius:50%;background:linear-gradient(135deg,var(--pri),var(--prid));border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:20px;box-shadow:0 4px 24px rgba(138,43,226,.5);transition:.2s}
#brv5-fab:hover{transform:scale(1.12)}
.brv5-fab-badge{position:absolute;top:-4px;right:-4px;background:var(--red);color:#fff;border-radius:50%;width:18px;height:18px;font-size:10px;font-weight:700;display:none;align-items:center;justify-content:center;font-family:'Inter',sans-serif}
.brv5-fab-badge.show{display:flex}
/* ── ПАНЕЛЬ ── */
#brv5-panel{position:fixed;bottom:84px;right:22px;z-index:999995;width:min(500px,96vw);max-height:86vh;background:var(--card);border:1px solid var(--brd);border-radius:var(--r);box-shadow:var(--sh);backdrop-filter:blur(20px);font-family:'Inter',sans-serif;color:var(--txt);display:none;flex-direction:column;overflow:hidden}
#brv5-panel.open{display:flex;animation:brv5Slide .3s cubic-bezier(.34,1.3,.64,1)}
/* ── ШАПКА ── */
.brv5-hdr{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;flex-shrink:0;background:linear-gradient(90deg,rgba(40,10,70,.9),rgba(16,10,28,.9));border-bottom:1px solid var(--brd)}
.brv5-hdr-l{display:flex;align-items:center;gap:8px}
.brv5-logo-sm{font:700 16px 'Rajdhani',sans-serif;color:var(--pril);letter-spacing:2px}
.brv5-nick-sm{font-size:11px;color:var(--mut)}
.brv5-dot{width:7px;height:7px;border-radius:50%;background:var(--grn);box-shadow:0 0 6px var(--grn)}
.brv5-x{background:none;border:none;color:var(--mut);font-size:18px;cursor:pointer;transition:.2s;padding:0;line-height:1}
.brv5-x:hover{color:var(--red)}
/* ── ТРЕКЕР ── */
.brv5-tracker{padding:5px 16px;font-size:10px;color:var(--mut);background:rgba(0,0,0,.3);border-bottom:1px solid var(--brd);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex-shrink:0}
.brv5-tracker span{color:var(--pril)}
/* ── СТРАНИЦЫ (свайп) ── */
.brv5-pages-wrap{position:relative;flex:1;overflow:hidden;display:flex;flex-direction:column}
.brv5-page-dots{display:flex;justify-content:center;gap:5px;padding:5px;flex-shrink:0;background:rgba(0,0,0,.2)}
.brv5-dot-ind{width:6px;height:6px;border-radius:50%;background:var(--brd);transition:.2s;cursor:pointer}
.brv5-dot-ind.on{background:var(--pril);width:18px;border-radius:3px}
.brv5-pages{display:flex;flex:1;transition:transform .35s cubic-bezier(.4,0,.2,1);will-change:transform}
.brv5-page{flex:0 0 100%;display:flex;flex-direction:column;overflow:hidden}
/* ── НАВ ── */
.brv5-nav{display:flex;padding:4px 6px;gap:2px;flex-shrink:0;background:rgba(8,5,16,.9);border-bottom:1px solid var(--brd)}
.brv5-tab{flex:1;display:flex;flex-direction:column;align-items:center;gap:2px;padding:7px 2px;background:none;border:none;cursor:pointer;font-size:10px;color:var(--mut);font-family:'Inter',sans-serif;border-radius:8px;transition:.2s;position:relative}
.brv5-tab i{font-size:13px;transition:transform .2s}
.brv5-tab:hover{color:var(--pril);background:rgba(138,43,226,.1)}
.brv5-tab:hover i{transform:translateY(-2px)}
.brv5-tab.on{color:var(--pril)}
.brv5-tab.on::after{content:'';position:absolute;bottom:0;left:50%;transform:translateX(-50%);width:16px;height:2px;background:var(--pril);border-radius:2px}
.brv5-tab-b{position:absolute;top:2px;right:4px;background:var(--red);color:#fff;border-radius:50%;width:13px;height:13px;font-size:9px;display:none;align-items:center;justify-content:center}
.brv5-tab-b.show{display:flex}
/* ── ТЕЛО ── */
.brv5-body{flex:1;overflow-y:auto;padding:13px}
.brv5-body::-webkit-scrollbar{width:3px}
.brv5-body::-webkit-scrollbar-thumb{background:var(--brd);border-radius:2px}
.brv5-sec{font:700 10px 'Rajdhani',sans-serif;color:var(--mut);text-transform:uppercase;letter-spacing:1.5px;margin:0 0 10px}
.brv5-hr{border:none;border-top:1px solid var(--brd);margin:12px 0}
/* ── ШАБЛОНЫ ── */
.brv5-tpl{background:rgba(138,43,226,.07);border:1px solid var(--brd);border-radius:10px;padding:12px 14px;margin-bottom:8px;transition:.2s}
.brv5-tpl:hover{border-color:var(--pril);background:rgba(138,43,226,.13)}
.brv5-tpl-top{display:flex;align-items:center;gap:6px;margin-bottom:3px;flex-wrap:wrap}
.brv5-tpl-name{font-size:13px;font-weight:700}
.brv5-bdg{font-size:9px;padding:2px 7px;border-radius:20px;background:rgba(255,255,255,.07);font-weight:700}
.brv5-prev{font-size:11px;color:var(--mut);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-bottom:9px}
.brv5-btns{display:flex;gap:5px;flex-wrap:wrap}
/* ── ЧАТ ── */
.brv5-msgs{overflow-y:auto;padding:6px 0;display:flex;flex-direction:column;gap:7px;max-height:260px;min-height:120px}
.brv5-msgs::-webkit-scrollbar{width:3px}
.brv5-msgs::-webkit-scrollbar-thumb{background:var(--brd)}
.brv5-msg{max-width:88%;padding:9px 13px;border-radius:12px;font-size:13px;line-height:1.4}
.brv5-msg.user{align-self:flex-end;background:rgba(138,43,226,.25);border:1px solid rgba(138,43,226,.4);border-bottom-right-radius:3px}
.brv5-msg.support{align-self:flex-start;background:rgba(46,212,122,.12);border:1px solid rgba(46,212,122,.3);border-bottom-left-radius:3px}
.brv5-msg-from{font-size:10px;color:var(--grn);font-weight:700;margin-bottom:2px}
.brv5-msg-time{font-size:9px;color:var(--mut);margin-top:3px;text-align:right}
.brv5-chat-hint{font-size:11px;color:var(--mut);margin-bottom:8px;text-align:center;padding:7px;background:rgba(138,43,226,.06);border-radius:8px}
.brv5-chat-row{display:flex;gap:6px;margin-top:8px;align-items:flex-end}
.brv5-chat-ta{flex:1;background:var(--inp);border:1px solid var(--brd);border-radius:10px;padding:9px 12px;color:var(--txt);font-size:13px;font-family:'Inter',sans-serif;outline:none;resize:none;min-height:38px;max-height:90px;transition:.2s}
.brv5-chat-ta:focus{border-color:var(--pri)}
.brv5-chat-send{background:linear-gradient(135deg,var(--pri),var(--prid));border:none;border-radius:10px;padding:9px 13px;color:#fff;font-size:14px;cursor:pointer;flex-shrink:0;transition:.2s}
/* ── ИНПУТЫ ── */
.brv5-fg{margin-bottom:11px}
.brv5-fg label{display:block;font-size:11px;color:var(--mut);margin-bottom:3px}
.brv5-inp,.brv5-ta,.brv5-sel{width:100%;box-sizing:border-box;background:var(--inp);border:1px solid rgba(255,255,255,.1);border-radius:9px;padding:9px 13px;color:var(--txt);font-size:13px;font-family:'Inter',sans-serif;outline:none;transition:.25s}
.brv5-inp:focus,.brv5-ta:focus,.brv5-sel:focus{border-color:var(--pri);background:rgba(138,43,226,.1)}
.brv5-ta{min-height:80px;resize:vertical}
.brv5-sel option{background:#120c20}
/* ── КНОПКИ ── */
.btn5{background:linear-gradient(90deg,var(--pri),var(--prid));border:none;border-radius:8px;padding:8px 14px;color:#fff;font-size:12px;font-weight:700;font-family:'Inter',sans-serif;cursor:pointer;transition:.2s}
.btn5:hover{filter:brightness(1.2)}.btn5:active{transform:scale(.97)}
.btn5g{background:transparent;border:1px solid rgba(255,255,255,.15);border-radius:8px;padding:8px 14px;color:var(--txt);font-size:12px;font-family:'Inter',sans-serif;cursor:pointer;transition:.2s}
.btn5g:hover{border-color:var(--pril);color:var(--pril)}
.btn5r{background:rgba(255,79,79,.1);border:1px solid rgba(255,79,79,.3);border-radius:8px;padding:8px 14px;color:var(--red);font-size:12px;font-family:'Inter',sans-serif;cursor:pointer;transition:.2s}
.btn5r:hover{background:rgba(255,79,79,.25)}
.btn5grn{background:rgba(46,212,122,.12);border:1px solid rgba(46,212,122,.3);border-radius:8px;padding:8px 14px;color:var(--grn);font-size:12px;font-family:'Inter',sans-serif;cursor:pointer;transition:.2s}
.btn5grn:hover{background:rgba(46,212,122,.28)}
/* ── РЕДАКТОР ШАБЛОНОВ (стр.2) ── */
.brv5-color-row{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:10px}
.brv5-color-btn{width:24px;height:24px;border-radius:50%;border:2px solid transparent;cursor:pointer;transition:.2s;flex-shrink:0}
.brv5-color-btn:hover,.brv5-color-btn.on{border-color:#fff;transform:scale(1.2)}
.brv5-bb-chips{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:10px}
.brv5-chip{background:rgba(138,43,226,.1);border:1px solid var(--brd);border-radius:20px;padding:4px 10px;font-size:11px;color:var(--mut);cursor:pointer;font-family:'Inter',sans-serif;transition:.2s}
.brv5-chip:hover{border-color:var(--pril);color:var(--pril)}
/* ── ФОТО ЗАГРУЗКА ── */
.brv5-upload-zone{border:2px dashed var(--brd);border-radius:12px;padding:24px;text-align:center;cursor:pointer;transition:.2s;margin-bottom:10px}
.brv5-upload-zone:hover{border-color:var(--pril)}
.brv5-upload-zone.drag{border-color:var(--pril);background:rgba(138,43,226,.08)}
.brv5-upload-ico{font-size:32px;margin-bottom:6px}
.brv5-upload-hint{font-size:12px;color:var(--mut)}
.brv5-photo-result{background:rgba(46,212,122,.08);border:1px solid rgba(46,212,122,.2);border-radius:9px;padding:10px;margin-top:8px;display:none}
.brv5-photo-url{font-size:11px;word-break:break-all;color:var(--grn);margin-bottom:6px}
/* ── ЗМЕЙКА ── */
#brv5-snake-canvas{border-radius:8px;display:block;margin:0 auto}
.brv5-snake-controls{display:flex;gap:8px;margin-top:8px;flex-direction:column;align-items:center}
.brv5-dpad{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:120px}
.brv5-dpad button{background:rgba(138,43,226,.15);border:1px solid var(--brd);border-radius:7px;padding:10px;color:var(--txt);font-size:14px;cursor:pointer;transition:.2s}
.brv5-dpad button:hover{background:rgba(138,43,226,.35)}
.brv5-snake-score{font:700 14px 'Rajdhani',sans-serif;color:var(--pril);text-align:center;margin-bottom:6px}
/* ── ЛОГИ ── */
.brv5-log{font-size:11px;padding:7px 10px;border-left:2px solid var(--brd);margin-bottom:5px;color:var(--mut);line-height:1.5}
.brv5-log b{color:var(--txt)}
.brv5-log-t{font-size:10px;float:right}
/* ── КОНТАКТЫ ── */
.brv5-con{display:flex;align-items:center;gap:12px;background:rgba(138,43,226,.07);border:1px solid var(--brd);border-radius:10px;padding:12px 14px;margin-bottom:8px;text-decoration:none;transition:.2s}
.brv5-con:hover{border-color:var(--pril);background:rgba(138,43,226,.15)}
.brv5-con-i{font-size:20px}.brv5-con-n{font-size:13px;font-weight:700;color:var(--txt)}.brv5-con-s{font-size:11px;color:var(--mut)}
/* ── ИГРА BR ── */
.brv5-game-card{background:rgba(138,43,226,.1);border:2px solid var(--brd);border-radius:14px;padding:20px;text-align:center;margin-bottom:12px;transition:.2s}
.brv5-game-card:hover{border-color:var(--pril)}
.brv5-game-ico{font-size:40px;margin-bottom:8px}
.brv5-game-title{font:700 16px 'Rajdhani',sans-serif;color:var(--pril);margin-bottom:4px;letter-spacing:1px}
.brv5-game-sub{font-size:11px;color:var(--mut);margin-bottom:14px}
/* ── АНИМАЦИИ ── */
@keyframes brv5Pop{from{transform:scale(.8) translateY(20px);opacity:0}to{transform:scale(1);opacity:1}}
@keyframes brv5Slide{from{transform:translateY(16px) scale(.97);opacity:0}to{transform:translateY(0) scale(1);opacity:1}}
@media(max-width:540px){
#brv5-panel{right:5px;left:5px;width:auto;bottom:72px}
#brv5-fab{bottom:14px;right:14px;width:46px;height:46px;font-size:18px}
.brv5-tab{font-size:9px}.brv5-auth{padding:32px 18px}
}
`}));
// FontAwesome
if (!document.querySelector('link[href*="font-awesome"]')) {
document.head.appendChild(Object.assign(document.createElement('link'),{rel:'stylesheet',href:'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css'}));
}
/* ══════════════════════════════════════
ЛОГИН
══════════════════════════════════════ */
function buildLogin() {
detectForumUser();
const wrap = document.createElement('div');
wrap.id = 'brv5-login';
wrap.innerHTML = `
<canvas id="brv5-canvas"></canvas>
<div class="brv5-login-bg"></div>
<div class="brv5-auth">
<div class="brv5-logo">BR <span>ADMIN</span></div>
<p class="brv5-sub">Панель куратора форума Black Russia</p>
<div class="brv5-detect" id="brv5-detect">${nickname ? '✅ Ник определён: '+nickname : '🔍 Определяем ник...'}</div>
<div class="brv5-field">
<i class="fa-solid fa-user"></i>
<input type="text" id="brv5-nick-i" value="${nickname||''}" placeholder="Ваш никнейм" autocomplete="off" />
</div>
<button class="brv5-btn" id="brv5-go">Войти в панель</button>
<div class="brv5-foot">Разработчик: <a href="https://vk.ru/brunoverona" target="_blank">vk.ru/brunoverona</a></div>
</div>
`;
document.body.appendChild(wrap);
// Частицы — FIX: сохраняем RAF ID чтобы остановить при входе
const cv = document.getElementById('brv5-canvas');
const ctx = cv.getContext('2d');
cv.width = innerWidth; cv.height = innerHeight;
const pts = Array.from({length:80}, () => ({
x:Math.random()*innerWidth, y:Math.random()*innerHeight,
r:Math.random()*1.8+.3, vx:(Math.random()-.5)*.4, vy:(Math.random()-.5)*.4,
}));
function animPts() {
ctx.clearRect(0,0,cv.width,cv.height);
pts.forEach(p => {
p.x+=p.vx; p.y+=p.vy;
if(p.x<0) p.x=cv.width; if(p.x>cv.width) p.x=0;
if(p.y<0) p.y=cv.height; if(p.y>cv.height) p.y=0;
ctx.fillStyle='rgba(176,102,255,.28)';
ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,Math.PI*2); ctx.fill();
});
_loginRaf = requestAnimationFrame(animPts);
}
_loginRaf = requestAnimationFrame(animPts);
const go = () => {
const v = document.getElementById('brv5-nick-i').value.trim();
if (!v) return;
nickname = v; S.set('brp_nick',v); users++; S.set('brp_users',users);
addLog('Вход', v);
// FIX: останавливаем анимацию частиц
if (_loginRaf) { cancelAnimationFrame(_loginRaf); _loginRaf = null; }
wrap.style.transition = 'opacity .5s'; wrap.style.opacity = '0';
setTimeout(() => { wrap.remove(); buildFab(); buildPanel(); startTracking(); }, 500);
};
document.getElementById('brv5-go').onclick = go;
document.getElementById('brv5-nick-i').onkeydown = e => { if(e.key==='Enter') go(); };
}
/* ══════════════════════════════════════
FAB
══════════════════════════════════════ */
function buildFab() {
const fab = document.createElement('button');
fab.id = 'brv5-fab';
fab.innerHTML = '<span>⚙️</span><span class="brv5-fab-badge" id="brv5-fbadge">0</span>';
fab.onclick = () => {
panelOpen = !panelOpen;
document.getElementById('brv5-panel').classList.toggle('open', panelOpen);
if (panelOpen) render(activeTab);
};
document.body.appendChild(fab);
}
function setBadge(n) {
unread = n;
const b = document.getElementById('brv5-fbadge'), tb = document.getElementById('brv5-tbadge');
if (b) { b.textContent = n; b.classList.toggle('show', n>0); }
if (tb) { tb.textContent = n; tb.classList.toggle('show', n>0); }
}
/* ══════════════════════════════════════
ТРЕКИНГ
══════════════════════════════════════ */
function getPageInfo() {
const topic = document.querySelector('.p-title-value')?.textContent?.trim() || '';
const crumbs = [...document.querySelectorAll('.p-breadcrumbs a,.breadBoxMinor a')];
const section = crumbs.length ? crumbs[crumbs.length-2]?.textContent?.trim() : '';
return {url:location.href, topic, section};
}
function updateTracker() {
const el = document.getElementById('brv5-tracker-text');
if (!el) return;
const i = getPageInfo();
const parts = [];
if (i.section) parts.push(`📂 ${i.section}`);
if (i.topic) parts.push(`📌 ${i.topic.substring(0,35)}`);
el.innerHTML = parts.length ? parts.map(p=>`<span>${p}</span>`).join(' · ') : '📍 Форум';
}
function startTracking() {
detectForumUser();
updateTracker();
let lastUrl = location.href;
// FIX: сохраняем ID интервала, чтобы не плодить несколько
if (_urlTimer) clearInterval(_urlTimer);
_urlTimer = setInterval(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
detectForumUser();
updateTracker();
}
}, 2000);
// Polling и ping — только если сервер настроен
if (serverOk()) {
if (_pollTimer) clearInterval(_pollTimer);
_pollTimer = setInterval(() => pollMessages(msgs => {
if (!msgs.length) return;
setBadge(unread + msgs.length);
msgs.forEach(m => toast(`💬 ${m.from||'Поддержка'}: ${m.text.substring(0,40)}`, 5000));
if (panelOpen && activeTab==='chat') render('chat');
}), 5000);
const info = getPageInfo();
api('/ping',{nick:nickname,...info,author:forumUser.name},()=>{});
if (_pingTimer) clearInterval(_pingTimer);
_pingTimer = setInterval(() => {
const i = getPageInfo();
api('/ping',{nick:nickname,...i,author:forumUser.name},()=>{});
}, 30000);
}
}
/* ══════════════════════════════════════
ПАНЕЛЬ
══════════════════════════════════════ */
const TABS = [
{id:'tpl', icon:'fa-list', label:'Шаблоны'},
{id:'chat', icon:'fa-comments', label:'Поддержка'},
{id:'log', icon:'fa-chart-bar', label:'Логи'},
{id:'con', icon:'fa-paper-plane', label:'Связь'},
{id:'game', icon:'fa-gamepad', label:'Игры'},
];
function buildPanel() {
const p = document.createElement('div');
p.id = 'brv5-panel';
p.innerHTML = `
<div class="brv5-hdr">
<div class="brv5-hdr-l">
<div class="brv5-dot"></div>
<span class="brv5-logo-sm">BR ADMIN</span>
<span class="brv5-nick-sm">👤 ${nickname}</span>
</div>
<button class="brv5-x" id="brv5-x">✕</button>
</div>
<div class="brv5-tracker"><span id="brv5-tracker-text">📍 Загрузка...</span></div>
<nav class="brv5-nav">
${TABS.map(t=>`<button class="brv5-tab${t.id===activeTab?' on':''}" data-t="${t.id}">
<i class="fa-solid ${t.icon}"></i>${t.label}
${t.id==='chat'?'<span class="brv5-tab-b" id="brv5-tbadge"></span>':''}
</button>`).join('')}
</nav>
<div class="brv5-pages-wrap">
<div class="brv5-page-dots" id="brv5-pdots">
<div class="brv5-dot-ind on" data-p="0"></div>
<div class="brv5-dot-ind" data-p="1"></div>
</div>
<div class="brv5-pages" id="brv5-pages">
<div class="brv5-page"><div class="brv5-body" id="brv5-body"></div></div>
<div class="brv5-page"><div class="brv5-body" id="brv5-body2"></div></div>
</div>
</div>
`;
document.body.appendChild(p);
document.getElementById('brv5-x').onclick = () => { panelOpen=false; p.classList.remove('open'); };
p.querySelectorAll('.brv5-tab').forEach(btn => {
btn.onclick = () => {
// FIX: при смене таба останавливаем змейку если играли
snakeCleanup();
activeTab = btn.dataset.t;
p.querySelectorAll('.brv5-tab').forEach(b => b.classList.remove('on'));
btn.classList.add('on');
if (activeTab==='chat') setBadge(0);
goPage(0);
render(activeTab);
};
});
p.querySelectorAll('.brv5-dot-ind').forEach(d => {
d.onclick = () => goPage(parseInt(d.dataset.p));
});
// FIX: свайп — добавляем проверку вертикального vs горизонтального движения
// чтобы вертикальный скролл внутри панели не ломался
let sx=0, sy=0, dragging=false;
const pages = document.getElementById('brv5-pages');
pages.addEventListener('touchstart', e => {
sx = e.touches[0].clientX;
sy = e.touches[0].clientY;
dragging = true;
}, {passive:true});
pages.addEventListener('touchend', e => {
if (!dragging) return; dragging = false;
const dx = sx - e.changedTouches[0].clientX;
const dy = sy - e.changedTouches[0].clientY;
// только горизонтальный свайп (не скролл)
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 40) {
goPage(dx > 0 ? 1 : 0);
}
}, {passive:true});
render(activeTab);
render2();
updateTracker();
}
function goPage(n) {
currentPage = n;
const pages = document.getElementById('brv5-pages');
if (pages) pages.style.transform = `translateX(-${n*100}%)`;
document.querySelectorAll('.brv5-dot-ind').forEach((d,i) => d.classList.toggle('on',i===n));
}
/* ══════════════════════════════════════
РЕНДЕР СТРАНИЦА 1 (табы)
══════════════════════════════════════ */
function render(tab) {
const body = document.getElementById('brv5-body');
if (!body) return;
({tpl:rTpl, chat:rChat, log:rLog, con:rCon, game:rGame}[tab] || rTpl)(body);
}
/* ── ШАБЛОНЫ ── */
function rTpl(body) {
// FIX: обновляем forumUser перед рендером чтобы имя было актуальным
detectForumUser();
body.innerHTML = `
<p class="brv5-sec">Шаблоны — клик применит и отправит</p>
<div style="font-size:11px;color:var(--mut);margin-bottom:10px">
👤 Автор темы: <b style="color:var(--pril)">${forumUser.name||'не определён'}</b>
</div>
<div id="brv5-tlist"></div>
`;
drawTpls();
}
function drawTpls() {
const el = document.getElementById('brv5-tlist');
if (!el) return;
el.innerHTML = '';
if (!templates.length) {
el.innerHTML = '<p style="color:var(--mut);font-size:12px">Нет шаблонов. Добавьте на второй странице (свайп ←)</p>';
return;
}
templates.forEach(tpl => {
const p = pfx(tpl.prefix);
const d = document.createElement('div');
d.className = 'brv5-tpl';
d.innerHTML = `
<div class="brv5-tpl-top">
<span class="brv5-tpl-name">${tpl.name}</span>
<span class="brv5-bdg" style="color:${p.color}">${p.name}</span>
${tpl.autopin ? '<span style="color:#f0c040;font-size:10px">📌</span>' : ''}
${tpl.img ? '<span style="font-size:10px;color:var(--pril)">🖼</span>' : ''}
</div>
<div class="brv5-prev">${tpl.content.replace(/\[.*?\]/g,'').trim().substring(0,80)}…</div>
<div class="brv5-btns">
<button class="btn5" data-ap="${tpl.id}">🚀 Применить</button>
<button class="btn5g" data-cp="${tpl.id}">📋 Копировать</button>
<button class="btn5r" data-dl="${tpl.id}">🗑</button>
</div>
`;
el.appendChild(d);
});
el.querySelectorAll('[data-ap]').forEach(b => {
b.onclick = () => { const t=templates.find(x=>x.id==b.dataset.ap); if(t) applyTemplate(t); };
});
el.querySelectorAll('[data-cp]').forEach(b => {
b.onclick = () => {
const t = templates.find(x=>x.id==b.dataset.cp); if(!t) return;
navigator.clipboard.writeText(applyMacros(t.content)).catch(()=>{});
addLog('Копирование', t.name);
b.textContent='✅'; setTimeout(()=>{ b.textContent='📋 Копировать'; },1500);
};
});
el.querySelectorAll('[data-dl]').forEach(b => {
b.onclick = () => {
const t = templates.find(x=>x.id==b.dataset.dl);
if (!confirm(`Удалить "${t?.name}"?`)) return;
addLog('Удалён шаблон', t?.name||'');
templates = templates.filter(x=>x.id!=b.dataset.dl);
saveTpls(); drawTpls();
};
});
}
/* ── ЧАТ ── */
function rChat(body) {
const noServer = !serverOk();
body.innerHTML = `
<p class="brv5-sec">Поддержка — чат с разработчиком</p>
${noServer ? '<div style="padding:8px 12px;background:rgba(255,79,79,.1);border:1px solid rgba(255,79,79,.3);border-radius:8px;font-size:11px;color:#ff7777;margin-bottom:8px">⚠️ Сервер не настроен. Укажите SERVER в конфиге скрипта.</div>' : ''}
<div class="brv5-chat-hint">💬 Напишите вопрос — разработчик или поддержка ответят здесь</div>
<div class="brv5-msgs" id="brv5-msgs"><div style="text-align:center;color:var(--mut);font-size:12px;padding:16px">Загрузка...</div></div>
<div class="brv5-chat-row">
<textarea class="brv5-chat-ta" id="brv5-cinp" placeholder="Ваш вопрос или жалоба..." ${noServer?'disabled':''}></textarea>
<button class="brv5-chat-send" id="brv5-csend" ${noServer?'disabled':''}><i class="fa-solid fa-paper-plane"></i></button>
</div>
`;
loadHistory(msgs => drawMsgs(msgs));
if (noServer) return;
document.getElementById('brv5-csend').onclick = () => {
const inp=document.getElementById('brv5-cinp'), text=inp.value.trim();
if (!text) return; inp.value=''; inp.disabled=true;
sendToSupport(text, (e,d) => {
inp.disabled = false;
if (e||!d?.ok) { toast('❌ Сервер недоступен'); return; }
addLog('Сообщение в поддержку', text.substring(0,40));
loadHistory(msgs => drawMsgs(msgs));
toast('✅ Отправлено');
});
};
document.getElementById('brv5-cinp').onkeydown = e => {
if (e.key==='Enter'&&!e.shiftKey) { e.preventDefault(); document.getElementById('brv5-csend').click(); }
};
}
function drawMsgs(msgs) {
const el = document.getElementById('brv5-msgs'); if (!el) return;
if (!msgs.length) {
el.innerHTML = '<div style="text-align:center;color:var(--mut);font-size:12px;padding:16px">Сообщений нет. Напишите первым!</div>';
return;
}
el.innerHTML = '';
msgs.forEach(m => {
const d = document.createElement('div');
d.className = `brv5-msg ${m.role==='support'?'support':'user'}`;
d.innerHTML = `${m.role==='support'?`<div class="brv5-msg-from">🛡 ${m.from||'Поддержка'}</div>`:''}${m.text}<div class="brv5-msg-time">${m.time||''}</div>`;
el.appendChild(d);
});
el.scrollTop = el.scrollHeight;
}
/* ── ЛОГИ ── */
function rLog(body) {
body.innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
<p class="brv5-sec" style="margin:0">История (${logs.length})</p>
<button class="btn5r" id="cl-l" style="padding:5px 10px;font-size:11px">🗑</button>
</div><div id="brv5-ll"></div>
`;
document.getElementById('cl-l').onclick = () => {
if (!confirm('Очистить?')) return;
logs = []; saveLogs(); rLog(body);
};
const ll = document.getElementById('brv5-ll');
if (!logs.length) { ll.innerHTML = '<p style="color:var(--mut);font-size:12px">Логов нет.</p>'; return; }
[...logs].reverse().forEach(l => {
ll.innerHTML += `<div class="brv5-log"><span class="brv5-log-t">${l.t}</span><b>${l.a}</b><br>${l.u} — ${l.d}</div>`;
});
}
/* ── СВЯЗЬ ── */
function rCon(body) {
body.innerHTML = `
<p class="brv5-sec">Связь с разработчиком</p>
<a class="brv5-con" href="https://vk.ru/brunoverona" target="_blank">
<span class="brv5-con-i">💙</span>
<div><div class="brv5-con-n">ВКонтакте разработчика</div><div class="brv5-con-s">vk.ru/brunoverona</div></div>
</a>
<a class="brv5-con" href="https://vk.ru/club237051164" target="_blank">
<span class="brv5-con-i">🏢</span>
<div><div class="brv5-con-n">Группа разработчиков</div><div class="brv5-con-s">vk.ru/club237051164</div></div>
</a>
<div style="padding:12px;background:rgba(138,43,226,.07);border:1px solid var(--brd);border-radius:10px;margin-top:12px">
<p style="font-size:11px;color:var(--mut);margin:0;line-height:1.7">
<b style="color:var(--pril)">BR Admin Panel Pro v5.1</b><br>
Автоник · Свайп-страницы · Загрузка фото · Змейка · Поддержка
</p>
</div>
`;
}
/* ── ИГРЫ ── */
function rGame(body) {
body.innerHTML = `
<p class="brv5-sec">Запуск и игры</p>
<div class="brv5-game-card">
<div class="brv5-game-ico">🎮</div>
<div class="brv5-game-title">BLACK RUSSIA</div>
<div class="brv5-game-sub">Нажми чтобы запустить игру<br>(требуется установленный лаунчер)</div>
<button class="btn5" id="br-launch">🚀 Запустить Black Russia</button>
</div>
<div class="brv5-game-card">
<div class="brv5-game-ico">🐍</div>
<div class="brv5-game-title">ЗМЕЙКА</div>
<div class="brv5-game-sub">Мини-игра для отдыха</div>
<button class="btn5g" id="br-snake">🎮 Играть в змейку</button>
</div>
`;
document.getElementById('br-launch').onclick = () => {
toast('🚀 Запускаем Black Russia...');
window.location.href = CR_LINK;
};
document.getElementById('br-snake').onclick = () => startSnake(body);
}
/* ── ОЧИСТКА ЗМЕЙКИ ── */
function snakeCleanup() {
if (_snakeTimer) { clearInterval(_snakeTimer); _snakeTimer = null; }
if (_snakeKeyHandler) { document.removeEventListener('keydown', _snakeKeyHandler); _snakeKeyHandler = null; }
}
/* ── ЗМЕЙКА ── */
function startSnake(body) {
snakeCleanup(); // FIX: останавливаем прошлый сеанс если есть
body.innerHTML = `
<p class="brv5-sec">🐍 Змейка</p>
<div class="brv5-snake-score" id="sn-score">Счёт: 0</div>
<canvas id="brv5-snake-canvas" width="240" height="240"></canvas>
<div class="brv5-snake-controls">
<div class="brv5-dpad">
<div></div>
<button id="sn-up">▲</button>
<div></div>
<button id="sn-left">◀</button>
<button id="sn-down">▼</button>
<button id="sn-right">▶</button>
</div>
<button class="btn5g" id="sn-back" style="margin-top:8px">← Назад</button>
</div>
`;
const cv = document.getElementById('brv5-snake-canvas');
const ctx = cv.getContext('2d');
const SZ=16, W=15, H=15;
let snake=[{x:7,y:7}], dir={x:1,y:0}, food=randFood(), score=0, alive=true;
function randFood() { return {x:Math.floor(Math.random()*W), y:Math.floor(Math.random()*H)}; }
function draw() {
ctx.fillStyle='#0a0614'; ctx.fillRect(0,0,240,240);
ctx.fillStyle='#ff4f4f'; ctx.beginPath(); ctx.arc(food.x*SZ+8,food.y*SZ+8,6,0,Math.PI*2); ctx.fill();
snake.forEach((s,i) => {
ctx.fillStyle = i===0 ? '#b066ff' : '#7a35c0';
ctx.fillRect(s.x*SZ+1, s.y*SZ+1, SZ-2, SZ-2);
});
if (!alive) {
ctx.fillStyle='rgba(0,0,0,.6)'; ctx.fillRect(0,0,240,240);
ctx.fillStyle='#fff'; ctx.font='bold 18px Rajdhani,sans-serif'; ctx.textAlign='center';
ctx.fillText('GAME OVER',120,110);
ctx.font='13px Inter,sans-serif'; ctx.fillText('Счёт: '+score,120,135);
ctx.font='11px Inter,sans-serif'; ctx.fillStyle='#666'; ctx.fillText('Тап для рестарта',120,158);
}
}
function step() {
if (!alive) return;
const head = {x:snake[0].x+dir.x, y:snake[0].y+dir.y};
if (head.x<0||head.x>=W||head.y<0||head.y>=H||snake.some(s=>s.x===head.x&&s.y===head.y)) {
alive=false; draw(); return;
}
snake.unshift(head);
if (head.x===food.x && head.y===food.y) {
score++;
const sc = document.getElementById('sn-score');
if (sc) sc.textContent = 'Счёт: '+score;
food = randFood();
} else {
snake.pop();
}
draw();
}
_snakeTimer = setInterval(step, 150);
draw();
// FIX: правильная логика setDir — блокируем разворот на 180°
// Оригинал: Math.abs(dx)===Math.abs(dir.x) — это блокировало поворот на 90° тоже!
const setDir = (dx, dy) => {
// Нельзя разворачиваться в обратную сторону
if (dx !== 0 && dx === -dir.x) return;
if (dy !== 0 && dy === -dir.y) return;
dir = {x:dx, y:dy};
};
document.getElementById('sn-up').onclick = () => setDir(0,-1);
document.getElementById('sn-down').onclick = () => setDir(0,1);
document.getElementById('sn-left').onclick = () => setDir(-1,0);
document.getElementById('sn-right').onclick = () => setDir(1,0);
_snakeKeyHandler = e => {
if (e.key==='ArrowUp') setDir(0,-1);
if (e.key==='ArrowDown') setDir(0,1);
if (e.key==='ArrowLeft') setDir(-1,0);
if (e.key==='ArrowRight') setDir(1,0);
};
document.addEventListener('keydown', _snakeKeyHandler);
cv.addEventListener('click', () => {
if (!alive) {
alive=true; snake=[{x:7,y:7}]; dir={x:1,y:0};
food=randFood(); score=0;
const sc=document.getElementById('sn-score');
if(sc) sc.textContent='Счёт: 0';
}
});
document.getElementById('sn-back').onclick = () => { snakeCleanup(); rGame(body); };
}
/* ══════════════════════════════════════
РЕНДЕР СТРАНИЦА 2 — редактор + фото
══════════════════════════════════════ */
function render2() {
const body = document.getElementById('brv5-body2');
if (!body) return;
const COLORS = ['#FF00FF','#00FF00','#FF4444','#2196f3','#ec7c26','#f0c040','#ffffff','#aaaaaa','#00bcd4','#9c27b0'];
const BB = [
['🌸 Привет', '[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{greeting}}, уважаемый(ая) игрок [USER={{uid}}]{{username}}[/USER][/ICODE].[/CENTER][/I][/SIZE][/FONT][/COLOR]\n\n'],
['✅ Одобрено', '[B][CENTER][FONT=times new roman][COLOR=#00FF00][ICODE]Одобрено.[/ICODE][/COLOR][/CENTER][/FONT][/B]'],
['❌ Отказано', '[B][CENTER][FONT=times new roman][COLOR=#FF4444][ICODE]Отказано.[/ICODE][/COLOR][/CENTER][/FONT][/B]'],
['⏳ На рассм.', '[B][CENTER][FONT=times new roman][COLOR=#ec7c26][ICODE]На рассмотрении.[/ICODE][/COLOR][/CENTER][/FONT][/B]'],
['🔒 Закрыто', '[B][CENTER][FONT=times new roman][COLOR=#aaaaaa][ICODE]Закрыто.[/ICODE][/COLOR][/CENTER][/FONT][/B]'],
['🖼 Фото', '[CENTER][url=https://postimages.org/][img]https://i.postimg.cc/cCG97p5p/Pics-Art-07-12-03-23-18-1.png[/img][/url][/CENTER]\n\n'],
];
const pfxOpts = PREFIXES.map(p=>`<option value="${p.id}">${p.name}</option>`).join('');
body.innerHTML = `
<p class="brv5-sec">← Свайп для возврата</p>
<div class="brv5-hr"></div>
<p class="brv5-sec">Новый шаблон</p>
<div class="brv5-fg"><label>Название</label><input class="brv5-inp" id="e-name" placeholder="Название шаблона" /></div>
<div class="brv5-fg"><label>Префикс</label><select class="brv5-sel" id="e-pfx">${pfxOpts}</select></div>
<div class="brv5-fg"><label style="display:flex;align-items:center;gap:6px;cursor:pointer"><input type="checkbox" id="e-pin" style="accent-color:var(--pri)"> 📌 Автозакреп</label></div>
<div class="brv5-fg">
<label>Цвет текста</label>
<div class="brv5-color-row" id="e-colors">
${COLORS.map(c=>`<div class="brv5-color-btn" style="background:${c}" data-c="${c}" title="${c}"></div>`).join('')}
<input type="color" id="e-custom-color" style="width:24px;height:24px;border-radius:50%;border:none;background:none;cursor:pointer;padding:0" title="Свой цвет">
</div>
</div>
<div class="brv5-fg">
<label>BB-вставки</label>
<div class="brv5-bb-chips">
${BB.map(([l],i)=>`<button class="brv5-chip" data-bb="${i}">${l}</button>`).join('')}
</div>
</div>
<div class="brv5-fg">
<label>Текст шаблона ({{greeting}}, {{username}}, {{uid}})</label>
<textarea class="brv5-ta" id="e-text" placeholder="[COLOR=#FF00FF]{{greeting}}, {{username}}[/COLOR] Ваш текст..."></textarea>
</div>
<button class="btn5" id="e-save" style="margin-bottom:14px">💾 Сохранить шаблон</button>
<div class="brv5-hr"></div>
<p class="brv5-sec">Загрузка фото (imgbb)</p>
<div class="brv5-fg"><label>imgbb API ключ</label><input class="brv5-inp" id="e-imgkey" value="${S.get('brp_imgbb','')}" placeholder="Ключ с imgbb.com → API" /></div>
<button class="btn5g" id="e-savekey" style="margin-bottom:10px">💾 Сохранить ключ</button>
<div class="brv5-upload-zone" id="e-drop">
<div class="brv5-upload-ico">📷</div>
<div class="brv5-upload-hint">Нажми или перетащи фото<br>Получишь ссылку для форума</div>
<input type="file" id="e-file" accept="image/*" style="display:none">
</div>
<div class="brv5-photo-result" id="e-result">
<div class="brv5-photo-url" id="e-url"></div>
<div style="display:flex;gap:6px;flex-wrap:wrap">
<button class="btn5" id="e-copy-url">📋 Копировать ссылку</button>
<button class="btn5g" id="e-copy-bb">🖼 BB-код [img]</button>
<button class="btn5grn" id="e-ins-ta">↑ Вставить в шаблон</button>
</div>
</div>
`;
// Цвета
let selColor = COLORS[0];
body.querySelectorAll('.brv5-color-btn').forEach(b => {
b.onclick = () => {
body.querySelectorAll('.brv5-color-btn').forEach(x=>x.classList.remove('on'));
b.classList.add('on'); selColor = b.dataset.c;
const ta = document.getElementById('e-text');
const pos=ta.selectionStart, end=ta.selectionEnd, sel=ta.value.substring(pos,end);
if (sel) ta.value = ta.value.substring(0,pos)+`[COLOR=${selColor}]${sel}[/COLOR]`+ta.value.substring(end);
else { ta.value += `[COLOR=${selColor}][/COLOR]`; ta.selectionStart=ta.selectionEnd=ta.value.length-8; }
ta.focus();
};
});
document.getElementById('e-custom-color').oninput = e => { selColor = e.target.value; };
// BB-чипы
body.querySelectorAll('[data-bb]').forEach(b => {
b.onclick = () => { const ta=document.getElementById('e-text'); ta.value+=BB[parseInt(b.dataset.bb)][1]; ta.focus(); };
});
// Сохранение шаблона
document.getElementById('e-save').onclick = () => {
const n = document.getElementById('e-name').value.trim();
const t = document.getElementById('e-text').value.trim();
const p = parseInt(document.getElementById('e-pfx').value);
const pin = document.getElementById('e-pin').checked;
if (!n||!t) { toast('⚠️ Заполните название и текст'); return; }
templates.push({id:Date.now(), name:n, prefix:p, autopin:pin, content:t});
saveTpls(); addLog('Шаблон создан', n); toast(`✅ "${n}" сохранён!`);
document.getElementById('e-name').value = '';
document.getElementById('e-text').value = '';
};
// Ключ imgbb
document.getElementById('e-savekey').onclick = () => {
S.set('brp_imgbb', document.getElementById('e-imgkey').value.trim());
toast('✅ Ключ сохранён');
};
// Загрузка фото
const drop = document.getElementById('e-drop');
drop.onclick = () => document.getElementById('e-file').click();
drop.ondragover = e => { e.preventDefault(); drop.classList.add('drag'); };
drop.ondragleave = () => drop.classList.remove('drag');
drop.ondrop = e => {
e.preventDefault(); drop.classList.remove('drag');
const f = e.dataTransfer.files[0]; if (f) handlePhoto(f);
};
document.getElementById('e-file').onchange = e => { if (e.target.files[0]) handlePhoto(e.target.files[0]); };
let lastUrl = '';
function handlePhoto(file) {
toast('📤 Загружаем фото...');
drop.querySelector('.brv5-upload-hint').textContent = '⏳ Загрузка...';
uploadPhoto(file, (url, err) => {
drop.querySelector('.brv5-upload-hint').textContent = 'Нажми или перетащи фото';
if (err) { toast('❌ '+err); return; }
lastUrl = url;
document.getElementById('e-url').textContent = url;
document.getElementById('e-result').style.display = 'block';
toast('✅ Фото загружено!');
});
}
document.getElementById('e-copy-url').onclick = () => { navigator.clipboard.writeText(lastUrl).catch(()=>{}); toast('✅ Ссылка скопирована'); };
document.getElementById('e-copy-bb').onclick = () => { navigator.clipboard.writeText(`[img]${lastUrl}[/img]`).catch(()=>{}); toast('✅ BB-код скопирован'); };
document.getElementById('e-ins-ta').onclick = () => {
const ta = document.getElementById('e-text');
ta.value += `[CENTER][img]${lastUrl}[/img][/CENTER]\n\n`;
ta.focus(); toast('✅ Вставлено в шаблон');
};
}
/* ══════════════════════════════════════
СТАРТ
══════════════════════════════════════ */
detectForumUser();
if (!nickname) {
buildLogin();
} else {
buildFab(); buildPanel(); startTracking();
}
})();