first commit

This commit is contained in:
MoonDev
2025-05-21 07:55:46 +03:00
commit 96ea504b10
28 changed files with 919 additions and 0 deletions

Binary file not shown.

51
callback.py Normal file
View File

@@ -0,0 +1,51 @@
import os
import importlib
import inspect
import asyncio
from typing import Dict, Any
# Словарь для хранения загруженных функций
_action_functions = {}
# Загружаем все файлы из папки ./callbacks
def load_callbacks():
callbacks_dir = "./callbacks"
if not os.path.exists(callbacks_dir):
return
print("\nLoaded callbacks:")
for filename in os.listdir(callbacks_dir):
if filename.endswith(".py") and filename != "__init__.py":
module_name = filename[:-3] # Удаляем .py
try:
module = importlib.import_module(f"callbacks.{module_name}")
# Получаем все функции из модуля
for func_name, func in inspect.getmembers(module, inspect.isfunction):
# Сохраняем с префиксом имени файла
_action_functions[f"{module_name}.{func_name}"] = func
print(f"{module_name}.{func_name}")
# Получаем все асинхронные функции
for func_name, func in inspect.getmembers(module, inspect.iscoroutinefunction):
_action_functions[f"{module_name}.{func_name}"] = func
except Exception as e:
print(f"Error loading module {module_name}: {str(e)}")
# Загружаем функции при старте
load_callbacks()
async def call_action(name: str, args: Dict[str, Any]):
"""
Вызывает функцию с заданным именем в формате filename.function_name, передавая ей аргументы.
Если функция асинхронная, использует await.
"""
if name not in _action_functions:
raise ValueError(f"Action '{name}' not found")
func = _action_functions[name]
# Проверяем, является ли функция асинхронной
if inspect.iscoroutinefunction(func):
return await func(args)
else:
return func(args)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

5
callbacks/example.py Normal file
View File

@@ -0,0 +1,5 @@
async def example_action(args):
return {"message": "Example action executed", "args": args}
def another_action(args):
return {"message": "Another action executed synchronously", "args": args}

21
callbacks/lock.py Normal file
View File

@@ -0,0 +1,21 @@
import ctypes
from typing import Dict, Any
async def lock_computer(args: Dict[str, Any]) -> Dict[str, Any]:
"""
Locks the Windows computer screen using the LockWorkStation API.
Args:
args: Dictionary of arguments (not used in this implementation).
Returns:
Dictionary with status message and input arguments.
Raises:
OSError: If the lock operation fails or the platform is not Windows.
"""
try:
# Use Windows API to lock the screen
result = ctypes.WinDLL('user32.dll').LockWorkStation()
if not result:
raise OSError("Failed to lock the Windows screen")
return {"message": "Windows screen locked successfully", "args": args}
except Exception as e:
raise OSError(f"Error while locking the Windows screen: {str(e)}")

109
callbacks/media.py Normal file
View File

