Compare commits

...

2 Commits

Author SHA1 Message Date
boyko_vn
879013782f feat: иконки в трее вместо нарисованных кружков
make_icon() грузит PNG из icons/ по состоянию (серый/жёлтый/зелёный — цвета
прежние) и кэширует. Если файла нет или он битый — прежняя отрисовка кружка.
В .gitignore исключение для icons/*.png, иначе иконки не попадали в репозиторий
под общим правилом *.png.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 01:24:45 +03:00
boyko_vn
092a78a2a4 feat: автозапуск трея через Планировщик заданий
launch_vpn.bat — запуск без окна консоли (pythonw из .venv, PATH или pyw -3).
vpn_tray_task.xml — задача с наивысшими правами, чтобы ensure_admin() не дёргал
UAC при каждом входе в систему.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 01:24:38 +03:00
13 changed files with 99 additions and 5 deletions

3
.gitignore vendored
View File

@ -6,5 +6,8 @@
*.zip *.zip
*.7z *.7z
# иконки трея — часть кода, а не личные картинки
!icons/*.png
vpn_config.json vpn_config.json
__pycache__/ __pycache__/

View File

@ -9,6 +9,31 @@
5. Положить секреты в keyring: `bash init.sh <secret> <static_pass>`. 5. Положить секреты в keyring: `bash init.sh <secret> <static_pass>`.
6. Запуск: `python launch_vpn_gui.py` (запросит права администратора). 6. Запуск: `python launch_vpn_gui.py` (запросит права администратора).
Ярлык без окна консоли — `launch_vpn.bat`: ищет `pythonw.exe` сначала в `.venv` проекта, затем в PATH, затем через `pyw.exe -3`.
## Автозапуск при входе в систему
Через Планировщик заданий, а не папку «Автозагрузка»: оттуда процесс стартует непривилегированным, и `ensure_admin()` дёргает UAC-окно при каждом входе. Задача с «Запускать с наивысшими правами» отдаёт админский токен сразу — промпта нет.
Регистрация (один раз, нужен админ):
```
schtasks /create /tn VPN-TOTP-Tray /xml vpn_tray_task.xml /f
```
`vpn_tray_task.xml` лежит в корне репозитория (в UTF-16 — этого требует `schtasks /xml`). Пути к `launch_vpn.bat` и имя пользователя в нём захардкожены абсолютными — при переносе проекта или на другой машине правь `<Arguments>`, `<WorkingDirectory>` и оба `<UserId>`.
Управление:
```
schtasks /query /tn VPN-TOTP-Tray /v /fo list # статус
schtasks /run /tn VPN-TOTP-Tray # запустить сейчас
schtasks /change /tn VPN-TOTP-Tray /disable # временно выключить автозапуск
schtasks /delete /tn VPN-TOTP-Tray /f # снести (нужен админ)
```
Или через GUI: `taskschd.msc` → Библиотека планировщика → `VPN-TOTP-Tray`.
## Индикатор в трее ## Индикатор в трее
- 🔘 серый — openvpn не запущен; - 🔘 серый — openvpn не запущен;
@ -17,6 +42,8 @@
Проверку связи можно отключить (`"ping_check_enabled": false`) — тогда только серый/зелёный. Проверку связи можно отключить (`"ping_check_enabled": false`) — тогда только серый/зелёный.
Картинки берутся из `icons/` (`v-gray-256.png`, `v-yellow-256.png`, `v-green-256.png`). Если файла нет — рисуется кружок того же цвета.
## Настройки (`vpn_config.json`) ## Настройки (`vpn_config.json`)
Правятся вручную или через пункт меню «Открыть настройки»; перечитываются на лету. Правятся вручную или через пункт меню «Открыть настройки»; перечитываются на лету.

BIN
icons/v-gray-256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
icons/v-gray.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
icons/v-green-256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
icons/v-green.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
icons/v-red-256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
icons/v-red.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
icons/v-yellow-256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
icons/v-yellow.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

36
launch_vpn.bat Normal file
View File

@ -0,0 +1,36 @@
@echo off
:: launch launch_vpn_gui.py without a console window via pythonw
setlocal
cd /d "%~dp0"
set "SCRIPT=%~dp0launch_vpn_gui.py"
if not exist "%SCRIPT%" (
echo launch_vpn_gui.py not found next to this .bat
pause
exit /b 1
)
:: 1) project venv, if present
if exist "%~dp0.venv\Scripts\pythonw.exe" (
start "" "%~dp0.venv\Scripts\pythonw.exe" "%SCRIPT%"
exit /b 0
)
:: 2) system pythonw from PATH
where pythonw.exe >nul 2>&1
if not errorlevel 1 (
start "" pythonw.exe "%SCRIPT%"
exit /b 0
)
:: 3) py launcher
where pyw.exe >nul 2>&1
if not errorlevel 1 (
start "" pyw.exe -3 "%SCRIPT%"
exit /b 0
)
echo pythonw.exe not found: install Python or create .venv
pause
exit /b 1

View File

@ -71,6 +71,15 @@ STATE_COLORS = {
STATE_WAIT: (230, 200, 40, 255), STATE_WAIT: (230, 200, 40, 255),
STATE_ON: (40, 200, 80, 255), STATE_ON: (40, 200, 80, 255),
} }
# Готовые иконки в icons/ (цвета те же, что в STATE_COLORS). Если файла нет —
# make_icon() рисует кружок соответствующего цвета, как раньше.
ICONS_DIR = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "icons")
STATE_ICON_FILES = {
STATE_OFF: "v-gray-256.png",
STATE_WAIT: "v-yellow-256.png",
STATE_ON: "v-green-256.png",
}
STATE_TITLES = { STATE_TITLES = {
STATE_OFF: "VPN: отключено", STATE_OFF: "VPN: отключено",
STATE_WAIT: "VPN: запущен, нет связи с сервером", STATE_WAIT: "VPN: запущен, нет связи с сервером",
@ -172,12 +181,31 @@ def current_state() -> str:
return STATE_ON if ping_alive else STATE_WAIT return STATE_ON if ping_alive else STATE_WAIT
_icon_cache = {}
def make_icon(state: str) -> Image.Image: def make_icon(state: str) -> Image.Image:
"""Кружок: серый = выкл, жёлтый = нет связи, зелёный = связь есть.""" """Иконка состояния: серый = выкл, жёлтый = нет связи, зелёный = связь есть.
Берём картинку из icons/, при её отсутствии рисуем кружок того же цвета."""
if state in _icon_cache:
return _icon_cache[state]
name = STATE_ICON_FILES.get(state, STATE_ICON_FILES[STATE_OFF])
path = os.path.join(ICONS_DIR, name)
img = None
try:
with Image.open(path) as f:
img = f.convert("RGBA").resize((64, 64), Image.LANCZOS)
except (OSError, ValueError):
img = None
if img is None:
img = Image.new("RGBA", (64, 64), (0, 0, 0, 0)) img = Image.new("RGBA", (64, 64), (0, 0, 0, 0))
d = ImageDraw.Draw(img) d = ImageDraw.Draw(img)
color = STATE_COLORS.get(state, STATE_COLORS[STATE_OFF]) color = STATE_COLORS.get(state, STATE_COLORS[STATE_OFF])
d.ellipse((12, 12, 52, 52), fill=color) d.ellipse((12, 12, 52, 52), fill=color)
_icon_cache[state] = img
return img return img

BIN
vpn_tray_task.xml Normal file

Binary file not shown.