From a2fb35f149e37389cc91033521760639de87a12d Mon Sep 17 00:00:00 2001 From: dv Date: Mon, 23 Mar 2026 16:47:34 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B8=D0=BD=D0=B8=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/content.js | 186 +++++++++++--- js/options.js | 511 +++++++++++++++++++++++++------------- js/popup.js | 26 +- js/strings.js | 25 +- modules/auto-login.js | 258 ++++++++++++------- modules/core.js | 40 ++- modules/mnemoschema.js | 1 + modules/novnc-redirect.js | 2 + modules/video-window.js | 1 + options.html | 60 ++--- popup.html | 7 + 11 files changed, 769 insertions(+), 348 deletions(-) diff --git a/js/content.js b/js/content.js index bb6f63a..7d3c8bb 100644 --- a/js/content.js +++ b/js/content.js @@ -6,7 +6,7 @@ } else { waitForConfigAndInit(); } - + // Ожидание загрузки конфигурации перед инициализацией function waitForConfigAndInit() { const core = window.VideoIPRedirect; @@ -14,22 +14,19 @@ console.error('[CAPS_Enhancer] ❌ CORE не загружен!'); return; } - + const debugLog = (msg, ...args) => core.debugLog('content', msg, ...args); - - // Если конфигурация уже загружена, запускаем инициализацию + if (core.configReady) { debugLog('⚙️ Конфигурация готова, запуск инициализации...'); init(); } else { - // Иначе ждём события о готовности конфигурации debugLog('⏳ Ожидание загрузки конфигурации...'); document.addEventListener('VideoIPRedirect:ConfigReady', () => { debugLog('⚙️ Конфигурация загружена, запуск инициализации...'); init(); }, { once: true }); - - // Таймаут на случай, если событие не сработает + setTimeout(() => { if (!core.configReady) { debugLog('⚠️ Таймаут ожидания конфигурации, запуск с настройками по умолчанию...'); @@ -38,25 +35,25 @@ }, 2000); } } - + function init() { const core = window.VideoIPRedirect; const debugLog = (msg, ...args) => core.debugLog('content', msg, ...args); - + const disabledSites = core.CONFIG.disabledSites || []; const hostname = window.location.hostname || ''; if (disabledSites.length && disabledSites.includes(hostname)) { debugLog('⏹ Расширение отключено на этом сайте:', hostname); return; } - + debugLog('========== ЗАПУСК =========='); debugLog('DOM загружен, инициализация...'); debugLog('📋 Текущая конфигурация:', core.CONFIG); - + const pageType = core.checkPageType(); - // Замена логотипа CleverPark на caps.svg (если включено). Лого может появиться позже (SPA), поэтому повторяем попытки + наблюдатель. + // Замена логотипа CleverPark на caps.svg (если включено) const LOGO_SELECTOR = 'img[src*="cleverpark.svg"], img[src*="cleverpark"], img[alt*="CLEVER PARK"], img[alt*="CLEVER"]'; const replaceLogoEnabled = core.CONFIG.replaceLogo !== false; const hasGetURL = typeof chrome !== 'undefined' && chrome.runtime && typeof chrome.runtime.getURL === 'function'; @@ -66,8 +63,10 @@ const logo = document.querySelector(LOGO_SELECTOR); if (logo && !logo.dataset.capsReplaced) { logo.dataset.capsReplaced = '1'; + logo.setAttribute('data-caps-ui', 'true'); logo.src = chrome.runtime.getURL('icons/caps.svg'); debugLog('🖼️ [LOGO] Заменён на caps.svg', from ? '(' + from + ')' : ''); + applyVisibilityToElement(logo); return true; } return false; @@ -87,72 +86,173 @@ setTimeout(() => logoObserver.disconnect(), 10000); } } - - // ПРИОРИТЕТ 1: Обработка страниц noVNC (переадресация или кнопка) + + // ПРИОРИТЕТ 1: Обработка страниц noVNC if (pageType.isNoVNCPage && core.NoVNCRedirect) { const mode = core.CONFIG.novnc.mode; debugLog(`🖥️ Обнаружена страница noVNC, режим: ${mode}`); const handled = core.NoVNCRedirect.handleNoVNCPage(); - - // Если режим redirect, прерываем инициализацию (будет переадресация) - // Если режим button, продолжаем работу (только добавили кнопку) if (mode === 'redirect' && handled) { return; } } - + // ПРИОРИТЕТ 2: Автологин на странице входа - if (core.AutoLogin && core.AutoLogin.isLoginPage()) { - debugLog('🔐 Обнаружена страница логина'); - core.AutoLogin.tryAutoLogin(); - // Продолжаем инициализацию (не прерываем) + if (core.AutoLogin) { + if (core.AutoLogin.isLoginPage1()) { + debugLog('🔐 Обнаружена страница логина типа 1'); + core.AutoLogin.tryAutoLogin(); + } else if (core.AutoLogin.isLoginPage2()) { + debugLog('🔐 Обнаружена страница логина типа 2'); + core.AutoLogin.performAutoLoginPage2(); + } } - + if (!pageType.hasAppMarker) { debugLog('⏹ Не CLEVER PARK, отключаемся'); return; } - + + // Кнопка переключения видимости UI (показывается на всех CAPS-страницах) + addToggleButton(); + // Настраиваем Escape if (core.CloseHandler) { core.CloseHandler.setupEscapeHandler(); } - + // Видео страница if (pageType.isVideoPage && core.VideoWindow) { debugLog('🎬 ВИДЕО СТРАНИЦА'); - - // Используем утилиту для повторных попыток core.retryWithBackoff(() => core.VideoWindow.addButtonToVideoWindow()); - const videoObserver = new MutationObserver(() => { core.VideoWindow.addButtonToVideoWindow(); }); - - videoObserver.observe(document.body, { - childList: true, - subtree: true - }); + videoObserver.observe(document.body, { childList: true, subtree: true }); } - + // Главная страница if (core.Mnemoschema) { debugLog('🏠 ГЛАВНАЯ СТРАНИЦА'); - const addMnemoButton = () => { if (!core.activeSchemeButton) { core.Mnemoschema.addSchemeButton(); } }; - - // Используем утилиту для повторных попыток core.retryWithBackoff(addMnemoButton); } - + + // Применяем текущий uiVisible после добавления всех элементов + setTimeout(() => applyVisibility(core.CONFIG.uiVisible !== false), 200); + + // Слушаем обновления конфигурации для мгновенного применения видимости + document.addEventListener('VideoIPRedirect:ConfigUpdated', () => { + applyVisibility(core.CONFIG.uiVisible !== false); + }); + + // Горячая клавиша Shift+F1 — переключение видимости улучшений + document.addEventListener('keydown', (e) => { + if (e.shiftKey && e.key === 'F1') { + e.preventDefault(); + toggleVisibility(); + } + }); + debugLog('========== ГОТОВО =========='); } - - // Обработка запроса на однократный вход из popup + + // ---- Управление видимостью UI-улучшений ---- + + function applyVisibilityToElement(el) { + if (!el) return; + const core = window.VideoIPRedirect; + const visible = core && core.CONFIG.uiVisible !== false; + el.style.display = visible ? '' : 'none'; + } + + function applyVisibility(visible) { + document.querySelectorAll('[data-caps-ui="true"]').forEach(el => { + // Кнопка-переключатель не скрывается — иначе не выйти из режима скрытия + if (el.classList.contains('caps-ui-toggle-btn')) return; + el.style.display = visible ? '' : 'none'; + }); + + // Обновляем иконку кнопки переключателя + const toggleBtn = document.querySelector('.caps-ui-toggle-btn'); + if (toggleBtn) { + toggleBtn.title = visible ? 'Скрыть улучшения CAPS (Shift+F1)' : 'Показать улучшения CAPS (Shift+F1)'; + toggleBtn.textContent = visible ? '👁' : '🙈'; + } + } + + function toggleVisibility() { + const core = window.VideoIPRedirect; + if (!core) return; + + const newVisible = !(core.CONFIG.uiVisible !== false); + core.CONFIG.uiVisible = newVisible; + + applyVisibility(newVisible); + + // Сохраняем в storage и рассылаем другим вкладкам + if (typeof chrome !== 'undefined' && chrome.storage) { + chrome.storage.sync.get(['config'], (result) => { + const cfg = result.config || {}; + cfg.uiVisible = newVisible; + chrome.storage.sync.set({ config: cfg }); + }); + } + + if (typeof chrome !== 'undefined' && chrome.runtime) { + chrome.runtime.sendMessage({ + type: 'CONFIG_UPDATED', + config: { uiVisible: newVisible } + }).catch(() => {}); + } + } + + function addToggleButton() { + if (document.querySelector('.caps-ui-toggle-btn')) return; + + const core = window.VideoIPRedirect; + const visible = core && core.CONFIG.uiVisible !== false; + + const btn = document.createElement('button'); + btn.className = 'caps-ui-toggle-btn'; + btn.setAttribute('data-caps-ui', 'true'); + btn.textContent = visible ? '👁' : '🙈'; + btn.title = visible ? 'Скрыть улучшения CAPS (Shift+F1)' : 'Показать улучшения CAPS (Shift+F1)'; + btn.style.cssText = ` + position: fixed; + bottom: 16px; + right: 16px; + z-index: 999999; + width: 36px; + height: 36px; + border-radius: 50%; + border: none; + background: rgba(102, 126, 234, 0.85); + color: white; + font-size: 18px; + cursor: pointer; + box-shadow: 0 2px 8px rgba(0,0,0,0.25); + display: flex; + align-items: center; + justify-content: center; + padding: 0; + line-height: 1; + transition: opacity 0.2s; + `; + btn.onmouseover = () => { btn.style.opacity = '1'; }; + btn.onmouseout = () => { btn.style.opacity = '0.7'; }; + btn.style.opacity = '0.7'; + btn.onclick = () => toggleVisibility(); + + document.body.appendChild(btn); + } + + // ---- Обработка запросов из popup ---- + if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.onMessage) { chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === 'PERFORM_LOGIN') { @@ -165,6 +265,12 @@ sendResponse(result); return true; } + + if (message.type === 'TOGGLE_UI_VISIBILITY') { + toggleVisibility(); + sendResponse({ ok: true }); + return true; + } }); } })(); diff --git a/js/options.js b/js/options.js index 26c17ad..d2fcbd1 100644 --- a/js/options.js +++ b/js/options.js @@ -22,7 +22,6 @@ function applyOptionsStrings() { }); const pageTitle = getStr('options.pageTitle'); if (pageTitle) document.title = pageTitle; - // Версия — из manifest.json (единый источник) const versionEl = document.getElementById('app-version'); if (versionEl && typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.getManifest) { const manifest = chrome.runtime.getManifest(); @@ -35,69 +34,52 @@ const DEFAULT_CONFIG = { debug: { enabled: true, modules: { - core: true, - video: true, - mnemo: true, - close: true, - content: true, - novnc: true, - autologin: true + 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 - }, + ports: { video: '5000', scheme: '8080' }, + novnc: { mode: 'button', redirectDelay: 500 }, + mnemo: { mode: 'host-port', skipOnDomain: false, addVideoButton: true, addSchemeButton: true }, autologin: { enabled: false, - username: '', - password: '', - autoSubmit: true, - delay: 500 + profilePage1: 0, + profilePage2: 1, + subnetRules: [], + profiles: [ + { name: 'Профиль 1', username: '', password: '', autoSubmit: true, delay: 500 } + ] }, disabledSites: [], - replaceLogo: true + replaceLogo: true, + uiVisible: true }; +// Состояние страницы +let currentProfiles = []; +let currentSubnetRules = []; + // Элементы формы const elements = { - // Порты portVideo: document.getElementById('port-video'), portScheme: document.getElementById('port-scheme'), - - // noVNC (страница экрана) 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'), - - // Замена логотипа replaceLogo: document.getElementById('replace-logo'), - - // Автологин + uiVisible: document.getElementById('ui-visible'), autologinEnabled: document.getElementById('autologin-enabled'), autologinSettings: document.getElementById('autologin-settings'), - autologinUsername: document.getElementById('autologin-username'), - autologinPassword: document.getElementById('autologin-password'), - autologinAutoSubmit: document.getElementById('autologin-autosubmit'), - autologinDelay: document.getElementById('autologin-delay'), - - // Отладка + 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'), @@ -107,69 +89,312 @@ const elements = { 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') }; -// Загрузка настроек при открытии страницы (сначала подставляем строки из strings.js) +// ---- Рендеринг профилей ---- + +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() { - console.log('Загрузка настроек...'); - chrome.storage.sync.get(['config'], (result) => { if (chrome.runtime.lastError) { - console.error('Ошибка загрузки:', chrome.runtime.lastError); showStatus(getStr('options.statusLoadError') + ' ' + chrome.runtime.lastError.message, 'error'); return; } - + const config = result.config || DEFAULT_CONFIG; - console.log('Настройки получены из storage:', result); - console.log('Используемая конфигурация:', config); - + // Порты elements.portVideo.value = config.ports?.video || DEFAULT_CONFIG.ports.video; elements.portScheme.value = config.ports?.scheme || DEFAULT_CONFIG.ports.scheme; - + // 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; - const mnemoSkip = config.mnemo?.skipOnDomain === true || mnemoMode === 'skip-on-domain'; elements.mnemoMode.value = (mnemoMode === 'ip-scheme' ? 'host-scheme' : (mnemoMode === 'skip-on-domain' || mnemoMode === 'default' ? 'host-port' : (mnemoMode || 'host-port'))); - elements.mnemoSkipOnDomain.checked = mnemoSkip; + 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.autologinEnabled.checked = config.autologin?.enabled || false; - elements.autologinUsername.value = config.autologin?.username || ''; - elements.autologinPassword.value = config.autologin?.password || ''; - elements.autologinAutoSubmit.checked = config.autologin?.autoSubmit !== false; - elements.autologinDelay.value = config.autologin?.delay || DEFAULT_CONFIG.autologin.delay; - // Отладка elements.debugEnabled.checked = config.debug?.enabled !== false; elements.debugCore.checked = config.debug?.modules?.core !== false; @@ -179,36 +404,28 @@ function loadSettings() { elements.debugContent.checked = config.debug?.modules?.content !== false; elements.debugNovnc.checked = config.debug?.modules?.novnc !== false; elements.debugAutologin.checked = config.debug?.modules?.autologin !== false; - - // Обновляем UI + toggleDebugModules(); toggleRedirectDelay(); toggleAutologinSettings(); - - console.log('✅ Настройки загружены успешно'); }); } -// Сохранение настроек +// ---- Сохранение настроек ---- + 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 redirectDelay = parseInt(elements.redirectDelay.value) || 500; - const autologinDelay = parseInt(elements.autologinDelay.value) || 500; - - // Собираем конфигурацию + const config = { debug: { enabled: elements.debugEnabled.checked, @@ -222,13 +439,10 @@ function saveSettings() { autologin: elements.debugAutologin.checked } }, - ports: { - video: portVideo, - scheme: portScheme - }, + ports: { video: portVideo, scheme: portScheme }, novnc: { mode: elements.novncRedirect.checked ? 'redirect' : 'button', - redirectDelay: redirectDelay + redirectDelay: parseInt(elements.redirectDelay.value) || 500 }, mnemo: { mode: elements.mnemoMode.value, @@ -237,58 +451,46 @@ function saveSettings() { addSchemeButton: elements.mnemoAddSchemeButton.checked }, replaceLogo: elements.replaceLogo.checked, + uiVisible: elements.uiVisible.checked, autologin: { enabled: elements.autologinEnabled.checked, - username: elements.autologinUsername.value.trim(), - password: elements.autologinPassword.value, - autoSubmit: elements.autologinAutoSubmit.checked, - delay: autologinDelay + profilePage1: parseInt(elements.autologinPage1Profile.value) || 0, + profilePage2: parseInt(elements.autologinPage2Profile.value) || 0, + profiles: currentProfiles.map(p => ({ ...p })), + subnetRules: currentSubnetRules.map(r => ({ ...r })) } }; - // Сохраняем список отключённых сайтов из текущего storage + chrome.storage.sync.get(['config'], (prev) => { if (prev.config && Array.isArray(prev.config.disabledSites)) { config.disabledSites = prev.config.disabledSites; } else { config.disabledSites = []; } - - // Сохраняем в chrome.storage - chrome.storage.sync.set({ config }, () => { - if (chrome.runtime.lastError) { - console.error('Ошибка сохранения:', chrome.runtime.lastError); - showStatus(getStr('options.statusSaveError') + ' ' + chrome.runtime.lastError.message, 'error'); - return; - } - - console.log('Настройки сохранены:', config); - showStatus(getStr('options.statusSaveOk'), 'success'); - - // Отправляем сообщение всем вкладкам для обновления конфига - chrome.tabs.query({}, (tabs) => { - tabs.forEach(tab => { - chrome.tabs.sendMessage(tab.id, { - type: 'CONFIG_UPDATED', - config: config - }, (response) => { - // Игнорируем ошибки для вкладок, где нет content script - if (chrome.runtime.lastError) { - // Это нормально, не все вкладки имеют content script - } + + 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; - } - - // Устанавливаем значения по умолчанию + 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.novncRedirect.checked = DEFAULT_CONFIG.novnc.mode === 'redirect'; @@ -298,11 +500,8 @@ function resetSettings() { 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.autologinUsername.value = DEFAULT_CONFIG.autologin.username; - elements.autologinPassword.value = DEFAULT_CONFIG.autologin.password; - elements.autologinAutoSubmit.checked = DEFAULT_CONFIG.autologin.autoSubmit; - elements.autologinDelay.value = DEFAULT_CONFIG.autologin.delay; elements.debugEnabled.checked = DEFAULT_CONFIG.debug.enabled; elements.debugCore.checked = DEFAULT_CONFIG.debug.modules.core; elements.debugVideo.checked = DEFAULT_CONFIG.debug.modules.video; @@ -311,10 +510,12 @@ function resetSettings() { 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 }, () => { - console.log('Настройки сброшены к значениям по умолчанию'); showStatus(getStr('options.statusResetDone'), 'success'); toggleDebugModules(); toggleRedirectDelay(); @@ -322,7 +523,8 @@ function resetSettings() { }); } -// Показать/скрыть модули отладки (при загрузке без .expanded блок изначально скрыт) +// ---- Вспомогательные функции ---- + function toggleDebugModules() { if (elements.debugEnabled.checked) { elements.debugModules.classList.add('expanded'); @@ -331,16 +533,10 @@ function toggleDebugModules() { } } -// Показать/скрыть поле задержки переадресации function toggleRedirectDelay() { - if (elements.novncRedirect.checked) { - elements.redirectDelayContainer.style.display = 'block'; - } else { - elements.redirectDelayContainer.style.display = 'none'; - } + elements.redirectDelayContainer.style.display = elements.novncRedirect.checked ? 'block' : 'none'; } -// Показать/скрыть настройки автологина function toggleAutologinSettings() { if (elements.autologinEnabled.checked) { elements.autologinSettings.classList.remove('disabled'); @@ -349,48 +545,13 @@ function toggleAutologinSettings() { } } -// Показать статус сообщение function showStatus(message, type = 'success') { elements.status.textContent = message; elements.status.className = `status ${type}`; elements.status.classList.remove('hidden'); - - // Автоматически скрыть через 5 секунд - setTimeout(() => { - elements.status.classList.add('hidden'); - }, 5000); + 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, ''); -}); - -// Тестирование chrome.storage при загрузке страницы -console.log('=== Проверка chrome.storage API ==='); -console.log('chrome.storage доступен:', typeof chrome.storage !== 'undefined'); -console.log('chrome.storage.sync доступен:', typeof chrome.storage?.sync !== 'undefined'); - -// Тестовое сохранение и чтение -if (typeof chrome.storage !== 'undefined' && chrome.storage.sync) { - chrome.storage.sync.set({ test: 'test_value' }, () => { - if (chrome.runtime.lastError) { - console.error('❌ Тест сохранения FAILED:', chrome.runtime.lastError); - } else { - console.log('✅ Тест сохранения OK'); - chrome.storage.sync.get(['test'], (result) => { - if (chrome.runtime.lastError) { - console.error('❌ Тест чтения FAILED:', chrome.runtime.lastError); - } else { - console.log('✅ Тест чтения OK:', result); - } - }); - } - }); -} else { - console.error('❌ chrome.storage.sync недоступен!'); -} +// Валидация при вводе портов +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, ''); }); diff --git a/js/popup.js b/js/popup.js index f6a4beb..fa14c44 100644 --- a/js/popup.js +++ b/js/popup.js @@ -3,13 +3,15 @@ const DEFAULT_CONFIG = { debug: { enabled: true }, disabledSites: [], - autologin: { username: '', password: '', delay: 500 } + uiVisible: true, + autologin: { profiles: [], profilePage1: 0 } }; const el = { disableOnSite: document.getElementById('disable-on-site'), disableOnSiteLabel: document.getElementById('disable-on-site-label'), debug: document.getElementById('debug'), + uiVisible: document.getElementById('ui-visible'), btnLogin: document.getElementById('btn-login'), loginStatus: document.getElementById('login-status'), reloadRow: document.getElementById('reload-row'), @@ -93,6 +95,8 @@ async function init() { const debug = config.debug || DEFAULT_CONFIG.debug; el.debug.checked = debug.enabled !== false; + el.uiVisible.checked = config.uiVisible !== false; + function showReloadButton() { if (el.reloadRow) el.reloadRow.classList.remove('hidden'); } @@ -117,6 +121,15 @@ async function init() { showReloadButton(); }); + el.uiVisible.addEventListener('change', async () => { + await saveConfig({ uiVisible: el.uiVisible.checked }); + // Немедленно применяем на активной вкладке через сообщение + const tab = await getActiveTab(); + if (tab && tab.id) { + chrome.tabs.sendMessage(tab.id, { type: 'TOGGLE_UI_VISIBILITY' }).catch(() => {}); + } + }); + el.btnReload.addEventListener('click', async () => { const tab = await getActiveTab(); if (tab && tab.id) { @@ -132,9 +145,11 @@ async function init() { return; } const config = await loadConfig(); - const user = (config.autologin || {}).username; - const pass = (config.autologin || {}).password; - if (!user || !pass) { + const al = config.autologin || {}; + const profiles = Array.isArray(al.profiles) ? al.profiles : []; + const profileIdx = al.profilePage1 || 0; + const profile = profiles[profileIdx] || profiles[0]; + if (!profile || !profile.username || !profile.password) { showLoginStatus(getStr('popup.statusNoCredentials'), 'error'); return; } @@ -143,7 +158,8 @@ async function init() { try { const response = await chrome.tabs.sendMessage(tab.id, { type: 'PERFORM_LOGIN' }); if (response && response.ok) { - showLoginStatus(getStr('popup.statusLoginOk'), 'success'); + const profileName = response.profileName ? ` (${response.profileName})` : ''; + showLoginStatus(getStr('popup.statusLoginOk') + profileName, 'success'); } else { const err = (response && response.error) || 'error'; const msg = err === 'not_login_page' ? getStr('popup.statusNotLoginPage') : err === 'no_credentials' ? getStr('popup.statusNoCredentialsShort') : getStr('popup.statusLoginError'); diff --git a/js/strings.js b/js/strings.js index 38644d4..390f08d 100644 --- a/js/strings.js +++ b/js/strings.js @@ -9,6 +9,7 @@ window.CAPS_STRINGS = { siteWorking: "Расширение работает на этом сайте", siteDisabled: "Расширение отключено на этом сайте", debug: "Дебаг", + uiVisible: "Показывать улучшения", login: "Автологин", reload: "🔄 Перезагрузить страницу", fullSettings: "⚙️ Полные настройки", @@ -61,7 +62,7 @@ window.CAPS_STRINGS = { // Автологин autologinTitle: "🔐 Автологин", autologinEnabledLabel: "Включить автозаполнение", - autologinEnabledDesc: "Автоматически заполнять логин и пароль на :5000/login", + autologinEnabledDesc: "Автоматически заполнять логин и пароль на странице входа", autologinUsernameLabel: "Логин", autologinUsernameDesc: "", autologinUsernamePlaceholder: "admin", @@ -73,6 +74,28 @@ window.CAPS_STRINGS = { autologinDelayLabel: "Задержка (мс)", autologinDelayPlaceholder: "500", + // Профили автологина + autologinProfilesTitle: "Профили учётных данных", + autologinProfileNameLabel: "Имя профиля", + autologinProfileNamePlaceholder: "Профиль 1", + autologinAddProfile: "➕ Добавить профиль", + autologinRemoveProfile: "✕", + autologinPage1ProfileLabel: "Профиль для страницы входа (тип 1, порт 5000)", + autologinPage2ProfileLabel: "Профиль для страницы входа (тип 2, auth-logo)", + + // Правила подсетей + autologinSubnetRulesTitle: "Правила для подсетей", + autologinSubnetRulesDesc: "При совпадении хоста с подсетью — пропустить или использовать другой профиль", + autologinSubnetPlaceholder: "192.168.1.", + autologinSubnetActionSkip: "Пропустить", + autologinSubnetActionProfile: "Использовать профиль", + autologinAddSubnetRule: "➕ Добавить правило", + autologinRemoveSubnetRule: "✕", + + // Видимость UI + appearanceUiVisibleLabel: "Показывать улучшения расширения", + appearanceUiVisibleDesc: "Кнопки, замена логотипа. Можно также переключить через Shift+F1", + // Отладка debugTitle: "🐛 Отладка", debugEnabledLabel: "Включить логи в консоль", diff --git a/modules/auto-login.js b/modules/auto-login.js index 4651dd4..43d008d 100644 --- a/modules/auto-login.js +++ b/modules/auto-login.js @@ -6,93 +6,82 @@ console.error('[VideoIPRedirect] CORE не загружен!'); return; } - + const debugLog = (msg, ...args) => core.debugLog('autologin', msg, ...args); - + debugLog('🔐 Загрузка модуля...'); - - // Проверка, является ли страница страницей логина - function isLoginPage() { - // Проверка 1: Порт 5000 - const port = window.location.port; - if (port !== '5000') { - return false; - } - - // Проверка 2: URL содержит /login - const path = window.location.pathname; - if (!path.includes('/login')) { - return false; - } - - // Проверка 3: Наличие полей логина и пароля + + // Получить профиль по индексу + function getProfile(index) { + const profiles = core.CONFIG.autologin.profiles; + if (!Array.isArray(profiles) || profiles.length === 0) return null; + const idx = (typeof index === 'number' && index >= 0 && index < profiles.length) ? index : 0; + return profiles[idx] || profiles[0]; + } + + // Найти правило подсети для текущего хоста + function getSubnetRule() { + const host = window.location.hostname; + const rules = core.CONFIG.autologin.subnetRules || []; + return rules.find(r => r.subnet && host.startsWith(r.subnet)) || null; + } + + // Проверка страницы логина типа 1 (порт 5000, /login, поля username/password) + function isLoginPage1() { + if (window.location.port !== '5000') return false; + if (!window.location.pathname.includes('/login')) return false; const usernameField = document.querySelector('input[name="username"]'); const passwordField = document.querySelector('input[name="password"]'); - - if (!usernameField || !passwordField) { - return false; - } - + return !!(usernameField && passwordField); + } + + // Проверка страницы логина типа 2 (auth-logo.png + react-notification-root + noscript) + function isLoginPage2() { + const hasNoscript = Array.from(document.querySelectorAll('noscript')) + .some(n => n.textContent.includes('You need to enable JavaScript to run this app')); + if (!hasNoscript) return false; + + const hasNotificationRoot = !!document.querySelector('div.react-notification-root, div[class*="react-notification-root"]'); + if (!hasNotificationRoot) return false; + + const hasAuthLogo = !!document.querySelector('img[src="/images/auth-logo.png"]'); + if (!hasAuthLogo) return false; + return true; } - - // Главная функция автологина - function performAutoLogin() { - debugLog('========== ПРОВЕРКА АВТОЛОГИНА =========='); - - // Проверяем, включен ли автологин - if (!core.CONFIG.autologin.enabled) { - debugLog('⏹ Автологин отключен в настройках'); + + // Заполнить форму логина данными профиля и нажать кнопку входа + function performLoginWithProfile(profile) { + if (!profile || !profile.username || !profile.password) { + debugLog('⚠️ Профиль не задан или пустые учётные данные'); return false; } - - // Проверяем, что это страница логина - if (!isLoginPage()) { - debugLog('❌ Это не страница логина'); - return false; - } - - debugLog('✅ Обнаружена страница логина'); - - // Получаем данные для входа - const username = core.CONFIG.autologin.username; - const password = core.CONFIG.autologin.password; - - if (!username || !password) { - debugLog('⚠️ Логин или пароль не заданы в настройках'); - return false; - } - - debugLog('🔑 Данные для входа найдены'); - - // Находим поля + const usernameField = document.querySelector('input[name="username"]'); const passwordField = document.querySelector('input[name="password"]'); const submitButton = document.querySelector('button[type="submit"]'); - + if (!usernameField || !passwordField) { debugLog('❌ Не удалось найти поля ввода'); return false; } - - // Заполняем поля - debugLog('✍️ Заполнение полей...'); - usernameField.value = username; - passwordField.value = password; - - // Генерируем события для совместимости с JS-фреймворками + + debugLog(`✍️ Заполнение полей (профиль: ${profile.name || 'без имени'})...`); + + // React перехватывает нативный сеттер value — используем его напрямую, + // чтобы обновление дошло до React-state компонента + const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; + nativeSetter.call(usernameField, profile.username); usernameField.dispatchEvent(new Event('input', { bubbles: true })); - usernameField.dispatchEvent(new Event('change', { bubbles: true })); + nativeSetter.call(passwordField, profile.password); passwordField.dispatchEvent(new Event('input', { bubbles: true })); - passwordField.dispatchEvent(new Event('change', { bubbles: true })); - + debugLog('✅ Поля заполнены'); - - // Автоматический вход, если включен - if (core.CONFIG.autologin.autoSubmit && submitButton) { - const delay = core.CONFIG.autologin.delay || 500; + + const autoSubmit = typeof profile.autoSubmit === 'boolean' ? profile.autoSubmit : true; + if (autoSubmit && submitButton) { + const delay = profile.delay || 500; debugLog(`⏱️ Вход через ${delay}мс...`); - setTimeout(() => { debugLog('🚀 Нажатие кнопки "Вход"'); submitButton.click(); @@ -100,58 +89,141 @@ } else { debugLog('ℹ️ Автоматический вход отключен'); } - - debugLog('========== АВТОЛОГИН ЗАВЕРШЕН =========='); + return true; } - - // Функция с повторными попытками (на случай если DOM еще не загружен) + + // Главная функция автологина для страницы типа 1 (с retry-логикой) + function performAutoLogin() { + debugLog('========== ПРОВЕРКА АВТОЛОГИНА (тип 1) =========='); + + if (!core.CONFIG.autologin.enabled) { + debugLog('⏹ Автологин отключен в настройках'); + return false; + } + + if (!isLoginPage1()) { + debugLog('❌ Это не страница логина типа 1'); + return false; + } + + debugLog('✅ Обнаружена страница логина типа 1'); + + // Проверяем правила подсетей + const rule = getSubnetRule(); + if (rule) { + debugLog(`📋 Найдено правило подсети: "${rule.subnet}", действие: "${rule.action}"`); + if (rule.action === 'skip') { + debugLog('⏭ Автологин пропущен по правилу подсети'); + return false; + } + if (rule.action === 'profile') { + const overrideProfile = getProfile(rule.profileIndex); + debugLog(`🔄 Используем профиль по правилу подсети: ${overrideProfile?.name}`); + return performLoginWithProfile(overrideProfile); + } + } + + const profile = getProfile(core.CONFIG.autologin.profilePage1); + debugLog(`🔑 Профиль для типа 1: ${profile?.name}`); + return performLoginWithProfile(profile); + } + + // Автологин для страницы типа 1 — с повторными попытками function tryAutoLogin(attempts = 3) { - debugLog(`🔄 Попытка автологина (осталось попыток: ${attempts})`); - + debugLog(`🔄 Попытка автологина типа 1 (осталось попыток: ${attempts})`); + const success = performAutoLogin(); - + if (!success && attempts > 0) { setTimeout(() => tryAutoLogin(attempts - 1), 500); } } - - // Однократный вход по кнопке (без проверки enabled, всегда отправка формы) + + // Автологин для страницы типа 2 — одна попытка, без retry + function performAutoLoginPage2() { + debugLog('========== ПРОВЕРКА АВТОЛОГИНА (тип 2) =========='); + + if (!core.CONFIG.autologin.enabled) { + debugLog('⏹ Автологин отключен в настройках'); + return false; + } + + if (!isLoginPage2()) { + debugLog('❌ Это не страница логина типа 2'); + return false; + } + + debugLog('✅ Обнаружена страница логина типа 2'); + + // Проверяем правила подсетей + const rule = getSubnetRule(); + if (rule) { + debugLog(`📋 Найдено правило подсети: "${rule.subnet}", действие: "${rule.action}"`); + if (rule.action === 'skip') { + debugLog('⏭ Автологин пропущен по правилу подсети'); + return false; + } + if (rule.action === 'profile') { + const overrideProfile = getProfile(rule.profileIndex); + debugLog(`🔄 Используем профиль по правилу подсети: ${overrideProfile?.name}`); + return performLoginWithProfile(overrideProfile); + } + } + + const profile = getProfile(core.CONFIG.autologin.profilePage2); + debugLog(`🔑 Профиль для типа 2: ${profile?.name}`); + return performLoginWithProfile(profile); + // Намеренно без retry — одна попытка + } + + // Однократный вход по кнопке из попапа (без проверки enabled, всегда отправка формы) function performLoginOnce() { - if (!isLoginPage()) { + if (!isLoginPage1() && !isLoginPage2()) { return { ok: false, error: 'not_login_page' }; } - const username = core.CONFIG.autologin.username; - const password = core.CONFIG.autologin.password; - if (!username || !password) { + + const isPage2 = isLoginPage2(); + const profileIndex = isPage2 + ? core.CONFIG.autologin.profilePage2 + : core.CONFIG.autologin.profilePage1; + const profile = getProfile(profileIndex); + + if (!profile || !profile.username || !profile.password) { return { ok: false, error: 'no_credentials' }; } + const usernameField = document.querySelector('input[name="username"]'); const passwordField = document.querySelector('input[name="password"]'); - const submitButton = document.querySelector('button[type="submit"]'); if (!usernameField || !passwordField) { return { ok: false, error: 'no_fields' }; } - usernameField.value = username; - passwordField.value = password; + + const nativeSetter2 = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; + nativeSetter2.call(usernameField, profile.username); usernameField.dispatchEvent(new Event('input', { bubbles: true })); - usernameField.dispatchEvent(new Event('change', { bubbles: true })); + nativeSetter2.call(passwordField, profile.password); passwordField.dispatchEvent(new Event('input', { bubbles: true })); - passwordField.dispatchEvent(new Event('change', { bubbles: true })); + + const submitButton = document.querySelector('button[type="submit"]'); if (submitButton) { - const delay = core.CONFIG.autologin.delay || 500; + const delay = profile.delay || 500; setTimeout(() => submitButton.click(), delay); } - return { ok: true }; + + return { ok: true, profileName: profile.name || '' }; } - + // Публичный API window.VideoIPRedirect.AutoLogin = { - performAutoLogin: performAutoLogin, tryAutoLogin: tryAutoLogin, - isLoginPage: isLoginPage, - performLoginOnce: performLoginOnce + performAutoLoginPage2: performAutoLoginPage2, + isLoginPage1: isLoginPage1, + isLoginPage2: isLoginPage2, + performLoginOnce: performLoginOnce, + getProfile: getProfile, + getSubnetRule: getSubnetRule }; - + debugLog('✅ Модуль загружен'); })(); diff --git a/modules/core.js b/modules/core.js index 9cea9b7..b1f545f 100644 --- a/modules/core.js +++ b/modules/core.js @@ -40,17 +40,22 @@ let CONFIG = { // Настройки автологина autologin: { enabled: false, // Включить/выключить автологин - username: '', // Логин для автозаполнения - password: '', // Пароль для автозаполнения - autoSubmit: true, // Автоматически нажимать кнопку "Вход" - delay: 500 // Задержка перед автовходом (мс) + profilePage1: 0, // Индекс профиля для страницы логина типа 1 (порт 5000, /login) + profilePage2: 1, // Индекс профиля для страницы логина типа 2 (auth-logo.png) + subnetRules: [], // Правила по подсетям: [{ subnet, action: 'skip'|'profile', profileIndex }] + profiles: [ // Массив профилей учётных данных + { name: 'Профиль 1', username: '', password: '', autoSubmit: true, delay: 500 } + ] }, // Сайты, на которых расширение отключено (массив hostname, например ['example.com']) disabledSites: [], // Замена логотипа CleverPark на иконку CAPS (icons/caps.svg) - replaceLogo: true + replaceLogo: true, + + // Показывать ли улучшения расширения (кнопки, логотип) + uiVisible: true }; // Обновление конфигурации (изменяет существующий объект, а не создает новый) @@ -87,7 +92,25 @@ function updateConfig(newConfig) { // Обновляем autologin if (newConfig.autologin) { - Object.assign(CONFIG.autologin, newConfig.autologin); + const a = newConfig.autologin; + + // Миграция старого формата (username/password) → profiles[0] + if ((a.username || a.password) && !Array.isArray(a.profiles)) { + CONFIG.autologin.profiles = [{ + name: 'Профиль 1', + username: a.username || '', + password: a.password || '', + autoSubmit: typeof a.autoSubmit === 'boolean' ? a.autoSubmit : true, + delay: a.delay || 500 + }]; + } else if (Array.isArray(a.profiles)) { + CONFIG.autologin.profiles = a.profiles; + } + + if (typeof a.enabled === 'boolean') CONFIG.autologin.enabled = a.enabled; + if (typeof a.profilePage1 === 'number') CONFIG.autologin.profilePage1 = a.profilePage1; + if (typeof a.profilePage2 === 'number') CONFIG.autologin.profilePage2 = a.profilePage2; + if (Array.isArray(a.subnetRules)) CONFIG.autologin.subnetRules = a.subnetRules; } // Обновляем disabledSites @@ -100,6 +123,11 @@ function updateConfig(newConfig) { CONFIG.replaceLogo = newConfig.replaceLogo; } + // Обновляем uiVisible + if (typeof newConfig.uiVisible === 'boolean') { + CONFIG.uiVisible = newConfig.uiVisible; + } + // Обновляем константы портов window.VideoIPRedirect.TARGET_PORT_VIDEO = CONFIG.ports.video; window.VideoIPRedirect.TARGET_PORT_SCHEME = CONFIG.ports.scheme; diff --git a/modules/mnemoschema.js b/modules/mnemoschema.js index c9e34f1..18e0346 100644 --- a/modules/mnemoschema.js +++ b/modules/mnemoschema.js @@ -139,6 +139,7 @@ wrapper.className = 'scheme-buttons-wrapper'; wrapper.style.display = 'inline-block'; wrapper.style.verticalAlign = 'middle'; + wrapper.setAttribute('data-caps-ui', 'true'); if (addVideo) wrapper.appendChild(createVideoButton(host)); if (addScheme) wrapper.appendChild(createButton(mnemoLink, host)); mnemoLink.parentNode.replaceChild(wrapper, mnemoLink); diff --git a/modules/novnc-redirect.js b/modules/novnc-redirect.js index e69f7be..e372471 100644 --- a/modules/novnc-redirect.js +++ b/modules/novnc-redirect.js @@ -104,6 +104,8 @@ 'novnc-settings-btn' ); + button.setAttribute('data-caps-ui', 'true'); + // Дополнительные стили для позиционирования button.style.position = 'fixed'; button.style.top = '20px'; diff --git a/modules/video-window.js b/modules/video-window.js index 5267ef4..0f74ab8 100644 --- a/modules/video-window.js +++ b/modules/video-window.js @@ -80,6 +80,7 @@ }, 'video-redirect-btn' ); + button.setAttribute('data-caps-ui', 'true'); const p = header.querySelector('p'); if (p) { diff --git a/options.html b/options.html index 2e448a6..c1756f9 100644 --- a/options.html +++ b/options.html @@ -99,19 +99,29 @@ - +

🖼️ Внешний вид

+
+ +
@@ -120,45 +130,39 @@
- +
+
-
-
-
- -
-
- - +
+ + +

Профили учётных данных

+
+ + + +

Правила для подсетей

+

При совпадении хоста с подсетью — пропустить или использовать другой профиль

+
+
diff --git a/popup.html b/popup.html index fdb383a..2b3bb10 100644 --- a/popup.html +++ b/popup.html @@ -26,6 +26,13 @@ +