@@ -0,0 +1,109 @@
import sys
import os
import asyncio
import comtypes
import ctypes
from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume
from ctypes import cast, POINTER
from typing import Dict, Any
from winrt.windows.media.control import GlobalSystemMediaTransportControlsSessionManager as MediaManager
# Добавляем родительскую директорию в sys.path
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
if parent_dir not in sys.path:
sys.path.append(parent_dir)
async def set_volume(args: Dict[str, Any]) -> Dict[str, Any]:
"""
Sets the Windows system volume using the Core Audio API.
Args:
args: Dictionary containing 'level' (float from 0.0 to 1.0) for volume level.
Returns:
Dictionary with status message and input arguments.
Raises:
OSError: If volume setting fails or the platform is not Windows.
"""
try:
# Initialize COM
comtypes.CoInitialize()
# Get volume level from args
level = args.get('level', 0.5) # Default to 50% if not specified
# Validate input level
if not isinstance(level, (int, float)) or not 0.0 <= level <= 1.0:
raise ValueError("Volume level must be a float between 0.0 and 1.0")
# Get audio endpoint interface
devices = AudioUtilities.GetSpeakers()
interface = devices.Activate(IAudioEndpointVolume._iid_, comtypes.CLSCTX_ALL, None)
volume = cast(interface, POINTER(IAudioEndpointVolume))
# Set volume level
volume.SetMasterVolumeLevelScalar(float(level), None)
return {
"message": f"Volume set to {level*100:.0f}% successfully",
"args": args
}
except Exception as e:
raise OSError(f"Error while setting volume: {str(e)}")
finally:
# Release COM resources
comtypes.CoUninitialize()
async def play(args):
# Получаем менеджер медиасессий
sessions = await MediaManager.request_async()
current_session = sessions.get_current_session()
if current_session:
# Воспроизведение
await current_session.try_play_async()
else:
print("No media session is active.")
async def pause(args):
#print(await read_config("media.yaml"))
# Получаем менеджер медиасессий
sessions = await MediaManager.request_async()
current_session = sessions.get_current_session()
if current_session:
# пауза
await current_session.try_pause_async()
else:
print("No media session is active.")
async def next(args):
# Получаем менеджер медиасессий
sessions = await MediaManager.request_async()
current_session = sessions.get_current_session()
if current_session:
# вперед
await current_session.try_skip_next_async()
# await show_notification({"title":"Test","message":"test"})
else:
print("No media session is active.")
async def prev(args):
# Получаем менеджер медиасессий
sessions = await MediaManager.request_async()
current_session = sessions.get_current_session()
if current_session:
# назад
await current_session.try_skip_previous_async()
else:
print("No media session is active.")

28
callbacks/screen.py Normal file
View File

@@ -0,0 +1,28 @@
import monitorcontrol
def get_monitors():
"""Retrieve a list of connected monitors."""
return monitorcontrol.get_monitors()
def set_brightness(monitor, brightness):
"""Set the brightness of a specific monitor."""
if not 0 <= brightness <= 100:
raise ValueError("Brightness must be between 0 and 100")
with monitor:
monitor.set_luminance(brightness)
print(f"Set brightness to {brightness}%")
def chenge_brightness(args):
try:
level = args.get('level', 10)
monitors = get_monitors()
if not monitors:
raise OSError(f"No DDC/CI compatible monitors found.")
return
for i, monitor in enumerate(monitors):
set_brightness(monitor, level)
except Exception as e:
raise OSError(f"An error occurred: {e}")

86
callbacks/system.py Normal file
View File

