фикс кассы + кнопки на стойке в мнеме
This commit is contained in:
parent
36998bbcce
commit
8b177097a3
9 changed files with 218 additions and 16 deletions
15
CHANGELOG.md
15
CHANGELOG.md
|
|
@ -2,6 +2,21 @@
|
|||
|
||||
---
|
||||
|
||||
## [2.9] — 2026-04-28
|
||||
|
||||
### Добавлено
|
||||
|
||||
- **Режим отображения IP-адреса в видео-окне** — новая настройка в разделе «Видео окна»: выбор между двумя вариантами:
|
||||
- **Кнопка с IP-адресом** (по умолчанию) — кнопка `🔗 IP:порт` добавляется рядом с заголовком окна
|
||||
- **Кнопка «Лещ»** — кнопка с фиксированным текстом «Лещ» и tooltip-подсказкой с URL устройства
|
||||
- **Поддержка нового типа noVNC-страницы** — страница «Платёжный терминал» теперь корректно определяется как noVNC по `<title>` (работает до инициализации React)
|
||||
|
||||
### Исправлено
|
||||
|
||||
- **Переадресация кассы** — пофиксил редирект на кассе, оттуда тоже перекидывает на порт 5000
|
||||
|
||||
---
|
||||
|
||||
## [2.8] — 2026-03-23
|
||||
|
||||
### Добавлено
|
||||
|
|
|
|||
98
CLAUDE.md
Normal file
98
CLAUDE.md
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# CAPS Enhancer — Chrome Extension
|
||||
|
||||
Chrome-расширение (Manifest V3) для улучшения интерфейса CAPS/UTP: навигационные кнопки, автологин, замена логотипа, редирект noVNC.
|
||||
|
||||
## Архитектура
|
||||
|
||||
```
|
||||
caps_enhancer-1/
|
||||
├── manifest.json # MV3, update URL, content scripts on <all_urls>
|
||||
├── js/
|
||||
│ ├── background.js # Service worker: проверка обновлений (chrome.alarms)
|
||||
│ ├── content.js # Инициализация: роутинг по типу страницы
|
||||
│ ├── popup.js # Popup: быстрые переключатели
|
||||
│ ├── options.js # Страница настроек: загрузка/сохранение конфига
|
||||
│ └── strings.js # Все строки UI (русский)
|
||||
├── modules/
|
||||
│ ├── core.js # CONFIG, утилиты, loadConfig(), updateConfig()
|
||||
│ ├── mnemoschema.js # Кнопки «Мнемосхема» и «Лещ» на главной странице
|
||||
│ ├── video-window.js # Кнопка с IP или «Лещ» в заголовке видео-окна
|
||||
│ ├── novnc-redirect.js # Редирект или кнопка на noVNC-странице
|
||||
│ ├── auto-login.js # Автологин с профилями и правилами подсетей
|
||||
│ └── close-handler.js # ESC закрывает видео-окна и карточки
|
||||
├── options.html / popup.html
|
||||
├── css/options.css / popup.css
|
||||
└── server/ # Деплой: .crx, nginx.conf, .reg, update.xml
|
||||
```
|
||||
|
||||
## Конфигурация (core.js CONFIG)
|
||||
|
||||
Хранится в `chrome.storage.sync` под ключом `config`. Структура:
|
||||
|
||||
```javascript
|
||||
{
|
||||
debug: { enabled, modules: { core, video, mnemo, close, content, novnc, autologin } },
|
||||
ports: { video: '5000', scheme: '8080' },
|
||||
videoWindow: {
|
||||
ipButtonMode: 'button' // 'button' — кнопка «🔗 IP:порт», 'clickable-title' — кнопка «Лещ»
|
||||
},
|
||||
novnc: { mode: 'button'|'redirect', redirectDelay: 500 },
|
||||
mnemo: { mode: 'host-port'|'host-scheme', skipOnDomain, addVideoButton, addSchemeButton },
|
||||
autologin: { enabled, profilePage1, profilePage2, subnetRules, profiles },
|
||||
disabledSites: [],
|
||||
replaceLogo: true,
|
||||
uiVisible: true
|
||||
}
|
||||
```
|
||||
|
||||
При добавлении нового поля конфига: обновить `CONFIG` в `core.js`, `updateConfig()` в `core.js`, `DEFAULT_CONFIG` в `options.js`, добавить элемент в `options.html`, загрузку/сохранение в `options.js`, строки в `strings.js`.
|
||||
|
||||
## Типы страниц (core.checkPageType)
|
||||
|
||||
| Тип | Признаки |
|
||||
|-----|---------|
|
||||
| `isMainPage` | логотип CleverPark + noscript-маркер |
|
||||
| `isVideoPage` | noscript-маркер, нет логотипа |
|
||||
| `isNoVNCPage` | noscript "You need to enable JavaScript" + один из признаков ниже |
|
||||
|
||||
**Признаки noVNC (второй критерий):**
|
||||
- `div[class*="app_AppWindow__"]` — старый тип страницы (CSS-модульный класс React-компонента)
|
||||
- `document.title` содержит `"Платёжный терминал"` — новый тип страницы (присутствует в HTML сразу при загрузке, до инициализации React)
|
||||
|
||||
**Важно:** не использовать DOM-элементы, которые появляются после загрузки React (например `div#app.App`, `div.multi-find-menu-container`) — они не существуют в момент первой проверки, что приводит к неверному определению типа страницы.
|
||||
|
||||
## Модуль video-window.js
|
||||
|
||||
Добавляет ссылку на IP устройства в заголовок видео-окна. Два режима (настройка `videoWindow.ipButtonMode`):
|
||||
|
||||
- **`button`** (по умолчанию): кнопка `🔗 IP:5000` после `<p>` в заголовке. Tooltip не задан.
|
||||
- **`clickable-title`**: кнопка **«Лещ»** с тем же стилем и тем же переходом. При наведении — нативный tooltip с полным URL (`button.title`).
|
||||
|
||||
Оба режима используют `core.createStyledButton()` — одинаковая стилизация. IP извлекается из src видео-элемента `img[src*=":8888/screen"][src*="/getVideo/"]` через regex `/\/getVideo\/http:\/\/([^\/:?#]+)(?::(\d+))?/i`.
|
||||
|
||||
## Модуль novnc-redirect.js
|
||||
|
||||
Два режима (`novnc.mode`):
|
||||
|
||||
- **`redirect`**: автоматический переход на `http://<host>:<videoPort>`. Редирект происходит если текущий порт **не равен** целевому (не жёсткая привязка к порту 80).
|
||||
- **`button`**: фиксированная кнопка в правом верхнем углу. После добавления следит за удалением через `MutationObserver` (15 сек) и добавляет кнопку снова — защита от React-рендеринга, который может очищать body.
|
||||
|
||||
Важно: у кнопки задан `dataset.capsUiDisplay = 'inline-block'`, чтобы `applyVisibility` не сбрасывала её display в `''`.
|
||||
|
||||
## Видимость UI (Shift+F1)
|
||||
|
||||
- `data-caps-ui="true"` — элементы расширения (скрываются при `uiVisible=false`)
|
||||
- `data-caps-ui-display` — исходное значение `display`, восстанавливается при `uiVisible=true`; **обязательно задавать** при создании кнопок с нестандартным display
|
||||
- `data-caps-original="true"` — оригинальные элементы (скрываются при `uiVisible=true`)
|
||||
- Логотип меняет `src` вместо `display`
|
||||
|
||||
## Паттерны
|
||||
|
||||
- Каждый модуль — IIFE, расширяет `window.VideoIPRedirect`
|
||||
- Общие утилиты: `core.createStyledButton()`, `core.debugLog()`, `core.retryWithBackoff()`
|
||||
- Конфиг обновляется через `chrome.runtime.onMessage` → `CONFIG_UPDATED`
|
||||
- Строки: все тексты UI только в `js/strings.js`, доступ через `getStr('options.key')`
|
||||
|
||||
## Деплой
|
||||
|
||||
См. [DEPLOY.md](DEPLOY.md). Кратко: упаковать `.crx` → загрузить на сервер → обновить `update.xml` с новой версией → версия в `manifest.json`.
|
||||
|
|
@ -50,6 +50,7 @@ const DEFAULT_CONFIG = {
|
|||
{ name: 'Профиль 1', username: '', password: '', autoSubmit: true, delay: 500 }
|
||||
]
|
||||
},
|
||||
videoWindow: { ipButtonMode: 'button' },
|
||||
disabledSites: [],
|
||||
replaceLogo: true,
|
||||
uiVisible: true
|
||||
|
|
@ -70,6 +71,7 @@ const elements = {
|
|||
mnemoSkipOnDomain: document.getElementById('mnemo-skip-on-domain'),
|
||||
mnemoAddSchemeButton: document.getElementById('mnemo-add-scheme-button'),
|
||||
mnemoAddVideoButton: document.getElementById('mnemo-add-video-button'),
|
||||
videoIpButtonMode: document.getElementById('video-ip-button-mode'),
|
||||
replaceLogo: document.getElementById('replace-logo'),
|
||||
uiVisible: document.getElementById('ui-visible'),
|
||||
autologinEnabled: document.getElementById('autologin-enabled'),
|
||||
|
|
@ -350,6 +352,9 @@ function loadSettings() {
|
|||
elements.portVideo.value = config.ports?.video || DEFAULT_CONFIG.ports.video;
|
||||
elements.portScheme.value = config.ports?.scheme || DEFAULT_CONFIG.ports.scheme;
|
||||
|
||||
// Видео окна
|
||||
elements.videoIpButtonMode.value = config.videoWindow?.ipButtonMode || DEFAULT_CONFIG.videoWindow.ipButtonMode;
|
||||
|
||||
// noVNC
|
||||
elements.novncRedirect.checked = (config.novnc?.mode || DEFAULT_CONFIG.novnc.mode) === 'redirect';
|
||||
elements.redirectDelay.value = config.novnc?.redirectDelay || DEFAULT_CONFIG.novnc.redirectDelay;
|
||||
|
|
@ -440,6 +445,9 @@ function saveSettings() {
|
|||
}
|
||||
},
|
||||
ports: { video: portVideo, scheme: portScheme },
|
||||
videoWindow: {
|
||||
ipButtonMode: elements.videoIpButtonMode.value
|
||||
},
|
||||
novnc: {
|
||||
mode: elements.novncRedirect.checked ? 'redirect' : 'button',
|
||||
redirectDelay: parseInt(elements.redirectDelay.value) || 500
|
||||
|
|
@ -493,6 +501,7 @@ function resetSettings() {
|
|||
|
||||
elements.portVideo.value = DEFAULT_CONFIG.ports.video;
|
||||
elements.portScheme.value = DEFAULT_CONFIG.ports.scheme;
|
||||
elements.videoIpButtonMode.value = DEFAULT_CONFIG.videoWindow.ipButtonMode;
|
||||
elements.novncRedirect.checked = DEFAULT_CONFIG.novnc.mode === 'redirect';
|
||||
elements.redirectDelay.value = DEFAULT_CONFIG.novnc.redirectDelay;
|
||||
elements.mnemoMode.value = DEFAULT_CONFIG.mnemo.mode;
|
||||
|
|
|
|||
|
|
@ -29,6 +29,13 @@ window.CAPS_STRINGS = {
|
|||
headerTitle: "Настройки CAPS Enhancer",
|
||||
headerSubtitle: "Настройте расширение под свои нужды",
|
||||
|
||||
// Видео окна
|
||||
videoWindowTitle: "🎬 Видео окна",
|
||||
videoIpButtonModeLabel: "Режим отображения IP-адреса",
|
||||
videoIpButtonModeDesc: "Как отображать ссылку на настройки устройства в заголовке видео-окна",
|
||||
videoIpButtonModeButton: "Кнопка с IP-адресом (🔗 IP:порт)",
|
||||
videoIpButtonModeClickable: "Кнопка «Лещ» (без IP-адреса)",
|
||||
|
||||
// noVNC (страница экрана)
|
||||
novncTitle: "🖥️ Страница экрана",
|
||||
novncRedirectLabel: "Включить переадресацию",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
"name": "CAPS Enhancer",
|
||||
"version": "2.8",
|
||||
"version": "2.9",
|
||||
"description": "Добавляет всякие фичи для УТП в CAPS",
|
||||
"update_url": "https://git.ihateamerica.ru/caps_enhancer/update.xml",
|
||||
"icons": {
|
||||
|
|
|
|||
|
|
@ -54,6 +54,11 @@ let CONFIG = {
|
|||
// Замена логотипа CleverPark на иконку CAPS (icons/caps.svg)
|
||||
replaceLogo: true,
|
||||
|
||||
// Настройки видео-окна
|
||||
videoWindow: {
|
||||
ipButtonMode: 'button' // 'button' — отдельная кнопка с IP, 'clickable-title' — заголовок окна становится кликабельным
|
||||
},
|
||||
|
||||
// Показывать ли улучшения расширения (кнопки, логотип)
|
||||
uiVisible: true
|
||||
};
|
||||
|
|
@ -123,6 +128,11 @@ function updateConfig(newConfig) {
|
|||
CONFIG.replaceLogo = newConfig.replaceLogo;
|
||||
}
|
||||
|
||||
// Обновляем videoWindow
|
||||
if (newConfig.videoWindow) {
|
||||
Object.assign(CONFIG.videoWindow, newConfig.videoWindow);
|
||||
}
|
||||
|
||||
// Обновляем uiVisible
|
||||
if (typeof newConfig.uiVisible === 'boolean') {
|
||||
CONFIG.uiVisible = newConfig.uiVisible;
|
||||
|
|
@ -252,8 +262,11 @@ function checkNoVNCPage() {
|
|||
}
|
||||
|
||||
// Признак 2: div с классом начинающимся на "app_AppWindow__"
|
||||
const appWindow = document.querySelector('div[class*="app_AppWindow__"]');
|
||||
const hasAppWindow = !!appWindow;
|
||||
// ИЛИ title страницы содержит "Платёжный терминал" (присутствует сразу при загрузке)
|
||||
const hasAppWindow = !!(
|
||||
document.querySelector('div[class*="app_AppWindow__"]') ||
|
||||
document.title.includes('Платёжный терминал')
|
||||
);
|
||||
|
||||
// Страница noVNC определяется по наличию обоих признаков
|
||||
return hasNoVNCNoscript && hasAppWindow;
|
||||
|
|
|
|||
|
|
@ -52,9 +52,9 @@
|
|||
debugLog('Текущий хост:', currentHost);
|
||||
debugLog('Текущий порт:', currentPort);
|
||||
|
||||
// Проверяем, что мы на 80 порту
|
||||
if (currentPort !== '80' && currentPort !== '') {
|
||||
debugLog('❌ Не на 80 порту, переадресация не нужна');
|
||||
// Проверяем, что мы не уже на целевом порту
|
||||
if (currentPort === core.TARGET_PORT_VIDEO) {
|
||||
debugLog('❌ Уже на целевом порту, переадресация не нужна');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -105,6 +105,7 @@
|
|||
);
|
||||
|
||||
button.setAttribute('data-caps-ui', 'true');
|
||||
button.dataset.capsUiDisplay = 'inline-block';
|
||||
|
||||
// Дополнительные стили для позиционирования
|
||||
button.style.position = 'fixed';
|
||||
|
|
@ -117,8 +118,18 @@
|
|||
|
||||
// Добавляем кнопку на страницу
|
||||
document.body.appendChild(button);
|
||||
|
||||
debugLog('✅ Кнопка настроек добавлена');
|
||||
|
||||
// React-приложение может заменить содержимое body — следим за удалением кнопки
|
||||
const observer = new MutationObserver(() => {
|
||||
if (!document.querySelector('.novnc-settings-btn')) {
|
||||
debugLog('⚠️ Кнопка удалена, добавляем снова');
|
||||
document.body.appendChild(button);
|
||||
}
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: false });
|
||||
setTimeout(() => observer.disconnect(), 15000);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@
|
|||
return null;
|
||||
}
|
||||
|
||||
// Функция для добавления кнопки в заголовок
|
||||
// Режим А: добавить отдельную кнопку рядом с заголовком
|
||||
function addButtonToHeader(header, ip) {
|
||||
if (header.querySelector('.video-redirect-btn')) {
|
||||
return null;
|
||||
|
|
@ -94,6 +94,35 @@
|
|||
return button;
|
||||
}
|
||||
|
||||
// Режим Б: кнопка «Лещ» вместо IP-адреса
|
||||
function addLeshButtonToHeader(header, ip) {
|
||||
if (header.querySelector('.video-redirect-btn')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const button = core.createStyledButton(
|
||||
'Лещ',
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
window.open(`http://${ip}:${core.TARGET_PORT_VIDEO}`, '_blank');
|
||||
},
|
||||
'video-redirect-btn'
|
||||
);
|
||||
button.setAttribute('data-caps-ui', 'true');
|
||||
button.title = `http://${ip}:${core.TARGET_PORT_VIDEO}`;
|
||||
|
||||
const p = header.querySelector('p');
|
||||
if (p) {
|
||||
p.style.display = 'inline-block';
|
||||
p.style.marginRight = '8px';
|
||||
p.insertAdjacentElement('afterend', button);
|
||||
} else {
|
||||
header.appendChild(button);
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
// Главная функция
|
||||
function addButtonToActiveWindow() {
|
||||
debugLog('========== НАЧАЛО ==========');
|
||||
|
|
@ -116,8 +145,14 @@
|
|||
return false;
|
||||
}
|
||||
|
||||
const mode = core.CONFIG.videoWindow?.ipButtonMode || 'button';
|
||||
if (mode === 'clickable-title') {
|
||||
addLeshButtonToHeader(activeWindow.header, ipData.ip);
|
||||
debugLog('✅ Кнопка «Лещ» добавлена');
|
||||
} else {
|
||||
addButtonToHeader(activeWindow.header, ipData.ip);
|
||||
debugLog('✅ Кнопка добавлена');
|
||||
}
|
||||
debugLog('========== КОНЕЦ ==========');
|
||||
|
||||
return true;
|
||||
|
|
@ -127,8 +162,7 @@
|
|||
window.VideoIPRedirect.VideoWindow = {
|
||||
addButtonToVideoWindow: addButtonToActiveWindow,
|
||||
removeButtonsFromWindow: function() {
|
||||
const btns = document.querySelectorAll('.video-redirect-btn');
|
||||
btns.forEach(btn => btn.remove());
|
||||
document.querySelectorAll('.video-redirect-btn').forEach(btn => btn.remove());
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
15
options.html
15
options.html
|
|
@ -21,6 +21,21 @@
|
|||
<div class="content">
|
||||
<div class="settings-grid">
|
||||
<div class="settings-col">
|
||||
<!-- Настройки видео-окна -->
|
||||
<section class="settings-section">
|
||||
<h2 data-i18n="options.videoWindowTitle">🎬 Видео окна</h2>
|
||||
<div class="setting-item">
|
||||
<label for="video-ip-button-mode">
|
||||
<span class="label-text" data-i18n="options.videoIpButtonModeLabel">Режим кнопки с IP-адресом</span>
|
||||
<span class="label-description" data-i18n="options.videoIpButtonModeDesc">Как отображать ссылку на настройки устройства в заголовке видео-окна</span>
|
||||
</label>
|
||||
<select id="video-ip-button-mode">
|
||||
<option value="button" data-i18n="options.videoIpButtonModeButton">Кнопка с IP-адресом (🔗 IP:порт)</option>
|
||||
<option value="clickable-title" data-i18n="options.videoIpButtonModeClickable">Кнопка «Лещ» (без IP-адреса)</option>
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Настройки noVNC (страница экрана) -->
|
||||
<section class="settings-section">
|
||||
<h2 data-i18n="options.novncTitle">🖥️ Страница экрана</h2>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue