// ==UserScript==
// @name Black Russia Helper
// @namespace http://tampermonkey.net/
// @version 6.2
// @description [v6.2] UI Оптимизация. Глобальные стили (Шрифт, B/I/U). Больше BBCode. Авто-ник/дата, Категории, сортировка, счетчик, выбор формата даты. Опция авто-отправки. Темный UI. Кнопка помощи.
// @author Maras Rofls
// @match *://forum.blackrussia.online/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const CURRENT_VERSION = '6.2';
const DATA_KEY = `blackrussia_signatures_helper_v${CURRENT_VERSION}`;
const PREVIOUS_DATA_KEY = 'blackrussia_signatures_helper_v6.1';
const DEFAULT_SEPARATOR = '\n\n---\n';
const DEFAULT_DATETIME_PRESET = 'DD.MM.YYYY HH:mm';
const MAX_EDITOR_FIND_ATTEMPTS = 20;
const EDITOR_FIND_INTERVAL = 500; // ms
const AUTO_SEND_DELAY = 350; // ms
const DATETIME_PRESETS = {
'DD.MM.YYYY HH:mm': { dateStyle: 'short', timeStyle: 'short', hour12: false },
'DD.MM.YY HH:mm': { year: '2-digit', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false },
'D MMMMற்றுப்ரம் г., HH:mm': { dateStyle: 'long', timeStyle: 'short', hour12: false },
'YYYY-MM-DD HH:mm': { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false, hourCycle: 'h23' },
'HH:mm DD.MM.YYYY': { timeStyle: 'short', dateStyle: 'short', hour12: false },
};
const FONT_LIST = ['По умолчанию', 'Arial', 'Verdana', 'Tahoma', 'Trebuchet MS', 'Times New Roman', 'Georgia', 'Courier New', 'Comic Sans MS', 'Impact'];
const defaultData = {
signatures: [],
settings: {
separator: DEFAULT_SEPARATOR,
dateTimePreset: DEFAULT_DATETIME_PRESET,
autoSendAfterInsert: false,
lastSelectedSignatureIndex: -1,
}
};
let appData = JSON.parse(JSON.stringify(defaultData));
let editorElement = null;
let mainUiContainer = null;
let selectSignatureElement = null;
let insertSignatureButton = null;
let randomSignatureButton = null;
let currentUsername = null;
let modalElement = null;
let modalListElement = null;
let modalCategoryFilter = null;
let modalSortSelect = null;
let modalFormElement = null;
let modalNameInput = null;
let modalContentInput = null;
let modalCategoryInput = null;
let modalSaveButton = null;
let modalSaveAndNewButton = null;
let modalCancelButton = null;
let modalSettingsSeparatorInput = null;
let modalSettingsDateTimeSelect = null;
let modalSettingsAutoSendCheckbox = null;
let editingIndex = null;
let currentSortType = 'name_asc';
let currentFilterCategory = 'all';
function loadData() {
const storedData = GM_getValue(DATA_KEY, null);
if (storedData) {
try {
const parsedData = JSON.parse(storedData);
const mergedSettings = { ...defaultData.settings, ...(parsedData.settings || {}) };
appData.signatures = Array.isArray(parsedData.signatures) ? parsedData.signatures : defaultData.signatures;
appData.settings = mergedSettings;
appData.signatures = appData.signatures.filter(isValidSignature).map(migrateSignatureData);
if (!DATETIME_PRESETS[appData.settings.dateTimePreset]) {
appData.settings.dateTimePreset = DEFAULT_DATETIME_PRESET;
}
} catch (e) {
console.error(`Signature Helper: Ошибка парсинга данных v${CURRENT_VERSION}. Используются стандартные.`, e);
appData = JSON.parse(JSON.stringify(defaultData));
alert(`Не удалось загрузить сохраненные данные Помощника Подписей v${CURRENT_VERSION}. Используются настройки по умолчанию.`);
}
} else {
const previousData = GM_getValue(PREVIOUS_DATA_KEY, null);
if (previousData) {
console.log(`Signature Helper: Миграция данных из ${PREVIOUS_DATA_KEY}...`);
try {
const parsedPreviousData = JSON.parse(previousData);
const migratedSettings = { ...defaultData.settings, ...(parsedPreviousData.settings || {}) };
appData.signatures = Array.isArray(parsedPreviousData.signatures) ? parsedPreviousData.signatures : defaultData.signatures;
appData.settings = migratedSettings;
appData.signatures = appData.signatures.filter(isValidSignature).map(migrateSignatureData);
if (!DATETIME_PRESETS[appData.settings.dateTimePreset]) {
appData.settings.dateTimePreset = DEFAULT_DATETIME_PRESET;
}
console.log(`Signature Helper: Данные ${PREVIOUS_DATA_KEY} успешно перенесены в v${CURRENT_VERSION}.`);
saveData();
} catch (e) {
console.error(`Signature Helper: Ошибка миграции данных из ${PREVIOUS_DATA_KEY}. Используются стандартные.`, e);
appData = JSON.parse(JSON.stringify(defaultData));
}
} else {
console.log('Signature Helper: Сохраненные данные не найдены, используются стандартные.');
appData = JSON.parse(JSON.stringify(defaultData));
}
}
appData.signatures = appData.signatures.filter(isValidSignature).map(migrateSignatureData);
}
function migrateSignatureData(signature) {
return {
name: signature.name || 'Без имени',
content: signature.content || '',
usageCount: signature.usageCount || 0,
category: signature.category || '',
dateAdded: signature.dateAdded || Date.now(),
lastUsed: signature.lastUsed || null,
};
}
function saveData() {
try {
const dataToSave = {
signatures: appData.signatures,
settings: appData.settings
};
GM_setValue(DATA_KEY, JSON.stringify(dataToSave));
} catch (e) {
console.error('Signature Helper: Ошибка сохранения данных.', e);
alert('Ошибка сохранения данных Помощника Подписей!');
}
updateMainUIState();
populateSignatureSelect();
}
function isValidSignature(item) {
return typeof item === 'object' && item !== null && typeof item.name === 'string' && typeof item.content === 'string';
}
function getUsername() {
if (currentUsername === null) {
const userLink = document.querySelector('.p-navgroup-link--user .p-navgroup-linkText');
if (userLink) currentUsername = userLink.textContent.trim();
else {
const avatar = document.querySelector('.p-navgroup-link--user .avatar');
if (avatar) currentUsername = avatar.getAttribute('alt')?.trim() || '';
}
if (!currentUsername) currentUsername = 'Пользователь';
}
return currentUsername;
}
function getCurrentDateTime(formatPreset = null) {
const presetKey = formatPreset || appData.settings.dateTimePreset;
const options = DATETIME_PRESETS[presetKey] || DATETIME_PRESETS[DEFAULT_DATETIME_PRESET];
const now = new Date();
let formattedString = '';
try {
// Try formatting using Intl first (more reliable for different presets)
if (presetKey === 'YYYY-MM-DD HH:mm') {
// Intl doesn't easily support this exact format with space separator AND 24h cycle consistently
const year = now.getFullYear();
const month = (now.getMonth() + 1).toString().padStart(2, '0');
const day = now.getDate().toString().padStart(2, '0');
const hours = now.getHours().toString().padStart(2, '0');
const minutes = now.getMinutes().toString().padStart(2, '0');
formattedString = `${year}-${month}-${day} ${hours}:${minutes}`;
} else if (presetKey === 'HH:mm DD.MM.YYYY') {
const timeFormatter = new Intl.DateTimeFormat('ru-RU', { hour: '2-digit', minute: '2-digit', hour12: false });
const dateFormatter = new Intl.DateTimeFormat('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' });
formattedString = `${timeFormatter.format(now)} ${dateFormatter.format(now)}`;
} else {
// Use Intl for other standard formats
const formatter = new Intl.DateTimeFormat('ru-RU', options);
formattedString = formatter.format(now);
}
} catch (e) {
console.error(`Signature Helper: Ошибка форматирования даты для пресета ${presetKey}`, e);
// Fallback to manual formatting if Intl fails
const d = now;
formattedString = `${d.getDate().toString().padStart(2, '0')}.${(d.getMonth() + 1).toString().padStart(2, '0')}.${d.getFullYear()} ${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
}
return formattedString;
}
function addSignature(name, content, category) {
if (!name?.trim() || !content) {
alert('Название и содержание подписи не могут быть пустыми.');
return false;
}
const newSignature = migrateSignatureData({
name: name.trim(),
content: content,
category: category?.trim() || '',
});
appData.signatures.push(newSignature);
return true;
}
function updateSignature(index, name, content, category) {
if (index < 0 || index >= appData.signatures.length) return false;
if (!name?.trim() || !content) {
alert('Название и содержание подписи не могут быть пустыми.');
return false;
}
const oldSignature = appData.signatures[index];
appData.signatures[index] = {
...oldSignature,
name: name.trim(),
content: content,
category: category?.trim() || '',
};
return true;
}
function deleteSignature(index) {
if (index < 0 || index >= appData.signatures.length) return;
setTimeout(() => {
const sigName = appData.signatures[index]?.name || 'Без имени';
if (confirm(`Вы уверены, что хотите удалить подпись "${sigName}"?`)) {
appData.signatures.splice(index, 1);
saveData();
renderModalList();
populateSignatureSelect();
}
}, 0);
}
function duplicateSignature(index) {
if (index < 0 || index >= appData.signatures.length) return;
const originalSig = appData.signatures[index];
const newName = `${originalSig.name} (Копия)`;
modalNameInput.value = newName;
modalContentInput.value = originalSig.content;
modalCategoryInput.value = originalSig.category;
showModalForm(null);
modalNameInput.focus();
modalNameInput.select();
}
function findEditorElement() {
let editor = document.querySelector('.fr-element.fr-view');
if (editor && editor.isContentEditable) return editor;
editor = document.querySelector('textarea[name="message"], textarea.input--labelled-textArea, textarea#message');
return editor;
}
function handleInsertion(signatureIndex) {
if (signatureIndex < 0 || signatureIndex >= appData.signatures.length) return false;
const signature = appData.signatures[signatureIndex];
if (!editorElement) {
editorElement = findEditorElement();
if (!editorElement) {
console.warn('Helper: Редактор не найден.');
alert('Не удалось найти поле редактора для вставки.');
return false;
}
}
const username = getUsername();
const dateTime = getCurrentDateTime();
const userInfoText = `${username} | ${dateTime}`;
const userSeparatorText = appData.settings.separator.replace(/<br\s*\/?>/gi, '\n'); // Allow <br> or <br/>
const userSeparatorHtml = appData.settings.separator.replace(/\n/g, '<br>');
const finalContentText = `${signature.content}${userSeparatorText}${userInfoText}`;
// Ensure BBCode line breaks are converted for HTML view, but keep existing <br>
const finalContentHtml = signature.content.replace(/(?<!<br\s*\/?>)\n/g, '<br>') + userSeparatorHtml + userInfoText;
const previewText = `--- Предпросмотр Подписи ---\n\n${finalContentText}\n\n---------------------------\n\nВставить?`;
if (!confirm(previewText)) return false;
let insertSuccess = false;
try {
const isTextArea = editorElement.tagName === 'TEXTAREA';
const isContentEditable = editorElement.isContentEditable;
if (isTextArea) {
insertIntoTextarea(finalContentText);
} else if (isContentEditable) {
insertIntoContentEditable(finalContentHtml);
} else {
throw new Error('Найденный редактор не поддерживается.');
}
insertSuccess = true;
} catch (e) {
console.error("Signature Helper: Ошибка вставки подписи:", e);
alert(`Ошибка вставки подписи: ${e.message}`);
return false;
}
signature.usageCount = (signature.usageCount || 0) + 1;
signature.lastUsed = Date.now();
appData.settings.lastSelectedSignatureIndex = signatureIndex;
saveData();
if (modalElement && modalElement.style.display !== 'none') {
renderModalList();
}
return true;
}
function insertIntoTextarea(textToInsert) {
const currentEditorText = editorElement.value.trim();
const initialSeparator = currentEditorText.length > 0 ? '\n\n' : '';
const start = editorElement.selectionStart;
const end = editorElement.selectionEnd;
const before = editorElement.value.substring(0, start);
const after = editorElement.value.substring(end);
editorElement.value = before + initialSeparator + textToInsert + after;
editorElement.focus();
const newCursorPos = start + initialSeparator.length + textToInsert.length;
editorElement.setSelectionRange(newCursorPos, newCursorPos);
editorElement.scrollTop = editorElement.scrollHeight; // Scroll to bottom after insert
// Trigger input event for frameworks that might listen for it
editorElement.dispatchEvent(new Event('input', { bubbles: true }));
}
function insertIntoContentEditable(htmlToInsert) {
editorElement.focus();
setTimeout(() => {
try {
// Check if editor already ends with paragraphs/breaks and avoid adding extra ones
const currentEditorHTML = editorElement.innerHTML.trim();
const endsWithBreak = /<(p|div|br)[\s>]/i.test(currentEditorHTML.slice(-20)); // Simple check if it ends likely with a block/break
const initialSeparator = currentEditorHTML.length > 0 && !endsWithBreak ? '<br><br>' : '';
document.execCommand('insertHTML', false, initialSeparator + htmlToInsert);
} catch (e) {
console.error("Helper: Ошибка execCommand('insertHTML'):", e);
// Fallback attempt (less reliable with cursor position)
try {
const currentEditorHTML = editorElement.innerHTML.trim();
const endsWithBreak = /<(p|div|br)[\s>]/i.test(currentEditorHTML.slice(-20));
const initialSeparator = currentEditorHTML.length > 0 && !endsWithBreak ? '<br><br>' : '';
// Append at the end as a last resort
editorElement.innerHTML += initialSeparator + htmlToInsert;
} catch (fallbackError) {
console.error("Helper: Резервная вставка не удалась:", fallbackError);
throw new Error('Не удалось вставить контент.');
}
}
// Ensure visibility after insertion
editorElement.scrollTop = editorElement.scrollHeight;
}, 50); // Timeout helps ensure focus is set before execCommand
}
function triggerAutoSend() {
if (!appData.settings.autoSendAfterInsert || !editorElement) return;
setTimeout(() => {
const form = editorElement.closest('form');
if (!form) {
console.warn('Signature Helper: Auto-send failed, could not find parent form.');
return;
}
let submitButton = form.querySelector('button[type="submit"].button--primary, button.button--cta[type="submit"]'); // More specific selectors first
if (!submitButton) submitButton = form.querySelector('button[type="submit"]');
if (!submitButton) submitButton = form.querySelector('input[type="submit"]');
if (!submitButton) {
// Try common button texts in Russian and English
const possibleTexts = ['Отправить', 'Ответить', 'Создать тему', 'Сохранить', 'Save', 'Reply', 'Submit', 'Post reply'];
const buttons = form.querySelectorAll('button, input[type="button"], input[type="submit"]');
for(const btn of buttons) {
const btnText = (btn.textContent || btn.value || '').trim();
if (possibleTexts.some(text => btnText.toLowerCase().includes(text.toLowerCase()))) {
submitButton = btn;
break;
}
}
}
if (submitButton) {
console.log('Signature Helper: Auto-sending form...');
submitButton.click();
} else {
console.warn('Signature Helper: Auto-send failed, could not find submit button.');
// alert('Не удалось найти кнопку отправки для авто-отправки.'); // Optional user feedback
}
}, AUTO_SEND_DELAY);
}
function updateMainUIState() {
const hasSignatures = appData.signatures.length > 0;
if (selectSignatureElement) selectSignatureElement.disabled = !hasSignatures;
if (insertSignatureButton) insertSignatureButton.disabled = !hasSignatures;
if (randomSignatureButton) randomSignatureButton.disabled = !hasSignatures;
}
function populateSignatureSelect() {
if (!selectSignatureElement) return;
const currentSelectedIndex = selectSignatureElement.value; // Remember currently selected value if any
selectSignatureElement.innerHTML = ''; // Clear existing options
if (appData.signatures.length === 0) {
const option = document.createElement('option');
option.textContent = 'Нет подписей';
option.disabled = true;
selectSignatureElement.appendChild(option);
} else {
// Group signatures by category
const categories = {};
appData.signatures.forEach((sig, index) => {
const categoryName = sig.category?.trim() || 'Без категории'; // Ensure category is a string
if (!categories[categoryName]) categories[categoryName] = [];
categories[categoryName].push({ ...sig, originalIndex: index });
});
// Sort category names, placing "Без категории" first
const sortedCategoryNames = Object.keys(categories).sort((a, b) => {
if (a === 'Без категории') return -1;
if (b === 'Без категории') return 1;
return a.localeCompare(b, 'ru'); // Locale-specific sorting for category names
});
// Create optgroups and options
sortedCategoryNames.forEach(categoryName => {
const group = document.createElement('optgroup');
group.label = categoryName;
// Sort signatures within each category by name
const signaturesInCategory = categories[categoryName].sort((a, b) => a.name.localeCompare(b.name, 'ru'));
signaturesInCategory.forEach(sigData => {
const option = document.createElement('option');
option.value = sigData.originalIndex.toString();
option.textContent = sigData.name;
// Add tooltip with category and preview (limited length)
const preview = sigData.content.substring(0, 100) + (sigData.content.length > 100 ? '...' : '');
option.title = `Категория: ${categoryName}\n---\n${preview}`;
group.appendChild(option);
});
selectSignatureElement.appendChild(group);
});
// Restore selection if possible
const lastIndex = appData.settings.lastSelectedSignatureIndex;
if (lastIndex !== -1 && selectSignatureElement.querySelector(`option[value="${lastIndex}"]`)) {
selectSignatureElement.value = lastIndex.toString();
} else if (selectSignatureElement.querySelector(`option[value="${currentSelectedIndex}"]`)) {
// If last index is invalid, try restoring the previous selection
selectSignatureElement.value = currentSelectedIndex;
} else if (selectSignatureElement.options.length > 0) {
// Otherwise, select the first available option
selectSignatureElement.value = selectSignatureElement.options[0].value;
// Update lastSelectedSignatureIndex setting if we selected the first one
appData.settings.lastSelectedSignatureIndex = parseInt(selectSignatureElement.value, 10);
}
}
updateMainUIState(); // Update button disabled states
}
function showHelpInfo() {
const helpText = `--- Помощник Подписей Black Russia v${CURRENT_VERSION} ---
Этот скрипт добавляет панель под редактором сообщений для быстрой вставки заранее созданных подписей.
Основные элементы:
- Выпадающий список: Выбор сохраненной подписи (сгруппированы по категориям).
- [Вставить]: Вставка выбранной подписи с предпросмотром. Ваш ник и текущая дата/время будут добавлены в конце согласно настройкам.
- [🎲]: Вставка случайной подписи из списка (также с предпросмотром).
- [⚙️]: Открытие окна управления подписями и настройками.
- [?]: Отображение этой справки.
Окно управления (⚙️):
- Фильтр по категориям и Сортировка: Навигация по списку подписей.
- [+ Добавить подпись]: Открывает форму для создания новой подписи.
- Список подписей: Показывает имя, категорию, количество использований ([N]), дату добавления и последнего использования. При наведении виден текст подписи.
- [✏️]: Редактировать подпись.
- [📋]: Дублировать подпись (создать копию).
- [❌]: Удалить подпись (с подтверждением).
- Настройки:
- Разделитель: Текст (можно использовать <br> или <br/> для переноса), который вставляется между подписью и блоком "Ник | Дата".
- Формат даты и времени: Выбор вида отображения даты и времени.
- Авто-отправка: Опция автоматического нажатия кнопки отправки формы после вставки подписи (используйте с осторожностью!).
Форма добавления/редактирования:
- Название: Обязательное поле для идентификации подписи.
- Категория: Необязательное поле для группировки.
- Глобальные стили: Применение шрифта, жирности (B), курсива (I) или подчеркивания (U) ко ВСЕЙ подписи. Выбор шрифта или нажатие B/I/U обернет весь текст в соответствующие BBCode теги ([font=...], [b], [i], [u]). Повторное нажатие B/I/U уберет тег.
- Содержание подписи: Основной текст вашей подписи. Можно использовать BBCode.
- Панель BBCode: Кнопки для быстрой вставки тегов [b], [i], [u], [strike], [center], [quote], [color], [size], [img], [url].
- Для [color] и [size] используются выбранные рядом цвет и размер.
- [🌈]: Применяет случайный цвет ([color=...]) к каждой непустой строке текста.
- [🕒]: Вставляет текущую дату и время (согласно настройкам) в место курсора.
- Кнопки: "Добавить/Сохранить", "Сохранить и Добавить еще" (только при добавлении), "Отмена" (закрывает форму без сохранения).
При вставке подписи ваш Ник и Дата/Время добавляются автоматически в конец, согласно выбранному формату и разделителю.
Приятного пользования!`;
alert(helpText);
}
function createMainUI(targetEditor) {
if (document.getElementById('sig-helper-main-ui')) return;
mainUiContainer = document.createElement('div');
mainUiContainer.id = 'sig-helper-main-ui';
mainUiContainer.className = 'sig-helper-main-ui sig-helper-dark';
selectSignatureElement = document.createElement('select');
selectSignatureElement.title = 'Выберите подпись (авто-ник/дата)';
selectSignatureElement.onchange = () => {
const selectedIndex = parseInt(selectSignatureElement.value, 10);
if (!isNaN(selectedIndex)) {
appData.settings.lastSelectedSignatureIndex = selectedIndex;
// No save needed on select change, only on insert
}
};
populateSignatureSelect();
insertSignatureButton = document.createElement('button');
insertSignatureButton.type = 'button';
insertSignatureButton.textContent = 'Вставить';
insertSignatureButton.className = 'button button--cta';
insertSignatureButton.title = 'Вставить выбранную подпись (с предпросмотром)';
insertSignatureButton.onclick = () => {
const selectedIndex = parseInt(selectSignatureElement.value, 10);
if (!isNaN(selectedIndex)) {
if (handleInsertion(selectedIndex)) {
triggerAutoSend();
}
} else {
alert('Пожалуйста, выберите подпись из списка.');
}
};
randomSignatureButton = document.createElement('button');
randomSignatureButton.type = 'button';
randomSignatureButton.innerHTML = '🎲';
randomSignatureButton.className = 'button';
randomSignatureButton.title = 'Вставить случайную подпись (с предпросмотром)';
randomSignatureButton.onclick = () => {
if (appData.signatures.length > 0) {
const randomIndex = Math.floor(Math.random() * appData.signatures.length);
// Select the random signature in the dropdown for visual feedback
selectSignatureElement.value = randomIndex.toString();
// Update setting as if user selected it
appData.settings.lastSelectedSignatureIndex = randomIndex;
if (handleInsertion(randomIndex)) {
triggerAutoSend();
}
} else {
alert('Нет сохраненных подписей.');
}
};
const manageButton = document.createElement('button');
manageButton.type = 'button';
manageButton.innerHTML = '⚙️';
manageButton.className = 'button';
manageButton.title = 'Управление подписями и настройками';
manageButton.onclick = openManageModal;
const helpButton = document.createElement('button');
helpButton.type = 'button';
helpButton.innerHTML = '?';
helpButton.className = 'button';
helpButton.title = 'Помощь и информация о скрипте';
helpButton.onclick = showHelpInfo;
mainUiContainer.append(selectSignatureElement, insertSignatureButton, randomSignatureButton, manageButton, helpButton);
updateMainUIState();
let insertBeforeElement = targetEditor.closest('.fr-box') || targetEditor.closest('.editorHtml') || targetEditor.parentNode;
if (insertBeforeElement?.parentNode) {
insertBeforeElement.parentNode.insertBefore(mainUiContainer, insertBeforeElement);
} else if(targetEditor?.parentNode) {
// Fallback: insert directly before the editor itself if no better container found
targetEditor.parentNode.insertBefore(mainUiContainer, targetEditor);
} else {
console.error("Signature Helper: Не удалось найти место для вставки UI.");
}
}
function createManageModal() {
if (modalElement) return;
modalElement = document.createElement('div');
modalElement.id = 'sig-helper-modal';
modalElement.className = 'sig-helper-modal sig-helper-dark';
modalElement.style.display = 'none';
// Close modal if backdrop is clicked
modalElement.addEventListener('click', (event) => {
if (event.target === modalElement) {
closeManageModal();
}
});
const modalContent = document.createElement('div');
modalContent.className = 'sig-helper-modal-content';
// Prevent backdrop click from closing when clicking inside content
modalContent.addEventListener('click', (event) => event.stopPropagation());
const closeButton = document.createElement('button');
closeButton.innerHTML = '×';
closeButton.className = 'sig-helper-modal-close';
closeButton.title = 'Закрыть';
closeButton.onclick = closeManageModal;
const title = document.createElement('h2');
title.textContent = 'Управление подписями и настройками';
// --- Main Area Wrapper (for scrolling list/settings) ---
const mainArea = document.createElement('div');
mainArea.className = 'sig-helper-modal-main-area';
const controlsContainer = document.createElement('div');
controlsContainer.className = 'sig-helper-modal-controls';
modalCategoryFilter = document.createElement('select');
modalCategoryFilter.title = 'Фильтр по категории';
modalCategoryFilter.onchange = () => { currentFilterCategory = modalCategoryFilter.value; renderModalList(); };
controlsContainer.appendChild(modalCategoryFilter);
modalSortSelect = document.createElement('select');
modalSortSelect.title = 'Сортировка списка';
const sortOptions = {
'name_asc': 'Имя (А-Я)', 'name_desc': 'Имя (Я-А)', 'usage_desc': 'Чаще используемые', 'usage_asc': 'Реже используемые',
'date_desc': 'Новые', 'date_asc': 'Старые', 'lastused_desc': 'Недавно использованные', 'lastused_asc': 'Давно использованные' };
Object.entries(sortOptions).forEach(([value, text]) => {
const option = document.createElement('option'); option.value = value; option.textContent = text; modalSortSelect.appendChild(option); });
modalSortSelect.value = currentSortType;
modalSortSelect.onchange = () => { currentSortType = modalSortSelect.value; renderModalList(); };
controlsContainer.appendChild(modalSortSelect);
modalListElement = document.createElement('ul');
modalListElement.className = 'sig-helper-modal-list';
modalListElement.addEventListener('click', handleModalListClick);
const addButton = document.createElement('button');
addButton.textContent = '+ Добавить подпись';
addButton.className = 'button button--primary sig-helper-add-button';
addButton.onclick = () => showModalForm(null);
const settingsContainer = document.createElement('div');
settingsContainer.className = 'sig-helper-modal-settings';
const settingsTitle = document.createElement('h3'); settingsTitle.textContent = 'Настройки';
settingsContainer.appendChild(settingsTitle);
createSettingsPane(settingsContainer);
// Append controls, list, add button, settings to the main scrollable area
mainArea.append(controlsContainer, modalListElement, addButton, settingsContainer);
// --- End Main Area Wrapper ---
createModalForm(); // Create form element (initially hidden)
// Append title, main scrollable area, and the form (conditionally displayed) to modal content
modalContent.append(closeButton, title, mainArea, modalFormElement);
modalElement.appendChild(modalContent);
document.body.appendChild(modalElement);
}
function createSettingsPane(container) {
const sepLabel = document.createElement('label');
sepLabel.textContent = 'Разделитель перед Ник/Дата:'; sepLabel.htmlFor = 'sig-helper-settings-separator';
modalSettingsSeparatorInput = document.createElement('textarea');
modalSettingsSeparatorInput.id = 'sig-helper-settings-separator'; modalSettingsSeparatorInput.rows = 2;
modalSettingsSeparatorInput.value = appData.settings.separator; modalSettingsSeparatorInput.placeholder = DEFAULT_SEPARATOR.replace(/\\n/g, '\n'); // Show default line breaks correctly
modalSettingsSeparatorInput.className = 'sig-helper-settings-input';
modalSettingsSeparatorInput.addEventListener('change', saveSettings); // Save on change
const dtLabel = document.createElement('label');
dtLabel.textContent = 'Формат даты и времени:'; dtLabel.htmlFor = 'sig-helper-settings-datetime';
modalSettingsDateTimeSelect = document.createElement('select');
modalSettingsDateTimeSelect.id = 'sig-helper-settings-datetime'; modalSettingsDateTimeSelect.className = 'sig-helper-settings-input';
Object.keys(DATETIME_PRESETS).forEach(presetKey => {
const option = document.createElement('option'); option.value = presetKey;
// Get a formatted example for each preset
option.textContent = `${presetKey} (Пример: ${getCurrentDateTime(presetKey)})`;
modalSettingsDateTimeSelect.appendChild(option); });
modalSettingsDateTimeSelect.value = appData.settings.dateTimePreset;
modalSettingsDateTimeSelect.addEventListener('change', saveSettings);
const autoSendLabel = document.createElement('label'); autoSendLabel.className = 'sig-helper-settings-label-checkbox';
modalSettingsAutoSendCheckbox = document.createElement('input');
modalSettingsAutoSendCheckbox.type = 'checkbox'; modalSettingsAutoSendCheckbox.id = 'sig-helper-settings-autosend';
modalSettingsAutoSendCheckbox.checked = appData.settings.autoSendAfterInsert;
modalSettingsAutoSendCheckbox.addEventListener('change', saveSettings);
autoSendLabel.appendChild(modalSettingsAutoSendCheckbox);
autoSendLabel.appendChild(document.createTextNode(' Автоматически отправлять форму после вставки (Рискованно!)'));
autoSendLabel.title = 'Если включено, скрипт попытается нажать кнопку отправки формы после успешной вставки подписи.';
container.append(sepLabel, modalSettingsSeparatorInput, dtLabel, modalSettingsDateTimeSelect, autoSendLabel);
}
function saveSettings() {
// Read values from inputs
let separatorValue = modalSettingsSeparatorInput.value;
// Ensure default separator if input is empty or only whitespace
appData.settings.separator = separatorValue.trim() === '' ? DEFAULT_SEPARATOR : separatorValue;
appData.settings.dateTimePreset = DATETIME_PRESETS[modalSettingsDateTimeSelect.value] ? modalSettingsDateTimeSelect.value : DEFAULT_DATETIME_PRESET;
appData.settings.autoSendAfterInsert = modalSettingsAutoSendCheckbox.checked;
// Update UI in case validation changed the value (e.g., empty separator)
modalSettingsSeparatorInput.value = appData.settings.separator; // Reflect potentially corrected value
modalSettingsDateTimeSelect.value = appData.settings.dateTimePreset;
modalSettingsAutoSendCheckbox.checked = appData.settings.autoSendAfterInsert;
saveData(); // Save the updated appData
}
function createModalForm() {
modalFormElement = document.createElement('div');
modalFormElement.className = 'sig-helper-modal-form';
modalFormElement.style.display = 'none'; // Initially hidden
const formTitle = document.createElement('h3');
formTitle.id = 'sig-helper-form-title';
const nameLabel = document.createElement('label'); nameLabel.textContent = 'Название:'; nameLabel.htmlFor = 'sig-helper-name-input';
modalNameInput = document.createElement('input'); modalNameInput.type = 'text'; modalNameInput.id = 'sig-helper-name-input'; modalNameInput.required = true;
const categoryLabel = document.createElement('label'); categoryLabel.textContent = 'Категория (необязательно):'; categoryLabel.htmlFor = 'sig-helper-category-input';
modalCategoryInput = document.createElement('input'); modalCategoryInput.type = 'text'; modalCategoryInput.id = 'sig-helper-category-input'; modalCategoryInput.placeholder = 'Например: RP, Гос. Орг., Жалобы';
const categoryDatalist = document.createElement('datalist'); categoryDatalist.id = 'sig-helper-categories';
modalCategoryInput.setAttribute('list', categoryDatalist.id); // Link input to datalist
// --- Global Styles ---
const globalStyleContainer = document.createElement('div');
globalStyleContainer.className = 'sig-helper-global-style-controls';
const fontLabel = document.createElement('label'); fontLabel.textContent = 'Шрифт:'; fontLabel.htmlFor = 'sig-helper-global-font';
const fontSelect = document.createElement('select'); fontSelect.id = 'sig-helper-global-font'; fontSelect.title = 'Применить шрифт ко всей подписи';
FONT_LIST.forEach(fontName => {
const option = document.createElement('option');
option.value = fontName === 'По умолчанию' ? '' : fontName; option.textContent = fontName;
if (fontName !== 'По умолчанию') option.style.fontFamily = fontName; // Show font preview in dropdown
fontSelect.appendChild(option);
});
fontSelect.onchange = () => applyGlobalStyle('font', fontSelect.value);
fontLabel.appendChild(fontSelect);
const styleButtonContainer = document.createElement('div'); styleButtonContainer.className = 'sig-helper-global-style-buttons';
[{ tag: 'b', text: 'B' }, { tag: 'i', text: 'I' }, { tag: 'u', text: 'U' }].forEach(style => {
const button = document.createElement('button'); button.type = 'button'; button.textContent = style.text; button.id = `sig-helper-global-${style.tag}`;
button.className = 'button button--small sig-helper-global-style-button'; button.title = `Применить/убрать ${style.tag}-стиль ко всей подписи`;
button.onclick = () => applyGlobalStyle(style.tag);
styleButtonContainer.appendChild(button);
});
globalStyleContainer.append(fontLabel, styleButtonContainer);
// --- End Global Styles ---
const contentLabel = document.createElement('label'); contentLabel.innerHTML = 'Содержание подписи <small>(BBCode)</small>:'; contentLabel.htmlFor = 'sig-helper-content-input';
modalContentInput = document.createElement('textarea'); modalContentInput.id = 'sig-helper-content-input';
modalContentInput.rows = 7; modalContentInput.required = true;
// Update global style UI based on content changes/focus
modalContentInput.addEventListener('input', debounce(updateGlobalStyleUI, 300));
modalContentInput.addEventListener('focus', updateGlobalStyleUI);
modalContentInput.addEventListener('blur', updateGlobalStyleUI); // Also check on blur
// --- Toolbar ---
const toolbarContainer = document.createElement('div'); toolbarContainer.className = 'sig-helper-toolbar-container';
const bbCodeToolbar = document.createElement('div'); bbCodeToolbar.className = 'sig-helper-bbcode-toolbar';
// Basic tags
['b', 'i', 'u'].forEach(tag => bbCodeToolbar.appendChild(createToolbarButton(tag, `[${tag}]`)));
// Additional tags
bbCodeToolbar.appendChild(createToolbarButton('strike', 'Зачеркнутый'));
bbCodeToolbar.appendChild(createToolbarButton('center', 'Центрировать'));
bbCodeToolbar.appendChild(createToolbarButton('quote', 'Цитата'));
// Color picker + button
const colorPicker = document.createElement('input'); colorPicker.type = 'color'; colorPicker.id = 'sig-helper-bbcode-color'; colorPicker.value = '#E0E0E0'; // Default dark theme text color
colorPicker.title = 'Выбрать цвет для [color]'; colorPicker.className = 'sig-helper-bbcode-control sig-helper-bbcode-colorpicker';
bbCodeToolbar.appendChild(colorPicker);
bbCodeToolbar.appendChild(createToolbarButton('color', 'Цвет текста'));
// Size select + button
const sizeSelect = document.createElement('select'); sizeSelect.id = 'sig-helper-bbcode-size'; sizeSelect.title = 'Выбрать размер для [size]'; sizeSelect.className = 'sig-helper-bbcode-control';
for (let i = 1; i <= 7; i++) { const opt = document.createElement('option'); opt.value = i; opt.textContent = ` ${i} `; if (i === 4) opt.selected = true; sizeSelect.appendChild(opt); } // Size 4 is often default
bbCodeToolbar.appendChild(sizeSelect);
bbCodeToolbar.appendChild(createToolbarButton('size', 'Размер текста'));
// Image and Link buttons
bbCodeToolbar.appendChild(createToolbarButton('img', 'Изображение'));
bbCodeToolbar.appendChild(createToolbarButton('url', 'Ссылка'));
// Action buttons (Random color, Insert Date/Time)
const actionButtonsContainer = document.createElement('div');
actionButtonsContainer.className = 'sig-helper-toolbar-actions';
const randomColorButton = document.createElement('button'); randomColorButton.type = 'button'; randomColorButton.innerHTML = '🌈';
randomColorButton.title = 'Применить случайный цвет к каждой строке'; randomColorButton.className = 'button button--small'; randomColorButton.onclick = applyRandomLineColors;
actionButtonsContainer.appendChild(randomColorButton);
const insertDateTimeButton = document.createElement('button'); insertDateTimeButton.type = 'button'; insertDateTimeButton.innerHTML = '🕒';
insertDateTimeButton.title = 'Вставить текущую дату/время в текст'; insertDateTimeButton.className = 'button button--small'; insertDateTimeButton.onclick = insertDateTimeIntoContent;
actionButtonsContainer.appendChild(insertDateTimeButton);
toolbarContainer.append(bbCodeToolbar, actionButtonsContainer);
// --- End Toolbar ---
// --- Form Buttons ---
const buttonGroup = document.createElement('div'); buttonGroup.className = 'sig-helper-form-buttons';
modalSaveButton = document.createElement('button'); modalSaveButton.type = 'button'; // Ensure type=button
modalSaveButton.className = 'button button--cta';
modalSaveButton.onclick = () => handleModalSave(false);
modalSaveAndNewButton = document.createElement('button'); modalSaveAndNewButton.type = 'button';
modalSaveAndNewButton.textContent = 'Сохранить и Добавить еще';
modalSaveAndNewButton.className = 'button button--primary';
modalSaveAndNewButton.title = 'Сохранить текущую и очистить форму для добавления следующей';
modalSaveAndNewButton.onclick = () => handleModalSave(true);
modalCancelButton = document.createElement('button'); modalCancelButton.type = 'button';
modalCancelButton.textContent = 'Отмена';
modalCancelButton.className = 'button';
modalCancelButton.onclick = hideModalForm; // Use the function to hide the form
buttonGroup.append(modalSaveButton, modalSaveAndNewButton, modalCancelButton);
// --- End Form Buttons ---
// Assemble the form
modalFormElement.append(
formTitle,
nameLabel, modalNameInput,
categoryLabel, modalCategoryInput, categoryDatalist,
globalStyleContainer,
contentLabel, modalContentInput,
toolbarContainer,
buttonGroup
);
}
function createToolbarButton(tag, title) {
const button = document.createElement('button'); button.type = 'button';
button.textContent = `[${tag}]`; button.className = 'button button--small sig-helper-bbcode-button';
button.title = title; button.onclick = () => insertBbCode(tag);
return button;
}
function insertDateTimeIntoContent() {
const currentDateTime = getCurrentDateTime(); const textarea = modalContentInput;
const start = textarea.selectionStart; const end = textarea.selectionEnd;
const before = textarea.value.substring(0, start);
const after = textarea.value.substring(end);
textarea.value = before + currentDateTime + after;
textarea.focus();
// Place cursor after inserted text
textarea.selectionStart = textarea.selectionEnd = start + currentDateTime.length;
textarea.dispatchEvent(new Event('input', { bubbles: true })); // Trigger updates
}
function insertBbCode(tag) {
const textarea = modalContentInput;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selectedText = textarea.value.substring(start, end);
const beforeText = textarea.value.substring(0, start);
const afterText = textarea.value.substring(end);
let replacement = '';
let cursorPosition = start; // Default cursor position if no text selected
switch (tag) {
case 'url':
const url = prompt('Введите URL адрес:', selectedText.startsWith('http') ? selectedText : 'https://');
if (url === null || url.trim() === '') return;
const urlText = selectedText || url; // Use selected text or URL itself as text
replacement = `[url=${url}]${urlText}[/url]`;
cursorPosition = start + `[url=${url}]`.length + urlText.length + `[/url]`.length; // End of tag
break;
case 'img':
const imgUrl = prompt('Введите URL изображения:', selectedText.startsWith('http') ? selectedText : 'https://');
if (imgUrl === null || imgUrl.trim() === '') return;
replacement = `[img]${imgUrl}[/img]`;
cursorPosition = start + replacement.length; // End of tag
break;
case 'color':
const colorInput = document.getElementById('sig-helper-bbcode-color');
const color = colorInput ? colorInput.value : '#E0E0E0';
replacement = `[color=${color}]${selectedText || 'текст'}[/color]`;
if (selectedText) {
cursorPosition = start + replacement.length; // End if text was selected
} else {
cursorPosition = start + `[color=${color}]`.length; // Inside tag if no text selected
}
break;
case 'size':
const sizeInput = document.getElementById('sig-helper-bbcode-size');
const size = sizeInput ? sizeInput.value : '4';
replacement = `[size=${size}]${selectedText || 'текст'}[/size]`;
if (selectedText) {
cursorPosition = start + replacement.length;
} else {
cursorPosition = start + `[size=${size}]`.length;
}
break;
case 'center':
case 'strike':
case 'quote':
case 'b':
case 'i':
case 'u':
default: // Standard tags like [b]...[/b]
replacement = `[${tag}]${selectedText}[/${tag}]`;
if (selectedText) {
cursorPosition = start + replacement.length;
} else {
cursorPosition = start + `[${tag}]`.length; // Position inside the tags
}
break;
}
textarea.value = beforeText + replacement + afterText;
textarea.focus();
textarea.setSelectionRange(cursorPosition, cursorPosition); // Set cursor position intelligently
textarea.dispatchEvent(new Event('input', { bubbles: true })); // Trigger updates
}
function getRandomHexColor() {
// Generate a bright, saturated color usable on a dark background
const h = Math.floor(Math.random() * 360); // Hue (0-359)
const s = Math.floor(Math.random() * 30) + 70; // Saturation (70-100%) - high saturation
const l = Math.floor(Math.random() * 20) + 60; // Lightness (60-80%) - bright but not white
// Convert HSL to RGB (standard algorithm)
const c = (1 - Math.abs(2 * l/100 - 1)) * (s/100);
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
const m = (l/100) - c/2;
let r = 0, g = 0, b = 0;
if (0 <= h && h < 60) { r = c; g = x; b = 0; }
else if (60 <= h && h < 120) { r = x; g = c; b = 0; }
else if (120 <= h && h < 180) { r = 0; g = c; b = x; }
else if (180 <= h && h < 240) { r = 0; g = x; b = c; }
else if (240 <= h && h < 300) { r = x; g = 0; b = c; }
else if (300 <= h && h < 360) { r = c; g = 0; b = x; }
r = Math.round((r + m) * 255);
g = Math.round((g + m) * 255);
b = Math.round((b + m) * 255);
// Convert RGB to Hex
const toHex = n => n.toString(16).padStart(2, '0');
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
function applyRandomLineColors() {
const textarea = modalContentInput;
const lines = textarea.value.split('\n');
const coloredLines = lines.map(line => {
const trimmedLine = line.trim();
if (trimmedLine === '') return line; // Keep empty lines as is
// Avoid re-coloring lines that already seem to have a color tag
if (/^\[color=#[0-9a-f]{6}\].*\[\/color\]$/i.test(trimmedLine)) return line;
// Remove existing color tags if they aren't wrapping the whole line
const lineWithoutColor = trimmedLine.replace(/\[color=#[0-9a-f]{6}\](.*?)\[\/color\]/gi, '$1');
return `[color=${getRandomHexColor()}]${lineWithoutColor}[/color]`;
});
textarea.value = coloredLines.join('\n');
textarea.focus();
updateGlobalStyleUI(); // Update UI in case global styles were affected
textarea.dispatchEvent(new Event('input', { bubbles: true }));
}
function applyGlobalStyle(styleType, value = null) {
const textarea = modalContentInput;
if (!textarea) return;
let currentContent = textarea.value;
let needsUpdate = false;
// Trim BOM if present
if (currentContent.charCodeAt(0) === 0xFEFF) {
currentContent = currentContent.substring(1);
}
let cleanedContent = currentContent.trim(); // Work with trimmed content initially
let prefix = currentContent.match(/^\s*/)?.[0] || ''; // Preserve leading whitespace
let suffix = currentContent.match(/\s*$/)?.[0] || ''; // Preserve trailing whitespace
let regex, tag;
let wasWrapped = false;
if (styleType === 'font') {
regex = /^\[font=([^\]]+)\]([\s\S]*)\[\/font\]$/i;
const match = cleanedContent.match(regex);
if (match) {
cleanedContent = match[2] || ''; // Content inside
wasWrapped = true; // Was wrapped
}
// Apply new font if selected, otherwise leave cleaned content
if (value && value !== '') {
cleanedContent = `[font=${value}]${cleanedContent}[/font]`;
needsUpdate = true;
} else if (wasWrapped) { // Font is removed (value='') and it was previously applied
needsUpdate = true;
}
} else if (['b', 'i', 'u'].includes(styleType)) {
tag = styleType;
regex = new RegExp(`^\\[${tag}\\]([\\s\\S]*)\\[\\/${tag}\\]$`, 'i');
const match = cleanedContent.match(regex);
if (match) { // Is wrapped, so unwrap
cleanedContent = match[1] || '';
wasWrapped = true;
needsUpdate = true;
} else { // Not wrapped, so apply wrap
cleanedContent = `[${tag}]${cleanedContent}[/${tag}]`;
needsUpdate = true;
}
}
if (needsUpdate) {
// Re-apply prefix/suffix
textarea.value = prefix + cleanedContent + suffix;
updateGlobalStyleUI(); // Update UI controls
textarea.focus();
// Move cursor to end after applying style
textarea.selectionStart = textarea.selectionEnd = textarea.value.length;
textarea.dispatchEvent(new Event('input', { bubbles: true }));
}
}
function updateGlobalStyleUI() {
const content = modalContentInput?.value.trim() || ''; // Check trimmed content for styles
// Font
const fontSelect = document.getElementById('sig-helper-global-font');
if (fontSelect) {
const fontMatch = content.match(/^\[font=([^\]]+)\]/i);
let currentFont = fontMatch ? fontMatch[1].trim() : '';
// Find the option matching the current font (case-insensitive)
const option = Array.from(fontSelect.options).find(opt => opt.value.toLowerCase() === currentFont.toLowerCase());
fontSelect.value = option ? option.value : ''; // Set to found or "По умолчанию"
}
// B/I/U buttons
['b', 'i', 'u'].forEach(tag => {
const button = document.getElementById(`sig-helper-global-${tag}`);
if (button) {
// Check if the content starts with [tag] and ends with [/tag] (case-insensitive)
const styleRegex = new RegExp(`^\\[${tag}\\][\\s\\S]*\\[\\/${tag}\\]$`, 'i');
button.classList.toggle('active', styleRegex.test(content));
}
});
}
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const context = this; // Capture context
const later = () => {
timeout = null; // Clear timeout ID
func.apply(context, args); // Execute function with original context and args
};
clearTimeout(timeout); // Clear the previous timeout
timeout = setTimeout(later, wait); // Set a new timeout
};
}
function handleModalListClick(event) {
const target = event.target;
const actionButton = target.closest('button[data-action]');
const listItem = target.closest('li[data-index]');
if (!listItem) return; // Click wasn't on a list item or its contents
const indexStr = listItem.dataset.index;
if (!indexStr) return; // No index found
const index = parseInt(indexStr, 10);
if (isNaN(index) || index < 0 || index >= appData.signatures.length) {
console.warn('Invalid index clicked:', indexStr);
return; // Invalid index
}
if (actionButton) {
// Handle button clicks (Edit, Delete, Duplicate)
const action = actionButton.dataset.action;
event.stopPropagation(); // Prevent triggering other actions if nested
if (action === 'edit') showModalForm(index);
else if (action === 'delete') deleteSignature(index);
else if (action === 'duplicate') duplicateSignature(index);
} else {
// Optional: Handle click on the list item itself (e.g., select for editing?)
// Currently, only buttons have actions.
}
}
function populateCategoryFilter() {
if (!modalCategoryFilter) return;
const currentFilterValue = modalCategoryFilter.value; // Remember current selection
modalCategoryFilter.innerHTML = ''; // Clear old options
// Add default options
const allOption = document.createElement('option'); allOption.value = 'all'; allOption.textContent = 'Все категории'; modalCategoryFilter.appendChild(allOption);
const noCategoryOption = document.createElement('option'); noCategoryOption.value = ''; noCategoryOption.textContent = 'Без категории'; modalCategoryFilter.appendChild(noCategoryOption);
// Get unique, sorted categories from signatures
const categories = [...new Set(appData.signatures.map(sig => sig.category?.trim()).filter(Boolean))] // Filter out empty/null, trim
.sort((a, b) => a.localeCompare(b, 'ru'));
// Add options for each category
categories.forEach(cat => {
const option = document.createElement('option');
option.value = cat;
option.textContent = cat;
modalCategoryFilter.appendChild(option); });
// Restore previous selection if possible, otherwise default to 'all'
modalCategoryFilter.value = currentFilterValue && modalCategoryFilter.querySelector(`option[value="${CSS.escape(currentFilterValue)}"]`) ? currentFilterValue : 'all';
currentFilterCategory = modalCategoryFilter.value; // Update state variable
}
function updateCategoryDatalist() {
const datalist = document.getElementById('sig-helper-categories'); if (!datalist) return;
datalist.innerHTML = ''; // Clear old suggestions
// Get unique, non-empty, sorted categories
const categories = [...new Set(appData.signatures.map(sig => sig.category?.trim()).filter(Boolean))]
.sort((a, b) => a.localeCompare(b, 'ru'));
categories.forEach(cat => { const option = document.createElement('option'); option.value = cat; datalist.appendChild(option); });
}
function renderModalList() {
if (!modalListElement) return;
modalListElement.innerHTML = ''; // Clear previous list
// Filter signatures based on the selected category
let filteredSignatures = appData.signatures.filter(sig => {
const sigCategory = sig.category?.trim() || '';
return currentFilterCategory === 'all' || sigCategory === currentFilterCategory;
});
// Sort the filtered signatures
filteredSignatures.sort((a, b) => {
// Helper for safe number comparison (treat null/undefined as 0)
const safeNum = (n) => n || 0;
// Helper for safe string comparison
const safeStr = (s) => s || '';
switch (currentSortType) {
case 'name_asc': return safeStr(a.name).localeCompare(safeStr(b.name), 'ru');
case 'name_desc': return safeStr(b.name).localeCompare(safeStr(a.name), 'ru');
case 'usage_desc': return safeNum(b.usageCount) - safeNum(a.usageCount);
case 'usage_asc': return safeNum(a.usageCount) - safeNum(b.usageCount);
case 'date_desc': return safeNum(b.dateAdded) - safeNum(a.dateAdded);
case 'date_asc': return safeNum(a.dateAdded) - safeNum(b.dateAdded);
case 'lastused_desc': return safeNum(b.lastUsed) - safeNum(a.lastUsed); // Nulls (never used) will be last
case 'lastused_asc': return safeNum(a.lastUsed) - safeNum(b.lastUsed); // Nulls (never used) will be first
default: return 0;
}
});
if (filteredSignatures.length === 0) {
modalListElement.innerHTML = '<li class="sig-helper-list-empty">Нет подписей, соответствующих фильтру.</li>';
return;
}
// Create list items for filtered and sorted signatures
filteredSignatures.forEach(sig => {
// Find the original index in the main appData array
const originalIndex = appData.signatures.findIndex(original => original === sig);
if (originalIndex === -1) return; // Should not happen, but safety check
const li = document.createElement('li');
li.dataset.index = originalIndex; // Store the original index
const mainInfoDiv = document.createElement('div');
mainInfoDiv.className = 'sig-helper-list-main';
const nameSpan = document.createElement('span');
nameSpan.className = 'sig-helper-list-name';
nameSpan.textContent = sig.name || 'Без имени';
mainInfoDiv.appendChild(nameSpan);
const categorySpan = document.createElement('span');
categorySpan.className = 'sig-helper-list-category';
categorySpan.textContent = sig.category?.trim() || 'Без категории';
mainInfoDiv.appendChild(categorySpan);
const usageSpan = document.createElement('span');
usageSpan.className = 'sig-helper-list-usage';
usageSpan.textContent = `[${sig.usageCount || 0}]`;
usageSpan.title = `Использовано: ${sig.usageCount || 0} раз`;
mainInfoDiv.appendChild(usageSpan);
const extraInfoDiv = document.createElement('div');
extraInfoDiv.className = 'sig-helper-list-extra';
const dateAdded = sig.dateAdded ? new Date(sig.dateAdded).toLocaleDateString('ru-RU', { day:'2-digit', month:'2-digit', year:'2-digit' }) : 'N/A';
const lastUsed = sig.lastUsed ? new Date(sig.lastUsed).toLocaleString('ru-RU', { day:'2-digit', month:'2-digit', year:'2-digit', hour:'2-digit', minute:'2-digit' }) : 'Никогда';
extraInfoDiv.textContent = `Добавлено: ${dateAdded} | Последний раз: ${lastUsed}`;
// Tooltip for the extra info could show full content or be removed if redundant
const preview = sig.content.substring(0, 150) + (sig.content.length > 150 ? '...' : '');
extraInfoDiv.title = `Содержание:\n${preview}`;
const buttonDiv = document.createElement('div');
buttonDiv.className = 'sig-helper-list-actions';
buttonDiv.append(
createModalButton('✏️', 'Редактировать', 'edit'),
createModalButton('📋', 'Дублировать', 'duplicate'),
createModalButton('❌', 'Удалить', 'delete', true) // isDanger = true
);
li.append(mainInfoDiv, extraInfoDiv, buttonDiv);
modalListElement.appendChild(li);
});
}
function createModalButton(text, title, action, isDanger = false) {
const button = document.createElement('button');
button.type = 'button';
button.innerHTML = text; // Use innerHTML for emoji icons
button.title = title;
button.className = `button button--small ${isDanger ? 'button--danger' : ''}`;
button.dataset.action = action; // Store action in data attribute
return button;
}
function showModalForm(index) {
editingIndex = index; // Store index being edited (null for new)
modalFormElement.style.display = 'flex'; // Show the form section
updateCategoryDatalist(); // Update category suggestions
const formTitle = modalFormElement.querySelector('#sig-helper-form-title');
const isEditing = index !== null && index >= 0 && index < appData.signatures.length;
if (isEditing) {
const sig = appData.signatures[index];
formTitle.textContent = 'Редактировать подпись';
modalNameInput.value = sig.name;
modalContentInput.value = sig.content;
modalCategoryInput.value = sig.category || '';
modalSaveButton.textContent = 'Сохранить изменения';
modalSaveAndNewButton.style.display = 'none'; // Hide "Save & New" when editing
// Focus last element or content area for editing flow
modalContentInput.focus();
// Optionally move cursor to end of content
modalContentInput.setSelectionRange(modalContentInput.value.length, modalContentInput.value.length);
} else {
// Adding a new signature
formTitle.textContent = 'Добавить подпись';
modalSaveButton.textContent = 'Добавить подпись';
modalSaveAndNewButton.style.display = 'inline-block'; // Show "Save & New"
// Clear form only if it wasn't just populated by 'duplicate'
if (modalNameInput.value.endsWith(' (Копия)') === false) {
modalNameInput.value = '';
modalContentInput.value = '';
modalCategoryInput.value = '';
}
modalNameInput.focus(); // Focus name field for new signature
}
updateGlobalStyleUI(); // Update B/I/U/Font buttons based on content
// Scroll the form into view if it's outside the viewport
modalFormElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
function hideModalForm() {
modalFormElement.style.display = 'none'; // Hide the form section
editingIndex = null; // Reset editing state
// Optionally clear fields on cancel, or leave them if user might reopen
// modalNameInput.value = '';
// modalContentInput.value = '';
// modalCategoryInput.value = '';
// Reset global style UI state visually
const fontSelect = document.getElementById('sig-helper-global-font'); if (fontSelect) fontSelect.value = '';
['b', 'i', 'u'].forEach(tag => { const btn = document.getElementById(`sig-helper-global-${tag}`); if (btn) btn.classList.remove('active'); });
}
function handleModalSave(keepFormOpen = false) {
const name = modalNameInput.value;
const content = modalContentInput.value;
const category = modalCategoryInput.value;
// Basic validation
if (!name?.trim()) {
alert('Название подписи не может быть пустым.');
modalNameInput.focus();
return;
}
if (!content) { // Allow empty content? For now, require something.
alert('Содержание подписи не может быть пустым.');
modalContentInput.focus();
return;
}
let success = false;
const isEditing = editingIndex !== null && editingIndex >= 0;
if (isEditing) {
success = updateSignature(editingIndex, name, content, category);
} else {
success = addSignature(name, content, category);
}
if (success) {
saveData(); // Persist changes
populateCategoryFilter(); // Update filter dropdown
renderModalList(); // Refresh the list view
populateSignatureSelect(); // Update main UI dropdown
if (!isEditing && keepFormOpen) {
// Clear form for next entry
modalNameInput.value = '';
modalContentInput.value = '';
modalCategoryInput.value = ''; // Keep category maybe? No, clear all.
editingIndex = null; // Ensure we are adding next time
const formTitle = modalFormElement.querySelector('#sig-helper-form-title');
if (formTitle) formTitle.textContent = 'Добавить подпись';
updateGlobalStyleUI(); // Reset style buttons for empty form
modalNameInput.focus(); // Focus name for next entry
} else {
hideModalForm(); // Close form on successful save/edit unless "Save & New"
}
}
// No 'else' needed, validation alerts handle failure cases
}
function openManageModal() {
if (!modalElement) createManageModal(); // Create modal if it doesn't exist
// Ensure UI elements reflect current state *before* showing
populateCategoryFilter();
renderModalList(); // Render list based on current filter/sort
modalSettingsSeparatorInput.value = appData.settings.separator;
modalSettingsDateTimeSelect.value = appData.settings.dateTimePreset;
modalSettingsAutoSendCheckbox.checked = appData.settings.autoSendAfterInsert;
// Ensure form is hidden when modal opens initially
hideModalForm();
modalElement.style.display = 'flex'; // Show the modal
// Focus filter or sort as a starting point
modalCategoryFilter.focus();
// Add class to body to prevent background scroll when modal is open
document.body.classList.add('sig-helper-modal-open');
}
function closeManageModal() {
if (modalElement) {
hideModalForm(); // Ensure form is hidden if user closes modal while form is open
modalElement.style.display = 'none'; // Hide the modal
// Remove class from body to allow background scroll again
document.body.classList.remove('sig-helper-modal-open');
}
}
function addStyles() {
GM_addStyle(`
/* Prevent background scroll when modal is open */
body.sig-helper-modal-open { overflow: hidden; }
/* Переменные */
.sig-helper-dark {
--bg-color: #2d2d2d; --text-color: #e0e0e0; --border-color: #555;
--input-bg: #3a3a3a; --input-text: #e0e0e0; --button-bg: #4a4a4a;
--button-text: #e0e0e0; --button-hover-bg: #5a5a5a;
--button-primary-bg: #007bff; --button-primary-hover-bg: #0056b3;
--button-danger-bg: #dc3545; --button-danger-hover-bg: #c82333;
--button-cta-bg: #28a745; --button-cta-hover-bg: #218838;
--modal-list-hover-bg: #3f3f3f; --modal-list-extra-color: #aaa;
--scrollbar-track-bg: #333; --scrollbar-thumb-bg: #666; --scrollbar-thumb-hover-bg: #888;
--global-style-active-bg: var(--button-primary-bg);
--global-style-active-text: white;
}
/* Основной UI */
.sig-helper-main-ui { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 10px 0; padding: 8px 10px; background-color: var(--bg-color); border: 1px solid var(--border-color); border-radius: 4px; color: var(--text-color); }
.sig-helper-main-ui select { padding: 5px 8px; border: 1px solid var(--border-color); border-radius: 3px; min-width: 150px; max-width: 250px; background-color: var(--input-bg); color: var(--input-text); cursor: pointer; flex-grow: 1; }
.sig-helper-main-ui button { padding: 5px 10px; font-size: 1em; line-height: 1.2; background-color: var(--button-bg); color: var(--button-text); border: 1px solid var(--border-color); border-radius: 3px; cursor: pointer; white-space: nowrap; transition: background-color 0.2s ease; }
.sig-helper-main-ui button:hover:not(:disabled) { background-color: var(--button-hover-bg); }
.sig-helper-main-ui select:disabled, .sig-helper-main-ui button:disabled { opacity: 0.6; cursor: not-allowed; }
.sig-helper-main-ui .button--cta { background-color: var(--button-cta-bg); border-color: var(--button-cta-bg); color: white; }
.sig-helper-main-ui .button--cta:hover:not(:disabled) { background-color: var(--button-cta-hover-bg); border-color: var(--button-cta-hover-bg); }
.sig-helper-main-ui select optgroup { font-style: italic; font-weight: bold; color: #ccc; background-color: var(--input-bg); }
.sig-helper-main-ui select option { color: var(--input-text); background-color: var(--input-bg); padding-left: 10px; }
/* Модальное окно */
.sig-helper-modal { display: none; position: fixed; z-index: 10001; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.75); justify-content: center; align-items: center; backdrop-filter: blur(2px); }
.sig-helper-modal-content { background-color: var(--bg-color); color: var(--text-color); padding: 0; border-radius: 6px; border: 1px solid var(--border-color); max-width: 850px; width: 95%; max-height: 90vh; box-shadow: 0 10px 30px rgba(0,0,0,0.5); display: flex; flex-direction: column; /* overflow: hidden; */ position: relative; } /* Removed overflow hidden from main content */
.sig-helper-modal-close { position: absolute; top: 10px; right: 12px; font-size: 28px; font-weight: bold; z-index: 10; border: none; background: none; cursor: pointer; padding: 0; line-height: 1; color: #aaa; transition: color 0.2s ease; }
.sig-helper-modal-close:hover { color: #fff; }
.sig-helper-modal-content h2 { color: var(--text-color); margin: 0; padding: 15px 25px; text-align: center; font-size: 1.3em; border-bottom: 1px solid var(--border-color); flex-shrink: 0; } /* Title fixed top */
/* Main scrollable area for list/settings */
.sig-helper-modal-main-area { display: flex; flex-direction: column; flex-grow: 1; overflow-y: auto; /* Primary scroll here */ overflow-x: hidden; /* Prevent horizontal scroll */ scrollbar-color: var(--scrollbar-thumb-bg) transparent; scrollbar-width: thin; }
.sig-helper-modal-main-area::-webkit-scrollbar { width: 8px; }
.sig-helper-modal-main-area::-webkit-scrollbar-track { background: transparent; }
.sig-helper-modal-main-area::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb-bg); border-radius: 4px;}
.sig-helper-modal-main-area::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover-bg); }
/* Controls, List, Add Button, Settings inside the main area */
.sig-helper-modal-controls { display: flex; gap: 10px; padding: 10px 20px; border-bottom: 1px solid var(--border-color); flex-shrink: 0; background-color: var(--input-bg); }
.sig-helper-modal-controls select { padding: 5px 8px; border: 1px solid var(--border-color); border-radius: 3px; background-color: var(--bg-color); color: var(--input-text); cursor: pointer; }
.sig-helper-modal-list { list-style: none; padding: 0; margin: 10px 20px; flex-shrink: 1; /* Allow list to shrink if needed, but main area grows */ border: 1px solid var(--border-color); border-radius: 4px; min-height: 150px; } /* Removed flex-grow from list itself */
.sig-helper-add-button { margin: 0 20px 15px 20px; align-self: flex-start; flex-shrink: 0; }
.sig-helper-modal-settings { padding: 10px 20px 15px 20px; border-top: 1px solid var(--border-color); background-color: var(--input-bg); flex-shrink: 0; margin-top: auto; /* Push settings towards bottom of main area */ }
/* List Item Styling */
.sig-helper-modal-list li { padding: 8px 12px; border-bottom: 1px solid var(--border-color); display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; transition: background-color 0.2s ease; gap: 10px; }
.sig-helper-modal-list li:last-child { border-bottom: none; }
.sig-helper-modal-list li:hover { background-color: var(--modal-list-hover-bg); }
.sig-helper-list-main { display: flex; align-items: baseline; gap: 8px; flex-grow: 1; min-width: 200px; flex-wrap: nowrap; overflow: hidden; }
.sig-helper-list-name { font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex-shrink: 1; }
.sig-helper-list-category { font-size: 0.85em; color: #ccc; background-color: #444; padding: 1px 5px; border-radius: 3px; white-space: nowrap; margin-left: 4px; flex-shrink: 0; }
.sig-helper-list-usage { font-size: 0.85em; color: var(--button-primary-bg); font-weight: bold; margin-left: auto; padding-left: 10px; flex-shrink: 0; }
.sig-helper-list-extra { font-size: 0.8em; color: var(--modal-list-extra-color); width: 100%; margin-top: 3px; cursor: default; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sig-helper-list-actions { display: flex; gap: 6px; flex-shrink: 0; }
.sig-helper-modal-list li.sig-helper-list-empty { padding: 15px; text-align: center; color: #aaa; font-style: italic; border: none; justify-content: center; }
/* Settings Content */
.sig-helper-modal-settings h3 { margin: 0 0 10px 0; font-size: 1.1em; color: #ccc; }
.sig-helper-modal-settings label:not(.sig-helper-settings-label-checkbox) { display: block; margin-bottom: 3px; font-size: 0.9em; color: #ccc; }
.sig-helper-settings-input { width: 100%; padding: 6px 8px; margin-bottom: 8px; border: 1px solid var(--border-color); border-radius: 3px; background-color: var(--bg-color); color: var(--input-text); font-size: 1em; font-family: inherit; box-sizing: border-box; }
.sig-helper-modal-settings textarea.sig-helper-settings-input { resize: vertical; min-height: 40px; }
.sig-helper-settings-label-checkbox { display: flex; align-items: center; margin-top: 5px; font-size: 0.95em; cursor: pointer; }
.sig-helper-settings-label-checkbox input[type="checkbox"] { margin-right: 8px; cursor: pointer; }
/* Form Styling (Appears below main area) */
.sig-helper-modal-form {
display: none; /* Initially hidden */
flex-direction: column;
gap: 10px;
padding: 15px 20px;
border-top: 1px solid var(--border-color);
background-color: var(--bg-color); /* Same as modal content */
/* max-height: 60%; /* Limit form height */
overflow-y: auto; /* Scroll form content if needed */
flex-shrink: 0; /* Prevent form from shrinking */
box-shadow: 0 -5px 15px rgba(0,0,0,0.3); /* Visual separation */
z-index: 5; /* Ensure form is above main area if overlap occurs (shouldn't) */
scrollbar-color: var(--scrollbar-thumb-bg) var(--scrollbar-track-bg);
scrollbar-width: thin;
}
.sig-helper-modal-form::-webkit-scrollbar { width: 8px; }
.sig-helper-modal-form::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb-bg); border-radius: 4px;}
.sig-helper-modal-form h3 { margin-top: 0; margin-bottom: 10px; }
.sig-helper-modal-form label { display: block; margin-bottom: 3px; font-weight: bold; color: #ccc; font-size: 0.9em; }
.sig-helper-modal-form input[type="text"],
.sig-helper-modal-form input[list], /* Style datalist input */
.sig-helper-modal-form textarea { width: 100%; padding: 8px; border: 1px solid var(--border-color); border-radius: 3px; background-color: var(--input-bg); color: var(--input-text); font-size: 1em; font-family: inherit; box-sizing: border-box; }
.sig-helper-modal-form textarea { resize: vertical; min-height: 100px; }
/* Global Styles Controls */
.sig-helper-global-style-controls { display: flex; align-items: center; gap: 15px; margin-bottom: 10px; flex-wrap: wrap; background-color: var(--input-bg); padding: 8px; border-radius: 3px; }
.sig-helper-global-style-controls label { margin-bottom: 0; display: flex; align-items: center; gap: 5px; font-size: 0.85em; }
.sig-helper-global-style-controls select { padding: 3px 6px; border: 1px solid var(--border-color); border-radius: 3px; background-color: var(--bg-color); color: var(--input-text); cursor: pointer; font-size: 0.9em; max-width: 180px; }
.sig-helper-global-style-buttons { display: flex; gap: 5px; }
.sig-helper-global-style-button { font-weight: bold; }
.sig-helper-global-style-button.active { background-color: var(--global-style-active-bg); color: var(--global-style-active-text); border-color: var(--global-style-active-bg); }
/* BBCode Toolbar */
.sig-helper-toolbar-container { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 5px; flex-wrap: wrap; }
.sig-helper-bbcode-toolbar { display: flex; gap: 5px; flex-wrap: wrap; align-items: center; flex-grow: 1; }
.sig-helper-bbcode-control { padding: 2px 4px; border: 1px solid var(--border-color); background-color: var(--input-bg); color: var(--input-text); border-radius: 3px; height: 26px; vertical-align: middle; }
.sig-helper-bbcode-colorpicker { padding: 1px; width: 30px; cursor: pointer; border: none; background: none; height: 24px; width: 24px; }
.sig-helper-toolbar-actions { display: flex; gap: 5px; flex-shrink: 0; /* Prevent action buttons wrapping unnecessarily */ }
.sig-helper-bbcode-toolbar button, .sig-helper-toolbar-actions button { background-color: var(--button-bg); color: var(--button-text); border: 1px solid var(--border-color); }
.sig-helper-bbcode-toolbar button:hover, .sig-helper-toolbar-actions button:hover { background-color: var(--button-hover-bg); }
/* Form Buttons */
.sig-helper-form-buttons { display: flex; gap: 10px; margin-top: 15px; flex-wrap: wrap; padding-top: 10px; border-top: 1px solid var(--border-color);} /* Add separator */
.sig-helper-modal-form .sig-helper-form-buttons > button { padding: 8px 15px; }
/* Common Button Styles (Small, Danger, Primary, CTA) */
.button--small { padding: 2px 6px !important; font-size: 0.9em !important; line-height: 1.4 !important; vertical-align: middle; } /* Adjusted line-height */
.button--danger { background-color: var(--button-danger-bg) !important; color: white !important; border-color: var(--button-danger-bg) !important; }
.button--danger:hover { background-color: var(--button-danger-hover-bg) !important; border-color: var(--button-danger-hover-bg) !important; }
.sig-helper-modal .button--primary { background-color: var(--button-primary-bg); border-color: var(--button-primary-bg); color: white; }
.sig-helper-modal .button--primary:hover { background-color: var(--button-primary-hover-bg); border-color: var(--button-primary-hover-bg); }
.sig-helper-modal .button--cta { background-color: var(--button-cta-bg); border-color: var(--button-cta-bg); color: white; }
.sig-helper-modal .button--cta:hover { background-color: var(--button-cta-hover-bg); border-color: var(--button-cta-hover-bg); }
`);
}
function initialize() {
loadData();
getUsername(); // Get username early if possible
addStyles(); // Inject CSS
let attempts = 0;
const checkInterval = setInterval(() => {
const currentEditor = findEditorElement();
if (currentEditor) {
clearInterval(checkInterval);
editorElement = currentEditor;
createMainUI(editorElement); // Create main UI below editor
console.log(`Signature Helper v${CURRENT_VERSION}: Инициализация завершена.`);
} else {
attempts++;
if (attempts >= MAX_EDITOR_FIND_ATTEMPTS) {
clearInterval(checkInterval);
console.warn(`Signature Helper v${CURRENT_VERSION}: Редактор не найден после ${MAX_EDITOR_FIND_ATTEMPTS} попыток.`);
}
}
}, EDITOR_FIND_INTERVAL);
}
// Wait for the page to be fully loaded or interactive before initializing
if (document.readyState === 'complete' || document.readyState === 'interactive') {
setTimeout(initialize, 350); // Small delay to ensure dynamic elements load
} else {
document.addEventListener('DOMContentLoaded', () => setTimeout(initialize, 350));
}
})();