first commit
This commit is contained in:
BIN
__pycache__/callback.cpython-311.pyc
Normal file
BIN
__pycache__/callback.cpython-311.pyc
Normal file
Binary file not shown.
51
callback.py
Normal file
51
callback.py
Normal 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)
|
||||
BIN
callbacks/__pycache__/example.cpython-311.pyc
Normal file
BIN
callbacks/__pycache__/example.cpython-311.pyc
Normal file
Binary file not shown.
BIN
callbacks/__pycache__/lock.cpython-311.pyc
Normal file
BIN
callbacks/__pycache__/lock.cpython-311.pyc
Normal file
Binary file not shown.
BIN
callbacks/__pycache__/media.cpython-311.pyc
Normal file
BIN
callbacks/__pycache__/media.cpython-311.pyc
Normal file
Binary file not shown.
BIN
callbacks/__pycache__/screen.cpython-311.pyc
Normal file
BIN
callbacks/__pycache__/screen.cpython-311.pyc
Normal file
Binary file not shown.
BIN
callbacks/__pycache__/system.cpython-311.pyc
Normal file
BIN
callbacks/__pycache__/system.cpython-311.pyc
Normal file
Binary file not shown.
BIN
callbacks/__pycache__/v2ray.cpython-311.pyc
Normal file
BIN
callbacks/__pycache__/v2ray.cpython-311.pyc
Normal file
Binary file not shown.
BIN
callbacks/__pycache__/web.cpython-311.pyc
Normal file
BIN
callbacks/__pycache__/web.cpython-311.pyc
Normal file
Binary file not shown.
5
callbacks/example.py
Normal file
5
callbacks/example.py
Normal 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
21
callbacks/lock.py
Normal 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
109
callbacks/media.py
Normal 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
28
callbacks/screen.py
Normal 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
86
callbacks/system.py
Normal 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
135
callbacks/v2ray.py
Normal 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
39
callbacks/web.py
Normal 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
2
config/v2ray.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
username: admin123
|
||||
password: admin123
|
||||
39
main.py
Normal file
39
main.py
Normal 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
BIN
static/icons/cloud.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
16
static/icons/steam-svgrepo-com.svg
Normal file
16
static/icons/steam-svgrepo-com.svg
Normal 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 |
2
static/icons/telegram-logo-svgrepo-com.svg
Normal file
2
static/icons/telegram-logo-svgrepo-com.svg
Normal 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
BIN
static/icons/vpn.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
196
static/index.html
Normal file
196
static/index.html
Normal 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
104
test.py
Normal 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())
|
||||
BIN
utils/__pycache__/config.cpython-311.pyc
Normal file
BIN
utils/__pycache__/config.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/windows_toast.cpython-311.pyc
Normal file
BIN
utils/__pycache__/windows_toast.cpython-311.pyc
Normal file
Binary file not shown.
41
utils/config.py
Normal file
41
utils/config.py
Normal 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
45
utils/windows_toast.py
Normal 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)}")
|
||||
Reference in New Issue
Block a user