您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
[v6.2] UI Оптимизация. Глобальные стили (Шрифт, B/I/U). Больше BBCode. Авто-ник/дата, Категории, сортировка, счетчик, выбор формата даты. Опция авто-отправки. Темный UI. Кнопка помощи.
// ==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)); } })();