@@ -0,0 +1,86 @@
import asyncio
import psutil
import pynvml
import time
from typing import Dict, Any
async def get_system_metrics(args: Dict[str, Any]) -> Dict[str, Any]:
"""
Retrieves system metrics including CPU usage, RAM usage, GPU usage (for NVIDIA GPUs),
system uptime, and network traffic of the primary network interface.
Args:
args: Dictionary of arguments (not used in this implementation).
Returns:
Dictionary with CPU usage (%), RAM used (GB), RAM total (GB), GPU usage (%),
system uptime (seconds), network traffic (Mbit/s), and input arguments.
Raises:
OSError: If retrieving metrics fails.
"""
try:
# Get CPU usage (averaged over 0.1 seconds for responsiveness)
cpu_usage = await asyncio.to_thread(psutil.cpu_percent, interval=0.1)
# Get RAM usage
memory = psutil.virtual_memory()
ram_used = memory.used / (1024 ** 3) # Convert bytes to GB
ram_total = memory.total / (1024 ** 3) # Convert bytes to GB
# Get system uptime
uptime = time.time() - psutil.boot_time() # Time since system boot in seconds
# Get network traffic for primary interface
network_traffic = 0.0
net_io = psutil.net_io_counters(pernic=True)
primary_interface = None
max_bytes = 0
# Find primary network interface (with most bytes sent/received)
for interface, stats in net_io.items():
total_bytes = stats.bytes_sent + stats.bytes_recv
if total_bytes > max_bytes:
max_bytes = total_bytes
primary_interface = interface
if primary_interface:
# Measure traffic over 1 second
initial_stats = net_io[primary_interface]
await asyncio.sleep(1)
final_stats = psutil.net_io_counters(pernic=True)[primary_interface]
# Calculate traffic in Mbit/s
bytes_diff = (final_stats.bytes_sent - initial_stats.bytes_sent +
final_stats.bytes_recv - initial_stats.bytes_recv)
network_traffic = (bytes_diff * 8) / (1024 ** 2) # Convert bytes to Mbit/s
# Try to get GPU usage (NVIDIA only)
gpu_usage = 0.0
try:
pynvml.nvmlInit()
device_count = pynvml.nvmlDeviceGetCount()
if device_count > 0:
handle = pynvml.nvmlDeviceGetHandleByIndex(0) # Use first GPU
utilization = pynvml.nvmlDeviceGetUtilizationRates(handle)
gpu_usage = utilization.gpu
pynvml.nvmlShutdown()
except pynvml.NVMLError:
gpu_usage = -1 # Indicate GPU metrics unavailable
return {
"message": "System metrics retrieved successfully",
"metrics": {
"cpu_usage_percent": round(cpu_usage, 2),
"ram_used_gb": round(ram_used, 2),
"ram_total_gb": round(ram_total, 2),
"gpu_usage_percent": gpu_usage,
"uptime_seconds": round(uptime, 2),
"network_traffic_mbps": round(network_traffic, 2)
},
"args": args
}
except Exception as e:
raise OSError(f"Error while retrieving system metrics: {str(e)}")

135
callbacks/v2ray.py Normal file
View File

@@ -0,0 +1,135 @@
import aiohttp
import sys
import os
# Добавляем родительскую директорию в sys.path
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
if parent_dir not in sys.path:
sys.path.append(parent_dir)
from utils.windows_toast import show_notification
from utils.config import read_config
class V2rayAController:
def __init__(self, host='http://localhost:2017', username='admin', password='admin'):
self.host = host
self.username = username
self.password = password
self.token = None
self.session = None
async def authenticate(self):
"""Аутентификация и получение токена"""
self.session = aiohttp.ClientSession()
auth_url = f"{self.host}/api/login"
credentials = {
'username': self.username,
'password': self.password
}
try:
async with self.session.post(auth_url, json=credentials) as response:
if response.status == 200:
data = await response.json()
self.token = data.get('data').get('token')
print("V2RayA auth OK")
return True
else:
print(f"Ошибка аутентификации: {response.status}")
return False
except Exception as e:
raise OSError("Failed to authenticate v2ray")
print(f"Ошибка при аутентификации: {e}")
return False
async def enable_proxy(self):
"""Включение прокси"""
if not self.token:
print("Не выполнен вход. Пожалуйста, сначала выполните аутентификацию")
return False
url = f"{self.host}/api/v2ray"
headers = {'Authorization': f"Bearer {self.token}"}
data = {}
try:
async with self.session.post(url, headers=headers, json=data) as response:
if response.status == 200:
print("Прокси успешно включен")
await show_notification({"title":"✅ V2Ray proxy","message":"Прокси успешно включен"})
return True
else:
print(f"Ошибка при включении прокси: {response.status}")
return False
except Exception as e:
raise OSError("Failed to enable proxy")
print(f"Ошибка при включении прокси: {e}")
return False
async def disable_proxy(self):
"""Выключение прокси"""
if not self.token:
print("Не выполнен вход. Пожалуйста, сначала выполните аутентификацию")
return False
url = f"{self.host}/api/v2ray"
headers = {'Authorization': f"Bearer {self.token}"}
data = {'operation': 'stop'}
try:
async with self.session.delete(url, headers=headers, json=data) as response:
if response.status == 200:
print("Прокси успешно выключен")
await show_notification({"title":"🔴 V2Ray proxy","message":"Прокси успешно выключен"})
return True
else:
print(f"Ошибка при выключении прокси: {response.status}")
return False
except Exception as e:
raise OSError("Failed to disable proxy")
print(f"Ошибка при выключении прокси: {e}")
return False
async def close(self):
"""Закрытие сессии"""
if self.session:
await self.session.close()
async def enable_vpn(args):
# Инициализация контроллера
config = await read_config("v2ray.yaml")
if config.get("username") and config.get("password"):
controller = V2rayAController(
host='http://localhost:2017',
username=config.get("username"),
password=config.get("password")
)
# Аутентификация
if await controller.authenticate():
await controller.enable_proxy()
# Закрытие сессии
await controller.close()
else:
raise OSError("Config unset")
async def disable_vpn(args):
# Инициализация контроллера
config = await read_config("v2ray.yaml")
if config.get("username") and config.get("password"):
controller = V2rayAController(
host='http://localhost:2017',
username=config.get("username"),
password=config.get("password")
)
# Аутентификация
if await controller.authenticate():
await controller.disable_proxy()
# Закрытие сессии
await controller.close()
else:
raise OSError("Config unset")

