// ==UserScript==
// @name LztStreamerModeRework
// @namespace https://github.com/nellimonix
// @version 1.2.4
// @description Режим стримера для Lolzteam. Улучшенная и модифицированная версия!
// @author llimonix
// @match https://zelenka.guru/*
// @match https://lolz.live/*
// @match https://lzt.market/*
// @icon https://cdn-icons-png.flaticon.com/512/18429/18429788.png
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @run-at document-start
// @license MIT
// ==/UserScript==
// Проверяем наши настройки
let StreamerModeSettings = GM_getValue('lzt_streamer_mode_settings', null);
const default_settings = {
'main_page': {
'title': 'Основное',
'description': 'LZT Streamer Mode',
'enabled': true
},
'banwords_page': {
'banwords': [],
'title': 'Запрещенные слова',
'description': 'Блокировать запрещенные слова (укажите их через запятую)',
'enabled': true
},
'selector_page': {
'title': 'Настройка режима стримера',
'general': {
'title': 'Общие настройки',
'balance': {
'selectors': [
'div#AccountMenu span.left',
'#MarketMoneyTransferForm .marketRefillBalance--Row:nth-of-type(2) .bigTextHeading',
'#MarketMoneyTransferForm div.MarketPopularTransfers',
'div.marketUserPanel div.balance-value',
'div.marketUserPanel a.holdPayment',
'div.marketUserPanel span.balanceNumber'
],
'disabled_selectors': ['span#NavigationAccountBalance'],
'ignored_selectors': [],
'description': 'Скрывать все элементы, отображающие баланс пользователя (меню, переводы и т.д.)',
'enabled': true
},
'messages': {
'selectors': [
'#ConversationListItems',
'div.ng-notification div.body',
],
'disabled_selectors': [],
'ignored_selectors': [],
'description': 'Скрывать личные сообщения, список диалогов и уведомления о новых сообщениях',
'enabled': true
},
'alerts': {
'selectors': [
'div#AlertPanels',
'div.account_alerts .alertGroup ol',
'div.liveAlert',
'div.imDialog',
],
'disabled_selectors': [],
'ignored_selectors': [],
'description': 'Скрывать все уведомления, включая всплывающие',
'enabled': true
},
'personal_details': {
'selectors': [
'div.account_security input[name="email"]'
],
'disabled_selectors': [],
'ignored_selectors': [],
'description': 'Скрывать элементы в настройках профиля',
'enabled': true
}
},
'forum': {},
'market': {
'title': 'Настройки маркета',
'viewed': {
'selectors': [
'div.marketViewedItems'
],
'disabled_selectors': [],
'ignored_selectors': ['.marketMyPayments'],
'description': 'Скрывать блок недавно просмотренных товаров и аккаунтов',
'enabled': true
},
'payments': {
'selectors': [
'div.marketMyPayments div.wrapper'
],
'disabled_selectors': [],
'ignored_selectors': ['.marketViewedItems'],
'description': 'Скрывать блок последних действий и историю операций',
'enabled': true
},
'orders': {
'selectors': [],
'disabled_selectors': [],
'ignored_selectors': [],
'description': 'Скрывать список купленных товаров и аккаунтов',
'enabled': true
},
'login_data': {
'selectors': [
'div.marketItemView--loginData',
'div.marketItemView--loginData--box',
],
'disabled_selectors': [],
'ignored_selectors': [
'.login_details_for_arbitrators',
'.make_claim_seller_block',
'.recommendation_change_password'
],
'description': 'Скрывать данные для входа в аккаунт: логин, пароль, email и т.п.',
'enabled': true
},
'payment_stats': {
'selectors': [
'div.paymentStats div.stats'
],
'disabled_selectors': [],
'ignored_selectors': [],
'description': 'Скрывать сумму платежей за все время / указанный срок',
'enabled': true
},
'need_payment': {
'selectors': [
'div.need-payment div.need-payment-item'
],
'disabled_selectors': [],
'ignored_selectors': [],
'description': 'Скрывать платежи ожидающие оплаты',
'enabled': true
}
},
'staff': {
'title': 'Настройки команды форума',
'warning': 'Может затронуть некоторые обычные функции, если они используют схожие элементы',
'balance_users': {
'selectors': [],
'disabled_selectors': [
'div.userContentLinks a[href$="/payments"]',
'div.userContentLinks a[href$="/donate-to-forum"]',
],
'ignored_selectors': [],
'description': 'Скрывать все элементы, отображающие баланс пользователей',
'enabled': false
},
'account_menu': {
'selectors': [],
'disabled_selectors': [
'a[href="account/email-blacklist"]',
'a[href="account/spam-words"]',
],
'ignored_selectors': [],
'description': 'Скрывать кнопки в меню аккаунта',
'enabled': false
},
'account_links': {
'selectors': [],
'disabled_selectors': [
'div.account-links .modLink',
],
'ignored_selectors': [],
'description': 'Скрывать иконки премодерации, репортов и службы поддержки',
'enabled': false
},
'profile_panel': {
'selectors': [],
'disabled_selectors': [
'div.banLogs div#logs-buttons-f',
'ul.mainTabs a[href$="#staff-notes"]',
],
'ignored_selectors': [],
'description': 'Скрывать кнопки в панели профиля (записи команды форума, журнал изменений)',
'enabled': false
},
'profile_info': {
'selectors': [],
'disabled_selectors': [
'div.profilePage a[href$="/telegram-info"]',
'div.profilePage a[href$="/eog-info"]',
'div.profilePage div.profile_info_row:nth-of-type(3)'
],
'ignored_selectors': [],
'description': 'Скрывать дополнительные функции, связанные с персональной информацией',
'enabled': false
},
'profile_id': {
'selectors': [],
'disabled_selectors': [
'div.profilePage div.profile_info_row:nth-of-type(2)'
],
'ignored_selectors': [],
'description': 'Скрывать ID пользователя в профиле',
'enabled': false
},
'personal_details': {
'selectors': [],
'disabled_selectors': [
'div.navigationSideBar a[href="account/moderator-settings"]',
'form.personalDetailsForm dl.customFieldEditscamURL',
'form.personalDetailsForm dl.customFieldEditlztUnbanAmount',
'form.personalDetailsForm dl.customFieldEditlztLikesZeroing',
'form.personalDetailsForm dl.customFieldEditlztAwardUserTrophy',
'form.personalDetailsForm dl.customFieldEditlztLikesIncreasing',
'form.personalDetailsForm dl.customFieldEditlztSympathyZeroing',
'form.personalDetailsForm dl.customFieldEditlztSympathyZeroing',
'form.personalDetailsForm dl.customFieldEditlztSympathyIncreasing',
],
'ignored_selectors': [],
'description': 'Скрывать элементы в настройках профиля',
'enabled': false
},
'thread_checkbox': {
'selectors': [],
'disabled_selectors': [
'body label[for*="inlineModCheck-thread"]',
'.InlineModCheck',
],
'ignored_selectors': [],
'description': 'Скрывать чекбоксы для выделения тем',
'enabled': false
},
'thread_visibility': {
'selectors': [],
'disabled_selectors': [
'a[href*="set-deleted-content-visibility"]',
'.messageList .message.deleted',
'.messageSimpleList .messageSimple.deleted',
'div.discussionListItem.moderated',
'div.discussionListItem.deleted'
],
'ignored_selectors': [],
'description': 'Скрывать удаленные и премодерациооные темы',
'enabled': false
},
'shared_ips': {
'selectors': [],
'disabled_selectors': [
'div.ipMatches a[href$="/#gauid"]',
'div.ipMatches a[href$="/#evercookie"]',
'div.ipMatches a[href$="/#fingerprints"]',
'div.ipMatches a[href$="/#multiple"]',
'div.logInfo li.ipLog span.info-separattor',
'div.logInfo li.ipLog span.muted',
],
'ignored_selectors': [],
'description': 'Скрывать дополнительную информацию в общих IP',
'enabled': false
},
'profile_dottes': {
'selectors': [],
'disabled_selectors': [
//'div.MenuContainer a[href$="/edit"]',
'div.MenuContainer a[href^="spam-cleaner"]',
'div.MenuContainer a[href^="support-tickets/list"]',
'div.MenuContainer a[href^="members/quick-ban"]',
'div.MenuContainer a[href^="members/quick-unban"]',
'div.MenuContainer a[href$="/change-logs"]',
],
'ignored_selectors': [],
'description': 'Скрывать функции управления над пользователем',
'enabled': false
},
'comments_control': {
'selectors': [],
'disabled_selectors': [
'div.MenuContainer a[href*="/warn"]',
'div.MenuContainer a[href*="/unapprove"]',
'div.MenuContainer a[href$="/history"]',
],
'ignored_selectors': [],
'description': 'Скрывать кнопки для управления комментариями',
'enabled': false
},
'thread_control': {
'selectors': [],
'disabled_selectors': [
'div.MenuContainer a[href$="/reply-bans"]',
'div.MenuContainer a[href$="/user-actions"]',
'div.MenuContainer a[href$="/show-posts"]',
'div.MenuContainer a[href$="/change-starter"]',
'div.MenuContainer a[href$="/reply-bans"]',
'div.MenuContainer a[href$="/undelete"]',
'div.MenuContainer form[action$="/quick-update"] li:nth-child(2)',
'div.MenuContainer #threadViewThreadCheck',
],
'ignored_selectors': [],
'description': 'Скрывать кнопки для управления темами',
'enabled': false
},
'arbitor_control': {
'selectors': [],
'disabled_selectors': [
'div.decideMenuWrapper',
'div.quickReply span.QuickUsernameInsert',
'div.pageContent div.staffClaimsStats',
],
'ignored_selectors': [],
'description': 'Скрывать кнопки управления арбитражем',
'enabled': false
},
'moderator_control': {
'selectors': [],
'disabled_selectors': [
'div.decideMenuWrapper',
'div.quickReply span.QuickThreadMove',
'div.pageContent div.staffClaimsStats',
],
'ignored_selectors': [],
'description': 'Скрывать кнопки управления жалобами, недочетами и предложениями',
'enabled': false
},
'quick_ban': {
'selectors': [],
'disabled_selectors': [
'div.profilePage a.siropuManageBan',
],
'ignored_selectors': [],
'description': 'Скрывать кнопки для заблокированного пользователя',
'enabled': false
},
'node_hide': {
'selectors': [],
'disabled_selectors': [
'.nodeList .list.node911',
'.nodeList .list.node993',
'.nodeList .list.node434',
],
'ignored_selectors': [],
'description': 'Скрывать разделы для команды форума',
'enabled': false
},
'market_control': {
'selectors': [],
'disabled_selectors': [
'.marketExtraSidebarMenu:has(a[href="games"])',
'div.marketItemView--sidebarUser a[href$="/orders"]',
'div.marketItemView--sidebarUser a[href$="/payments"]',
'div.marketItemView--sidebarUser span.Tooltip',
],
'ignored_selectors': [],
'description': 'Скрывать баланс и историю операций пользователя на маркете',
'enabled': false
},
'search_popup': {
'selectors': [],
'disabled_selectors': [
'div.search_results div.Popup',
],
'ignored_selectors': [],
'description': 'Скрывать инструменты модератора при поиске',
'enabled': false
}
}
}
}
if (!StreamerModeSettings) {
GM_setValue('lzt_streamer_mode_settings', default_settings);
StreamerModeSettings = default_settings;
}
function UpdateConfig() {
function mergeConfigsSmart(defaultConfig, userConfig, preserveFields = ['banwords', 'enabled']) {
const merged = structuredClone(defaultConfig);
function deepMerge(target, source) {
for (const key in source) {
if (source.hasOwnProperty(key)) {
const sourceValue = source[key];
if (sourceValue === undefined) continue;
if (sourceValue && typeof sourceValue === 'object' && !Array.isArray(sourceValue)) {
if (!target[key] || typeof target[key] !== 'object' || Array.isArray(target[key])) {
target[key] = {};
}
deepMerge(target[key], sourceValue);
} else {
// Сохраняем только указанные поля из пользовательского конфига
if (preserveFields.includes(key)) {
target[key] = sourceValue;
}
}
}
}
}
deepMerge(merged, userConfig);
return merged;
}
const updated = mergeConfigsSmart(default_settings, StreamerModeSettings);
GM_setValue('lzt_streamer_mode_settings', updated);
StreamerModeSettings = updated;
}
function collectSelectors(settings, mode = 'enabled') {
const collect = (obj) => {
if (!obj || typeof obj !== 'object' || !StreamerModeSettings.main_page.enabled) return [];
return Object.values(obj).flatMap(value => {
if (value.enabled && Array.isArray(value[mode === 'enabled' ? 'selectors' : 'disabled_selectors'])) {
return value[mode === 'enabled' ? 'selectors' : 'disabled_selectors'];
}
return collect(value);
});
};
return collect(settings);
}
// 1. Сразу добавляем CSS-блюр с плавными переходами
const selectors = collectSelectors(StreamerModeSettings, 'enabled');
const disabled_selectors = collectSelectors(StreamerModeSettings, 'disabled');
const path = window.location.pathname;
if (path === "/user/orders" || path === "/viewed") {
selectors.push('div.MarketItems')
} else if (path === "/streamer-mode") {
disabled_selectors.push(
'div.pageContent div.error-container',
'div.pageContent div.errorOverlay',
'div.pageContent div.titleBar',
'div.pageContent div.market-selectCategory-block'
);
}
const style = document.createElement('style');
style.textContent = `
${selectors.map(selector => `${selector}`).join(', ')} {filter: blur(6px);}
${disabled_selectors.map(selector => `${selector}`).join(', ')} {display: none !important;}
div.bbCodeHide blockquote.hideContainer {filter: ${StreamerModeSettings.main_page?.enabled ? 'blur(6px)' : 'none'}}
.member_tabs li a[href*="#change-logs"] {
font-size:0px
}
.member_tabs li a[href*="#change-logs"]:before {
content: "История блокировок"; font-size: 13px
}
.CensorStreamer {
filter: blur(6px) !important;
transition: filter 0.3s ease !important;
position: relative !important;
pointer-events: none !important;
}
.CensorStreamer:hover {
filter: blur(4px) !important;
}
.CensorStreamerClicked {
filter: none !important;
pointer-events: auto !important;
}
.CensorStreamer::before {
content: '' !important;
position: absolute !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
z-index: 9999 !important;
cursor: pointer !important;
pointer-events: auto !important;
background: transparent !important;
}
.CensorStreamerClicked::before {
display: none !important;
}
`;
document.head.appendChild(style);
// Обновляем конфиг если надо
UpdateConfig()
// 2. Ждём, пока появится XenForo
const win = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
Object.defineProperty(win, 'XenForo', {
configurable: true,
set(value) {
delete win.XenForo;
win.XenForo = value;
if (typeof win.XenForo.register === 'function') {
try {
initBlurFeature();
} catch (error) {
console.error('Ошибка при вызове initBlurFeature:', error);
}
}
}
});
// 3. Инициализация блюра через XenForo
function initBlurFeature() {
// Основная функция
XenForo.applyBlurCustom = function(element, ignoredSelectors = [], index = null) {
let el = element instanceof jQuery ? element[0] : element;
if (!el || typeof el.classList === 'undefined') return;
const isIgnored = (target) =>
ignoredSelectors.some((selector) => target.matches(selector));
if (isIgnored(el)) {
el.style.filter = "none"
return; // Не применяем блюр к игнорируемым элементам
}
// Проверяем, не обработан ли уже элемент
if (el.classList.contains("CensorStreamer") || el.classList.contains("CensorStreamerClicked")) return;
// Добавляем класс для блюра
el.classList.add("CensorStreamer");
// Обработчик клика для снятия блюра
const handleClick = (event) => {
// Проверяем, что клик был по overlay (::before элементу)
if (event.target === el) {
event.preventDefault();
event.stopPropagation();
// Снимаем блюр и делаем элемент кликабельным
el.classList.remove("CensorStreamer");
el.classList.add("CensorStreamerClicked");
// Удаляем обработчики, так как блюр снят
el.removeEventListener("click", handleClick);
}
};
// Добавляем обработчик клика
el.addEventListener("click", handleClick);
};
function registerSelectors(section) {
if (!section.enabled) return;
const { selectors = [], ignored_selectors = [] } = section;
selectors.forEach(sel => {
XenForo.register(sel, function() {
XenForo.applyBlurCustom(this, ignored_selectors);
});
});
}
function registerBlurForSelectorPage(settings) {
const { selector_page } = settings;
if (!selector_page || !settings.main_page?.enabled) return;
Object.values(selector_page).forEach(section => {
Object.values(section).forEach(category => {
registerSelectors(category);
});
});
// Специфическая логика для сообщений
if (selector_page.general?.messages?.enabled) {
XenForo.register('li.conversationItem', function() {
const container = this.closest('ol');
XenForo.applyBlurCustom(container);
});
}
// Логика для MarketItems
const marketEnabled = selector_page.market?.viewed?.enabled || selector_page.market?.orders?.enabled;
if (marketEnabled) {
XenForo.register('div.MarketItems', function() {
const path = window.location.pathname;
if ((path === "/user/orders" && selector_page.market?.orders?.enabled) ||
(path === "/viewed" && selector_page.market?.viewed?.enabled)) {
XenForo.applyBlurCustom(this);
}
});
}
// Секретный вопрос
XenForo.register('input[name="secret_answer"]', function() {
let el = this instanceof jQuery ? this[0] : this;
el.setAttribute('type', 'password');
});
// Хайды
XenForo.register('div.bbCodeHide', function() {
let el = this instanceof jQuery ? this[0] : this;
const quote = el.querySelector('div.quote');
const quoteContainer = el.querySelector('blockquote.hideContainer');
const attribution = el.querySelector('aside div.attribution.type');
if (quote && quoteContainer && attribution) {
XenForo.applyBlurCustom(quoteContainer);
} else {
quoteContainer.style.filter = "none"
}
});
}
const banWordsList = StreamerModeSettings?.banwords_page?.banwords
XenForo.applyCensorCustom = function(element, banwords = []) {
let el = element instanceof jQuery ? element[0] : element;
if (!el || typeof el.classList === 'undefined') return;
const regex = new RegExp(banWordsList.map(word => word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'gi');
function censorText(text) {
return text.replace(regex, match => '*'.repeat(match.length));
}
function censorNode(node) {
if (node.nodeType === Node.TEXT_NODE) {
const censored = censorText(node.nodeValue);
if (node.nodeValue !== censored) {
node.nodeValue = censored;
}
} else if (node.nodeType === Node.ELEMENT_NODE) {
for (let child of node.childNodes) {
censorNode(child);
}
}
}
censorNode(el);
};
function pochinit_kostili() {
const api_use_info = document.querySelector('div.profilePage div.profile_info_row:nth-of-type(3)');
if (api_use_info) {
const api_text = api_use_info.querySelector('div.labeled').textContent.trim();
if (!api_text.includes('API')) {
api_use_info.style.display = 'revert !important';
}
}
const id_info = document.querySelector('div.profilePage div.profile_info_row:nth-of-type(2)');
if (id_info) {
const id_user = id_info.querySelector('div.label').textContent.trim();
if (!id_user.includes('ID')) {
id_info.style.display = 'revert !important';
}
}
}
registerBlurForSelectorPage(StreamerModeSettings);
pochinit_kostili();
if (StreamerModeSettings?.banwords_page?.enabled && StreamerModeSettings?.main_page?.enabled) {
const selectors = [
'div.messageContent', 'div.commentContent', 'div.pageContent div.titleBar',
'.threadLastPost', '.discussionListItem', 'li.Alert', ' div.liveAlert',
'ul li.conversationItem', '#page_current_info', '.userStatus',
'div.ng-notification div.body'
]
selectors.forEach(sel => {
XenForo.register(sel, function() {
XenForo.applyCensorCustom(this, banWordsList);
});
});
XenForo.register('ol li.conversationItem', function() {
const container = this.querySelector('.Content');
XenForo.applyCensorCustom(container);
});
XenForo.register('ul li.conversationItem', function() {
const container = this.querySelector('.Content');
XenForo.applyCensorCustom(container);
});
}
const publicTabsCount = document.querySelectorAll('ul.account-links .navTab').length;
const searchBar = document.querySelector('div#searchBar .formPopup');
if (publicTabsCount >= 7 && searchBar) {
searchBar.style.width = '110px';
}
const publicTabs = document.querySelector('ul.account-links');
const newItem = document.createElement('li');
newItem.classList.add('navTab', 'PopupClosed');
newItem.innerHTML = `
<a href="/streamer-mode" class="navLink">
<div class="counter-container">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
<path d="M23 7l-7 5 7 5V7z"/>
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
</svg>
</div>
</a>
`;
publicTabs.prepend(newItem);
if (path === "/streamer-mode") {
document.title = 'LZT Streamer Mode [made ❤︎ llimonix]';
const settingsRoot = document.querySelector('div#headerMover div.pageContent');
const htmlTree = buildStreamerModeSettings(StreamerModeSettings);
settingsRoot.appendChild(htmlTree);
buildBanwordsInputStyle();
document.getElementById('lzt_streamer_mode_save')?.addEventListener('click', lzt_streamer_mode_save);
document.getElementById('lzt_streamer_mode_reset')?.addEventListener('click', function () {
GM_deleteValue('lzt_streamer_mode_settings');
location.reload();
});
document.getElementById('lzt_sm_banwords_show')?.addEventListener('click', toggleBanwordVisibility);
document.getElementById('lzt_sm_banwords_import')?.addEventListener('click', buttonBanwordImport);
document.getElementById('lzt_sm_banwords_export')?.addEventListener('click', buttonBanwordExport);
}
}
function toggleBanwordVisibility() {
const btnBanwords = document.getElementById('lzt_sm_banwords_show');
const isCurrentlyShown = btnBanwords.classList.contains('showBanwords');
const show = !isCurrentlyShown;
if (show) {
btnBanwords.classList.remove('unshowBanwords');
btnBanwords.classList.add('showBanwords');
btnBanwords.textContent = 'Показать запрещенные слова'
} else {
btnBanwords.classList.remove('showBanwords');
btnBanwords.classList.add('unshowBanwords');
btnBanwords.textContent = 'Скрыть запрещенные слова'
}
document.querySelectorAll('.word[data-real]').forEach(span => {
const real = span.dataset.real;
span.textContent = isCurrentlyShown ? real : '•'.repeat(real.length);
});
}
function buttonBanwordExport() {
// Получение банвордов
const banwordElements = document.querySelectorAll('.lzt_sm_input_banword .word');
const banwords = Array.from(banwordElements)
.map(el => el.dataset.real?.trim()) // <-- Берём из data-real
.filter(word => word && word.length > 0);
console.log(banwords)
// Создание текстового файла
const text = banwords.join(', ');
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
// Создание и клик по ссылке
const a = document.createElement('a');
a.href = url;
a.download = 'banwords.txt';
a.style.display = 'none';
document.body.appendChild(a);
a.click();
// Очистка
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function buttonBanwordImport() {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.txt';
fileInput.style.display = 'none';
fileInput.addEventListener('change', () => {
const file = fileInput.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function (e) {
const content = e.target.result;
if (!content) return;
// Получаем контейнер и input
const inputContainer = document.querySelector('.lzt_sm_input');
const textInput = document.getElementById('lzt_sm_banwords_input');
if (!inputContainer || !textInput) return;
// Список уже добавленных слов
const existingWords = new Set();
inputContainer.querySelectorAll('.word[data-real]').forEach(span => {
const real = span.dataset.real?.trim();
if (real) existingWords.add(real);
});
// Обработка слов из файла
const importedWords = content
.split(/[\n,]+/)
.map(w => w.trim())
.filter(w => w.length > 0);
const added = new Set();
for (const word of importedWords) {
if (existingWords.has(word) || added.has(word)) continue;
// Создание тега слова
const wordTag = document.createElement('div');
wordTag.className = 'lzt_sm_input_banword';
const span = document.createElement('span');
span.className = 'word';
span.dataset.real = word;
span.textContent = '•'.repeat(word.length); // скрытое отображение
const removeBtn = document.createElement('div');
removeBtn.className = 'remove_word';
removeBtn.textContent = '×';
removeBtn.onclick = () => {
wordTag.remove();
existingWords.delete(word);
};
wordTag.appendChild(span);
wordTag.appendChild(removeBtn);
inputContainer.insertBefore(wordTag, textInput);
added.add(word);
}
XenForo.stackAlert(`Импортировано слов: ${added.size}`, 5000)
};
reader.readAsText(file);
});
document.body.appendChild(fileInput);
fileInput.click();
document.body.removeChild(fileInput);
}
function lzt_streamer_mode_save() {
// Клонируем текущие настройки
const updatedSettings = structuredClone(StreamerModeSettings);
document.querySelectorAll('div.lztStreamerMode input[type="checkbox"]').forEach(checkbox => {
const path = checkbox.id.replace('lzt_sm_', '')
if (!path) return;
const keys = path.split('.'); // Разбиваем путь по точкам
let current = updatedSettings;
// Идем по иерархии ключей
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!(key in current)) {
console.warn(`Путь не найден: ${key} в ${path}`);
return;
}
current = current[key];
}
const lastKey = keys[keys.length - 1];
if (lastKey in current) {
current[lastKey].enabled = checkbox.checked;
} else {
console.warn(`Ключ ${lastKey} не найден в ${path}`);
}
});
// Получение банвордов
const banwordElements = document.querySelectorAll('.lzt_sm_input_banword .word');
const banwords = Array.from(banwordElements)
.map(el => el.dataset.real?.trim()) // <-- Берём из data-real
.filter(word => word && word.length > 0);
if (updatedSettings.banwords_page) {
updatedSettings.banwords_page.banwords = banwords;
} else {
console.warn('banwords_page отсутствует в настройках');
}
// Сохраняем
GM_setValue('lzt_streamer_mode_settings', updatedSettings);
XenForo.alert('[LZT Streamer Mode] Настройки сохранены', '', 5000)
console.log('[LZT Streamer Mode] Настройки сохранены');
}
function buildBanwordsInputStyle() {
const container = document.querySelector('.lzt_sm_input');
const input = document.getElementById('lzt_sm_banwords_input');
const existingWords = new Set();
// Создание тега
function addWordTag(word) {
const btnBanwords = document.getElementById('lzt_sm_banwords_show');
const isCurrentlyShown = !btnBanwords.classList.contains('showBanwords');
const trimmed = word.trim();
if (!trimmed || existingWords.has(trimmed.toLowerCase())) return;
const tag = document.createElement('div');
tag.className = 'lzt_sm_input_banword';
const span = document.createElement('span');
span.className = 'word';
span.textContent = isCurrentlyShown ? trimmed : '•'.repeat(trimmed.length);
span.dataset.real = trimmed; // сохраняем оригинальное значение
const remove = document.createElement('div');
remove.className = 'remove_word';
remove.textContent = '×';
remove.addEventListener('click', () => {
tag.remove();
existingWords.delete(trimmed.toLowerCase());
});
tag.appendChild(span);
tag.appendChild(remove);
container.insertBefore(tag, input);
existingWords.add(trimmed.toLowerCase());
}
// Обработка Enter и запятой
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ',' || e.code === "Space") {
e.preventDefault();
const parts = input.value.split(',');
parts.forEach(word => addWordTag(word));
input.value = '';
} else if (e.key === 'Backspace' && input.value === '') {
const tags = container.querySelectorAll('.lzt_sm_input_banword');
if (tags.length > 0) {
const lastTag = tags[tags.length - 1];
const word = lastTag.querySelector('.word').textContent;
existingWords.delete(word.toLowerCase());
lastTag.remove();
}
}
});
// Обработка вставки текста
input.addEventListener('paste', function (e) {
e.preventDefault();
const paste = (e.clipboardData || window.clipboardData).getData('text');
const parts = paste.split(',');
parts.forEach(word => addWordTag(word));
input.value = '';
});
}
function buildStreamerModeSettings(settings) {
const container = document.createElement('div');
container.className = 'container';
const mainContentBlock = document.createElement('div');
mainContentBlock.className = 'mainContentBlock sectionMain';
container.appendChild(mainContentBlock);
const lztStreamerMode = document.createElement('div');
lztStreamerMode.className = 'lztStreamerMode';
mainContentBlock.appendChild(lztStreamerMode);
const title = document.createElement('div');
title.className = 'lzt_sm_title';
title.style.borderRadius = '10px 10px 0 0';
title.style.margin = '0';
title.textContent = 'LZT Streamer Mode [made ❤︎ llimonix]';
lztStreamerMode.appendChild(title);
for (const [pageKey, page] of Object.entries(settings)) {
const dl = document.createElement('dl');
dl.className = 'ctrlUnit_blocks';
const heading = document.createElement('div');
heading.className = 'textHeading';
heading.textContent = page.title || 'Без названия';
dl.appendChild(heading);
if (pageKey === 'main_page') {
const div = document.createElement('div');
const label = document.createElement('label');
const input = document.createElement('input');
input.type = 'checkbox';
input.id = `lzt_sm_${pageKey}`;
input.checked = page.enabled;
label.appendChild(input);
label.appendChild(document.createTextNode(' ' + (page.description || key)));
div.appendChild(label);
dl.appendChild(div);
lztStreamerMode.appendChild(dl);
continue;
}
if (pageKey === 'banwords_page') {
const div = document.createElement('div');
const label = document.createElement('label');
const input = document.createElement('input');
input.type = 'checkbox';
input.id = `lzt_sm_${pageKey}`;
input.checked = page.enabled;
label.appendChild(input);
label.appendChild(document.createTextNode(' ' + (page.description || key)));
div.appendChild(label);
dl.appendChild(div);
const dd = document.createElement('dd');
dd.style.margin = '15px 0 0';
// Контейнер для ввода и тегов
const inputContainer = document.createElement('div');
inputContainer.className = 'lzt_sm_input';
const textInput = document.createElement('input');
textInput.type = 'text';
textInput.id = 'lzt_sm_banwords_input';
textInput.className = 'lzt_sm_input';
inputContainer.appendChild(textInput);
dd.appendChild(inputContainer);
// Контейнер с кнопками
const buttonsContainer = document.createElement('div');
buttonsContainer.className = 'banwordsOptionsBtn';
buttonsContainer.style.marginTop = '10px';
// Кнопка: Показать запрещенные слова
const showButton = document.createElement('button');
showButton.id = 'lzt_sm_banwords_show'
showButton.className = 'button smallButton showBanwords';
showButton.style.marginRight = '5px';
showButton.textContent = 'Показать запрещенные слова';
// Кнопка: Импортировать слова
const importButton = document.createElement('button');
importButton.id = 'lzt_sm_banwords_import'
importButton.className = 'button smallButton';
importButton.style.marginRight = '5px';
importButton.textContent = 'Импортировать слова';
// Кнопка: Экспортировать слова
const exportButton = document.createElement('button');
exportButton.id = 'lzt_sm_banwords_export'
exportButton.className = 'button smallButton';
exportButton.textContent = 'Экспортировать слова';
// Добавляем кнопки в контейнер
buttonsContainer.appendChild(showButton);
buttonsContainer.appendChild(importButton);
buttonsContainer.appendChild(exportButton);
dl.appendChild(dd);
dl.appendChild(buttonsContainer);
lztStreamerMode.appendChild(dl);
// Загрузить слова из page.banwords (массив слов)
const existingWords = new Set(); // для исключения дублей
const renderBanwordTag = (word) => {
if (!word || existingWords.has(word)) return;
existingWords.add(word);
const wordTag = document.createElement('div');
wordTag.className = 'lzt_sm_input_banword';
const span = document.createElement('span');
span.className = 'word';
span.textContent = word;
const removeBtn = document.createElement('div');
removeBtn.className = 'remove_word';
removeBtn.textContent = '×';
span.textContent = '•'.repeat(word.length); // маскировка
span.dataset.real = word; // сохраняем оригинальное значение
removeBtn.onclick = () => {
wordTag.remove();
existingWords.delete(word);
};
wordTag.appendChild(span);
wordTag.appendChild(removeBtn);
inputContainer.insertBefore(wordTag, textInput);
};
// подгрузка из конфига
if (Array.isArray(page.banwords)) {
page.banwords.forEach(renderBanwordTag);
}
continue;
}
const dd = document.createElement('dd');
const ul = document.createElement('ul');
for (const [sectionKey, section] of Object.entries(page)) {
if (['title', 'description', 'enabled'].includes(sectionKey)) continue;
if (!section || Object.keys(section).length === 0) continue;
const li = document.createElement('li');
const desc = document.createElement('div');
desc.className = 'lzt_sm_description';
desc.textContent = section.title || sectionKey;
li.appendChild(desc);
const checkboxDiv = document.createElement('div');
checkboxDiv.className = 'checkbox_streamer';
if (section.warning) {
const label = document.createElement('label');
label.className = 'warningText';
label.appendChild(document.createTextNode(' ' + (section.warning || sectionKey)));
checkboxDiv.appendChild(label);
}
for (const [key, item] of Object.entries(section)) {
if (['title', 'description', 'enabled'].includes(key)) continue;
if (!item.selectors) continue;
const label = document.createElement('label');
const input = document.createElement('input');
input.type = 'checkbox';
input.id = `lzt_sm_${pageKey}.${sectionKey}.${key}`;
input.checked = item.enabled;
label.appendChild(input);
label.appendChild(document.createTextNode(' ' + (item.description || key)));
checkboxDiv.appendChild(label);
}
li.appendChild(checkboxDiv);
ul.appendChild(li);
}
dd.appendChild(ul);
dl.appendChild(dd);
lztStreamerMode.appendChild(dl);
}
// Кнопки
const btnDl = document.createElement('dl');
btnDl.className = 'ctrlUnit_blocks_button_save';
const btnDd = document.createElement('dd');
btnDd.innerHTML = `
<div style="display: flex; flex-direction: row; gap: 8px; flex-wrap: wrap;">
<button class="button primary" id="lzt_streamer_mode_save">Сохранить изменения</button>
<button class="button" id="lzt_streamer_mode_reset">Сбросить настройки</button>
</div>`;
btnDl.appendChild(btnDd);
mainContentBlock.appendChild(btnDl);
const style = document.createElement('style');
style.textContent = `
.lztStreamerMode .lzt_sm_description {
margin: 0 0 10px;
color: #949494;
}
input.lzt_sm_input {
height: 38px;
line-height: 38px;
padding-left: 4px;
border: 0;
color: rgb(214, 214, 214);
flex-grow: 1;
background: none;
width: 100px;
}
div.lzt_sm_input {
font-size: 13px;
color: #d6d6d6;
border: 0 none #000;
box-sizing: border-box;
padding: 4px 8px;
cursor: text;
line-height: 34px;
border-radius: 8px;
outline: none;
display: flex;
flex-wrap: wrap;
height: unset !important;
background: none;
box-shadow: 0 0 0 1px #323232;
}
.lztStreamerMode .textHeading {
margin: 0 0 10px;
}
.lzt_sm_title {
display: flex;
align-items: center;
gap: 8px;
padding: 20px;
font-size: 16px;
font-weight: 600;
border-bottom: 1px solid #242424;
margin: -10px -10px 16px -10px;
width: 100%;
box-sizing: border-box;
}
.mainContentBlock {
border-radius: 10px;
vertical-align: top;
margin: 10px auto;
zoom: 1;
}
.ctrlUnit_blocks {
border-bottom: 1px solid rgb(36, 36, 36);
padding: 20px 20px 16px 20px;
}
.ctrlUnit_blocks_button {
border-bottom: 1px solid rgb(36, 36, 36);
padding: 20px 0px 20px 20px;
}
.lztStreamerMode .ctrlUnit_blocks label {
margin: 0 20px 0 0;
white-space: nowrap;
}
.ctrlUnit_blocks>dd>*>li {
margin: 5px 0 14px;
padding-left: 1px;
}
.ctrlUnit_blocks_button_save {
padding: 20px 0px 20px 20px;
}
.checkbox_streamer {
display: flex;
gap: 8px;
flex-direction: column;
flex-wrap: wrap;
}
.lzt_sm_input_banword {
border-radius: 6px;
display: inline-flex;
gap: 5px;
padding: 4px 8px;
align-items: center;
margin: 5px 5px 5px 0;
height: 20px;
line-height: 20px;
background-color: #2d2d2d;
}
.lzt_sm_input_banword span.word {
height: 100%;
width: 100%;
display: flex;
align-items: center;
font-weight: 600;
}
.lzt_sm_input_banword div.remove_word {
font-size: 19px;
color: #999;
cursor: pointer;
margin-left: 2px;
}`;
mainContentBlock.appendChild(style);
return container;
}