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>
3
.gitignore
vendored
@ -6,5 +6,8 @@
|
||||
*.zip
|
||||
*.7z
|
||||
|
||||
# иконки трея — часть кода, а не личные картинки
|
||||
!icons/*.png
|
||||
|
||||
vpn_config.json
|
||||
__pycache__/
|
||||
|
||||
BIN
icons/v-gray-256.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
icons/v-gray.ico
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
icons/v-green-256.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
icons/v-green.ico
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
icons/v-red-256.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
icons/v-red.ico
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
icons/v-yellow-256.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
icons/v-yellow.ico
Normal file
|
After Width: | Height: | Size: 28 KiB |
36
launch_vpn.bat
Normal 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
|
||||
@ -71,6 +71,15 @@ STATE_COLORS = {
|
||||
STATE_WAIT: (230, 200, 40, 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_OFF: "VPN: отключено",
|
||||
STATE_WAIT: "VPN: запущен, нет связи с сервером",
|
||||
@ -172,12 +181,31 @@ def current_state() -> str:
|
||||
return STATE_ON if ping_alive else STATE_WAIT
|
||||
|
||||
|
||||
_icon_cache = {}
|
||||
|
||||
|
||||
def make_icon(state: str) -> Image.Image:
|
||||
"""Кружок: серый = выкл, жёлтый = нет связи, зелёный = связь есть."""
|
||||
img = Image.new("RGBA", (64, 64), (0, 0, 0, 0))
|
||||
d = ImageDraw.Draw(img)
|
||||
color = STATE_COLORS.get(state, STATE_COLORS[STATE_OFF])
|
||||
d.ellipse((12, 12, 52, 52), fill=color)
|
||||
"""Иконка состояния: серый = выкл, жёлтый = нет связи, зелёный = связь есть.
|
||||
Берём картинку из 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))
|
||||
d = ImageDraw.Draw(img)
|
||||
color = STATE_COLORS.get(state, STATE_COLORS[STATE_OFF])
|
||||
d.ellipse((12, 12, 52, 52), fill=color)
|
||||
|
||||
_icon_cache[state] = img
|
||||
return img
|
||||
|
||||
|
||||
|
||||