39
callbacks/web.py Normal file
View File

@@ -0,0 +1,39 @@
import asyncio
import webbrowser
from typing import Dict, Any
async def open_url(args: Dict[str, Any]) -> Dict[str, Any]:
"""
Opens a website in the default browser on Windows.
Args:
args: Dictionary containing 'url' (string) of the website to open.
Returns:
Dictionary with status message and input arguments.
Raises:
OSError: If opening the website fails.
"""
try:
# Get URL from args
url = args.get('url')
if not url:
raise ValueError("URL must be provided in args")
# Ensure URL has a scheme (http:// or https://)
if not url.startswith(('http://', 'https://')):
url = 'https://' + url
# Open website in default browser
success = await asyncio.to_thread(webbrowser.open, url)
if not success:
raise OSError("Failed to open the website")
return {
"message": f"Website {url} opened successfully",
"args": args
}
except Exception as e:
raise OSError(f"Error while opening website: {str(e)}")

2
config/v2ray.yaml Normal file
View File

@@ -0,0 +1,2 @@
username: admin123
password: admin123

39
main.py Normal file
View File

@@ -0,0 +1,39 @@
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse
from typing import Optional, Dict, Any
import json
from callback import call_action
from fastapi.responses import JSONResponse
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
class ActionArgs(BaseModel):
args: Optional[Dict[str, Any]] = None
@app.get("/")
async def read_index():
with open("static/index.html", "r", encoding="utf-8") as f:
html_content = f.read()
return HTMLResponse(content=html_content, status_code=200)
@app.post("/action/{name:path}")
async def handle_action(name: str, action_args: ActionArgs):
try:
# Вызываем функцию из callback.py, передавая имя действия и аргументы
result = await call_action(name, action_args.args or {})
return JSONResponse(content={"status": "success", "result": result})
except ValueError as e:
# Если функция не найдена, возвращаем 404
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
# Обработка остальных ошибок
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

