Publish Auto Mirror Switcher 1.4.3 with installation and privacy documentation
This commit is contained in:
commit
5052f542de
33 files changed
+4912
No files matched your search
+767
@@ -0,0 +1,767 @@
|
||||
(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();
|
||||
})();
|
||||
Reference in new issue
Block a user