(function () { 'use strict'; var S = window.AutoMirrorShared; var I = window.AutoMirrorI18n || { t: function (_key, fallback, vars) { return String(fallback == null ? '' : fallback).replace(/\{([a-zA-Z0-9_]+)\}/g, function (match, name) { return vars && Object.prototype.hasOwnProperty.call(vars, name) ? String(vars[name]) : match; }); }, validationError: function (_error, fallback) { return fallback || ''; } }; var DEFAULT_MARKERS = S.DEFAULT_MARKERS_TEXT; var state = { enabled: true, rules: [], selectedId: null, autoSave: false }; var SECTION_STATE_KEY = 'optionsSectionState'; var autoSaveTimer = null; function $(id) { return document.getElementById(id); } function uid() { return S.uid(); } function tr(key, fallback, vars) { return I.t(key, fallback, vars); } function multclubRule() { return S.createMultclubRule(); } function emptyRule() { return S.createEmptyRule(); } function normalizeRule(rule) { return S.normalizeRule(rule); } function selectedRule() { for (var i = 0; i < state.rules.length; i += 1) { if (state.rules[i].id === state.selectedId) return state.rules[i]; } return null; } function setStatus(text) { if (!text) return; var container = $('toastContainer'); if (!container) return; var toast = document.createElement('div'); toast.className = 'toast'; toast.textContent = text; container.appendChild(toast); window.setTimeout(function () { toast.classList.add('leaving'); }, 3200); window.setTimeout(function () { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 3700); } function isAutoSaveEnabled() { var toggle = $('autoSaveEnabled'); return Boolean(toggle && toggle.checked); } function persistSettings(showMessage) { if (!saveFormToCurrentRule(false)) { if (showMessage) setStatus(tr('saveFixRule', 'Исправь ошибку в текущем правиле перед сохранением.')); return false; } state.enabled = $('globalEnabled').checked; state.autoSave = isAutoSaveEnabled(); chrome.storage.local.set({ enabled: state.enabled, rules: state.rules, autoSaveChanges: state.autoSave }, function () { if (showMessage) setStatus(tr('saved', 'Сохранено.')); renderAll(); }); return true; } function queueAutoSave() { if (!isAutoSaveEnabled()) return; window.clearTimeout(autoSaveTimer); autoSaveTimer = window.setTimeout(function () { persistSettings(false); }, 450); } function handleAutoSaveToggle() { state.autoSave = isAutoSaveEnabled(); updateAutoSaveUi(); chrome.storage.local.set({ autoSaveChanges: state.autoSave }, function () { setStatus(state.autoSave ? tr('autoSaveOn', 'Автосохранение включено.') : tr('autoSaveOff', 'Автосохранение выключено.')); if (state.autoSave) queueAutoSave(); }); } function updateAutoSaveUi() { var enabled = state.autoSave === true; var saveButton = $('saveBtn'); document.body.classList.toggle('autosave-active', enabled); if (saveButton) saveButton.disabled = enabled; } function renderRulesList() { var list = $('rulesList'); list.innerHTML = ''; if (!state.rules.length) { var empty = document.createElement('p'); empty.textContent = tr('noRules', 'Правил пока нет.'); list.appendChild(empty); return; } state.rules.forEach(function (rule) { var item = document.createElement('button'); item.type = 'button'; item.className = 'rule-item' + (rule.id === state.selectedId ? ' active' : ''); item.dataset.id = rule.id; var title = document.createElement('div'); title.className = 'rule-title'; title.textContent = (rule.enabled === false ? '⛔ ' : '✅ ') + (rule.name || 'Untitled'); var meta = document.createElement('div'); meta.className = 'rule-meta'; meta.textContent = (rule.strategy === 'list' ? tr('hostListLabel', 'список host') : rule.hostRegex) + ' · ' + rule.strategy + ' · ' + rule.mode; item.appendChild(title); item.appendChild(meta); item.addEventListener('click', function () { saveFormToCurrentRule(false); state.selectedId = rule.id; renderAll(); }); list.appendChild(item); }); } function setValue(id, value) { var element = $(id); if (!element) return; if (element.type === 'checkbox') { element.checked = Boolean(value); } else { element.value = value == null ? '' : value; } } function getValue(id) { var element = $(id); if (!element) return ''; if (element.type === 'checkbox') return element.checked; if (element.type === 'number') return Number(element.value); return element.value; } function renderForm() { var rule = selectedRule(); var disabled = !rule; Array.prototype.forEach.call($('ruleForm').elements, function (element) { element.disabled = disabled; }); if (!rule) return; setValue('ruleId', rule.id); setValue('ruleEnabled', rule.enabled !== false); setValue('name', rule.name); setValue('hostRegex', rule.hostRegex); setValue('protocol', rule.protocol); setValue('mode', rule.mode); setValue('strategy', rule.strategy); setValue('template', rule.template); setValue('includeEmpty', rule.includeEmpty === true); setValue('numberStart', rule.numberStart); setValue('numberEnd', rule.numberEnd); setValue('numberStep', rule.numberStep); setValue('padLength', rule.padLength); setValue('letterCharset', rule.letterCharset); setValue('letterMinLen', rule.letterMinLen); setValue('letterMaxLen', rule.letterMaxLen); setValue('maxCandidates', rule.maxCandidates); setValue('customHosts', rule.customHosts); setValue('timeoutMs', rule.timeoutMs); setValue('parallelChecks', rule.parallelChecks); setValue('maxRedirects', rule.maxRedirects); setValue('cacheTtlHours', rule.cacheTtlHours); setValue('preferCachedHost', rule.preferCachedHost !== false); setValue('blockedMarkers', rule.blockedMarkers || DEFAULT_MARKERS); updateStrategyBlocks(); } function replaceFirstToken(template, token, value) { return String(template || '').replace(token, value); } function firstNumberValue() { var n = Number($('numberStart').value); return Number.isFinite(n) ? n : 1; } function secondNumberValue() { var start = firstNumberValue(); var step = Math.max(1, Number($('numberStep').value) || 1); return start + step; } function padNumberForPreview(value) { var pad = Math.max(0, Number($('padLength').value) || 0); var text = String(Math.max(0, Number(value) || 0)); while (text.length < pad) text = '0' + text; return text; } function updateDynamicExamples() { var template = $('template').value.trim() || $('template').placeholder || 'example{n}.com'; var strategy = $('strategy').value; var includeEmptyHost = '—'; var includeEmptyTemplate = template; var cachedFrom = 'example1.com/page.html'; var cachedTo = 'example2.com/page.html'; if (strategy === 'number') { var n1 = padNumberForPreview(firstNumberValue()); var n2 = padNumberForPreview(secondNumberValue()); includeEmptyHost = replaceFirstToken(template, '{n}', ''); cachedFrom = replaceFirstToken(template, '{n}', n1) + '/page.html'; cachedTo = replaceFirstToken(template, '{n}', n2) + '/page.html'; } else if (strategy === 'letters') { var charset = $('letterCharset').value || 'abc'; var s1 = charset.charAt(0) || 'a'; var s2 = charset.charAt(1) || s1 + s1; includeEmptyHost = '—'; cachedFrom = replaceFirstToken(template, '{s}', s1) + '/page.html'; cachedTo = replaceFirstToken(template, '{s}', s2) + '/page.html'; } else { var hosts = ($('customHosts').value || '') .split(/[\s,]+/) .map(function (host) { return host.trim(); }) .filter(Boolean); includeEmptyHost = '—'; includeEmptyTemplate = tr('hostListLabel', 'список host'); cachedFrom = (hosts[0] || 'mirror1.example.com') + '/page.html'; cachedTo = (hosts[1] || 'mirror2.example.com') + '/page.html'; } if ($('includeEmptyHostExample')) $('includeEmptyHostExample').textContent = includeEmptyHost || '—'; if ($('includeEmptyTemplateExample')) $('includeEmptyTemplateExample').textContent = includeEmptyTemplate || '—'; if ($('cachedFromExample')) $('cachedFromExample').textContent = cachedFrom; if ($('cachedToExample')) $('cachedToExample').textContent = cachedTo; } function updateStrategyBlocks() { var strategy = $('strategy').value; $('numberBlock').classList.toggle('hidden', strategy !== 'number'); $('lettersBlock').classList.toggle('hidden', strategy !== 'letters'); $('listBlock').classList.toggle('hidden', strategy !== 'list'); $('hostRegexBlock').classList.toggle('hidden', strategy === 'list'); document.querySelector('.templateBlock').classList.toggle('hidden', strategy === 'list'); updateDynamicExamples(); } function readRuleFromForm() { return { id: getValue('ruleId') || uid(), name: getValue('name').trim() || tr('untitledRule', 'Untitled rule'), enabled: getValue('ruleEnabled'), hostRegex: getValue('strategy') === 'list' ? '' : getValue('hostRegex').trim(), protocol: getValue('protocol'), mode: getValue('mode'), strategy: getValue('strategy'), template: getValue('template').trim(), includeEmpty: getValue('includeEmpty'), numberStart: getValue('numberStart'), numberEnd: getValue('numberEnd'), numberStep: Math.max(1, getValue('numberStep') || 1), padLength: Math.max(0, getValue('padLength') || 0), letterCharset: getValue('letterCharset'), letterMinLen: Math.max(1, getValue('letterMinLen') || 1), letterMaxLen: Math.max(1, getValue('letterMaxLen') || 1), maxCandidates: Math.max(1, getValue('maxCandidates') || 1), customHosts: getValue('customHosts'), timeoutMs: Math.max(500, getValue('timeoutMs') || 3000), parallelChecks: Math.max(1, getValue('parallelChecks') || 8), maxRedirects: Math.max(1, getValue('maxRedirects') || 220), cacheTtlHours: Math.max(0, getValue('cacheTtlHours') || 0), preferCachedHost: getValue('preferCachedHost'), blockedMarkers: getValue('blockedMarkers') || DEFAULT_MARKERS }; } function validateRule(rule) { var error = S.validateRuleCode ? S.validateRuleCode(rule) : null; if (!error) return S.validateRule(rule); return I.validationError(error, error.message); } function saveFormToCurrentRule(showMessage) { var current = selectedRule(); if (!current) return true; var updated = readRuleFromForm(); var error = validateRule(updated); if (error) { if (showMessage) setStatus(error); return false; } for (var i = 0; i < state.rules.length; i += 1) { if (state.rules[i].id === current.id) { state.rules[i] = updated; break; } } if (showMessage) { setStatus(isAutoSaveEnabled() ? tr('ruleAppliedAuto', 'Правило применено и сохранится автоматически.') : tr('ruleAppliedManual', 'Правило применено. Не забудь нажать «Сохранить».')); } renderRulesList(); return true; } function renderAll() { $('globalEnabled').checked = state.enabled !== false; if ($('autoSaveEnabled')) $('autoSaveEnabled').checked = state.autoSave === true; updateAutoSaveUi(); renderRulesList(); renderForm(); } function load() { chrome.storage.local.get({ enabled: true, rules: [], autoSaveChanges: false }, function (items) { state.enabled = items.enabled !== false; state.autoSave = items.autoSaveChanges === true; state.rules = Array.isArray(items.rules) ? items.rules.map(normalizeRule) : []; if (!state.rules.length) state.rules.push(multclubRule()); state.selectedId = state.rules[0].id; renderAll(); }); } function saveAll() { persistSettings(true); } function addRule(rule) { saveFormToCurrentRule(false); state.rules.push(rule); state.selectedId = rule.id; renderAll(); setStatus(isAutoSaveEnabled() ? tr('ruleAddedAuto', 'Правило добавлено и сохранится автоматически.') : tr('ruleAddedManual', 'Правило добавлено. Не забудь нажать «Сохранить».')); queueAutoSave(); } function getCacheKey(rule) { return S.getCacheKey(rule); } function clearCurrentRuleCache() { var current = selectedRule(); if (!current) return; chrome.storage.local.remove(getCacheKey(current), function () { setStatus(tr('cacheCleared', 'Кэш последнего рабочего host очищен для правила «{name}».', { name: current.name || current.id })); }); } function deleteCurrentRule() { var current = selectedRule(); if (!current) return; if (!confirm(tr('confirmDeleteRule', 'Удалить правило «{name}»?', { name: current.name || current.id }))) return; state.rules = state.rules.filter(function (rule) { return rule.id !== current.id; }); state.selectedId = state.rules.length ? state.rules[0].id : null; renderAll(); setStatus(isAutoSaveEnabled() ? tr('ruleDeletedAuto', 'Правило удалено и сохранится автоматически.') : tr('ruleDeletedManual', 'Правило удалено. Не забудь нажать «Сохранить».')); queueAutoSave(); } function duplicateCurrentRule() { var current = selectedRule(); if (!current) return; var copy = Object.assign({}, readRuleFromForm()); copy.id = uid(); copy.name = copy.name + ' copy'; addRule(copy); } function safeClone(value) { return JSON.parse(JSON.stringify(value)); } function normalizeImportedRules(rawRules) { if (!Array.isArray(rawRules)) { throw new Error(tr('importNoRulesArray', 'В файле не найден массив rules.')); } var seenIds = Object.create(null); var rules = rawRules.map(function (rawRule, index) { var rule = normalizeRule(rawRule); var error = validateRule(rule); if (error) { throw new Error(tr('importRuleError', 'Ошибка в правиле #{index} «{name}»: {error}', { index: index + 1, name: rule.name || rule.id || 'Untitled', error: error })); } if (!rule.id || seenIds[rule.id]) { rule.id = uid(); } seenIds[rule.id] = true; return rule; }); if (!rules.length) { throw new Error(tr('importNoRules', 'В импортируемом файле нет правил.')); } return rules; } function buildExportPayload() { if (!saveFormToCurrentRule(false)) return null; state.enabled = $('globalEnabled').checked; return { app: 'Auto Mirror Switcher', formatVersion: 1, extensionVersion: chrome.runtime && chrome.runtime.getManifest ? chrome.runtime.getManifest().version : 'unknown', exportedAt: new Date().toISOString(), enabled: state.enabled !== false, rules: safeClone(state.rules) }; } function makeExportFileName() { var stamp = new Date().toISOString() .replace(/[-:]/g, '') .replace(/\.\d{3}Z$/, 'Z') .replace('T', '-'); return 'auto-mirror-switcher-settings-' + stamp + '.json'; } function downloadTextFile(filename, text) { var blob = new Blob([text], { type: 'application/json;charset=utf-8' }); var url = URL.createObjectURL(blob); var link = document.createElement('a'); link.href = url; link.download = filename; link.style.display = 'none'; document.body.appendChild(link); link.click(); window.setTimeout(function () { URL.revokeObjectURL(url); if (link.parentNode) link.parentNode.removeChild(link); }, 1000); } function exportSettings() { var payload = buildExportPayload(); if (!payload) { setStatus(tr('exportCanceled', 'Экспорт отменён: сначала исправь ошибку в текущем правиле.')); return; } downloadTextFile(makeExportFileName(), JSON.stringify(payload, null, 2)); setStatus(tr('settingsExported', 'Настройки экспортированы в JSON-файл.')); } function extractImportPayload(data) { if (Array.isArray(data)) { return { enabled: true, rules: data }; } if (!data || typeof data !== 'object') { throw new Error(tr('importInvalidObject', 'Файл должен содержать JSON-объект с полем rules.')); } if (data.app && data.app !== 'Auto Mirror Switcher') { var ok = confirm(tr('importWrongAppConfirm', 'Файл не похож на экспорт Auto Mirror Switcher. Всё равно попробовать импортировать?')); if (!ok) throw new Error(tr('importCanceled', 'Импорт отменён.')); } if (Array.isArray(data.rules)) { return { enabled: data.enabled !== false, rules: data.rules }; } if (data.settings && Array.isArray(data.settings.rules)) { return { enabled: data.settings.enabled !== false, rules: data.settings.rules }; } throw new Error(tr('importNoRulesArray', 'В файле не найден массив rules.')); } function persistImportedSettings(enabled, rules) { state.enabled = enabled !== false; state.rules = rules; state.selectedId = state.rules[0] ? state.rules[0].id : null; chrome.storage.local.set({ enabled: state.enabled, rules: state.rules }, function () { renderAll(); setStatus(tr('importedSavedCount', 'Импортировано и сохранено правил: {count}.', { count: state.rules.length })); }); } function importSettingsObject(data) { var payload = extractImportPayload(data); var importedRules = normalizeImportedRules(payload.rules); var message = tr('importReplaceConfirm', 'Импорт заменит текущие правила ({current}) на правила из файла ({incoming}). Продолжить?', { current: state.rules.length, incoming: importedRules.length }); if (!confirm(message)) { setStatus(tr('importCanceled', 'Импорт отменён.')); return; } persistImportedSettings(payload.enabled, importedRules); } function importSettingsFromFile(file) { if (!file) return; var reader = new FileReader(); reader.onload = function () { try { var data = JSON.parse(String(reader.result || '')); importSettingsObject(data); } catch (e) { setStatus(tr('importError', 'Ошибка импорта: {message}', { message: e && e.message ? e.message : String(e) })); } finally { $('importFileInput').value = ''; } }; reader.onerror = function () { setStatus(tr('importReadError', 'Ошибка импорта: не удалось прочитать файл.')); $('importFileInput').value = ''; }; reader.readAsText(file, 'utf-8'); } function formatDebugLogs(logs) { if (!Array.isArray(logs) || !logs.length) { return tr('logEmpty', 'Журнал пуст.'); } return logs.map(function (entry, index) { return JSON.stringify({ index: index + 1, time: entry.time, level: entry.level, event: entry.event, data: entry.data || {} }, null, 2); }).join('\n---\n'); } function getDebugLogs(callback) { chrome.storage.local.get({ debugLogs: [] }, function (items) { callback(Array.isArray(items.debugLogs) ? items.debugLogs : []); }); } function refreshDebugLog() { getDebugLogs(function (logs) { var output = $('debugLogOutput'); if (output) { output.value = formatDebugLogs(logs); output.scrollTop = output.scrollHeight; } setStatus(tr('logUpdated', 'Журнал обновлён. Событий: {count}.', { count: logs.length })); }); } function makeDebugLogFileName() { var stamp = new Date().toISOString() .replace(/[-:]/g, '') .replace(/\.\d{3}Z$/, 'Z') .replace('T', '-'); return 'auto-mirror-switcher-debug-log-' + stamp + '.json'; } function exportDebugLog() { getDebugLogs(function (logs) { var payload = { app: 'Auto Mirror Switcher', type: 'debug-log', extensionVersion: chrome.runtime && chrome.runtime.getManifest ? chrome.runtime.getManifest().version : 'unknown', exportedAt: new Date().toISOString(), logs: logs }; downloadTextFile(makeDebugLogFileName(), JSON.stringify(payload, null, 2)); setStatus(tr('logExported', 'Журнал экспортирован. Событий: {count}.', { count: logs.length })); }); } function clearDebugLog() { if (!confirm(tr('confirmClearLog', 'Очистить журнал диагностики?'))) return; chrome.storage.local.set({ debugLogs: [] }, function () { if ($('debugLogOutput')) $('debugLogOutput').value = tr('logEmpty', 'Журнал пуст.'); setStatus(tr('logCleared', 'Журнал диагностики очищен.')); }); } function copyDebugLog() { var text = $('debugLogOutput') ? $('debugLogOutput').value : ''; if (!text || text === tr('logEmpty', 'Журнал пуст.')) { refreshDebugLog(); text = $('debugLogOutput') ? $('debugLogOutput').value : ''; } function fallbackCopy() { var output = $('debugLogOutput'); if (!output) return; output.focus(); output.select(); try { document.execCommand('copy'); setStatus(tr('logCopied', 'Журнал скопирован.')); } catch (e) { setStatus(tr('logCopyManual', 'Не удалось скопировать журнал автоматически. Выдели текст вручную.')); } } if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(function () { setStatus(tr('logCopied', 'Журнал скопирован.')); }).catch(fallbackCopy); } else { fallbackCopy(); } } function setupPersistentSection(id, key, defaultOpen) { var section = $(id); if (!section) return; chrome.storage.local.get({ [SECTION_STATE_KEY]: {} }, function (items) { var saved = items[SECTION_STATE_KEY] || {}; section.open = typeof saved[key] === 'boolean' ? saved[key] : defaultOpen === true; }); section.addEventListener('toggle', function () { chrome.storage.local.get({ [SECTION_STATE_KEY]: {} }, function (items) { var saved = items[SECTION_STATE_KEY] || {}; saved[key] = section.open; var payload = {}; payload[SECTION_STATE_KEY] = saved; chrome.storage.local.set(payload); }); }); } $('strategy').addEventListener('change', updateStrategyBlocks); ['template', 'numberStart', 'numberStep', 'padLength', 'letterCharset', 'customHosts'].forEach(function (id) { var element = $(id); if (element) element.addEventListener('input', updateDynamicExamples); }); $('globalEnabled').addEventListener('change', queueAutoSave); $('autoSaveEnabled').addEventListener('change', handleAutoSaveToggle); $('ruleForm').addEventListener('input', queueAutoSave); $('ruleForm').addEventListener('change', queueAutoSave); $('ruleForm').addEventListener('submit', function (event) { event.preventDefault(); if (saveFormToCurrentRule(true)) queueAutoSave(); }); $('addRuleBtn').addEventListener('click', function () { addRule(emptyRule()); }); $('addMultclubBtn').addEventListener('click', function () { addRule(multclubRule()); }); $('exportSettingsBtn').addEventListener('click', exportSettings); $('importSettingsBtn').addEventListener('click', function () { $('importFileInput').click(); }); $('importFileInput').addEventListener('change', function () { importSettingsFromFile(this.files && this.files[0]); }); $('saveBtn').addEventListener('click', saveAll); $('deleteRuleBtn').addEventListener('click', deleteCurrentRule); $('duplicateRuleBtn').addEventListener('click', duplicateCurrentRule); $('clearRuleCacheBtn').addEventListener('click', clearCurrentRuleCache); $('refreshLogBtn').addEventListener('click', refreshDebugLog); $('copyLogBtn').addEventListener('click', copyDebugLog); $('exportLogBtn').addEventListener('click', exportDebugLog); $('clearLogBtn').addEventListener('click', clearDebugLog); setupPersistentSection('guideSection', 'guideOpen', false); setupPersistentSection('debugLogSection', 'debugLogOpen', false); load(); refreshDebugLog(); })();