BIN
static/icons/cloud.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.56967 20.0269C4.30041 25.7964 9.65423 30 15.9906 30C23.7274 30 29.9995 23.7318 29.9995 16C29.9995 8.26803 23.7274 2 15.9906 2C8.56634 2 2.49151 7.77172 2.01172 15.0699C2.01172 17.1667 2.01172 18.0417 2.56967 20.0269Z" fill="url(#paint0_linear_87_8314)"/>
<path d="M15.2706 12.5629L11.8426 17.5395C11.0345 17.5028 10.221 17.7314 9.54572 18.1752L2.01829 15.0784C2.01829 15.0784 1.84411 17.9421 2.56999 20.0763L7.89147 22.2707C8.15866 23.464 8.97779 24.5107 10.1863 25.0142C12.1635 25.8398 14.4433 24.8988 15.2658 22.922C15.4799 22.4052 15.5797 21.8633 15.5652 21.3225L20.5904 17.8219C23.5257 17.8219 25.9114 15.4305 25.9114 12.4937C25.9114 9.55673 23.5257 7.16748 20.5904 7.16748C17.7553 7.16748 15.1117 9.64126 15.2706 12.5629ZM14.4469 22.5783C13.8103 24.1057 12.054 24.8303 10.5273 24.1946C9.82302 23.9014 9.29128 23.3642 8.98452 22.7237L10.7167 23.4411C11.8426 23.9098 13.1343 23.3762 13.6023 22.2514C14.0718 21.1254 13.5392 19.8324 12.4139 19.3637L10.6233 18.6222C11.3142 18.3603 12.0997 18.3507 12.8336 18.6559C13.5734 18.9635 14.1475 19.5428 14.4517 20.283C14.756 21.0233 14.7548 21.8404 14.4469 22.5783ZM20.5904 16.0434C18.6364 16.0434 17.0455 14.4511 17.0455 12.4937C17.0455 10.5379 18.6364 8.94518 20.5904 8.94518C22.5457 8.94518 24.1365 10.5379 24.1365 12.4937C24.1365 14.4511 22.5457 16.0434 20.5904 16.0434ZM17.9341 12.4883C17.9341 11.0159 19.127 9.82159 20.5964 9.82159C22.0671 9.82159 23.2599 11.0159 23.2599 12.4883C23.2599 13.9609 22.0671 15.1541 20.5964 15.1541C19.127 15.1541 17.9341 13.9609 17.9341 12.4883Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_87_8314" x1="16.0056" y1="2" x2="16.0056" y2="30" gradientUnits="userSpaceOnUse">
<stop stop-color="#111D2E"/>
<stop offset="0.21248" stop-color="#051839"/>
<stop offset="0.40695" stop-color="#0A1B48"/>
<stop offset="0.5811" stop-color="#132E62"/>
<stop offset="0.7376" stop-color="#144B7E"/>
<stop offset="0.87279" stop-color="#136497"/>
<stop offset="1" stop-color="#1387B8"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 240 240" id="svg2" xmlns="http://www.w3.org/2000/svg"><style>.st0{fill:url(#path2995-1-0_1_)}.st1{fill:#c8daea}.st2{fill:#a9c9dd}.st3{fill:url(#path2991_1_)}</style><linearGradient id="path2995-1-0_1_" gradientUnits="userSpaceOnUse" x1="-683.305" y1="534.845" x2="-693.305" y2="511.512" gradientTransform="matrix(6 0 0 -6 4255 3247)"><stop offset="0" stop-color="#37aee2"/><stop offset="1" stop-color="#1e96c8"/></linearGradient><path id="path2995-1-0" class="st0" d="M240 120c0 66.3-53.7 120-120 120S0 186.3 0 120 53.7 0 120 0s120 53.7 120 120z"/><path id="path2993" class="st1" d="M98 175c-3.9 0-3.2-1.5-4.6-5.2L82 132.2 152.8 88l8.3 2.2-6.9 18.8L98 175z"/><path id="path2989" class="st2" d="M98 175c3 0 4.3-1.4 6-3 2.6-2.5 36-35 36-35l-20.5-5-19 12-2.5 30v1z"/><linearGradient id="path2991_1_" gradientUnits="userSpaceOnUse" x1="128.991" y1="118.245" x2="153.991" y2="78.245" gradientTransform="matrix(1 0 0 -1 0 242)"><stop offset="0" stop-color="#eff7fc"/><stop offset="1" stop-color="#ffffff"/></linearGradient><path id="path2991" class="st3" d="M100 144.4l48.4 35.7c5.5 3 9.5 1.5 10.9-5.1L179 82.2c2-8.1-3.1-11.7-8.4-9.3L55 117.5c-7.9 3.2-7.8 7.6-1.4 9.5l29.7 9.3L152 93c3.2-2 6.2-.9 3.8 1.3L100 144.4z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
static/icons/vpn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

196
static/index.html Normal file
View File

@@ -0,0 +1,196 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dynamic Glassmorphism Tile Panel</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.glass {
background: rgba(31, 41, 55, 0.2);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(147, 51, 234, 0.3);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
}
.glass:hover {
background: rgba(147, 51, 234, 0.2);
border-color: rgba(147, 51, 234, 0.7);
box-shadow: 0 6px 12px rgba(147, 51, 234, 0.3);
}
.glass_enabled {
background: rgba(30, 240, 135, 0.2);
border-color: rgba(51, 234, 127, 0.7);
box-shadow: 0 6px 12px rgba(30, 240, 135, 0.3);
}
.gauge {
width: 64px;
height: 64px;
background: conic-gradient(#a758f1 0deg, #7f0bfa var(--value), #4b5563 0);
box-shadow: 0 6px 12px rgba(147, 51, 234, 0.3);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.gauge::before {
content: '';
width: 48px;
height: 48px;
background: rgba(31, 41, 55, 0.9);
border-radius: 50%;
position: absolute;
}
.gauge span {
color: #d8b4fe;
font-size: 12px;
font-weight: bold;
z-index: 1;
}
.clock,
.number {
font-size: 3rem;
color: #d8b4fe;
font-weight: bold;
}
input[type="range"] {
accent-color: #9333ea;
box-shadow: 0 6px 12px rgba(147, 51, 234, 0.3);
}
.tile-content {
min-height: 12rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 1rem;
}
.gauge-tile-content {
justify-content: flex-start;
align-items: flex-start;
}
</style>
</head>
<body class="bg-gray-900 min-h-screen p-4" >
<div class="container mx-auto">
<div id="tileContainer" class="flex flex-wrap gap-4"></div>
</div>
<script>
const tiles = [
{ id: 6, type: "clock" },
{ id: 2, type: "gauge", gauges: [{ value: 3.60 * 10, label: "Загрузка CPU" }, { value: 3.60 * 80, label: "Занято RAM" }] },
{ id: 10, type: "number", value: 250, label: "Скорость сети" },
{ id: 7, type: "slider", label: "Настройки", sliders: [{ value: 50, label: "Яроксть экрана", action: "screen.chenge_brightness" }, { value: 75, label: "Громкость звука", action: "media.set_volume" }] },
{ id: 10, type: "break" },
{ id: 3, type: "image", imageUrl: "/static/icons/steam-svgrepo-com.svg", label: "Steam" },
{ id: 4, type: "image", imageUrl: "/static/icons/telegram-logo-svgrepo-com.svg", label: "Telegram" },
{ id: 5, type: "image", imageUrl: "/static/icons/cloud.png", label: "Cloud" },
{ id: 8, type: "image", imageUrl: "/static/icons/vpn.png", label: "VPN", enabled: true },
];
function createTile(tile) {
const tileDiv = document.createElement("div");
if (tile.type == "break") {
tileDiv.className = "basis-full";
document.getElementById("tileContainer").appendChild(tileDiv);
return;
}
tileDiv.id = "tile_"+tile.id;
tileDiv.className = `glass ${tile.enabled ? "glass_enabled" : ""} rounded-lg ${tile.type === "gauge" || tile.type === "slider" || tile.type === "clock" ? "w-96 h-48" : "w-48 h-48"} ${tile.type === "image" || tile.type === "text" ? "cursor-pointer" : ""}`;
const contentDiv = document.createElement("div");
contentDiv.className = `tile-content ${tile.type === "gauge" ? "gauge-tile-content" : ""}`;
switch (tile.type) {
case "text":
contentDiv.innerHTML = `<span class="text-purple-400 text-lg font-semibold">${tile.label}</span>`;
break;
case "image":
contentDiv.innerHTML = `<img src="${tile.imageUrl}" class="rounded-lg w-[6.3rem] h-[6.3rem] object-cover" alt="${tile.label}"><span class="mt-2 text-purple-400 text-lg font-semibold">${tile.label}</span>`;
break;
case "clock":
contentDiv.innerHTML = `<span id="clock-${tile.id}" class="clock"></span><span id="date-${tile.id}" class="text-purple-400 text-lg font-semibold"></span>`;
break;
case "gauge":
const gaugeContent = tile.gauges.map(g => `
<div class="flex items-center gap-4">
<div class="gauge" style="--value: ${g.value}deg;">
<span>${Math.round((g.value / 360) * 100)}%</span>
</div>
<span class="text-purple-400 text-lg font-semibold">${g.label}</span>
</div>
`).join("");
contentDiv.innerHTML = `<div class="flex flex-col gap-4">${gaugeContent}</div>`;
break;
case "slider":
const sliderContent = tile.sliders.map((s, index) => `
<div class="flex flex-col items-start">
<div class="flex items-center gap-2">
<span class="text-purple-400 text-sm font-semibold">${s.label}</span>
<span id="slider-value-${tile.id}-${index}" class="text-purple-300 text-sm font-normal">${s.value}</span>
</div>
<input type="range" min="0" max="100" value="${s.value}" class="w-full" data-tile-id="${tile.id}" data-slider-index="${index}">
</div>
`).join("");
contentDiv.innerHTML = `<span class="text-purple-400 text-lg font-semibold mb-2">${tile.label}</span><div class="flex flex-col w-full gap-4">${sliderContent}</div>`;
break;
case "number":
contentDiv.innerHTML = `<span class="number">${tile.value}</span><span class="text-purple-400 text-lg font-semibold">${tile.label}</span>`;
break;
}
tileDiv.appendChild(contentDiv);
document.getElementById("tileContainer").appendChild(tileDiv);
if (tile.type === "clock") {
function updateClock() {
const now = new Date();
const time = now.toLocaleTimeString('ru-RU', { hour12: false });
const options = { weekday: 'short', day: 'numeric', month: 'long', year: 'numeric' };
const date = now.toLocaleDateString('ru-RU', options)
.replace(/,/, '')
.replace(/(\d+)\s(\S+)/, '$1 $2')
.replace(/г\./, '');
document.getElementById(`clock-${tile.id}`).textContent = time;
document.getElementById(`date-${tile.id}`).textContent = date.charAt(0).toUpperCase() + date.slice(1);
}
updateClock();
setInterval(updateClock, 1000);
}
if (tile.type === "slider") {
tile.sliders.forEach((_, index) => {
const slider = contentDiv.querySelector(`input[data-tile-id="${tile.id}"][data-slider-index="${index}"]`);
const valueSpan = document.getElementById(`slider-value-${tile.id}-${index}`);
slider.addEventListener("input", (e) => {
valueSpan.textContent = e.target.value;
});
});
}
}
tiles.forEach(createTile);
</script>
</body>
</html>

104
test.py Normal file
View File

@@ -0,0 +1,104 @@
import aiohttp
import asyncio
import json
class V2rayAController:
def __init__(self, host='http://localhost:2017', username='admin', password='admin'):
self.host = host
self.username = username
self.password = password
self.token = None
self.session = None
async def authenticate(self):
"""Аутентификация и получение токена"""
self.session = aiohttp.ClientSession()
auth_url = f"{self.host}/api/login"
credentials = {
'username': self.username,
'password': self.password
}
try:
async with self.session.post(auth_url, json=credentials) as response:
if response.status == 200:
data = await response.json()
self.token = data.get('data').get('token')
print("V2RayA auth OK")
return True
else:
print(f"Ошибка аутентификации: {response.status}")
return False
except Exception as e:
print(f"Ошибка при аутентификации: {e}")
return False
async def enable_proxy(self):
"""Включение прокси"""
if not self.token:
print("Не выполнен вход. Пожалуйста, сначала выполните аутентификацию")
return False
url = f"{self.host}/api/v2ray"
headers = {'Authorization': f"Bearer {self.token}"}
data = {}
try:
async with self.session.post(url, headers=headers, json=data) as response:
if response.status == 200:
print("Прокси успешно включен")
return True
else:
print(f"Ошибка при включении прокси: {response.status}")
return False
except Exception as e:
print(f"Ошибка при включении прокси: {e}")
return False
async def disable_proxy(self):
"""Выключение прокси"""
if not self.token:
print("Не выполнен вход. Пожалуйста, сначала выполните аутентификацию")
return False
url = f"{self.host}/api/v2ray"
headers = {'Authorization': f"Bearer {self.token}"}
data = {'operation': 'stop'}
try:
async with self.session.delete(url, headers=headers, json=data) as response:
if response.status == 200:
print("Прокси успешно выключен")
return True
else:
print(f"Ошибка при выключении прокси: {response.status}")
return False
except Exception as e:
print(f"Ошибка при выключении прокси: {e}")
return False
async def close(self):
"""Закрытие сессии"""
if self.session:
await self.session.close()
async def main():
# Инициализация контроллера
controller = V2rayAController(
host='http://localhost:2017',
username='admin123',
password='admin123'
)
# Аутентификация
if await controller.authenticate():
# Примеры использования
await controller.enable_proxy()
await asyncio.sleep(20) # Пауза для демонстрации
await controller.disable_proxy()
# Закрытие сессии
await controller.close()
if __name__ == "__main__":
asyncio.run(main())

Binary file not shown.

Binary file not shown.

41
utils/config.py Normal file
View File

@@ -0,0 +1,41 @@
import os
import yaml
from typing import Dict, Any
import asyncio
async def read_config(config_file):
"""
Reads and parses a YAML configuration file from the parent directory.
Args:
'config_file' (str) with the name of the YAML file.
If not provided, defaults to 'config.yaml'.
Returns:
Dictionary with the parsed configuration and input arguments.
Raises:
OSError: If the file cannot be read or parsed.
"""
try:
# Get the config file name from args, default to 'config.yaml'
config_file = "./config/"+config_file
# Construct path to the parent directory
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
config_path = os.path.join(parent_dir, config_file)
# Check if file exists
if not os.path.exists(config_path):
raise OSError(f"Configuration file '{config_file}' not found in parent directory")
# Read and parse YAML file
with open(config_path, 'r', encoding='utf-8') as f:
config_data = yaml.safe_load(f)
if config_data is None:
raise OSError(f"Configuration file '{config_file}' is empty or invalid")
return config_data
except yaml.YAMLError as e:
raise OSError(f"Error parsing YAML file: {str(e)}")
except Exception as e:
raise OSError(f"Error reading configuration file: {str(e)}")

45
utils/windows_toast.py Normal file
View File

@@ -0,0 +1,45 @@
from win10toast import ToastNotifier
import asyncio
from typing import Dict, Any
async def show_notification(args: Dict[str, Any]) -> Dict[str, Any]:
"""
Displays a system notification in the Windows system tray (bottom-right corner).
Args:
args: Dictionary containing 'title' (str), 'message' (str), and optional 'duration' (int, in seconds).
Returns:
Dictionary with status message and input arguments.
Raises:
ValueError: If required 'title' or 'message' arguments are missing.
OSError: If the notification fails to display.
"""
try:
# Check for required arguments
title = args.get('title')
message = args.get('message')
if not title or not message:
raise ValueError("Both 'title' and 'message' are required in args")
# Optional duration (default to 5 seconds)
duration = args.get('duration', 5)
if not isinstance(duration, int) or duration < 1:
raise ValueError("'duration' must be a positive integer")
# Initialize the notifier
toaster = ToastNotifier()
# Show notification (run in a separate thread to avoid blocking)
await asyncio.get_event_loop().run_in_executor(
None,
lambda: toaster.show_toast(
title=title,
msg=message,
duration=duration,
threaded=True
)
)
return {"message": "Notification displayed successfully", "args": args}
except Exception as e:
raise OSError(f"Error displaying notification: {str(e)}")