mirror of
https://github.com/ikunshare/Onekey.git
synced 2026-01-12 16:25:53 +08:00
Initial project setup and source code import
Add project files including Python source code, web assets, configuration, and CI/CD workflows. Includes main application logic, web interface, supporting modules, and documentation for the Onekey Steam Depot Manifest Downloader.
This commit is contained in:
0
web/__init__.py
Normal file
0
web/__init__.py
Normal file
437
web/app.py
Normal file
437
web/app.py
Normal file
@@ -0,0 +1,437 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
import httpx
|
||||
import asyncio
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from src.constants import STEAM_API_BASE
|
||||
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
project_root = Path(__file__)
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
|
||||
def get_base_path():
|
||||
"""获取程序基础路径"""
|
||||
if hasattr(sys, "_MEIPASS"):
|
||||
return Path(sys._MEIPASS)
|
||||
elif getattr(sys, "frozen", False):
|
||||
return Path(os.path.dirname(os.path.abspath(sys.executable)))
|
||||
else:
|
||||
return Path(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
base_path = get_base_path()
|
||||
|
||||
try:
|
||||
from src.main import OnekeyApp
|
||||
from src.config import ConfigManager
|
||||
except ImportError as e:
|
||||
print(f"导入错误: {e}")
|
||||
print("请确保在项目根目录中运行此程序")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class ConnectionManager:
|
||||
"""WebSocket 连接管理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.active_connections: List[WebSocket] = []
|
||||
|
||||
async def connect(self, websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
self.active_connections.append(websocket)
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
self.active_connections.remove(websocket)
|
||||
|
||||
async def send_personal_message(self, message: str, websocket: WebSocket):
|
||||
await websocket.send_text(message)
|
||||
|
||||
async def broadcast(self, message: str):
|
||||
for connection in self.active_connections:
|
||||
try:
|
||||
await connection.send_text(message)
|
||||
except:
|
||||
# 连接可能已关闭
|
||||
pass
|
||||
|
||||
|
||||
class WebOnekeyApp:
|
||||
"""Web版本的Onekey应用"""
|
||||
|
||||
def __init__(self, manager: ConnectionManager):
|
||||
self.onekey_app = None
|
||||
self.current_task = None
|
||||
self.task_status = "idle" # idle, running, completed, error
|
||||
self.task_progress = []
|
||||
self.task_result = None
|
||||
self.manager = manager
|
||||
|
||||
def init_app(self):
|
||||
"""初始化Onekey应用"""
|
||||
try:
|
||||
self.onekey_app = OnekeyApp()
|
||||
return True
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
async def run_unlock_task(self, app_id: str, tool_type: str, dlc: bool):
|
||||
"""运行解锁任务"""
|
||||
try:
|
||||
self.task_status = "running"
|
||||
self.task_progress = []
|
||||
|
||||
# 重新初始化应用以确保新的任务状态
|
||||
self.onekey_app = OnekeyApp()
|
||||
|
||||
# 添加自定义日志处理器来捕获进度
|
||||
self._add_progress_handler()
|
||||
|
||||
# 执行解锁任务
|
||||
result = await self.onekey_app.run(app_id, tool_type, dlc)
|
||||
|
||||
if result:
|
||||
self.task_status = "completed"
|
||||
self.task_result = {
|
||||
"success": True,
|
||||
"message": "游戏解锁配置成功!重启Steam后生效",
|
||||
}
|
||||
else:
|
||||
self.task_status = "error"
|
||||
self.task_result = {"success": False, "message": "配置失败"}
|
||||
|
||||
except Exception as e:
|
||||
self.task_status = "error"
|
||||
self.task_result = {"success": False, "message": f"配置失败: {str(e)}"}
|
||||
finally:
|
||||
# 确保应用资源被清理
|
||||
if hasattr(self, "onekey_app") and self.onekey_app:
|
||||
try:
|
||||
if hasattr(self.onekey_app, "client"):
|
||||
await self.onekey_app.client.close()
|
||||
except:
|
||||
pass
|
||||
self.onekey_app = None
|
||||
|
||||
def _add_progress_handler(self):
|
||||
"""添加进度处理器"""
|
||||
if self.onekey_app and self.onekey_app.logger:
|
||||
original_info = self.onekey_app.logger.info
|
||||
original_warning = self.onekey_app.logger.warning
|
||||
original_error = self.onekey_app.logger.error
|
||||
|
||||
def info_with_progress(msg):
|
||||
self.task_progress.append(
|
||||
{"type": "info", "message": str(msg), "timestamp": time.time()}
|
||||
)
|
||||
# 广播进度消息
|
||||
asyncio.create_task(
|
||||
self.manager.broadcast(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "task_progress",
|
||||
"data": {"type": "info", "message": str(msg)},
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
return original_info(msg)
|
||||
|
||||
def warning_with_progress(msg):
|
||||
self.task_progress.append(
|
||||
{"type": "warning", "message": str(msg), "timestamp": time.time()}
|
||||
)
|
||||
asyncio.create_task(
|
||||
self.manager.broadcast(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "task_progress",
|
||||
"data": {"type": "warning", "message": str(msg)},
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
return original_warning(msg)
|
||||
|
||||
def error_with_progress(msg):
|
||||
self.task_progress.append(
|
||||
{"type": "error", "message": str(msg), "timestamp": time.time()}
|
||||
)
|
||||
asyncio.create_task(
|
||||
self.manager.broadcast(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "task_progress",
|
||||
"data": {"type": "error", "message": str(msg)},
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
return original_error(msg)
|
||||
|
||||
self.onekey_app.logger.info = info_with_progress
|
||||
self.onekey_app.logger.warning = warning_with_progress
|
||||
self.onekey_app.logger.error = error_with_progress
|
||||
|
||||
|
||||
# 创建FastAPI应用
|
||||
app = FastAPI(title="Onekey")
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
manager = ConnectionManager()
|
||||
|
||||
# 修复:为静态文件路由添加name参数
|
||||
app.mount("/static", StaticFiles(directory=f"{base_path}/static"), name="static")
|
||||
templates = Jinja2Templates(directory=f"{base_path}/templates")
|
||||
|
||||
# 创建Web应用实例
|
||||
web_app = WebOnekeyApp(manager)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def index(request: Request):
|
||||
"""主页"""
|
||||
config = ConfigManager()
|
||||
if not config.app_config.key:
|
||||
return RedirectResponse(request.url_for("oobe"))
|
||||
else:
|
||||
return templates.TemplateResponse("index.html", {"request": request})
|
||||
|
||||
|
||||
@app.get("/oobe")
|
||||
async def oobe(request: Request):
|
||||
"""OOBE页面"""
|
||||
return templates.TemplateResponse("oobe.html", {"request": request})
|
||||
|
||||
|
||||
@app.post("/api/init")
|
||||
async def init_app():
|
||||
"""初始化应用"""
|
||||
result = web_app.init_app()
|
||||
if isinstance(result, tuple):
|
||||
return JSONResponse({"success": False, "message": result[1]})
|
||||
return JSONResponse({"success": True})
|
||||
|
||||
|
||||
@app.get("/api/config")
|
||||
async def get_config():
|
||||
"""获取配置信息"""
|
||||
try:
|
||||
config = ConfigManager()
|
||||
return JSONResponse(
|
||||
{
|
||||
"success": True,
|
||||
"config": {
|
||||
"steam_path": str(config.steam_path) if config.steam_path else "",
|
||||
"debug_mode": config.app_config.debug_mode,
|
||||
},
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
return JSONResponse({"success": False, "message": str(e)})
|
||||
|
||||
|
||||
@app.post("/api/start_unlock")
|
||||
async def start_unlock(request: Request):
|
||||
"""开始解锁任务"""
|
||||
data = await request.json()
|
||||
app_id = data.get("app_id", "").strip()
|
||||
tool_type = data.get("tool_type", "steamtools")
|
||||
dlc = data.get("dlc")
|
||||
|
||||
if not app_id:
|
||||
return JSONResponse({"success": False, "message": "请输入有效的App ID"})
|
||||
|
||||
# 验证App ID格式
|
||||
app_id_list = [id for id in app_id.split("-") if id.isdigit()]
|
||||
if not app_id_list:
|
||||
return JSONResponse({"success": False, "message": "App ID格式无效"})
|
||||
|
||||
if web_app.task_status == "running":
|
||||
return JSONResponse({"success": False, "message": "已有任务正在运行"})
|
||||
|
||||
try:
|
||||
await web_app.run_unlock_task(app_id_list[0], tool_type, dlc)
|
||||
except Exception as e:
|
||||
web_app.task_status = "error"
|
||||
web_app.task_result = {
|
||||
"success": False,
|
||||
"message": f"任务执行失败: {str(e)}",
|
||||
}
|
||||
|
||||
return JSONResponse({"success": True, "message": "任务已开始"})
|
||||
|
||||
|
||||
@app.get("/api/task_status")
|
||||
async def get_task_status():
|
||||
"""获取任务状态"""
|
||||
return JSONResponse(
|
||||
{
|
||||
"status": web_app.task_status,
|
||||
"progress": (
|
||||
web_app.task_progress[-10:] if web_app.task_progress else []
|
||||
), # 只返回最近10条
|
||||
"result": web_app.task_result,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.get("/about")
|
||||
async def settings_page(request: Request):
|
||||
"""关于页面"""
|
||||
return templates.TemplateResponse("about.html", {"request": request})
|
||||
|
||||
|
||||
@app.get("/settings")
|
||||
async def settings_page(request: Request):
|
||||
"""设置页面"""
|
||||
return templates.TemplateResponse("settings.html", {"request": request})
|
||||
|
||||
|
||||
@app.post("/api/config/update")
|
||||
async def update_config(request: Request):
|
||||
"""更新配置"""
|
||||
try:
|
||||
data = await request.json()
|
||||
|
||||
# 验证必需的字段
|
||||
if not isinstance(data, dict):
|
||||
return {"success": False, "message": "无效的配置数据"}
|
||||
|
||||
# 加载当前配置
|
||||
config_manager = ConfigManager()
|
||||
|
||||
# 准备新的配置数据
|
||||
new_config = {
|
||||
"KEY": data.get("key", ""),
|
||||
"Custom_Steam_Path": data.get("steam_path", ""),
|
||||
"Debug_Mode": data.get("debug_mode", False),
|
||||
"Logging_Files": data.get("logging_files", True),
|
||||
"Show_Console": data.get("show_console", True),
|
||||
}
|
||||
|
||||
# 保存配置
|
||||
import json
|
||||
|
||||
config_path = config_manager.config_path
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
json.dump(new_config, f, indent=2, ensure_ascii=False)
|
||||
|
||||
return {"success": True, "message": "配置已保存"}
|
||||
|
||||
except Exception as e:
|
||||
return {"success": False, "message": f"保存配置失败: {str(e)}"}
|
||||
|
||||
|
||||
@app.post("/api/config/reset")
|
||||
async def reset_config():
|
||||
"""重置配置为默认值"""
|
||||
try:
|
||||
from src.config import DEFAULT_CONFIG
|
||||
import json
|
||||
|
||||
config_manager = ConfigManager()
|
||||
config_path = config_manager.config_path
|
||||
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
json.dump(DEFAULT_CONFIG, f, indent=2, ensure_ascii=False)
|
||||
|
||||
return {"success": True, "message": "配置已重置为默认值"}
|
||||
|
||||
except Exception as e:
|
||||
return {"success": False, "message": f"重置配置失败: {str(e)}"}
|
||||
|
||||
|
||||
@app.get("/api/config/detailed")
|
||||
async def get_detailed_config():
|
||||
"""获取详细配置信息"""
|
||||
try:
|
||||
config = ConfigManager()
|
||||
return {
|
||||
"success": True,
|
||||
"config": {
|
||||
"steam_path": str(config.steam_path) if config.steam_path else "",
|
||||
"debug_mode": config.app_config.debug_mode,
|
||||
"logging_files": config.app_config.logging_files,
|
||||
"show_console": config.app_config.show_console,
|
||||
"steam_path_exists": (
|
||||
config.steam_path.exists() if config.steam_path else False
|
||||
),
|
||||
"key": getattr(config.app_config, "key", ""),
|
||||
},
|
||||
}
|
||||
except Exception as e:
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
|
||||
@app.post("/api/getKeyInfo")
|
||||
async def get_key_info(request: Request):
|
||||
"""获取卡密信息"""
|
||||
try:
|
||||
data = await request.json()
|
||||
key = data.get("key", "").strip()
|
||||
|
||||
if not key:
|
||||
return JSONResponse({"success": False, "message": "卡密不能为空"})
|
||||
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.post(
|
||||
f"{STEAM_API_BASE}/getKeyInfo",
|
||||
json={"key": key},
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
return JSONResponse(result)
|
||||
else:
|
||||
return JSONResponse({"success": False, "message": "卡密验证服务不可用"})
|
||||
except httpx.TimeoutException:
|
||||
return JSONResponse({"success": False, "message": "验证超时,请检查网络连接"})
|
||||
except Exception as e:
|
||||
return JSONResponse({"success": False, "message": f"验证失败: {str(e)}"})
|
||||
|
||||
|
||||
@app.websocket("/ws")
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
"""WebSocket 端点"""
|
||||
await manager.connect(websocket)
|
||||
try:
|
||||
await websocket.send_text(
|
||||
json.dumps({"type": "connected", "data": {"message": "已连接到服务器"}})
|
||||
)
|
||||
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
message = json.loads(data)
|
||||
if message.get("type") == "ping":
|
||||
await websocket.send_text(
|
||||
json.dumps({"type": "pong", "data": {"timestamp": time.time()}})
|
||||
)
|
||||
except WebSocketDisconnect:
|
||||
manager.disconnect(websocket)
|
||||
print("客户端断开连接")
|
||||
except Exception as e:
|
||||
print(f"WebSocket 错误: {e}")
|
||||
manager.disconnect(websocket)
|
||||
|
||||
|
||||
print("启动Onekey Web GUI...")
|
||||
print("请在浏览器中访问: http://localhost:5000")
|
||||
277
web/static/css/animations.css
Normal file
277
web/static/css/animations.css
Normal file
@@ -0,0 +1,277 @@
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInDown {
|
||||
from {
|
||||
transform: translateY(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
transform: translateY(60px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes subtle-rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% center;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% center;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
10%,
|
||||
30%,
|
||||
50%,
|
||||
70%,
|
||||
90% {
|
||||
transform: translateX(-5px);
|
||||
}
|
||||
20%,
|
||||
40%,
|
||||
60%,
|
||||
80% {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes success-pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.4);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 0 10px rgba(76, 175, 80, 0);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 rgba(76, 175, 80, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loading-bounce {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: scale(0);
|
||||
opacity: 0.5;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
.loading::before {
|
||||
content: "";
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--md-sys-color-primary-container);
|
||||
border-top: 2px solid var(--md-sys-color-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s cubic-bezier(0.68, -0.55, 0.265, 1.55) infinite;
|
||||
}
|
||||
|
||||
.loading-dots {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.loading-dots span {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--md-sys-color-primary);
|
||||
animation: loading-bounce 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.loading-dots span:nth-child(1) {
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
.loading-dots span:nth-child(2) {
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
.skeleton-loader {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--md-sys-color-surface-container) 25%,
|
||||
var(--md-sys-color-surface-container-high) 50%,
|
||||
var(--md-sys-color-surface-container) 75%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
.error-state {
|
||||
animation: shake 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.success-state {
|
||||
animation: success-pulse 0.5s ease-out;
|
||||
}
|
||||
|
||||
.ripple {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ripple::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: var(--ripple-y, 50%);
|
||||
left: var(--ripple-x, 50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
transform: translate(-50%, -50%);
|
||||
transition:
|
||||
width 0.6s ease,
|
||||
height 0.6s ease,
|
||||
opacity 0.6s ease;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.ripple:active::before {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fadeIn {
|
||||
animation: fadeIn var(--transition-medium) ease;
|
||||
}
|
||||
|
||||
.animate-slideUp {
|
||||
animation: slideUp var(--transition-medium) ease-out;
|
||||
}
|
||||
|
||||
.animate-slideIn {
|
||||
animation: slideIn var(--transition-medium) ease-out;
|
||||
}
|
||||
|
||||
.animate-pulse {
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-float {
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
165
web/static/css/base.css
Normal file
165
web/static/css/base.css
Normal file
@@ -0,0 +1,165 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
transition:
|
||||
background-color var(--transition-medium) ease,
|
||||
color var(--transition-medium) ease,
|
||||
border-color var(--transition-medium) ease;
|
||||
}
|
||||
|
||||
:root {
|
||||
transition:
|
||||
background-color 0.3s ease,
|
||||
color 0.3s ease;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family:
|
||||
"LXGW Wenkai Mono",
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
background-color: var(--md-sys-color-background);
|
||||
color: var(--md-sys-color-on-background);
|
||||
line-height: 1.5;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
top: -50%;
|
||||
right: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 30% 80%,
|
||||
rgba(103, 80, 164, 0.05) 0%,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 80% 20%,
|
||||
rgba(0, 188, 212, 0.05) 0%,
|
||||
transparent 50%
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] body::before {
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 30% 80%,
|
||||
rgba(208, 188, 255, 0.03) 0%,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 80% 20%,
|
||||
rgba(77, 208, 225, 0.03) 0%,
|
||||
transparent 50%
|
||||
);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--md-sys-color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--md-sys-color-primary);
|
||||
color: var(--md-sys-color-on-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] ::selection {
|
||||
background: var(--md-sys-color-primary);
|
||||
color: var(--md-sys-color-on-primary);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--md-sys-color-surface-container);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--md-sys-color-primary);
|
||||
border-radius: 6px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--gradient-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"]::-webkit-scrollbar-track {
|
||||
background: var(--md-sys-color-surface-container);
|
||||
}
|
||||
|
||||
[data-theme="dark"]::-webkit-scrollbar-thumb {
|
||||
background: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"]::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--gradient-primary);
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
font-family:
|
||||
"LXGW Wenkai Mono",
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
font-size: 12px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
[data-theme="dark"] input:-webkit-autofill,
|
||||
[data-theme="dark"] input:-webkit-autofill:hover,
|
||||
[data-theme="dark"] input:-webkit-autofill:focus {
|
||||
-webkit-text-fill-color: var(--md-sys-color-on-surface);
|
||||
-webkit-box-shadow: 0 0 0px 1000px var(--md-sys-color-surface-container) inset;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
|
||||
.btn,
|
||||
.card,
|
||||
.theme-toggle {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.btn:not(:hover),
|
||||
.card:not(:hover),
|
||||
.theme-toggle:not(:hover) {
|
||||
will-change: auto;
|
||||
}
|
||||
|
||||
button:active,
|
||||
.btn:active,
|
||||
.card:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
* {
|
||||
transition:
|
||||
background-color var(--transition-medium) ease,
|
||||
color var(--transition-medium) ease,
|
||||
border-color var(--transition-medium) ease,
|
||||
box-shadow var(--transition-medium) ease;
|
||||
}
|
||||
459
web/static/css/components.css
Normal file
459
web/static/css/components.css
Normal file
@@ -0,0 +1,459 @@
|
||||
.card {
|
||||
background: var(--md-sys-color-surface);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-radius: var(--md-sys-shape-corner-large);
|
||||
box-shadow: var(--md-sys-elevation-level1);
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
overflow: hidden;
|
||||
transition: all var(--transition-medium) cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
--hover-scale: 1.02;
|
||||
--hover-shadow: var(--md-sys-elevation-level3);
|
||||
animation: slideUp 0.6s ease-out backwards;
|
||||
}
|
||||
|
||||
.card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: var(--gradient-primary);
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition-medium) ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px) scale(var(--hover-scale, 1));
|
||||
box-shadow: var(--hover-shadow);
|
||||
}
|
||||
|
||||
.card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.card:nth-child(1) {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
.card:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
.card:nth-child(3) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
background: var(--md-sys-color-surface-container-low);
|
||||
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
||||
}
|
||||
|
||||
.card-header .material-icons {
|
||||
font-size: 28px;
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 14px 28px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
transition: all var(--transition-medium) cubic-bezier(0.4, 0, 0.2, 1);
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translate(-50%, -50%);
|
||||
transition:
|
||||
width var(--transition-slow) ease,
|
||||
height var(--transition-slow) ease;
|
||||
}
|
||||
|
||||
.btn:active::before {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--md-sys-color-primary);
|
||||
color: var(--md-sys-color-on-primary);
|
||||
box-shadow: var(--md-sys-elevation-level1);
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--gradient-primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--md-sys-elevation-level3);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--md-sys-color-secondary-container);
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: var(--md-sys-color-secondary);
|
||||
color: var(--md-sys-color-on-secondary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--md-sys-elevation-level2);
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
background-color: transparent;
|
||||
color: var(--md-sys-color-primary);
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.btn-text:hover:not(:disabled) {
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.text-field {
|
||||
padding: 16px;
|
||||
border: 2px solid var(--md-sys-color-outline-variant);
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
background-color: var(--md-sys-color-surface-container-low);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
font-size: 16px;
|
||||
transition: all var(--transition-fast) ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.text-field:hover {
|
||||
border-color: var(--md-sys-color-outline);
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
}
|
||||
|
||||
.text-field:focus {
|
||||
outline: none;
|
||||
border-color: var(--md-sys-color-primary);
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
box-shadow: 0 0 0 3px rgba(103, 80, 164, 0.15);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .text-field:focus {
|
||||
box-shadow: 0 0 0 3px rgba(208, 188, 255, 0.15);
|
||||
}
|
||||
|
||||
.input-helper {
|
||||
font-size: 12px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.radio-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
transition: background-color var(--transition-fast) ease;
|
||||
}
|
||||
|
||||
.radio-item:hover {
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
}
|
||||
|
||||
.radio-item input[type="radio"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.radio-button {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--md-sys-color-outline);
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
transition: all var(--transition-fast) ease;
|
||||
}
|
||||
|
||||
.radio-button::after {
|
||||
content: "";
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: var(--md-sys-color-primary);
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
transition: transform var(--transition-fast)
|
||||
cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
}
|
||||
|
||||
.radio-item input[type="radio"]:checked + .radio-button {
|
||||
border-color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.radio-item input[type="radio"]:checked + .radio-button::after {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
transition: background-color var(--transition-fast) ease;
|
||||
}
|
||||
|
||||
.checkbox-item:hover {
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
}
|
||||
|
||||
.checkbox-item input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.checkbox-button {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--md-sys-color-outline);
|
||||
border-radius: var(--md-sys-shape-corner-extra-small);
|
||||
position: relative;
|
||||
transition: all var(--transition-fast) ease;
|
||||
}
|
||||
|
||||
.checkbox-button::after {
|
||||
content: "✓";
|
||||
color: var(--md-sys-color-on-primary);
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
transition: transform var(--transition-fast)
|
||||
cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
}
|
||||
|
||||
.checkbox-item input[type="checkbox"]:checked + .checkbox-button {
|
||||
background: var(--md-sys-color-primary);
|
||||
border-color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.checkbox-item input[type="checkbox"]:checked + .checkbox-button::after {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: var(--md-sys-color-surface-container);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all var(--transition-fast) ease;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
background: var(--md-sys-color-primary-container);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.theme-toggle .material-icons {
|
||||
font-size: 20px;
|
||||
transition: transform var(--transition-medium) ease;
|
||||
}
|
||||
|
||||
.theme-toggle:active .material-icons {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.snackbar {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(100px);
|
||||
background: var(--md-sys-color-inverse-surface);
|
||||
color: var(--md-sys-color-inverse-on-surface);
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
box-shadow: var(--md-sys-elevation-level4);
|
||||
z-index: 2000;
|
||||
max-width: 560px;
|
||||
min-width: 344px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all var(--transition-medium) cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.snackbar.show {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.snackbar.success {
|
||||
background: var(--md-sys-color-success);
|
||||
color: var(--md-sys-color-on-success);
|
||||
}
|
||||
|
||||
.snackbar.error {
|
||||
background: var(--md-sys-color-error);
|
||||
color: var(--md-sys-color-on-error);
|
||||
}
|
||||
|
||||
.snackbar.warning {
|
||||
background: var(--md-sys-color-warning);
|
||||
color: var(--md-sys-color-on-warning);
|
||||
}
|
||||
|
||||
.snackbar.info {
|
||||
background: var(--md-sys-color-primary);
|
||||
color: var(--md-sys-color-on-primary);
|
||||
}
|
||||
|
||||
.snackbar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.snackbar-content span {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.snackbar-action {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 50%;
|
||||
transition: all var(--transition-fast) ease;
|
||||
}
|
||||
|
||||
.snackbar-action:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
background: var(--md-sys-color-surface-container);
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
transition: all var(--transition-fast) ease;
|
||||
}
|
||||
|
||||
.status-item:hover {
|
||||
transform: translateX(4px);
|
||||
border-color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 20px;
|
||||
transition: transform var(--transition-fast) ease;
|
||||
}
|
||||
|
||||
.status-item:hover .status-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.status-icon.success {
|
||||
color: var(--md-sys-color-success);
|
||||
}
|
||||
.status-icon.error {
|
||||
color: var(--md-sys-color-error);
|
||||
}
|
||||
.status-icon.warning {
|
||||
color: var(--md-sys-color-warning);
|
||||
}
|
||||
|
||||
.status-text {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
@media (prefers-contrast: high) {
|
||||
.card {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: 2px solid currentColor;
|
||||
}
|
||||
|
||||
.text-field {
|
||||
border-width: 3px;
|
||||
}
|
||||
}
|
||||
218
web/static/css/layout.css
Normal file
218
web/static/css/layout.css
Normal file
@@ -0,0 +1,218 @@
|
||||
.app-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 24px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.app-bar {
|
||||
background: var(--md-sys-color-surface);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: var(--md-sys-elevation-level1);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
transition: all var(--transition-medium) ease;
|
||||
}
|
||||
|
||||
.app-bar:hover {
|
||||
box-shadow: var(--md-sys-elevation-level2);
|
||||
}
|
||||
|
||||
.app-bar-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 16px 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
background: var(--gradient-primary);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
font-size: 32px;
|
||||
animation: subtle-rotate 20s linear infinite;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.app-version {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--md-sys-color-primary);
|
||||
background: var(--md-sys-color-primary-container);
|
||||
padding: 6px 16px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
border: 1px solid var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 32px 24px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.app-footer {
|
||||
background: var(--md-sys-color-surface);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
margin-top: 32px;
|
||||
border-top: 1px solid var(--md-sys-color-outline-variant);
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 32px 24px;
|
||||
}
|
||||
|
||||
.copyright {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
border-top: 1px solid var(--md-sys-color-outline-variant);
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.copyright p {
|
||||
font-size: 13px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
margin: 4px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.copyright a {
|
||||
color: var(--md-sys-color-primary);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.copyright a::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: var(--md-sys-color-primary);
|
||||
transition: width var(--transition-medium) ease;
|
||||
}
|
||||
|
||||
.copyright a:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.main-content {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.progress-card {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.app-bar-content {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.project-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.project-version {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.project-links {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.button-group .btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settings-text,
|
||||
.about-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.project-info-card {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.tech-info,
|
||||
.usage-notice {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.app-bar,
|
||||
.theme-toggle,
|
||||
.settings-link,
|
||||
.about-link,
|
||||
.btn,
|
||||
.snackbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body {
|
||||
background: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: none;
|
||||
border: 1px solid #ccc;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
:root {
|
||||
--md-sys-color-background: #ffffff !important;
|
||||
--md-sys-color-on-background: #000000 !important;
|
||||
--md-sys-color-surface: #ffffff !important;
|
||||
--md-sys-color-on-surface: #000000 !important;
|
||||
}
|
||||
}
|
||||
230
web/static/css/oobe.css
Normal file
230
web/static/css/oobe.css
Normal file
@@ -0,0 +1,230 @@
|
||||
.oobe-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--md-sys-color-primary-container) 0%,
|
||||
var(--md-sys-color-secondary-container) 100%
|
||||
);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.oobe-card {
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
background: var(--md-sys-color-surface);
|
||||
border-radius: var(--md-sys-shape-corner-extra-large);
|
||||
box-shadow: var(--md-sys-elevation-level3);
|
||||
overflow: hidden;
|
||||
animation: slideInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
.oobe-header {
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
var(--md-sys-color-primary),
|
||||
var(--md-sys-color-tertiary)
|
||||
);
|
||||
color: var(--md-sys-color-on-primary);
|
||||
padding: 40px 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.oobe-logo {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.oobe-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px 0;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.oobe-subtitle {
|
||||
font-size: 16px;
|
||||
opacity: 0.9;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.oobe-content {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.oobe-step {
|
||||
display: none;
|
||||
animation: fadeIn 0.4s ease-out;
|
||||
}
|
||||
|
||||
.oobe-step.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.step-indicator {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.step-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--md-sys-color-outline);
|
||||
margin: 0 6px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.step-dot.active {
|
||||
background: var(--md-sys-color-primary);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.step-dot.completed {
|
||||
background: var(--md-sys-color-tertiary);
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.welcome-text h3 {
|
||||
color: var(--md-sys-color-on-surface);
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.welcome-text p {
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
margin: 0 0 12px 0;
|
||||
line-height: 1.5;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.welcome-text a {
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
margin: 0 0 12px 0;
|
||||
line-height: 1.5;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.key-input-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.key-status {
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
background: var(--md-sys-color-surface-variant);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.key-status.show {
|
||||
display: block;
|
||||
animation: slideInDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
.key-status.success {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
border: 1px solid rgba(76, 175, 80, 0.3);
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.key-status.error {
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
border: 1px solid rgba(244, 67, 54, 0.3);
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.key-info {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.key-info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.key-info-item .material-icons {
|
||||
font-size: 18px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.oobe-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.btn-large {
|
||||
padding: 16px 32px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--md-sys-shape-corner-extra-large);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.loading-overlay.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid var(--md-sys-color-outline);
|
||||
border-top: 4px solid var(--md-sys-color-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.oobe-container {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.oobe-header {
|
||||
padding: 32px 24px;
|
||||
}
|
||||
|
||||
.oobe-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.key-info {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.oobe-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
314
web/static/css/project-info.css
Normal file
314
web/static/css/project-info.css
Normal file
@@ -0,0 +1,314 @@
|
||||
.project-info-card {
|
||||
background: var(--gradient-primary);
|
||||
border-radius: var(--md-sys-shape-corner-extra-large);
|
||||
padding: 32px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: var(--md-sys-elevation-level3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .project-info-card {
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--md-sys-color-primary-container) 0%,
|
||||
var(--md-sys-color-secondary-container) 100%
|
||||
);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
|
||||
.project-info-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
right: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(255, 255, 255, 0.1) 0%,
|
||||
transparent 70%
|
||||
);
|
||||
animation: rotate 30s linear infinite;
|
||||
}
|
||||
|
||||
.project-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.project-logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: var(--md-sys-shape-corner-large);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||
transition: transform var(--transition-medium) ease;
|
||||
}
|
||||
|
||||
.project-logo:hover {
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
}
|
||||
|
||||
.project-logo .material-icons {
|
||||
color: white;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.project-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.project-name {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
margin: 0 0 4px 0;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .project-info-card .project-name {
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
|
||||
.project-subtitle {
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .project-info-card .project-subtitle {
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
|
||||
.project-version {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.version-label {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
color: white;
|
||||
padding: 6px 16px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .project-info-card .version-label {
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
|
||||
.version-type {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.project-description {
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.project-description p {
|
||||
font-size: 15px;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.project-links {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 12px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.project-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(10px);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
transition: all var(--transition-medium) ease;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .project-link {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
|
||||
.project-link:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .project-link:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.project-link .material-icons {
|
||||
font-size: 24px;
|
||||
transition: transform var(--transition-fast) ease;
|
||||
}
|
||||
|
||||
.project-link:hover .material-icons {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.link-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.link-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.link-url {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.tech-info {
|
||||
background: var(--md-sys-color-surface-container);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
border-radius: var(--md-sys-shape-corner-large);
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: var(--md-sys-elevation-level1);
|
||||
transition: all var(--transition-medium) ease;
|
||||
}
|
||||
|
||||
.tech-info:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--md-sys-elevation-level3);
|
||||
}
|
||||
|
||||
.tech-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.tech-header .material-icons {
|
||||
font-size: 28px;
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.tech-header h4 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tech-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.tech-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 12px;
|
||||
background: var(--md-sys-color-surface-container-low);
|
||||
border-radius: var(--md-sys-shape-corner-small);
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
transition: all var(--transition-fast) ease;
|
||||
}
|
||||
|
||||
.tech-item:hover {
|
||||
transform: translateX(4px);
|
||||
border-color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.tech-item strong {
|
||||
color: var(--md-sys-color-on-surface);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tech-item span {
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.usage-notice {
|
||||
background: var(--md-sys-color-warning-container);
|
||||
border: 1px solid var(--md-sys-color-warning);
|
||||
border-radius: var(--md-sys-shape-corner-large);
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: var(--md-sys-elevation-level1);
|
||||
}
|
||||
|
||||
.notice-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.notice-header .material-icons {
|
||||
color: var(--md-sys-color-warning);
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.notice-header h4 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--md-sys-color-on-warning-container);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.notice-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.notice-content p {
|
||||
font-size: 14px;
|
||||
color: var(--md-sys-color-on-warning-container);
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
background: var(--md-sys-color-surface);
|
||||
border-radius: var(--md-sys-shape-corner-small);
|
||||
border-left: 3px solid var(--md-sys-color-warning);
|
||||
}
|
||||
|
||||
.notice-content strong {
|
||||
color: var(--md-sys-color-on-warning-container);
|
||||
font-weight: 600;
|
||||
}
|
||||
299
web/static/css/settings.css
Normal file
299
web/static/css/settings.css
Normal file
@@ -0,0 +1,299 @@
|
||||
.settings-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.checkbox-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
.checkbox-description {
|
||||
font-size: 13px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
#keyInfoSection {
|
||||
padding: 20px;
|
||||
background: var(--md-sys-color-surface-container-low);
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.key-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.key-info-card {
|
||||
background: var(--md-sys-color-surface-variant);
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.key-info-card:hover {
|
||||
background: var(--md-sys-color-primary-container);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.key-info-icon {
|
||||
color: var(--md-sys-color-primary);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.key-info-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.key-info-label {
|
||||
font-size: 12px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.key-info-value {
|
||||
font-size: 16px;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
font-weight: 500;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.key-status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--md-sys-shape-corner-small);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.key-status-badge.active {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.key-status-badge.expired {
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.key-status-badge.inactive {
|
||||
background: rgba(158, 158, 158, 0.1);
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
.key-change-section {
|
||||
margin-top: 24px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--md-sys-color-outline);
|
||||
}
|
||||
|
||||
.key-input-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.key-input-group .text-field {
|
||||
flex: 1;
|
||||
font-family: monospace;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.path-input-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.path-input-group .text-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: var(--md-sys-color-surface-container-low);
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.status-indicator .status-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.status-indicator .status-text {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.config-status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.config-status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: var(--md-sys-color-surface-container-low);
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
}
|
||||
|
||||
.config-status-item .material-icons {
|
||||
font-size: 24px;
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.config-status-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.config-status-label {
|
||||
font-size: 12px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.config-status-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1100;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all var(--transition-medium) ease;
|
||||
}
|
||||
|
||||
.dialog-overlay.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background: var(--md-sys-color-surface);
|
||||
border-radius: var(--md-sys-shape-corner-extra-large);
|
||||
box-shadow: var(--md-sys-elevation-level5);
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
transform: scale(0.9);
|
||||
transition: transform var(--transition-medium) ease;
|
||||
}
|
||||
|
||||
.dialog-overlay.show .dialog {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
padding: 24px 24px 16px;
|
||||
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
||||
}
|
||||
|
||||
.dialog-header h3 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.dialog-content p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
padding: 16px 24px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
border-top: 1px solid var(--md-sys-color-outline-variant);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.key-input-group {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.path-input-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.action-buttons .btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.config-status-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.btn.btn-text:first-child {
|
||||
margin-right: 8px;
|
||||
}
|
||||
8
web/static/css/style.css
Normal file
8
web/static/css/style.css
Normal file
@@ -0,0 +1,8 @@
|
||||
@import url("./variables.css");
|
||||
@import url("./base.css");
|
||||
@import url("./layout.css");
|
||||
@import url("./components.css");
|
||||
@import url("./animations.css");
|
||||
@import url("./oobe.css");
|
||||
@import url("./project-info.css");
|
||||
@import url("./utilities.css");
|
||||
222
web/static/css/utilities.css
Normal file
222
web/static/css/utilities.css
Normal file
@@ -0,0 +1,222 @@
|
||||
.progress-container {
|
||||
background: var(--md-sys-color-surface-container-low);
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
min-height: 200px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
}
|
||||
|
||||
.progress-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.progress-container::-webkit-scrollbar-track {
|
||||
background: var(--md-sys-color-surface-container);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.progress-container::-webkit-scrollbar-thumb {
|
||||
background: var(--md-sys-color-primary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.progress-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.progress-placeholder .material-icons {
|
||||
font-size: 48px;
|
||||
opacity: 0.3;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
||||
font-family: "Courier New", monospace;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
transition: background-color var(--transition-fast) ease;
|
||||
animation: slideIn var(--transition-medium) ease-out;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.log-entry::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background: var(--md-sys-color-primary);
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition-fast) ease;
|
||||
}
|
||||
|
||||
.log-entry:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.log-entry:hover {
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .log-entry {
|
||||
background: var(--md-sys-color-surface-container);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .log-entry:hover {
|
||||
background: var(--md-sys-color-surface-container-high);
|
||||
}
|
||||
|
||||
.log-entry:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.log-entry.warning {
|
||||
background-color: var(--md-sys-color-warning-container);
|
||||
border-left: 3px solid var(--md-sys-color-warning);
|
||||
}
|
||||
|
||||
.log-entry.error {
|
||||
background-color: var(--md-sys-color-error-container);
|
||||
border-left: 3px solid var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
.log-timestamp {
|
||||
font-family:
|
||||
"LXGW Wenkai Mono",
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font-size: 11px;
|
||||
min-width: 60px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
font-family:
|
||||
"LXGW Wenkai Mono",
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
.config-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.settings-link,
|
||||
.about-link {
|
||||
color: var(--md-sys-color-on-surface) !important;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
background: var(--md-sys-color-surface-container);
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
transition: all var(--transition-medium) ease;
|
||||
}
|
||||
|
||||
.settings-link:hover,
|
||||
.about-link:hover {
|
||||
background: var(--md-sys-color-primary-container);
|
||||
border-color: var(--md-sys-color-primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--md-sys-elevation-level2);
|
||||
}
|
||||
|
||||
.settings-link:hover .material-icons,
|
||||
.about-link:hover .material-icons {
|
||||
animation: rotate 1s ease-in-out;
|
||||
}
|
||||
|
||||
.settings-text,
|
||||
.about-text {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[data-tooltip] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[data-tooltip]::after {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-8px);
|
||||
background: var(--md-sys-color-inverse-surface);
|
||||
color: var(--md-sys-color-inverse-on-surface);
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--md-sys-shape-corner-small);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all var(--transition-fast) ease;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
[data-tooltip]:hover::after {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
|
||||
.unlock-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (prefers-contrast: high) {
|
||||
.card {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: 2px solid currentColor;
|
||||
}
|
||||
|
||||
.text-field {
|
||||
border-width: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) and (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) .card {
|
||||
box-shadow: var(--md-sys-elevation-level2);
|
||||
}
|
||||
}
|
||||
206
web/static/css/variables.css
Normal file
206
web/static/css/variables.css
Normal file
@@ -0,0 +1,206 @@
|
||||
:root {
|
||||
--transition-fast: 200ms;
|
||||
--transition-medium: 300ms;
|
||||
--transition-slow: 400ms;
|
||||
|
||||
--md-sys-color-primary: #6750a4;
|
||||
--md-sys-color-on-primary: #ffffff;
|
||||
--md-sys-color-primary-container: #e9ddff;
|
||||
--md-sys-color-on-primary-container: #22005d;
|
||||
|
||||
--md-sys-color-secondary: #00bcd4;
|
||||
--md-sys-color-on-secondary: #ffffff;
|
||||
--md-sys-color-secondary-container: #b2ebf2;
|
||||
--md-sys-color-on-secondary-container: #00363d;
|
||||
|
||||
--md-sys-color-tertiary: #ff6f00;
|
||||
--md-sys-color-on-tertiary: #ffffff;
|
||||
--md-sys-color-tertiary-container: #ffe0b2;
|
||||
--md-sys-color-on-tertiary-container: #4a1c00;
|
||||
|
||||
--md-sys-color-error: #dc2626;
|
||||
--md-sys-color-on-error: #ffffff;
|
||||
--md-sys-color-error-container: #fee2e2;
|
||||
--md-sys-color-on-error-container: #7f1d1d;
|
||||
|
||||
--md-sys-color-background: #fdfcff;
|
||||
--md-sys-color-on-background: #1a1c1e;
|
||||
|
||||
--md-sys-color-surface: #fdfcff;
|
||||
--md-sys-color-on-surface: #1a1c1e;
|
||||
--md-sys-color-surface-variant: #e7e0ec;
|
||||
--md-sys-color-on-surface-variant: #49454e;
|
||||
--md-sys-color-surface-container-lowest: #ffffff;
|
||||
--md-sys-color-surface-container-low: #f7f2fa;
|
||||
--md-sys-color-surface-container: #f1ecf4;
|
||||
--md-sys-color-surface-container-high: #ebe6ee;
|
||||
--md-sys-color-surface-container-highest: #e6e0e9;
|
||||
|
||||
--md-sys-color-outline: #79747e;
|
||||
--md-sys-color-outline-variant: #cac4cf;
|
||||
|
||||
--md-sys-color-success: #16a34a;
|
||||
--md-sys-color-on-success: #ffffff;
|
||||
--md-sys-color-success-container: #dcfce7;
|
||||
|
||||
--md-sys-color-warning: #f59e0b;
|
||||
--md-sys-color-on-warning: #ffffff;
|
||||
--md-sys-color-warning-container: #fef3c7;
|
||||
|
||||
--md-sys-color-surface-tint: #6750a4;
|
||||
--md-sys-color-scrim: #000000;
|
||||
--md-sys-color-inverse-surface: #313033;
|
||||
--md-sys-color-inverse-on-surface: #f4eff4;
|
||||
--md-sys-color-inverse-primary: #d0bcff;
|
||||
|
||||
--md-sys-elevation-level0: none;
|
||||
--md-sys-elevation-level1:
|
||||
0px 1px 3px 1px rgba(0, 0, 0, 0.15), 0px 1px 2px 0px rgba(0, 0, 0, 0.3);
|
||||
--md-sys-elevation-level2:
|
||||
0px 2px 6px 2px rgba(0, 0, 0, 0.15), 0px 1px 2px 0px rgba(0, 0, 0, 0.3);
|
||||
--md-sys-elevation-level3:
|
||||
0px 4px 8px 3px rgba(0, 0, 0, 0.15), 0px 1px 3px 0px rgba(0, 0, 0, 0.3);
|
||||
--md-sys-elevation-level4:
|
||||
0px 6px 10px 4px rgba(0, 0, 0, 0.15), 0px 2px 3px 0px rgba(0, 0, 0, 0.3);
|
||||
--md-sys-elevation-level5:
|
||||
0px 8px 12px 6px rgba(0, 0, 0, 0.15), 0px 4px 4px 0px rgba(0, 0, 0, 0.3);
|
||||
|
||||
--md-sys-shape-corner-none: 0px;
|
||||
--md-sys-shape-corner-extra-small: 4px;
|
||||
--md-sys-shape-corner-small: 8px;
|
||||
--md-sys-shape-corner-medium: 12px;
|
||||
--md-sys-shape-corner-large: 16px;
|
||||
--md-sys-shape-corner-extra-large: 28px;
|
||||
--md-sys-shape-corner-full: 999px;
|
||||
|
||||
--gradient-primary: linear-gradient(135deg, #6750a4 0%, #8b7cc4 100%);
|
||||
--gradient-secondary: linear-gradient(135deg, #00bcd4 0%, #4dd0e1 100%);
|
||||
--gradient-surface: linear-gradient(135deg, #fdfcff 0%, #f7f2fa 100%);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--md-sys-color-primary: #d0bcff;
|
||||
--md-sys-color-on-primary: #381e72;
|
||||
--md-sys-color-primary-container: #4f378a;
|
||||
--md-sys-color-on-primary-container: #e9ddff;
|
||||
|
||||
--md-sys-color-secondary: #4dd0e1;
|
||||
--md-sys-color-on-secondary: #00363d;
|
||||
--md-sys-color-secondary-container: #005662;
|
||||
--md-sys-color-on-secondary-container: #b2ebf2;
|
||||
|
||||
--md-sys-color-tertiary: #ffb74d;
|
||||
--md-sys-color-on-tertiary: #4a1c00;
|
||||
--md-sys-color-tertiary-container: #6a2c00;
|
||||
--md-sys-color-on-tertiary-container: #ffe0b2;
|
||||
|
||||
--md-sys-color-error: #f87171;
|
||||
--md-sys-color-on-error: #7f1d1d;
|
||||
--md-sys-color-error-container: #991b1b;
|
||||
--md-sys-color-on-error-container: #fee2e2;
|
||||
|
||||
--md-sys-color-background: #1a1c1e;
|
||||
--md-sys-color-on-background: #e3e2e6;
|
||||
|
||||
--md-sys-color-surface: #1a1c1e;
|
||||
--md-sys-color-on-surface: #e3e2e6;
|
||||
--md-sys-color-surface-variant: #49454e;
|
||||
--md-sys-color-on-surface-variant: #cac4cf;
|
||||
--md-sys-color-surface-container-lowest: #0e0f11;
|
||||
--md-sys-color-surface-container-low: #1a1c1e;
|
||||
--md-sys-color-surface-container: #1e2022;
|
||||
--md-sys-color-surface-container-high: #282a2d;
|
||||
--md-sys-color-surface-container-highest: #333538;
|
||||
|
||||
--md-sys-color-outline: #938f99;
|
||||
--md-sys-color-outline-variant: #49454e;
|
||||
|
||||
--md-sys-color-success: #4ade80;
|
||||
--md-sys-color-on-success: #14532d;
|
||||
--md-sys-color-success-container: #166534;
|
||||
|
||||
--md-sys-color-warning: #fbbf24;
|
||||
--md-sys-color-on-warning: #451a03;
|
||||
--md-sys-color-warning-container: #78350f;
|
||||
|
||||
--md-sys-color-surface-tint: #d0bcff;
|
||||
--md-sys-color-scrim: #000000;
|
||||
--md-sys-color-inverse-surface: #e6e0e9;
|
||||
--md-sys-color-inverse-on-surface: #313033;
|
||||
--md-sys-color-inverse-primary: #6750a4;
|
||||
|
||||
--md-sys-elevation-level0: none;
|
||||
--md-sys-elevation-level1:
|
||||
0px 1px 2px 0px rgba(0, 0, 0, 0.3), 0px 1px 3px 1px rgba(0, 0, 0, 0.15);
|
||||
--md-sys-elevation-level2:
|
||||
0px 1px 2px 0px rgba(0, 0, 0, 0.3), 0px 2px 6px 2px rgba(0, 0, 0, 0.15);
|
||||
--md-sys-elevation-level3:
|
||||
0px 1px 3px 0px rgba(0, 0, 0, 0.3), 0px 4px 8px 3px rgba(0, 0, 0, 0.15);
|
||||
--md-sys-elevation-level4:
|
||||
0px 2px 3px 0px rgba(0, 0, 0, 0.3), 0px 6px 10px 4px rgba(0, 0, 0, 0.15);
|
||||
--md-sys-elevation-level5:
|
||||
0px 4px 4px 0px rgba(0, 0, 0, 0.3), 0px 8px 12px 6px rgba(0, 0, 0, 0.15);
|
||||
|
||||
--gradient-primary: linear-gradient(135deg, #4f378a 0%, #6750a4 100%);
|
||||
--gradient-secondary: linear-gradient(135deg, #005662 0%, #00838f 100%);
|
||||
--gradient-surface: linear-gradient(135deg, #1a1c1e 0%, #1e2022 100%);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) {
|
||||
--md-sys-color-primary: #d0bcff;
|
||||
--md-sys-color-on-primary: #381e72;
|
||||
--md-sys-color-primary-container: #4f378a;
|
||||
--md-sys-color-on-primary-container: #e9ddff;
|
||||
--md-sys-color-secondary: #4dd0e1;
|
||||
--md-sys-color-on-secondary: #00363d;
|
||||
--md-sys-color-secondary-container: #005662;
|
||||
--md-sys-color-on-secondary-container: #b2ebf2;
|
||||
--md-sys-color-tertiary: #ffb74d;
|
||||
--md-sys-color-on-tertiary: #4a1c00;
|
||||
--md-sys-color-tertiary-container: #6a2c00;
|
||||
--md-sys-color-on-tertiary-container: #ffe0b2;
|
||||
--md-sys-color-error: #f87171;
|
||||
--md-sys-color-on-error: #7f1d1d;
|
||||
--md-sys-color-error-container: #991b1b;
|
||||
--md-sys-color-on-error-container: #fee2e2;
|
||||
--md-sys-color-background: #1a1c1e;
|
||||
--md-sys-color-on-background: #e3e2e6;
|
||||
--md-sys-color-surface: #1a1c1e;
|
||||
--md-sys-color-on-surface: #e3e2e6;
|
||||
--md-sys-color-surface-variant: #49454e;
|
||||
--md-sys-color-on-surface-variant: #cac4cf;
|
||||
--md-sys-color-surface-container-lowest: #0e0f11;
|
||||
--md-sys-color-surface-container-low: #1a1c1e;
|
||||
--md-sys-color-surface-container: #1e2022;
|
||||
--md-sys-color-surface-container-high: #282a2d;
|
||||
--md-sys-color-surface-container-highest: #333538;
|
||||
--md-sys-color-outline: #938f99;
|
||||
--md-sys-color-outline-variant: #49454e;
|
||||
--md-sys-color-success: #4ade80;
|
||||
--md-sys-color-on-success: #14532d;
|
||||
--md-sys-color-success-container: #166534;
|
||||
--md-sys-color-warning: #fbbf24;
|
||||
--md-sys-color-on-warning: #451a03;
|
||||
--md-sys-color-warning-container: #78350f;
|
||||
--md-sys-color-surface-tint: #d0bcff;
|
||||
--md-sys-color-scrim: #000000;
|
||||
--md-sys-color-inverse-surface: #e6e0e9;
|
||||
--md-sys-color-inverse-on-surface: #313033;
|
||||
--md-sys-color-inverse-primary: #6750a4;
|
||||
--md-sys-elevation-level0: none;
|
||||
--md-sys-elevation-level1:
|
||||
0px 1px 2px 0px rgba(0, 0, 0, 0.3), 0px 1px 3px 1px rgba(0, 0, 0, 0.15);
|
||||
--md-sys-elevation-level2:
|
||||
0px 1px 2px 0px rgba(0, 0, 0, 0.3), 0px 2px 6px 2px rgba(0, 0, 0, 0.15);
|
||||
--md-sys-elevation-level3:
|
||||
0px 1px 3px 0px rgba(0, 0, 0, 0.3), 0px 4px 8px 3px rgba(0, 0, 0, 0.15);
|
||||
--md-sys-elevation-level4:
|
||||
0px 2px 3px 0px rgba(0, 0, 0, 0.3), 0px 6px 10px 4px rgba(0, 0, 0, 0.15);
|
||||
--md-sys-elevation-level5:
|
||||
0px 4px 4px 0px rgba(0, 0, 0, 0.3), 0px 8px 12px 6px rgba(0, 0, 0, 0.15);
|
||||
--gradient-primary: linear-gradient(135deg, #4f378a 0%, #6750a4 100%);
|
||||
--gradient-secondary: linear-gradient(135deg, #005662 0%, #00838f 100%);
|
||||
--gradient-surface: linear-gradient(135deg, #1a1c1e 0%, #1e2022 100%);
|
||||
}
|
||||
}
|
||||
682
web/static/js/app.js
Normal file
682
web/static/js/app.js
Normal file
@@ -0,0 +1,682 @@
|
||||
class OnekeyWebApp {
|
||||
constructor() {
|
||||
this.socket = null;
|
||||
this.taskStatus = "idle";
|
||||
this.reconnectTimer = null;
|
||||
this.reconnectAttempts = 0;
|
||||
this.maxReconnectAttempts = 5;
|
||||
this.reconnectDelay = 2000;
|
||||
this.initializeSocket();
|
||||
this.initializeEventListeners();
|
||||
this.checkConfig();
|
||||
}
|
||||
|
||||
initializeSocket() {
|
||||
this.connectWebSocket();
|
||||
}
|
||||
|
||||
connectWebSocket() {
|
||||
try {
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws`;
|
||||
|
||||
this.socket = new WebSocket(wsUrl);
|
||||
|
||||
this.socket.onopen = () => {
|
||||
console.log("Connected to server");
|
||||
this.showSnackbar("已连接到服务器", "success");
|
||||
this.reconnectAttempts = 0;
|
||||
|
||||
this.startHeartbeat();
|
||||
};
|
||||
|
||||
this.socket.onclose = (event) => {
|
||||
console.log("Disconnected from server", event);
|
||||
this.showSnackbar("与服务器连接断开", "error");
|
||||
|
||||
this.stopHeartbeat();
|
||||
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.reconnectAttempts++;
|
||||
console.log(
|
||||
`尝试重连... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`,
|
||||
);
|
||||
this.connectWebSocket();
|
||||
}, this.reconnectDelay);
|
||||
}
|
||||
};
|
||||
|
||||
this.socket.onerror = (error) => {
|
||||
console.error("WebSocket error:", error);
|
||||
};
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
this.handleMessage(message);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse message:", e);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to connect WebSocket:", error);
|
||||
this.showSnackbar("无法连接到服务器", "error");
|
||||
}
|
||||
}
|
||||
|
||||
handleMessage(message) {
|
||||
switch (message.type) {
|
||||
case "connected":
|
||||
console.log(message.data.message);
|
||||
break;
|
||||
case "task_progress":
|
||||
this.addLogEntry(message.data.type, message.data.message);
|
||||
break;
|
||||
case "pong":
|
||||
break;
|
||||
default:
|
||||
console.log("Unknown message type:", message.type);
|
||||
}
|
||||
}
|
||||
|
||||
startHeartbeat() {
|
||||
this.heartbeatInterval = setInterval(() => {
|
||||
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
||||
this.socket.send(JSON.stringify({ type: "ping" }));
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
stopHeartbeat() {
|
||||
if (this.heartbeatInterval) {
|
||||
clearInterval(this.heartbeatInterval);
|
||||
this.heartbeatInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
this.stopHeartbeat();
|
||||
if (this.socket) {
|
||||
this.socket.close();
|
||||
this.socket = null;
|
||||
}
|
||||
}
|
||||
|
||||
initializeEventListeners() {
|
||||
const unlockForm = document.getElementById("unlockForm");
|
||||
unlockForm.addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
this.startUnlockTask();
|
||||
});
|
||||
|
||||
const resetBtn = document.getElementById("resetBtn");
|
||||
resetBtn.addEventListener("click", () => {
|
||||
this.resetForm();
|
||||
});
|
||||
|
||||
const clearLogBtn = document.getElementById("clearLogBtn");
|
||||
clearLogBtn.addEventListener("click", () => {
|
||||
this.clearLogs();
|
||||
});
|
||||
|
||||
const snackbarClose = document.getElementById("snackbarClose");
|
||||
snackbarClose.addEventListener("click", () => {
|
||||
this.hideSnackbar();
|
||||
});
|
||||
|
||||
window.addEventListener("beforeunload", () => {
|
||||
this.disconnect();
|
||||
});
|
||||
}
|
||||
|
||||
async checkConfig() {
|
||||
const configStatus = document.getElementById("configStatus");
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/config");
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
configStatus.innerHTML = this.generateConfigStatusHTML(data.config);
|
||||
} else {
|
||||
configStatus.innerHTML = `
|
||||
<div class="status-item">
|
||||
<span class="material-icons status-icon error">error</span>
|
||||
<span class="status-text">配置加载失败: ${data.message}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
configStatus.innerHTML = `
|
||||
<div class="status-item">
|
||||
<span class="material-icons status-icon error">error</span>
|
||||
<span class="status-text">无法连接到服务器</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
generateConfigStatusHTML(config) {
|
||||
const items = [];
|
||||
|
||||
if (config.steam_path) {
|
||||
items.push(`
|
||||
<div class="status-item">
|
||||
<span class="material-icons status-icon success">check_circle</span>
|
||||
<span class="status-text">Steam路径: ${config.steam_path}</span>
|
||||
</div>
|
||||
`);
|
||||
} else {
|
||||
items.push(`
|
||||
<div class="status-item">
|
||||
<span class="material-icons status-icon error">error</span>
|
||||
<span class="status-text">Steam路径未找到</span>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
if (config.debug_mode) {
|
||||
items.push(`
|
||||
<div class="status-item">
|
||||
<span class="material-icons status-icon warning">bug_report</span>
|
||||
<span class="status-text">调试模式已启用</span>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
return items.join("");
|
||||
}
|
||||
|
||||
toggleAndDLC() {
|
||||
document.getElementById("+DLC").checked = true;
|
||||
}
|
||||
|
||||
async startUnlockTask() {
|
||||
if (this.taskStatus === "running") {
|
||||
this.showSnackbar("已有任务正在运行", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData(document.getElementById("unlockForm"));
|
||||
const appId = formData.get("appId").trim();
|
||||
const toolType = formData.get("toolType");
|
||||
const ADLC = formData.get("+DLC") === "on";
|
||||
|
||||
if (!appId) {
|
||||
this.showSnackbar("请输入App ID", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const appIdPattern = /^[\d-]+$/;
|
||||
if (!appIdPattern.test(appId)) {
|
||||
this.showSnackbar("App ID格式无效,应为数字或用-分隔的数字", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
this.taskStatus = "running";
|
||||
this.updateUIForRunningTask();
|
||||
this.clearLogs();
|
||||
this.addLogEntry("info", `开始处理游戏 ${appId}...`);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/start_unlock", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
app_id: appId,
|
||||
tool_type: toolType,
|
||||
dlc: ADLC,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showSnackbar("任务已开始", "success");
|
||||
this.startStatusPolling();
|
||||
} else {
|
||||
this.taskStatus = "idle";
|
||||
this.updateUIForIdleTask();
|
||||
this.showSnackbar(data.message, "error");
|
||||
this.addLogEntry("error", data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
this.taskStatus = "idle";
|
||||
this.updateUIForIdleTask();
|
||||
this.showSnackbar("启动任务失败", "error");
|
||||
this.addLogEntry("error", `启动任务失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
startStatusPolling() {
|
||||
const pollInterval = setInterval(async () => {
|
||||
try {
|
||||
const response = await fetch("/api/task_status");
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === "completed") {
|
||||
clearInterval(pollInterval);
|
||||
this.taskStatus = "completed";
|
||||
this.updateUIForIdleTask();
|
||||
|
||||
if (data.result && data.result.success) {
|
||||
this.showSnackbar(data.result.message, "success");
|
||||
this.addLogEntry("info", data.result.message);
|
||||
} else if (data.result) {
|
||||
this.showSnackbar(data.result.message, "error");
|
||||
this.addLogEntry("error", data.result.message);
|
||||
}
|
||||
} else if (data.status === "error") {
|
||||
clearInterval(pollInterval);
|
||||
this.taskStatus = "error";
|
||||
this.updateUIForIdleTask();
|
||||
|
||||
if (data.result) {
|
||||
this.showSnackbar(data.result.message, "error");
|
||||
this.addLogEntry("error", data.result.message);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Status polling error:", error);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
updateUIForRunningTask() {
|
||||
const unlockBtn = document.getElementById("unlockBtn");
|
||||
const resetBtn = document.getElementById("resetBtn");
|
||||
const appIdInput = document.getElementById("appId");
|
||||
const toolTypeRadios = document.querySelectorAll('input[name="toolType"]');
|
||||
|
||||
unlockBtn.disabled = true;
|
||||
unlockBtn.innerHTML = `
|
||||
<span class="material-icons">hourglass_empty</span>
|
||||
执行中...
|
||||
`;
|
||||
|
||||
resetBtn.disabled = true;
|
||||
appIdInput.disabled = true;
|
||||
toolTypeRadios.forEach((radio) => (radio.disabled = true));
|
||||
}
|
||||
|
||||
updateUIForIdleTask() {
|
||||
const unlockBtn = document.getElementById("unlockBtn");
|
||||
const resetBtn = document.getElementById("resetBtn");
|
||||
const appIdInput = document.getElementById("appId");
|
||||
const toolTypeRadios = document.querySelectorAll('input[name="toolType"]');
|
||||
|
||||
unlockBtn.disabled = false;
|
||||
unlockBtn.innerHTML = `
|
||||
<span class="material-icons">play_arrow</span>
|
||||
开始解锁
|
||||
`;
|
||||
|
||||
resetBtn.disabled = false;
|
||||
appIdInput.disabled = false;
|
||||
toolTypeRadios.forEach((radio) => (radio.disabled = false));
|
||||
}
|
||||
|
||||
resetForm() {
|
||||
if (this.taskStatus === "running") {
|
||||
this.showSnackbar("任务运行中,无法重置", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("unlockForm").reset();
|
||||
document.querySelector(
|
||||
'input[name="toolType"][value="steamtools"]',
|
||||
).checked = true;
|
||||
this.clearLogs();
|
||||
this.showSnackbar("表单已重置", "success");
|
||||
}
|
||||
|
||||
addLogEntry(type, message) {
|
||||
const progressContainer = document.getElementById("progressContainer");
|
||||
const placeholder = progressContainer.querySelector(
|
||||
".progress-placeholder",
|
||||
);
|
||||
|
||||
if (placeholder) {
|
||||
placeholder.remove();
|
||||
}
|
||||
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const logEntry = document.createElement("div");
|
||||
logEntry.className = `log-entry ${type}`;
|
||||
logEntry.innerHTML = `
|
||||
<span class="log-timestamp">${timestamp}</span>
|
||||
<span class="log-message">${this.escapeHtml(message)}</span>
|
||||
`;
|
||||
|
||||
progressContainer.appendChild(logEntry);
|
||||
progressContainer.scrollTop = progressContainer.scrollHeight;
|
||||
}
|
||||
|
||||
clearLogs() {
|
||||
const progressContainer = document.getElementById("progressContainer");
|
||||
progressContainer.innerHTML = `
|
||||
<div class="progress-placeholder">
|
||||
<span class="material-icons">info</span>
|
||||
<p>等待任务开始...</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
showSnackbar(message, type = "info") {
|
||||
const snackbar = document.getElementById("snackbar");
|
||||
const snackbarMessage = document.getElementById("snackbarMessage");
|
||||
|
||||
snackbarMessage.textContent = message;
|
||||
snackbar.className = `snackbar ${type}`;
|
||||
|
||||
snackbar.offsetHeight;
|
||||
|
||||
snackbar.classList.add("show");
|
||||
|
||||
setTimeout(() => {
|
||||
this.hideSnackbar();
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
hideSnackbar() {
|
||||
const snackbar = document.getElementById("snackbar");
|
||||
snackbar.classList.remove("show");
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
@keyframes iconRotate {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
|
||||
anchor.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute("href"));
|
||||
if (target) {
|
||||
target.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const cards = document.querySelectorAll(".card");
|
||||
|
||||
cards.forEach((card) => {
|
||||
card.addEventListener("mousemove", (e) => {
|
||||
const rect = card.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
const centerX = rect.width / 2;
|
||||
const centerY = rect.height / 2;
|
||||
|
||||
const percentX = (x - centerX) / centerX;
|
||||
const percentY = (y - centerY) / centerY;
|
||||
|
||||
const rotateX = percentY * 5;
|
||||
const rotateY = percentX * 5;
|
||||
|
||||
card.style.transform = `perspective(1000px) rotateX(${-rotateX}deg) rotateY(${rotateY}deg) translateZ(10px)`;
|
||||
});
|
||||
|
||||
card.addEventListener("mouseleave", () => {
|
||||
card.style.transform = "";
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function typeWriter(element, text, speed = 50) {
|
||||
let i = 0;
|
||||
element.textContent = "";
|
||||
|
||||
function type() {
|
||||
if (i < text.length) {
|
||||
element.textContent += text.charAt(i);
|
||||
i++;
|
||||
setTimeout(type, speed);
|
||||
}
|
||||
}
|
||||
|
||||
type();
|
||||
}
|
||||
|
||||
function animateValue(element, start, end, duration) {
|
||||
const range = end - start;
|
||||
const increment = range / (duration / 16);
|
||||
let current = start;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
current += increment;
|
||||
if (
|
||||
(increment > 0 && current >= end) ||
|
||||
(increment < 0 && current <= end)
|
||||
) {
|
||||
current = end;
|
||||
clearInterval(timer);
|
||||
}
|
||||
element.textContent = Math.round(current);
|
||||
}, 16);
|
||||
}
|
||||
|
||||
document.querySelectorAll(".btn").forEach((button) => {
|
||||
button.addEventListener("mousemove", (e) => {
|
||||
const rect = button.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left - rect.width / 2;
|
||||
const y = e.clientY - rect.top - rect.height / 2;
|
||||
|
||||
button.style.transform = `translate(${x * 0.1}px, ${y * 0.1}px)`;
|
||||
});
|
||||
|
||||
button.addEventListener("mouseleave", () => {
|
||||
button.style.transform = "";
|
||||
});
|
||||
});
|
||||
|
||||
function createParticles() {
|
||||
const particlesContainer = document.createElement("div");
|
||||
particlesContainer.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
`;
|
||||
document.body.appendChild(particlesContainer);
|
||||
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const particle = document.createElement("div");
|
||||
particle.style.cssText = `
|
||||
position: absolute;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: rgba(94, 53, 177, 0.3);
|
||||
border-radius: 50%;
|
||||
top: ${Math.random() * 100}%;
|
||||
left: ${Math.random() * 100}%;
|
||||
animation: floatParticle ${
|
||||
10 + Math.random() * 20
|
||||
}s linear infinite;
|
||||
`;
|
||||
particlesContainer.appendChild(particle);
|
||||
}
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
@keyframes floatParticle {
|
||||
0% {
|
||||
transform: translateY(100vh) rotate(0deg);
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
opacity: 1;
|
||||
}
|
||||
90% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-100vh) rotate(720deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
const observerOptions = {
|
||||
threshold: 0.1,
|
||||
rootMargin: "0px 0px -50px 0px",
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.style.opacity = "1";
|
||||
entry.target.style.transform = "translateY(0)";
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
document.querySelectorAll(".card").forEach((card) => {
|
||||
card.style.opacity = "0";
|
||||
card.style.transform = "translateY(20px)";
|
||||
card.style.transition = "opacity 0.6s ease, transform 0.6s ease";
|
||||
observer.observe(card);
|
||||
});
|
||||
|
||||
document.addEventListener("mousemove", (e) => {
|
||||
const light = document.createElement("div");
|
||||
light.style.cssText = `
|
||||
position: fixed;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background: radial-gradient(circle, rgba(94, 53, 177, 0.1) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
transform: translate(-50%, -50%);
|
||||
transition: opacity 0.3s ease;
|
||||
`;
|
||||
light.style.left = e.clientX + "px";
|
||||
light.style.top = e.clientY + "px";
|
||||
document.body.appendChild(light);
|
||||
|
||||
setTimeout(() => {
|
||||
light.style.opacity = "0";
|
||||
setTimeout(() => light.remove(), 300);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
document.querySelectorAll(".status-icon").forEach((icon) => {
|
||||
if (icon.classList.contains("success")) {
|
||||
icon.style.animation = "pulse-icon 2s ease-in-out infinite";
|
||||
}
|
||||
});
|
||||
|
||||
const pulseStyle = document.createElement("style");
|
||||
pulseStyle.textContent = `
|
||||
@keyframes pulse-icon {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(pulseStyle);
|
||||
|
||||
const originalShowSnackbar = window.showSnackbar;
|
||||
if (typeof originalShowSnackbar === "function") {
|
||||
window.showSnackbar = function (message, type = "info") {
|
||||
originalShowSnackbar(message, type);
|
||||
|
||||
if ("vibrate" in navigator) {
|
||||
if (type === "error") {
|
||||
navigator.vibrate([100, 50, 100]);
|
||||
} else {
|
||||
navigator.vibrate(50);
|
||||
}
|
||||
}
|
||||
|
||||
const audio = new Audio(
|
||||
`data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBSuBzvLZiTYIG2m98OScTgwOUarm7blmFgU7k9n1unEiBC13yO/eizEIHWq+8+OWT` +
|
||||
`BEFS6Xj67xqGAU+lNr1unIiBCx0xvDdiTYIHWu+8+OWT`,
|
||||
);
|
||||
if (type === "success") {
|
||||
audio.volume = 0.1;
|
||||
audio.play().catch(() => {});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
document.querySelectorAll(".text-field").forEach((input) => {
|
||||
input.addEventListener("focus", (e) => {
|
||||
const ripple = document.createElement("div");
|
||||
ripple.style.cssText = `
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border: 2px solid var(--md-sys-color-primary);
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
animation: inputRipple 0.6s ease-out;
|
||||
`;
|
||||
|
||||
const wrapper = input.parentElement;
|
||||
wrapper.style.position = "relative";
|
||||
wrapper.appendChild(ripple);
|
||||
|
||||
setTimeout(() => ripple.remove(), 600);
|
||||
});
|
||||
});
|
||||
|
||||
const inputRippleStyle = document.createElement("style");
|
||||
inputRippleStyle.textContent = `
|
||||
@keyframes inputRipple {
|
||||
0% {
|
||||
transform: scale(0.8);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(inputRippleStyle);
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
createParticles();
|
||||
document.body.classList.add("loaded");
|
||||
console.log("UI 增强效果已加载 ✨");
|
||||
|
||||
new OnekeyWebApp();
|
||||
});
|
||||
180
web/static/js/project-info.js
Normal file
180
web/static/js/project-info.js
Normal file
@@ -0,0 +1,180 @@
|
||||
class ProjectInfoEnhancer {
|
||||
constructor() {
|
||||
this.initializeProjectInfo();
|
||||
}
|
||||
|
||||
initializeProjectInfo() {
|
||||
this.addProjectLinkTracking();
|
||||
|
||||
this.addVersionClickEaster();
|
||||
|
||||
this.addLogoClickEffect();
|
||||
}
|
||||
|
||||
addProjectLinkTracking() {
|
||||
const projectLinks = document.querySelectorAll(".project-link");
|
||||
projectLinks.forEach((link) => {
|
||||
link.addEventListener("click", (e) => {
|
||||
const linkType = link.classList.contains("github")
|
||||
? "GitHub仓库"
|
||||
: link.classList.contains("releases")
|
||||
? "下载发布版"
|
||||
: link.classList.contains("docs")
|
||||
? "使用文档"
|
||||
: link.classList.contains("issues")
|
||||
? "问题反馈"
|
||||
: "未知链接";
|
||||
|
||||
console.log(`用户点击了 ${linkType} 链接`);
|
||||
|
||||
link.style.transform = "scale(0.95)";
|
||||
setTimeout(() => {
|
||||
link.style.transform = "";
|
||||
}, 150);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
addVersionClickEaster() {
|
||||
const versionLabels = document.querySelectorAll(".version-label");
|
||||
let clickCount = 0;
|
||||
|
||||
versionLabels.forEach((label) => {
|
||||
label.addEventListener("click", () => {
|
||||
clickCount++;
|
||||
|
||||
if (clickCount === 5) {
|
||||
this.showEasterEgg();
|
||||
clickCount = 0;
|
||||
}
|
||||
|
||||
label.style.animation = "pulse 0.3s ease";
|
||||
setTimeout(() => {
|
||||
label.style.animation = "";
|
||||
}, 300);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
addLogoClickEffect() {
|
||||
const logos = document.querySelectorAll(".project-logo");
|
||||
|
||||
logos.forEach((logo) => {
|
||||
logo.addEventListener("click", () => {
|
||||
logo.style.transform = "rotate(360deg)";
|
||||
logo.style.transition = "transform 0.6s ease";
|
||||
|
||||
setTimeout(() => {
|
||||
logo.style.transform = "";
|
||||
logo.style.transition = "";
|
||||
}, 600);
|
||||
|
||||
this.showTooltip(logo, "🎮 Onekey - 让Steam解锁变得简单!");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
showEasterEgg() {
|
||||
const messages = [
|
||||
"🎉 你发现了隐藏彩蛋!",
|
||||
"🚀 感谢你使用Onekey工具!",
|
||||
"⭐ 别忘了给项目点个Star哦!",
|
||||
"🎮 祝你游戏愉快!",
|
||||
|
||||
"🔓 一键解锁,畅享游戏!",
|
||||
];
|
||||
|
||||
const randomMessage = messages[Math.floor(Math.random() * messages.length)];
|
||||
|
||||
const easterEgg = document.createElement("div");
|
||||
easterEgg.className = "easter-egg";
|
||||
easterEgg.textContent = randomMessage;
|
||||
easterEgg.style.cssText = `
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: linear-gradient(45deg, #6750a4, #7d5260);
|
||||
color: white;
|
||||
padding: 20px 30px;
|
||||
border-radius: 15px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
z-index: 9999;
|
||||
animation: easterEggBounce 0.6s ease-out;
|
||||
`;
|
||||
|
||||
if (!document.getElementById("easter-egg-styles")) {
|
||||
const style = document.createElement("style");
|
||||
style.id = "easter-egg-styles";
|
||||
style.textContent = `
|
||||
@keyframes easterEggBounce {
|
||||
0% { transform: translate(-50%, -50%) scale(0); opacity: 0; }
|
||||
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 1; }
|
||||
100% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
|
||||
}
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
document.body.appendChild(easterEgg);
|
||||
|
||||
setTimeout(() => {
|
||||
easterEgg.style.animation = "easterEggBounce 0.3s ease-in reverse";
|
||||
setTimeout(() => {
|
||||
if (easterEgg.parentNode) {
|
||||
easterEgg.parentNode.removeChild(easterEgg);
|
||||
}
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
showTooltip(element, message) {
|
||||
const tooltip = document.createElement("div");
|
||||
tooltip.className = "custom-tooltip";
|
||||
tooltip.textContent = message;
|
||||
tooltip.style.cssText = `
|
||||
position: absolute;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
tooltip.style.left = rect.left + rect.width / 2 + "px";
|
||||
tooltip.style.top = rect.bottom + 10 + "px";
|
||||
tooltip.style.transform = "translateX(-50%)";
|
||||
|
||||
document.body.appendChild(tooltip);
|
||||
|
||||
setTimeout(() => {
|
||||
tooltip.style.opacity = "1";
|
||||
}, 10);
|
||||
|
||||
setTimeout(() => {
|
||||
tooltip.style.opacity = "0";
|
||||
setTimeout(() => {
|
||||
if (tooltip.parentNode) {
|
||||
tooltip.parentNode.removeChild(tooltip);
|
||||
}
|
||||
}, 300);
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
new ProjectInfoEnhancer();
|
||||
});
|
||||
641
web/static/js/settings.js
Normal file
641
web/static/js/settings.js
Normal file
@@ -0,0 +1,641 @@
|
||||
class SettingsManager {
|
||||
constructor() {
|
||||
this.currentConfig = {};
|
||||
this.currentKeyInfo = null;
|
||||
this.newKeyData = null;
|
||||
this.initializeEventListeners();
|
||||
this.loadConfig();
|
||||
this.loadKeyInfo();
|
||||
}
|
||||
|
||||
initializeEventListeners() {
|
||||
document.getElementById("saveConfig").addEventListener("click", () => {
|
||||
this.saveConfig();
|
||||
});
|
||||
|
||||
document.getElementById("resetConfig").addEventListener("click", () => {
|
||||
this.showConfirmDialog(
|
||||
"重置配置",
|
||||
"确定要重置所有配置为默认值吗?此操作不可恢复。",
|
||||
() => this.resetConfig()
|
||||
);
|
||||
});
|
||||
|
||||
document.getElementById("testConfig").addEventListener("click", () => {
|
||||
this.testConfig();
|
||||
});
|
||||
|
||||
document.getElementById("detectSteamPath").addEventListener("click", () => {
|
||||
this.detectSteamPath();
|
||||
});
|
||||
|
||||
document.getElementById("steamPath").addEventListener("input", () => {
|
||||
this.validateSteamPath();
|
||||
});
|
||||
|
||||
document.getElementById("verifyNewKey").addEventListener("click", () => {
|
||||
this.verifyNewKey();
|
||||
});
|
||||
|
||||
document.getElementById("changeKey").addEventListener("click", () => {
|
||||
this.showConfirmDialog(
|
||||
"更换卡密",
|
||||
"确定要更换为新的卡密吗?更换后需要重新验证。",
|
||||
() => this.changeKey()
|
||||
);
|
||||
});
|
||||
|
||||
document.getElementById("newKey").addEventListener("input", () => {
|
||||
this.resetNewKeyStatus();
|
||||
});
|
||||
|
||||
document.getElementById("newKey").addEventListener("keypress", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
this.verifyNewKey();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("dialogCancel").addEventListener("click", () => {
|
||||
this.hideConfirmDialog();
|
||||
});
|
||||
|
||||
document.getElementById("dialogConfirm").addEventListener("click", () => {
|
||||
this.executeConfirmAction();
|
||||
});
|
||||
|
||||
document.getElementById("snackbarClose").addEventListener("click", () => {
|
||||
this.hideSnackbar();
|
||||
});
|
||||
}
|
||||
|
||||
async loadConfig() {
|
||||
try {
|
||||
const response = await fetch("/api/config/detailed");
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.currentConfig = data.config;
|
||||
this.populateForm();
|
||||
this.updateConfigStatus();
|
||||
} else {
|
||||
this.showSnackbar("加载配置失败: " + data.message, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
this.showSnackbar("无法连接到服务器", "error");
|
||||
console.error("Load config error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadKeyInfo() {
|
||||
const keyInfoSection = document.getElementById("keyInfoSection");
|
||||
|
||||
try {
|
||||
const configResponse = await fetch("/api/config/detailed");
|
||||
const configData = await configResponse.json();
|
||||
|
||||
if (!configData.success || !configData.config.key) {
|
||||
keyInfoSection.innerHTML = `
|
||||
<div class="expiry-warning">
|
||||
<span class="material-icons">warning</span>
|
||||
<div>
|
||||
<strong>未设置卡密</strong><br>
|
||||
请在下方输入您的授权卡密
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const keyResponse = await fetch("/api/getKeyInfo", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ key: configData.config.key }),
|
||||
});
|
||||
|
||||
const keyData = await keyResponse.json();
|
||||
|
||||
if (keyData.key && keyData.info) {
|
||||
this.currentKeyInfo = keyData.info;
|
||||
this.displayKeyInfo(keyData.info);
|
||||
} else {
|
||||
keyInfoSection.innerHTML = `
|
||||
<div class="expiry-warning">
|
||||
<span class="material-icons">error</span>
|
||||
<div>
|
||||
<strong>卡密验证失败</strong><br>
|
||||
当前卡密无效或已过期,请更换新的卡密
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
keyInfoSection.innerHTML = `
|
||||
<div class="expiry-warning">
|
||||
<span class="material-icons">error</span>
|
||||
<div>
|
||||
<strong>获取卡密信息失败</strong><br>
|
||||
请检查网络连接或联系客服
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
console.error("Load key info error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
displayKeyInfo(keyInfo) {
|
||||
const keyInfoSection = document.getElementById("keyInfoSection");
|
||||
const expiresAt = new Date(keyInfo.expiresAt);
|
||||
const createdAt = new Date(keyInfo.createdAt);
|
||||
const firstUsedAt = keyInfo.firstUsedAt
|
||||
? new Date(keyInfo.firstUsedAt)
|
||||
: null;
|
||||
const now = new Date();
|
||||
|
||||
const isExpired = expiresAt < now;
|
||||
const daysLeft = Math.ceil((expiresAt - now) / (1000 * 60 * 60 * 24));
|
||||
const isExpiringSoon = daysLeft <= 7 && daysLeft > 0;
|
||||
|
||||
const typeNames = {
|
||||
day: "日卡",
|
||||
week: "周卡",
|
||||
month: "月卡",
|
||||
year: "年卡",
|
||||
permanent: "永久卡",
|
||||
};
|
||||
|
||||
let statusBadge = "";
|
||||
if (isExpired && keyInfo.type != "permanent") {
|
||||
statusBadge =
|
||||
'<span class="key-status-badge expired"><span class="material-icons" style="font-size: 14px;">cancel</span>已过期</span>';
|
||||
} else if (!keyInfo.isActive) {
|
||||
statusBadge =
|
||||
'<span class="key-status-badge inactive"><span class="material-icons" style="font-size: 14px;">pause</span>未激活</span>';
|
||||
} else {
|
||||
statusBadge =
|
||||
'<span class="key-status-badge active"><span class="material-icons" style="font-size: 14px;">check_circle</span>正常</span>';
|
||||
}
|
||||
|
||||
let warningSection = "";
|
||||
if (isExpiringSoon) {
|
||||
warningSection = `
|
||||
<div class="expiry-warning">
|
||||
<span class="material-icons">schedule</span>
|
||||
<div>
|
||||
<strong>即将到期提醒</strong><br>
|
||||
您的卡密将在 ${daysLeft} 天后到期,请及时续费
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
keyInfoSection.innerHTML = `
|
||||
<div class="key-info-grid">
|
||||
<div class="key-info-card">
|
||||
<span class="material-icons key-info-icon">fingerprint</span>
|
||||
<div class="key-info-content">
|
||||
<div class="key-info-label">卡密</div>
|
||||
<div class="key-info-value">${keyInfo.key.substring(
|
||||
0,
|
||||
8
|
||||
)}...${keyInfo.key.substring(
|
||||
keyInfo.key.length - 8
|
||||
)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="key-info-card">
|
||||
<span class="material-icons key-info-icon">label</span>
|
||||
<div class="key-info-content">
|
||||
<div class="key-info-label">类型</div>
|
||||
<div class="key-info-value">${
|
||||
typeNames[keyInfo.type] || keyInfo.type
|
||||
}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="key-info-card">
|
||||
<span class="material-icons key-info-icon">toggle_on</span>
|
||||
<div class="key-info-content">
|
||||
<div class="key-info-label">状态</div>
|
||||
<div class="key-info-value">${statusBadge}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="key-info-card">
|
||||
<span class="material-icons key-info-icon">event</span>
|
||||
<div class="key-info-content">
|
||||
<div class="key-info-label">到期时间</div>
|
||||
<div class="key-info-value">${expiresAt.toLocaleDateString()} ${expiresAt
|
||||
.toLocaleTimeString()
|
||||
.substring(0, 5)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="key-info-card">
|
||||
<span class="material-icons key-info-icon">analytics</span>
|
||||
<div class="key-info-content">
|
||||
<div class="key-info-label">使用次数</div>
|
||||
<div class="key-info-value">${keyInfo.usageCount} / ${
|
||||
keyInfo.totalUsage || "∞"
|
||||
}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="key-info-card">
|
||||
<span class="material-icons key-info-icon">schedule</span>
|
||||
<div class="key-info-content">
|
||||
<div class="key-info-label">创建时间</div>
|
||||
<div class="key-info-value">${createdAt.toLocaleDateString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${warningSection}
|
||||
`;
|
||||
}
|
||||
|
||||
async verifyNewKey() {
|
||||
const newKeyInput = document.getElementById("newKey");
|
||||
const key = newKeyInput.value.trim();
|
||||
|
||||
if (!key) {
|
||||
this.showSnackbar("请输入新卡密", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!key.match(/^[A-Z0-9_-]+$/)) {
|
||||
this.showSnackbar("卡密格式不正确", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const verifyBtn = document.getElementById("verifyNewKey");
|
||||
const changeBtn = document.getElementById("changeKey");
|
||||
|
||||
verifyBtn.disabled = true;
|
||||
verifyBtn.innerHTML =
|
||||
'<span class="material-icons">hourglass_empty</span>验证中...';
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/getKeyInfo", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ key: key }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.key && data.info) {
|
||||
this.newKeyData = data.info;
|
||||
this.showSnackbar("新卡密验证成功!", "success");
|
||||
|
||||
changeBtn.style.display = "flex";
|
||||
verifyBtn.style.display = "none";
|
||||
|
||||
const typeNames = {
|
||||
day: "日卡",
|
||||
week: "周卡",
|
||||
month: "月卡",
|
||||
year: "年卡",
|
||||
permanent: "永久卡",
|
||||
};
|
||||
|
||||
const expiresAt = new Date(data.info.expiresAt);
|
||||
this.showSnackbar(
|
||||
`验证成功!新卡密类型:${
|
||||
typeNames[data.info.type]
|
||||
},有效期至:${expiresAt.toLocaleDateString()}`,
|
||||
"success"
|
||||
);
|
||||
} else {
|
||||
this.showSnackbar("新卡密无效或已过期", "error");
|
||||
this.newKeyData = null;
|
||||
}
|
||||
} catch (error) {
|
||||
this.showSnackbar("验证失败,请检查网络连接", "error");
|
||||
console.error("New key verification error:", error);
|
||||
} finally {
|
||||
verifyBtn.disabled = false;
|
||||
verifyBtn.innerHTML = '<span class="material-icons">check</span>验证';
|
||||
}
|
||||
}
|
||||
|
||||
async changeKey() {
|
||||
if (!this.newKeyData) {
|
||||
this.showSnackbar("请先验证新卡密", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newKey = document.getElementById("newKey").value.trim();
|
||||
|
||||
const updateData = {
|
||||
key: newKey,
|
||||
steam_path: this.currentConfig.steam_path || "",
|
||||
debug_mode: this.currentConfig.debug_mode || false,
|
||||
logging_files: this.currentConfig.logging_files !== false,
|
||||
show_console: this.currentConfig.show_console !== false,
|
||||
};
|
||||
|
||||
const response = await fetch("/api/config/update", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(updateData),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showSnackbar("卡密更换成功!", "success");
|
||||
|
||||
await this.loadKeyInfo();
|
||||
|
||||
this.resetNewKeyStatus();
|
||||
document.getElementById("newKey").value = "";
|
||||
} else {
|
||||
this.showSnackbar("更换失败: " + data.message, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
this.showSnackbar("更换卡密时发生错误", "error");
|
||||
console.error("Change key error:", error);
|
||||
}
|
||||
|
||||
this.hideConfirmDialog();
|
||||
}
|
||||
|
||||
resetNewKeyStatus() {
|
||||
const verifyBtn = document.getElementById("verifyNewKey");
|
||||
const changeBtn = document.getElementById("changeKey");
|
||||
|
||||
verifyBtn.style.display = "flex";
|
||||
changeBtn.style.display = "none";
|
||||
this.newKeyData = null;
|
||||
}
|
||||
|
||||
populateForm() {
|
||||
document.getElementById("steamPath").value =
|
||||
this.currentConfig.steam_path || "";
|
||||
document.getElementById("debugMode").checked =
|
||||
this.currentConfig.debug_mode || false;
|
||||
document.getElementById("loggingFiles").checked =
|
||||
this.currentConfig.logging_files !== false;
|
||||
document.getElementById("showConsole").checked =
|
||||
this.currentConfig.show_console !== false;
|
||||
|
||||
this.validateSteamPath();
|
||||
}
|
||||
|
||||
async saveConfig() {
|
||||
try {
|
||||
const config = {
|
||||
key: this.currentConfig.key || "",
|
||||
steam_path: document.getElementById("steamPath").value.trim(),
|
||||
debug_mode: document.getElementById("debugMode").checked,
|
||||
logging_files: document.getElementById("loggingFiles").checked,
|
||||
show_console: document.getElementById("showConsole").checked,
|
||||
};
|
||||
|
||||
const response = await fetch("/api/config/update", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showSnackbar("配置已保存", "success");
|
||||
await this.loadConfig();
|
||||
} else {
|
||||
this.showSnackbar("保存失败: " + data.message, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
this.showSnackbar("保存配置时发生错误", "error");
|
||||
console.error("Save config error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async resetConfig() {
|
||||
try {
|
||||
const response = await fetch("/api/config/reset", {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showSnackbar("配置已重置(卡密保持不变)", "success");
|
||||
await this.loadConfig();
|
||||
} else {
|
||||
this.showSnackbar("重置失败: " + data.message, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
this.showSnackbar("重置配置时发生错误", "error");
|
||||
console.error("Reset config error:", error);
|
||||
}
|
||||
|
||||
this.hideConfirmDialog();
|
||||
}
|
||||
|
||||
async testConfig() {
|
||||
this.showSnackbar("正在测试配置...", "info");
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/config");
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
let messages = [];
|
||||
|
||||
if (data.config.steam_path) {
|
||||
messages.push("✓ Steam 路径配置正常");
|
||||
} else {
|
||||
messages.push("✗ Steam 路径配置异常");
|
||||
}
|
||||
|
||||
if (this.currentKeyInfo) {
|
||||
const expiresAt = new Date(this.currentKeyInfo.expiresAt);
|
||||
let isExpired = expiresAt < new Date();
|
||||
|
||||
if ((this.currentKeyInfo.type = "permanent")) {
|
||||
isExpired = false;
|
||||
}
|
||||
|
||||
if (this.currentKeyInfo.isActive && !isExpired) {
|
||||
messages.push("✓ 卡密状态正常");
|
||||
} else {
|
||||
messages.push("✗ 卡密状态异常");
|
||||
}
|
||||
}
|
||||
|
||||
this.showSnackbar(`配置测试完成: ${messages.join(", ")}`, "success");
|
||||
} else {
|
||||
this.showSnackbar("配置测试失败: " + data.message, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
this.showSnackbar("配置测试时发生错误", "error");
|
||||
console.error("Test config error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
detectSteamPath() {
|
||||
const commonPaths = [
|
||||
"C:\\Program Files (x86)\\Steam",
|
||||
"C:\\Program Files\\Steam",
|
||||
"D:\\Steam",
|
||||
"E:\\Steam",
|
||||
];
|
||||
|
||||
const suggestedPath = commonPaths[0];
|
||||
document.getElementById("steamPath").value = suggestedPath;
|
||||
|
||||
this.validateSteamPath();
|
||||
this.showSnackbar("已设置为常见路径,请确认是否正确", "info");
|
||||
}
|
||||
|
||||
validateSteamPath() {
|
||||
const steamPath = document.getElementById("steamPath").value.trim();
|
||||
const statusElement = document.getElementById("steamPathStatus");
|
||||
|
||||
if (!steamPath) {
|
||||
statusElement.className = "status-indicator";
|
||||
statusElement.innerHTML = `
|
||||
<span class="material-icons status-icon">info</span>
|
||||
<span class="status-text">将使用自动检测的路径</span>
|
||||
`;
|
||||
} else {
|
||||
if (steamPath.toLowerCase().includes("steam")) {
|
||||
statusElement.className = "status-indicator success";
|
||||
statusElement.innerHTML = `
|
||||
<span class="material-icons status-icon">check_circle</span>
|
||||
<span class="status-text">路径格式看起来正确</span>
|
||||
`;
|
||||
} else {
|
||||
statusElement.className = "status-indicator warning";
|
||||
statusElement.innerHTML = `
|
||||
<span class="material-icons status-icon">warning</span>
|
||||
<span class="status-text">路径可能不正确,请确认</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateConfigStatus() {
|
||||
const statusGrid = document.getElementById("configStatusGrid");
|
||||
const config = this.currentConfig;
|
||||
|
||||
const statusCards = [];
|
||||
|
||||
if (config.steam_path && config.steam_path_exists) {
|
||||
statusCards.push({
|
||||
type: "success",
|
||||
icon: "folder",
|
||||
title: "Steam 路径",
|
||||
description: `路径有效: ${config.steam_path}`,
|
||||
});
|
||||
} else if (config.steam_path) {
|
||||
statusCards.push({
|
||||
type: "warning",
|
||||
icon: "folder_off",
|
||||
title: "Steam 路径",
|
||||
description: "路径已设置但可能无效",
|
||||
});
|
||||
} else {
|
||||
statusCards.push({
|
||||
type: "error",
|
||||
icon: "error",
|
||||
title: "Steam 路径",
|
||||
description: "未设置或自动检测失败",
|
||||
});
|
||||
}
|
||||
|
||||
if (config.debug_mode) {
|
||||
statusCards.push({
|
||||
type: "warning",
|
||||
icon: "bug_report",
|
||||
title: "调试模式",
|
||||
description: "已启用,会输出详细日志",
|
||||
});
|
||||
}
|
||||
|
||||
if (config.logging_files) {
|
||||
statusCards.push({
|
||||
type: "success",
|
||||
icon: "description",
|
||||
title: "日志文件",
|
||||
description: "已启用,日志将保存到文件",
|
||||
});
|
||||
}
|
||||
|
||||
statusGrid.innerHTML = statusCards
|
||||
.map(
|
||||
(card) => `
|
||||
<div class="status-card ${card.type}">
|
||||
<span class="material-icons status-card-icon">${card.icon}</span>
|
||||
<div class="status-card-content">
|
||||
<div class="status-card-title">${card.title}</div>
|
||||
<div class="status-card-description">${card.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
showConfirmDialog(title, message, confirmAction) {
|
||||
document.getElementById("dialogTitle").textContent = title;
|
||||
document.getElementById("dialogMessage").textContent = message;
|
||||
this.confirmAction = confirmAction;
|
||||
|
||||
const dialog = document.getElementById("confirmDialog");
|
||||
dialog.classList.add("show");
|
||||
}
|
||||
|
||||
hideConfirmDialog() {
|
||||
const dialog = document.getElementById("confirmDialog");
|
||||
dialog.classList.remove("show");
|
||||
this.confirmAction = null;
|
||||
}
|
||||
|
||||
executeConfirmAction() {
|
||||
if (this.confirmAction) {
|
||||
this.confirmAction();
|
||||
}
|
||||
this.hideConfirmDialog();
|
||||
}
|
||||
|
||||
showSnackbar(message, type = "info") {
|
||||
const snackbar = document.getElementById("snackbar");
|
||||
const snackbarMessage = document.getElementById("snackbarMessage");
|
||||
|
||||
snackbarMessage.textContent = message;
|
||||
snackbar.className = `snackbar ${type}`;
|
||||
|
||||
snackbar.offsetHeight;
|
||||
|
||||
snackbar.classList.add("show");
|
||||
|
||||
setTimeout(() => {
|
||||
this.hideSnackbar();
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
hideSnackbar() {
|
||||
const snackbar = document.getElementById("snackbar");
|
||||
snackbar.classList.remove("show");
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
window.location.href = "/";
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
new SettingsManager();
|
||||
});
|
||||
178
web/static/js/theme.js
Normal file
178
web/static/js/theme.js
Normal file
@@ -0,0 +1,178 @@
|
||||
class ThemeManager {
|
||||
constructor() {
|
||||
this.themeToggle = document.getElementById("themeToggle");
|
||||
this.currentTheme = this.getStoredTheme() || this.getPreferredTheme();
|
||||
this.isTransitioning = false;
|
||||
|
||||
this.createThemeIndicator();
|
||||
this.applyTheme(this.currentTheme, false);
|
||||
this.initializeEventListeners();
|
||||
}
|
||||
|
||||
getStoredTheme() {
|
||||
return localStorage.getItem("theme");
|
||||
}
|
||||
|
||||
getPreferredTheme() {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
|
||||
createThemeIndicator() {
|
||||
const indicator = document.createElement("div");
|
||||
indicator.className = "theme-indicator";
|
||||
indicator.id = "themeIndicator";
|
||||
document.body.appendChild(indicator);
|
||||
this.themeIndicator = indicator;
|
||||
}
|
||||
|
||||
showThemeIndicator(message) {
|
||||
this.themeIndicator.textContent = message;
|
||||
this.themeIndicator.classList.add("show");
|
||||
|
||||
setTimeout(() => {
|
||||
this.themeIndicator.classList.remove("show");
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
applyTheme(theme, animate = true) {
|
||||
if (this.isTransitioning) return;
|
||||
|
||||
this.isTransitioning = true;
|
||||
|
||||
if (animate) {
|
||||
document.body.classList.add("theme-transitioning");
|
||||
}
|
||||
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
this.updateToggleButton(theme);
|
||||
localStorage.setItem("theme", theme);
|
||||
this.currentTheme = theme;
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("themechange", {
|
||||
detail: { theme, animated: animate },
|
||||
}),
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.classList.remove("theme-transitioning");
|
||||
this.isTransitioning = false;
|
||||
}, 600);
|
||||
}
|
||||
|
||||
updateToggleButton(theme) {
|
||||
if (this.themeToggle) {
|
||||
const icon = this.themeToggle.querySelector(".material-icons");
|
||||
icon.textContent = theme === "dark" ? "dark_mode" : "light_mode";
|
||||
this.themeToggle.title =
|
||||
theme === "dark" ? "切换到浅色模式" : "切换到深色模式";
|
||||
|
||||
icon.style.animation = "none";
|
||||
icon.offsetHeight;
|
||||
icon.style.animation = "iconRotate 300ms ease";
|
||||
}
|
||||
}
|
||||
|
||||
toggleTheme(event) {
|
||||
const newTheme = this.currentTheme === "dark" ? "light" : "dark";
|
||||
|
||||
if (event && event.currentTarget) {
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
const x = ((rect.left + rect.width / 2) / window.innerWidth) * 100;
|
||||
const y = ((rect.top + rect.height / 2) / window.innerHeight) * 100;
|
||||
|
||||
document.documentElement.style.setProperty("--x", `${x}%`);
|
||||
document.documentElement.style.setProperty("--y", `${y}%`);
|
||||
}
|
||||
|
||||
if ("vibrate" in navigator) {
|
||||
navigator.vibrate(50);
|
||||
}
|
||||
|
||||
this.applyTheme(newTheme);
|
||||
|
||||
this.logThemeSwitch(newTheme);
|
||||
}
|
||||
|
||||
logThemeSwitch(theme) {
|
||||
console.log(`主题切换到: ${theme}`);
|
||||
}
|
||||
|
||||
initializeEventListeners() {
|
||||
if (this.themeToggle) {
|
||||
this.themeToggle.addEventListener("click", (e) => this.toggleTheme(e));
|
||||
|
||||
this.themeToggle.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
this.toggleTheme(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
mediaQuery.addEventListener("change", (e) => {
|
||||
if (!this.getStoredTheme()) {
|
||||
this.applyTheme(e.matches ? "dark" : "light");
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === "T") {
|
||||
e.preventDefault();
|
||||
this.toggleTheme();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("storage", (e) => {
|
||||
if (e.key === "theme" && e.newValue) {
|
||||
this.applyTheme(e.newValue, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getThemePalette() {
|
||||
const computedStyle = getComputedStyle(document.documentElement);
|
||||
return {
|
||||
primary: computedStyle.getPropertyValue("--md-sys-color-primary").trim(),
|
||||
secondary: computedStyle
|
||||
.getPropertyValue("--md-sys-color-secondary")
|
||||
.trim(),
|
||||
surface: computedStyle.getPropertyValue("--md-sys-color-surface").trim(),
|
||||
background: computedStyle
|
||||
.getPropertyValue("--md-sys-color-background")
|
||||
.trim(),
|
||||
onBackground: computedStyle
|
||||
.getPropertyValue("--md-sys-color-on-background")
|
||||
.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
shouldUseDarkMode() {
|
||||
const hour = new Date().getHours();
|
||||
return hour >= 18 || hour < 6;
|
||||
}
|
||||
|
||||
enableAutoThemeSwitch() {
|
||||
const checkTime = () => {
|
||||
if (!this.getStoredTheme()) {
|
||||
const shouldBeDark = this.shouldUseDarkMode();
|
||||
const currentTheme = this.currentTheme;
|
||||
|
||||
if (
|
||||
(shouldBeDark && currentTheme === "light") ||
|
||||
(!shouldBeDark && currentTheme === "dark")
|
||||
) {
|
||||
this.applyTheme(shouldBeDark ? "dark" : "light");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setInterval(checkTime, 60000);
|
||||
checkTime();
|
||||
}
|
||||
}
|
||||
|
||||
window.ThemeManager = new ThemeManager();
|
||||
143
web/templates/about.html
Normal file
143
web/templates/about.html
Normal file
@@ -0,0 +1,143 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Onekey - About</title>
|
||||
|
||||
<!-- Material Design 3 -->
|
||||
<link href="https://cdn.jsdmirror.com/gh/ikun0014/font@main/style.css" rel="stylesheet" />
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
||||
|
||||
<!-- 自定义样式 -->
|
||||
<link rel="stylesheet" href="/static/css/style.css" />
|
||||
</head>
|
||||
|
||||
<div class="footer-content">
|
||||
<!-- 项目介绍卡片 -->
|
||||
<div class="project-info-card">
|
||||
<div class="project-header">
|
||||
<div class="project-logo">
|
||||
<span class="material-icons">extension</span>
|
||||
</div>
|
||||
<div class="project-details">
|
||||
<h3 class="project-name">Onekey</h3>
|
||||
<p class="project-subtitle">直观,优雅的游戏解锁解决方案</p>
|
||||
</div>
|
||||
<div class="project-version">
|
||||
<span class="version-label">v2.0.4</span>
|
||||
<span class="version-type">Web UI</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-links">
|
||||
<a href="https://github.com/ikunshare/Onekey" target="_blank" class="project-link github">
|
||||
<span class="material-icons">code</span>
|
||||
<div class="link-content">
|
||||
<span class="link-title">GitHub 仓库</span>
|
||||
<span class="link-url">github.com/ikunshare/Onekey</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href="https://github.com/ikunshare/Onekey/releases" target="_blank" class="project-link releases">
|
||||
<span class="material-icons">file_download</span>
|
||||
<div class="link-content">
|
||||
<span class="link-title">下载发布版</span>
|
||||
<span class="link-url">获取最新版本</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href="http://192.140.166.230:911" target="_blank" class="project-link buy_cdk">
|
||||
<span class="material-icons">buy</span>
|
||||
<div class="link-content">
|
||||
<span class="link-title">购卡链接</span>
|
||||
<span class="link-url">购买卡密</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href="https://github.com/qwq-xinkeng" target="_blank" class="project-link author">
|
||||
<span class="material-icons">person</span>
|
||||
<div class="link-content">
|
||||
<span class="link-title">作者主页</span>
|
||||
<span class="link-url">github.com/qwq-xinkeng</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href="https://github.com/ikun0014" target="_blank" class="project-link author">
|
||||
<span class="material-icons">person</span>
|
||||
<div class="link-content">
|
||||
<span class="link-title">作者主页</span>
|
||||
<span class="link-url">github.com/ikun0014</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href="https://github.com/ikunshare/Onekey/issues" target="_blank" class="project-link issues">
|
||||
<span class="material-icons">bug_report</span>
|
||||
<div class="link-content">
|
||||
<span class="link-title">问题反馈</span>
|
||||
<span class="link-url">报告Bug或建议</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 技术信息 -->
|
||||
<div class="tech-info">
|
||||
<div class="tech-header">
|
||||
<span class="material-icons">code</span>
|
||||
<h4>技术信息</h4>
|
||||
</div>
|
||||
<div class="tech-content">
|
||||
<div class="tech-item">
|
||||
<strong>🐍 后端技术</strong>
|
||||
<span>Python 3.8+ • FastAPI • AsyncIO • HTTPX</span>
|
||||
</div>
|
||||
<div class="tech-item">
|
||||
<strong>🌐 前端技术</strong>
|
||||
<span>HTML5 • CSS3 • JavaScript ES6+ • Material Design 3.0</span>
|
||||
</div>
|
||||
<div class="tech-item">
|
||||
<strong>🔧 支持工具</strong>
|
||||
<span>SteamTools • GreenLuma</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 使用须知 -->
|
||||
<div class="usage-notice">
|
||||
<div class="notice-header">
|
||||
<span class="material-icons">info</span>
|
||||
<h4>使用须知</h4>
|
||||
</div>
|
||||
<div class="notice-content">
|
||||
<p>
|
||||
<strong>⚠️ 重要提醒</strong> -
|
||||
本工具完全免费,严禁任何形式的商业化使用或倒卖
|
||||
</p>
|
||||
<p>
|
||||
<strong>🖥️ 系统要求</strong> - 请确保已安装Windows
|
||||
10/11并正确配置Steam客户端
|
||||
</p>
|
||||
<p>
|
||||
<strong>🛠️ 工具准备</strong> -
|
||||
使用前请先安装SteamTools或GreenLuma解锁工具
|
||||
</p>
|
||||
<p>
|
||||
<strong>🔒 免责声明</strong> -
|
||||
本工具仅供学习交流使用,使用者需自行承担相关风险
|
||||
</p>
|
||||
<p>
|
||||
<strong>⭐ 支持项目</strong> -
|
||||
如果此工具对您有帮助,欢迎在GitHub上给项目点Star支持
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 版权信息 -->
|
||||
<div class="copyright">
|
||||
<p>© 2025 Onekey Steam解锁工具 • 作者:qwq-xinkeng && ikun0014</p>
|
||||
<p>
|
||||
项目地址:<a href="https://github.com/ikunshare/Onekey" target="_blank">https://github.com/ikunshare/Onekey</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', path='js/theme.js') }}"></script>
|
||||
</div>
|
||||
|
||||
</html>
|
||||
175
web/templates/index.html
Normal file
175
web/templates/index.html
Normal file
@@ -0,0 +1,175 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Onekey - Home</title>
|
||||
|
||||
<!-- Material Design 3 -->
|
||||
<link
|
||||
href="https://cdn.jsdmirror.com/gh/ikun0014/font@main/style.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- 自定义样式 -->
|
||||
<link rel="stylesheet" href="/static/css/style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- 顶部应用栏 -->
|
||||
<header class="app-bar">
|
||||
<div class="app-bar-content">
|
||||
<span class="material-icons app-icon">games</span>
|
||||
<h1 class="app-title">Onekey</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="theme-toggle"
|
||||
id="themeToggle"
|
||||
title="切换主题"
|
||||
>
|
||||
<span class="material-icons">light_mode</span>
|
||||
</button>
|
||||
<a href="/settings" class="btn btn-text settings-link">
|
||||
<span class="material-icons">settings</span>
|
||||
<span class="settings-text">设置</span>
|
||||
</a>
|
||||
<a href="/about" class="btn btn-text about-link">
|
||||
<span class="material-icons">info</span>
|
||||
<span class="about-text">关于本项目</span>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="main-content">
|
||||
<!-- 配置状态卡片 -->
|
||||
<div class="card config-card">
|
||||
<div class="card-header">
|
||||
<span class="material-icons">settings</span>
|
||||
<h2>配置状态</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="config-status" id="configStatus">
|
||||
<div class="loading">正在检查配置...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 游戏解锁卡片 -->
|
||||
<div class="card unlock-card">
|
||||
<div class="card-header">
|
||||
<span class="material-icons">lock_open</span>
|
||||
<h2>游戏解锁</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<form id="unlockForm" class="unlock-form">
|
||||
<div class="input-group">
|
||||
<label for="appId" class="input-label">Steam App ID</label>
|
||||
<input
|
||||
type="text"
|
||||
id="appId"
|
||||
name="appId"
|
||||
class="text-field"
|
||||
placeholder="请输入游戏的App ID"
|
||||
inputmode="numeric"
|
||||
autocomplete="off"
|
||||
autofocus
|
||||
required
|
||||
/>
|
||||
<div class="input-feedback" id="appIdFeedback"></div>
|
||||
<div class="input-helper">例如: 730 (CS2), 570 (Dota 2)</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label class="input-label">解锁工具</label>
|
||||
<div class="radio-group">
|
||||
<label class="radio-item">
|
||||
<input
|
||||
type="radio"
|
||||
name="toolType"
|
||||
value="steamtools"
|
||||
checked
|
||||
/>
|
||||
<span class="radio-button"></span>
|
||||
<span class="radio-label"
|
||||
>SteamTools(更新积极, 推荐使用)</span
|
||||
>
|
||||
</label>
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="toolType" value="greenluma" />
|
||||
<span class="radio-button"></span>
|
||||
<span class="radio-label">GreenLuma(一年一更, 无GUI)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group" id="+DLCGroup">
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" id="+DLC" name="+DLC" />
|
||||
<span class="checkbox-button"></span>
|
||||
<span class="checkbox-label">检索并入库所有DLC</span>
|
||||
</label>
|
||||
<div class="input-helper">
|
||||
需要注意: 有些DLC的Depot与游戏本体在一起, 不会分离
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button type="submit" class="btn btn-primary" id="unlockBtn">
|
||||
<span class="material-icons">play_arrow</span>
|
||||
开始解锁
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" id="resetBtn">
|
||||
<span class="material-icons">refresh</span>
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度日志卡片 -->
|
||||
<div class="card progress-card">
|
||||
<div class="card-header">
|
||||
<span class="material-icons">timeline</span>
|
||||
<h2>执行日志</h2>
|
||||
<div class="card-actions">
|
||||
<button class="btn btn-text" id="clearLogBtn">
|
||||
<span class="material-icons">clear_all</span>
|
||||
清空
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="progress-container" id="progressContainer">
|
||||
<div class="progress-placeholder">
|
||||
<span class="material-icons">info</span>
|
||||
<p>等待任务开始...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 提示框 -->
|
||||
<div id="snackbar" class="snackbar">
|
||||
<div class="snackbar-content">
|
||||
<span id="snackbarMessage"></span>
|
||||
<button id="snackbarClose" class="snackbar-action">
|
||||
<span class="material-icons">close</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 脚本 -->
|
||||
<script src="{{ url_for('static', path='js/app.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='js/theme.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='js/project-info.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
473
web/templates/oobe.html
Normal file
473
web/templates/oobe.html
Normal file
@@ -0,0 +1,473 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Onekey - 首次使用向导</title>
|
||||
|
||||
<!-- Material Design 3 -->
|
||||
<link
|
||||
href="https://cdn.jsdmirror.com/gh/ikun0014/font@main/style.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- 自定义样式 -->
|
||||
<link rel="stylesheet" href="/static/css/style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="oobe-container">
|
||||
<!-- 顶部应用栏 -->
|
||||
<div class="oobe-card">
|
||||
<div class="oobe-header">
|
||||
<button
|
||||
type="button"
|
||||
class="theme-toggle"
|
||||
id="themeToggle"
|
||||
title="切换主题"
|
||||
>
|
||||
<span class="material-icons">light_mode</span>
|
||||
</button>
|
||||
<div class="oobe-logo">
|
||||
<span class="material-icons" style="font-size: inherit"
|
||||
>extension</span
|
||||
>
|
||||
</div>
|
||||
<h1 class="oobe-title">欢迎使用 Onekey</h1>
|
||||
<p class="oobe-subtitle">一键解锁,畅享游戏体验</p>
|
||||
</div>
|
||||
|
||||
<div class="oobe-content">
|
||||
<div class="step-indicator">
|
||||
<div class="step-dot active" data-step="0"></div>
|
||||
<div class="step-dot" data-step="1"></div>
|
||||
<div class="step-dot" data-step="2"></div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤 1: 欢迎 -->
|
||||
<div class="oobe-step active" data-step="0">
|
||||
<div class="welcome-text">
|
||||
<h3>🎮 欢迎来到 Onekey 世界</h3>
|
||||
<p>
|
||||
Onekey 是一个强大的 Steam
|
||||
游戏解锁工具,帮助您轻松管理和解锁游戏。
|
||||
</p>
|
||||
<p>在开始使用之前,我们需要验证您的授权卡密。</p>
|
||||
<p><strong>特点:</strong></p>
|
||||
<p>• 支持 SteamTools 和 GreenLuma 两种解锁方式</p>
|
||||
<p>• 直观的 Web 界面,操作简单</p>
|
||||
<p>• 实时日志显示,过程透明</p>
|
||||
<p>• 前端代码完全开源, 绝对不盗号/挖矿</p>
|
||||
<a href="http://103.217.184.26:911" target="_blank">• 点我购买卡密</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤 2: 卡密验证 -->
|
||||
<div class="oobe-step" data-step="1">
|
||||
<div class="welcome-text">
|
||||
<h3>🔑 激活您的卡密</h3>
|
||||
<p>请输入您的授权卡密以激活 Onekey 工具。</p>
|
||||
</div>
|
||||
|
||||
<div class="key-input-section">
|
||||
<div class="input-group">
|
||||
<label for="activationKey" class="input-label">授权卡密</label>
|
||||
<input
|
||||
type="text"
|
||||
id="activationKey"
|
||||
class="text-field"
|
||||
placeholder="请输入您的卡密"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="input-helper">
|
||||
卡密格式:[PREFIX]_XXXXXXXX-XXXXXXXXXXXXXXXX
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="key-status" id="keyStatus">
|
||||
<div class="status-header">
|
||||
<span class="material-icons" id="statusIcon">info</span>
|
||||
<span id="statusMessage">验证中...</span>
|
||||
</div>
|
||||
<div class="key-info" id="keyInfo"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤 4: 完成 -->
|
||||
<div class="oobe-step" data-step="3">
|
||||
<div class="welcome-text">
|
||||
<h3>🎉 设置完成</h3>
|
||||
<p>恭喜!您已成功激活 Onekey 工具。</p>
|
||||
<p>现在您可以开始使用所有功能了。</p>
|
||||
<div
|
||||
class="key-info"
|
||||
id="finalKeyInfo"
|
||||
style="margin-top: 24px"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="oobe-actions">
|
||||
<button
|
||||
type="button"
|
||||
id="prevBtn"
|
||||
class="btn btn-text"
|
||||
style="display: none"
|
||||
>
|
||||
<span class="material-icons">arrow_back</span>
|
||||
上一步
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
id="nextBtn"
|
||||
class="btn btn-primary btn-large"
|
||||
>
|
||||
<span class="material-icons">arrow_forward</span>
|
||||
下一步
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
id="verifyBtn"
|
||||
class="btn btn-primary btn-large"
|
||||
style="display: none"
|
||||
>
|
||||
<span class="material-icons">verified</span>
|
||||
验证卡密
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
id="finishBtn"
|
||||
class="btn btn-primary btn-large"
|
||||
style="display: none"
|
||||
>
|
||||
<span class="material-icons">check</span>
|
||||
开始使用
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="loading-overlay" id="loadingOverlay">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示框 -->
|
||||
<div id="snackbar" class="snackbar">
|
||||
<div class="snackbar-content">
|
||||
<span id="snackbarMessage"></span>
|
||||
<button id="snackbarClose" class="snackbar-action">
|
||||
<span class="material-icons">close</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
class OOBEManager {
|
||||
constructor() {
|
||||
this.currentStep = 0;
|
||||
this.totalSteps = 3;
|
||||
this.keyData = null;
|
||||
|
||||
this.initializeEventListeners();
|
||||
this.updateStepDisplay();
|
||||
}
|
||||
|
||||
initializeEventListeners() {
|
||||
document.getElementById("nextBtn").addEventListener("click", () => {
|
||||
this.nextStep();
|
||||
});
|
||||
|
||||
document.getElementById("prevBtn").addEventListener("click", () => {
|
||||
this.prevStep();
|
||||
});
|
||||
|
||||
document.getElementById("verifyBtn").addEventListener("click", () => {
|
||||
this.verifyKey();
|
||||
});
|
||||
|
||||
document.getElementById("finishBtn").addEventListener("click", () => {
|
||||
this.finishSetup();
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("activationKey")
|
||||
.addEventListener("input", () => {
|
||||
this.resetKeyStatus();
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("activationKey")
|
||||
.addEventListener("keypress", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
this.verifyKey();
|
||||
}
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("snackbarClose")
|
||||
.addEventListener("click", () => {
|
||||
this.hideSnackbar();
|
||||
});
|
||||
}
|
||||
|
||||
nextStep() {
|
||||
if (this.currentStep < this.totalSteps - 1) {
|
||||
this.currentStep++;
|
||||
this.updateStepDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
prevStep() {
|
||||
if (this.currentStep > 0) {
|
||||
this.currentStep--;
|
||||
this.updateStepDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
updateStepDisplay() {
|
||||
document.querySelectorAll(".step-dot").forEach((dot, index) => {
|
||||
dot.classList.remove("active", "completed");
|
||||
if (index < this.currentStep) {
|
||||
dot.classList.add("completed");
|
||||
} else if (index === this.currentStep) {
|
||||
dot.classList.add("active");
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll(".oobe-step").forEach((step, index) => {
|
||||
step.classList.toggle("active", index === this.currentStep);
|
||||
});
|
||||
|
||||
this.updateButtons();
|
||||
}
|
||||
|
||||
updateButtons() {
|
||||
const prevBtn = document.getElementById("prevBtn");
|
||||
const nextBtn = document.getElementById("nextBtn");
|
||||
const verifyBtn = document.getElementById("verifyBtn");
|
||||
const finishBtn = document.getElementById("finishBtn");
|
||||
|
||||
[prevBtn, nextBtn, verifyBtn, finishBtn].forEach((btn) => {
|
||||
btn.style.display = "none";
|
||||
});
|
||||
|
||||
if (this.currentStep > 0) {
|
||||
prevBtn.style.display = "flex";
|
||||
}
|
||||
|
||||
switch (this.currentStep) {
|
||||
case 0:
|
||||
nextBtn.style.display = "flex";
|
||||
break;
|
||||
case 1:
|
||||
verifyBtn.style.display = "flex";
|
||||
break;
|
||||
case 2:
|
||||
finishBtn.style.display = "flex";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
resetKeyStatus() {
|
||||
const keyStatus = document.getElementById("keyStatus");
|
||||
keyStatus.classList.remove("show", "success", "error");
|
||||
}
|
||||
|
||||
async verifyKey() {
|
||||
const keyInput = document.getElementById("activationKey");
|
||||
const key = keyInput.value.trim();
|
||||
|
||||
if (!key) {
|
||||
this.showSnackbar("请输入卡密", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!key.match(/^[A-Z0-9_-]+$/)) {
|
||||
this.showKeyStatus("error", "卡密格式不正确", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
this.showLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/getKeyInfo", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ key: key }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.key && data.info) {
|
||||
this.keyData = data.info;
|
||||
this.showKeyStatus("success", "卡密验证成功!", "check_circle");
|
||||
this.displayKeyInfo(data.info);
|
||||
|
||||
setTimeout(() => {
|
||||
this.nextStep();
|
||||
this.showFinalKeyInfo(data.info);
|
||||
}, 2000);
|
||||
} else {
|
||||
this.showKeyStatus(
|
||||
"error",
|
||||
data.message || "卡密不存在或已过期",
|
||||
"error",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.showKeyStatus("error", "验证失败,请检查网络连接", "error");
|
||||
console.error("Key verification error:", error);
|
||||
} finally {
|
||||
this.showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
showKeyStatus(type, message, icon) {
|
||||
const keyStatus = document.getElementById("keyStatus");
|
||||
const statusIcon = document.getElementById("statusIcon");
|
||||
const statusMessage = document.getElementById("statusMessage");
|
||||
|
||||
statusIcon.textContent = icon;
|
||||
statusMessage.textContent = message;
|
||||
|
||||
keyStatus.className = `key-status show ${type}`;
|
||||
}
|
||||
|
||||
displayKeyInfo(keyInfo) {
|
||||
const keyInfoContainer = document.getElementById("keyInfo");
|
||||
const expiresAt = new Date(keyInfo.expiresAt);
|
||||
const isExpired = expiresAt < new Date();
|
||||
|
||||
const typeNames = {
|
||||
day: "日卡",
|
||||
week: "周卡",
|
||||
month: "月卡",
|
||||
year: "年卡",
|
||||
permanent: "永久卡",
|
||||
};
|
||||
|
||||
keyInfoContainer.innerHTML = `
|
||||
<div class="key-info-item">
|
||||
<span class="material-icons">label</span>
|
||||
<span>类型:${typeNames[keyInfo.type] || keyInfo.type}</span>
|
||||
</div>
|
||||
<div class="key-info-item">
|
||||
<span class="material-icons">schedule</span>
|
||||
<span>到期:${expiresAt.toLocaleDateString()}</span>
|
||||
</div>
|
||||
<div class="key-info-item">
|
||||
<span class="material-icons">analytics</span>
|
||||
<span>使用次数:${keyInfo.usageCount}</span>
|
||||
</div>
|
||||
<div class="key-info-item">
|
||||
<span class="material-icons">${keyInfo.isActive && !isExpired ? "check_circle" : "cancel"}</span>
|
||||
<span>状态:${keyInfo.isActive && !isExpired ? "有效" : "无效"}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
showFinalKeyInfo(keyInfo) {
|
||||
const finalKeyInfo = document.getElementById("finalKeyInfo");
|
||||
const expiresAt = new Date(keyInfo.expiresAt);
|
||||
|
||||
const typeNames = {
|
||||
day: "日卡",
|
||||
week: "周卡",
|
||||
month: "月卡",
|
||||
year: "年卡",
|
||||
permanent: "永久卡",
|
||||
};
|
||||
|
||||
finalKeyInfo.innerHTML = `
|
||||
<div class="key-info-item">
|
||||
<span class="material-icons">verified_user</span>
|
||||
<span><strong>卡密类型:</strong>${typeNames[keyInfo.type] || keyInfo.type}</span>
|
||||
</div>
|
||||
<div class="key-info-item">
|
||||
<span class="material-icons">event</span>
|
||||
<span><strong>有效期至:</strong>${expiresAt.toLocaleDateString()} ${expiresAt.toLocaleTimeString()}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async finishSetup() {
|
||||
if (!this.keyData) {
|
||||
this.showSnackbar("卡密数据丢失,请重新验证", "error");
|
||||
this.currentStep = 1;
|
||||
this.updateStepDisplay();
|
||||
return;
|
||||
}
|
||||
|
||||
this.showLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/config/update", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
key: document.getElementById("activationKey").value.trim(),
|
||||
steam_path: "",
|
||||
debug_mode: false,
|
||||
logging_files: true,
|
||||
show_console: false,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showSnackbar("配置保存成功,正在跳转...", "success");
|
||||
setTimeout(() => {
|
||||
window.location.href = "/";
|
||||
}, 1500);
|
||||
} else {
|
||||
throw new Error(data.message || "保存配置失败");
|
||||
}
|
||||
} catch (error) {
|
||||
this.showSnackbar("保存配置失败:" + error.message, "error");
|
||||
console.error("Save config error:", error);
|
||||
} finally {
|
||||
this.showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
showLoading(show) {
|
||||
const overlay = document.getElementById("loadingOverlay");
|
||||
overlay.classList.toggle("show", show);
|
||||
}
|
||||
|
||||
showSnackbar(message, type = "info") {
|
||||
const snackbar = document.getElementById("snackbar");
|
||||
const snackbarMessage = document.getElementById("snackbarMessage");
|
||||
|
||||
snackbarMessage.textContent = message;
|
||||
snackbar.className = `snackbar ${type} show`;
|
||||
|
||||
setTimeout(() => {
|
||||
this.hideSnackbar();
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
hideSnackbar() {
|
||||
const snackbar = document.getElementById("snackbar");
|
||||
snackbar.classList.remove("show");
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
new OOBEManager();
|
||||
});
|
||||
</script>
|
||||
<script src="{{ url_for('static', path='js/theme.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
262
web/templates/settings.html
Normal file
262
web/templates/settings.html
Normal file
@@ -0,0 +1,262 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Onekey - Settings</title>
|
||||
|
||||
<!-- Material Design 3 -->
|
||||
<link
|
||||
href="https://cdn.jsdmirror.com/gh/ikun0014/font@main/style.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- 自定义样式 -->
|
||||
<link rel="stylesheet" href="/static/css/style.css" />
|
||||
<link rel="stylesheet" href="/static/css/settings.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- 顶部应用栏 -->
|
||||
<header class="app-bar">
|
||||
<div class="app-bar-content">
|
||||
<button class="btn btn-text" onclick="goBack()">
|
||||
<span class="material-icons">arrow_back</span>
|
||||
</button>
|
||||
<span class="material-icons app-icon">settings</span>
|
||||
<h1 class="app-title">设置</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="main-content settings-main">
|
||||
<!-- 卡密管理卡片 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="material-icons">verified</span>
|
||||
<h2>卡密管理</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="settings-section">
|
||||
<div id="keyInfoSection">
|
||||
<div class="loading">正在加载卡密信息...</div>
|
||||
</div>
|
||||
|
||||
<div class="key-change-section">
|
||||
<h4
|
||||
style="
|
||||
margin: 0 0 16px 0;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
"
|
||||
>
|
||||
<span
|
||||
class="material-icons"
|
||||
style="vertical-align: middle; margin-right: 8px"
|
||||
>swap_horiz</span
|
||||
>
|
||||
更换卡密
|
||||
</h4>
|
||||
<div class="key-input-group">
|
||||
<div class="input-group" style="flex: 1; margin: 0">
|
||||
<label for="newKey" class="input-label">新卡密</label>
|
||||
<input
|
||||
type="text"
|
||||
id="newKey"
|
||||
class="text-field"
|
||||
placeholder="请输入新的卡密"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="input-helper">
|
||||
格式:[PREFIX]_XXXXXXXX-XXXXXXXXXXXXXXXX
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
id="verifyNewKey"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
<span class="material-icons">check</span>
|
||||
验证
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
id="changeKey"
|
||||
class="btn btn-primary"
|
||||
style="display: none"
|
||||
>
|
||||
<span class="material-icons">save</span>
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Steam 配置卡片 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="material-icons">games</span>
|
||||
<h2>Steam 配置</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="settings-section">
|
||||
<div class="input-group">
|
||||
<label for="steamPath" class="input-label"
|
||||
>Steam 安装路径</label
|
||||
>
|
||||
<div class="path-input-group">
|
||||
<input
|
||||
type="text"
|
||||
id="steamPath"
|
||||
class="text-field"
|
||||
placeholder="留空自动检测,或手动输入Steam安装路径"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
id="detectSteamPath"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
<span class="material-icons">search</span>
|
||||
自动检测
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-helper">
|
||||
程序会尝试自动检测Steam安装路径,如果检测失败请手动输入。
|
||||
通常位于:C:\Program Files (x86)\Steam
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-indicator" id="steamPathStatus">
|
||||
<span class="material-icons status-icon">info</span>
|
||||
<span class="status-text">等待检测...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 应用程序配置卡片 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="material-icons">tune</span>
|
||||
<h2>应用程序配置</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="settings-section">
|
||||
<div class="setting-item">
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" id="debugMode" />
|
||||
<span class="checkbox-button"></span>
|
||||
<div class="checkbox-content">
|
||||
<span class="checkbox-label">调试模式</span>
|
||||
<span class="checkbox-description"
|
||||
>启用详细的调试日志输出</span
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" id="loggingFiles" />
|
||||
<span class="checkbox-button"></span>
|
||||
<div class="checkbox-content">
|
||||
<span class="checkbox-label">保存日志文件</span>
|
||||
<span class="checkbox-description"
|
||||
>将日志保存到文件中,便于问题排查</span
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" id="showConsole" />
|
||||
<span class="checkbox-button"></span>
|
||||
<div class="checkbox-content">
|
||||
<span class="checkbox-label">显示终端窗口</span>
|
||||
<span class="checkbox-description"
|
||||
>启动时显示终端窗口和日志输出</span
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮卡片 -->
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="action-buttons">
|
||||
<button type="button" id="saveConfig" class="btn btn-primary">
|
||||
<span class="material-icons">save</span>
|
||||
保存配置
|
||||
</button>
|
||||
<button type="button" id="resetConfig" class="btn btn-secondary">
|
||||
<span class="material-icons">restore</span>
|
||||
重置为默认值
|
||||
</button>
|
||||
<button type="button" id="testConfig" class="btn btn-secondary">
|
||||
<span class="material-icons">check_circle</span>
|
||||
测试配置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 配置信息显示卡片 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="material-icons">info</span>
|
||||
<h2>配置状态</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="config-status-grid" id="configStatusGrid">
|
||||
<div class="loading">正在加载配置状态...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 确认对话框 -->
|
||||
<div id="confirmDialog" class="dialog-overlay">
|
||||
<div class="dialog">
|
||||
<div class="dialog-header">
|
||||
<h3 id="dialogTitle">确认操作</h3>
|
||||
</div>
|
||||
<div class="dialog-content">
|
||||
<p id="dialogMessage">确定要执行此操作吗?</p>
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<button type="button" id="dialogCancel" class="btn btn-text">
|
||||
取消
|
||||
</button>
|
||||
<button type="button" id="dialogConfirm" class="btn btn-primary">
|
||||
确认
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示框 -->
|
||||
<div id="snackbar" class="snackbar">
|
||||
<div class="snackbar-content">
|
||||
<span id="snackbarMessage"></span>
|
||||
<button id="snackbarClose" class="snackbar-action">
|
||||
<span class="material-icons">close</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 脚本 -->
|
||||
<script src="{{ url_for('static', path='js/settings.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='js/theme.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='js/project-info.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user