Compare commits
5 commits
7d2c977a9f
...
221f25c82c
| Author | SHA1 | Date | |
|---|---|---|---|
| 221f25c82c | |||
| 38b30e4f25 | |||
| a2fb35f149 | |||
| 76571fafc5 | |||
| a11134f303 |
28 changed files with 1487 additions and 351 deletions
27
$remote
Normal file
27
$remote
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
Split-Path : Cannot bind argument to parameter 'Path' because it is null.
|
||||||
|
At line:1 char:177
|
||||||
|
+ ... ; $EXT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path; $manif ...
|
||||||
|
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
+ CategoryInfo : InvalidData: (:) [Split-Path], ParameterBindingValidationException
|
||||||
|
+ FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.SplitPathCo
|
||||||
|
mmand
|
||||||
|
|
||||||
|
Join-Path : Cannot bind argument to parameter 'Path' because it is null.
|
||||||
|
At line:1 char:242
|
||||||
|
+ ... n.MyCommand.Path; $manifest = Get-Content (Join-Path $EXT_DIR 'manife ...
|
||||||
|
+ ~~~~~~~~
|
||||||
|
+ CategoryInfo : InvalidData: (:) [Join-Path], ParameterBindingValidationException
|
||||||
|
+ FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.JoinPathCom
|
||||||
|
mand
|
||||||
|
|
||||||
|
Local: Remote: 2.7
|
||||||
|
[INFO] Updating - ...
|
||||||
|
Expand-Archive : Cannot validate argument on parameter 'DestinationPath'. The argument is null or empty. Provide an arg
|
||||||
|
ument that is not null or empty, and then try the command again.
|
||||||
|
At line:1 char:756
|
||||||
|
+ ... OutFile $tmp; Expand-Archive $tmp -DestinationPath $EXT_DIR -Force; ...
|
||||||
|
+ ~~~~~~~~
|
||||||
|
+ CategoryInfo : InvalidData: (:) [Expand-Archive], ParameterBindingValidationException
|
||||||
|
+ FullyQualifiedErrorId : ParameterArgumentValidationError,Expand-Archive
|
||||||
|
|
||||||
|
[OK] Done. Reload extension in chrome://extensions
|
||||||
8
.claude/settings.local.json
Normal file
8
.claude/settings.local.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(python3 -c \":*)",
|
||||||
|
"Bash(python3:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
13
CHANGELOG.md
Normal file
13
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [2.8] — 2026-03-23
|
||||||
|
|
||||||
|
### Добавлено
|
||||||
|
|
||||||
|
- **Видимость расширения** — новая настройка «Показывать улучшения» в popup и в разделе «Внешний вид» в options; горячая клавиша Shift+F1 для быстрого переключения прямо на странице
|
||||||
|
- **Профили учётных данных для автологина** — Возможность создания нескольких профилей. Каждому профилю задается свой логин и пароль. Их можно использовать для привязки к своим подсетям.
|
||||||
|
- **Правила подсетей для автологина** — можно задать поведение для конкретного диапазона IP: «пропустить автологин» или «использовать другой профиль»
|
||||||
|
- **Поддержка страницы логина типа 2** — помимо стандартной страницы на порту `:5000/login`, добавлен автологин в капс и на мнемосхему, попытка логина осуществляется 1 раз
|
||||||
|
|
||||||
289
DEPLOY.md
Normal file
289
DEPLOY.md
Normal file
|
|
@ -0,0 +1,289 @@
|
||||||
|
# Деплой CAPS Enhancer — Self-Hosted Auto-Update
|
||||||
|
|
||||||
|
## Как это работает
|
||||||
|
|
||||||
|
```
|
||||||
|
[Сервер] [Браузер пользователя]
|
||||||
|
update.xml <── Chrome опрашивает каждые ~5 часов
|
||||||
|
caps-enhancer.crx <── Chrome скачивает при новой версии
|
||||||
|
```
|
||||||
|
|
||||||
|
Chrome сравнивает `version` в `update.xml` с версией установленного расширения.
|
||||||
|
Если версия на сервере выше — скачивает `.crx` и устанавливает обновление автоматически.
|
||||||
|
|
||||||
|
Пользователь устанавливает расширение **один раз** через реестр Windows — дальше всё автоматически.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Часть 1: Первоначальная упаковка (делается один раз)
|
||||||
|
|
||||||
|
### Шаг 1. Прописать URL обновления в manifest.json
|
||||||
|
|
||||||
|
Открой `manifest.json` и замени `YOUR_DOMAIN` на реальный домен:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"update_url": "https://updates.example.com/caps-enhancer/update.xml",
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 2. Упаковать расширение в .crx
|
||||||
|
|
||||||
|
**Способ A — через bat-скрипт (рекомендуется):**
|
||||||
|
|
||||||
|
Запусти `pack-crx.bat` из папки проекта. Скрипт сам найдёт Chrome и упакует расширение.
|
||||||
|
|
||||||
|
**Способ B — вручную через Chrome:**
|
||||||
|
|
||||||
|
1. Открой `chrome://extensions`
|
||||||
|
2. Включи "Режим разработчика" (правый верхний угол)
|
||||||
|
3. Нажми "Упаковать расширение"
|
||||||
|
4. В поле "Корневой каталог расширения" укажи папку `caps-enhancer`
|
||||||
|
5. Поле "Файл закрытого ключа" оставь пустым при первой упаковке
|
||||||
|
6. Нажми "Упаковать расширение"
|
||||||
|
|
||||||
|
Chrome создаст два файла **в папке рядом с `caps-enhancer`**:
|
||||||
|
- `caps-enhancer.crx` — файл расширения
|
||||||
|
- `caps-enhancer.pem` — приватный ключ
|
||||||
|
|
||||||
|
> **КРИТИЧНО:** Сохрани `caps-enhancer.pem` в надёжное место!
|
||||||
|
> Этот ключ определяет ID расширения. Если потеряешь — ID изменится,
|
||||||
|
> и все установленные у пользователей копии придётся переустанавливать вручную.
|
||||||
|
|
||||||
|
### Шаг 3. Узнать ID расширения
|
||||||
|
|
||||||
|
**После первой упаковки:**
|
||||||
|
|
||||||
|
1. Открой `chrome://extensions`
|
||||||
|
2. Нажми "Загрузить распакованное расширение" → выбери папку `caps-enhancer`
|
||||||
|
3. Скопируй ID расширения (длинная строка из 32 символов, например `abcdefghijklmnopqrstuvwxyzabcdef`)
|
||||||
|
|
||||||
|
**Или через файл ключа (если Chrome уже закрыт):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# На Linux/Mac:
|
||||||
|
openssl rsa -in caps-enhancer.pem -pubout | openssl dgst -sha256 -binary | base32 | head -c 32 | tr '[:upper:]' '[:lower:]'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 4. Обновить update.xml
|
||||||
|
|
||||||
|
Открой `server/update.xml` и замени:
|
||||||
|
- `EXTENSION_ID` — ID из предыдущего шага
|
||||||
|
- `YOUR_DOMAIN` — твой домен
|
||||||
|
- `version` — текущая версия (из manifest.json)
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
|
||||||
|
<app appid='abcdefghijklmnopqrstuvwxyzabcdef'>
|
||||||
|
<updatecheck
|
||||||
|
status='ok'
|
||||||
|
codebase='https://updates.example.com/caps-enhancer/caps-enhancer.crx'
|
||||||
|
version='2.7'
|
||||||
|
/>
|
||||||
|
</app>
|
||||||
|
</gupdate>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Часть 2: Настройка сервера
|
||||||
|
|
||||||
|
### Требования к серверу
|
||||||
|
|
||||||
|
- Любой веб-сервер (nginx, Apache, Caddy, даже GitHub Pages)
|
||||||
|
- **HTTPS обязателен** — Chrome отказывается скачивать `.crx` по HTTP
|
||||||
|
- Корректные MIME-типы (см. ниже)
|
||||||
|
|
||||||
|
### Вариант A: nginx
|
||||||
|
|
||||||
|
Скопируй `server/nginx.conf` на сервер и замени `YOUR_DOMAIN`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo cp server/nginx.conf /etc/nginx/sites-available/caps-enhancer
|
||||||
|
sudo ln -s /etc/nginx/sites-available/caps-enhancer /etc/nginx/sites-enabled/
|
||||||
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
Структура файлов на сервере:
|
||||||
|
```
|
||||||
|
/var/www/caps-enhancer/
|
||||||
|
└── caps-enhancer/
|
||||||
|
├── update.xml
|
||||||
|
└── caps-enhancer.crx
|
||||||
|
```
|
||||||
|
|
||||||
|
### Вариант B: Apache
|
||||||
|
|
||||||
|
Добавь в `.htaccess` или конфиг:
|
||||||
|
|
||||||
|
```apache
|
||||||
|
<Directory /var/www/caps-enhancer>
|
||||||
|
AddType application/x-chrome-extension .crx
|
||||||
|
AddType application/xml .xml
|
||||||
|
|
||||||
|
<Files "update.xml">
|
||||||
|
Header set Cache-Control "no-cache, no-store, must-revalidate"
|
||||||
|
Header set Pragma "no-cache"
|
||||||
|
Header set Expires 0
|
||||||
|
</Files>
|
||||||
|
</Directory>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Вариант C: Caddy (самый простой, HTTPS автоматически)
|
||||||
|
|
||||||
|
```
|
||||||
|
updates.example.com {
|
||||||
|
root * /var/www/caps-enhancer
|
||||||
|
file_server
|
||||||
|
|
||||||
|
@xml path *.xml
|
||||||
|
header @xml Cache-Control "no-cache, no-store, must-revalidate"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Вариант D: GitHub Pages / любой статический хостинг
|
||||||
|
|
||||||
|
Положи `update.xml` и `caps-enhancer.crx` в репозиторий.
|
||||||
|
GitHub Pages раздаёт файлы по HTTPS автоматически.
|
||||||
|
|
||||||
|
> Убедись что хостинг отдаёт `.crx` с типом `application/x-chrome-extension`
|
||||||
|
> или `application/octet-stream`. GitHub Pages обычно справляется.
|
||||||
|
|
||||||
|
### Загрузка файлов на сервер
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# С локальной машины через scp
|
||||||
|
scp server/caps-enhancer.crx server/update.xml user@your-server:/var/www/caps-enhancer/caps-enhancer/
|
||||||
|
|
||||||
|
# Или через rsync
|
||||||
|
rsync -av server/caps-enhancer.crx server/update.xml user@your-server:/var/www/caps-enhancer/caps-enhancer/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проверка сервера
|
||||||
|
|
||||||
|
После загрузки проверь:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# update.xml должен вернуть XML с версией
|
||||||
|
curl -I https://updates.example.com/caps-enhancer/update.xml
|
||||||
|
|
||||||
|
# .crx должен вернуть 200 и Content-Type: application/x-chrome-extension
|
||||||
|
curl -I https://updates.example.com/caps-enhancer/caps-enhancer.crx
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Часть 3: Установка на компьютеры пользователей
|
||||||
|
|
||||||
|
Chrome не позволяет устанавливать сторонние расширения вручную через stable-канал.
|
||||||
|
Единственный способ — через **Windows Policy (реестр)**.
|
||||||
|
|
||||||
|
### Способ A: .reg файл (для ручной установки)
|
||||||
|
|
||||||
|
1. Открой `server/install-client.reg`
|
||||||
|
2. Замени `EXTENSION_ID` и `YOUR_DOMAIN`
|
||||||
|
3. Дай пользователю этот файл
|
||||||
|
4. Пользователь дважды кликает на `.reg` → соглашается → перезапускает Chrome
|
||||||
|
|
||||||
|
```reg
|
||||||
|
[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Google\Chrome\ExtensionInstallForcelist]
|
||||||
|
"1"="abcdefghijklmnopqrstuvwxyzabcdef;https://updates.example.com/caps-enhancer/update.xml"
|
||||||
|
```
|
||||||
|
|
||||||
|
> Требует прав администратора (HKLM).
|
||||||
|
> Для установки без прав — использовать HKCU (только для текущего пользователя).
|
||||||
|
|
||||||
|
### Способ B: PowerShell скрипт
|
||||||
|
|
||||||
|
Отредактируй `server/install-client.ps1` (замени ID и домен), запусти от имени администратора:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Запуск от имени администратора:
|
||||||
|
Start-Process powershell -Verb RunAs -ArgumentList "-File C:\path\to\install-client.ps1"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Способ C: Групповые политики (GPO) — для домена
|
||||||
|
|
||||||
|
1. Открой **Group Policy Management Console** (gpmc.msc)
|
||||||
|
2. Создай или отредактируй GPO
|
||||||
|
3. Перейди: `Computer Configuration` → `Administrative Templates` → `Google` → `Google Chrome` → `Extensions`
|
||||||
|
4. Включи политику **"Configure the list of force-installed apps and extensions"**
|
||||||
|
5. Добавь строку: `abcdefghijklmnopqrstuvwxyzabcdef;https://updates.example.com/caps-enhancer/update.xml`
|
||||||
|
|
||||||
|
> Если шаблонов Google нет — скачай ADMX: https://chromeenterprise.google/intl/ru/browser/download/
|
||||||
|
|
||||||
|
### Что видит пользователь после установки
|
||||||
|
|
||||||
|
- Chrome автоматически устанавливает расширение при следующем запуске
|
||||||
|
- Иконка появляется в панели расширений
|
||||||
|
- Пользователь **не может удалить** принудительно установленное расширение (managed)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Часть 4: Выпуск обновления
|
||||||
|
|
||||||
|
При каждом обновлении расширения:
|
||||||
|
|
||||||
|
### 1. Обнови версию в manifest.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
"version": "2.8"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Упакуй расширение с тем же ключом
|
||||||
|
|
||||||
|
Запусти `pack-crx.bat` — скрипт автоматически использует существующий `caps-enhancer.pem`.
|
||||||
|
|
||||||
|
### 3. Обнови update.xml
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<updatecheck
|
||||||
|
status='ok'
|
||||||
|
codebase='https://updates.example.com/caps-enhancer/caps-enhancer.crx'
|
||||||
|
version='2.8'
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Загрузи на сервер
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scp caps-enhancer.crx update.xml user@your-server:/var/www/caps-enhancer/caps-enhancer/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Готово
|
||||||
|
|
||||||
|
Chrome проверяет обновления примерно каждые **5 часов** в фоне.
|
||||||
|
Обновление применяется при следующем перезапуске браузера (или при перезагрузке расширения).
|
||||||
|
|
||||||
|
Форсировать проверку обновлений вручную можно в `chrome://extensions` → "Обновить расширения".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Итоговая структура файлов
|
||||||
|
|
||||||
|
```
|
||||||
|
caps-enhancer/
|
||||||
|
├── manifest.json ← update_url прописан
|
||||||
|
├── pack-crx.bat ← запускать для упаковки
|
||||||
|
├── caps-enhancer.pem ← ХРАНИТЬ В БЕЗОПАСНОМ МЕСТЕ (создаётся при первой упаковке)
|
||||||
|
├── DEPLOY.md ← эта инструкция
|
||||||
|
├── ...остальные файлы расширения...
|
||||||
|
└── server/
|
||||||
|
├── update.xml ← загружать на сервер при каждом обновлении
|
||||||
|
├── caps-enhancer.crx ← загружать на сервер при каждом обновлении
|
||||||
|
├── nginx.conf ← конфиг для nginx (один раз)
|
||||||
|
├── deploy.sh ← скрипт деплоя на сервере
|
||||||
|
├── install-client.reg ← раздать пользователям для установки
|
||||||
|
└── install-client.ps1 ← альтернатива .reg файлу
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Проблема | Причина | Решение |
|
||||||
|
|----------|---------|---------|
|
||||||
|
| Chrome не устанавливает расширение | Нет ключа реестра | Проверь `HKLM\SOFTWARE\Policies\Google\Chrome\ExtensionInstallForcelist` |
|
||||||
|
| "Extension not from Chrome Web Store" | Расширение установлено не через policy | Используй реестр, не ручную установку |
|
||||||
|
| Обновление не прилетает | update.xml закэширован или неверный ID | Очисти кэш, проверь EXTENSION_ID в update.xml |
|
||||||
|
| Chrome отказывается качать .crx | HTTP вместо HTTPS | Настрой SSL на сервере |
|
||||||
|
| .crx не скачивается | Неверный MIME-тип | Добавь `application/x-chrome-extension` для `.crx` |
|
||||||
|
| ID расширения изменился | Потерян .pem файл | Перераспространить .reg с новым ID |
|
||||||
53
auto-update — копия (2).bat
Normal file
53
auto-update — копия (2).bat
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
set VERSION_URL=https://git.ihateamerica.ru/caps_enhancer/version.json
|
||||||
|
set ZIP_URL=https://git.ihateamerica.ru/caps_enhancer/caps-enhancer.zip
|
||||||
|
set TMP_ZIP=%TEMP%\caps-enhancer-update.zip
|
||||||
|
set TMP_VER=%TEMP%\caps-ver.json
|
||||||
|
set TMP_LOC=%TEMP%\caps-loc.txt
|
||||||
|
set TMP_FIL=%TEMP%\caps-fil.txt
|
||||||
|
|
||||||
|
set EXT_DIR=%~dp0
|
||||||
|
if "%EXT_DIR:~-1%"=="\" set EXT_DIR=%EXT_DIR:~0,-1%
|
||||||
|
|
||||||
|
findstr "version" "%EXT_DIR%\manifest.json" > "%TMP_LOC%"
|
||||||
|
findstr /v "manifest" "%TMP_LOC%" > "%TMP_FIL%"
|
||||||
|
for /f "usebackq tokens=2 delims=:," %%v in ("%TMP_FIL%") do set LOCAL_VER=%%v
|
||||||
|
set LOCAL_VER=%LOCAL_VER: =%
|
||||||
|
set LOCAL_VER=%LOCAL_VER:"=%
|
||||||
|
|
||||||
|
curl -sf "%VERSION_URL%" -o "%TMP_VER%"
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [ERROR] Server unavailable
|
||||||
|
goto :end
|
||||||
|
)
|
||||||
|
for /f "usebackq tokens=2 delims=:}" %%v in ("%TMP_VER%") do set REMOTE_VER=%%v
|
||||||
|
set REMOTE_VER=%REMOTE_VER: =%
|
||||||
|
set REMOTE_VER=%REMOTE_VER:"=%
|
||||||
|
|
||||||
|
del "%TMP_LOC%" "%TMP_FIL%" "%TMP_VER%" 2>nul
|
||||||
|
|
||||||
|
echo Local: %LOCAL_VER% Remote: %REMOTE_VER%
|
||||||
|
|
||||||
|
if "%LOCAL_VER%"=="%REMOTE_VER%" (
|
||||||
|
echo [OK] Already up to date
|
||||||
|
goto :end
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [INFO] Updating %LOCAL_VER% -^> %REMOTE_VER%
|
||||||
|
|
||||||
|
curl -f "%ZIP_URL%" -o "%TMP_ZIP%"
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [ERROR] Download failed
|
||||||
|
goto :end
|
||||||
|
)
|
||||||
|
|
||||||
|
tar -xf "%TMP_ZIP%" -C "%EXT_DIR%"
|
||||||
|
del "%TMP_ZIP%"
|
||||||
|
|
||||||
|
echo [OK] Updated to %REMOTE_VER%. Reload extension in chrome://extensions
|
||||||
|
|
||||||
|
:end
|
||||||
|
endlocal
|
||||||
|
pause
|
||||||
53
auto-update.bat
Normal file
53
auto-update.bat
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
set VERSION_URL=https://git.ihateamerica.ru/caps_enhancer/version.json
|
||||||
|
set ZIP_URL=https://git.ihateamerica.ru/caps_enhancer/caps-enhancer.zip
|
||||||
|
set TMP_ZIP=%TEMP%\caps-enhancer-update.zip
|
||||||
|
set TMP_VER=%TEMP%\caps-ver.json
|
||||||
|
set TMP_LOC=%TEMP%\caps-loc.txt
|
||||||
|
set TMP_FIL=%TEMP%\caps-fil.txt
|
||||||
|
|
||||||
|
set EXT_DIR=%~dp0
|
||||||
|
if "%EXT_DIR:~-1%"=="\" set EXT_DIR=%EXT_DIR:~0,-1%
|
||||||
|
|
||||||
|
findstr "version" "%EXT_DIR%\manifest.json" > "%TMP_LOC%"
|
||||||
|
findstr /v "manifest" "%TMP_LOC%" > "%TMP_FIL%"
|
||||||
|
for /f "usebackq tokens=2 delims=:," %%v in ("%TMP_FIL%") do set LOCAL_VER=%%v
|
||||||
|
set LOCAL_VER=%LOCAL_VER: =%
|
||||||
|
set LOCAL_VER=%LOCAL_VER:"=%
|
||||||
|
|
||||||
|
curl -sf "%VERSION_URL%" -o "%TMP_VER%"
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [ERROR] Server unavailable
|
||||||
|
goto :end
|
||||||
|
)
|
||||||
|
for /f "usebackq tokens=2 delims=:}" %%v in ("%TMP_VER%") do set REMOTE_VER=%%v
|
||||||
|
set REMOTE_VER=%REMOTE_VER: =%
|
||||||
|
set REMOTE_VER=%REMOTE_VER:"=%
|
||||||
|
|
||||||
|
del "%TMP_LOC%" "%TMP_FIL%" "%TMP_VER%" 2>nul
|
||||||
|
|
||||||
|
echo Local: %LOCAL_VER% Remote: %REMOTE_VER%
|
||||||
|
|
||||||
|
if "%LOCAL_VER%"=="%REMOTE_VER%" (
|
||||||
|
echo [OK] Already up to date
|
||||||
|
goto :end
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [INFO] Updating %LOCAL_VER% -^> %REMOTE_VER%
|
||||||
|
|
||||||
|
curl -f "%ZIP_URL%" -o "%TMP_ZIP%"
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [ERROR] Download failed
|
||||||
|
goto :end
|
||||||
|
)
|
||||||
|
|
||||||
|
tar -xf "%TMP_ZIP%" -C "%EXT_DIR%"
|
||||||
|
del "%TMP_ZIP%"
|
||||||
|
|
||||||
|
echo [OK] Updated to %REMOTE_VER%. Reload extension in chrome://extensions
|
||||||
|
|
||||||
|
:end
|
||||||
|
endlocal
|
||||||
|
pause
|
||||||
47
js/background.js
Normal file
47
js/background.js
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
// Фоновый service worker — проверка обновлений расширения
|
||||||
|
// Работает только в режиме "Load unpacked" (Developer Mode)
|
||||||
|
|
||||||
|
const VERSION_URL = 'https://git.ihateamerica.ru/caps_enhancer/version.json';
|
||||||
|
const CHECK_INTERVAL_MINUTES = 60;
|
||||||
|
|
||||||
|
chrome.runtime.onInstalled.addListener(() => {
|
||||||
|
chrome.alarms.create('updateCheck', {
|
||||||
|
delayInMinutes: 1,
|
||||||
|
periodInMinutes: CHECK_INTERVAL_MINUTES
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
||||||
|
if (alarm.name === 'updateCheck') {
|
||||||
|
checkForUpdates();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function checkForUpdates() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(VERSION_URL + '?t=' + Date.now());
|
||||||
|
if (!res.ok) return;
|
||||||
|
|
||||||
|
const { version: remote } = await res.json();
|
||||||
|
const local = chrome.runtime.getManifest().version;
|
||||||
|
|
||||||
|
if (isNewer(remote, local)) {
|
||||||
|
console.log(`[CAPS Enhancer] Обновление: ${local} → ${remote}. Перезагружаю...`);
|
||||||
|
// Небольшая задержка — дать scheduled task время распаковать файлы
|
||||||
|
await new Promise(r => setTimeout(r, 3000));
|
||||||
|
chrome.runtime.reload();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Нет соединения или сервер недоступен — молча игнорируем
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNewer(remote, local) {
|
||||||
|
const r = remote.split('.').map(Number);
|
||||||
|
const l = local.split('.').map(Number);
|
||||||
|
for (let i = 0; i < Math.max(r.length, l.length); i++) {
|
||||||
|
if ((r[i] || 0) > (l[i] || 0)) return true;
|
||||||
|
if ((r[i] || 0) < (l[i] || 0)) return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
129
js/content.js
129
js/content.js
|
|
@ -1,6 +1,8 @@
|
||||||
// ==================== ГЛАВНЫЙ ФАЙЛ ====================
|
// ==================== ГЛАВНЫЙ ФАЙЛ ====================
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
|
let capsLogoUrl = null; // URL caps.svg, сохраняется при первой замене логотипа
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', waitForConfigAndInit);
|
document.addEventListener('DOMContentLoaded', waitForConfigAndInit);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -17,19 +19,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 +55,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';
|
||||||
|
|
@ -65,9 +64,13 @@
|
||||||
if (!replaceLogoEnabled || !hasGetURL) return false;
|
if (!replaceLogoEnabled || !hasGetURL) return false;
|
||||||
const logo = document.querySelector(LOGO_SELECTOR);
|
const logo = document.querySelector(LOGO_SELECTOR);
|
||||||
if (logo && !logo.dataset.capsReplaced) {
|
if (logo && !logo.dataset.capsReplaced) {
|
||||||
|
logo.dataset.capsOriginalSrc = logo.src;
|
||||||
logo.dataset.capsReplaced = '1';
|
logo.dataset.capsReplaced = '1';
|
||||||
logo.src = chrome.runtime.getURL('icons/caps.svg');
|
logo.setAttribute('data-caps-logo', 'true');
|
||||||
|
capsLogoUrl = chrome.runtime.getURL('icons/caps.svg');
|
||||||
|
logo.src = capsLogoUrl;
|
||||||
debugLog('🖼️ [LOGO] Заменён на caps.svg', from ? '(' + from + ')' : '');
|
debugLog('🖼️ [LOGO] Заменён на caps.svg', from ? '(' + from + ')' : '');
|
||||||
|
applyVisibilityToElement(logo);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -88,24 +91,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()) {
|
||||||
|
debugLog('🔐 Обнаружена страница логина типа 1');
|
||||||
core.AutoLogin.tryAutoLogin();
|
core.AutoLogin.tryAutoLogin();
|
||||||
// Продолжаем инициализацию (не прерываем)
|
} else if (core.AutoLogin.isLoginPage2()) {
|
||||||
|
debugLog('🔐 Обнаружена страница логина типа 2');
|
||||||
|
core.AutoLogin.performAutoLoginPage2();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pageType.hasAppMarker) {
|
if (!pageType.hasAppMarker) {
|
||||||
|
|
@ -121,38 +125,109 @@
|
||||||
// Видео страница
|
// Видео страница
|
||||||
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;
|
||||||
|
if (el.hasAttribute('data-caps-logo')) {
|
||||||
|
if (visible && capsLogoUrl) {
|
||||||
|
el.src = capsLogoUrl;
|
||||||
|
} else if (!visible && el.dataset.capsOriginalSrc) {
|
||||||
|
el.src = el.dataset.capsOriginalSrc;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
el.style.display = visible ? '' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyVisibility(visible) {
|
||||||
|
// Скрываем/показываем UI-элементы расширения
|
||||||
|
document.querySelectorAll('[data-caps-ui="true"]').forEach(el => {
|
||||||
|
el.style.display = visible ? (el.dataset.capsUiDisplay || '') : 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Показываем/скрываем оригинальные элементы (обратная логика)
|
||||||
|
document.querySelectorAll('[data-caps-original="true"]').forEach(el => {
|
||||||
|
el.style.display = visible ? 'none' : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Для логотипа меняем src вместо скрытия
|
||||||
|
document.querySelectorAll('[data-caps-logo="true"]').forEach(el => {
|
||||||
|
if (visible && capsLogoUrl) {
|
||||||
|
el.src = capsLogoUrl;
|
||||||
|
} else if (!visible && el.dataset.capsOriginalSrc) {
|
||||||
|
el.src = el.dataset.capsOriginalSrc;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Обработка запросов из 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 +240,12 @@
|
||||||
sendResponse(result);
|
sendResponse(result);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.type === 'TOGGLE_UI_VISIBILITY') {
|
||||||
|
toggleVisibility();
|
||||||
|
sendResponse({ ok: true });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
473
js/options.js
473
js/options.js
|
|
@ -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) {
|
||||||
console.error('Ошибка сохранения:', chrome.runtime.lastError);
|
|
||||||
showStatus(getStr('options.statusSaveError') + ' ' + chrome.runtime.lastError.message, 'error');
|
showStatus(getStr('options.statusSaveError') + ' ' + chrome.runtime.lastError.message, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Настройки сохранены:', config);
|
|
||||||
showStatus(getStr('options.statusSaveOk'), 'success');
|
showStatus(getStr('options.statusSaveOk'), 'success');
|
||||||
|
|
||||||
// Отправляем сообщение всем вкладкам для обновления конфига
|
|
||||||
chrome.tabs.query({}, (tabs) => {
|
chrome.tabs.query({}, (tabs) => {
|
||||||
tabs.forEach(tab => {
|
tabs.forEach(tab => {
|
||||||
chrome.tabs.sendMessage(tab.id, {
|
chrome.tabs.sendMessage(tab.id, { type: 'CONFIG_UPDATED', config }).catch(() => {});
|
||||||
type: 'CONFIG_UPDATED',
|
|
||||||
config: config
|
|
||||||
}, (response) => {
|
|
||||||
// Игнорируем ошибки для вкладок, где нет content script
|
|
||||||
if (chrome.runtime.lastError) {
|
|
||||||
// Это нормально, не все вкладки имеют content script
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сброс настроек к значениям по умолчанию
|
// ---- Сброс настроек ----
|
||||||
|
|
||||||
function resetSettings() {
|
function resetSettings() {
|
||||||
if (!confirm(getStr('options.confirmReset'))) {
|
if (!confirm(getStr('options.confirmReset'))) return;
|
||||||
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 недоступен!');
|
|
||||||
}
|
|
||||||
|
|
|
||||||
26
js/popup.js
26
js/popup.js
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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: "Включить логи в консоль",
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,16 @@
|
||||||
"name": "CAPS Enhancer",
|
"name": "CAPS Enhancer",
|
||||||
"version": "2.7",
|
"version": "2.7",
|
||||||
"description": "Добавляет всякие фичи для УТП в CAPS",
|
"description": "Добавляет всякие фичи для УТП в CAPS",
|
||||||
|
"update_url": "https://git.ihateamerica.ru/caps_enhancer/update.xml",
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "icons/icon16.png",
|
"16": "icons/icon16.png",
|
||||||
"48": "icons/icon48.png",
|
"48": "icons/icon48.png",
|
||||||
"128": "icons/icon128.png"
|
"128": "icons/icon128.png"
|
||||||
},
|
},
|
||||||
"permissions": ["activeTab", "storage", "tabs"],
|
"background": {
|
||||||
|
"service_worker": "js/background.js"
|
||||||
|
},
|
||||||
|
"permissions": ["activeTab", "storage", "tabs", "alarms"],
|
||||||
"host_permissions": ["<all_urls>"],
|
"host_permissions": ["<all_urls>"],
|
||||||
"action": {
|
"action": {
|
||||||
"default_popup": "popup.html",
|
"default_popup": "popup.html",
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Проверка страницы логина типа 2 (auth-logo.png + react-notification-root + noscript)
|
||||||
|
function isLoginPage2() {
|
||||||
|
const hasNoscript = Array.from(document.querySelectorAll('noscript'))
|
||||||
|
.some(n => n.textContent.includes('You need to enable JavaScript to run this app'));
|
||||||
|
if (!hasNoscript) return false;
|
||||||
|
|
||||||
|
const hasNotificationRoot = !!document.querySelector('div.react-notification-root, div[class*="react-notification-root"]');
|
||||||
|
if (!hasNotificationRoot) return false;
|
||||||
|
|
||||||
|
const hasAuthLogo = !!document.querySelector('img[src="/images/auth-logo.png"]');
|
||||||
|
if (!hasAuthLogo) return false;
|
||||||
|
|
||||||
return true;
|
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('✅ Модуль загружен');
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -139,9 +139,17 @@
|
||||||
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');
|
||||||
|
wrapper.setAttribute('data-caps-ui-display', 'inline-block');
|
||||||
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);
|
|
||||||
|
// Оставляем оригинальную ссылку в DOM — она показывается когда UI скрыт
|
||||||
|
const isVisible = core.CONFIG?.uiVisible !== false;
|
||||||
|
mnemoLink.setAttribute('data-caps-original', 'true');
|
||||||
|
mnemoLink.style.display = isVisible ? 'none' : '';
|
||||||
|
wrapper.style.display = isVisible ? 'inline-block' : 'none';
|
||||||
|
mnemoLink.parentNode.insertBefore(wrapper, mnemoLink);
|
||||||
|
|
||||||
core.activeSchemeButton = true;
|
core.activeSchemeButton = true;
|
||||||
debugLog('✅ Кнопка успешно добавлена!');
|
debugLog('✅ Кнопка успешно добавлена!');
|
||||||
|
|
@ -155,6 +163,15 @@
|
||||||
removeSchemeButton: function() {
|
removeSchemeButton: function() {
|
||||||
const wrapper = document.querySelector('.scheme-buttons-wrapper');
|
const wrapper = document.querySelector('.scheme-buttons-wrapper');
|
||||||
if (wrapper) {
|
if (wrapper) {
|
||||||
|
// Восстанавливаем оригинальную ссылку
|
||||||
|
const parent = wrapper.parentNode;
|
||||||
|
if (parent) {
|
||||||
|
const originalLink = parent.querySelector('[data-caps-original="true"]');
|
||||||
|
if (originalLink) {
|
||||||
|
originalLink.style.display = '';
|
||||||
|
originalLink.removeAttribute('data-caps-original');
|
||||||
|
}
|
||||||
|
}
|
||||||
wrapper.remove();
|
wrapper.remove();
|
||||||
core.activeSchemeButton = false;
|
core.activeSchemeButton = false;
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
58
options.html
58
options.html
|
|
@ -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>
|
||||||
|
|
|
||||||
46
pack-crx.bat
Normal file
46
pack-crx.bat
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
@echo off
|
||||||
|
:: Скрипт упаковки расширения в .crx
|
||||||
|
:: Запускать из папки caps-enhancer
|
||||||
|
:: Требует: Google Chrome установлен
|
||||||
|
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
:: Путь к Chrome (проверяем оба варианта)
|
||||||
|
set CHROME="C:\Program Files\Google\Chrome\Application\chrome.exe"
|
||||||
|
if not exist %CHROME% set CHROME="C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"
|
||||||
|
|
||||||
|
:: Текущая директория (где лежит manifest.json)
|
||||||
|
set EXT_DIR=%~dp0
|
||||||
|
|
||||||
|
:: Файл ключа (генерируется при первой упаковке, сохранять!)
|
||||||
|
set KEY_FILE=%~dp0caps-enhancer.pem
|
||||||
|
|
||||||
|
echo === CAPS Enhancer - Упаковка расширения ===
|
||||||
|
echo.
|
||||||
|
|
||||||
|
if exist %KEY_FILE% (
|
||||||
|
echo [INFO] Используется существующий ключ: %KEY_FILE%
|
||||||
|
%CHROME% --pack-extension="%EXT_DIR%" --pack-extension-key="%KEY_FILE%" --no-message-box
|
||||||
|
) else (
|
||||||
|
echo [INFO] Ключ не найден. Будет создан новый ключ.
|
||||||
|
echo [WARN] ПЕРВЫЙ РАЗ: сохраните caps-enhancer.pem - он нужен для всех обновлений!
|
||||||
|
%CHROME% --pack-extension="%EXT_DIR%" --no-message-box
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
if exist "%~dp0..\caps-enhancer.crx" (
|
||||||
|
copy "%~dp0..\caps-enhancer.crx" "%~dp0server\caps-enhancer.crx" >nul
|
||||||
|
echo [OK] caps-enhancer.crx скопирован в server\
|
||||||
|
) else (
|
||||||
|
echo [WARN] .crx не найден. Chrome мог создать его на уровень выше.
|
||||||
|
echo Проверьте папку рядом с caps-enhancer\
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo === Готово! ===
|
||||||
|
echo Следующие шаги:
|
||||||
|
echo 1. Проверь ID расширения в chrome://extensions
|
||||||
|
echo 2. Обнови server\update.xml (EXTENSION_ID и version)
|
||||||
|
echo 3. Загрузи server\caps-enhancer.crx и server\update.xml на сервер
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
BIN
server/caps-enhancer.zip
Normal file
BIN
server/caps-enhancer.zip
Normal file
Binary file not shown.
38
server/deploy.sh
Normal file
38
server/deploy.sh
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Скрипт деплоя на сервер.
|
||||||
|
# Запускать на сервере после загрузки новых файлов.
|
||||||
|
# Использование: ./deploy.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DEPLOY_DIR="/var/www/caps-enhancer/caps-enhancer"
|
||||||
|
CRX_FILE="caps-enhancer.crx"
|
||||||
|
UPDATE_XML="update.xml"
|
||||||
|
|
||||||
|
echo "=== CAPS Enhancer Deploy ==="
|
||||||
|
|
||||||
|
# Создаём директорию если не существует
|
||||||
|
mkdir -p "$DEPLOY_DIR"
|
||||||
|
|
||||||
|
# Копируем файлы (запускать из папки где лежат .crx и update.xml)
|
||||||
|
if [ -f "$CRX_FILE" ]; then
|
||||||
|
cp "$CRX_FILE" "$DEPLOY_DIR/"
|
||||||
|
echo "[OK] $CRX_FILE скопирован"
|
||||||
|
else
|
||||||
|
echo "[WARN] $CRX_FILE не найден в текущей директории"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$UPDATE_XML" ]; then
|
||||||
|
cp "$UPDATE_XML" "$DEPLOY_DIR/"
|
||||||
|
echo "[OK] $UPDATE_XML скопирован"
|
||||||
|
else
|
||||||
|
echo "[WARN] $UPDATE_XML не найден в текущей директории"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Права доступа
|
||||||
|
chown -R www-data:www-data "$DEPLOY_DIR"
|
||||||
|
chmod -R 755 "$DEPLOY_DIR"
|
||||||
|
echo "[OK] Права выставлены"
|
||||||
|
|
||||||
|
echo "=== Деплой завершён ==="
|
||||||
|
echo "Проверь: https://YOUR_DOMAIN/caps-enhancer/update.xml"
|
||||||
38
server/install-client.ps1
Normal file
38
server/install-client.ps1
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Скрипт установки расширения через PowerShell (альтернатива .reg файлу)
|
||||||
|
# Запускать с правами администратора:
|
||||||
|
# Right Click -> "Запустить от имени администратора"
|
||||||
|
# или: Start-Process powershell -Verb RunAs -ArgumentList "-File install-client.ps1"
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$ExtensionId = "EXTENSION_ID",
|
||||||
|
[string]$UpdateUrl = "https://YOUR_DOMAIN/caps-enhancer/update.xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
$registryPath = "HKLM:\SOFTWARE\Policies\Google\Chrome\ExtensionInstallForcelist"
|
||||||
|
$value = "$ExtensionId;$UpdateUrl"
|
||||||
|
|
||||||
|
# Создаём ключ если не существует
|
||||||
|
if (-not (Test-Path $registryPath)) {
|
||||||
|
New-Item -Path $registryPath -Force | Out-Null
|
||||||
|
Write-Host "[OK] Создан ключ реестра: $registryPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ищем свободный номер (1, 2, 3, ...)
|
||||||
|
$index = 1
|
||||||
|
while (Get-ItemProperty -Path $registryPath -Name "$index" -ErrorAction SilentlyContinue) {
|
||||||
|
# Проверяем, вдруг расширение уже добавлено
|
||||||
|
$existing = (Get-ItemProperty -Path $registryPath -Name "$index")."$index"
|
||||||
|
if ($existing -like "$ExtensionId;*") {
|
||||||
|
Write-Host "[INFO] Расширение уже установлено (запись $index)."
|
||||||
|
Write-Host " Значение: $existing"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
$index++
|
||||||
|
}
|
||||||
|
|
||||||
|
Set-ItemProperty -Path $registryPath -Name "$index" -Value $value -Type String
|
||||||
|
Write-Host "[OK] Расширение добавлено в реестр (запись $index):"
|
||||||
|
Write-Host " $value"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Chrome автоматически установит расширение при следующем запуске."
|
||||||
|
Write-Host "Если Chrome уже открыт — перезапустите его."
|
||||||
13
server/install-client.reg
Normal file
13
server/install-client.reg
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
Windows Registry Editor Version 5.00
|
||||||
|
|
||||||
|
; ============================================================
|
||||||
|
; Принудительная установка CAPS Enhancer через Windows Registry
|
||||||
|
; ============================================================
|
||||||
|
|
||||||
|
; Для всех пользователей компьютера (требует прав администратора)
|
||||||
|
[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Google\Chrome\ExtensionInstallForcelist]
|
||||||
|
"1"="hpjnlbenmhmigajedbgmonmnediljbfa;https://git.ihateamerica.ru/caps_enhancer/update.xml"
|
||||||
|
|
||||||
|
; --- ИЛИ только для текущего пользователя (без прав админа) ---
|
||||||
|
; [HKEY_CURRENT_USER\SOFTWARE\Policies\Google\Chrome\ExtensionInstallForcelist]
|
||||||
|
; "1"="hpjnlbenmhmigajedbgmonmnediljbfa;https://git.ihateamerica.ru/caps_enhancer/update.xml"
|
||||||
40
server/nginx.conf
Normal file
40
server/nginx.conf
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name YOUR_DOMAIN;
|
||||||
|
|
||||||
|
# SSL (рекомендуется — Chrome требует HTTPS для update_url)
|
||||||
|
# ssl_certificate /etc/letsencrypt/live/YOUR_DOMAIN/fullchain.pem;
|
||||||
|
# ssl_certificate_key /etc/letsencrypt/live/YOUR_DOMAIN/privkey.pem;
|
||||||
|
|
||||||
|
root /var/www/caps-enhancer;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Директория с файлами расширения
|
||||||
|
location /caps-enhancer/ {
|
||||||
|
# MIME-тип для .crx — обязателен, иначе Chrome откажется качать
|
||||||
|
types {
|
||||||
|
application/x-chrome-extension crx;
|
||||||
|
application/xml xml;
|
||||||
|
text/plain txt;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Разрешаем скачивание
|
||||||
|
add_header Content-Disposition 'attachment' always;
|
||||||
|
|
||||||
|
# CORS — если сервер и расширение на разных доменах
|
||||||
|
add_header Access-Control-Allow-Origin '*' always;
|
||||||
|
|
||||||
|
# Кэш для update.xml — не кэшировать (чтобы обновления прилетали быстро)
|
||||||
|
location ~* \.xml$ {
|
||||||
|
add_header Cache-Control 'no-cache, no-store, must-revalidate' always;
|
||||||
|
add_header Pragma 'no-cache' always;
|
||||||
|
expires 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
# .crx можно кэшировать (файл меняется только при новой версии)
|
||||||
|
location ~* \.crx$ {
|
||||||
|
add_header Cache-Control 'public, max-age=86400' always;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
server/pack-zip.bat
Normal file
35
server/pack-zip.bat
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
set ROOT=%~dp0..
|
||||||
|
|
||||||
|
for /f "delims=" %%v in ('powershell -NoProfile -Command "(Get-Content '%ROOT%\manifest.json' | ConvertFrom-Json).version"') do set VERSION=%%v
|
||||||
|
echo Packing version %VERSION%...
|
||||||
|
|
||||||
|
set TMP=%TEMP%\caps-enhancer-pack
|
||||||
|
if exist "%TMP%" rd /s /q "%TMP%"
|
||||||
|
md "%TMP%"
|
||||||
|
|
||||||
|
copy "%ROOT%\manifest.json" "%TMP%\" >nul
|
||||||
|
copy "%ROOT%\popup.html" "%TMP%\" >nul
|
||||||
|
copy "%ROOT%\popup.css" "%TMP%\" >nul
|
||||||
|
copy "%ROOT%\options.html" "%TMP%\" >nul
|
||||||
|
xcopy "%ROOT%\js" "%TMP%\js\" /e /q >nul
|
||||||
|
xcopy "%ROOT%\css" "%TMP%\css\" /e /q >nul
|
||||||
|
xcopy "%ROOT%\modules" "%TMP%\modules\" /e /q >nul
|
||||||
|
xcopy "%ROOT%\icons" "%TMP%\icons\" /e /q >nul
|
||||||
|
|
||||||
|
set OUT=%~dp0caps-enhancer.zip
|
||||||
|
if exist "%OUT%" del "%OUT%"
|
||||||
|
powershell -NoProfile -Command "Compress-Archive -Path '%TMP%\*' -DestinationPath '%OUT%'"
|
||||||
|
rd /s /q "%TMP%"
|
||||||
|
|
||||||
|
set VER_FILE=%~dp0version.json
|
||||||
|
(echo {"version": "%VERSION%"})>"%VER_FILE%"
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [OK] server\caps-enhancer.zip
|
||||||
|
echo [OK] server\version.json (v%VERSION%)
|
||||||
|
echo.
|
||||||
|
echo Upload both files to: /var/www/caps_enhancer/
|
||||||
|
pause
|
||||||
|
endlocal
|
||||||
19
server/update.xml
Normal file
19
server/update.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
|
<!--
|
||||||
|
Файл обновления расширения для Chrome.
|
||||||
|
Chrome периодически опрашивает этот файл и сравнивает версию.
|
||||||
|
|
||||||
|
Замените:
|
||||||
|
EXTENSION_ID — ID расширения (см. chrome://extensions после первой установки)
|
||||||
|
version — текущая версия (должна совпадать с manifest.json)
|
||||||
|
codebase — прямая ссылка на .crx файл на вашем сервере
|
||||||
|
-->
|
||||||
|
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
|
||||||
|
<app appid='hpjnlbenmhmigajedbgmonmnediljbfa'>
|
||||||
|
<updatecheck
|
||||||
|
status='ok'
|
||||||
|
codebase='https://git.ihateamerica.ru/caps_enhancer/caps-enhancer.crx'
|
||||||
|
version='2.7'
|
||||||
|
/>
|
||||||
|
</app>
|
||||||
|
</gupdate>
|
||||||
1
server/version.json
Normal file
1
server/version.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version": "2.7"}
|
||||||
Loading…
Add table
Reference in a new issue