This commit is contained in:
dv 2026-03-23 16:47:34 +03:00
parent 76571fafc5
commit a2fb35f149
11 changed files with 769 additions and 348 deletions

View file

@ -17,19 +17,16 @@
const debugLog = (msg, ...args) => core.debugLog('content', msg, ...args); const debugLog = (msg, ...args) => core.debugLog('content', msg, ...args);
// Если конфигурация уже загружена, запускаем инициализацию
if (core.configReady) { if (core.configReady) {
debugLog('⚙️ Конфигурация готова, запуск инициализации...'); debugLog('⚙️ Конфигурация готова, запуск инициализации...');
init(); init();
} else { } else {
// Иначе ждём события о готовности конфигурации
debugLog('⏳ Ожидание загрузки конфигурации...'); debugLog('⏳ Ожидание загрузки конфигурации...');
document.addEventListener('VideoIPRedirect:ConfigReady', () => { document.addEventListener('VideoIPRedirect:ConfigReady', () => {
debugLog('⚙️ Конфигурация загружена, запуск инициализации...'); debugLog('⚙️ Конфигурация загружена, запуск инициализации...');
init(); init();
}, { once: true }); }, { once: true });
// Таймаут на случай, если событие не сработает
setTimeout(() => { setTimeout(() => {
if (!core.configReady) { if (!core.configReady) {
debugLog('⚠️ Таймаут ожидания конфигурации, запуск с настройками по умолчанию...'); debugLog('⚠️ Таймаут ожидания конфигурации, запуск с настройками по умолчанию...');
@ -56,7 +53,7 @@
const pageType = core.checkPageType(); 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 LOGO_SELECTOR = 'img[src*="cleverpark.svg"], img[src*="cleverpark"], img[alt*="CLEVER PARK"], img[alt*="CLEVER"]';
const replaceLogoEnabled = core.CONFIG.replaceLogo !== false; const replaceLogoEnabled = core.CONFIG.replaceLogo !== false;
const hasGetURL = typeof chrome !== 'undefined' && chrome.runtime && typeof chrome.runtime.getURL === 'function'; const hasGetURL = typeof chrome !== 'undefined' && chrome.runtime && typeof chrome.runtime.getURL === 'function';
@ -66,8 +63,10 @@
const logo = document.querySelector(LOGO_SELECTOR); const logo = document.querySelector(LOGO_SELECTOR);
if (logo && !logo.dataset.capsReplaced) { if (logo && !logo.dataset.capsReplaced) {
logo.dataset.capsReplaced = '1'; logo.dataset.capsReplaced = '1';
logo.setAttribute('data-caps-ui', 'true');
logo.src = chrome.runtime.getURL('icons/caps.svg'); logo.src = chrome.runtime.getURL('icons/caps.svg');
debugLog('🖼️ [LOGO] Заменён на caps.svg', from ? '(' + from + ')' : ''); debugLog('🖼️ [LOGO] Заменён на caps.svg', from ? '(' + from + ')' : '');
applyVisibilityToElement(logo);
return true; return true;
} }
return false; return false;
@ -88,24 +87,25 @@
} }
} }
// ПРИОРИТЕТ 1: Обработка страниц noVNC (переадресация или кнопка) // ПРИОРИТЕТ 1: Обработка страниц noVNC
if (pageType.isNoVNCPage && core.NoVNCRedirect) { if (pageType.isNoVNCPage && core.NoVNCRedirect) {
const mode = core.CONFIG.novnc.mode; const mode = core.CONFIG.novnc.mode;
debugLog(`🖥️ Обнаружена страница noVNC, режим: ${mode}`); debugLog(`🖥️ Обнаружена страница noVNC, режим: ${mode}`);
const handled = core.NoVNCRedirect.handleNoVNCPage(); const handled = core.NoVNCRedirect.handleNoVNCPage();
// Если режим redirect, прерываем инициализацию (будет переадресация)
// Если режим button, продолжаем работу (только добавили кнопку)
if (mode === 'redirect' && handled) { if (mode === 'redirect' && handled) {
return; return;
} }
} }
// ПРИОРИТЕТ 2: Автологин на странице входа // ПРИОРИТЕТ 2: Автологин на странице входа
if (core.AutoLogin && core.AutoLogin.isLoginPage()) { if (core.AutoLogin) {
debugLog('🔐 Обнаружена страница логина'); if (core.AutoLogin.isLoginPage1()) {
core.AutoLogin.tryAutoLogin(); debugLog('🔐 Обнаружена страница логина типа 1');
// Продолжаем инициализацию (не прерываем) core.AutoLogin.tryAutoLogin();
} else if (core.AutoLogin.isLoginPage2()) {
debugLog('🔐 Обнаружена страница логина типа 2');
core.AutoLogin.performAutoLoginPage2();
}
} }
if (!pageType.hasAppMarker) { if (!pageType.hasAppMarker) {
@ -113,6 +113,9 @@
return; return;
} }
// Кнопка переключения видимости UI (показывается на всех CAPS-страницах)
addToggleButton();
// Настраиваем Escape // Настраиваем Escape
if (core.CloseHandler) { if (core.CloseHandler) {
core.CloseHandler.setupEscapeHandler(); core.CloseHandler.setupEscapeHandler();
@ -121,38 +124,135 @@
// Видео страница // Видео страница
if (pageType.isVideoPage && core.VideoWindow) { if (pageType.isVideoPage && core.VideoWindow) {
debugLog('🎬 ВИДЕО СТРАНИЦА'); debugLog('🎬 ВИДЕО СТРАНИЦА');
// Используем утилиту для повторных попыток
core.retryWithBackoff(() => core.VideoWindow.addButtonToVideoWindow()); core.retryWithBackoff(() => core.VideoWindow.addButtonToVideoWindow());
const videoObserver = new MutationObserver(() => { const videoObserver = new MutationObserver(() => {
core.VideoWindow.addButtonToVideoWindow(); core.VideoWindow.addButtonToVideoWindow();
}); });
videoObserver.observe(document.body, { childList: true, subtree: true });
videoObserver.observe(document.body, {
childList: true,
subtree: true
});
} }
// Главная страница // Главная страница
if (core.Mnemoschema) { if (core.Mnemoschema) {
debugLog('🏠 ГЛАВНАЯ СТРАНИЦА'); debugLog('🏠 ГЛАВНАЯ СТРАНИЦА');
const addMnemoButton = () => { const addMnemoButton = () => {
if (!core.activeSchemeButton) { if (!core.activeSchemeButton) {
core.Mnemoschema.addSchemeButton(); core.Mnemoschema.addSchemeButton();
} }
}; };
// Используем утилиту для повторных попыток
core.retryWithBackoff(addMnemoButton); 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('========== ГОТОВО =========='); 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) { if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.onMessage) {
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'PERFORM_LOGIN') { if (message.type === 'PERFORM_LOGIN') {
@ -165,6 +265,12 @@
sendResponse(result); sendResponse(result);
return true; return true;
} }
if (message.type === 'TOGGLE_UI_VISIBILITY') {
toggleVisibility();
sendResponse({ ok: true });
return true;
}
}); });
} }
})(); })();

