From 7d2c977a9fcacfad68ce57838d5d23ca80c31b96 Mon Sep 17 00:00:00 2001 From: dv Date: Mon, 23 Mar 2026 14:02:46 +0300 Subject: [PATCH] init --- css/options.css | 442 ++++++++++++++++++++++++++++++++++++++ css/popup.css | 154 +++++++++++++ icons/caps.svg | 137 ++++++++++++ icons/icon128.png | Bin 0 -> 5559 bytes icons/icon16.png | Bin 0 -> 5559 bytes icons/icon48.png | Bin 0 -> 5559 bytes js/content.js | 170 +++++++++++++++ js/options.js | 396 ++++++++++++++++++++++++++++++++++ js/popup.js | 159 ++++++++++++++ js/strings.js | 108 ++++++++++ manifest.json | 39 ++++ modules/auto-login.js | 157 ++++++++++++++ modules/close-handler.js | 239 +++++++++++++++++++++ modules/core.js | 355 ++++++++++++++++++++++++++++++ modules/mnemoschema.js | 179 +++++++++++++++ modules/novnc-redirect.js | 199 +++++++++++++++++ modules/video-window.js | 135 ++++++++++++ options.html | 252 ++++++++++++++++++++++ popup.html | 44 ++++ 19 files changed, 3165 insertions(+) create mode 100644 css/options.css create mode 100644 css/popup.css create mode 100644 icons/caps.svg create mode 100644 icons/icon128.png create mode 100644 icons/icon16.png create mode 100644 icons/icon48.png create mode 100644 js/content.js create mode 100644 js/options.js create mode 100644 js/popup.js create mode 100644 js/strings.js create mode 100644 manifest.json create mode 100644 modules/auto-login.js create mode 100644 modules/close-handler.js create mode 100644 modules/core.js create mode 100644 modules/mnemoschema.js create mode 100644 modules/novnc-redirect.js create mode 100644 modules/video-window.js create mode 100644 options.html create mode 100644 popup.html diff --git a/css/options.css b/css/options.css new file mode 100644 index 0000000..4b832fe --- /dev/null +++ b/css/options.css @@ -0,0 +1,442 @@ +/* ==================== СТИЛИ СТРАНИЦЫ НАСТРОЕК ==================== */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + padding: 20px; +} + +.container { + max-width: 900px; + margin: 0 auto; +} + +header { + background: white; + padding: 20px; + border-radius: 12px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); + margin-bottom: 20px; +} + +.header-content { + display: flex; + align-items: center; + gap: 20px; + justify-content: center; +} + +.header-logo { + width: 64px; + height: 64px; + object-fit: contain; +} + +header h1 { + color: #333; + font-size: 32px; + margin-bottom: 10px; + text-align: left; +} + +.subtitle { + color: #666; + font-size: 16px; + text-align: left; +} + +.content { + background: #f4f5f8; + padding: 20px 24px 24px; + border-radius: 12px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); +} + +.settings-grid { + display: grid; + grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); + gap: 20px; +} + +.settings-col.secondary { + align-self: flex-start; +} + +.settings-section { + margin-bottom: 18px; + background: #ffffff; + border-radius: 10px; + border: 1px solid #e2e4ea; + padding: 12px 14px 10px; + box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08); +} + +.settings-section h2 { + color: #333; + font-size: 18px; + margin-bottom: 10px; + display: flex; + align-items: center; + gap: 10px; +} + +.settings-section h3 { + color: #555; + font-size: 16px; + margin-bottom: 15px; + margin-top: 10px; +} + +.setting-item { + margin-bottom: 14px; +} + +.setting-item label { + display: block; + margin-bottom: 8px; +} + +.label-text { + display: block; + font-weight: 600; + color: #333; + font-size: 14px; + margin-bottom: 4px; +} + +.label-description { + display: block; + font-size: 12px; + color: #888; + margin-bottom: 8px; +} + +input[type="text"], +input[type="number"], +select { + width: 100%; + padding: 8px 10px; + border: 2px solid #e0e0e0; + border-radius: 8px; + font-size: 15px; + transition: all 0.3s ease; + font-family: inherit; +} + +input[type="text"]:focus, +input[type="number"]:focus, +select:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +/* Поле пароля: text + маскировка точками (без type=password из-за особенностей рендеринга) */ +.password-field { + -webkit-text-security: disc; + text-security: disc; +} + +select { + cursor: pointer; + background: white; +} + +/* Switch toggle */ +.switch-label { + display: flex; + justify-content: space-between; + align-items: center; +} + +.switch-label .label-text, +.switch-label .label-description { + margin-bottom: 0; +} + +.switch { + position: relative; + display: inline-block; + width: 44px; + height: 24px; + flex-shrink: 0; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: 0.4s; + border-radius: 24px; +} + +.slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + transition: 0.4s; + border-radius: 50%; +} + +input:checked + .slider { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +input:checked + .slider:before { + transform: translateX(20px); +} + +/* Компактные inline-элементы (label + input в одну линию) */ +.setting-item.inline { + display: flex; + align-items: center; + gap: 10px; +} + +.setting-item.inline label { + margin-bottom: 0; +} + +/* Подсказки под полями (например, под паролем) */ +.hint { + display: block; + margin-top: 4px; + font-size: 11px; + color: #888; +} + +/* Sub settings */ +.sub-settings { + margin-top: 12px; + padding: 10px 12px; + background: #f8f9fa; + border-radius: 8px; +} + +.sub-settings.disabled { + opacity: 0.5; + pointer-events: none; +} + +/* Список модулей отладки: по умолчанию скрыт (нет .expanded при загрузке), + показывается только при включённой отладке — без анимации при первой отрисовке */ +#debug-modules { + max-height: 0; + overflow: hidden; + margin-top: 0; + padding-top: 0; + padding-bottom: 0; + border: none; +} + +#debug-modules.expanded { + max-height: 600px; + margin-top: 12px; + padding: 10px 12px; + background: #f8f9fa; + border: 1px solid #e2e4ea; + border-radius: 8px; + transition: max-height 0.25s ease; +} + +.setting-item-compact { + margin-bottom: 12px; +} + +.setting-item-compact label { + display: flex; + align-items: center; + gap: 10px; + cursor: pointer; + padding: 8px; + border-radius: 6px; + transition: background 0.2s ease; +} + +.setting-item-compact label:hover { + background: rgba(102, 126, 234, 0.1); +} + +.setting-item-compact input[type="checkbox"] { + width: 20px; + height: 20px; + cursor: pointer; +} + +.setting-item-compact span { + font-size: 15px; + color: #333; +} + +/* Buttons */ +.actions { + display: flex; + gap: 10px; + margin-top: 16px; + margin-bottom: 4px; +} + +.btn { + flex: 1; + padding: 10px 18px; + border: none; + border-radius: 8px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + font-family: inherit; +} + +.btn-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); +} + +.btn-secondary { + background: #f0f0f0; + color: #333; +} + +.btn-secondary:hover { + background: #e0e0e0; + transform: translateY(-2px); +} + +/* Status message */ +.status { + padding: 15px 20px; + border-radius: 8px; + margin-bottom: 20px; + font-weight: 500; + text-align: center; + animation: slideIn 0.3s ease; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.status.success { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.status.error { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +.status.hidden { + display: none; +} + +/* Info section */ +.info-section { + margin-top: 10px; + padding: 14px 16px; + background: #f8f9fa; + border-radius: 8px; + border-left: 4px solid #667eea; +} + +.info-section h3 { + color: #333; + margin-bottom: 15px; +} + +.info-section p { + color: #666; + line-height: 1.4; + margin-bottom: 4px; +} + +.info-section p:last-child { + margin-bottom: 0; + margin-top: 12px; + font-style: italic; +} + +/* Responsive */ +@media (max-width: 768px) { + .container { + padding: 10px; + } + + header { + padding: 20px; + } + + .header-content { + flex-direction: column; + gap: 15px; + } + + .header-logo { + width: 48px; + height: 48px; + } + + header h1 { + font-size: 24px; + text-align: center; + } + + .subtitle { + text-align: center; + } + + .content { + padding: 16px; + } + + .actions { + flex-direction: column; + } + + .switch-label { + flex-direction: column; + align-items: flex-start; + gap: 10px; + } +} + +/* Hidden elements */ +.hidden { + display: none; +} diff --git a/css/popup.css b/css/popup.css new file mode 100644 index 0000000..b6ea234 --- /dev/null +++ b/css/popup.css @@ -0,0 +1,154 @@ +/* Popup — компактное выпадающее окно */ +* { + box-sizing: border-box; +} +body { + margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-size: 13px; + color: #333; + min-width: 260px; +} +.popup { + padding: 0; + display: flex; + flex-direction: column; +} +.popup-header { + padding: 12px 18px; + border-bottom: 1px solid #e0e0e0; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: #fff; +} +.popup-title { + font-weight: 600; + font-size: 14px; +} +.popup-body { + padding: 16px 18px; +} +.row { + display: flex; + align-items: center; + margin-bottom: 16px; + gap: 12px; +} +.row:last-of-type { + margin-bottom: 0; +} +.row-btn { + margin-top: 14px; + margin-bottom: 4px; +} +.switch-row { + cursor: pointer; + user-select: none; + justify-content: space-between; +} +.switch-wrap { + flex-shrink: 0; + position: relative; + display: inline-block; +} +.switch-input { + position: absolute; + opacity: 0; + width: 0; + height: 0; + margin: 0; +} +.switch-track { + display: inline-block; + width: 40px; + height: 22px; + border-radius: 11px; + background: #cbd5e0; + position: relative; + transition: background 0.2s ease; +} +.switch-track::after { + content: ''; + position: absolute; + width: 18px; + height: 18px; + border-radius: 50%; + background: #fff; + top: 2px; + left: 2px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25); + transition: transform 0.2s ease; +} +.switch-input:checked + .switch-track { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} +.switch-input:checked + .switch-track::after { + transform: translateX(18px); +} +.switch-row .label { + flex: 1; + line-height: 1.35; + margin-right: 8px; +} +.btn { + padding: 8px 16px; + border: none; + border-radius: 8px; + font-size: 13px; + cursor: pointer; + font-weight: 500; +} +.btn-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: #fff; +} +.btn-primary:hover { + opacity: 0.9; +} +.btn-primary:disabled { + opacity: 0.6; + cursor: not-allowed; +} +.row-reload { + margin-top: 8px; +} +.row-reload.hidden { + display: none !important; +} +.btn-reload { + background: #e2e8f0; + color: #475569; +} +.btn-reload:hover { + background: #cbd5e1; +} +.status-text { + font-size: 12px; + color: #666; +} +.status-text.success { color: #2e7d32; } +.status-text.error { color: #c62828; } +.popup-footer { + padding: 12px 18px; + border-top: 1px solid #e5e7eb; + background: linear-gradient(180deg, #f8f9fc 0%, #eef0f5 100%); +} +.popup-footer .footer-link { + display: inline-flex; + align-items: center; + gap: 6px; + color: #5a67d8; + text-decoration: none; + font-size: 13px; + font-weight: 500; + padding: 6px 10px; + border-radius: 8px; + transition: background 0.15s ease, color 0.15s ease; +} +.popup-footer .footer-link:hover { + background: rgba(102, 126, 234, 0.12); + color: #434190; +} +.popup-footer .footer-link:active { + background: rgba(102, 126, 234, 0.2); +} diff --git a/icons/caps.svg b/icons/caps.svg new file mode 100644 index 0000000..e719615 --- /dev/null +++ b/icons/caps.svg @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/icon128.png b/icons/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..e3c6d423e6c8eef746806e211c64ea3fa6009cf8 GIT binary patch literal 5559 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 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('⚠️ Таймаут ожидания конфигурации, запуск с настройками по умолчанию...'); + init(); + } + }, 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), поэтому повторяем попытки + наблюдатель. + 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'; + + function tryReplaceLogo(from) { + if (!replaceLogoEnabled || !hasGetURL) return false; + const logo = document.querySelector(LOGO_SELECTOR); + if (logo && !logo.dataset.capsReplaced) { + logo.dataset.capsReplaced = '1'; + logo.src = chrome.runtime.getURL('icons/caps.svg'); + debugLog('🖼️ [LOGO] Заменён на caps.svg', from ? '(' + from + ')' : ''); + return true; + } + return false; + } + + if (replaceLogoEnabled && hasGetURL) { + debugLog('🖼️ [LOGO] Включена замена, селектор:', LOGO_SELECTOR); + tryReplaceLogo('init'); + [400, 1000, 2500].forEach((ms, i) => { + setTimeout(() => { tryReplaceLogo('retry-' + (i + 1)); }, ms); + }); + const logoObserver = new MutationObserver(() => { + if (tryReplaceLogo('observer')) logoObserver.disconnect(); + }); + if (document.body) { + logoObserver.observe(document.body, { childList: true, subtree: true }); + setTimeout(() => logoObserver.disconnect(), 10000); + } + } + + // ПРИОРИТЕТ 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 (!pageType.hasAppMarker) { + debugLog('⏹ Не CLEVER PARK, отключаемся'); + return; + } + + // Настраиваем 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 + }); + } + + // Главная страница + if (core.Mnemoschema) { + debugLog('🏠 ГЛАВНАЯ СТРАНИЦА'); + + const addMnemoButton = () => { + if (!core.activeSchemeButton) { + core.Mnemoschema.addSchemeButton(); + } + }; + + // Используем утилиту для повторных попыток + core.retryWithBackoff(addMnemoButton); + } + + debugLog('========== ГОТОВО =========='); + } + + // Обработка запроса на однократный вход из popup + if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.onMessage) { + chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.type === 'PERFORM_LOGIN') { + const core = window.VideoIPRedirect; + if (!core || !core.AutoLogin) { + sendResponse({ ok: false, error: 'not_ready' }); + return true; + } + const result = core.AutoLogin.performLoginOnce(); + sendResponse(result); + return true; + } + }); + } +})(); diff --git a/js/options.js b/js/options.js new file mode 100644 index 0000000..26c17ad --- /dev/null +++ b/js/options.js @@ -0,0 +1,396 @@ +// ==================== СКРИПТ СТРАНИЦЫ НАСТРОЕК ==================== + +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; + // Версия — из manifest.json (единый источник) + 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, + username: '', + password: '', + autoSubmit: true, + delay: 500 + }, + disabledSites: [], + replaceLogo: true +}; + +// Элементы формы +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'), + + // Автологин + 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'), + + // Отладка + 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') +}; + +// Загрузка настроек при открытии страницы (сначала подставляем строки из strings.js) +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); + +// Загрузка сохранённых настроек +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.mnemoAddSchemeButton.checked = config.mnemo?.addSchemeButton !== false; + elements.mnemoAddVideoButton.checked = config.mnemo?.addVideoButton !== false; + + // Замена логотипа + elements.replaceLogo.checked = config.replaceLogo !== false; + + // Автологин + 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; + 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; + + // Обновляем 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, + 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 + }, + novnc: { + mode: elements.novncRedirect.checked ? 'redirect' : 'button', + redirectDelay: redirectDelay + }, + mnemo: { + mode: elements.mnemoMode.value, + skipOnDomain: elements.mnemoSkipOnDomain.checked, + addVideoButton: elements.mnemoAddVideoButton.checked, + addSchemeButton: elements.mnemoAddSchemeButton.checked + }, + replaceLogo: elements.replaceLogo.checked, + autologin: { + enabled: elements.autologinEnabled.checked, + username: elements.autologinUsername.value.trim(), + password: elements.autologinPassword.value, + autoSubmit: elements.autologinAutoSubmit.checked, + delay: autologinDelay + } + }; + // Сохраняем список отключённых сайтов из текущего 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 + } + }); + }); + }); + }); + }); +} + +// Сброс настроек к значениям по умолчанию +function resetSettings() { + if (!confirm(getStr('options.confirmReset'))) { + return; + } + + // Устанавливаем значения по умолчанию + elements.portVideo.value = DEFAULT_CONFIG.ports.video; + elements.portScheme.value = DEFAULT_CONFIG.ports.scheme; + 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.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; + 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; + + // Сохраняем + chrome.storage.sync.set({ config: DEFAULT_CONFIG }, () => { + console.log('Настройки сброшены к значениям по умолчанию'); + showStatus(getStr('options.statusResetDone'), 'success'); + toggleDebugModules(); + toggleRedirectDelay(); + toggleAutologinSettings(); + }); +} + +// Показать/скрыть модули отладки (при загрузке без .expanded блок изначально скрыт) +function toggleDebugModules() { + if (elements.debugEnabled.checked) { + elements.debugModules.classList.add('expanded'); + } else { + elements.debugModules.classList.remove('expanded'); + } +} + +// Показать/скрыть поле задержки переадресации +function toggleRedirectDelay() { + if (elements.novncRedirect.checked) { + elements.redirectDelayContainer.style.display = 'block'; + } else { + elements.redirectDelayContainer.style.display = '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'); + + // Автоматически скрыть через 5 секунд + 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 недоступен!'); +} diff --git a/js/popup.js b/js/popup.js new file mode 100644 index 0000000..f6a4beb --- /dev/null +++ b/js/popup.js @@ -0,0 +1,159 @@ +// ==================== POPUP РАСШИРЕНИЯ ==================== + +const DEFAULT_CONFIG = { + debug: { enabled: true }, + disabledSites: [], + autologin: { username: '', password: '', delay: 500 } +}; + +const el = { + disableOnSite: document.getElementById('disable-on-site'), + disableOnSiteLabel: document.getElementById('disable-on-site-label'), + debug: document.getElementById('debug'), + btnLogin: document.getElementById('btn-login'), + loginStatus: document.getElementById('login-status'), + reloadRow: document.getElementById('reload-row'), + btnReload: document.getElementById('btn-reload') +}; + +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 applyPopupStrings() { + 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.tagName !== 'OPTION') { + node.textContent = text; + } + }); +} +function updateDisableOnSiteLabel() { + if (!el.disableOnSiteLabel) return; + el.disableOnSiteLabel.textContent = el.disableOnSite.checked ? getStr('popup.siteWorking') : getStr('popup.siteDisabled'); +} + +let currentHostname = ''; + +function showLoginStatus(text, type) { + el.loginStatus.textContent = text; + el.loginStatus.className = 'status-text ' + (type || ''); +} + +function loadConfig() { + return new Promise((resolve) => { + chrome.storage.sync.get(['config'], (result) => { + const config = result.config || {}; + resolve(config); + }); + }); +} + +function saveConfig(updates) { + return new Promise((resolve) => { + chrome.storage.sync.get(['config'], (result) => { + const config = { ...(result.config || {}), ...updates }; + if (!Array.isArray(config.disabledSites)) config.disabledSites = []; + chrome.storage.sync.set({ config }, () => { + // Уведомить вкладки об обновлении конфига + chrome.tabs.query({}, (tabs) => { + tabs.forEach((tab) => { + chrome.tabs.sendMessage(tab.id, { type: 'CONFIG_UPDATED', config }).catch(() => {}); + }); + }); + resolve(); + }); + }); + }); +} + +function getActiveTab() { + return new Promise((resolve) => { + chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { + resolve(tabs[0] || null); + }); + }); +} + +async function init() { + applyPopupStrings(); + const [config, tab] = await Promise.all([loadConfig(), getActiveTab()]); + currentHostname = tab ? new URL(tab.url || 'http://a').hostname : ''; + + /* Слайд вкл = расширение работает (сайт НЕ в списке отключённых) */ + const disabledSites = config.disabledSites || []; + el.disableOnSite.checked = currentHostname ? !disabledSites.includes(currentHostname) : false; + el.disableOnSite.disabled = !currentHostname; + updateDisableOnSiteLabel(); + + const debug = config.debug || DEFAULT_CONFIG.debug; + el.debug.checked = debug.enabled !== false; + + function showReloadButton() { + if (el.reloadRow) el.reloadRow.classList.remove('hidden'); + } + + el.disableOnSite.addEventListener('change', async () => { + updateDisableOnSiteLabel(); + let list = (await loadConfig()).disabledSites || []; + if (!Array.isArray(list)) list = []; + if (el.disableOnSite.checked) { + list = list.filter((h) => h !== currentHostname); + } else { + if (currentHostname && !list.includes(currentHostname)) list.push(currentHostname); + } + await saveConfig({ disabledSites: list }); + showReloadButton(); + }); + + el.debug.addEventListener('change', async () => { + const config = await loadConfig(); + const debug = config.debug || { modules: {} }; + await saveConfig({ debug: { ...debug, enabled: el.debug.checked } }); + showReloadButton(); + }); + + el.btnReload.addEventListener('click', async () => { + const tab = await getActiveTab(); + if (tab && tab.id) { + chrome.tabs.reload(tab.id); + window.close(); + } + }); + + el.btnLogin.addEventListener('click', async () => { + const tab = await getActiveTab(); + if (!tab || !tab.id) { + showLoginStatus(getStr('popup.statusNoTab'), 'error'); + return; + } + const config = await loadConfig(); + const user = (config.autologin || {}).username; + const pass = (config.autologin || {}).password; + if (!user || !pass) { + showLoginStatus(getStr('popup.statusNoCredentials'), 'error'); + return; + } + el.btnLogin.disabled = true; + showLoginStatus('…', ''); + try { + const response = await chrome.tabs.sendMessage(tab.id, { type: 'PERFORM_LOGIN' }); + if (response && response.ok) { + showLoginStatus(getStr('popup.statusLoginOk'), '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'); + showLoginStatus(msg, 'error'); + } + } catch (e) { + showLoginStatus(getStr('popup.statusOpenLoginPage'), 'error'); + } + el.btnLogin.disabled = false; + }); +} + +init(); diff --git a/js/strings.js b/js/strings.js new file mode 100644 index 0000000..38644d4 --- /dev/null +++ b/js/strings.js @@ -0,0 +1,108 @@ +/** + * Все пользовательские тексты расширения. + * Редактируйте этот файл, чтобы изменить подписи, кнопки и сообщения в popup и на странице настроек. + */ +window.CAPS_STRINGS = { + // ========== Всплывающее окно (popup) ========== + popup: { + title: "CAPS Enhancer", + siteWorking: "Расширение работает на этом сайте", + siteDisabled: "Расширение отключено на этом сайте", + debug: "Дебаг", + login: "Автологин", + reload: "🔄 Перезагрузить страницу", + fullSettings: "⚙️ Полные настройки", + // Сообщения при нажатии «Войти» + statusNoTab: "Нет активной вкладки", + statusNoCredentials: "Укажите логин и пароль в настройках", + statusLoginOk: "Вход выполнен", + statusNotLoginPage: "Не на странице логина", + statusNoCredentialsShort: "Нет данных входа", + statusLoginError: "Ошибка входа", + statusOpenLoginPage: "Откройте страницу входа и нажмите снова" + }, + + // ========== Страница настроек (options) ========== + options: { + pageTitle: "CAPS Enhancer - Настройки", + headerTitle: "Настройки CAPS Enhancer", + headerSubtitle: "Настройте расширение под свои нужды", + + // noVNC (страница экрана) + novncTitle: "🖥️ Страница экрана", + novncRedirectLabel: "Включить переадресацию", + novncRedirectDesc: "Автоматически перенаправлять со страницы экрана на настройки (лещ)", + novncDelayLabel: "Задержка (мс)", + novncDelayPlaceholder: "500", + novncPortLabel: "Порт настроек", + novncPortPlaceholder: "5000", + + // Мнемосхема + mnemoTitle: "📍 Мнемосхема", + mnemoAddSchemeButtonLabel: "Добавлять кнопку «Мнемосхема»", + mnemoAddSchemeButtonDesc: "Показывать кнопку перехода на мнемосхему на главной странице", + mnemoModeLabel: "Режим перехода", + mnemoModeDesc: "Куда ведёт кнопка «Мнемосхема»", + mnemoModeHostPort: "host:порт (http://текущий_хост:8080)", + mnemoModeHostScheme: "host/scheme (http://текущий_хост/scheme)", + mnemoAddVideoButtonLabel: "Добавлять кнопку «Лещ»", + mnemoAddVideoButtonDesc: "Кнопка перехода в настройки рядом с «Мнемосхема»", + mnemoSkipOnDomainLabel: "Не добавлять на доменах", + mnemoSkipOnDomainDesc: "При доменах по порту на мнему не попасть", + + mnemoPortLabel: "Порт мнемосхемы", + mnemoPortPlaceholder: "8080", + + // Внешний вид + appearanceTitle: "🖼️ Внешний вид", + replaceLogoLabel: "Заменять логотип CleverPark на иконку CAPS Enhancer", + replaceLogoDesc: "", // На главной странице показывать icons/caps.svg вместо логотипа CleverPark + + // Автологин + autologinTitle: "🔐 Автологин", + autologinEnabledLabel: "Включить автозаполнение", + autologinEnabledDesc: "Автоматически заполнять логин и пароль на :5000/login", + autologinUsernameLabel: "Логин", + autologinUsernameDesc: "", + autologinUsernamePlaceholder: "admin", + autologinPasswordLabel: "Пароль", + autologinPasswordDesc: "", + autologinPasswordPlaceholder: "••••••••", + autologinPasswordHint: "Пароль хранится в настройках Chrome!⚠️", + autologinAutoSubmitLabel: "Автоматически нажимать «Вход»", + autologinDelayLabel: "Задержка (мс)", + autologinDelayPlaceholder: "500", + + // Отладка + debugTitle: "🐛 Отладка", + debugEnabledLabel: "Включить логи в консоль", + debugEnabledDesc: "Показывать отладочные сообщения", + debugModulesTitle: "Модули для отладки", + debugCore: "🟢 Core (ядро)", + debugVideo: "🔵 Video (видео окна)", + debugMnemo: "🟠 Mnemo (мнемосхема)", + debugClose: "🔴 Close (закрытие)", + debugContent: "🟣 Content (главный)", + debugNovnc: "💠 NoVNC (переадресация)", + debugAutologin: "🔐 AutoLogin (автовход)", + + // Информация + infoTitle: "ℹ️ Информация", + infoVersion: "Версия:", + infoName: "Название:", + infoReloadHint: "После изменения настроек перезагрузите страницы для применения изменений.", + + // Кнопки + saveBtn: "💾 Сохранить", + resetBtn: "🔄 Сбросить", + + // Сообщения статуса + statusSaveOk: "Настройки успешно сохранены! Перезагрузите страницы для применения.", + statusLoadError: "Ошибка загрузки настроек", + statusSaveError: "Ошибка сохранения настроек", + statusResetDone: "Настройки сброшены к значениям по умолчанию", + confirmReset: "Вы уверены, что хотите сбросить все настройки к значениям по умолчанию?", + errorPortVideo: "Ошибка: Неверный порт для видео (1-65535)", + errorPortScheme: "Ошибка: Неверный порт для мнемосхемы (1-65535)" + } +}; diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..bd160c9 --- /dev/null +++ b/manifest.json @@ -0,0 +1,39 @@ +{ + "manifest_version": 3, + "name": "CAPS Enhancer", + "version": "2.7", + "description": "Добавляет всякие фичи для УТП в CAPS", + "icons": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + }, + "permissions": ["activeTab", "storage", "tabs"], + "host_permissions": [""], + "action": { + "default_popup": "popup.html", + "default_title": "CAPS Enhancer" + }, + "options_page": "options.html", + "web_accessible_resources": [ + { + "resources": ["icons/caps.svg"], + "matches": [""] + } + ], + "content_scripts": [ + { + "matches": [""], + "js": [ + "modules/core.js", + "modules/video-window.js", + "modules/mnemoschema.js", + "modules/close-handler.js", + "modules/novnc-redirect.js", + "modules/auto-login.js", + "js/content.js" + ], + "run_at": "document_idle" + } + ] +} diff --git a/modules/auto-login.js b/modules/auto-login.js new file mode 100644 index 0000000..4651dd4 --- /dev/null +++ b/modules/auto-login.js @@ -0,0 +1,157 @@ +// ==================== МОДУЛЬ АВТОЛОГИНА ==================== + +(function() { + const core = window.VideoIPRedirect; + if (!core) { + 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: Наличие полей логина и пароля + const usernameField = document.querySelector('input[name="username"]'); + const passwordField = document.querySelector('input[name="password"]'); + + if (!usernameField || !passwordField) { + return false; + } + + return true; + } + + // Главная функция автологина + function performAutoLogin() { + debugLog('========== ПРОВЕРКА АВТОЛОГИНА =========='); + + // Проверяем, включен ли автологин + if (!core.CONFIG.autologin.enabled) { + 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-фреймворками + usernameField.dispatchEvent(new Event('input', { bubbles: true })); + usernameField.dispatchEvent(new Event('change', { bubbles: true })); + 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; + debugLog(`⏱️ Вход через ${delay}мс...`); + + setTimeout(() => { + debugLog('🚀 Нажатие кнопки "Вход"'); + submitButton.click(); + }, delay); + } else { + debugLog('ℹ️ Автоматический вход отключен'); + } + + debugLog('========== АВТОЛОГИН ЗАВЕРШЕН =========='); + return true; + } + + // Функция с повторными попытками (на случай если DOM еще не загружен) + function tryAutoLogin(attempts = 3) { + debugLog(`🔄 Попытка автологина (осталось попыток: ${attempts})`); + + const success = performAutoLogin(); + + if (!success && attempts > 0) { + setTimeout(() => tryAutoLogin(attempts - 1), 500); + } + } + + // Однократный вход по кнопке (без проверки enabled, всегда отправка формы) + function performLoginOnce() { + if (!isLoginPage()) { + return { ok: false, error: 'not_login_page' }; + } + const username = core.CONFIG.autologin.username; + const password = core.CONFIG.autologin.password; + if (!username || !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; + usernameField.dispatchEvent(new Event('input', { bubbles: true })); + usernameField.dispatchEvent(new Event('change', { bubbles: true })); + passwordField.dispatchEvent(new Event('input', { bubbles: true })); + passwordField.dispatchEvent(new Event('change', { bubbles: true })); + if (submitButton) { + const delay = core.CONFIG.autologin.delay || 500; + setTimeout(() => submitButton.click(), delay); + } + return { ok: true }; + } + + // Публичный API + window.VideoIPRedirect.AutoLogin = { + performAutoLogin: performAutoLogin, + tryAutoLogin: tryAutoLogin, + isLoginPage: isLoginPage, + performLoginOnce: performLoginOnce + }; + + debugLog('✅ Модуль загружен'); +})(); diff --git a/modules/close-handler.js b/modules/close-handler.js new file mode 100644 index 0000000..3996c5f --- /dev/null +++ b/modules/close-handler.js @@ -0,0 +1,239 @@ +// ==================== МОДУЛЬ ЗАКРЫТИЯ ==================== + +(function() { + const core = window.VideoIPRedirect; + if (!core) { + console.error('[CAPS_Enhancer] CORE не загружен!'); + return; + } + + const debugLog = (msg, ...args) => core.debugLog('close', msg, ...args); + const isElementVisible = core.isElementVisible; + + debugLog('❌ Загрузка модуля...'); + + // Функция для поиска кнопки закрытия ПО ТОЧНОМУ СЕЛЕКТОРУ + function findVideoCloseButton() { + debugLog('========== ПОИСК КНОПКИ ЗАКРЫТИЯ ВИДЕО =========='); + + // 1. Ищем по точному классу sc-heWoWI + debugLog('Поиск по селектору: button.sc-heWoWI'); + let closeBtn = document.querySelector('button.sc-heWoWI'); + + if (!closeBtn) { + // 2. Ищем по части класса + debugLog('Поиск по селектору: button[class*="sc-heWoWI"]'); + closeBtn = document.querySelector('button[class*="sc-heWoWI"]'); + } + + if (!closeBtn) { + // 3. Ищем по href="#" + debugLog('Поиск по селектору: button[href="#"]'); + closeBtn = document.querySelector('button[href="#"]'); + } + + if (!closeBtn) { + // 4. Ищем по img с close_icon.png + debugLog('Поиск по селектору: button img[src*="close_icon.png"]'); + const closeImg = document.querySelector('button img[src*="close_icon.png"]'); + if (closeImg) { + closeBtn = closeImg.closest('button'); + } + } + + if (!closeBtn) { + // 5. Ищем во всех окнах с видео + debugLog('Поиск в окнах с видео'); + const videos = document.querySelectorAll('img[src*="/getVideo/"]'); + + for (let video of videos) { + let parent = video.closest('div[style*="fixed"], div[style*="absolute"], div[class*="sc-"]'); + if (parent) { + const btn = parent.querySelector('button.sc-heWoWI, button[href="#"], button img[src*="close_icon.png"]')?.closest('button'); + if (btn) { + closeBtn = btn; + debugLog('✅ Найдена кнопка в окне с видео'); + break; + } + } + } + } + + if (closeBtn) { + debugLog('✅ КНОПКА ЗАКРЫТИЯ НАЙДЕНА!'); + debugLog(' Текст:', closeBtn.textContent?.trim() || 'нет текста'); + debugLog(' Класс:', closeBtn.className); + debugLog(' Href:', closeBtn.getAttribute('href')); + debugLog(' HTML:', closeBtn.outerHTML.substring(0, 100)); + debugLog(' Видима:', isElementVisible(closeBtn)); + + // Проверяем наличие img внутри + const img = closeBtn.querySelector('img'); + if (img) { + debugLog(' Изображение:', { + src: img.src, + alt: img.alt + }); + } + } else { + debugLog('❌ КНОПКА ЗАКРЫТИЯ НЕ НАЙДЕНА'); + + // Выводим все кнопки для отладки + debugLog('Все кнопки на странице:'); + document.querySelectorAll('button').forEach((btn, i) => { + debugLog(` Кнопка ${i}:`, { + class: btn.className, + href: btn.getAttribute('href'), + hasImg: !!btn.querySelector('img'), + text: btn.textContent?.trim() || 'нет текста' + }); + }); + } + + debugLog('========== ПОИСК ЗАВЕРШЕН =========='); + return closeBtn; + } + + // Функция для поиска кнопки "Закрыть карточку" + function findCloseCardButton() { + debugLog('========== ПОИСК КНОПКИ "ЗАКРЫТЬ КАРТОЧКУ" =========='); + + const allButtons = document.querySelectorAll('button'); + + for (let button of allButtons) { + const text = button.textContent?.toLowerCase().trim() || ''; + if (text.includes('закрыть карточку')) { + debugLog('✅ КНОПКА "ЗАКРЫТЬ КАРТОЧКУ" НАЙДЕНА!'); + debugLog(' Текст:', button.textContent?.trim()); + debugLog(' Класс:', button.className); + debugLog(' Href:', button.getAttribute('href')); + return button; + } + if (text.includes('отменить')) { + debugLog('✅ КНОПКА "отменить" НАЙДЕНА!'); + debugLog(' Текст:', button.textContent?.trim()); + debugLog(' Класс:', button.className); + debugLog(' Href:', button.getAttribute('href')); + return button; + } + } + + debugLog('❌ Кнопка "Закрыть карточку" не найдена'); + return null; + } + + function setupEscapeHandler() { + debugLog('========== НАСТРОЙКА ОБРАБОТЧИКА ESCAPE =========='); + + document.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + debugLog('🔴🔴🔴 ESCAPE НАЖАТ! 🔴🔴🔴'); + + e.preventDefault(); + e.stopPropagation(); + + // 1. Сначала ищем кнопку "Закрыть карточку" + debugLog('--- ЭТАП 1: Поиск "Закрыть карточку" ---'); + const closeCardBtn = findCloseCardButton(); + + if (closeCardBtn && isElementVisible(closeCardBtn)) { + debugLog('🟢 НАЖИМАЕМ "ЗАКРЫТЬ КАРТОЧКУ" 🟢'); + closeCardBtn.click(); + debugLog('✅ Клик выполнен'); + + setTimeout(() => { + if (core.VideoWindow) { + core.VideoWindow.removeButtonsFromWindow(); + } + }, 100); + return; + } + + // 2. Если нет, ищем кнопку закрытия видео + debugLog('--- ЭТАП 2: Поиск кнопки закрытия видео ---'); + const videoCloseBtn = findVideoCloseButton(); + + if (videoCloseBtn && isElementVisible(videoCloseBtn)) { + debugLog('🟢 НАЖИМАЕМ КНОПКУ ЗАКРЫТИЯ ВИДЕО 🟢'); + videoCloseBtn.click(); + debugLog('✅ Клик выполнен'); + + setTimeout(() => { + if (core.VideoWindow) { + core.VideoWindow.removeButtonsFromWindow(); + } + }, 100); + return; + } + + debugLog('❌ НЕ УДАЛОСЬ НАЙТИ КНОПКУ ДЛЯ ЗАКРЫТИЯ'); + } + }); + + debugLog('✅ Обработчик Escape установлен'); + } + + // Функция для принудительного добавления обработчика на кнопку + function setupDirectCloseHandler() { + debugLog('Настройка прямого обработчика на кнопку закрытия'); + + const closeBtn = findVideoCloseButton(); + if (closeBtn) { + // Сохраняем оригинальный обработчик + const originalClick = closeBtn.onclick; + + // Добавляем свой обработчик + closeBtn.addEventListener('click', function() { + debugLog('Кнопка закрытия нажата!'); + setTimeout(() => { + if (core.VideoWindow) { + core.VideoWindow.removeButtonsFromWindow(); + } + }, 100); + }); + + debugLog('✅ Прямой обработчик добавлен'); + } + } + + // Функция для отладки - подсветка всех кнопок закрытия на странице + function colorAllCloseButtons() { + debugLog('🎨 Подсветка всех кнопок закрытия'); + + const selectors = [ + 'button.sc-heWoWI', + 'button[class*="sc-heWoWI"]', + 'button[href="#"]', + 'button img[src*="close_icon.png"]' + ]; + + let count = 0; + selectors.forEach(selector => { + const buttons = document.querySelectorAll(selector); + buttons.forEach(btn => { + const button = btn.tagName === 'BUTTON' ? btn : btn.closest('button'); + if (button && !button.dataset.colored) { + button.style.border = '3px solid red'; + button.style.boxShadow = '0 0 10px red'; + button.dataset.colored = 'true'; + count++; + debugLog(` Кнопка #${count}:`, button.className || 'без класса'); + } + }); + }); + + debugLog(`✅ Подсвечено кнопок: ${count}`); + return count; + } + + window.VideoIPRedirect.CloseHandler = { + setupEscapeHandler: setupEscapeHandler, + setupDirectCloseHandler: setupDirectCloseHandler, + findVideoCloseButton: findVideoCloseButton, + findCloseCardButton: findCloseCardButton, + colorAllCloseButtons: colorAllCloseButtons, + isElementVisible: isElementVisible + }; + + debugLog('✅ Модуль загружен'); +})(); \ No newline at end of file diff --git a/modules/core.js b/modules/core.js new file mode 100644 index 0000000..9cea9b7 --- /dev/null +++ b/modules/core.js @@ -0,0 +1,355 @@ +// ==================== ЯДРО РАСШИРЕНИЯ ==================== + +// ============ ГЛАВНАЯ КОНФИГУРАЦИЯ РАСШИРЕНИЯ ============ +// Значения по умолчанию (будут перезаписаны из 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, // Включить/выключить автологин + username: '', // Логин для автозаполнения + password: '', // Пароль для автозаполнения + autoSubmit: true, // Автоматически нажимать кнопку "Вход" + delay: 500 // Задержка перед автовходом (мс) + }, + + // Сайты, на которых расширение отключено (массив hostname, например ['example.com']) + disabledSites: [], + + // Замена логотипа CleverPark на иконку CAPS (icons/caps.svg) + replaceLogo: 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) { + Object.assign(CONFIG.autologin, newConfig.autologin); + } + + // Обновляем disabledSites + if (Array.isArray(newConfig.disabledSites)) { + CONFIG.disabledSites = newConfig.disabledSites; + } + + // Обновляем replaceLogo + if (typeof newConfig.replaceLogo === 'boolean') { + CONFIG.replaceLogo = newConfig.replaceLogo; + } + + // Обновляем константы портов + 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__" + const appWindow = document.querySelector('div[class*="app_AppWindow__"]'); + const hasAppWindow = !!appWindow; + + // Страница 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 } + })); +}); \ No newline at end of file diff --git a/modules/mnemoschema.js b/modules/mnemoschema.js new file mode 100644 index 0000000..c9e34f1 --- /dev/null +++ b/modules/mnemoschema.js @@ -0,0 +1,179 @@ +// ==================== МОДУЛЬ МНЕМОСХЕМЫ ==================== + +(function() { + const core = window.VideoIPRedirect; + if (!core) { + console.error('[CAPS_Enhancer] CORE не загружен!'); + return; + } + + const debugLog = (msg, ...args) => core.debugLog('mnemo', msg, ...args); + + debugLog('🏠 Загрузка модуля...'); + + // Функция для получения хоста из текущего URL + function getCurrentHostFromUrl() { + const hostname = window.location.hostname; + debugLog('Используем hostname:', hostname); + return hostname; + } + + // Функция для поиска ссылки на мнемосхему + function findMnemoLink() { + debugLog('Поиск ссылки на мнемосхему'); + + const allLinks = document.querySelectorAll('a'); + + for (let link of allLinks) { + const text = link.textContent.toLowerCase().trim(); + const href = link.getAttribute('href') || ''; + + if (text.includes('мнемосхема') || href.includes('/scheme')) { + debugLog('✅ Найдена!'); + return link; + } + } + + debugLog('❌ Ссылка не найдена'); + return null; + } + + // Кнопка "Лещ" — переход на host:порт_видео (тот же стиль, что и Мнемосхема) + function createVideoButton(host) { + const oldVideoBtn = document.querySelector('.video-redirect-btn'); + if (oldVideoBtn) oldVideoBtn.remove(); + + const portVideo = core.TARGET_PORT_VIDEO || core.CONFIG?.ports?.video || '5000'; + const targetUrl = `http://${host}:${portVideo}`; + + const button = core.createStyledButton( + 'Лещ', + (e) => { + e.preventDefault(); + e.stopPropagation(); + window.open(targetUrl, '_blank'); + }, + 'video-redirect-btn' + ); + button.setAttribute('data-host', host); + button.setAttribute('data-port', portVideo); + return button; + } + + // Функция для создания кнопки "Мнемосхема" + function createButton(originalLink, host) { + const oldBtn = document.querySelector('.scheme-redirect-btn'); + if (oldBtn) oldBtn.remove(); + + const mode = core.CONFIG?.mnemo?.mode || 'host-port'; + const currentHostWithPort = window.location.host || host; + + let targetUrl; + if (mode === 'host-scheme') { + targetUrl = `http://${currentHostWithPort}/scheme`; + } else { + targetUrl = `http://${host}:${core.TARGET_PORT_SCHEME}`; + } + + const button = core.createStyledButton( + 'Мнемосхема', + (e) => { + e.preventDefault(); + e.stopPropagation(); + window.open(targetUrl, '_blank'); + }, + 'scheme-redirect-btn' + ); + + // Для кнопки мнемосхемы используем вместо + + + + + + + + + diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..fdb383a --- /dev/null +++ b/popup.html @@ -0,0 +1,44 @@ + + + + + + CAPS Enhancer + + + + + + + +