caps_enhancer/js/options.js

566 lines
25 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ==================== СКРИПТ СТРАНИЦЫ НАСТРОЕК ====================
function getStr(path) {
const o = window.CAPS_STRINGS || {};
const val = path.split('.').reduce((acc, k) => acc && acc[k], o);
return (val !== undefined && val !== null) ? val : path;
}
function applyOptionsStrings() {
document.querySelectorAll('[data-i18n]').forEach((node) => {
const key = node.getAttribute('data-i18n');
const text = getStr(key);
if (text === '') {
node.style.display = 'none';
} else if (text) {
node.textContent = text;
}
});
document.querySelectorAll('[data-i18n-placeholder]').forEach((node) => {
const key = node.getAttribute('data-i18n-placeholder');
const text = getStr(key);
if (text !== undefined && text !== null) node.placeholder = text;
});
const pageTitle = getStr('options.pageTitle');
if (pageTitle) document.title = pageTitle;
const versionEl = document.getElementById('app-version');
if (versionEl && typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.getManifest) {
const manifest = chrome.runtime.getManifest();
if (manifest && manifest.version) versionEl.textContent = manifest.version;
}
}
// Значения по умолчанию
const DEFAULT_CONFIG = {
debug: {
enabled: true,
modules: {
core: true, video: true, mnemo: true,
close: true, content: true, novnc: true, autologin: true
}
},
ports: { video: '5000', scheme: '8080' },
novnc: { mode: 'button', redirectDelay: 500 },
mnemo: { mode: 'host-port', skipOnDomain: false, addVideoButton: true, addSchemeButton: true },
autologin: {
enabled: false,
profilePage1: 0,
profilePage2: 1,
subnetRules: [],
profiles: [
{ name: 'Профиль 1', username: '', password: '', autoSubmit: true, delay: 500 }
]
},
videoWindow: { ipButtonMode: 'button' },
disabledSites: [],
replaceLogo: true,
uiVisible: true
};
// Состояние страницы
let currentProfiles = [];
let currentSubnetRules = [];
// Элементы формы
const elements = {
portVideo: document.getElementById('port-video'),
portScheme: document.getElementById('port-scheme'),
novncRedirect: document.getElementById('novnc-redirect'),
redirectDelay: document.getElementById('redirect-delay'),
redirectDelayContainer: document.getElementById('redirect-delay-container'),
mnemoMode: document.getElementById('mnemo-mode'),
mnemoSkipOnDomain: document.getElementById('mnemo-skip-on-domain'),
mnemoAddSchemeButton: document.getElementById('mnemo-add-scheme-button'),
mnemoAddVideoButton: document.getElementById('mnemo-add-video-button'),
videoIpButtonMode: document.getElementById('video-ip-button-mode'),
replaceLogo: document.getElementById('replace-logo'),
uiVisible: document.getElementById('ui-visible'),
autologinEnabled: document.getElementById('autologin-enabled'),
autologinSettings: document.getElementById('autologin-settings'),
autologinPage1Profile: document.getElementById('autologin-page1-profile'),
autologinPage2Profile: document.getElementById('autologin-page2-profile'),
autologinProfilesList: document.getElementById('autologin-profiles-list'),
autologinAddProfile: document.getElementById('autologin-add-profile'),
autologinSubnetRules: document.getElementById('autologin-subnet-rules'),
autologinAddSubnetRule: document.getElementById('autologin-add-subnet-rule'),
debugEnabled: document.getElementById('debug-enabled'),
debugModules: document.getElementById('debug-modules'),
debugCore: document.getElementById('debug-core'),
debugVideo: document.getElementById('debug-video'),
debugMnemo: document.getElementById('debug-mnemo'),
debugClose: document.getElementById('debug-close'),
debugContent: document.getElementById('debug-content'),
debugNovnc: document.getElementById('debug-novnc'),
debugAutologin: document.getElementById('debug-autologin'),
saveBtn: document.getElementById('save-btn'),
resetBtn: document.getElementById('reset-btn'),
status: document.getElementById('status')
};
// ---- Рендеринг профилей ----
function renderProfiles() {
const container = elements.autologinProfilesList;
if (!container) return;
container.innerHTML = '';
currentProfiles.forEach((profile, idx) => {
const card = document.createElement('div');
card.style.cssText = 'border:1px solid #e2e4ea;border-radius:8px;padding:10px 12px;margin-bottom:8px;background:#fff;';
const header = document.createElement('div');
header.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;';
const nameInput = document.createElement('input');
nameInput.type = 'text';
nameInput.value = profile.name || '';
nameInput.placeholder = getStr('options.autologinProfileNamePlaceholder');
nameInput.style.cssText = 'font-weight:600;border:none;border-bottom:1px solid #ccc;border-radius:0;padding:2px 4px;width:auto;font-size:13px;flex:1;';
nameInput.addEventListener('input', () => {
currentProfiles[idx].name = nameInput.value;
rebuildProfileSelects();
});
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.textContent = getStr('options.autologinRemoveProfile');
removeBtn.title = 'Удалить профиль';
removeBtn.style.cssText = 'border:none;background:none;color:#e53935;cursor:pointer;font-size:16px;padding:0 4px;margin-left:8px;';
removeBtn.disabled = currentProfiles.length <= 1;
removeBtn.addEventListener('click', () => {
if (currentProfiles.length <= 1) return;
currentProfiles.splice(idx, 1);
// Корректируем индексы выбранных профилей
const p1 = parseInt(elements.autologinPage1Profile.value) || 0;
const p2 = parseInt(elements.autologinPage2Profile.value) || 0;
elements.autologinPage1Profile.dataset.value = Math.min(p1, currentProfiles.length - 1);
elements.autologinPage2Profile.dataset.value = Math.min(p2, currentProfiles.length - 1);
renderProfiles();
renderSubnetRules();
rebuildProfileSelects();
});
header.appendChild(nameInput);
header.appendChild(removeBtn);
const fields = document.createElement('div');
fields.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:8px;';
fields.appendChild(makeField(
getStr('options.autologinUsernameLabel'),
'text', profile.username, getStr('options.autologinUsernamePlaceholder'), 'off',
(v) => { currentProfiles[idx].username = v; }
));
const pwdField = makeField(
getStr('options.autologinPasswordLabel'),
'text', profile.password, '••••••••', 'off',
(v) => { currentProfiles[idx].password = v; }
);
pwdField.querySelector('input').classList.add('password-field');
fields.appendChild(pwdField);
const autoSubmitWrap = document.createElement('div');
autoSubmitWrap.style.cssText = 'display:flex;align-items:center;gap:8px;font-size:13px;';
const cbLabel = document.createElement('label');
cbLabel.style.cssText = 'display:flex;align-items:center;gap:6px;cursor:pointer;';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.checked = profile.autoSubmit !== false;
cb.addEventListener('change', () => { currentProfiles[idx].autoSubmit = cb.checked; });
cbLabel.appendChild(cb);
cbLabel.appendChild(document.createTextNode(getStr('options.autologinAutoSubmitLabel')));
autoSubmitWrap.appendChild(cbLabel);
const delayWrap = makeField(
getStr('options.autologinDelayLabel'),
'number', profile.delay || 500, '500', 'off',
(v) => { currentProfiles[idx].delay = parseInt(v) || 500; }
);
delayWrap.querySelector('input').min = '0';
delayWrap.querySelector('input').max = '5000';
delayWrap.querySelector('input').step = '100';
const hint = document.createElement('small');
hint.className = 'hint';
hint.style.gridColumn = '1/-1';
hint.textContent = getStr('options.autologinPasswordHint');
fields.appendChild(autoSubmitWrap);
fields.appendChild(delayWrap);
fields.appendChild(hint);
card.appendChild(header);
card.appendChild(fields);
container.appendChild(card);
});
}
function makeField(label, type, value, placeholder, autocomplete, onChange) {
const wrap = document.createElement('div');
const lbl = document.createElement('label');
lbl.style.cssText = 'font-size:12px;color:#555;display:block;margin-bottom:3px;';
lbl.textContent = label;
const inp = document.createElement('input');
inp.type = type;
inp.value = value !== undefined ? value : '';
inp.placeholder = placeholder || '';
if (autocomplete) inp.autocomplete = autocomplete;
inp.style.cssText = 'width:100%;padding:5px 8px;border:1px solid #ddd;border-radius:6px;font-size:13px;';
inp.addEventListener('input', () => onChange(inp.value));
wrap.appendChild(lbl);
wrap.appendChild(inp);
return wrap;
}
function rebuildProfileSelects() {
const saved1 = parseInt(elements.autologinPage1Profile.value);
const saved2 = parseInt(elements.autologinPage2Profile.value);
[elements.autologinPage1Profile, elements.autologinPage2Profile].forEach((sel, selIdx) => {
const prev = selIdx === 0 ? (isNaN(saved1) ? 0 : saved1) : (isNaN(saved2) ? 1 : saved2);
sel.innerHTML = '';
currentProfiles.forEach((p, idx) => {
const opt = document.createElement('option');
opt.value = idx;
opt.textContent = p.name || `Профиль ${idx + 1}`;
sel.appendChild(opt);
});
sel.value = Math.min(prev, currentProfiles.length - 1);
});
// Тоже обновляем selects в правилах подсетей
document.querySelectorAll('.subnet-rule-profile-select').forEach((sel) => {
const prev = parseInt(sel.value);
sel.innerHTML = '';
currentProfiles.forEach((p, idx) => {
const opt = document.createElement('option');
opt.value = idx;
opt.textContent = p.name || `Профиль ${idx + 1}`;
sel.appendChild(opt);
});
sel.value = isNaN(prev) ? 0 : Math.min(prev, currentProfiles.length - 1);
});
}
// ---- Рендеринг правил подсетей ----
function renderSubnetRules() {
const container = elements.autologinSubnetRules;
if (!container) return;
container.innerHTML = '';
if (currentSubnetRules.length === 0) {
const empty = document.createElement('p');
empty.style.cssText = 'font-size:12px;color:#aaa;margin-bottom:4px;';
empty.textContent = 'Нет правил';
container.appendChild(empty);
return;
}
currentSubnetRules.forEach((rule, idx) => {
const row = document.createElement('div');
row.style.cssText = 'display:flex;align-items:center;gap:6px;margin-bottom:6px;flex-wrap:wrap;';
const subnetInput = document.createElement('input');
subnetInput.type = 'text';
subnetInput.value = rule.subnet || '';
subnetInput.placeholder = getStr('options.autologinSubnetPlaceholder');
subnetInput.style.cssText = 'width:130px;padding:5px 8px;border:1px solid #ddd;border-radius:6px;font-size:13px;font-family:monospace;';
subnetInput.addEventListener('input', () => { currentSubnetRules[idx].subnet = subnetInput.value; });
const actionSelect = document.createElement('select');
actionSelect.style.cssText = 'padding:5px 8px;border:1px solid #ddd;border-radius:6px;font-size:13px;';
const optSkip = document.createElement('option');
optSkip.value = 'skip';
optSkip.textContent = getStr('options.autologinSubnetActionSkip');
const optProfile = document.createElement('option');
optProfile.value = 'profile';
optProfile.textContent = getStr('options.autologinSubnetActionProfile');
actionSelect.appendChild(optSkip);
actionSelect.appendChild(optProfile);
actionSelect.value = rule.action || 'skip';
const profileSelect = document.createElement('select');
profileSelect.className = 'subnet-rule-profile-select';
profileSelect.style.cssText = 'padding:5px 8px;border:1px solid #ddd;border-radius:6px;font-size:13px;';
currentProfiles.forEach((p, pi) => {
const opt = document.createElement('option');
opt.value = pi;
opt.textContent = p.name || `Профиль ${pi + 1}`;
profileSelect.appendChild(opt);
});
profileSelect.value = rule.profileIndex || 0;
profileSelect.style.display = rule.action === 'profile' ? '' : 'none';
actionSelect.addEventListener('change', () => {
currentSubnetRules[idx].action = actionSelect.value;
profileSelect.style.display = actionSelect.value === 'profile' ? '' : 'none';
});
profileSelect.addEventListener('change', () => {
currentSubnetRules[idx].profileIndex = parseInt(profileSelect.value);
});
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.textContent = getStr('options.autologinRemoveSubnetRule');
removeBtn.title = 'Удалить правило';
removeBtn.style.cssText = 'border:none;background:none;color:#e53935;cursor:pointer;font-size:16px;padding:0 4px;';
removeBtn.addEventListener('click', () => {
currentSubnetRules.splice(idx, 1);
renderSubnetRules();
});
row.appendChild(subnetInput);
row.appendChild(actionSelect);
row.appendChild(profileSelect);
row.appendChild(removeBtn);
container.appendChild(row);
});
}
// ---- Загрузка настроек ----
document.addEventListener('DOMContentLoaded', () => {
applyOptionsStrings();
loadSettings();
});
elements.saveBtn.addEventListener('click', saveSettings);
elements.resetBtn.addEventListener('click', resetSettings);
elements.debugEnabled.addEventListener('change', toggleDebugModules);
elements.novncRedirect.addEventListener('change', toggleRedirectDelay);
elements.autologinEnabled.addEventListener('change', toggleAutologinSettings);
elements.autologinAddProfile.addEventListener('click', () => {
currentProfiles.push({ name: `Профиль ${currentProfiles.length + 1}`, username: '', password: '', autoSubmit: true, delay: 500 });
renderProfiles();
rebuildProfileSelects();
});
elements.autologinAddSubnetRule.addEventListener('click', () => {
currentSubnetRules.push({ subnet: '', action: 'skip', profileIndex: 0 });
renderSubnetRules();
});
function loadSettings() {
chrome.storage.sync.get(['config'], (result) => {
if (chrome.runtime.lastError) {
showStatus(getStr('options.statusLoadError') + ' ' + chrome.runtime.lastError.message, 'error');
return;
}
const config = result.config || DEFAULT_CONFIG;
// Порты
elements.portVideo.value = config.ports?.video || DEFAULT_CONFIG.ports.video;
elements.portScheme.value = config.ports?.scheme || DEFAULT_CONFIG.ports.scheme;
// Видео окна
elements.videoIpButtonMode.value = config.videoWindow?.ipButtonMode || DEFAULT_CONFIG.videoWindow.ipButtonMode;
// noVNC
elements.novncRedirect.checked = (config.novnc?.mode || DEFAULT_CONFIG.novnc.mode) === 'redirect';
elements.redirectDelay.value = config.novnc?.redirectDelay || DEFAULT_CONFIG.novnc.redirectDelay;
// Мнемосхема
const mnemoMode = config.mnemo?.mode;
elements.mnemoMode.value = (mnemoMode === 'ip-scheme' ? 'host-scheme' : (mnemoMode === 'skip-on-domain' || mnemoMode === 'default' ? 'host-port' : (mnemoMode || 'host-port')));
elements.mnemoSkipOnDomain.checked = config.mnemo?.skipOnDomain === true || mnemoMode === 'skip-on-domain';
elements.mnemoAddSchemeButton.checked = config.mnemo?.addSchemeButton !== false;
elements.mnemoAddVideoButton.checked = config.mnemo?.addVideoButton !== false;
// Внешний вид
elements.replaceLogo.checked = config.replaceLogo !== false;
elements.uiVisible.checked = config.uiVisible !== false;
// Автологин — загружаем профили (с миграцией старого формата)
const al = config.autologin || {};
if (Array.isArray(al.profiles) && al.profiles.length > 0) {
currentProfiles = al.profiles.map(p => ({ ...p }));
} else if (al.username || al.password) {
// Миграция старого формата
currentProfiles = [
{ name: 'Профиль 1', username: al.username || '', password: al.password || '', autoSubmit: al.autoSubmit !== false, delay: al.delay || 500 }
];
if (al.username2 || al.password2) {
currentProfiles.push({ name: 'Профиль 2', username: al.username2 || '', password: al.password2 || '', autoSubmit: true, delay: 500 });
}
} else {
currentProfiles = [{ name: 'Профиль 1', username: '', password: '', autoSubmit: true, delay: 500 }];
}
currentSubnetRules = Array.isArray(al.subnetRules) ? al.subnetRules.map(r => ({ ...r })) : [];
elements.autologinEnabled.checked = al.enabled || false;
renderProfiles();
renderSubnetRules();
rebuildProfileSelects();
// Выставляем выбранные профили
const p1 = typeof al.profilePage1 === 'number' ? al.profilePage1 : 0;
const p2 = typeof al.profilePage2 === 'number' ? al.profilePage2 : Math.min(1, currentProfiles.length - 1);
elements.autologinPage1Profile.value = Math.min(p1, currentProfiles.length - 1);
elements.autologinPage2Profile.value = Math.min(p2, currentProfiles.length - 1);
// Отладка
elements.debugEnabled.checked = config.debug?.enabled !== false;
elements.debugCore.checked = config.debug?.modules?.core !== false;
elements.debugVideo.checked = config.debug?.modules?.video !== false;
elements.debugMnemo.checked = config.debug?.modules?.mnemo !== false;
elements.debugClose.checked = config.debug?.modules?.close !== false;
elements.debugContent.checked = config.debug?.modules?.content !== false;
elements.debugNovnc.checked = config.debug?.modules?.novnc !== false;
elements.debugAutologin.checked = config.debug?.modules?.autologin !== false;
toggleDebugModules();
toggleRedirectDelay();
toggleAutologinSettings();
});
}
// ---- Сохранение настроек ----
function saveSettings() {
const portVideo = elements.portVideo.value.trim();
const portScheme = elements.portScheme.value.trim();
if (!portVideo || isNaN(portVideo) || portVideo < 1 || portVideo > 65535) {
showStatus(getStr('options.errorPortVideo'), 'error');
return;
}
if (!portScheme || isNaN(portScheme) || portScheme < 1 || portScheme > 65535) {
showStatus(getStr('options.errorPortScheme'), 'error');
return;
}
const config = {
debug: {
enabled: elements.debugEnabled.checked,
modules: {
core: elements.debugCore.checked,
video: elements.debugVideo.checked,
mnemo: elements.debugMnemo.checked,
close: elements.debugClose.checked,
content: elements.debugContent.checked,
novnc: elements.debugNovnc.checked,
autologin: elements.debugAutologin.checked
}
},
ports: { video: portVideo, scheme: portScheme },
videoWindow: {
ipButtonMode: elements.videoIpButtonMode.value
},
novnc: {
mode: elements.novncRedirect.checked ? 'redirect' : 'button',
redirectDelay: parseInt(elements.redirectDelay.value) || 500
},
mnemo: {
mode: elements.mnemoMode.value,
skipOnDomain: elements.mnemoSkipOnDomain.checked,
addVideoButton: elements.mnemoAddVideoButton.checked,
addSchemeButton: elements.mnemoAddSchemeButton.checked
},
replaceLogo: elements.replaceLogo.checked,
uiVisible: elements.uiVisible.checked,
autologin: {
enabled: elements.autologinEnabled.checked,
profilePage1: parseInt(elements.autologinPage1Profile.value) || 0,
profilePage2: parseInt(elements.autologinPage2Profile.value) || 0,
profiles: currentProfiles.map(p => ({ ...p })),
subnetRules: currentSubnetRules.map(r => ({ ...r }))
}
};
chrome.storage.sync.get(['config'], (prev) => {
if (prev.config && Array.isArray(prev.config.disabledSites)) {
config.disabledSites = prev.config.disabledSites;
} else {
config.disabledSites = [];
}
chrome.storage.sync.set({ config }, () => {
if (chrome.runtime.lastError) {
showStatus(getStr('options.statusSaveError') + ' ' + chrome.runtime.lastError.message, 'error');
return;
}
showStatus(getStr('options.statusSaveOk'), 'success');
chrome.tabs.query({}, (tabs) => {
tabs.forEach(tab => {
chrome.tabs.sendMessage(tab.id, { type: 'CONFIG_UPDATED', config }).catch(() => {});
});
});
});
});
}
// ---- Сброс настроек ----
function resetSettings() {
if (!confirm(getStr('options.confirmReset'))) return;
currentProfiles = DEFAULT_CONFIG.autologin.profiles.map(p => ({ ...p }));
currentSubnetRules = [];
elements.portVideo.value = DEFAULT_CONFIG.ports.video;
elements.portScheme.value = DEFAULT_CONFIG.ports.scheme;
elements.videoIpButtonMode.value = DEFAULT_CONFIG.videoWindow.ipButtonMode;
elements.novncRedirect.checked = DEFAULT_CONFIG.novnc.mode === 'redirect';
elements.redirectDelay.value = DEFAULT_CONFIG.novnc.redirectDelay;
elements.mnemoMode.value = DEFAULT_CONFIG.mnemo.mode;
elements.mnemoSkipOnDomain.checked = DEFAULT_CONFIG.mnemo.skipOnDomain;
elements.mnemoAddSchemeButton.checked = DEFAULT_CONFIG.mnemo.addSchemeButton;
elements.mnemoAddVideoButton.checked = DEFAULT_CONFIG.mnemo.addVideoButton;
elements.replaceLogo.checked = DEFAULT_CONFIG.replaceLogo !== false;
elements.uiVisible.checked = DEFAULT_CONFIG.uiVisible !== false;
elements.autologinEnabled.checked = DEFAULT_CONFIG.autologin.enabled;
elements.debugEnabled.checked = DEFAULT_CONFIG.debug.enabled;
elements.debugCore.checked = DEFAULT_CONFIG.debug.modules.core;
elements.debugVideo.checked = DEFAULT_CONFIG.debug.modules.video;
elements.debugMnemo.checked = DEFAULT_CONFIG.debug.modules.mnemo;
elements.debugClose.checked = DEFAULT_CONFIG.debug.modules.close;
elements.debugContent.checked = DEFAULT_CONFIG.debug.modules.content;
elements.debugNovnc.checked = DEFAULT_CONFIG.debug.modules.novnc;
elements.debugAutologin.checked = DEFAULT_CONFIG.debug.modules.autologin;
renderProfiles();
renderSubnetRules();
rebuildProfileSelects();
chrome.storage.sync.set({ config: DEFAULT_CONFIG }, () => {
showStatus(getStr('options.statusResetDone'), 'success');
toggleDebugModules();
toggleRedirectDelay();
toggleAutologinSettings();
});
}
// ---- Вспомогательные функции ----
function toggleDebugModules() {
if (elements.debugEnabled.checked) {
elements.debugModules.classList.add('expanded');
} else {
elements.debugModules.classList.remove('expanded');
}
}
function toggleRedirectDelay() {
elements.redirectDelayContainer.style.display = elements.novncRedirect.checked ? 'block' : 'none';
}
function toggleAutologinSettings() {
if (elements.autologinEnabled.checked) {
elements.autologinSettings.classList.remove('disabled');
} else {
elements.autologinSettings.classList.add('disabled');
}
}
function showStatus(message, type = 'success') {
elements.status.textContent = message;
elements.status.className = `status ${type}`;
elements.status.classList.remove('hidden');
setTimeout(() => { elements.status.classList.add('hidden'); }, 5000);
}
// Валидация при вводе портов
elements.portVideo.addEventListener('input', (e) => { e.target.value = e.target.value.replace(/[^0-9]/g, ''); });
elements.portScheme.addEventListener('input', (e) => { e.target.value = e.target.value.replace(/[^0-9]/g, ''); });