// ==================== ЯДРО РАСШИРЕНИЯ ==================== // ============ ГЛАВНАЯ КОНФИГУРАЦИЯ РАСШИРЕНИЯ ============ // Значения по умолчанию (будут перезаписаны из chrome.storage) let CONFIG = { // Настройки отладки debug: { enabled: true, // Главный переключатель (если false, все логи отключены) modules: { core: true, // Логи ядра video: true, // Логи модуля видео mnemo: true, // Логи модуля мнемосхемы close: true, // Логи модуля закрытия content: true, // Логи главного файла novnc: true, // Логи модуля noVNC autologin: true // Логи модуля автологина } }, // Настройки портов ports: { video: '5000', // Порт для видео/настроек scheme: '8080' // Порт для мнемосхемы }, // Настройки модуля noVNC novnc: { mode: 'button', // 'redirect' - автоматическая переадресация, 'button' - добавить кнопку redirectDelay: 500 // Задержка перед переадресацией (мс), только для mode: 'redirect' }, // Настройки кнопки "Мнемосхема" mnemo: { mode: 'host-port', // 'host-port' — host:порт мнемосхемы, 'host-scheme' — http://хост/scheme skipOnDomain: false, // не добавлять кнопку при заходе по домену (не по IP) addVideoButton: true, // добавлять кнопку "Лещ" (переход на host:порт видео) addSchemeButton: true // добавлять кнопку "Мнемосхема" }, // Настройки автологина autologin: { enabled: false, // Включить/выключить автологин 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, // Настройки видео-окна videoWindow: { ipButtonMode: 'button' // 'button' — отдельная кнопка с IP, 'clickable-title' — заголовок окна становится кликабельным }, // Показывать ли улучшения расширения (кнопки, логотип) uiVisible: true }; // Обновление конфигурации (изменяет существующий объект, а не создает новый) function updateConfig(newConfig) { // Обновляем debug if (newConfig.debug) { CONFIG.debug.enabled = newConfig.debug.enabled; if (newConfig.debug.modules) { Object.assign(CONFIG.debug.modules, newConfig.debug.modules); } } // Обновляем ports if (newConfig.ports) { Object.assign(CONFIG.ports, newConfig.ports); } // Обновляем novnc if (newConfig.novnc) { Object.assign(CONFIG.novnc, newConfig.novnc); } // Обновляем mnemo (совместимость со старыми значениями mode) if (newConfig.mnemo) { const m = newConfig.mnemo; const mode = m.mode === 'ip-scheme' ? 'host-scheme' : (m.mode === 'skip-on-domain' || m.mode === 'default' ? 'host-port' : (m.mode || 'host-port')); CONFIG.mnemo.mode = mode; CONFIG.mnemo.skipOnDomain = m.skipOnDomain === true || m.mode === 'skip-on-domain'; if (typeof m.addVideoButton === 'boolean') CONFIG.mnemo.addVideoButton = m.addVideoButton; else if (CONFIG.mnemo.addVideoButton === undefined) CONFIG.mnemo.addVideoButton = true; if (typeof m.addSchemeButton === 'boolean') CONFIG.mnemo.addSchemeButton = m.addSchemeButton; else if (CONFIG.mnemo.addSchemeButton === undefined) CONFIG.mnemo.addSchemeButton = true; } // Обновляем autologin if (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 if (Array.isArray(newConfig.disabledSites)) { CONFIG.disabledSites = newConfig.disabledSites; } // Обновляем replaceLogo if (typeof newConfig.replaceLogo === 'boolean') { CONFIG.replaceLogo = newConfig.replaceLogo; } // Обновляем videoWindow if (newConfig.videoWindow) { Object.assign(CONFIG.videoWindow, newConfig.videoWindow); } // Обновляем 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; } // Загрузка конфигурации из chrome.storage (возвращает Promise) function loadConfig() { return new Promise((resolve) => { if (typeof chrome !== 'undefined' && chrome.storage) { chrome.storage.sync.get(['config'], (result) => { if (result.config) { // Обновляем CONFIG из сохранённых настроек updateConfig(result.config); debugLog('core', '⚙️ Конфигурация загружена из настроек:', CONFIG); } else { debugLog('core', 'ℹ️ Используется конфигурация по умолчанию'); } resolve(); }); } else { debugLog('core', 'ℹ️ chrome.storage недоступен, используется конфигурация по умолчанию'); resolve(); } }); } // Слушаем обновления конфигурации if (typeof chrome !== 'undefined' && chrome.runtime) { chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === 'CONFIG_UPDATED') { updateConfig(message.config); debugLog('core', '🔄 Конфигурация обновлена:', CONFIG); // Отправляем событие об обновлении конфигурации document.dispatchEvent(new CustomEvent('VideoIPRedirect:ConfigUpdated', { detail: { config: CONFIG } })); debugLog('core', '💡 Для применения всех изменений рекомендуется перезагрузить страницу'); } }); } // Константы портов (для обратной совместимости) const TARGET_PORT_VIDEO = CONFIG.ports.video; const TARGET_PORT_SCHEME = CONFIG.ports.scheme; // Улучшенная функция отладочного логирования function debugLog(module, ...args) { // Проверяем глобальный флаг и флаг конкретного модуля if (!CONFIG.debug.enabled || !CONFIG.debug.modules[module]) { return; } // Цвета для разных модулей const moduleColors = { core: '#4CAF50', // Зелёный video: '#2196F3', // Синий mnemo: '#FF9800', // Оранжевый close: '#F44336', // Красный content: '#9C27B0', // Фиолетовый novnc: '#00BCD4', // Голубой autologin: '#8BC34A' // Светло-зелёный }; const color = moduleColors[module] || '#607D8B'; const prefix = `[CAPS_Enhancer::${module.toUpperCase()}]`; console.log( `%c${prefix}`, `background: ${color}; color: white; padding: 2px 5px; border-radius: 3px; font-weight: bold;`, ...args ); } // ПРОСТАЯ проверка типа страницы function checkPageType() { debugLog('core', '=== ПРОВЕРКА ТИПА СТРАНИЦЫ ==='); // 1. Проверяем маркер приложения const hasAppMarker = checkAppMarker(); debugLog('core', 'Маркер приложения:', hasAppMarker ? '✅' : '❌'); // 2. Проверяем логотип (/images/cleverpark.svg или alt="CLEVER PARK") const logoSelector = 'img[src*="cleverpark.svg"], img[src*="cleverpark"], img[alt*="CLEVER PARK"], img[alt*="CLEVER"]'; const logo = document.querySelector(logoSelector); const hasLogo = !!logo; debugLog('core', 'Логотип CLEVER PARK:', hasLogo ? '✅' : '❌'); if (logo) debugLog('core', ' Найден лого:', logo.src); // 3. Проверяем признаки страницы noVNC (экран на 80 порту) const isNoVNC = checkNoVNCPage(); debugLog('core', 'Страница noVNC:', isNoVNC ? '✅' : '❌'); const pageType = { isMainPage: hasLogo && hasAppMarker && !isNoVNC, isVideoPage: !hasLogo && hasAppMarker && !isNoVNC, isNoVNCPage: isNoVNC, hasAppMarker: hasAppMarker, hasLogo: hasLogo }; debugLog('core', 'ИТОГОВЫЙ ТИП СТРАНИЦЫ:', pageType.isNoVNCPage ? 'NOVNC (экран)' : pageType.isMainPage ? 'ГЛАВНАЯ' : pageType.isVideoPage ? 'ВИДЕО' : 'НЕ ЦЕЛЕВАЯ'); return pageType; } // Проверка страницы noVNC по характерным признакам function checkNoVNCPage() { // Признак 1: noscript с текстом "You need to enable JavaScript to run this app." const noscripts = document.querySelectorAll('noscript'); let hasNoVNCNoscript = false; for (let noscript of noscripts) { if (noscript.textContent.includes('You need to enable JavaScript to run this app')) { hasNoVNCNoscript = true; break; } } // Признак 2: div с классом начинающимся на "app_AppWindow__" // ИЛИ title страницы содержит "Платёжный терминал" (присутствует сразу при загрузке) const hasAppWindow = !!( document.querySelector('div[class*="app_AppWindow__"]') || document.title.includes('Платёжный терминал') ); // Страница noVNC определяется по наличию обоих признаков return hasNoVNCNoscript && hasAppWindow; } // Проверка маркера приложения function checkAppMarker() { const noscripts = document.querySelectorAll('noscript'); for (let noscript of noscripts) { if (noscript.textContent.includes('You need to enable JavaScript')) { return true; } } return document.body?.textContent?.includes('You need to enable JavaScript') || false; } // ПРОСТОЕ извлечение IP function extractIpAndPort(videoUrl) { if (!videoUrl) return null; const match = videoUrl.match(/\/getVideo\/http:\/\/([^\/:?#]+)(?::(\d+))?/i); return match ? { ip: match[1], originalPort: match[2] || '80' } : null; } // Проверка видимости элемента (общая функция для всех модулей) function isElementVisible(element) { if (!element) return false; try { const style = window.getComputedStyle(element); if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') { return false; } const rect = element.getBoundingClientRect(); if (rect.width === 0 || rect.height === 0) { return false; } return true; } catch (e) { return false; } } // Создание стилизованной кнопки (общая функция) function createStyledButton(text, onClick, className = 'redirect-btn') { const button = document.createElement('button'); button.className = className; button.textContent = text; button.style.cssText = ` margin-left: 12px; padding: 6px 16px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 18px; cursor: pointer; font-size: 13px; font-weight: bold; display: inline-block; vertical-align: middle; transition: all 0.2s ease; border: 1px solid rgba(255,255,255,0.5); box-shadow: 0 2px 8px rgba(0,0,0,0.2); font-family: Arial, sans-serif; `; button.onmouseover = () => { button.style.transform = 'scale(1.05)'; button.style.boxShadow = '0 4px 12px rgba(0,0,0,0.3)'; }; button.onmouseout = () => { button.style.transform = 'scale(1)'; button.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)'; }; button.onclick = onClick; return button; } // Утилита для повторных попыток function retryWithBackoff(fn, maxAttempts = 3, delays = [500, 1500, 3000]) { delays.forEach((delay, index) => { setTimeout(() => { if (index < maxAttempts) { fn(); } }, delay); }); } // Глобальный объект window.VideoIPRedirect = { CONFIG: CONFIG, debugLog: debugLog, checkPageType: checkPageType, extractIpAndPort: extractIpAndPort, isElementVisible: isElementVisible, createStyledButton: createStyledButton, retryWithBackoff: retryWithBackoff, TARGET_PORT_VIDEO: TARGET_PORT_VIDEO, TARGET_PORT_SCHEME: TARGET_PORT_SCHEME, activeSchemeButton: false, // Для обратной совместимости DEBUG_CONFIG: CONFIG.debug }; debugLog('core', '✅ CORE: Модуль инициализирован'); debugLog('core', '📌 СТРАНИЦА:', window.location.href); debugLog('core', '📌 USER AGENT:', navigator.userAgent); // Флаг готовности конфигурации window.VideoIPRedirect.configReady = false; // Загружаем конфигурацию из настроек loadConfig().then(() => { window.VideoIPRedirect.configReady = true; debugLog('core', '🎯 Конфигурация готова к использованию'); // Отправляем событие о готовности конфигурации document.dispatchEvent(new CustomEvent('VideoIPRedirect:ConfigReady', { detail: { config: CONFIG } })); });