mirror of
https://github.com/ikunshare/Onekey.git
synced 2026-01-15 01:23:02 +08:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b91bf42981 | ||
|
|
dacaa1c62e | ||
|
|
22619df037 | ||
|
|
840a99a1e9 | ||
|
|
ba3c60c775 | ||
|
|
80628ad17a | ||
|
|
9880b90d19 | ||
|
|
3f4d327f6e | ||
|
|
361150b3bc | ||
|
|
4f213f9514 | ||
|
|
56eab758cd | ||
|
|
9c4890db37 | ||
|
|
ddd1cb7b6c | ||
|
|
839e922d79 | ||
|
|
2bd2f23838 | ||
|
|
9e68621e76 | ||
|
|
2a6a9421ca | ||
|
|
1a6cf47882 | ||
|
|
09f94c5f11 | ||
|
|
5307dc0766 | ||
|
|
15b3caddcf | ||
|
|
eefff08159 | ||
|
|
956efef428 | ||
|
|
56901f1b92 | ||
|
|
490dc096c1 | ||
|
|
911f6f39e3 | ||
|
|
343c86808d | ||
|
|
6a0e6e926d | ||
|
|
611edaec04 | ||
|
|
92559710f7 | ||
|
|
03a661f288 | ||
|
|
bb160f9f6d | ||
|
|
e5d4ad55ee |
15
.github/FUNDING.yml
vendored
15
.github/FUNDING.yml
vendored
@@ -1,15 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||
thanks_dev: # Replace with a single thanks.dev username
|
||||
custom: ['https://afdian.com/a/ikun0014']
|
||||
25
.github/ISSUE_TEMPLATE/bug反馈.md
vendored
25
.github/ISSUE_TEMPLATE/bug反馈.md
vendored
@@ -1,25 +0,0 @@
|
||||
---
|
||||
name: Bug反馈
|
||||
about: 反馈一个虫子(确信)
|
||||
title: "[BUG]: "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**触发Bug后出现了什么症状**
|
||||
1.
|
||||
|
||||
**如何复现该Bug**
|
||||
步骤:
|
||||
1.
|
||||
|
||||
**日志**
|
||||
复制在这里
|
||||
|
||||
**截图**
|
||||
对程序的完整截图
|
||||
|
||||
**系统:**
|
||||
- OS: Windows
|
||||
- Version: 10
|
||||
41
.github/workflows/dev.yml
vendored
Normal file
41
.github/workflows/dev.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: DEV Test Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: 拉取仓库
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 获取版本
|
||||
shell: powershell
|
||||
run: |
|
||||
$version = (Get-Content package.json | ConvertFrom-Json).version
|
||||
echo "PACKAGE_VERSION=$version" >> $env:GITHUB_ENV
|
||||
|
||||
- name: 安装Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: 安装依赖
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install imageio
|
||||
pip install -r requirements.txt
|
||||
pip install nuitka
|
||||
|
||||
- name: 编译
|
||||
run: |
|
||||
python -m nuitka --standalone --onefile --assume-yes-for-downloads --show-memory --show-progress --onefile-tempdir-spec="%TEMP%\\onekey_%PID%_%TIME%" --windows-icon-from-ico="icon.jpg" --company-name="ikunshare" --product-name="Onekey" --file-version="${{ env.PACKAGE_VERSION }}" --product-version="${{ env.PACKAGE_VERSION }}" --file-description="Onekey Depot Manifest Downloader." --copyright="Copyright © 2025 ikunshare All Rights Reserved." --include-data-dir="web=web" --output-dir="build" --output-filename="Onekey_v${{ env.PACKAGE_VERSION }}.exe" --include-data-file="./icon.jpg=./icon.jpg" main.py
|
||||
|
||||
- name: 上传包
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Onekey_v${{ env.PACKAGE_VERSION }}.exe
|
||||
path: build/Onekey_v${{ env.PACKAGE_VERSION }}.exe
|
||||
132
.github/workflows/release.yml
vendored
132
.github/workflows/release.yml
vendored
@@ -1,11 +1,6 @@
|
||||
name: Build and Release
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -13,126 +8,67 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
- name: 拉取仓库
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get package version
|
||||
id: get_version
|
||||
- name: 获取版本
|
||||
shell: powershell
|
||||
run: |
|
||||
$packageJson = Get-Content package.json | ConvertFrom-Json
|
||||
$version = $packageJson.version
|
||||
echo "VERSION=$version" >> $env:GITHUB_OUTPUT
|
||||
echo "Package version: $version"
|
||||
$version = (Get-Content package.json | ConvertFrom-Json).version
|
||||
echo "PACKAGE_VERSION=$version" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Set up Python
|
||||
- name: 安装Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
architecture: "x64"
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install dependencies
|
||||
- name: 安装依赖
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install imageio
|
||||
pip install -r requirements.txt
|
||||
pip install Pillow
|
||||
pip install pyinstaller
|
||||
pip install nuitka
|
||||
|
||||
- name: Create icon if not exists
|
||||
shell: powershell
|
||||
- name: 编译
|
||||
run: |
|
||||
if (-not (Test-Path "icon.ico")) {
|
||||
Write-Host "Icon file not found, using default Windows icon"
|
||||
$iconPath = "C:\Windows\System32\shell32.dll,0"
|
||||
}
|
||||
python -m nuitka --standalone --onefile --assume-yes-for-downloads --show-memory --show-progress --onefile-tempdir-spec="%TEMP%\\onekey_%PID%_%TIME%" --windows-icon-from-ico="icon.jpg" --company-name="ikunshare" --product-name="Onekey" --file-version="${{ env.PACKAGE_VERSION }}" --product-version="${{ env.PACKAGE_VERSION }}" --file-description="Onekey Depot Manifest Downloader." --copyright="Copyright © 2025 ikunshare All Rights Reserved." --include-data-dir="web=web" --output-dir="build" --output-filename="Onekey_v${{ env.PACKAGE_VERSION }}.exe" --include-data-file="./icon.jpg=./icon.jpg" main.py
|
||||
|
||||
- name: Build executable
|
||||
run: |
|
||||
pyinstaller --onefile `
|
||||
--icon "./icon.jpg" `
|
||||
--name "Onekey_v${{ steps.get_version.outputs.VERSION }}" `
|
||||
--add-data "src;src" `
|
||||
--hidden-import=winreg `
|
||||
--hidden-import=httpx `
|
||||
--hidden-import=vdf `
|
||||
--hidden-import=colorama `
|
||||
--hidden-import=logzero `
|
||||
--hidden-import=ujson `
|
||||
--console `
|
||||
main.py
|
||||
|
||||
- name: Compress with UPX
|
||||
uses: crazy-max/ghaction-upx@v3.2.0
|
||||
- name: 创建标签
|
||||
uses: pkgdeps/git-tag-action@v3
|
||||
with:
|
||||
version: latest
|
||||
files: |
|
||||
./dist/*.exe
|
||||
args: "-fq"
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
github_repo: ${{ github.repository }}
|
||||
version: ${{ env.PACKAGE_VERSION }}
|
||||
git_commit_sha: ${{ github.sha }}
|
||||
git_tag_prefix: "v"
|
||||
|
||||
- name: 上传包
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Onekey_v${{ env.PACKAGE_VERSION }}.exe
|
||||
path: build/Onekey_v${{ env.PACKAGE_VERSION }}.exe
|
||||
|
||||
- name: Create Tag
|
||||
run: |
|
||||
git config user.name github-actions
|
||||
git config user.email github-actions@github.com
|
||||
$version = "${{ steps.get_version.outputs.VERSION }}"
|
||||
|
||||
# Check if tag exists
|
||||
$tagExists = git tag -l "v$version"
|
||||
if (-not $tagExists) {
|
||||
git tag -a "v$version" -m "Release version $version"
|
||||
git push origin "v$version"
|
||||
}
|
||||
|
||||
- name: Create Release
|
||||
- name: 发布
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: dist/Onekey_v${{ steps.get_version.outputs.VERSION }}.exe
|
||||
draft: false
|
||||
tag_name: v${{ env.PACKAGE_VERSION }}
|
||||
files: build/Onekey_v${{ env.PACKAGE_VERSION }}.exe
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
draft: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload to Gitee Release
|
||||
if: success() && (startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main')
|
||||
continue-on-error: true
|
||||
- name: 发布到Gitee
|
||||
uses: nicennnnnnnlee/action-gitee-release@v1.0.5
|
||||
with:
|
||||
gitee_owner: ikun0014
|
||||
gitee_repo: Onekey
|
||||
gitee_token: ${{ secrets.GITEE_TOKEN }}
|
||||
gitee_tag_name: v${{ steps.get_version.outputs.VERSION }}
|
||||
gitee_release_name: v${{ steps.get_version.outputs.VERSION }}
|
||||
gitee_release_body: "Onekey version ${{ steps.get_version.outputs.VERSION }} release"
|
||||
gitee_tag_name: v${{ env.PACKAGE_VERSION }}
|
||||
gitee_release_name: v${{ env.PACKAGE_VERSION }}
|
||||
gitee_release_body: I don't know
|
||||
gitee_target_commitish: main
|
||||
gitee_upload_retry_times: 3
|
||||
gitee_file_name: Onekey_v${{ steps.get_version.outputs.VERSION }}.exe
|
||||
gitee_file_path: dist/Onekey_v${{ steps.get_version.outputs.VERSION }}.exe
|
||||
|
||||
- name: Upload to Telegram Channel
|
||||
if: success() && (startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main')
|
||||
continue-on-error: true
|
||||
shell: powershell
|
||||
run: |
|
||||
$version = "${{ steps.get_version.outputs.VERSION }}"
|
||||
$filePath = "dist/Onekey_v${version}.exe"
|
||||
|
||||
if (Test-Path $filePath) {
|
||||
curl.exe -F "chat_id=${{ secrets.TELEGRAM_TO }}" `
|
||||
-F "thread_id=${{ secrets.TELEGRAM_THREAD }}" `
|
||||
-F "document=@$filePath" `
|
||||
-F "caption=🎮 **Onekey New Update** v${version}`n`n✨ Steam Game Unlocker`n💻 Windows 10/11`n`n[GitHub](https://github.com/ikunshare/Onekey)" `
|
||||
-F "parse_mode=Markdown" `
|
||||
"https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendDocument"
|
||||
} else {
|
||||
Write-Error "File not found: $filePath"
|
||||
}
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Onekey_v${{ steps.get_version.outputs.VERSION }}
|
||||
path: dist/Onekey_v${{ steps.get_version.outputs.VERSION }}.exe
|
||||
retention-days: 30
|
||||
gitee_file_name: Onekey_v${{ env.PACKAGE_VERSION }}.exe
|
||||
gitee_file_path: build/Onekey_v${{ env.PACKAGE_VERSION }}.exe
|
||||
|
||||
25
.github/workflows/sync.yml
vendored
25
.github/workflows/sync.yml
vendored
@@ -1,25 +0,0 @@
|
||||
name: Sync to Gitee
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Sync to Gitee
|
||||
uses: Yikun/hub-mirror-action@master
|
||||
with:
|
||||
src: github/ikunshare
|
||||
dst: gitee/ikun0014
|
||||
dst_key: ${{ secrets.GITEE_PRIVATE_KEY }}
|
||||
dst_token: ${{ secrets.GITEE_TOKEN }}
|
||||
src_account_type: org
|
||||
dst_account_type: user
|
||||
white_list: "Onekey"
|
||||
force_update: true
|
||||
debug: true
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -144,8 +144,6 @@ Thumbs.db
|
||||
config.json
|
||||
logs/
|
||||
*.exe
|
||||
icon.ico
|
||||
icon.jpg
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
@@ -163,4 +161,11 @@ coverage.xml
|
||||
# Build artifacts
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
*.egg-info/
|
||||
|
||||
# Nuitka
|
||||
*.build
|
||||
*.dist
|
||||
*.onefile-build
|
||||
|
||||
.node_modules/
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"python.analysis.autoImportCompletions": true,
|
||||
"python.analysis.typeCheckingMode": "off"
|
||||
}
|
||||
47
README.md
47
README.md
@@ -11,28 +11,31 @@
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
## Onekey
|
||||
|
||||
Onekey Steam Depot Manifest Downloader
|
||||
对本软件有意见的
|
||||
欢迎拨打中华人民共和国公安部门报警电话:110进行报警
|
||||
对本软件有意见的
|
||||
欢迎拨打中华人民共和国公安部门报警电话:110 进行报警
|
||||
|
||||
## 先让我挂些人
|
||||
|
||||
- 沧海颐粟,早期倒卖大手子,现在不知道跑哪了,通过一点手段查到在江西
|
||||
- 玩家资源站,贼喊捉贼笑传,随便改改别人软件的名字就是自己的,还去报官了
|
||||
|
||||
## 使用方法
|
||||
去Releases处下载最新的发布,并且安装好SteamTools或者GreenLuma
|
||||
然后打开Onekey输入App ID即可使用
|
||||
|
||||
去 Releases 处下载最新的发布,并且安装好 SteamTools 或者 GreenLuma
|
||||
然后打开 Onekey 输入 App ID 即可使用
|
||||
|
||||
## 开发
|
||||
本程序使用Python编程语言开发
|
||||
要求环境:
|
||||
1.Python 3.10及以上
|
||||
2.Windows 10及以上
|
||||
3.使用Git进行版本管理
|
||||
|
||||
1.克隆项目到本地
|
||||
本程序使用 Python 编程语言开发
|
||||
要求环境:
|
||||
1.Python 3.10 及以上
|
||||
2.Windows 10 及以上
|
||||
3.使用 Git 进行版本管理
|
||||
|
||||
1.克隆项目到本地
|
||||
|
||||
```
|
||||
git clone https://github.com/ikunshare/Onekey
|
||||
@@ -44,24 +47,9 @@ git clone https://github.com/ikunshare/Onekey
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 项目协议
|
||||
本项目基于 GPL-2.0 许可证发行,以下协议是对于 GPL-2.0 原协议的补充,如有冲突,以以下协议为准。
|
||||
|
||||
词语约定: “使用者”指签署本协议的使用者;“版权数据”指包括但不限于图像、音频、名字等在内的他人拥有所属版权的数据。
|
||||
|
||||
本项目的数据来源原理是从Steam官方的CDN服务器中拉取游戏清单数据,经过对数据简单地筛选与合并后进行展示,因此本项目不对数据的准确性负责。
|
||||
使用本项目的过程中可能会产生版权数据,对于这些版权数据,本项目不拥有它们的所有权,为了避免造成侵权,使用者务必在24 小时内清除使用本项目的过程中所产生的版权数据。
|
||||
由于使用本项目产生的包括由于本协议或由于使用或无法使用本项目而引起的任何性质的任何直接、间接、特殊、偶然或结果性损害(包括但不限于因商誉损失、停工、计算机故障或故障引起的损害赔偿,或任何及所有其他商业损害或损失)由使用者负责。
|
||||
本项目完全免费,且开源发布于 GitHub 面向全世界人用作对技术的学习交流,本项目不对项目内的技术可能存在违反当地法律法规的行为作保证,禁止在违反当地法律法规的情况下使用本项目,对于使用者在明知或不知当地法律法规不允许的情况下使用本项目所造成的任何违法违规行为由使用者承担,本项目不承担由此造成的任何直接、间接、特殊、偶然或结果性责任。
|
||||
而且,本项目已禁止使用于商业用途,以及不得进行未经允许的二次修改,否则必须同时发布源代码。
|
||||
若你使用了本项目,将代表你接受以上协议。
|
||||
|
||||
Steam正版平台不易,请尊重版权,支持正版。
|
||||
本项目仅用于对技术可行性的探索及研究,不接受任何商业(包括但不限于广告等)合作。
|
||||
|
||||
## Star 趋势图
|
||||
|
||||
[](https://starchart.cc/ikunshare/Onekey)
|
||||
[](https://starchart.cc/ikunshare/Onekey)
|
||||
|
||||
## 贡献者
|
||||
|
||||
@@ -70,10 +58,13 @@ pip install -r requirements.txt
|
||||
</a>
|
||||
|
||||
## 常见问题解答(FAQ)
|
||||
|
||||
查看 [FAQ](https://ikunshare.top/d/49) 获取常见问题的解答。
|
||||
|
||||
## 社区和支持
|
||||
加入我们的社区,参与讨论和支持:
|
||||
|
||||
加入我们的社区,参与讨论和支持:
|
||||
|
||||
- [GitHub Discussions](https://github.com/ikunshare/Onekey/discussions)
|
||||
- [Telegram](https://t.me/ikunshare_qun)
|
||||
- [QQ](https://qm.qq.com/q/NPRVbglteK)
|
||||
|
||||
172
main.py
172
main.py
@@ -1,14 +1,164 @@
|
||||
import os
|
||||
import asyncio
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
import webbrowser
|
||||
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
|
||||
from src.config import ConfigManager
|
||||
from src.utils.i18n import t
|
||||
|
||||
project_root = Path(__file__)
|
||||
config_manager = ConfigManager()
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
|
||||
def hide_console() -> None:
|
||||
"""隐藏控制台窗口"""
|
||||
try:
|
||||
import ctypes
|
||||
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
user32 = ctypes.windll.user32
|
||||
|
||||
console_window = kernel32.GetConsoleWindow()
|
||||
if console_window:
|
||||
user32.ShowWindow(console_window, 0) # SW_HIDE = 0
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def create_icon() -> Image.Image:
|
||||
"""创建托盘图标"""
|
||||
try:
|
||||
return Image.open("./icon.jpg")
|
||||
except Exception as e:
|
||||
if config_manager.app_config.show_console:
|
||||
print(t("error.load_icon", error=str(e)))
|
||||
# 创建默认图标
|
||||
return Image.new("RGBA", (32, 32), color=(103, 80, 164, 255))
|
||||
|
||||
|
||||
def create_system_tray() -> bool:
|
||||
"""创建系统托盘"""
|
||||
try:
|
||||
import pystray
|
||||
|
||||
def on_quit(icon, item):
|
||||
icon.stop()
|
||||
os._exit(0)
|
||||
|
||||
def on_open_browser(icon, item):
|
||||
try:
|
||||
webbrowser.open(f"http://localhost:{config_manager.app_config.port}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def on_show_console(icon, item):
|
||||
try:
|
||||
import ctypes
|
||||
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
user32 = ctypes.windll.user32
|
||||
console_window = kernel32.GetConsoleWindow()
|
||||
if console_window:
|
||||
user32.ShowWindow(console_window, 1) # SW_NORMAL = 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 创建托盘菜单
|
||||
menu = pystray.Menu(
|
||||
pystray.MenuItem(t("tray.open_browser"), on_open_browser),
|
||||
pystray.MenuItem(t("tray.show_console"), on_show_console),
|
||||
pystray.MenuItem(t("tray.exit"), on_quit),
|
||||
)
|
||||
|
||||
# 创建托盘图标
|
||||
icon = pystray.Icon("Onekey", create_icon(), menu=menu)
|
||||
|
||||
# 在单独的线程中运行托盘
|
||||
def run_tray():
|
||||
icon.run()
|
||||
|
||||
tray_thread = threading.Thread(target=run_tray)
|
||||
tray_thread.daemon = True
|
||||
tray_thread.start()
|
||||
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
def open_browser_delayed(port: int) -> None:
|
||||
"""延迟打开浏览器"""
|
||||
time.sleep(2)
|
||||
try:
|
||||
webbrowser.open(f"http://localhost:{port}")
|
||||
if config_manager.app_config.show_console:
|
||||
print(t("main.browser_opened", port=port))
|
||||
except Exception:
|
||||
if config_manager.app_config.show_console:
|
||||
print(t("main.browser_open_failed", port=port))
|
||||
|
||||
|
||||
def start_web_server() -> None:
|
||||
"""启动Web服务器"""
|
||||
from web.app import app
|
||||
from uvicorn import Config
|
||||
from uvicorn.server import Server
|
||||
|
||||
server = Server(
|
||||
Config(
|
||||
app, host="0.0.0.0", port=config_manager.app_config.port, log_level="error"
|
||||
)
|
||||
)
|
||||
server.run()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""主函数"""
|
||||
try:
|
||||
config = config_manager.app_config
|
||||
show_console = config.show_console
|
||||
|
||||
if show_console:
|
||||
print(t("main.starting"))
|
||||
print("=" * 50)
|
||||
|
||||
# 处理控制台显示
|
||||
if not show_console:
|
||||
hide_console()
|
||||
tray_created = create_system_tray()
|
||||
else:
|
||||
tray_created = create_system_tray()
|
||||
if tray_created:
|
||||
print(t("main.tray_created"))
|
||||
|
||||
# 启动浏览器
|
||||
browser_thread = threading.Thread(
|
||||
target=open_browser_delayed, args=(config.port,)
|
||||
)
|
||||
browser_thread.daemon = True
|
||||
browser_thread.start()
|
||||
|
||||
# 启动Web应用
|
||||
start_web_server()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
if config_manager.app_config.show_console:
|
||||
print(f"\n{t('main.exit')}")
|
||||
except Exception as e:
|
||||
if config_manager.app_config.show_console:
|
||||
print(t("main.start_error", error=str(e)))
|
||||
input(t("main.press_enter"))
|
||||
else:
|
||||
# 在隐藏控制台模式下记录错误
|
||||
error_log = Path("error.log")
|
||||
with open(error_log, "w", encoding="utf-8") as f:
|
||||
f.write(t("main.startup_failed", error=str(e)) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
from src.main import main
|
||||
|
||||
asyncio.run(main())
|
||||
except (asyncio.CancelledError, KeyboardInterrupt):
|
||||
print("\n程序已退出")
|
||||
except Exception as e:
|
||||
print(f"错误:{e}")
|
||||
finally:
|
||||
os.system("pause")
|
||||
main()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "onekey",
|
||||
"version": "1.5.1",
|
||||
"version": "2.1.1",
|
||||
"description": "一个Steam仓库清单下载器",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -14,7 +14,7 @@
|
||||
"Onekey"
|
||||
],
|
||||
"author": "ikun0014",
|
||||
"license": "GPL-2.0",
|
||||
"license": "null",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ikunshare/Onekey/issues"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
colorama==0.4.6
|
||||
httpx==0.28.1
|
||||
logzero==1.7.0
|
||||
vdf==3.4
|
||||
vdf
|
||||
httpx
|
||||
Pillow
|
||||
pystray
|
||||
uvicorn
|
||||
logzero
|
||||
colorama
|
||||
fastapi[all]
|
||||
steam[client]
|
||||
@@ -1,3 +0,0 @@
|
||||
__version__ = "1.5.1"
|
||||
__author__ = "ikun0014"
|
||||
__website__ = "ikunshare.top"
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
import winreg
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
from .constants import CONFIG_FILE
|
||||
from .models import AppConfig
|
||||
from .utils.i18n import t
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"Github_Personal_Token": "",
|
||||
"Custom_Steam_Path": "",
|
||||
"KEY": "",
|
||||
"Port": 5000,
|
||||
"Debug_Mode": False,
|
||||
"Logging_Files": True,
|
||||
"Help": "Github Personal Token可在GitHub设置的Developer settings中生成",
|
||||
"Show_Console": False,
|
||||
"Custom_Steam_Path": "",
|
||||
"Language": "zh",
|
||||
}
|
||||
|
||||
|
||||
@@ -32,41 +35,69 @@ class ConfigManager:
|
||||
try:
|
||||
with open(self.config_path, "w", encoding="utf-8") as f:
|
||||
json.dump(DEFAULT_CONFIG, f, indent=2, ensure_ascii=False)
|
||||
print("配置文件已生成")
|
||||
print(t("config.generated"))
|
||||
os.system("pause")
|
||||
sys.exit(1)
|
||||
except IOError as e:
|
||||
print(f"配置文件创建失败: {str(e)}")
|
||||
print(t("config.create_failed", error=str(e)))
|
||||
os.system("pause")
|
||||
sys.exit(1)
|
||||
|
||||
def _load_config(self) -> None:
|
||||
"""加载配置文件"""
|
||||
if not self.config_path.exists():
|
||||
self._generate_config()
|
||||
print("请填写配置文件后重新运行程序,5秒后退出")
|
||||
time.sleep(5)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
with open(self.config_path, "r", encoding="utf-8") as f:
|
||||
self._config_data = json.load(f)
|
||||
|
||||
self.app_config = AppConfig(
|
||||
github_token=self._config_data.get("Github_Personal_Token", ""),
|
||||
key=self._config_data.get("KEY", ""),
|
||||
port=self._config_data.get("Port", 5000),
|
||||
custom_steam_path=self._config_data.get("Custom_Steam_Path", ""),
|
||||
debug_mode=self._config_data.get("Debug_Mode", False),
|
||||
logging_files=self._config_data.get("Logging_Files", True),
|
||||
show_console=self._config_data.get("Show_Console", True),
|
||||
language=self._config_data.get("Language", "zh"),
|
||||
)
|
||||
|
||||
self.steam_path = self._get_steam_path()
|
||||
|
||||
except json.JSONDecodeError:
|
||||
print("配置文件损坏,正在重新生成...")
|
||||
print(t("config.corrupted"))
|
||||
self._generate_config()
|
||||
sys.exit(1)
|
||||
print(t("config.regenerated"))
|
||||
self.app_config = AppConfig(
|
||||
key=DEFAULT_CONFIG.get("KEY", ""),
|
||||
port=DEFAULT_CONFIG.get("Port", 5000),
|
||||
custom_steam_path=DEFAULT_CONFIG.get("Custom_Steam_Path", ""),
|
||||
debug_mode=DEFAULT_CONFIG.get("Debug_Mode", False),
|
||||
logging_files=DEFAULT_CONFIG.get("Logging_Files", True),
|
||||
show_console=DEFAULT_CONFIG.get("Show_Console", True),
|
||||
language=DEFAULT_CONFIG.get("Language", "zh"),
|
||||
)
|
||||
try:
|
||||
self.steam_path = self._get_steam_path()
|
||||
except Exception:
|
||||
self.steam_path = None
|
||||
except Exception as e:
|
||||
print(f"配置加载失败: {str(e)}")
|
||||
sys.exit(1)
|
||||
print(t("config.load_failed", error=str(e)))
|
||||
print(t("config.use_default"))
|
||||
self.app_config = AppConfig(
|
||||
key=DEFAULT_CONFIG.get("KEY", ""),
|
||||
port=DEFAULT_CONFIG.get("Port", 5000),
|
||||
custom_steam_path=DEFAULT_CONFIG.get("Custom_Steam_Path", ""),
|
||||
debug_mode=DEFAULT_CONFIG.get("Debug_Mode", False),
|
||||
logging_files=DEFAULT_CONFIG.get("Logging_Files", True),
|
||||
show_console=DEFAULT_CONFIG.get("Show_Console", True),
|
||||
language=DEFAULT_CONFIG.get("Language", "zh"),
|
||||
)
|
||||
try:
|
||||
self.steam_path = self._get_steam_path()
|
||||
except Exception:
|
||||
self.steam_path = None
|
||||
|
||||
def _get_steam_path(self) -> Path:
|
||||
def _get_steam_path(self) -> Optional[Path]:
|
||||
"""获取Steam安装路径"""
|
||||
try:
|
||||
if self.app_config.custom_steam_path:
|
||||
@@ -77,12 +108,6 @@ class ConfigManager:
|
||||
) as key:
|
||||
return Path(winreg.QueryValueEx(key, "SteamPath")[0])
|
||||
except Exception as e:
|
||||
print(f"Steam路径获取失败: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
@property
|
||||
def github_headers(self) -> Optional[Dict[str, str]]:
|
||||
"""获取GitHub请求头"""
|
||||
if self.app_config.github_token:
|
||||
return {"Authorization": f"Bearer {self.app_config.github_token}"}
|
||||
return None
|
||||
print(t("config.steam_path_failed", error=str(e)))
|
||||
print(t("config.continue_partial"))
|
||||
return None
|
||||
|
||||
@@ -1,35 +1,50 @@
|
||||
"""常量定义"""
|
||||
|
||||
from pathlib import Path
|
||||
from httpx import Client
|
||||
|
||||
APP_NAME = "Onekey"
|
||||
BANNER = r"""
|
||||
_____ __ _ _____ _ _ _____ __ __
|
||||
/ _ \ | \ | | | ____| | | / / | ____| \ \ / /
|
||||
| | | | | \| | | |__ | |/ / | |__ \ \/ /
|
||||
| | | | | |\ | | __| | |\ \ | __| \ /
|
||||
| |_| | | | \ | | |___ | | \ \ | |___ / /
|
||||
\_____/ |_| \_| |_____| |_| \_\ |_____| /_/
|
||||
"""
|
||||
LOG_DIR: Path = Path("logs")
|
||||
CONFIG_FILE: Path = Path("config.json")
|
||||
|
||||
REPO_LIST = [
|
||||
"SteamAutoCracks/ManifestHub",
|
||||
"ikun0014/ManifestHub",
|
||||
"Auiowu/ManifestAutoUpdate",
|
||||
"tymolu233/ManifestAutoUpdate-fix",
|
||||
]
|
||||
|
||||
CN_CDN_LIST = [
|
||||
"https://cdn.jsdmirror.com/gh/{repo}@{sha}/{path}",
|
||||
"https://raw.gitmirror.com/{repo}/{sha}/{path}",
|
||||
"https://raw.dgithub.xyz/{repo}/{sha}/{path}",
|
||||
"https://gh.akass.cn/{repo}/{sha}/{path}",
|
||||
]
|
||||
def check_ip():
|
||||
try:
|
||||
with Client(timeout=5.0) as client:
|
||||
req = client.get(
|
||||
"https://mips.kugou.com/check/iscn",
|
||||
)
|
||||
req.raise_for_status()
|
||||
body = req.json()
|
||||
print("已获取IP属地")
|
||||
return bool(body["flag"])
|
||||
except BaseException:
|
||||
print("获取IP属地失败, 默认您位于中国大陆境内")
|
||||
return True
|
||||
|
||||
GLOBAL_CDN_LIST = ["https://raw.githubusercontent.com/{repo}/{sha}/{path}"]
|
||||
|
||||
GITHUB_API_BASE = "https://api.github.com"
|
||||
REGION_CHECK_URL = "https://mips.kugou.com/check/iscn?&format=json"
|
||||
IS_CN: bool = check_ip()
|
||||
|
||||
LOG_DIR = Path("logs")
|
||||
CONFIG_FILE = Path("config.json")
|
||||
|
||||
STEAM_API_BASE: str = "https://steam.ikunshare.com/api"
|
||||
STEAM_CACHE_CDN_LIST: list = (
|
||||
[
|
||||
"http://alibaba.cdn.steampipe.steamcontent.com",
|
||||
"http://steampipe.steamcontent.tnkjmec.com",
|
||||
]
|
||||
if IS_CN
|
||||
else [
|
||||
"http://fastly.cdn.steampipe.steamcontent.com",
|
||||
"http://akamai.cdn.steampipe.steamcontent.com",
|
||||
"http://telus.cdn.steampipe.steamcontent.com",
|
||||
"https://cache1-hkg1.steamcontent.com",
|
||||
"https://cache2-hkg1.steamcontent.com",
|
||||
"https://cache3-hkg1.steamcontent.com",
|
||||
"https://cache4-hkg1.steamcontent.com",
|
||||
"https://cache5-hkg1.steamcontent.com",
|
||||
"https://cache6-hkg1.steamcontent.com",
|
||||
"https://cache7-hkg1.steamcontent.com",
|
||||
"https://cache8-hkg1.steamcontent.com",
|
||||
"https://cache9-hkg1.steamcontent.com",
|
||||
"https://cache10-hkg1.steamcontent.com",
|
||||
]
|
||||
)
|
||||
|
||||
317
src/main.py
317
src/main.py
@@ -1,17 +1,11 @@
|
||||
import traceback
|
||||
from typing import List, Dict, Tuple
|
||||
|
||||
from . import __version__, __author__, __website__
|
||||
from .constants import BANNER, REPO_LIST
|
||||
from .constants import STEAM_API_BASE
|
||||
from .config import ConfigManager
|
||||
from .logger import Logger
|
||||
from .models import DepotInfo
|
||||
from .models import DepotInfo, ManifestInfo, SteamAppInfo, SteamAppManifestInfo
|
||||
from .network.client import HttpClient
|
||||
from .network.github import GitHubAPI
|
||||
from .utils.region import RegionDetector
|
||||
from .utils.steam import parse_key_file, parse_manifest_filename
|
||||
from .tools.steamtools import SteamTools
|
||||
from .tools.greenluma import GreenLuma
|
||||
from .manifest_handler import ManifestHandler
|
||||
from .utils.i18n import t
|
||||
|
||||
|
||||
class OnekeyApp:
|
||||
@@ -25,146 +19,203 @@ class OnekeyApp:
|
||||
log_file=self.config.app_config.logging_files,
|
||||
)
|
||||
self.client = HttpClient()
|
||||
self.github = GitHubAPI(self.client, self.config.github_headers, self.logger)
|
||||
|
||||
def show_banner(self):
|
||||
"""显示横幅"""
|
||||
self.logger.info(BANNER)
|
||||
self.logger.info(
|
||||
f"本程序源代码基于GPL 2.0许可证开放于Github"
|
||||
)
|
||||
self.logger.info(
|
||||
f"作者: {__author__} | 版本: {__version__} | 官网: {__website__}"
|
||||
)
|
||||
self.logger.info("项目仓库: GitHub: https://github.com/ikunshare/Onekey")
|
||||
self.logger.warning("ikunshare.top | 严禁倒卖")
|
||||
self.logger.warning(
|
||||
"提示: 请确保已安装Windows 10/11并正确配置Steam;SteamTools/GreenLuma"
|
||||
)
|
||||
if not self.config.app_config.github_token:
|
||||
self.logger.warning("开梯子必须配置Token, 你的IP我不相信能干净到哪")
|
||||
|
||||
async def handle_depot_files(
|
||||
self, app_id: str
|
||||
) -> Tuple[List[DepotInfo], Dict[str, List[str]]]:
|
||||
"""处理仓库文件"""
|
||||
depot_list = []
|
||||
depot_map = {}
|
||||
|
||||
repo_info = await self.github.get_latest_repo_info(REPO_LIST, app_id)
|
||||
if not repo_info:
|
||||
return depot_list, depot_map
|
||||
|
||||
self.logger.info(f"当前选择清单仓库: https://github.com/{repo_info.name}")
|
||||
self.logger.info(f"此清单分支上次更新时间:{repo_info.last_update}")
|
||||
|
||||
branch_url = f"https://api.github.com/repos/{repo_info.name}/branches/{app_id}"
|
||||
branch_res = await self.client.get(
|
||||
branch_url, headers=self.config.github_headers
|
||||
)
|
||||
branch_res.raise_for_status()
|
||||
|
||||
tree_url = branch_res.json()["commit"]["commit"]["tree"]["url"]
|
||||
tree_res = await self.client.get(tree_url)
|
||||
tree_res.raise_for_status()
|
||||
|
||||
depot_cache = self.config.steam_path / "depotcache"
|
||||
depot_cache.mkdir(exist_ok=True)
|
||||
|
||||
for item in tree_res.json()["tree"]:
|
||||
file_path = item["path"]
|
||||
|
||||
if file_path.endswith(".manifest"):
|
||||
save_path = depot_cache / file_path
|
||||
if save_path.exists():
|
||||
self.logger.warning(f"已存在清单: {save_path}")
|
||||
continue
|
||||
|
||||
content = await self.github.fetch_file(
|
||||
repo_info.name, repo_info.sha, file_path
|
||||
)
|
||||
save_path.write_bytes(content)
|
||||
self.logger.info(f"清单下载成功: {file_path}")
|
||||
|
||||
depot_id, manifest_id = parse_manifest_filename(file_path)
|
||||
if depot_id and manifest_id:
|
||||
depot_map.setdefault(depot_id, []).append(manifest_id)
|
||||
|
||||
elif "key.vdf" in file_path.lower():
|
||||
key_content = await self.github.fetch_file(
|
||||
repo_info.name, repo_info.sha, file_path
|
||||
)
|
||||
depot_list.extend(parse_key_file(key_content))
|
||||
|
||||
for depot_id in depot_map:
|
||||
depot_map[depot_id].sort(key=lambda x: int(x), reverse=True)
|
||||
|
||||
return depot_list, depot_map
|
||||
|
||||
async def run(self, app_id: str):
|
||||
"""运行主程序"""
|
||||
async def fetch_key(self) -> bool:
|
||||
"""获取并验证卡密信息"""
|
||||
try:
|
||||
detector = RegionDetector(self.client, self.logger)
|
||||
is_cn, country = await detector.check_cn()
|
||||
self.github.is_cn = is_cn
|
||||
response = await self.client._client.post(
|
||||
f"{STEAM_API_BASE}/getKeyInfo",
|
||||
json={"key": self.config.app_config.key},
|
||||
)
|
||||
body = response.json()
|
||||
|
||||
await self.github.check_rate_limit()
|
||||
if not body["info"]:
|
||||
self.logger.error(t("api.key_not_exist"))
|
||||
return False
|
||||
|
||||
self.logger.info(f"正在处理游戏 {app_id}...")
|
||||
depot_data, depot_map = await self.handle_depot_files(app_id)
|
||||
key_type = body["info"]["type"]
|
||||
self.logger.info(t("api.key_type", type=t(f"key_type.{key_type}")))
|
||||
|
||||
if not depot_data:
|
||||
self.logger.error("未找到此游戏的清单")
|
||||
return
|
||||
if key_type != "permanent":
|
||||
self.logger.info(t("api.key_expires", time=body["info"]["expiresAt"]))
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(t("api.key_info_failed", error=str(e)))
|
||||
return True
|
||||
|
||||
print("\n请选择解锁工具:")
|
||||
print("1. SteamTools")
|
||||
print("2. GreenLuma")
|
||||
async def fetch_app_data(
|
||||
self, app_id: str, and_dlc: bool = True
|
||||
) -> Tuple[SteamAppInfo, SteamAppManifestInfo]:
|
||||
"""
|
||||
从API获取应用数据
|
||||
"""
|
||||
main_app_manifests = []
|
||||
dlc_manifests = []
|
||||
|
||||
choice = input("请输入选择 (1/2): ").strip()
|
||||
try:
|
||||
self.logger.info(t("api.fetching_game", app_id=app_id))
|
||||
|
||||
response = await self.client._client.post(
|
||||
f"{STEAM_API_BASE}/getGame",
|
||||
json={"appId": int(app_id), "dlc": and_dlc},
|
||||
headers={"X-Api-Key": self.config.app_config.key},
|
||||
)
|
||||
|
||||
if response.status_code == 401:
|
||||
self.logger.error(t("api.invalid_key"))
|
||||
return SteamAppInfo(), SteamAppManifestInfo(mainapp=[], dlcs=[])
|
||||
|
||||
if response.status_code != 200:
|
||||
self.logger.error(t("api.request_failed", code=response.status_code))
|
||||
return SteamAppInfo(), SteamAppManifestInfo(mainapp=[], dlcs=[])
|
||||
|
||||
data = response.json()
|
||||
|
||||
if not data:
|
||||
self.logger.error(t("api.no_manifest"))
|
||||
return SteamAppInfo(), SteamAppManifestInfo(mainapp=[], dlcs=[])
|
||||
|
||||
self.logger.info(t("api.game_name", name=data.get("name", "Unknown")))
|
||||
self.logger.info(
|
||||
t("api.depot_count", count=data.get("depotCount", "Unknown"))
|
||||
)
|
||||
|
||||
if and_dlc:
|
||||
for item in data.get("gameManifests", []):
|
||||
manifest = ManifestInfo(
|
||||
app_id=item["app_id"],
|
||||
depot_id=item["depot_id"],
|
||||
depot_key=item["depot_key"],
|
||||
manifest_id=item["manifest_id"],
|
||||
url=item["url"],
|
||||
)
|
||||
main_app_manifests.append(manifest)
|
||||
|
||||
for item in data.get("dlcManifests", []):
|
||||
self.logger.info(
|
||||
t("api.dlc_name", name=item.get("dlcName", "Unknown"))
|
||||
)
|
||||
self.logger.info(
|
||||
t("api.dlc_appid", id=item.get("dlcId", "Unknown"))
|
||||
)
|
||||
for manifests in item.get("manifests", []):
|
||||
manifest = ManifestInfo(
|
||||
app_id=manifests["app_id"],
|
||||
depot_id=manifests["depot_id"],
|
||||
depot_key=manifests["depot_key"],
|
||||
manifest_id=manifests["manifest_id"],
|
||||
url=manifests["url"],
|
||||
)
|
||||
dlc_manifests.append(manifest)
|
||||
else:
|
||||
for item in data.get("manifests", []):
|
||||
manifest = ManifestInfo(
|
||||
app_id=item["app_id"],
|
||||
depot_id=item["depot_id"],
|
||||
depot_key=item["depot_key"],
|
||||
manifest_id=item["manifest_id"],
|
||||
url=item["url"],
|
||||
)
|
||||
main_app_manifests.append(manifest)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(t("api.fetch_failed", error=str(e)))
|
||||
return SteamAppInfo(), SteamAppManifestInfo(mainapp=[], dlcs=[])
|
||||
|
||||
return SteamAppInfo(
|
||||
app_id,
|
||||
data.get("name", ""),
|
||||
data.get("totalDLCCount", data.get("dlcCount", 0)),
|
||||
data.get("depotCount", 0),
|
||||
data.get("workshopDecryptionKey", "None"),
|
||||
), SteamAppManifestInfo(mainapp=main_app_manifests, dlcs=dlc_manifests)
|
||||
|
||||
def prepare_depot_data(
|
||||
self, manifests: List[ManifestInfo]
|
||||
) -> Tuple[List[DepotInfo], Dict[str, List[str]]]:
|
||||
"""准备仓库数据"""
|
||||
depot_data = []
|
||||
depot_dict = {}
|
||||
|
||||
for manifest in manifests:
|
||||
if manifest.depot_id not in depot_dict:
|
||||
depot_dict[manifest.depot_id] = {
|
||||
"key": manifest.depot_key,
|
||||
"manifests": [],
|
||||
}
|
||||
depot_dict[manifest.depot_id]["manifests"].append(manifest.manifest_id)
|
||||
|
||||
for depot_id, info in depot_dict.items():
|
||||
depot_info = DepotInfo(
|
||||
depot_id=depot_id,
|
||||
decryption_key=info["key"],
|
||||
manifest_ids=info["manifests"],
|
||||
)
|
||||
depot_data.append(depot_info)
|
||||
|
||||
return depot_data, depot_dict
|
||||
|
||||
async def run(self, app_id: str, tool_type: str, dlc: bool) -> bool:
|
||||
"""
|
||||
为Web版本提供的运行方法。
|
||||
|
||||
Args:
|
||||
app_id: Steam应用ID
|
||||
tool_type: 解锁工具类型 (steamtools/greenluma)
|
||||
dlc: 是否包含DLC
|
||||
|
||||
Returns:
|
||||
是否成功执行
|
||||
"""
|
||||
try:
|
||||
if not self.config.steam_path:
|
||||
self.logger.error(t("task.no_steam_path"))
|
||||
return False
|
||||
|
||||
await self.fetch_key()
|
||||
|
||||
app_info, manifests = await self.fetch_app_data(app_id, dlc)
|
||||
|
||||
if not manifests.mainapp and not manifests.dlcs:
|
||||
return False
|
||||
|
||||
manifest_handler = ManifestHandler(
|
||||
self.client, self.logger, self.config.steam_path
|
||||
)
|
||||
|
||||
processed_manifests = await manifest_handler.process_manifests(manifests)
|
||||
|
||||
if not processed_manifests:
|
||||
self.logger.error(t("task.no_manifest_processed"))
|
||||
return False
|
||||
|
||||
depot_data, _ = self.prepare_depot_data(processed_manifests)
|
||||
self.logger.info(t("tool.selected", tool=tool_type))
|
||||
|
||||
if tool_type == "steamtools":
|
||||
from .tools.steamtools import SteamTools
|
||||
|
||||
if choice == "1":
|
||||
tool = SteamTools(self.config.steam_path)
|
||||
success = await tool.setup(depot_data, app_info)
|
||||
elif tool_type == "greenluma":
|
||||
from .tools.greenluma import GreenLuma
|
||||
|
||||
version_lock = False
|
||||
lock_choice = input(
|
||||
"是否锁定版本(推荐在选择仓库SteamAutoCracks/ManifestHub时使用)?(y/n): "
|
||||
).lower()
|
||||
if lock_choice == "y":
|
||||
version_lock = True
|
||||
|
||||
success = await tool.setup(
|
||||
depot_data, app_id, depot_map=depot_map, version_lock=version_lock
|
||||
)
|
||||
elif choice == "2":
|
||||
tool = GreenLuma(self.config.steam_path)
|
||||
success = await tool.setup(depot_data, app_id)
|
||||
else:
|
||||
self.logger.error("无效的选择")
|
||||
return
|
||||
self.logger.error(t("tool.invalid_selection"))
|
||||
return False
|
||||
|
||||
if success:
|
||||
self.logger.info("游戏解锁配置成功!")
|
||||
self.logger.info("重启Steam后生效")
|
||||
self.logger.info(t("tool.config_success"))
|
||||
self.logger.info(t("tool.restart_steam"))
|
||||
return True
|
||||
else:
|
||||
self.logger.error("配置失败")
|
||||
self.logger.error(t("tool.config_failed"))
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"运行错误: {traceback.format_exc()}")
|
||||
self.logger.error(t("task.run_error", error=str(e)))
|
||||
return False
|
||||
finally:
|
||||
await self.client.close()
|
||||
|
||||
|
||||
async def main():
|
||||
"""程序入口"""
|
||||
app = OnekeyApp()
|
||||
app.show_banner()
|
||||
|
||||
app_id = input("\n请输入游戏AppID: ").strip()
|
||||
|
||||
app_id_list = [id for id in app_id.split("-") if id.isdigit()]
|
||||
if not app_id_list:
|
||||
app.logger.error("App ID无效")
|
||||
return
|
||||
|
||||
await app.run(app_id_list[0])
|
||||
|
||||
136
src/manifest_handler.py
Normal file
136
src/manifest_handler.py
Normal file
@@ -0,0 +1,136 @@
|
||||
import vdf
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
from steam.client.cdn import DepotManifest
|
||||
from .constants import STEAM_CACHE_CDN_LIST
|
||||
from .models import ManifestInfo, SteamAppManifestInfo
|
||||
from .logger import Logger
|
||||
from .network.client import HttpClient
|
||||
from .utils.i18n import t
|
||||
|
||||
|
||||
class ManifestHandler:
|
||||
"""清单处理器"""
|
||||
|
||||
def __init__(self, client: HttpClient, logger: Logger, steam_path: Path):
|
||||
self.client = client
|
||||
self.logger = logger
|
||||
self.steam_path = steam_path
|
||||
self.depot_cache = steam_path / "depotcache"
|
||||
self.depot_cache.mkdir(exist_ok=True)
|
||||
|
||||
async def download_manifest(self, manifest_info: ManifestInfo) -> Optional[bytes]:
|
||||
"""下载清单文件"""
|
||||
for _ in range(3):
|
||||
for cdn in STEAM_CACHE_CDN_LIST:
|
||||
url = cdn + manifest_info.url
|
||||
try:
|
||||
r = await self.client.get(url)
|
||||
if r.status_code == 200:
|
||||
return r.content
|
||||
except Exception as e:
|
||||
self.logger.debug(t("manifest.download.failed", url=url, error=e))
|
||||
|
||||
def process_manifest(
|
||||
self, manifest_data: bytes, manifest_info: ManifestInfo, remove_old: bool = True
|
||||
) -> bool:
|
||||
"""处理清单文件"""
|
||||
try:
|
||||
depot_id = manifest_info.depot_id
|
||||
manifest_id = manifest_info.manifest_id
|
||||
depot_key = bytes.fromhex(manifest_info.depot_key)
|
||||
|
||||
manifest = DepotManifest(manifest_data)
|
||||
manifest_path = self.depot_cache / f"{depot_id}_{manifest_id}.manifest"
|
||||
|
||||
config_path = self.depot_cache / "config.vdf"
|
||||
if config_path.exists():
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
d = vdf.load(f)
|
||||
else:
|
||||
d = {"depots": {}}
|
||||
|
||||
d["depots"][depot_id] = {"DecryptionKey": depot_key.hex()}
|
||||
d = {"depots": dict(sorted(d["depots"].items()))}
|
||||
|
||||
if remove_old:
|
||||
for file in self.depot_cache.iterdir():
|
||||
if file.suffix == ".manifest":
|
||||
parts = file.stem.split("_")
|
||||
if (
|
||||
len(parts) == 2
|
||||
and parts[0] == str(depot_id)
|
||||
and parts[1] != str(manifest_id)
|
||||
):
|
||||
file.unlink(missing_ok=True)
|
||||
self.logger.info(t("manifest.delete_old", name=file.name))
|
||||
|
||||
with open(manifest_path, "wb") as f:
|
||||
f.write(manifest.serialize(compress=False))
|
||||
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
vdf.dump(d, f, pretty=True)
|
||||
|
||||
self.logger.info(
|
||||
t(
|
||||
"manifest.process.success",
|
||||
depot_id=depot_id,
|
||||
manifest_id=manifest_id,
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(t("manifest.process.failed", error=e))
|
||||
return False
|
||||
|
||||
async def process_manifests(
|
||||
self, manifests: SteamAppManifestInfo
|
||||
) -> List[ManifestInfo]:
|
||||
"""批量处理清单"""
|
||||
processed = []
|
||||
|
||||
app_manifest = manifests.mainapp
|
||||
dlc_manifest = manifests.dlcs
|
||||
|
||||
for manifest_info in app_manifest + dlc_manifest:
|
||||
manifest_path = (
|
||||
self.depot_cache
|
||||
/ f"{manifest_info.depot_id}_{manifest_info.manifest_id}.manifest"
|
||||
)
|
||||
|
||||
if manifest_path.exists():
|
||||
self.logger.warning(t("manifest.exists", name=manifest_path.name))
|
||||
processed.append(manifest_info)
|
||||
continue
|
||||
|
||||
self.logger.info(
|
||||
t(
|
||||
"manifest.downloading",
|
||||
depot_id=manifest_info.depot_id,
|
||||
manifest_id=manifest_info.manifest_id,
|
||||
)
|
||||
)
|
||||
manifest_data = await self.download_manifest(manifest_info)
|
||||
|
||||
if manifest_data:
|
||||
if self.process_manifest(manifest_data, manifest_info):
|
||||
processed.append(manifest_info)
|
||||
else:
|
||||
self.logger.error(
|
||||
t(
|
||||
"manifest.downloading.failed",
|
||||
depot_id=manifest_info.depot_id,
|
||||
manifest_id=manifest_info.manifest_id,
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.logger.error(
|
||||
t(
|
||||
"manifest.process.failed2",
|
||||
depot_id=manifest_info.depot_id,
|
||||
manifest_id=manifest_info.manifest_id,
|
||||
)
|
||||
)
|
||||
|
||||
return processed
|
||||
@@ -1,6 +1,5 @@
|
||||
from typing import List
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -17,19 +16,39 @@ class DepotInfo:
|
||||
|
||||
|
||||
@dataclass
|
||||
class RepoInfo:
|
||||
"""GitHub仓库信息"""
|
||||
class ManifestInfo:
|
||||
"""清单信息"""
|
||||
|
||||
app_id: int
|
||||
depot_id: str
|
||||
depot_key: str
|
||||
manifest_id: str
|
||||
url: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class SteamAppInfo:
|
||||
appId: int
|
||||
name: str
|
||||
last_update: datetime
|
||||
sha: str
|
||||
dlcCount: int
|
||||
depotCount: int
|
||||
workshopDecryptionKey: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class SteamAppManifestInfo:
|
||||
mainapp: List[ManifestInfo]
|
||||
dlcs: List[ManifestInfo]
|
||||
|
||||
|
||||
@dataclass
|
||||
class AppConfig:
|
||||
"""应用配置"""
|
||||
|
||||
github_token: str = ""
|
||||
key: str = ""
|
||||
port: int = 5000
|
||||
custom_steam_path: str = ""
|
||||
debug_mode: bool = False
|
||||
logging_files: bool = True
|
||||
show_console: bool = True
|
||||
language: str = "zh"
|
||||
|
||||
@@ -8,7 +8,7 @@ class HttpClient:
|
||||
"""HTTP客户端封装"""
|
||||
|
||||
def __init__(self):
|
||||
self._client = httpx.AsyncClient(verify=False, timeout=30.0)
|
||||
self._client = httpx.AsyncClient(timeout=60.0)
|
||||
|
||||
async def get(self, url: str, headers: Optional[Dict] = None) -> httpx.Response:
|
||||
"""GET请求"""
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import time
|
||||
from typing import List, Dict, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from ..constants import GITHUB_API_BASE, CN_CDN_LIST, GLOBAL_CDN_LIST
|
||||
from ..models import RepoInfo
|
||||
from ..logger import Logger
|
||||
from .client import HttpClient
|
||||
|
||||
|
||||
class GitHubAPI:
|
||||
"""GitHub API封装"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: HttpClient,
|
||||
headers: Optional[Dict] = None,
|
||||
logger: Optional[Logger] = None,
|
||||
):
|
||||
self.client = client
|
||||
self.headers = headers or {}
|
||||
self.logger = logger or Logger("GitHubAPI")
|
||||
self.is_cn = True
|
||||
|
||||
async def check_rate_limit(self) -> None:
|
||||
"""检查API请求限制"""
|
||||
url = f"{GITHUB_API_BASE}/rate_limit"
|
||||
try:
|
||||
r = await self.client.get(url, headers=self.headers)
|
||||
if r.status_code == 200:
|
||||
r_json = r.json()
|
||||
rate_limit = r_json.get("rate", {})
|
||||
remaining = rate_limit.get("remaining", 0)
|
||||
reset_time = rate_limit.get("reset", 0)
|
||||
reset_formatted = time.strftime(
|
||||
"%Y-%m-%d %H:%M:%S", time.localtime(reset_time)
|
||||
)
|
||||
self.logger.info(f"剩余Github API请求次数: {remaining}")
|
||||
if remaining == 0:
|
||||
self.logger.warning(
|
||||
f"GitHub API 请求数已用尽, 将在 {reset_formatted} 重置"
|
||||
)
|
||||
else:
|
||||
self.logger.error("Github请求数检查失败, 网络错误")
|
||||
except Exception as e:
|
||||
self.logger.error(f"检查Github API 请求数失败: {str(e)}")
|
||||
|
||||
async def get_latest_repo_info(
|
||||
self, repos: List[str], app_id: str
|
||||
) -> Optional[RepoInfo]:
|
||||
"""获取最新的仓库信息"""
|
||||
latest_date = None
|
||||
selected_repo = None
|
||||
selected_sha = None
|
||||
|
||||
for repo in repos:
|
||||
url = f"{GITHUB_API_BASE}/repos/{repo}/branches/{app_id}"
|
||||
try:
|
||||
r = await self.client.get(url, headers=self.headers)
|
||||
if r.status_code == 200:
|
||||
r_json = r.json()
|
||||
if "commit" in r_json:
|
||||
date_str = r_json["commit"]["commit"]["author"]["date"]
|
||||
date = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
|
||||
if latest_date is None or date > latest_date:
|
||||
latest_date = date
|
||||
selected_repo = repo
|
||||
selected_sha = r_json["commit"]["sha"]
|
||||
except Exception as e:
|
||||
self.logger.warning(f"检查仓库 {repo} 失败: {str(e)}")
|
||||
|
||||
if selected_repo:
|
||||
return RepoInfo(
|
||||
name=selected_repo, last_update=latest_date, sha=selected_sha
|
||||
)
|
||||
return None
|
||||
|
||||
async def fetch_file(self, repo: str, sha: str, path: str) -> bytes:
|
||||
"""获取文件内容"""
|
||||
cdn_list = CN_CDN_LIST if self.is_cn else GLOBAL_CDN_LIST
|
||||
|
||||
for _ in range(3):
|
||||
for cdn_template in cdn_list:
|
||||
url = cdn_template.format(repo=repo, sha=sha, path=path)
|
||||
try:
|
||||
r = await self.client.get(url, headers=self.headers)
|
||||
if r.status_code == 200:
|
||||
return r.content
|
||||
except Exception as e:
|
||||
self.logger.debug(f"从 {url} 下载失败: {str(e)}")
|
||||
|
||||
raise Exception(f"无法下载文件: {path}")
|
||||
@@ -4,36 +4,65 @@ from typing import List
|
||||
from .base import UnlockTool
|
||||
from ..models import DepotInfo
|
||||
|
||||
|
||||
class GreenLuma(UnlockTool):
|
||||
"""GreenLuma解锁工具实现"""
|
||||
|
||||
async def setup(self, depot_data: List[DepotInfo], app_id: str, **kwargs) -> bool:
|
||||
"""设置GreenLuma解锁"""
|
||||
applist_dir = self.steam_path / "AppList"
|
||||
applist_dir.mkdir(exist_ok=True)
|
||||
|
||||
for f in applist_dir.glob("*.txt"):
|
||||
f.unlink()
|
||||
|
||||
for idx, depot in enumerate(depot_data, 1):
|
||||
(applist_dir / f"{idx}.txt").write_text(depot.depot_id)
|
||||
|
||||
|
||||
if applist_dir.is_file():
|
||||
applist_dir.unlink(missing_ok=True)
|
||||
if not applist_dir.is_dir():
|
||||
applist_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
depot_dict = {}
|
||||
for i in applist_dir.iterdir():
|
||||
if i.stem.isdecimal() and i.suffix == '.txt':
|
||||
with i.open('r', encoding='utf-8') as f:
|
||||
app_id_ = f.read().strip()
|
||||
depot_dict[int(i.stem)] = int(app_id_) if app_id_.isdecimal() else None
|
||||
|
||||
def find_next_index():
|
||||
if not depot_dict:
|
||||
return 0
|
||||
for i in range(max(depot_dict.keys())):
|
||||
if i not in depot_dict:
|
||||
return i
|
||||
return max(depot_dict.keys()) + 1
|
||||
|
||||
if app_id and app_id.isdecimal():
|
||||
app_id_int = int(app_id)
|
||||
if app_id_int not in depot_dict.values():
|
||||
index = find_next_index()
|
||||
with (applist_dir / f'{index}.txt').open('w', encoding='utf-8') as f:
|
||||
f.write(str(app_id))
|
||||
depot_dict[index] = app_id_int
|
||||
|
||||
for depot in depot_data:
|
||||
depot_id = int(depot.depot_id)
|
||||
if depot_id not in depot_dict.values():
|
||||
index = find_next_index()
|
||||
with (applist_dir / f'{index}.txt').open('w', encoding='utf-8') as f:
|
||||
f.write(str(depot_id))
|
||||
depot_dict[index] = depot_id
|
||||
|
||||
config_path = self.steam_path / "config" / "config.vdf"
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
content = vdf.loads(f.read())
|
||||
|
||||
content.setdefault("depots", {}).update(
|
||||
{
|
||||
depot.depot_id: {"DecryptionKey": depot.decryption_key}
|
||||
for depot in depot_data
|
||||
}
|
||||
)
|
||||
|
||||
if config_path.is_file():
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
content = vdf.loads(f.read())
|
||||
else:
|
||||
content = {}
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if "depots" not in content:
|
||||
content["depots"] = {}
|
||||
|
||||
for depot in depot_data:
|
||||
content["depots"][depot.depot_id] = {"DecryptionKey": depot.decryption_key}
|
||||
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
f.write(vdf.dumps(content))
|
||||
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"GreenLuma配置失败: {e}")
|
||||
return False
|
||||
@@ -1,7 +1,8 @@
|
||||
from typing import List, Dict
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
|
||||
from .base import UnlockTool
|
||||
from ..models import DepotInfo
|
||||
from ..models import DepotInfo, SteamAppInfo
|
||||
|
||||
|
||||
class SteamTools(UnlockTool):
|
||||
@@ -10,29 +11,32 @@ class SteamTools(UnlockTool):
|
||||
async def setup(
|
||||
self,
|
||||
depot_data: List[DepotInfo],
|
||||
app_id: str,
|
||||
depot_map: Dict[str, List[str]] = None,
|
||||
version_lock: bool = False,
|
||||
app_info: SteamAppInfo,
|
||||
) -> bool:
|
||||
"""设置SteamTools解锁"""
|
||||
st_path = self.steam_path / "config" / "stplug-in"
|
||||
st_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
lua_content = f'addappid({app_id}, 1, "None")\n'
|
||||
lua_content = f"""
|
||||
-- Generated Lua Manifest by Onekey
|
||||
-- Steam App {app_info.appId} Manifest
|
||||
-- Name: {app_info.name}
|
||||
-- Generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
||||
-- Total Depots: {app_info.depotCount}
|
||||
-- Total DLCs: {app_info.dlcCount}
|
||||
|
||||
-- MAIN APP
|
||||
addappid({app_info.appId}, "0", "{app_info.workshopDecryptionKey}")
|
||||
|
||||
-- ALL Depots
|
||||
"""
|
||||
|
||||
for depot in depot_data:
|
||||
if version_lock and depot_map and depot.depot_id in depot_map:
|
||||
for manifest_id in depot_map[depot.depot_id]:
|
||||
lua_content += (
|
||||
f'addappid({depot.depot_id}, 1, "{depot.decryption_key}")\n'
|
||||
f'setManifestid({depot.depot_id},"{manifest_id}")\n'
|
||||
)
|
||||
else:
|
||||
lua_content += (
|
||||
f'addappid({depot.depot_id}, 1, "{depot.decryption_key}")\n'
|
||||
)
|
||||
lua_content += (
|
||||
f'addappid({depot.depot_id}, "1", "{depot.decryption_key}")\n'
|
||||
)
|
||||
|
||||
lua_file = st_path / f"{app_id}.lua"
|
||||
lua_file = st_path / f"{app_info.appId}.lua"
|
||||
lua_file.write_text(lua_content, encoding="utf-8")
|
||||
|
||||
return True
|
||||
|
||||
247
src/utils/i18n.py
Normal file
247
src/utils/i18n.py
Normal file
@@ -0,0 +1,247 @@
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class I18n:
|
||||
"""国际化管理类"""
|
||||
|
||||
def __init__(self, default_lang: str = "zh"):
|
||||
self.current_lang = default_lang
|
||||
self.translations: Dict[str, Dict[str, str]] = {}
|
||||
self._load_translations()
|
||||
|
||||
def _load_translations(self):
|
||||
"""加载所有语言的翻译"""
|
||||
# 中文翻译
|
||||
self.translations["zh"] = {
|
||||
# 系统托盘
|
||||
"tray.open_browser": "打开浏览器",
|
||||
"tray.show_console": "显示控制台",
|
||||
"tray.exit": "退出程序",
|
||||
# 主程序
|
||||
"main.starting": "正在启动Onekey...",
|
||||
"main.tray_created": "系统托盘已创建",
|
||||
"main.browser_opened": "浏览器已自动打开",
|
||||
"main.browser_open_failed": "无法自动打开浏览器,请手动访问: http://localhost:{port}",
|
||||
"main.exit": "程序已退出",
|
||||
"main.start_error": "启动错误: {error}",
|
||||
"main.press_enter": "按回车键退出...",
|
||||
"main.startup_failed": "启动失败: {error}",
|
||||
"main.checkip_success": "已获取IP属地",
|
||||
"main.checkip_failed": "获取IP属地失败: {error}, 默认您在中国大陆境内",
|
||||
# 配置
|
||||
"config.generated": "配置文件已生成",
|
||||
"config.create_failed": "配置文件创建失败: {error}",
|
||||
"config.corrupted": "配置文件损坏,正在重新生成...",
|
||||
"config.regenerated": "配置文件已重新生成,使用默认配置继续运行",
|
||||
"config.load_failed": "配置加载失败: {error}",
|
||||
"config.use_default": "使用默认配置继续运行",
|
||||
"config.steam_path_failed": "Steam路径获取失败: {error}",
|
||||
"config.continue_partial": "程序将继续运行,但部分功能可能不可用",
|
||||
# API相关
|
||||
"api.key_not_exist": "卡密不存在",
|
||||
"api.key_type": "卡密类型: {type}",
|
||||
"api.key_expires": "卡密过期时间: {time}",
|
||||
"api.key_info_failed": "获取卡密信息失败: {error}",
|
||||
"api.fetching_game": "正在获取游戏 {app_id} 的信息...",
|
||||
"api.invalid_key": "API密钥无效",
|
||||
"api.request_failed": "API请求失败,状态码: {code}",
|
||||
"api.no_manifest": "未找到此游戏的清单信息",
|
||||
"api.game_name": "游戏名称: {name}",
|
||||
"api.depot_count": "Depot数量: {count}",
|
||||
"api.dlc_name": "DLC名称: {name}",
|
||||
"api.dlc_appid": "DLC AppId: {id}",
|
||||
"api.fetch_failed": "获取主游戏信息失败: {error}",
|
||||
# 工具相关
|
||||
"tool.selected": "选择的解锁工具: {tool}",
|
||||
"tool.invalid_selection": "无效的工具选择",
|
||||
"tool.config_success": "游戏解锁配置成功!",
|
||||
"tool.restart_steam": "重启Steam后生效",
|
||||
"tool.config_failed": "配置失败",
|
||||
# 任务相关
|
||||
"task.no_steam_path": "Steam路径未配置或无效,无法继续",
|
||||
"task.no_manifest_processed": "没有成功处理的清单",
|
||||
"task.run_error": "运行错误: {error}",
|
||||
# 卡密类型
|
||||
"key_type.week": "周卡",
|
||||
"key_type.month": "月卡",
|
||||
"key_type.year": "年卡",
|
||||
"key_type.permanent": "永久卡",
|
||||
# 错误消息
|
||||
"error.load_icon": "加载图标失败: {error}",
|
||||
"error.import": "导入错误: {error}",
|
||||
"error.ensure_root": "请确保在项目根目录中运行此程序",
|
||||
# Web相关
|
||||
"web.starting": "启动Onekey Web GUI...",
|
||||
"web.visit": "请在浏览器中访问: http://localhost:{port}",
|
||||
"web.task_running": "已有任务正在运行",
|
||||
"web.invalid_appid": "请输入有效的App ID",
|
||||
"web.invalid_format": "App ID格式无效",
|
||||
"web.task_started": "任务已开始",
|
||||
"web.task_failed": "任务执行失败: {error}",
|
||||
"web.config_saved": "配置已保存",
|
||||
"web.config_save_failed": "保存配置失败: {error}",
|
||||
"web.config_reset": "配置已重置为默认值",
|
||||
"web.config_reset_failed": "重置配置失败: {error}",
|
||||
"web.key_empty": "卡密不能为空",
|
||||
"web.key_service_unavailable": "卡密验证服务不可用",
|
||||
"web.verify_timeout": "验证超时,请检查网络连接",
|
||||
"web.verify_failed": "验证失败: {error}",
|
||||
"web.connected": "已连接到服务器",
|
||||
"web.client_disconnected": "客户端断开连接",
|
||||
"web.websocket_error": "WebSocket 错误: {error}",
|
||||
"web.invalid_config_data": "无效的配置数据",
|
||||
# 清单处理
|
||||
"manifest.download.failed": "从 {url} 下载失败: {error}",
|
||||
"manifest.delete_old": "删除旧清单: {name}",
|
||||
"manifest.process.success": "清单处理成功: {depot_id}_{manifest_id}.manifest",
|
||||
"manifest.process.failed": "处理清单时出错: {error}",
|
||||
"manifest.process.failed2": "处理清单失败: {depot_id}_{manifest_id}",
|
||||
"manifest.exists": "清单已存在: {name}",
|
||||
"manifest.downloading": "正在下载清单: {depot_id}_{manifest_id}",
|
||||
"manifest.downloading.failed": "下载清单失败: {depot_id}_{manifest_id}",
|
||||
}
|
||||
|
||||
# 英文翻译
|
||||
self.translations["en"] = {
|
||||
# System tray
|
||||
"tray.open_browser": "Open Browser",
|
||||
"tray.show_console": "Show Console",
|
||||
"tray.exit": "Exit",
|
||||
# Main program
|
||||
"main.starting": "Starting Onekey...",
|
||||
"main.tray_created": "System tray created",
|
||||
"main.browser_opened": "Browser opened automatically",
|
||||
"main.browser_open_failed": "Failed to open browser automatically, please visit: http://localhost:{port}",
|
||||
"main.exit": "Program exited",
|
||||
"main.start_error": "Startup error: {error}",
|
||||
"main.press_enter": "Press Enter to exit...",
|
||||
"main.startup_failed": "Startup failed: {error}",
|
||||
"main.checkip_success": "Obtained IP territory",
|
||||
"main.checkip_failed": "Failed to obtain IP territory: {error}, by default you are in mainland China",
|
||||
# Configuration
|
||||
"config.generated": "Configuration file generated",
|
||||
"config.create_failed": "Failed to create configuration file: {error}",
|
||||
"config.corrupted": "Configuration file corrupted, regenerating...",
|
||||
"config.regenerated": "Configuration file regenerated, continuing with default config",
|
||||
"config.load_failed": "Failed to load configuration: {error}",
|
||||
"config.use_default": "Continuing with default configuration",
|
||||
"config.steam_path_failed": "Failed to get Steam path: {error}",
|
||||
"config.continue_partial": "Program will continue but some features may be unavailable",
|
||||
# API related
|
||||
"api.key_not_exist": "Key does not exist",
|
||||
"api.key_type": "Key type: {type}",
|
||||
"api.key_expires": "Key expires at: {time}",
|
||||
"api.key_info_failed": "Failed to get key info: {error}",
|
||||
"api.fetching_game": "Fetching game {app_id} information...",
|
||||
"api.invalid_key": "Invalid API key",
|
||||
"api.request_failed": "API request failed with status code: {code}",
|
||||
"api.no_manifest": "No manifest found for this game",
|
||||
"api.game_name": "Game name: {name}",
|
||||
"api.depot_count": "Depot count: {count}",
|
||||
"api.dlc_name": "DLC name: {name}",
|
||||
"api.dlc_appid": "DLC AppId: {id}",
|
||||
"api.fetch_failed": "Failed to fetch main game info: {error}",
|
||||
# Tool related
|
||||
"tool.selected": "Selected unlock tool: {tool}",
|
||||
"tool.invalid_selection": "Invalid tool selection",
|
||||
"tool.config_success": "Game unlock configuration successful!",
|
||||
"tool.restart_steam": "Restart Steam to take effect",
|
||||
"tool.config_failed": "Configuration failed",
|
||||
# Task related
|
||||
"task.no_steam_path": "Steam path not configured or invalid, cannot continue",
|
||||
"task.no_manifest_processed": "No manifests successfully processed",
|
||||
"task.run_error": "Run error: {error}",
|
||||
# Key types
|
||||
"key_type.week": "Weekly",
|
||||
"key_type.month": "Monthly",
|
||||
"key_type.year": "Yearly",
|
||||
"key_type.permanent": "Permanent",
|
||||
# Error messages
|
||||
"error.load_icon": "Failed to load icon: {error}",
|
||||
"error.import": "Import error: {error}",
|
||||
"error.ensure_root": "Please ensure running this program from project root",
|
||||
# Web related
|
||||
"web.starting": "Starting Onekey Web GUI...",
|
||||
"web.visit": "Please visit: http://localhost:{port}",
|
||||
"web.task_running": "A task is already running",
|
||||
"web.invalid_appid": "Please enter a valid App ID",
|
||||
"web.invalid_format": "Invalid App ID format",
|
||||
"web.task_started": "Task started",
|
||||
"web.task_failed": "Task execution failed: {error}",
|
||||
"web.config_saved": "Configuration saved",
|
||||
"web.config_save_failed": "Failed to save configuration: {error}",
|
||||
"web.config_reset": "Configuration reset to default",
|
||||
"web.config_reset_failed": "Failed to reset configuration: {error}",
|
||||
"web.key_empty": "Key cannot be empty",
|
||||
"web.key_service_unavailable": "Key verification service unavailable",
|
||||
"web.verify_timeout": "Verification timeout, please check network connection",
|
||||
"web.verify_failed": "Verification failed: {error}",
|
||||
"web.connected": "Connected to server",
|
||||
"web.client_disconnected": "Client disconnected",
|
||||
"web.websocket_error": "WebSocket error: {error}",
|
||||
"web.invalid_config_data": "Invalid configuration data",
|
||||
# List processing
|
||||
"manifest.download.failed": "Downloading from {url} failed: {error}",
|
||||
"manifest.delete_old": "Delete old manifest: {name}",
|
||||
"manifest.process.success": "Manifest processing successful: {depot_id}_{manifest_id}.manifest",
|
||||
"manifest.process.failed": "Error while processing manifest: {error}",
|
||||
"manifest.process.failed2": "Manifest processing failed: {depot_id}_{manifest_id}",
|
||||
"manifest.exists": "Manifest already exists: {name}",
|
||||
"manifest.downloading": "Downloading manifest: {depot_id}_{manifest_id}",
|
||||
"manifest.downloading.failed": "Manifest download failed: {depot_id}_{manifest_id}",
|
||||
}
|
||||
|
||||
def set_language(self, lang: str):
|
||||
"""设置当前语言"""
|
||||
if lang in self.translations:
|
||||
self.current_lang = lang
|
||||
else:
|
||||
raise ValueError(f"Unsupported language: {lang}")
|
||||
|
||||
def t(self, key: str, **kwargs) -> str:
|
||||
"""
|
||||
获取翻译文本
|
||||
|
||||
Args:
|
||||
key: 翻译键
|
||||
**kwargs: 格式化参数
|
||||
|
||||
Returns:
|
||||
翻译后的文本
|
||||
"""
|
||||
lang_dict = self.translations.get(self.current_lang, {})
|
||||
text = lang_dict.get(key, key)
|
||||
|
||||
# 格式化文本
|
||||
if kwargs:
|
||||
try:
|
||||
text = text.format(**kwargs)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return text
|
||||
|
||||
def get_all_translations(self, lang: str = None) -> Dict[str, str]:
|
||||
"""获取指定语言的所有翻译"""
|
||||
lang = lang or self.current_lang
|
||||
return self.translations.get(lang, {})
|
||||
|
||||
|
||||
# 全局i18n实例
|
||||
_i18n_instance = None
|
||||
|
||||
|
||||
def get_i18n() -> I18n:
|
||||
"""获取全局i18n实例"""
|
||||
global _i18n_instance
|
||||
if _i18n_instance is None:
|
||||
from ..config import ConfigManager
|
||||
|
||||
config = ConfigManager()
|
||||
_i18n_instance = I18n(config.app_config.language)
|
||||
return _i18n_instance
|
||||
|
||||
|
||||
def t(key: str, **kwargs) -> str:
|
||||
"""便捷的翻译函数"""
|
||||
return get_i18n().t(key, **kwargs)
|
||||
@@ -1,32 +0,0 @@
|
||||
from typing import Tuple
|
||||
|
||||
from ..constants import REGION_CHECK_URL
|
||||
from ..network.client import HttpClient
|
||||
from ..logger import Logger
|
||||
|
||||
|
||||
class RegionDetector:
|
||||
"""地区检测器"""
|
||||
|
||||
def __init__(self, client: HttpClient, logger: Logger):
|
||||
self.client = client
|
||||
self.logger = logger
|
||||
|
||||
async def check_cn(self) -> Tuple[bool, str]:
|
||||
"""检查是否在中国大陆"""
|
||||
try:
|
||||
req = await self.client.get(REGION_CHECK_URL)
|
||||
body = req.json()
|
||||
is_cn = bool(body.get("flag", True))
|
||||
country = body.get("country", "Unknown")
|
||||
|
||||
if not is_cn:
|
||||
self.logger.info(
|
||||
f"您在非中国大陆地区({country})上使用了项目, "
|
||||
"已自动切换回Github官方下载CDN"
|
||||
)
|
||||
|
||||
return is_cn, country
|
||||
except Exception as e:
|
||||
self.logger.warning("检查服务器位置失败,自动认为你在中国大陆")
|
||||
return True, "CN"
|
||||
@@ -1,19 +1,4 @@
|
||||
import vdf
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from ..models import DepotInfo
|
||||
|
||||
|
||||
def parse_key_file(content: bytes) -> List[DepotInfo]:
|
||||
"""解析密钥文件"""
|
||||
try:
|
||||
depots = vdf.loads(content.decode("utf-8"))["depots"]
|
||||
return [
|
||||
DepotInfo(depot_id=d_id, decryption_key=d_info["DecryptionKey"])
|
||||
for d_id, d_info in depots.items()
|
||||
]
|
||||
except Exception:
|
||||
return []
|
||||
from typing import Optional, Tuple
|
||||
|
||||
|
||||
def parse_manifest_filename(filename: str) -> Tuple[Optional[str], Optional[str]]:
|
||||
|
||||
0
web/__init__.py
Normal file
0
web/__init__.py
Normal file
448
web/app.py
Normal file
448
web/app.py
Normal file
@@ -0,0 +1,448 @@
|
||||
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
|
||||
from src.utils.i18n import t
|
||||
|
||||
|
||||
# 添加项目根目录到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(t("error.import", error=str(e)))
|
||||
print(t("error.ensure_root"))
|
||||
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 BaseException:
|
||||
# 连接可能已关闭
|
||||
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 BaseException:
|
||||
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参数
|
||||
config = ConfigManager()
|
||||
app.mount(
|
||||
"/static",
|
||||
StaticFiles(directory=f"{base_path}/{config.app_config.language}/static"),
|
||||
name="static",
|
||||
)
|
||||
templates = Jinja2Templates(
|
||||
directory=f"{base_path}/{config.app_config.language}/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 about_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", ""),
|
||||
"Port": config_manager.app_config.port,
|
||||
"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),
|
||||
"Language": data.get("language", "zh"),
|
||||
}
|
||||
|
||||
# 保存配置
|
||||
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", ""),
|
||||
"language": config.app_config.language,
|
||||
},
|
||||
}
|
||||
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(t("web.client_disconnected"))
|
||||
except Exception as e:
|
||||
print(t("web.websocket_error", error=str(e)))
|
||||
manager.disconnect(websocket)
|
||||
|
||||
|
||||
print(t("web.starting"))
|
||||
print(t("web.visit", port=config.app_config.port))
|
||||
277
web/en/static/css/animations.css
Normal file
277
web/en/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/en/static/css/base.css
Normal file
165
web/en/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/en/static/css/components.css
Normal file
459
web/en/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/en/static/css/layout.css
Normal file
218
web/en/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/en/static/css/oobe.css
Normal file
230
web/en/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/en/static/css/project-info.css
Normal file
314
web/en/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/en/static/css/settings.css
Normal file
299
web/en/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/en/static/css/style.css
Normal file
8
web/en/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/en/static/css/utilities.css
Normal file
222
web/en/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/en/static/css/variables.css
Normal file
206
web/en/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%);
|
||||
}
|
||||
}
|
||||
685
web/en/static/js/app.js
Normal file
685
web/en/static/js/app.js
Normal file
@@ -0,0 +1,685 @@
|
||||
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("Connected to server", "success");
|
||||
this.reconnectAttempts = 0;
|
||||
|
||||
this.startHeartbeat();
|
||||
};
|
||||
|
||||
this.socket.onclose = (event) => {
|
||||
console.log("Disconnected from server", event);
|
||||
this.showSnackbar("Disconnected from server", "error");
|
||||
|
||||
this.stopHeartbeat();
|
||||
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.reconnectAttempts++;
|
||||
console.log(
|
||||
`Trying reconnect... (${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("Failed to connect WebSocket", "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">Load config failed: ${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">Failed to connect WebSocket</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 Path: ${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 path not found</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">Debug mode is enable</span>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
return items.join("");
|
||||
}
|
||||
|
||||
toggleAndDLC() {
|
||||
document.getElementById("+DLC").checked = true;
|
||||
}
|
||||
|
||||
async startUnlockTask() {
|
||||
if (this.taskStatus === "running") {
|
||||
this.showSnackbar("There is already a task running", "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("Please enter App ID", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const appIdPattern = /^[\d-]+$/;
|
||||
if (!appIdPattern.test(appId)) {
|
||||
this.showSnackbar(
|
||||
"Invalid App ID format, should be a number or numbers separated by -",
|
||||
"error",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.taskStatus = "running";
|
||||
this.updateUIForRunningTask();
|
||||
this.clearLogs();
|
||||
this.addLogEntry("info", `Start working on the game ${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("Mission has started", "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("Failed to start task", "error");
|
||||
this.addLogEntry("error", `Failed to start task: ${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>
|
||||
Executing...
|
||||
`;
|
||||
|
||||
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>
|
||||
Start unlocking
|
||||
`;
|
||||
|
||||
resetBtn.disabled = false;
|
||||
appIdInput.disabled = false;
|
||||
toolTypeRadios.forEach((radio) => (radio.disabled = false));
|
||||
}
|
||||
|
||||
resetForm() {
|
||||
if (this.taskStatus === "running") {
|
||||
this.showSnackbar("The task is running and cannot be reset.", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("unlockForm").reset();
|
||||
document.querySelector(
|
||||
'input[name="toolType"][value="steamtools"]',
|
||||
).checked = true;
|
||||
this.clearLogs();
|
||||
this.showSnackbar("Form has been reset", "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>Wait for task to start...</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 enhancements loaded ✨");
|
||||
|
||||
new OnekeyWebApp();
|
||||
});
|
||||
180
web/en/static/js/project-info.js
Normal file
180
web/en/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 repository"
|
||||
: link.classList.contains("releases")
|
||||
? "Download release version"
|
||||
: link.classList.contains("docs")
|
||||
? "Use documentation"
|
||||
: link.classList.contains("issues")
|
||||
? "Problem feedback"
|
||||
: "unknown link";
|
||||
|
||||
console.log(`User clicked on ${linkType} link`);
|
||||
|
||||
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 unlocking made easy!");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
showEasterEgg() {
|
||||
const messages = [
|
||||
"🎉 You found a hidden easter egg!",
|
||||
"🚀 Thank you for using Onekey Tools!",
|
||||
"⭐ Don’t forget to give the project a star!",
|
||||
"🎮 Happy gaming!",
|
||||
|
||||
"🔓 Unlock it with one click and enjoy the game!",
|
||||
];
|
||||
|
||||
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();
|
||||
});
|
||||
677
web/en/static/js/settings.js
Normal file
677
web/en/static/js/settings.js
Normal file
@@ -0,0 +1,677 @@
|
||||
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(
|
||||
"Reset configuration",
|
||||
"Are you sure you want to reset all configurations to default? This operation is irreversible.",
|
||||
() => 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(
|
||||
"Change card password",
|
||||
"Are you sure you want to change to a new card password? Re-verification is required after replacement.",
|
||||
() => 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(
|
||||
"Failed to load configuration: " + data.message,
|
||||
"error",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.showSnackbar("Unable to connect to server", "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>No card password set</strong><br>
|
||||
Please enter your authorization card password below
|
||||
</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>Card password verification failed</strong><br>
|
||||
The current card password is invalid or expired, please replace it with a new one
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
keyInfoSection.innerHTML = `
|
||||
<div class="expiry-warning">
|
||||
<span class="material-icons">error</span>
|
||||
<div>
|
||||
<strong>Failed to obtain card password information</strong><br>
|
||||
Please check the network connection or contact customer service
|
||||
</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: "day card",
|
||||
week: "Weekly card",
|
||||
month: "monthly card",
|
||||
year: "Annual Pass",
|
||||
permanent: "permanent card",
|
||||
};
|
||||
|
||||
let statusBadge = "";
|
||||
if (isExpired && keyInfo.type != "permanent") {
|
||||
statusBadge =
|
||||
'<span class="key-status-badge expired"><span class="material-icons" style="font-size: 14px;">cancel</span>Expired</span>';
|
||||
} else if (!keyInfo.isActive) {
|
||||
statusBadge =
|
||||
'<span class="key-status-badge inactive"><span class="material-icons" style="font-size: 14px;">pause</span>Not activated</span>';
|
||||
} else {
|
||||
statusBadge =
|
||||
'<span class="key-status-badge active"><span class="material-icons" style="font-size: 14px;">check_circle</span>normal</span>';
|
||||
}
|
||||
|
||||
let warningSection = "";
|
||||
if (isExpiringSoon) {
|
||||
warningSection = `
|
||||
<div class="expiry-warning">
|
||||
<span class="material-icons">schedule</span>
|
||||
<div>
|
||||
<strong>upcoming expiry reminder</strong><br>
|
||||
Your card password will expire in ${daysLeft} days, please renew in time
|
||||
</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">Cardamom</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">Type</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">State</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">Expiration time</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">Number of uses</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">Creation time</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("Please enter new card password", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!key.match(/^[A-Z0-9_-]+$/)) {
|
||||
this.showSnackbar("The card password format is incorrect", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const verifyBtn = document.getElementById("verifyNewKey");
|
||||
const changeBtn = document.getElementById("changeKey");
|
||||
|
||||
verifyBtn.disabled = true;
|
||||
verifyBtn.innerHTML =
|
||||
'<span class="material-icons">hourglass_empty</span>Verifying...';
|
||||
|
||||
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(
|
||||
"New card password verification successful!",
|
||||
"success",
|
||||
);
|
||||
|
||||
changeBtn.style.display = "flex";
|
||||
verifyBtn.style.display = "none";
|
||||
|
||||
const typeNames = {
|
||||
day: "day card",
|
||||
week: "Weekly card",
|
||||
month: "Monthly card",
|
||||
year: "Annual Pass",
|
||||
permanent: "permanent card",
|
||||
};
|
||||
|
||||
const expiresAt = new Date(data.info.expiresAt);
|
||||
this.showSnackbar(
|
||||
`Verification successful! New card password type:
|
||||
${typeNames[data.info.type]}
|
||||
}, valid until:${expiresAt.toLocaleDateString()}`,
|
||||
"success",
|
||||
);
|
||||
} else {
|
||||
this.showSnackbar(
|
||||
"The new card password is invalid or expired",
|
||||
"error",
|
||||
);
|
||||
this.newKeyData = null;
|
||||
}
|
||||
} catch (error) {
|
||||
this.showSnackbar(
|
||||
"Verification failed, please check network connection",
|
||||
"error",
|
||||
);
|
||||
console.error("New key verification error:", error);
|
||||
} finally {
|
||||
verifyBtn.disabled = false;
|
||||
verifyBtn.innerHTML = '<span class="material-icons">check</span>Verify';
|
||||
}
|
||||
}
|
||||
|
||||
async changeKey() {
|
||||
if (!this.newKeyData) {
|
||||
this.showSnackbar("Please verify the new card password first", "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("Card secret changed successfully!", "success");
|
||||
|
||||
await this.loadKeyInfo();
|
||||
|
||||
this.resetNewKeyStatus();
|
||||
document.getElementById("newKey").value = "";
|
||||
} else {
|
||||
this.showSnackbar("Replacement failed: " + data.message, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
this.showSnackbar(
|
||||
"An error occurred while changing the card password",
|
||||
"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("Configuration saved", "success");
|
||||
await this.loadConfig();
|
||||
} else {
|
||||
this.showSnackbar("Save failed: " + data.message, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
this.showSnackbar(
|
||||
"An error occurred while saving the configuration",
|
||||
"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(
|
||||
"The configuration has been reset (the card password remains unchanged)",
|
||||
"success",
|
||||
);
|
||||
await this.loadConfig();
|
||||
} else {
|
||||
this.showSnackbar("Reset failed: " + data.message, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
this.showSnackbar(
|
||||
"An error occurred while resetting the configuration",
|
||||
"error",
|
||||
);
|
||||
console.error("Reset config error:", error);
|
||||
}
|
||||
|
||||
this.hideConfirmDialog();
|
||||
}
|
||||
|
||||
async testConfig() {
|
||||
this.showSnackbar("Testing configuration...", "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 Path configuration is normal");
|
||||
} else {
|
||||
messages.push("✗ Steam Abnormal path configuration");
|
||||
}
|
||||
|
||||
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("✓ Card secret status is normal");
|
||||
} else {
|
||||
messages.push("✗ Abnormal card secret status");
|
||||
}
|
||||
}
|
||||
|
||||
this.showSnackbar(
|
||||
`Configuration test completed: ${messages.join(", ")}`,
|
||||
"success",
|
||||
);
|
||||
} else {
|
||||
this.showSnackbar(
|
||||
"Configuration test failed: " + data.message,
|
||||
"error",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.showSnackbar(
|
||||
"An error occurred while configuring the test",
|
||||
"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(
|
||||
"It has been set as a common path, please confirm whether it is correct",
|
||||
"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">The automatically detected path will be used</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">The path format looks correct</span>
|
||||
`;
|
||||
} else {
|
||||
statusElement.className = "status-indicator warning";
|
||||
statusElement.innerHTML = `
|
||||
<span class="material-icons status-icon">warning</span>
|
||||
<span class="status-text">The path may be incorrect, please confirm</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 path",
|
||||
description: `Path is valid: ${config.steam_path}`,
|
||||
});
|
||||
} else if (config.steam_path) {
|
||||
statusCards.push({
|
||||
type: "warning",
|
||||
icon: "folder_off",
|
||||
title: "Steam path",
|
||||
description: "Path is set but may be invalid",
|
||||
});
|
||||
} else {
|
||||
statusCards.push({
|
||||
type: "error",
|
||||
icon: "error",
|
||||
title: "Steam path",
|
||||
description: "Not set or auto-detection failed",
|
||||
});
|
||||
}
|
||||
|
||||
if (config.debug_mode) {
|
||||
statusCards.push({
|
||||
type: "warning",
|
||||
icon: "bug_report",
|
||||
title: "Debug mode",
|
||||
description: "Enabled, detailed logs will be output",
|
||||
});
|
||||
}
|
||||
|
||||
if (config.logging_files) {
|
||||
statusCards.push({
|
||||
type: "success",
|
||||
icon: "description",
|
||||
title: "Log file",
|
||||
description: "Enabled, logs will be saved to file",
|
||||
});
|
||||
}
|
||||
|
||||
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/en/static/js/theme.js
Normal file
178
web/en/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" ? "Switch to light mode" : "Switch to dark mode";
|
||||
|
||||
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(`Topic switch to: ${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();
|
||||
172
web/en/templates/about.html
Normal file
172
web/en/templates/about.html
Normal file
@@ -0,0 +1,172 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<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/LXGWWenKaiMono-Regular/result.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Custom Styles -->
|
||||
<link rel="stylesheet" href="/static/css/style.css" />
|
||||
</head>
|
||||
|
||||
<div class="footer-content">
|
||||
<!-- Project Info Card -->
|
||||
<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">
|
||||
Intuitive, elegant game unlock solution
|
||||
</p>
|
||||
</div>
|
||||
<div class="project-version">
|
||||
<span class="version-label">v2.1.1</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 Repository</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">Download Release</span>
|
||||
<span class="link-url">Get Latest Version</span>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="https://shop.ikunshare.com"
|
||||
target="_blank"
|
||||
class="project-link buy_cdk"
|
||||
>
|
||||
<span class="material-icons">shopping_cart</span>
|
||||
<div class="link-content">
|
||||
<span class="link-title">Purchase Link</span>
|
||||
<span class="link-url">Buy Activation Key</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">Author Page</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">Author Page</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">Issue Feedback</span>
|
||||
<span class="link-url">Report Bugs or Suggestions</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Technical Information -->
|
||||
<div class="tech-info">
|
||||
<div class="tech-header">
|
||||
<span class="material-icons">code</span>
|
||||
<h4>Technical Information</h4>
|
||||
</div>
|
||||
<div class="tech-content">
|
||||
<div class="tech-item">
|
||||
<strong>🐍 Backend Technology</strong>
|
||||
<span>Python 3.8+ • FastAPI • AsyncIO • HTTPX</span>
|
||||
</div>
|
||||
<div class="tech-item">
|
||||
<strong>🌐 Frontend Technology</strong>
|
||||
<span>HTML5 • CSS3 • JavaScript ES6+ • Material Design 3.0</span>
|
||||
</div>
|
||||
<div class="tech-item">
|
||||
<strong>🔧 Supported Tools</strong>
|
||||
<span>SteamTools • GreenLuma</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Usage Notice -->
|
||||
<div class="usage-notice">
|
||||
<div class="notice-header">
|
||||
<span class="material-icons">info</span>
|
||||
<h4>Usage Notice</h4>
|
||||
</div>
|
||||
<div class="notice-content">
|
||||
<p>
|
||||
<strong>🖥️ System Requirements</strong> - Please ensure Windows 10/11
|
||||
is installed and Steam client is properly configured
|
||||
</p>
|
||||
<p>
|
||||
<strong>🛠️ Tool Preparation</strong> - Please install SteamTools or
|
||||
GreenLuma unlock tool before use
|
||||
</p>
|
||||
<p>
|
||||
<strong>🔒 Disclaimer</strong> - This tool is for educational and
|
||||
communication purposes only, users assume all related risks
|
||||
</p>
|
||||
<p>
|
||||
<strong>⭐ Support Project</strong> - If this tool helps you, feel
|
||||
free to star the project on GitHub
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Copyright Information -->
|
||||
<div class="copyright">
|
||||
<p>© 2025 Onekey Steam Unlock Tool • Authors: qwq-xinkeng && ikun0014</p>
|
||||
<p>
|
||||
Project URL:
|
||||
<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>
|
||||
180
web/en/templates/index.html
Normal file
180
web/en/templates/index.html
Normal file
@@ -0,0 +1,180 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<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/LXGWWenKaiMono-Regular/result.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Custom Styles -->
|
||||
<link rel="stylesheet" href="/static/css/style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- Top App Bar -->
|
||||
<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="Toggle Theme"
|
||||
>
|
||||
<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">Settings</span>
|
||||
</a>
|
||||
<a href="/about" class="btn btn-text about-link">
|
||||
<span class="material-icons">info</span>
|
||||
<span class="about-text">About</span>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="main-content">
|
||||
<!-- Configuration Status Card -->
|
||||
<div class="card config-card">
|
||||
<div class="card-header">
|
||||
<span class="material-icons">settings</span>
|
||||
<h2>Configuration Status</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="config-status" id="configStatus">
|
||||
<div class="loading">Checking configuration...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Unlock Card -->
|
||||
<div class="card unlock-card">
|
||||
<div class="card-header">
|
||||
<span class="material-icons">lock_open</span>
|
||||
<h2>Game Unlock</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="Enter game App ID"
|
||||
inputmode="numeric"
|
||||
autocomplete="off"
|
||||
autofocus
|
||||
required
|
||||
/>
|
||||
<div class="input-feedback" id="appIdFeedback"></div>
|
||||
<div class="input-helper">Example: 730 (CS2), 570 (Dota 2)</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label class="input-label">Unlock Tool</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 (actively updated, recommended)</span
|
||||
>
|
||||
</label>
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="toolType" value="greenluma" />
|
||||
<span class="radio-button"></span>
|
||||
<span class="radio-label"
|
||||
>GreenLuma (updated yearly, no 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"
|
||||
>Retrieve and include all DLCs</span
|
||||
>
|
||||
</label>
|
||||
<div class="input-helper">
|
||||
Note: Some DLC depots are bundled with the base game and not
|
||||
separated
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button type="submit" class="btn btn-primary" id="unlockBtn">
|
||||
<span class="material-icons">play_arrow</span>
|
||||
Start Unlock
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" id="resetBtn">
|
||||
<span class="material-icons">refresh</span>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Log Card -->
|
||||
<div class="card progress-card">
|
||||
<div class="card-header">
|
||||
<span class="material-icons">timeline</span>
|
||||
<h2>Execution Log</h2>
|
||||
<div class="card-actions">
|
||||
<button class="btn btn-text" id="clearLogBtn">
|
||||
<span class="material-icons">clear_all</span>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="progress-container" id="progressContainer">
|
||||
<div class="progress-placeholder">
|
||||
<span class="material-icons">info</span>
|
||||
<p>Waiting for task to start...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Snackbar -->
|
||||
<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>
|
||||
|
||||
<!-- Scripts -->
|
||||
<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>
|
||||
501
web/en/templates/oobe.html
Normal file
501
web/en/templates/oobe.html
Normal file
@@ -0,0 +1,501 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Onekey - First Time Setup Wizard</title>
|
||||
|
||||
<!-- Material Design 3 -->
|
||||
<link
|
||||
href="https://cdn.jsdmirror.com/gh/ikun0014/font@main/LXGWWenKaiMono-Regular/result.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Custom Styles -->
|
||||
<link rel="stylesheet" href="/static/css/style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="oobe-container">
|
||||
<!-- Top App Bar -->
|
||||
<div class="oobe-card">
|
||||
<div class="oobe-header">
|
||||
<button
|
||||
type="button"
|
||||
class="theme-toggle"
|
||||
id="themeToggle"
|
||||
title="Toggle Theme"
|
||||
>
|
||||
<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">Welcome to Onekey</h1>
|
||||
<p class="oobe-subtitle">One-click unlock, enjoy gaming experience</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>
|
||||
|
||||
<!-- Step 1: Welcome -->
|
||||
<div class="oobe-step active" data-step="0">
|
||||
<div class="welcome-text">
|
||||
<h3>🎮 Welcome to the Onekey World</h3>
|
||||
<p>
|
||||
Onekey is a powerful Steam game unlock tool that helps you
|
||||
easily manage and unlock games.
|
||||
</p>
|
||||
<p>
|
||||
Before getting started, we need to verify your activation key.
|
||||
</p>
|
||||
<p><strong>Features:</strong></p>
|
||||
<p>• Supports both SteamTools and GreenLuma unlock methods</p>
|
||||
<p>• Intuitive web interface, simple operation</p>
|
||||
<p>• Real-time log display, transparent process</p>
|
||||
<p>
|
||||
• Frontend code completely open source, absolutely no account
|
||||
theft/mining
|
||||
</p>
|
||||
<a href="https://shop.ikunshare.com" target="_blank"
|
||||
>• Click here to purchase activation key</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Key Verification -->
|
||||
<div class="oobe-step" data-step="1">
|
||||
<div class="welcome-text">
|
||||
<h3>🔑 Activate Your Key</h3>
|
||||
<p>
|
||||
Please enter your activation key to activate the Onekey tool.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="key-input-section">
|
||||
<div class="input-group">
|
||||
<label for="activationKey" class="input-label"
|
||||
>Activation Key</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="activationKey"
|
||||
class="text-field"
|
||||
placeholder="Please enter your key"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="input-helper">
|
||||
Key format: [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">Verifying...</span>
|
||||
</div>
|
||||
<div class="key-info" id="keyInfo"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 4: Complete -->
|
||||
<div class="oobe-step" data-step="3">
|
||||
<div class="welcome-text">
|
||||
<h3>🎉 Setup Complete</h3>
|
||||
<p>
|
||||
Congratulations! You have successfully activated the Onekey
|
||||
tool.
|
||||
</p>
|
||||
<p>Now you can start using all features.</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>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
id="nextBtn"
|
||||
class="btn btn-primary btn-large"
|
||||
>
|
||||
<span class="material-icons">arrow_forward</span>
|
||||
Next
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
id="verifyBtn"
|
||||
class="btn btn-primary btn-large"
|
||||
style="display: none"
|
||||
>
|
||||
<span class="material-icons">verified</span>
|
||||
Verify Key
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
id="finishBtn"
|
||||
class="btn btn-primary btn-large"
|
||||
style="display: none"
|
||||
>
|
||||
<span class="material-icons">check</span>
|
||||
Start Using
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="loading-overlay" id="loadingOverlay">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Snackbar -->
|
||||
<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("Please enter the key", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!key.match(/^[A-Z0-9_-]+$/)) {
|
||||
this.showKeyStatus("error", "Invalid key format", "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",
|
||||
"Key verified successfully!",
|
||||
"check_circle",
|
||||
);
|
||||
this.displayKeyInfo(data.info);
|
||||
|
||||
setTimeout(() => {
|
||||
this.nextStep();
|
||||
this.showFinalKeyInfo(data.info);
|
||||
}, 2000);
|
||||
} else {
|
||||
this.showKeyStatus(
|
||||
"error",
|
||||
data.message || "Key does not exist or has expired",
|
||||
"error",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.showKeyStatus(
|
||||
"error",
|
||||
"Verification failed, please check network connection",
|
||||
"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: "Daily",
|
||||
week: "Weekly",
|
||||
month: "Monthly",
|
||||
year: "Yearly",
|
||||
permanent: "Permanent",
|
||||
};
|
||||
|
||||
keyInfoContainer.innerHTML = `
|
||||
<div class="key-info-item">
|
||||
<span class="material-icons">label</span>
|
||||
<span>Type: ${typeNames[keyInfo.type] || keyInfo.type}</span>
|
||||
</div>
|
||||
<div class="key-info-item">
|
||||
<span class="material-icons">schedule</span>
|
||||
<span>Expires: ${expiresAt.toLocaleDateString()}</span>
|
||||
</div>
|
||||
<div class="key-info-item">
|
||||
<span class="material-icons">analytics</span>
|
||||
<span>Usage Count: ${keyInfo.usageCount}</span>
|
||||
</div>
|
||||
<div class="key-info-item">
|
||||
<span class="material-icons">${keyInfo.isActive && !isExpired ? "check_circle" : "cancel"}</span>
|
||||
<span>Status: ${keyInfo.isActive && !isExpired ? "Valid" : "Invalid"}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
showFinalKeyInfo(keyInfo) {
|
||||
const finalKeyInfo = document.getElementById("finalKeyInfo");
|
||||
const expiresAt = new Date(keyInfo.expiresAt);
|
||||
|
||||
const typeNames = {
|
||||
day: "Daily",
|
||||
week: "Weekly",
|
||||
month: "Monthly",
|
||||
year: "Yearly",
|
||||
permanent: "Permanent",
|
||||
};
|
||||
|
||||
finalKeyInfo.innerHTML = `
|
||||
<div class="key-info-item">
|
||||
<span class="material-icons">verified_user</span>
|
||||
<span><strong>Key Type: </strong>${typeNames[keyInfo.type] || keyInfo.type}</span>
|
||||
</div>
|
||||
<div class="key-info-item">
|
||||
<span class="material-icons">event</span>
|
||||
<span><strong>Valid Until: </strong>${expiresAt.toLocaleDateString()} ${expiresAt.toLocaleTimeString()}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async finishSetup() {
|
||||
if (!this.keyData) {
|
||||
this.showSnackbar("Key data lost, please verify again", "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(
|
||||
"Configuration saved successfully, redirecting...",
|
||||
"success",
|
||||
);
|
||||
setTimeout(() => {
|
||||
window.location.href = "/";
|
||||
}, 1500);
|
||||
} else {
|
||||
throw new Error(data.message || "Failed to save configuration");
|
||||
}
|
||||
} catch (error) {
|
||||
this.showSnackbar(
|
||||
"Failed to save configuration: " + 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>
|
||||
292
web/en/templates/settings.html
Normal file
292
web/en/templates/settings.html
Normal file
@@ -0,0 +1,292 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<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/LXGWWenKaiMono-Regular/result.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Custom Styles -->
|
||||
<link rel="stylesheet" href="/static/css/style.css" />
|
||||
<link rel="stylesheet" href="/static/css/settings.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- Top App Bar -->
|
||||
<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">Settings</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="main-content settings-main">
|
||||
<!-- Key Management Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="material-icons">verified</span>
|
||||
<h2>Key Management</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="settings-section">
|
||||
<div id="keyInfoSection">
|
||||
<div class="loading">Loading key information...</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
|
||||
>
|
||||
Change Key
|
||||
</h4>
|
||||
<div class="key-input-group">
|
||||
<div class="input-group" style="flex: 1; margin: 0">
|
||||
<label for="newKey" class="input-label">New Key</label>
|
||||
<input
|
||||
type="text"
|
||||
id="newKey"
|
||||
class="text-field"
|
||||
placeholder="Please enter new key"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="input-helper">
|
||||
Format: [PREFIX]_XXXXXXXX-XXXXXXXXXXXXXXXX
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
id="verifyNewKey"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
<span class="material-icons">check</span>
|
||||
Verify
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
id="changeKey"
|
||||
class="btn btn-primary"
|
||||
style="display: none"
|
||||
>
|
||||
<span class="material-icons">save</span>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Steam Configuration Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="material-icons">games</span>
|
||||
<h2>Steam Configuration</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="settings-section">
|
||||
<div class="input-group">
|
||||
<label for="steamPath" class="input-label"
|
||||
>Steam Installation Path</label
|
||||
>
|
||||
<div class="path-input-group">
|
||||
<input
|
||||
type="text"
|
||||
id="steamPath"
|
||||
class="text-field"
|
||||
placeholder="Leave blank for auto-detect, or manually enter Steam installation path"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
id="detectSteamPath"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
<span class="material-icons">search</span>
|
||||
Auto Detect
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-helper">
|
||||
The program will attempt to automatically detect the Steam
|
||||
installation path. If detection fails, please enter it
|
||||
manually. Usually located at: 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">Waiting for detection...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Application Configuration Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="material-icons">tune</span>
|
||||
<h2>Application Configuration</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="settings-section">
|
||||
<div class="setting-item">
|
||||
<label class="setting-label">Language Selection</label>
|
||||
<div class="radio-group">
|
||||
<label class="radio-item">
|
||||
<input
|
||||
type="radio"
|
||||
name="language"
|
||||
value="zh"
|
||||
id="language-zh"
|
||||
/>
|
||||
<span class="radio-button"></span>
|
||||
<span class="radio-label">Simplified Chinese</span>
|
||||
</label>
|
||||
<label class="radio-item">
|
||||
<input
|
||||
type="radio"
|
||||
name="language"
|
||||
value="en"
|
||||
id="language-en"
|
||||
checked
|
||||
/>
|
||||
<span class="radio-button"></span>
|
||||
<span class="radio-label">English</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">Debug Mode</span>
|
||||
<span class="checkbox-description"
|
||||
>Enable detailed debug log output</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">Save Log Files</span>
|
||||
<span class="checkbox-description"
|
||||
>Save logs to files for troubleshooting</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">Show Console Window</span>
|
||||
<span class="checkbox-description"
|
||||
>Display console window and log output on startup</span
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons Card -->
|
||||
<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>
|
||||
Save Configuration
|
||||
</button>
|
||||
<button type="button" id="resetConfig" class="btn btn-secondary">
|
||||
<span class="material-icons">restore</span>
|
||||
Reset to Default
|
||||
</button>
|
||||
<button type="button" id="testConfig" class="btn btn-secondary">
|
||||
<span class="material-icons">check_circle</span>
|
||||
Test Configuration
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuration Information Display Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="material-icons">info</span>
|
||||
<h2>Configuration Status</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="config-status-grid" id="configStatusGrid">
|
||||
<div class="loading">Loading configuration status...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Confirmation Dialog -->
|
||||
<div id="confirmDialog" class="dialog-overlay">
|
||||
<div class="dialog">
|
||||
<div class="dialog-header">
|
||||
<h3 id="dialogTitle">Confirm Action</h3>
|
||||
</div>
|
||||
<div class="dialog-content">
|
||||
<p id="dialogMessage">
|
||||
Are you sure you want to perform this action?
|
||||
</p>
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<button type="button" id="dialogCancel" class="btn btn-text">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" id="dialogConfirm" class="btn btn-primary">
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Snackbar -->
|
||||
<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>
|
||||
|
||||
<!-- Scripts -->
|
||||
<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>
|
||||
277
web/zh/static/css/animations.css
Normal file
277
web/zh/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/zh/static/css/base.css
Normal file
165
web/zh/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/zh/static/css/components.css
Normal file
459
web/zh/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/zh/static/css/layout.css
Normal file
218
web/zh/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/zh/static/css/oobe.css
Normal file
230
web/zh/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/zh/static/css/project-info.css
Normal file
314
web/zh/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/zh/static/css/settings.css
Normal file
299
web/zh/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/zh/static/css/style.css
Normal file
8
web/zh/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/zh/static/css/utilities.css
Normal file
222
web/zh/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/zh/static/css/variables.css
Normal file
206
web/zh/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/zh/static/js/app.js
Normal file
682
web/zh/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/zh/static/js/project-info.js
Normal file
180
web/zh/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/zh/static/js/settings.js
Normal file
641
web/zh/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/zh/static/js/theme.js
Normal file
178
web/zh/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();
|
||||
169
web/zh/templates/about.html
Normal file
169
web/zh/templates/about.html
Normal file
@@ -0,0 +1,169 @@
|
||||
<!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/LXGWWenKaiMono-Regular/result.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.1.1</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="https://shop.ikunshare.com"
|
||||
target="_blank"
|
||||
class="project-link buy_cdk"
|
||||
>
|
||||
<span class="material-icons">shopping_cart</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> - 请确保已安装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/zh/templates/index.html
Normal file
175
web/zh/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/LXGWWenKaiMono-Regular/result.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>
|
||||
475
web/zh/templates/oobe.html
Normal file
475
web/zh/templates/oobe.html
Normal file
@@ -0,0 +1,475 @@
|
||||
<!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="https://shop.ikunshare.com" 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>
|
||||
289
web/zh/templates/settings.html
Normal file
289
web/zh/templates/settings.html
Normal file
@@ -0,0 +1,289 @@
|
||||
<!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="setting-label">语言选择</label>
|
||||
<div class="radio-group">
|
||||
<label class="radio-item">
|
||||
<input
|
||||
type="radio"
|
||||
name="language"
|
||||
value="zh"
|
||||
id="language-zh"
|
||||
checked
|
||||
/>
|
||||
<span class="radio-button"></span>
|
||||
<span class="radio-label">简体中文</span>
|
||||
</label>
|
||||
<label class="radio-item">
|
||||
<input
|
||||
type="radio"
|
||||
name="language"
|
||||
value="en"
|
||||
id="language-en"
|
||||
/>
|
||||
<span class="radio-button"></span>
|
||||
<span class="radio-label">English</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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