View file

@ -22,7 +22,6 @@ function applyOptionsStrings() {
}); });
const pageTitle = getStr('options.pageTitle'); const pageTitle = getStr('options.pageTitle');
if (pageTitle) document.title = pageTitle; if (pageTitle) document.title = pageTitle;
// Версия — из manifest.json (единый источник)
const versionEl = document.getElementById('app-version'); const versionEl = document.getElementById('app-version');
if (versionEl && typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.getManifest) { if (versionEl && typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.getManifest) {
const manifest = chrome.runtime.getManifest(); const manifest = chrome.runtime.getManifest();
@ -35,69 +34,52 @@ const DEFAULT_CONFIG = {
debug: { debug: {
enabled: true, enabled: true,
modules: { modules: {
core: true, core: true, video: true, mnemo: true,
video: true, close: true, content: true, novnc: true, autologin: true
mnemo: true,
close: true,
content: true,
novnc: true,
autologin: true
} }
}, },
ports: { ports: { video: '5000', scheme: '8080' },
video: '5000', novnc: { mode: 'button', redirectDelay: 500 },
scheme: '8080' mnemo: { mode: 'host-port', skipOnDomain: false, addVideoButton: true, addSchemeButton: true },
},
novnc: {
mode: 'button',
redirectDelay: 500
},
mnemo: {
mode: 'host-port',
skipOnDomain: false,
addVideoButton: true,
addSchemeButton: true
},
autologin: { autologin: {
enabled: false, enabled: false,
username: '', profilePage1: 0,
password: '', profilePage2: 1,
autoSubmit: true, subnetRules: [],
delay: 500 profiles: [
{ name: 'Профиль 1', username: '', password: '', autoSubmit: true, delay: 500 }
]
}, },
disabledSites: [], disabledSites: [],
replaceLogo: true replaceLogo: true,
uiVisible: true
}; };
// Состояние страницы
let currentProfiles = [];
let currentSubnetRules = [];
// Элементы формы // Элементы формы
const elements = { const elements = {
// Порты
portVideo: document.getElementById('port-video'), portVideo: document.getElementById('port-video'),
portScheme: document.getElementById('port-scheme'), portScheme: document.getElementById('port-scheme'),
// noVNC (страница экрана)
novncRedirect: document.getElementById('novnc-redirect'), novncRedirect: document.getElementById('novnc-redirect'),
redirectDelay: document.getElementById('redirect-delay'), redirectDelay: document.getElementById('redirect-delay'),
redirectDelayContainer: document.getElementById('redirect-delay-container'), redirectDelayContainer: document.getElementById('redirect-delay-container'),
// Мнемосхема
mnemoMode: document.getElementById('mnemo-mode'), mnemoMode: document.getElementById('mnemo-mode'),
mnemoSkipOnDomain: document.getElementById('mnemo-skip-on-domain'), mnemoSkipOnDomain: document.getElementById('mnemo-skip-on-domain'),
mnemoAddSchemeButton: document.getElementById('mnemo-add-scheme-button'), mnemoAddSchemeButton: document.getElementById('mnemo-add-scheme-button'),
mnemoAddVideoButton: document.getElementById('mnemo-add-video-button'), mnemoAddVideoButton: document.getElementById('mnemo-add-video-button'),
// Замена логотипа
replaceLogo: document.getElementById('replace-logo'), replaceLogo: document.getElementById('replace-logo'),
uiVisible: document.getElementById('ui-visible'),
// Автологин
autologinEnabled: document.getElementById('autologin-enabled'), autologinEnabled: document.getElementById('autologin-enabled'),
autologinSettings: document.getElementById('autologin-settings'), autologinSettings: document.getElementById('autologin-settings'),
autologinUsername: document.getElementById('autologin-username'), autologinPage1Profile: document.getElementById('autologin-page1-profile'),
autologinPassword: document.getElementById('autologin-password'), autologinPage2Profile: document.getElementById('autologin-page2-profile'),
autologinAutoSubmit: document.getElementById('autologin-autosubmit'), autologinProfilesList: document.getElementById('autologin-profiles-list'),
autologinDelay: document.getElementById('autologin-delay'), autologinAddProfile: document.getElementById('autologin-add-profile'),
autologinSubnetRules: document.getElementById('autologin-subnet-rules'),
// Отладка autologinAddSubnetRule: document.getElementById('autologin-add-subnet-rule'),
debugEnabled: document.getElementById('debug-enabled'), debugEnabled: document.getElementById('debug-enabled'),
debugModules: document.getElementById('debug-modules'), debugModules: document.getElementById('debug-modules'),
debugCore: document.getElementById('debug-core'), debugCore: document.getElementById('debug-core'),
@ -107,42 +89,262 @@ const elements = {
debugContent: document.getElementById('debug-content'), debugContent: document.getElementById('debug-content'),
debugNovnc: document.getElementById('debug-novnc'), debugNovnc: document.getElementById('debug-novnc'),
debugAutologin: document.getElementById('debug-autologin'), debugAutologin: document.getElementById('debug-autologin'),
// Кнопки
saveBtn: document.getElementById('save-btn'), saveBtn: document.getElementById('save-btn'),
resetBtn: document.getElementById('reset-btn'), resetBtn: document.getElementById('reset-btn'),
// Статус
status: document.getElementById('status') 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', () => { document.addEventListener('DOMContentLoaded', () => {
applyOptionsStrings(); applyOptionsStrings();
loadSettings(); loadSettings();
}); });
// Обработчики событий
elements.saveBtn.addEventListener('click', saveSettings); elements.saveBtn.addEventListener('click', saveSettings);
elements.resetBtn.addEventListener('click', resetSettings); elements.resetBtn.addEventListener('click', resetSettings);
elements.debugEnabled.addEventListener('change', toggleDebugModules); elements.debugEnabled.addEventListener('change', toggleDebugModules);
elements.novncRedirect.addEventListener('change', toggleRedirectDelay); elements.novncRedirect.addEventListener('change', toggleRedirectDelay);
elements.autologinEnabled.addEventListener('change', toggleAutologinSettings); 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() { function loadSettings() {
console.log('Загрузка настроек...');
chrome.storage.sync.get(['config'], (result) => { chrome.storage.sync.get(['config'], (result) => {
if (chrome.runtime.lastError) { if (chrome.runtime.lastError) {
console.error('Ошибка загрузки:', chrome.runtime.lastError);
showStatus(getStr('options.statusLoadError') + ' ' + chrome.runtime.lastError.message, 'error'); showStatus(getStr('options.statusLoadError') + ' ' + chrome.runtime.lastError.message, 'error');
return; return;
} }
const config = result.config || DEFAULT_CONFIG; const config = result.config || DEFAULT_CONFIG;
console.log('Настройки получены из storage:', result);
console.log('Используемая конфигурация:', config);
// Порты // Порты
elements.portVideo.value = config.ports?.video || DEFAULT_CONFIG.ports.video; elements.portVideo.value = config.ports?.video || DEFAULT_CONFIG.ports.video;
@ -154,21 +356,44 @@ function loadSettings() {
// Мнемосхема // Мнемосхема
const mnemoMode = config.mnemo?.mode; 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.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.mnemoAddSchemeButton.checked = config.mnemo?.addSchemeButton !== false;
elements.mnemoAddVideoButton.checked = config.mnemo?.addVideoButton !== false; elements.mnemoAddVideoButton.checked = config.mnemo?.addVideoButton !== false;
// Замена логотипа // Внешний вид
elements.replaceLogo.checked = config.replaceLogo !== false; elements.replaceLogo.checked = config.replaceLogo !== false;
elements.uiVisible.checked = config.uiVisible !== false;
// Автологин // Автологин — загружаем профили (с миграцией старого формата)
elements.autologinEnabled.checked = config.autologin?.enabled || false; const al = config.autologin || {};
elements.autologinUsername.value = config.autologin?.username || ''; if (Array.isArray(al.profiles) && al.profiles.length > 0) {
elements.autologinPassword.value = config.autologin?.password || ''; currentProfiles = al.profiles.map(p => ({ ...p }));
elements.autologinAutoSubmit.checked = config.autologin?.autoSubmit !== false; } else if (al.username || al.password) {
elements.autologinDelay.value = config.autologin?.delay || DEFAULT_CONFIG.autologin.delay; // Миграция старого формата
currentProfiles = [
{ name: 'Профиль 1', username: al.username || '', password: al.password || '', autoSubmit: al.autoSubmit !== false, delay: al.delay || 500 }
];
if (al.username2 || al.password2) {
currentProfiles.push({ name: 'Профиль 2', username: al.username2 || '', password: al.password2 || '', autoSubmit: true, delay: 500 });
}
} else {
currentProfiles = [{ name: 'Профиль 1', username: '', password: '', autoSubmit: true, delay: 500 }];
}
currentSubnetRules = Array.isArray(al.subnetRules) ? al.subnetRules.map(r => ({ ...r })) : [];
elements.autologinEnabled.checked = al.enabled || false;
renderProfiles();
renderSubnetRules();
rebuildProfileSelects();
// Выставляем выбранные профили
const p1 = typeof al.profilePage1 === 'number' ? al.profilePage1 : 0;
const p2 = typeof al.profilePage2 === 'number' ? al.profilePage2 : Math.min(1, currentProfiles.length - 1);
elements.autologinPage1Profile.value = Math.min(p1, currentProfiles.length - 1);
elements.autologinPage2Profile.value = Math.min(p2, currentProfiles.length - 1);
// Отладка // Отладка
elements.debugEnabled.checked = config.debug?.enabled !== false; elements.debugEnabled.checked = config.debug?.enabled !== false;
@ -180,18 +405,15 @@ function loadSettings() {
elements.debugNovnc.checked = config.debug?.modules?.novnc !== false; elements.debugNovnc.checked = config.debug?.modules?.novnc !== false;
elements.debugAutologin.checked = config.debug?.modules?.autologin !== false; elements.debugAutologin.checked = config.debug?.modules?.autologin !== false;
// Обновляем UI
toggleDebugModules(); toggleDebugModules();
toggleRedirectDelay(); toggleRedirectDelay();
toggleAutologinSettings(); toggleAutologinSettings();
console.log('✅ Настройки загружены успешно');
}); });
} }
// Сохранение настроек // ---- Сохранение настроек ----
function saveSettings() { function saveSettings() {
// Валидация портов
const portVideo = elements.portVideo.value.trim(); const portVideo = elements.portVideo.value.trim();
const portScheme = elements.portScheme.value.trim(); const portScheme = elements.portScheme.value.trim();
@ -199,16 +421,11 @@ function saveSettings() {
showStatus(getStr('options.errorPortVideo'), 'error'); showStatus(getStr('options.errorPortVideo'), 'error');
return; return;
} }
if (!portScheme || isNaN(portScheme) || portScheme < 1 || portScheme > 65535) { if (!portScheme || isNaN(portScheme) || portScheme < 1 || portScheme > 65535) {
showStatus(getStr('options.errorPortScheme'), 'error'); showStatus(getStr('options.errorPortScheme'), 'error');
return; return;
} }
const redirectDelay = parseInt(elements.redirectDelay.value) || 500;
const autologinDelay = parseInt(elements.autologinDelay.value) || 500;
// Собираем конфигурацию
const config = { const config = {
debug: { debug: {
enabled: elements.debugEnabled.checked, enabled: elements.debugEnabled.checked,
@ -222,13 +439,10 @@ function saveSettings() {
autologin: elements.debugAutologin.checked autologin: elements.debugAutologin.checked
} }
}, },
ports: { ports: { video: portVideo, scheme: portScheme },
video: portVideo,
scheme: portScheme
},
novnc: { novnc: {
mode: elements.novncRedirect.checked ? 'redirect' : 'button', mode: elements.novncRedirect.checked ? 'redirect' : 'button',
redirectDelay: redirectDelay redirectDelay: parseInt(elements.redirectDelay.value) || 500
}, },
mnemo: { mnemo: {
mode: elements.mnemoMode.value, mode: elements.mnemoMode.value,
@ -237,15 +451,16 @@ function saveSettings() {
addSchemeButton: elements.mnemoAddSchemeButton.checked addSchemeButton: elements.mnemoAddSchemeButton.checked
}, },
replaceLogo: elements.replaceLogo.checked, replaceLogo: elements.replaceLogo.checked,
uiVisible: elements.uiVisible.checked,
autologin: { autologin: {
enabled: elements.autologinEnabled.checked, enabled: elements.autologinEnabled.checked,
username: elements.autologinUsername.value.trim(), profilePage1: parseInt(elements.autologinPage1Profile.value) || 0,
password: elements.autologinPassword.value, profilePage2: parseInt(elements.autologinPage2Profile.value) || 0,
autoSubmit: elements.autologinAutoSubmit.checked, profiles: currentProfiles.map(p => ({ ...p })),
delay: autologinDelay subnetRules: currentSubnetRules.map(r => ({ ...r }))
} }
}; };
// Сохраняем список отключённых сайтов из текущего storage
chrome.storage.sync.get(['config'], (prev) => { chrome.storage.sync.get(['config'], (prev) => {
if (prev.config && Array.isArray(prev.config.disabledSites)) { if (prev.config && Array.isArray(prev.config.disabledSites)) {
config.disabledSites = prev.config.disabledSites; config.disabledSites = prev.config.disabledSites;
@ -253,42 +468,29 @@ function saveSettings() {
config.disabledSites = []; config.disabledSites = [];
} }
// Сохраняем в chrome.storage chrome.storage.sync.set({ config }, () => {
chrome.storage.sync.set({ config }, () => { if (chrome.runtime.lastError) {
if (chrome.runtime.lastError) { showStatus(getStr('options.statusSaveError') + ' ' + chrome.runtime.lastError.message, 'error');
console.error('Ошибка сохранения:', chrome.runtime.lastError); return;
showStatus(getStr('options.statusSaveError') + ' ' + chrome.runtime.lastError.message, 'error'); }
return; showStatus(getStr('options.statusSaveOk'), 'success');
} chrome.tabs.query({}, (tabs) => {
tabs.forEach(tab => {
console.log('Настройки сохранены:', config); chrome.tabs.sendMessage(tab.id, { type: 'CONFIG_UPDATED', config }).catch(() => {});
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'))) { function resetSettings() {
return; if (!confirm(getStr('options.confirmReset'))) return;
}
currentProfiles = DEFAULT_CONFIG.autologin.profiles.map(p => ({ ...p }));
currentSubnetRules = [];
// Устанавливаем значения по умолчанию
elements.portVideo.value = DEFAULT_CONFIG.ports.video; elements.portVideo.value = DEFAULT_CONFIG.ports.video;
elements.portScheme.value = DEFAULT_CONFIG.ports.scheme; elements.portScheme.value = DEFAULT_CONFIG.ports.scheme;
elements.novncRedirect.checked = DEFAULT_CONFIG.novnc.mode === 'redirect'; elements.novncRedirect.checked = DEFAULT_CONFIG.novnc.mode === 'redirect';
@ -298,11 +500,8 @@ function resetSettings() {
elements.mnemoAddSchemeButton.checked = DEFAULT_CONFIG.mnemo.addSchemeButton; elements.mnemoAddSchemeButton.checked = DEFAULT_CONFIG.mnemo.addSchemeButton;
elements.mnemoAddVideoButton.checked = DEFAULT_CONFIG.mnemo.addVideoButton; elements.mnemoAddVideoButton.checked = DEFAULT_CONFIG.mnemo.addVideoButton;
elements.replaceLogo.checked = DEFAULT_CONFIG.replaceLogo !== false; elements.replaceLogo.checked = DEFAULT_CONFIG.replaceLogo !== false;
elements.uiVisible.checked = DEFAULT_CONFIG.uiVisible !== false;
elements.autologinEnabled.checked = DEFAULT_CONFIG.autologin.enabled; 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.debugEnabled.checked = DEFAULT_CONFIG.debug.enabled;
elements.debugCore.checked = DEFAULT_CONFIG.debug.modules.core; elements.debugCore.checked = DEFAULT_CONFIG.debug.modules.core;
elements.debugVideo.checked = DEFAULT_CONFIG.debug.modules.video; elements.debugVideo.checked = DEFAULT_CONFIG.debug.modules.video;
@ -312,9 +511,11 @@ function resetSettings() {
elements.debugNovnc.checked = DEFAULT_CONFIG.debug.modules.novnc; elements.debugNovnc.checked = DEFAULT_CONFIG.debug.modules.novnc;
elements.debugAutologin.checked = DEFAULT_CONFIG.debug.modules.autologin; elements.debugAutologin.checked = DEFAULT_CONFIG.debug.modules.autologin;
// Сохраняем renderProfiles();
renderSubnetRules();
rebuildProfileSelects();
chrome.storage.sync.set({ config: DEFAULT_CONFIG }, () => { chrome.storage.sync.set({ config: DEFAULT_CONFIG }, () => {
console.log('Настройки сброшены к значениям по умолчанию');
showStatus(getStr('options.statusResetDone'), 'success'); showStatus(getStr('options.statusResetDone'), 'success');
toggleDebugModules(); toggleDebugModules();
toggleRedirectDelay(); toggleRedirectDelay();
@ -322,7 +523,8 @@ function resetSettings() {
}); });
} }
// Показать/скрыть модули отладки (при загрузке без .expanded блок изначально скрыт) // ---- Вспомогательные функции ----
function toggleDebugModules() { function toggleDebugModules() {
if (elements.debugEnabled.checked) { if (elements.debugEnabled.checked) {
elements.debugModules.classList.add('expanded'); elements.debugModules.classList.add('expanded');
@ -331,16 +533,10 @@ function toggleDebugModules() {
} }
} }
// Показать/скрыть поле задержки переадресации
function toggleRedirectDelay() { function toggleRedirectDelay() {
if (elements.novncRedirect.checked) { elements.redirectDelayContainer.style.display = elements.novncRedirect.checked ? 'block' : 'none';
elements.redirectDelayContainer.style.display = 'block';
} else {
elements.redirectDelayContainer.style.display = 'none';
}
} }
// Показать/скрыть настройки автологина
function toggleAutologinSettings() { function toggleAutologinSettings() {
if (elements.autologinEnabled.checked) { if (elements.autologinEnabled.checked) {
elements.autologinSettings.classList.remove('disabled'); elements.autologinSettings.classList.remove('disabled');
@ -349,48 +545,13 @@ function toggleAutologinSettings() {
} }
} }
// Показать статус сообщение
function showStatus(message, type = 'success') { function showStatus(message, type = 'success') {
elements.status.textContent = message; elements.status.textContent = message;
elements.status.className = `status ${type}`; elements.status.className = `status ${type}`;
elements.status.classList.remove('hidden'); elements.status.classList.remove('hidden');
setTimeout(() => { elements.status.classList.add('hidden'); }, 5000);
// Автоматически скрыть через 5 секунд
setTimeout(() => {
elements.status.classList.add('hidden');
}, 5000);
} }
// Валидация при вводе (только цифры для портов) // Валидация при вводе портов
elements.portVideo.addEventListener('input', (e) => { elements.portVideo.addEventListener('input', (e) => { e.target.value = e.target.value.replace(/[^0-9]/g, ''); });
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, ''); });
});
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 недоступен!');
}

View file

@ -3,13 +3,15 @@
const DEFAULT_CONFIG = { const DEFAULT_CONFIG = {
debug: { enabled: true }, debug: { enabled: true },
disabledSites: [], disabledSites: [],
autologin: { username: '', password: '', delay: 500 } uiVisible: true,
autologin: { profiles: [], profilePage1: 0 }
}; };
const el = { const el = {
disableOnSite: document.getElementById('disable-on-site'), disableOnSite: document.getElementById('disable-on-site'),
disableOnSiteLabel: document.getElementById('disable-on-site-label'), disableOnSiteLabel: document.getElementById('disable-on-site-label'),
debug: document.getElementById('debug'), debug: document.getElementById('debug'),
uiVisible: document.getElementById('ui-visible'),
btnLogin: document.getElementById('btn-login'), btnLogin: document.getElementById('btn-login'),
loginStatus: document.getElementById('login-status'), loginStatus: document.getElementById('login-status'),
reloadRow: document.getElementById('reload-row'), reloadRow: document.getElementById('reload-row'),
@ -93,6 +95,8 @@ async function init() {
const debug = config.debug || DEFAULT_CONFIG.debug; const debug = config.debug || DEFAULT_CONFIG.debug;
el.debug.checked = debug.enabled !== false; el.debug.checked = debug.enabled !== false;
el.uiVisible.checked = config.uiVisible !== false;
function showReloadButton() { function showReloadButton() {
if (el.reloadRow) el.reloadRow.classList.remove('hidden'); if (el.reloadRow) el.reloadRow.classList.remove('hidden');
} }
@ -117,6 +121,15 @@ async function init() {
showReloadButton(); 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 () => { el.btnReload.addEventListener('click', async () => {
const tab = await getActiveTab(); const tab = await getActiveTab();
if (tab && tab.id) { if (tab && tab.id) {
@ -132,9 +145,11 @@ async function init() {
return; return;
} }
const config = await loadConfig(); const config = await loadConfig();
const user = (config.autologin || {}).username; const al = config.autologin || {};
const pass = (config.autologin || {}).password; const profiles = Array.isArray(al.profiles) ? al.profiles : [];
if (!user || !pass) { const profileIdx = al.profilePage1 || 0;
const profile = profiles[profileIdx] || profiles[0];
if (!profile || !profile.username || !profile.password) {
showLoginStatus(getStr('popup.statusNoCredentials'), 'error'); showLoginStatus(getStr('popup.statusNoCredentials'), 'error');
return; return;
} }
@ -143,7 +158,8 @@ async function init() {
try { try {
const response = await chrome.tabs.sendMessage(tab.id, { type: 'PERFORM_LOGIN' }); const response = await chrome.tabs.sendMessage(tab.id, { type: 'PERFORM_LOGIN' });
if (response && response.ok) { if (response && response.ok) {
showLoginStatus(getStr('popup.statusLoginOk'), 'success'); const profileName = response.profileName ? ` (${response.profileName})` : '';
showLoginStatus(getStr('popup.statusLoginOk') + profileName, 'success');
} else { } else {
const err = (response && response.error) || 'error'; const err = (response && response.error) || 'error';
const msg = err === 'not_login_page' ? getStr('popup.statusNotLoginPage') : err === 'no_credentials' ? getStr('popup.statusNoCredentialsShort') : getStr('popup.statusLoginError'); const msg = err === 'not_login_page' ? getStr('popup.statusNotLoginPage') : err === 'no_credentials' ? getStr('popup.statusNoCredentialsShort') : getStr('popup.statusLoginError');

View file

@ -9,6 +9,7 @@ window.CAPS_STRINGS = {
siteWorking: "Расширение работает на этом сайте", siteWorking: "Расширение работает на этом сайте",
siteDisabled: "Расширение отключено на этом сайте", siteDisabled: "Расширение отключено на этом сайте",
debug: "Дебаг", debug: "Дебаг",
uiVisible: "Показывать улучшения",
login: "Автологин", login: "Автологин",
reload: "🔄 Перезагрузить страницу", reload: "🔄 Перезагрузить страницу",
fullSettings: "⚙️ Полные настройки", fullSettings: "⚙️ Полные настройки",
@ -61,7 +62,7 @@ window.CAPS_STRINGS = {
// Автологин // Автологин
autologinTitle: "🔐 Автологин", autologinTitle: "🔐 Автологин",
autologinEnabledLabel: "Включить автозаполнение", autologinEnabledLabel: "Включить автозаполнение",
autologinEnabledDesc: "Автоматически заполнять логин и пароль на :5000/login", autologinEnabledDesc: "Автоматически заполнять логин и пароль на странице входа",
autologinUsernameLabel: "Логин", autologinUsernameLabel: "Логин",
autologinUsernameDesc: "", autologinUsernameDesc: "",
autologinUsernamePlaceholder: "admin", autologinUsernamePlaceholder: "admin",
@ -73,6 +74,28 @@ window.CAPS_STRINGS = {
autologinDelayLabel: "Задержка (мс)", autologinDelayLabel: "Задержка (мс)",
autologinDelayPlaceholder: "500", 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: "🐛 Отладка", debugTitle: "🐛 Отладка",
debugEnabledLabel: "Включить логи в консоль", debugEnabledLabel: "Включить логи в консоль",

View file

@ -11,61 +11,52 @@
debugLog('🔐 Загрузка модуля...'); debugLog('🔐 Загрузка модуля...');
// Проверка, является ли страница страницей логина // Получить профиль по индексу
function isLoginPage() { function getProfile(index) {
// Проверка 1: Порт 5000 const profiles = core.CONFIG.autologin.profiles;
const port = window.location.port; if (!Array.isArray(profiles) || profiles.length === 0) return null;
if (port !== '5000') { const idx = (typeof index === 'number' && index >= 0 && index < profiles.length) ? index : 0;
return false; return profiles[idx] || profiles[0];
} }
// Проверка 2: URL содержит /login // Найти правило подсети для текущего хоста
const path = window.location.pathname; function getSubnetRule() {
if (!path.includes('/login')) { const host = window.location.hostname;
return false; const rules = core.CONFIG.autologin.subnetRules || [];
} return rules.find(r => r.subnet && host.startsWith(r.subnet)) || null;
}
// Проверка 3: Наличие полей логина и пароля // Проверка страницы логина типа 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 usernameField = document.querySelector('input[name="username"]');
const passwordField = document.querySelector('input[name="password"]'); const passwordField = document.querySelector('input[name="password"]');
return !!(usernameField && passwordField);
}
if (!usernameField || !passwordField) { // Проверка страницы логина типа 2 (auth-logo.png + react-notification-root + noscript)
return false; 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; return true;
} }
// Главная функция автологина // Заполнить форму логина данными профиля и нажать кнопку входа
function performAutoLogin() { function performLoginWithProfile(profile) {
debugLog('========== ПРОВЕРКА АВТОЛОГИНА =========='); if (!profile || !profile.username || !profile.password) {
debugLog('⚠️ Профиль не задан или пустые учётные данные');
// Проверяем, включен ли автологин
if (!core.CONFIG.autologin.enabled) {
debugLog('⏹ Автологин отключен в настройках');
return false; 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 usernameField = document.querySelector('input[name="username"]');
const passwordField = document.querySelector('input[name="password"]'); const passwordField = document.querySelector('input[name="password"]');
const submitButton = document.querySelector('button[type="submit"]'); const submitButton = document.querySelector('button[type="submit"]');
@ -75,24 +66,22 @@
return false; return false;
} }
// Заполняем поля debugLog(`✍️ Заполнение полей (профиль: ${profile.name || 'без имени'})...`);
debugLog('✍️ Заполнение полей...');
usernameField.value = username;
passwordField.value = password;
// Генерируем события для совместимости с JS-фреймворками // 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('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('input', { bubbles: true }));
passwordField.dispatchEvent(new Event('change', { bubbles: true }));
debugLog('✅ Поля заполнены'); debugLog('✅ Поля заполнены');
// Автоматический вход, если включен const autoSubmit = typeof profile.autoSubmit === 'boolean' ? profile.autoSubmit : true;
if (core.CONFIG.autologin.autoSubmit && submitButton) { if (autoSubmit && submitButton) {
const delay = core.CONFIG.autologin.delay || 500; const delay = profile.delay || 500;
debugLog(`⏱️ Вход через ${delay}мс...`); debugLog(`⏱️ Вход через ${delay}мс...`);
setTimeout(() => { setTimeout(() => {
debugLog('🚀 Нажатие кнопки "Вход"'); debugLog('🚀 Нажатие кнопки "Вход"');
submitButton.click(); submitButton.click();
@ -101,13 +90,48 @@
debugLog(' Автоматический вход отключен'); debugLog(' Автоматический вход отключен');
} }
debugLog('========== АВТОЛОГИН ЗАВЕРШЕН ==========');
return true; 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) { function tryAutoLogin(attempts = 3) {
debugLog(`🔄 Попытка автологина (осталось попыток: ${attempts})`); debugLog(`🔄 Попытка автологина типа 1 (осталось попыток: ${attempts})`);
const success = performAutoLogin(); const success = performAutoLogin();
@ -116,41 +140,89 @@
} }
} }
// Однократный вход по кнопке (без проверки 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() { function performLoginOnce() {
if (!isLoginPage()) { if (!isLoginPage1() && !isLoginPage2()) {
return { ok: false, error: 'not_login_page' }; return { ok: false, error: 'not_login_page' };
} }
const username = core.CONFIG.autologin.username;
const password = core.CONFIG.autologin.password; const isPage2 = isLoginPage2();
if (!username || !password) { 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' }; return { ok: false, error: 'no_credentials' };
} }
const usernameField = document.querySelector('input[name="username"]'); const usernameField = document.querySelector('input[name="username"]');
const passwordField = document.querySelector('input[name="password"]'); const passwordField = document.querySelector('input[name="password"]');
const submitButton = document.querySelector('button[type="submit"]');
if (!usernameField || !passwordField) { if (!usernameField || !passwordField) {
return { ok: false, error: 'no_fields' }; 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('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('input', { bubbles: true }));
passwordField.dispatchEvent(new Event('change', { bubbles: true }));
const submitButton = document.querySelector('button[type="submit"]');
if (submitButton) { if (submitButton) {
const delay = core.CONFIG.autologin.delay || 500; const delay = profile.delay || 500;
setTimeout(() => submitButton.click(), delay); setTimeout(() => submitButton.click(), delay);
} }
return { ok: true };
return { ok: true, profileName: profile.name || '' };
} }
// Публичный API // Публичный API
window.VideoIPRedirect.AutoLogin = { window.VideoIPRedirect.AutoLogin = {
performAutoLogin: performAutoLogin,
tryAutoLogin: tryAutoLogin, tryAutoLogin: tryAutoLogin,
isLoginPage: isLoginPage, performAutoLoginPage2: performAutoLoginPage2,
performLoginOnce: performLoginOnce isLoginPage1: isLoginPage1,
isLoginPage2: isLoginPage2,
performLoginOnce: performLoginOnce,
getProfile: getProfile,
getSubnetRule: getSubnetRule
}; };
debugLog('✅ Модуль загружен'); debugLog('✅ Модуль загружен');

View file

@ -40,17 +40,22 @@ let CONFIG = {
// Настройки автологина // Настройки автологина
autologin: { autologin: {
enabled: false, // Включить/выключить автологин enabled: false, // Включить/выключить автологин
username: '', // Логин для автозаполнения profilePage1: 0, // Индекс профиля для страницы логина типа 1 (порт 5000, /login)
password: '', // Пароль для автозаполнения profilePage2: 1, // Индекс профиля для страницы логина типа 2 (auth-logo.png)
autoSubmit: true, // Автоматически нажимать кнопку "Вход" subnetRules: [], // Правила по подсетям: [{ subnet, action: 'skip'|'profile', profileIndex }]
delay: 500 // Задержка перед автовходом (мс) profiles: [ // Массив профилей учётных данных
{ name: 'Профиль 1', username: '', password: '', autoSubmit: true, delay: 500 }
]
}, },
// Сайты, на которых расширение отключено (массив hostname, например ['example.com']) // Сайты, на которых расширение отключено (массив hostname, например ['example.com'])
disabledSites: [], disabledSites: [],
// Замена логотипа CleverPark на иконку CAPS (icons/caps.svg) // Замена логотипа CleverPark на иконку CAPS (icons/caps.svg)
replaceLogo: true replaceLogo: true,
// Показывать ли улучшения расширения (кнопки, логотип)
uiVisible: true
}; };
// Обновление конфигурации (изменяет существующий объект, а не создает новый) // Обновление конфигурации (изменяет существующий объект, а не создает новый)
@ -87,7 +92,25 @@ function updateConfig(newConfig) {
// Обновляем autologin // Обновляем autologin
if (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 // Обновляем disabledSites
@ -100,6 +123,11 @@ function updateConfig(newConfig) {
CONFIG.replaceLogo = newConfig.replaceLogo; 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_VIDEO = CONFIG.ports.video;
window.VideoIPRedirect.TARGET_PORT_SCHEME = CONFIG.ports.scheme; window.VideoIPRedirect.TARGET_PORT_SCHEME = CONFIG.ports.scheme;

View file

@ -139,6 +139,7 @@
wrapper.className = 'scheme-buttons-wrapper'; wrapper.className = 'scheme-buttons-wrapper';
wrapper.style.display = 'inline-block'; wrapper.style.display = 'inline-block';
wrapper.style.verticalAlign = 'middle'; wrapper.style.verticalAlign = 'middle';
wrapper.setAttribute('data-caps-ui', 'true');
if (addVideo) wrapper.appendChild(createVideoButton(host)); if (addVideo) wrapper.appendChild(createVideoButton(host));
if (addScheme) wrapper.appendChild(createButton(mnemoLink, host)); if (addScheme) wrapper.appendChild(createButton(mnemoLink, host));
mnemoLink.parentNode.replaceChild(wrapper, mnemoLink); mnemoLink.parentNode.replaceChild(wrapper, mnemoLink);

View file

@ -104,6 +104,8 @@
'novnc-settings-btn' 'novnc-settings-btn'
); );
button.setAttribute('data-caps-ui', 'true');
// Дополнительные стили для позиционирования // Дополнительные стили для позиционирования
button.style.position = 'fixed'; button.style.position = 'fixed';
button.style.top = '20px'; button.style.top = '20px';

View file

@ -80,6 +80,7 @@
}, },
'video-redirect-btn' 'video-redirect-btn'
); );
button.setAttribute('data-caps-ui', 'true');
const p = header.querySelector('p'); const p = header.querySelector('p');
if (p) { if (p) {

View file

@ -99,19 +99,29 @@
</div> </div>
</section> </section>
<!-- Замена логотипа --> <!-- Замена логотипа и видимость UI -->
<section class="settings-section"> <section class="settings-section">
<h2 data-i18n="options.appearanceTitle">🖼️ Внешний вид</h2> <h2 data-i18n="options.appearanceTitle">🖼️ Внешний вид</h2>
<div class="setting-item"> <div class="setting-item">
<label class="switch-label"> <label class="switch-label">
<span class="label-text" data-i18n="options.replaceLogoLabel">Заменять логотип CleverPark на иконку CAPS</span> <span class="label-text" data-i18n="options.replaceLogoLabel">Заменять логотип CleverPark на иконку CAPS</span>
<span class="label-description" data-i18n="options.replaceLogoDesc">На главной странице показывать icons/caps.svg вместо логотипа CleverPark</span> <span class="label-description" data-i18n="options.replaceLogoDesc"></span>
<label class="switch"> <label class="switch">
<input type="checkbox" id="replace-logo"> <input type="checkbox" id="replace-logo">
<span class="slider"></span> <span class="slider"></span>
</label> </label>
</label> </label>
</div> </div>
<div class="setting-item">
<label class="switch-label">
<span class="label-text" data-i18n="options.appearanceUiVisibleLabel">Показывать улучшения расширения</span>
<span class="label-description" data-i18n="options.appearanceUiVisibleDesc">Кнопки, замена логотипа. Можно также переключить через Shift+F1</span>
<label class="switch">
<input type="checkbox" id="ui-visible">
<span class="slider"></span>
</label>
</label>
</div>
</section> </section>
<!-- Настройки автологина --> <!-- Настройки автологина -->
@ -120,7 +130,7 @@
<div class="setting-item"> <div class="setting-item">
<label class="switch-label"> <label class="switch-label">
<span class="label-text" data-i18n="options.autologinEnabledLabel">Включить автозаполнение</span> <span class="label-text" data-i18n="options.autologinEnabledLabel">Включить автозаполнение</span>
<span class="label-description" data-i18n="options.autologinEnabledDesc">Автоматически заполнять логин и пароль на /login (порт 5000)</span> <span class="label-description" data-i18n="options.autologinEnabledDesc">Автоматически заполнять логин и пароль на странице входа</span>
<label class="switch"> <label class="switch">
<input type="checkbox" id="autologin-enabled"> <input type="checkbox" id="autologin-enabled">
<span class="slider"></span> <span class="slider"></span>
@ -129,36 +139,30 @@
</div> </div>
<div id="autologin-settings" class="sub-settings"> <div id="autologin-settings" class="sub-settings">
<!-- Назначение профилей на типы страниц -->
<div class="setting-item"> <div class="setting-item">
<label for="autologin-username"> <label for="autologin-page1-profile">
<span class="label-text" data-i18n="options.autologinUsernameLabel">Логин</span> <span class="label-text" data-i18n="options.autologinPage1ProfileLabel">Профиль для страницы входа (тип 1, порт 5000)</span>
<span class="label-description" data-i18n="options.autologinUsernameDesc">Имя пользователя для автоматического входа</span>
</label> </label>
<input type="text" id="autologin-username" data-i18n-placeholder="options.autologinUsernamePlaceholder" placeholder="admin" autocomplete="off"> <select id="autologin-page1-profile"></select>
</div> </div>
<div class="setting-item"> <div class="setting-item">
<label for="autologin-password"> <label for="autologin-page2-profile">
<span class="label-text" data-i18n="options.autologinPasswordLabel">Пароль</span> <span class="label-text" data-i18n="options.autologinPage2ProfileLabel">Профиль для страницы входа (тип 2, auth-logo)</span>
<span class="label-description" data-i18n="options.autologinPasswordDesc">Пароль для автологина (только для тестовых/локальных систем)</span>
</label> </label>
<input type="text" id="autologin-password" class="password-field" data-i18n-placeholder="options.autologinPasswordPlaceholder" placeholder="••••••••" autocomplete="off"> <select id="autologin-page2-profile"></select>
<small class="hint" data-i18n="options.autologinPasswordHint">⚠️ Пароль хранится в настройках Chrome. Не используйте важные или личные пароли.</small>
</div>
<div class="setting-item inline">
<label class="switch-label">
<span class="label-text" data-i18n="options.autologinAutoSubmitLabel">Автоматически нажимать "Вход"</span>
<label class="switch">
<input type="checkbox" id="autologin-autosubmit">
<span class="slider"></span>
</label>
</label>
</div>
<div class="setting-item inline">
<label for="autologin-delay">
<span class="label-text" data-i18n="options.autologinDelayLabel">Задержка (мс)</span>
</label>
<input type="number" id="autologin-delay" data-i18n-placeholder="options.autologinDelayPlaceholder" placeholder="500" min="0" max="5000" step="100">
</div> </div>
<!-- Динамический список профилей -->
<h3 data-i18n="options.autologinProfilesTitle">Профили учётных данных</h3>
<div id="autologin-profiles-list"></div>
<button type="button" id="autologin-add-profile" class="btn btn-secondary" style="margin-top:6px;padding:6px 12px;font-size:13px;" data-i18n="options.autologinAddProfile"> Добавить профиль</button>
<!-- Правила подсетей -->
<h3 style="margin-top:16px;" data-i18n="options.autologinSubnetRulesTitle">Правила для подсетей</h3>
<p style="font-size:12px;color:#888;margin-bottom:8px;" data-i18n="options.autologinSubnetRulesDesc">При совпадении хоста с подсетью — пропустить или использовать другой профиль</p>
<div id="autologin-subnet-rules"></div>
<button type="button" id="autologin-add-subnet-rule" class="btn btn-secondary" style="margin-top:6px;padding:6px 12px;font-size:13px;" data-i18n="options.autologinAddSubnetRule"> Добавить правило</button>
</div> </div>
</section> </section>
</div> </div>

View file

@ -26,6 +26,13 @@
<span class="switch-track"></span> <span class="switch-track"></span>
</span> </span>
</label> </label>
<label class="row switch-row">
<span class="label" data-i18n="popup.uiVisible"></span>
<span class="switch-wrap">
<input type="checkbox" id="ui-visible" class="switch-input" />
<span class="switch-track"></span>
</span>
</label>
<div class="row row-btn"> <div class="row row-btn">
<button type="button" id="btn-login" class="btn btn-primary" data-i18n="popup.login"></button> <button type="button" id="btn-login" class="btn btn-primary" data-i18n="popup.login"></button>
<span id="login-status" class="status-text"></span> <span id="login-status" class="status-text"></span>