// ==================== СКРИПТ СТРАНИЦЫ НАСТРОЕК ==================== 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, ''); });