Files

768 lines
28 KiB
JavaScript

(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();
})();