mirror of
https://github.com/ikunshare/Onekey.git
synced 2026-01-15 01:23:02 +08:00
Compare commits
245 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60983a4f9d | ||
|
|
93096ffc08 | ||
|
|
2cf9af811a | ||
|
|
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 | ||
|
|
7968d40491 | ||
|
|
cc9778e537 | ||
|
|
1f09166b49 | ||
|
|
02e139e23e | ||
|
|
3e336e0b65 | ||
|
|
a511ec20df | ||
|
|
00f81b0263 | ||
|
|
63e302b565 | ||
|
|
7a0924cfea | ||
|
|
1072acd698 | ||
|
|
d0ea16de02 | ||
|
|
b1146dd9ff | ||
|
|
df2f66961e | ||
|
|
5a9be8004d | ||
|
|
33d00b3738 | ||
|
|
2c88a769a4 | ||
|
|
5fb2ed26bd | ||
|
|
1a36dc507c | ||
|
|
f207604b0e | ||
|
|
74a74e5fa3 | ||
|
|
f7118f0224 | ||
|
|
a475dcb6b8 | ||
|
|
2ea7c76004 | ||
|
|
14684cf1b7 | ||
|
|
f560dab35f | ||
|
|
8cdd9aa208 | ||
|
|
37f862ba9e | ||
|
|
8612fd0c94 | ||
|
|
7fcbadabdf | ||
|
|
6a21200ccc | ||
|
|
0a384ce114 | ||
|
|
041f8d6a00 | ||
|
|
4db910c8da | ||
|
|
8bf15eda57 | ||
|
|
628b92b86d | ||
|
|
ee8c2242f2 | ||
|
|
76340538b8 | ||
|
|
c693220d73 | ||
|
|
324e537c60 | ||
|
|
50b9f1b724 | ||
|
|
7ba02c4e8f | ||
|
|
b2dada2018 | ||
|
|
5ca4f26242 | ||
|
|
da596964da | ||
|
|
ed8fa1cd7f | ||
|
|
485a9d85e2 | ||
|
|
41cfa244e3 | ||
|
|
09a9e48f7e | ||
|
|
7ef7297119 | ||
|
|
ad26456d6c | ||
|
|
cd18a2f49d | ||
|
|
485fca07f1 | ||
|
|
452be816b1 | ||
|
|
2ba17f1bac | ||
|
|
17e1fea9cf | ||
|
|
2fd7a13bcc | ||
|
|
4edcfa8c8e | ||
|
|
a0536fb4d6 | ||
|
|
0912841e44 | ||
|
|
3b39253d0f | ||
|
|
1ddbf5e02f | ||
|
|
fd6df047dc | ||
|
|
df2fd4a10a | ||
|
|
72bb4a7e97 | ||
|
|
6b37034360 | ||
|
|
8ca12ea7b0 | ||
|
|
b89c38721f | ||
|
|
76d3d2caeb | ||
|
|
38462bf6cd | ||
|
|
3d028a0e0c | ||
|
|
acfad07a07 | ||
|
|
479661a8a3 | ||
|
|
a5d100078b | ||
|
|
0647419bdf | ||
|
|
e9b466f6df | ||
|
|
7e72bea8a1 | ||
|
|
bb789d8cf7 | ||
|
|
3178303b0a | ||
|
|
738e0eb617 | ||
|
|
fb0806aea7 | ||
|
|
2a02d07e8d | ||
|
|
df4342957f | ||
|
|
e2f2120b0c | ||
|
|
580cd44247 | ||
|
|
0e57caefd1 | ||
|
|
062e58ea57 | ||
|
|
651d9f79b2 | ||
|
|
8f8aaf81a1 | ||
|
|
e8dd606db4 | ||
|
|
b50183e723 | ||
|
|
74c5464bb4 | ||
|
|
7f087983d3 | ||
|
|
9a3668a2f4 | ||
|
|
15d2d46dda | ||
|
|
c03df383a0 | ||
|
|
5893e07901 | ||
|
|
bf6024e4c1 | ||
|
|
39d426d806 | ||
|
|
26fbb82357 | ||
|
|
6fc06a681a | ||
|
|
600a8679f3 | ||
|
|
3fa925c161 | ||
|
|
3642dbde30 | ||
|
|
cec2d0fedb | ||
|
|
59bcd7bcdc | ||
|
|
57f285af37 | ||
|
|
afdcc5d51e | ||
|
|
f8bc9ace69 | ||
|
|
7135e13fd7 | ||
|
|
b87867088b | ||
|
|
fc03979107 | ||
|
|
d36678f53c | ||
|
|
0418dbb685 | ||
|
|
0c783b3a7b | ||
|
|
eb1501a43a | ||
|
|
22df5426d8 | ||
|
|
ea3b682eb7 | ||
|
|
cf0d508237 | ||
|
|
5886b1e4b5 | ||
|
|
a1cde89971 | ||
|
|
697a3769ec | ||
|
|
b1b7e720c0 | ||
|
|
0eb0fb3c7e | ||
|
|
59d5852d11 | ||
|
|
5ac9f8f28a | ||
|
|
b8d76f98da | ||
|
|
1e5f0b6774 | ||
|
|
5357ed5fe7 | ||
|
|
5cd5e89ed7 | ||
|
|
e34cad1889 | ||
|
|
4aa1c22c25 | ||
|
|
8ee7765bb0 | ||
|
|
4bd1362317 | ||
|
|
3c0f2ecd6f | ||
|
|
7590f3f791 | ||
|
|
7adcf004e5 | ||
|
|
9ff08d0f91 | ||
|
|
5356947022 | ||
|
|
6a9fed2e39 | ||
|
|
8cefcae6a7 | ||
|
|
0a3ee59d45 | ||
|
|
194e6a41d2 | ||
|
|
07079a6d58 | ||
|
|
4ed51987ba | ||
|
|
28db8b15db | ||
|
|
c33b871188 | ||
|
|
9bd6cc5a60 | ||
|
|
87594e0bf0 | ||
|
|
6f4dac876f | ||
|
|
2073fbdd9a | ||
|
|
a821938f01 | ||
|
|
8bc6095dcf | ||
|
|
b1b0fe9517 | ||
|
|
5964b5fb4e | ||
|
|
aa27e11cd7 | ||
|
|
ecc454de61 | ||
|
|
1cdb19b3df | ||
|
|
53b76aea64 | ||
|
|
7e3e06ac00 | ||
|
|
cfe9c5c8d6 | ||
|
|
b8f0b5caf4 | ||
|
|
c866f19967 | ||
|
|
d68a49cdd1 | ||
|
|
3fd5590530 | ||
|
|
213f089c1e | ||
|
|
069ac110e3 | ||
|
|
8296f03f70 | ||
|
|
2041403c87 | ||
|
|
7411c95c05 | ||
|
|
c7251233c3 | ||
|
|
be1ec3e132 | ||
|
|
15f2f655fc | ||
|
|
8106624ed4 | ||
|
|
6c76dd374d | ||
|
|
3eec80b45f | ||
|
|
04c07c5036 | ||
|
|
6e6b933e1a | ||
|
|
bd86a05fad | ||
|
|
cedd86740e | ||
|
|
f92ced9e80 | ||
|
|
d8d0eb1156 | ||
|
|
63391cb048 | ||
|
|
98ce0c091a | ||
|
|
7c75628966 | ||
|
|
d5b4aded3e | ||
|
|
63ce0244b0 | ||
|
|
ce41fcb908 | ||
|
|
f07fc6447e | ||
|
|
3e89c58348 | ||
|
|
4ced52a87f | ||
|
|
7352f20eb4 | ||
|
|
6ec83c4196 | ||
|
|
a7ce46aa52 | ||
|
|
b4ea1edb53 | ||
|
|
f555f0273d | ||
|
|
3ba8b67f0e | ||
|
|
73c92e550e | ||
|
|
a2fa038324 | ||
|
|
ea3aedbab3 | ||
|
|
51ccc579f4 | ||
|
|
17e654a68e | ||
|
|
c4f4fb9e92 | ||
|
|
e38af1675c | ||
|
|
50194cf7de | ||
|
|
7063f2f5dc | ||
|
|
2c6b3bebe1 | ||
|
|
5c74940702 | ||
|
|
dc64d2a9be | ||
|
|
e31e1c40dc |
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
|
||||
74
.github/workflows/release.yml
vendored
Normal file
74
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
name: 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: pkgdeps/git-tag-action@v3
|
||||
with:
|
||||
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: 发布
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{ env.PACKAGE_VERSION }}
|
||||
files: build/Onekey_v${{ env.PACKAGE_VERSION }}.exe
|
||||
prerelease: false
|
||||
draft: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- 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${{ 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${{ env.PACKAGE_VERSION }}.exe
|
||||
gitee_file_path: build/Onekey_v${{ env.PACKAGE_VERSION }}.exe
|
||||
83
.gitignore
vendored
83
.gitignore
vendored
@@ -27,16 +27,9 @@ share/python-wheels/
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Nuitka
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
dist/
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
@@ -88,33 +81,12 @@ profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
# PEP 582
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
@@ -157,14 +129,43 @@ dmypy.json
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
*.json
|
||||
/output
|
||||
*.bat
|
||||
*.xml
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Project specific
|
||||
config.json
|
||||
logs/
|
||||
*.exe
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
htmlcov/
|
||||
.coverage
|
||||
coverage.xml
|
||||
*.cover
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
*.bak
|
||||
*.backup
|
||||
|
||||
# Build artifacts
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
|
||||
# Nuitka
|
||||
*.build
|
||||
*.dist
|
||||
*.onefile-build
|
||||
|
||||
.node_modules/
|
||||
341
LICENSE
Normal file
341
LICENSE
Normal file
@@ -0,0 +1,341 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
<https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Moe Ghoul>, 1 April 1989
|
||||
Moe Ghoul, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
67
LICENSE.md
67
LICENSE.md
@@ -1,67 +0,0 @@
|
||||
# Anti CSDN License (ACSL)
|
||||
|
||||
Version 1.0, June 2024
|
||||
|
||||
Copyright 2024
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
|
||||
|
||||
## Preamble
|
||||
|
||||
The proliferation of software and the ease of copying and modifying it has led to a wide variety of licenses designed to protect the rights of creators while enabling collaboration and further development of the software. This license, the Anti CSDN License (ACSL), is designed with the explicit intent of prohibiting the CSDN and its related websites, including 'gitcode' (hereinafter referred to as "CSDN") from copying, modifying, or redistributing the software (hereinafter referred to as "the Software") it applies to, while still maintaining the software's status as open source for others. The ACSL aims to promote the free use, modification, and sharing of the Software by the open-source community, with the sole restriction of CSDN's involvement.
|
||||
|
||||
## CSDN Details
|
||||
|
||||
CSDN (Chinese Software Developer Network) belongs to Beijing Innovation Lezhi Network Technology Co., Ltd. It is a Chinese information technology knowledge service website with services including information technology dissemination and communication, education and training, and professional technical talent services. It operates a network community, learning platform, and communication platform.
|
||||
|
||||
## Terms and Conditions
|
||||
|
||||
### 1. Definitions
|
||||
|
||||
"This License" refers to version 2.0 of the Anti CSDN License.
|
||||
|
||||
"The Software" refers to the software distributed under this License.
|
||||
|
||||
"CSDN" refers to the website and all its affiliated entities and services that are known for aggregating and redistributing content without explicit permission from the original creators. This includes but is not limited to Gitcode. For detailed information, CSDN (Chinese Software Developer Network) belongs to Beijing Innovation Lezhi Network Technology Co., Ltd. It is a Chinese information technology knowledge service website with services including information technology dissemination and communication, education and training, and professional technical talent services. It operates a network community, learning platform, and communication platform.
|
||||
|
||||
### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each contributor to the Software grants you a worldwide, royalty-free, non-exclusive, perpetual copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute the Software and such derivative works in source code or object form.
|
||||
|
||||
### 3. Prohibition for CSDN
|
||||
|
||||
Notwithstanding the above grant, CSDN and its associated entities, including Gitcode, are expressly prohibited from:
|
||||
|
||||
a. Copying, modifying, or redistributing the Software or any derivative works thereof, in any form.
|
||||
|
||||
b. Using the Software for any form of aggregation, compilation, or database that is accessible on or through any platforms owned, operated, or controlled by CSDN and its associated entities, including Gitcode.
|
||||
|
||||
c. Engaging in any activity that directly or indirectly infringes on the rights granted under this License to any user of the Software.
|
||||
|
||||
### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Software or any derivative works thereof in any medium, with or without modifications, provided that you meet the following conditions:
|
||||
|
||||
a. You must give any other recipients of the Software or derivative works a copy of this License; and
|
||||
|
||||
b. You must cause any modified files to carry prominent notices stating that you changed the files; and
|
||||
|
||||
c. You must retain, in the Software or derivative works, all copyright and permission notices as found in the original Software.(To prevent theft, recommend that you also add Anti CSDN license for your projects)
|
||||
|
||||
### 5. Disclaimer of Warranty
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
### 6. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any contributor to the Software be liable to you for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Software (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such contributor has been advised of the possibility of such damages.
|
||||
|
||||
### 7. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Software or derivative works thereof, you may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, you may act only on your own behalf and on your sole responsibility, not on behalf of any other contributors to the Software, and only if you agree to indemnify, defend, and hold each contributor harmless for any liability incurred by, or claims asserted against, such contributor by reason of your accepting any such warranty or additional liability.
|
||||
|
||||
## END OF TERMS AND CONDITIONS
|
||||
|
||||
By using, copying, modifying, or distributing the Software (or any work based on the Software), you agree to be bound by the terms of this License. If you do not agree to the terms of this License, do not use, copy, modify, or distribute the Software.
|
||||
|
||||
If you seek to redistribute the Software in a manner not permitted by this License, or if you have questions about obtaining additional permissions, please contact the original creators of the Software.
|
||||
39
README.md
39
README.md
@@ -7,36 +7,39 @@
|
||||
[](https://github.com/ikunshare/Onekey/releases)
|
||||
[](https://github.com/ikunshare/Onekey/blob/main/LICENSE)
|
||||
|
||||
[](https://dartnode.com "Powered by DartNode - Free VPS for Open Source")
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
## Onekey
|
||||
Onekey Steam Depot Manifest Downloader
|
||||
|
||||
Onekey Steam Depot Manifest Downloader
|
||||
对本软件有意见的
|
||||
欢迎拨打中华人民共和国公安部门报警电话:110 进行报警
|
||||
|
||||
## 先让我挂些人
|
||||
|
||||
- 沧海颐粟,早期倒卖大手子,现在不知道跑哪了,通过一点手段查到在江西
|
||||
- 玩家资源站,贼喊捉贼笑传,随便改改别人软件的名字就是自己的,还去报官了
|
||||
|
||||
## 使用方法
|
||||
先去Release下最新发布,然后去steamtools官网下steamtools,日志会有点石介意别用
|
||||
|
||||
## 项目协议
|
||||
本项目基于 ACSL V2.0 许可证发行,以下协议是对于 ACSL V2.0 原协议的补充,如有冲突,以以下协议为准。
|
||||
|
||||
词语约定:“使用者”指签署本协议的使用者;“版权数据”指包括但不限于图像、音频、名字等在内的他人拥有所属版权的数据。
|
||||
|
||||
本项目的数据来源原理是从Steam官方的CDN服务器中拉取游戏清单数据,经过对数据简单地筛选与合并后进行展示,因此本项目不对数据的准确性负责。
|
||||
使用本项目的过程中可能会产生版权数据,对于这些版权数据,本项目不拥有它们的所有权,为了避免造成侵权,使用者务必在24 小时内清除使用本项目的过程中所产生的版权数据。
|
||||
由于使用本项目产生的包括由于本协议或由于使用或无法使用本项目而引起的任何性质的任何直接、间接、特殊、偶然或结果性损害(包括但不限于因商誉损失、停工、计算机故障或故障引起的损害赔偿,或任何及所有其他商业损害或损失)由使用者负责。
|
||||
本项目完全免费,且开源发布于 GitHub 面向全世界人用作对技术的学习交流,本项目不对项目内的技术可能存在违反当地法律法规的行为作保证,禁止在违反当地法律法规的情况下使用本项目,对于使用者在明知或不知当地法律法规不允许的情况下使用本项目所造成的任何违法违规行为由使用者承担,本项目不承担由此造成的任何直接、间接、特殊、偶然或结果性责任。
|
||||
而且,本项目已禁止使用于商业用途。
|
||||
若你使用了本项目,将代表你接受以上协议。
|
||||
|
||||
Steam正版平台不易,请尊重版权,支持正版。
|
||||
本项目仅用于对技术可行性的探索及研究,不接受任何商业(包括但不限于广告等)合作。
|
||||
去 Releases 处下载最新的发布,并且安装好 SteamTools 或者 GreenLuma
|
||||
然后打开 Onekey 输入 App ID 即可使用
|
||||
|
||||
## Star 趋势图
|
||||
|
||||
[](https://starchart.cc/ikunshare/Onekey)
|
||||
[](https://starchart.cc/ikunshare/Onekey)
|
||||
|
||||
## 贡献者
|
||||
|
||||
<a href="https://github.com/ikunshare/Onekey/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=ikunshare/Onekey" />
|
||||
</a>
|
||||
|
||||
## 社区和支持
|
||||
|
||||
加入我们的社区,参与讨论和支持:
|
||||
|
||||
- [Telegram](https://t.me/ikunshare_qun)
|
||||
- [QQ](https://qm.qq.com/q/d97QO8XheM)
|
||||
|
||||
BIN
icon.jpg
BIN
icon.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 262 KiB |
442
main.py
442
main.py
@@ -1,311 +1,153 @@
|
||||
import os
|
||||
import vdf
|
||||
import winreg
|
||||
import argparse
|
||||
import aiohttp
|
||||
import aiofiles
|
||||
import traceback
|
||||
import subprocess
|
||||
import colorlog
|
||||
import logging
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
import psutil
|
||||
import asyncio
|
||||
import threading
|
||||
import webview
|
||||
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
|
||||
# 初始化日志记录器
|
||||
def init_log():
|
||||
logger = logging.getLogger('Onekey')
|
||||
logger.setLevel(logging.DEBUG)
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.setLevel(logging.DEBUG)
|
||||
fmt_string = '%(log_color)s[%(name)s][%(levelname)s]%(message)s'
|
||||
log_colors = {
|
||||
'DEBUG': 'cyan',
|
||||
'INFO': 'green',
|
||||
'WARNING': 'yellow',
|
||||
'ERROR': 'red',
|
||||
'CRITICAL': 'purple'
|
||||
}
|
||||
fmt = colorlog.ColoredFormatter(fmt_string, log_colors=log_colors)
|
||||
stream_handler.setFormatter(fmt)
|
||||
logger.addHandler(stream_handler)
|
||||
return logger
|
||||
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))
|
||||
window = webview.create_window(
|
||||
title="Onekey",
|
||||
url=f"http://localhost:{config_manager.app_config.port}",
|
||||
width=1600,
|
||||
height=900,
|
||||
)
|
||||
|
||||
|
||||
# 生成配置文件
|
||||
def gen_config_file():
|
||||
default_config = {"Github_Persoal_Token": "", "Custom_Steam_Path": ""}
|
||||
with open('./config.json', 'w', encoding='utf-8') as f:
|
||||
json.dump(default_config, f)
|
||||
log.info('程序可能为第一次启动,请填写配置文件后重新启动程序')
|
||||
|
||||
|
||||
# 加载配置文件
|
||||
def load_config():
|
||||
if not os.path.exists('./config.json'):
|
||||
gen_config_file()
|
||||
os.system('pause')
|
||||
sys.exit()
|
||||
else:
|
||||
with open('./config.json', 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
return config
|
||||
|
||||
|
||||
log = init_log()
|
||||
config = load_config()
|
||||
lock = asyncio.Lock()
|
||||
|
||||
|
||||
print('\033[1;32;40m _____ __ _ _____ _ _ _____ __ __ \033[0m')
|
||||
print('\033[1;32;40m / _ \ | \ | | | ____| | | / / | ____| \ \ / /\033[0m')
|
||||
print('\033[1;32;40m | | | | | \| | | |__ | |/ / | |__ \ \/ /\033[0m')
|
||||
print('\033[1;32;40m | | | | | |\ | | __| | |\ \ | __| \ /')
|
||||
print('\033[1;32;40m | |_| | | | \ | | |___ | | \ \ | |___ / /\033[0m')
|
||||
print('\033[1;32;40m \_____/ |_| \_| |_____| |_| \_\ |_____| /_/\033[0m')
|
||||
log.info('作者ikun0014')
|
||||
log.info('本项目基于wxy1343/ManifestAutoUpdate进行修改,采用GPL V3许可证')
|
||||
log.info('版本:1.0.4')
|
||||
log.info('项目仓库:https://github.com/ikunshare/Onekey')
|
||||
log.debug('官网:ikunshare.com')
|
||||
log.warning('注意:据传Steam新版本对部分解锁工具进行了检测,但目前未发现问题,如果你被封号可以issue反馈')
|
||||
log.warning('本项目完全免费,如果你在淘宝,QQ群内通过购买方式获得,赶紧回去骂商家死全家\n交流群组:\n点击链接加入群聊【ikun分享】:https://qm.qq.com/q/D9Uiva3RVS\nhttps://t.me/ikunshare_group')
|
||||
|
||||
|
||||
# 通过注册表获取Steam安装路径
|
||||
def get_steam_path():
|
||||
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r'Software\Valve\Steam')
|
||||
steam_path = Path(winreg.QueryValueEx(key, 'SteamPath')[0])
|
||||
custom_steam_path = config.get("Custom_Steam_Path", "")
|
||||
if not custom_steam_path == '':
|
||||
return Path(custom_steam_path)
|
||||
else:
|
||||
return steam_path
|
||||
|
||||
|
||||
steam_path = get_steam_path()
|
||||
isGreenLuma = any((steam_path / dll).exists() for dll in ['GreenLuma_2024_x86.dll', 'GreenLuma_2024_x64.dll', 'User32.dll'])
|
||||
isSteamTools = (steam_path / 'config' / 'stplug-in').is_dir()
|
||||
|
||||
|
||||
# 错误堆栈处理
|
||||
def stack_error(exception):
|
||||
stack_trace = traceback.format_exception(type(exception), exception, exception.__traceback__)
|
||||
return ''.join(stack_trace)
|
||||
|
||||
|
||||
# 下载清单
|
||||
async def get(sha, path):
|
||||
url_list = [
|
||||
f'https://gcore.jsdelivr.net/gh/{repo}@{sha}/{path}',
|
||||
f'https://fastly.jsdelivr.net/gh/{repo}@{sha}/{path}',
|
||||
f'https://cdn.jsdelivr.net/gh/{repo}@{sha}/{path}',
|
||||
f'https://github.moeyy.xyz/https://raw.githubusercontent.com/{repo}/{sha}/{path}',
|
||||
f'https://mirror.ghproxy.com/https://raw.githubusercontent.com/{repo}/{sha}/{path}',
|
||||
f'https://ghproxy.org/https://raw.githubusercontent.com/{repo}/{sha}/{path}',
|
||||
f'https://raw.githubusercontent.com/{repo}/{sha}/{path}'
|
||||
]
|
||||
retry = 3
|
||||
async with aiohttp.ClientSession() as session:
|
||||
while retry:
|
||||
for url in url_list:
|
||||
try:
|
||||
async with session.get(url, ssl=False) as r:
|
||||
if r.status == 200:
|
||||
return await r.read()
|
||||
else:
|
||||
log.error(f'获取失败: {path} - 状态码: {r.status}')
|
||||
except aiohttp.ClientError:
|
||||
log.error(f'获取失败: {path} - 连接错误')
|
||||
retry -= 1
|
||||
log.warning(f'重试剩余次数: {retry} - {path}')
|
||||
log.error(f'超过最大重试次数: {path}')
|
||||
raise Exception(f'Failed to download: {path}')
|
||||
|
||||
|
||||
# 获取清单信息
|
||||
async def get_manifest(sha, path, steam_path: Path):
|
||||
collected_depots = []
|
||||
def hide_console() -> None:
|
||||
"""隐藏控制台窗口"""
|
||||
try:
|
||||
if path.endswith('.manifest'):
|
||||
depot_cache_path = steam_path / 'depotcache'
|
||||
async with lock:
|
||||
if not depot_cache_path.exists():
|
||||
depot_cache_path.mkdir(exist_ok=True)
|
||||
save_path = depot_cache_path / path
|
||||
if save_path.exists():
|
||||
async with lock:
|
||||
log.warning(f'已存在清单: {path}')
|
||||
return collected_depots
|
||||
content = await get(sha, path)
|
||||
async with lock:
|
||||
log.info(f'清单下载成功: {path}')
|
||||
async with aiofiles.open(save_path, 'wb') as f:
|
||||
await f.write(content)
|
||||
elif path == 'Key.vdf':
|
||||
content = await get(sha, path)
|
||||
async with lock:
|
||||
log.info(f'密钥下载成功: {path}')
|
||||
depots_config = vdf.loads(content.decode(encoding='utf-8'))
|
||||
for depot_id, depot_info in depots_config['depots'].items():
|
||||
collected_depots.append((depot_id, depot_info['DecryptionKey']))
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except Exception as e:
|
||||
log.error(f'处理失败: {path} - {stack_error(e)}')
|
||||
traceback.print_exc()
|
||||
raise
|
||||
return collected_depots
|
||||
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
|
||||
|
||||
|
||||
# 合并DecryptionKey
|
||||
async def depotkey_merge(config_path, depots_config):
|
||||
if not config_path.exists():
|
||||
async with lock:
|
||||
log.error('Steam默认配置不存在,可能是没有登录账号')
|
||||
return
|
||||
with open(config_path, encoding='utf-8') as f:
|
||||
config = vdf.load(f)
|
||||
software = config['InstallConfigStore']['Software']
|
||||
valve = software.get('Valve') or software.get('valve')
|
||||
steam = valve.get('Steam') or valve.get('steam')
|
||||
if 'depots' not in steam:
|
||||
steam['depots'] = {}
|
||||
steam['depots'].update(depots_config['depots'])
|
||||
with open(config_path, 'w', encoding='utf-8') as f:
|
||||
vdf.dump(config, f, pretty=True)
|
||||
return True
|
||||
|
||||
|
||||
# 增加SteamTools解锁相关文件
|
||||
async def stool_add(depot_data, app_id):
|
||||
lua_filename = f"Onekey_unlock_{app_id}.lua"
|
||||
lua_filepath = steam_path / "config" / "stplug-in" / lua_filename
|
||||
|
||||
async with lock:
|
||||
log.info(f'SteamTools解锁文件生成: {lua_filepath}')
|
||||
with open(lua_filepath, "w", encoding="utf-8") as lua_file:
|
||||
lua_file.write(f'addappid({app_id}, 1, "None")\n')
|
||||
for depot_id, depot_key in depot_data:
|
||||
lua_file.write(f'addappid({depot_id}, 1, "{depot_key}")\n')
|
||||
|
||||
luapacka_path = steam_path / "config" / "stplug-in" / "luapacka.exe"
|
||||
subprocess.run([str(luapacka_path), str(lua_filepath)])
|
||||
os.remove(lua_filepath)
|
||||
return True
|
||||
|
||||
|
||||
# 增加GreenLuma解锁相关文件
|
||||
async def greenluma_add(depot_id_list):
|
||||
app_list_path = steam_path / 'appcache' / 'appinfo.vdf'
|
||||
if app_list_path.exists() and app_list_path.is_file():
|
||||
app_list_path.unlink(missing_ok=True)
|
||||
if not app_list_path.is_dir():
|
||||
app_list_path.mkdir(parents=True, exist_ok=True)
|
||||
depot_dict = {}
|
||||
for i in app_list_path.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)] = None
|
||||
if app_id_.isdecimal():
|
||||
depot_dict[int(i.stem)] = int(app_id_)
|
||||
for depot_id in depot_id_list:
|
||||
if int(depot_id) not in depot_dict.values():
|
||||
index = max(depot_dict.keys()) + 1 if depot_dict.keys() else 0
|
||||
if index != 0:
|
||||
for i in range(max(depot_dict.keys())):
|
||||
if i not in depot_dict.keys():
|
||||
index = i
|
||||
break
|
||||
with (app_list_path / f'{index}.txt').open('w', encoding='utf-8') as f:
|
||||
f.write(str(depot_id))
|
||||
depot_dict[index] = int(depot_id)
|
||||
return True
|
||||
|
||||
|
||||
# 检测Github Api请求数量
|
||||
async def check_github_api_limit(headers):
|
||||
url = 'https://api.github.com/rate_limit'
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, headers=headers, ssl=False) as r:
|
||||
r_json = await r.json()
|
||||
remain_limit = r_json['rate']['remaining']
|
||||
use_limit = r_json['rate']['used']
|
||||
reset_time = r_json['rate']['reset']
|
||||
f_reset_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(reset_time))
|
||||
log.info(f'已用Github请求数:{use_limit}')
|
||||
log.info(f'剩余Github请求数:{remain_limit}')
|
||||
if r.status == 429:
|
||||
log.info(f'你的Github Api请求数已超限,请尝试增加Persoal Token')
|
||||
log.info(f'请求数重置时间:{f_reset_time}')
|
||||
return True
|
||||
|
||||
|
||||
# 检查进程是否运行
|
||||
def check_process_running(process_name):
|
||||
for process in psutil.process_iter(['name']):
|
||||
if process.info['name'] == process_name:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# 主函数
|
||||
async def main(app_id):
|
||||
app_id_list = list(filter(str.isdecimal, app_id.strip().split('-')))
|
||||
app_id = app_id_list[0]
|
||||
github_token = config.get("Github_Persoal_Token", "")
|
||||
headers = {'Authorization': f'Bearer {github_token}'} if github_token else None
|
||||
|
||||
await check_github_api_limit(headers)
|
||||
|
||||
url = f'https://api.github.com/repos/{repo}/branches/{app_id}'
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, headers=headers, ssl=False) as r:
|
||||
r_json = await r.json()
|
||||
if 'commit' in r_json:
|
||||
sha = r_json['commit']['sha']
|
||||
url = r_json['commit']['commit']['tree']['url']
|
||||
date = r_json['commit']['commit']['author']['date']
|
||||
async with session.get(url, headers=headers, ssl=False) as r2:
|
||||
r2_json = await r2.json()
|
||||
if 'tree' in r2_json:
|
||||
collected_depots = []
|
||||
for i in r2_json['tree']:
|
||||
result = await get_manifest(sha, i['path'], steam_path)
|
||||
collected_depots.extend(result)
|
||||
if collected_depots:
|
||||
if isSteamTools:
|
||||
await stool_add(collected_depots, app_id)
|
||||
log.info('找到SteamTools,已添加解锁文件')
|
||||
if isGreenLuma:
|
||||
await greenluma_add([app_id])
|
||||
depot_config = {'depots': {depot_id: {'DecryptionKey': depot_key} for depot_id, depot_key in collected_depots}}
|
||||
depotkey_merge(steam_path / 'config' / 'config.vdf', depot_config)
|
||||
if await greenluma_add([int(i) for i in depot_config['depots'] if i.isdecimal()]):
|
||||
log.info('找到GreenLuma,已添加解锁文件')
|
||||
log.info(f'清单最后更新时间:{date}')
|
||||
log.info(f'入库成功: {app_id}')
|
||||
return True
|
||||
log.error(f'清单下载或生成.st失败: {app_id}')
|
||||
return False
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-a', '--app-id')
|
||||
args = parser.parse_args()
|
||||
repo = 'ManifestHub/ManifestHub'
|
||||
if __name__ == '__main__':
|
||||
def create_icon() -> Image.Image:
|
||||
"""创建托盘图标"""
|
||||
try:
|
||||
log.debug('App ID可以在SteamDB或Steam商店链接页面查看')
|
||||
asyncio.run(main(args.app_id or input('需要入库的App ID: ')))
|
||||
except KeyboardInterrupt:
|
||||
exit()
|
||||
return Image.open(project_root.parent / "icon.jpg")
|
||||
except Exception as e:
|
||||
log.error(f'发生错误: {stack_error(e)}')
|
||||
traceback.print_exc()
|
||||
if not args.app_id:
|
||||
os.system('pause')
|
||||
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_show_window(icon, item):
|
||||
window.show()
|
||||
|
||||
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.show_window"), on_show_window),
|
||||
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 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"))
|
||||
|
||||
def on_closing():
|
||||
if window.create_confirmation_dialog("Onekey", "是否关闭Onekey"):
|
||||
os._exit(0)
|
||||
return False
|
||||
|
||||
window.events.closing += on_closing
|
||||
|
||||
# 启动浏览器
|
||||
webview.start(func=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__":
|
||||
main()
|
||||
|
||||
22
package.json
Normal file
22
package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "onekey",
|
||||
"version": "2.1.2",
|
||||
"description": "一个Steam仓库清单下载器",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/ikunshare/Onekey.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Onekey"
|
||||
],
|
||||
"author": "ikun0014",
|
||||
"license": "null",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ikunshare/Onekey/issues"
|
||||
},
|
||||
"homepage": "https://github.com/ikunshare/Onekey#readme"
|
||||
}
|
||||
@@ -1,5 +1,15 @@
|
||||
aiofiles==24.1.0
|
||||
aiohttp==3.9.5
|
||||
colorlog==6.8.2
|
||||
psutil==6.0.0
|
||||
vdf==3.4
|
||||
# Server
|
||||
loguru
|
||||
fastapi
|
||||
uvicorn
|
||||
# Encode
|
||||
vdf
|
||||
ujson
|
||||
# Web
|
||||
jinja2
|
||||
aiohttp
|
||||
pywebview
|
||||
websockets
|
||||
# Any things
|
||||
Pillow
|
||||
pystray
|
||||
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
113
src/config.py
Normal file
113
src/config.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import os
|
||||
import sys
|
||||
import ujson
|
||||
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 = {
|
||||
"KEY": "",
|
||||
"Port": 5000,
|
||||
"Debug_Mode": False,
|
||||
"Logging_Files": True,
|
||||
"Show_Console": False,
|
||||
"Custom_Steam_Path": "",
|
||||
"Language": "zh",
|
||||
}
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
"""配置管理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.config_path = CONFIG_FILE
|
||||
self._config_data: Dict = {}
|
||||
self.app_config: AppConfig = AppConfig()
|
||||
self.steam_path: Optional[Path] = None
|
||||
self._load_config()
|
||||
|
||||
def _generate_config(self) -> None:
|
||||
"""生成默认配置文件"""
|
||||
try:
|
||||
with open(self.config_path, "w", encoding="utf-8") as f:
|
||||
ujson.dump(DEFAULT_CONFIG, f, indent=2, ensure_ascii=False)
|
||||
print(t("config.generated"))
|
||||
os.system("pause")
|
||||
sys.exit(1)
|
||||
except IOError as 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()
|
||||
|
||||
try:
|
||||
with open(self.config_path, "r", encoding="utf-8") as f:
|
||||
self._config_data = ujson.load(f)
|
||||
|
||||
self.app_config = AppConfig(
|
||||
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 ujson.JSONDecodeError:
|
||||
print(t("config.corrupted"))
|
||||
self._generate_config()
|
||||
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(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) -> Optional[Path]:
|
||||
"""获取Steam安装路径"""
|
||||
try:
|
||||
if self.app_config.custom_steam_path:
|
||||
return Path(self.app_config.custom_steam_path)
|
||||
|
||||
with winreg.OpenKey(
|
||||
winreg.HKEY_CURRENT_USER, r"Software\Valve\Steam"
|
||||
) as key:
|
||||
return Path(winreg.QueryValueEx(key, "SteamPath")[0])
|
||||
except Exception as e:
|
||||
print(t("config.steam_path_failed", error=str(e)))
|
||||
print(t("config.continue_partial"))
|
||||
return None
|
||||
24
src/constants.py
Normal file
24
src/constants.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""常量定义"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
LOG_DIR: Path = Path("logs")
|
||||
CONFIG_FILE: Path = 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",
|
||||
"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",
|
||||
]
|
||||
63
src/logger.py
Normal file
63
src/logger.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""日志模块"""
|
||||
|
||||
import sys
|
||||
from loguru import logger
|
||||
|
||||
from .constants import LOG_DIR
|
||||
|
||||
|
||||
class Logger:
|
||||
"""统一的日志管理器"""
|
||||
|
||||
def __init__(self, name: str, debug_mode: bool = False, log_file: bool = True):
|
||||
self.name = name
|
||||
self.debug_mode = debug_mode
|
||||
self.log_file = log_file
|
||||
self._setup_logger()
|
||||
self._logger = logger.bind(name=name)
|
||||
|
||||
def _setup_logger(self):
|
||||
"""设置日志器"""
|
||||
# 移除默认的 handler
|
||||
logger.remove()
|
||||
|
||||
level = "DEBUG" if self.debug_mode else "INFO"
|
||||
|
||||
# 控制台输出
|
||||
logger.add(
|
||||
sys.stderr, format="<level>{message}</level>", level=level, colorize=True
|
||||
)
|
||||
|
||||
if self.log_file:
|
||||
LOG_DIR.mkdir(exist_ok=True)
|
||||
logfile = LOG_DIR / f"{self.name}.log"
|
||||
|
||||
# 文件输出
|
||||
file_format = (
|
||||
"[{time:YYYY-MM-DD HH:mm:ss}] | "
|
||||
"[{extra[name]}:{level}] | "
|
||||
"[{module}.{function}:{line}] - {message}"
|
||||
)
|
||||
|
||||
logger.add(
|
||||
logfile,
|
||||
format=file_format,
|
||||
level=level,
|
||||
encoding="utf-8",
|
||||
filter=lambda record: record["extra"].get("name") == self.name,
|
||||
)
|
||||
|
||||
def debug(self, msg: str):
|
||||
self._logger.opt(depth=1).debug(msg)
|
||||
|
||||
def info(self, msg: str):
|
||||
self._logger.opt(depth=1).info(msg)
|
||||
|
||||
def warning(self, msg: str):
|
||||
self._logger.opt(depth=1).warning(msg)
|
||||
|
||||
def error(self, msg: str):
|
||||
self._logger.opt(depth=1).error(msg)
|
||||
|
||||
def critical(self, msg: str):
|
||||
self._logger.opt(depth=1).critical(msg)
|
||||
223
src/main.py
Normal file
223
src/main.py
Normal file
@@ -0,0 +1,223 @@
|
||||
from typing import List, Dict, Tuple
|
||||
|
||||
import ujson
|
||||
from .constants import STEAM_API_BASE
|
||||
from .config import ConfigManager
|
||||
from .logger import Logger
|
||||
from .models import DepotInfo, ManifestInfo, SteamAppInfo, SteamAppManifestInfo
|
||||
from .network.client import HttpClient
|
||||
from .manifest_handler import ManifestHandler
|
||||
from .utils.i18n import t
|
||||
|
||||
|
||||
class OnekeyApp:
|
||||
"""Onekey主应用"""
|
||||
|
||||
def __init__(self):
|
||||
self.config = ConfigManager()
|
||||
self.logger = Logger(
|
||||
"Onekey",
|
||||
debug_mode=self.config.app_config.debug_mode,
|
||||
log_file=self.config.app_config.logging_files,
|
||||
)
|
||||
self.client = HttpClient()
|
||||
|
||||
async def fetch_key(self) -> bool:
|
||||
"""获取并验证卡密信息"""
|
||||
try:
|
||||
response = await self.client._client.post(
|
||||
f"{STEAM_API_BASE}/getKeyInfo",
|
||||
json={"key": self.config.app_config.key},
|
||||
)
|
||||
body = ujson.loads(await response.content.read())
|
||||
|
||||
if not body["info"]:
|
||||
self.logger.error(t("api.key_not_exist"))
|
||||
return False
|
||||
|
||||
key_type = body["info"]["type"]
|
||||
self.logger.info(t("api.key_type", type=t(f"key_type.{key_type}")))
|
||||
|
||||
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
|
||||
|
||||
async def fetch_app_data(
|
||||
self, app_id: str, and_dlc: bool = True
|
||||
) -> Tuple[SteamAppInfo, SteamAppManifestInfo]:
|
||||
"""
|
||||
从API获取应用数据
|
||||
"""
|
||||
main_app_manifests = []
|
||||
dlc_manifests = []
|
||||
|
||||
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 == 401:
|
||||
self.logger.error(t("api.invalid_key"))
|
||||
return SteamAppInfo(), SteamAppManifestInfo(mainapp=[], dlcs=[])
|
||||
|
||||
if response.status != 200:
|
||||
self.logger.error(t("api.request_failed", code=response.status))
|
||||
return SteamAppInfo(), SteamAppManifestInfo(mainapp=[], dlcs=[])
|
||||
|
||||
data = ujson.loads(await response.content.read())
|
||||
|
||||
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
|
||||
|
||||
tool = SteamTools(self.config.steam_path)
|
||||
success = await tool.setup(depot_data, app_info)
|
||||
elif tool_type == "greenluma":
|
||||
from .tools.greenluma import GreenLuma
|
||||
|
||||
tool = GreenLuma(self.config.steam_path)
|
||||
success = await tool.setup(depot_data, app_id)
|
||||
else:
|
||||
self.logger.error(t("tool.invalid_selection"))
|
||||
return False
|
||||
|
||||
if success:
|
||||
self.logger.info(t("tool.config_success"))
|
||||
self.logger.info(t("tool.restart_steam"))
|
||||
return True
|
||||
else:
|
||||
self.logger.error(t("tool.config_failed"))
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(t("task.run_error", error=str(e)))
|
||||
return False
|
||||
finally:
|
||||
await self.client.close()
|
||||
139
src/manifest_handler.py
Normal file
139
src/manifest_handler.py
Normal file
@@ -0,0 +1,139 @@
|
||||
import io
|
||||
import struct
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
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 == 200:
|
||||
return await r.content.read()
|
||||
except Exception as e:
|
||||
self.logger.debug(t("manifest.download.failed", url=url, error=e))
|
||||
|
||||
@staticmethod
|
||||
def _serialize_manifest_data(content: bytes) -> bytes:
|
||||
magic_signature = struct.pack("<I", 0x71F617D0)
|
||||
payload = content
|
||||
|
||||
if len(content) >= 4 and content[:4] == magic_signature:
|
||||
payload = content[8:]
|
||||
else:
|
||||
try:
|
||||
with zipfile.ZipFile(io.BytesIO(content)) as zf:
|
||||
payload = zf.read("z")
|
||||
except (zipfile.BadZipFile, KeyError):
|
||||
pass
|
||||
|
||||
return magic_signature + struct.pack("<I", len(payload)) + payload
|
||||
|
||||
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
|
||||
|
||||
_ = bytes.fromhex(manifest_info.depot_key)
|
||||
|
||||
serialized_data = self._serialize_manifest_data(manifest_data)
|
||||
|
||||
manifest_path = self.depot_cache / f"{depot_id}_{manifest_id}.manifest"
|
||||
|
||||
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(serialized_data)
|
||||
|
||||
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 = []
|
||||
all_manifests = manifests.mainapp + manifests.dlcs
|
||||
|
||||
for manifest_info in all_manifests:
|
||||
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
|
||||
54
src/models.py
Normal file
54
src/models.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from typing import List
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class DepotInfo:
|
||||
"""仓库信息"""
|
||||
|
||||
depot_id: str
|
||||
decryption_key: str
|
||||
manifest_ids: List[str] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.manifest_ids is None:
|
||||
self.manifest_ids = []
|
||||
|
||||
|
||||
@dataclass
|
||||
class ManifestInfo:
|
||||
"""清单信息"""
|
||||
|
||||
app_id: int
|
||||
depot_id: str
|
||||
depot_key: str
|
||||
manifest_id: str
|
||||
url: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class SteamAppInfo:
|
||||
appId: int
|
||||
name: str
|
||||
dlcCount: int
|
||||
depotCount: int
|
||||
workshopDecryptionKey: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class SteamAppManifestInfo:
|
||||
mainapp: List[ManifestInfo]
|
||||
dlcs: List[ManifestInfo]
|
||||
|
||||
|
||||
@dataclass
|
||||
class AppConfig:
|
||||
"""应用配置"""
|
||||
|
||||
key: str = ""
|
||||
port: int = 5000
|
||||
custom_steam_path: str = ""
|
||||
debug_mode: bool = False
|
||||
logging_files: bool = True
|
||||
show_console: bool = True
|
||||
language: str = "zh"
|
||||
25
src/network/client.py
Normal file
25
src/network/client.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""HTTP客户端模块"""
|
||||
|
||||
import aiohttp
|
||||
from typing import Optional, Dict
|
||||
|
||||
|
||||
class HttpClient:
|
||||
"""HTTP客户端封装"""
|
||||
|
||||
def __init__(self):
|
||||
self._client = aiohttp.ClientSession(conn_timeout=60.0)
|
||||
|
||||
async def get(self, url: str, headers: Optional[Dict] = None) -> aiohttp.ClientResponse:
|
||||
"""GET请求"""
|
||||
return await self._client.get(url, headers=headers)
|
||||
|
||||
async def close(self):
|
||||
"""关闭客户端"""
|
||||
await self._client.close()
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
await self.close()
|
||||
17
src/tools/base.py
Normal file
17
src/tools/base.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
|
||||
from ..models import DepotInfo
|
||||
|
||||
|
||||
class UnlockTool(ABC):
|
||||
"""解锁工具基类"""
|
||||
|
||||
def __init__(self, steam_path: Path):
|
||||
self.steam_path = steam_path
|
||||
|
||||
@abstractmethod
|
||||
async def setup(self, depot_data: List[DepotInfo], app_id: str, **kwargs) -> bool:
|
||||
"""设置解锁"""
|
||||
pass
|
||||
74
src/tools/greenluma.py
Normal file
74
src/tools/greenluma.py
Normal file
@@ -0,0 +1,74 @@
|
||||
import vdf
|
||||
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:
|
||||
applist_dir = self.steam_path / "AppList"
|
||||
|
||||
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:
|
||||
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 as e:
|
||||
print(f"GreenLuma配置失败: {e}")
|
||||
return False
|
||||
42
src/tools/steamtools.py
Normal file
42
src/tools/steamtools.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
|
||||
from .base import UnlockTool
|
||||
from ..models import DepotInfo, SteamAppInfo
|
||||
|
||||
|
||||
class SteamTools(UnlockTool):
|
||||
"""SteamTools解锁工具实现"""
|
||||
|
||||
async def setup(
|
||||
self,
|
||||
depot_data: List[DepotInfo],
|
||||
app_info: SteamAppInfo,
|
||||
) -> bool:
|
||||
"""设置SteamTools解锁"""
|
||||
st_path = self.steam_path / "config" / "stplug-in"
|
||||
st_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
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:
|
||||
lua_content += (
|
||||
f'addappid({depot.depot_id}, "1", "{depot.decryption_key}")\n'
|
||||
)
|
||||
|
||||
lua_file = st_path / f"{app_info.appId}.lua"
|
||||
lua_file.write_text(lua_content, encoding="utf-8")
|
||||
|
||||
return True
|
||||
241
src/utils/i18n.py
Normal file
241
src/utils/i18n.py
Normal file
@@ -0,0 +1,241 @@
|
||||
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.show_window": "显示程序",
|
||||
"tray.show_console": "显示控制台",
|
||||
"tray.exit": "退出程序",
|
||||
# 主程序
|
||||
"main.starting": "正在启动Onekey...",
|
||||
"main.tray_created": "系统托盘已创建",
|
||||
"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.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.show_window": "Show Window",
|
||||
"tray.show_console": "Show Console",
|
||||
"tray.exit": "Exit",
|
||||
# Main program
|
||||
"main.starting": "Starting Onekey...",
|
||||
"main.tray_created": "System tray created",
|
||||
"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.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)
|
||||
17
src/utils/steam.py
Normal file
17
src/utils/steam.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from typing import Optional, Tuple
|
||||
|
||||
|
||||
def parse_manifest_filename(filename: str) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""解析清单文件名"""
|
||||
if not filename.endswith(".manifest"):
|
||||
return None, None
|
||||
|
||||
name = filename.replace(".manifest", "")
|
||||
if "_" not in name:
|
||||
return None, None
|
||||
|
||||
parts = name.split("_", 1)
|
||||
if len(parts) != 2 or not all(p.isdigit() for p in parts):
|
||||
return None, None
|
||||
|
||||
return parts[0], parts[1]
|
||||
0
web/__init__.py
Normal file
0
web/__init__.py
Normal file
422
web/app.py
Normal file
422
web/app.py
Normal file
@@ -0,0 +1,422 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import ujson
|
||||
import aiohttp
|
||||
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
|
||||
|
||||
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:
|
||||
|
||||
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:
|
||||
def __init__(self, manager: ConnectionManager):
|
||||
self.onekey_app = None
|
||||
self.current_task = None
|
||||
self.task_status = "idle"
|
||||
self.task_progress = []
|
||||
self.task_result = None
|
||||
self.manager = manager
|
||||
|
||||
def init_app(self):
|
||||
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(
|
||||
ujson.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(
|
||||
ujson.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(
|
||||
ujson.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
|
||||
|
||||
|
||||
app = FastAPI(title="Onekey")
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
manager = ConnectionManager()
|
||||
|
||||
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_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_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 []),
|
||||
"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 ujson
|
||||
|
||||
config_path = config_manager.config_path
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
ujson.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 ujson
|
||||
|
||||
config_manager = ConfigManager()
|
||||
config_path = config_manager.config_path
|
||||
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
ujson.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 aiohttp.ClientSession(conn_timeout=10.0) as client:
|
||||
response = await client.post(
|
||||
url=f"{STEAM_API_BASE}/getKeyInfo",
|
||||
json={"key": key},
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
if response.status == 200:
|
||||
result = ujson.loads(await response.content.read())
|
||||
return JSONResponse(result)
|
||||
else:
|
||||
return JSONResponse({"success": False, "message": "卡密验证服务不可用"})
|
||||
except aiohttp.ConnectionTimeoutError:
|
||||
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(
|
||||
ujson.dumps({"type": "connected", "data": {"message": "已连接到服务器"}})
|
||||
)
|
||||
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
message = ujson.loads(data)
|
||||
if message.get("type") == "ping":
|
||||
await websocket.send_text(
|
||||
ujson.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"))
|
||||
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.2</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 • AIOHTTP</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.2</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 • AIOHTTP</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