768 lines
28 KiB
JavaScript
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();
|
|
})();
|