mirror of
https://github.com/ikunshare/Onekey.git
synced 2026-01-12 16:25:53 +08:00
Initial project setup and source code import
Add project files including Python source code, web assets, configuration, and CI/CD workflows. Includes main application logic, web interface, supporting modules, and documentation for the Onekey Steam Depot Manifest Downloader.
This commit is contained in:
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
||||||
58
.github/workflows/dev.yml
vendored
Normal file
58
.github/workflows/dev.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
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@v3
|
||||||
|
with:
|
||||||
|
python-version: 3.13.1
|
||||||
|
|
||||||
|
- name: 安装依赖
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install imageio
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
- name: 编译
|
||||||
|
uses: Nuitka/Nuitka-Action@main
|
||||||
|
with:
|
||||||
|
nuitka-version: main
|
||||||
|
script-name: main.py
|
||||||
|
mode: onefile
|
||||||
|
show-memory: true
|
||||||
|
onefile-tempdir-spec: "%TEMP%\\onekey_%PID%_%TIME%"
|
||||||
|
windows-icon-from-ico: icon.jpg
|
||||||
|
company-name: "ikunshare"
|
||||||
|
product-name: "Onekey"
|
||||||
|
include-data-dir: |
|
||||||
|
./web/templates=web/templates
|
||||||
|
./web/static=web/static
|
||||||
|
file-version: ${{ env.PACKAGE_VERSION }}
|
||||||
|
product-version: ${{ env.PACKAGE_VERSION }}
|
||||||
|
file-description: "Onekey Depot Manifest Downloader."
|
||||||
|
copyright: "Copyright © 2025 ikunshare All Rights Reserved."
|
||||||
|
output-file: Onekey_v${{ env.PACKAGE_VERSION }}.exe
|
||||||
|
assume-yes-for-downloads: true
|
||||||
|
output-dir: build
|
||||||
|
|
||||||
|
- name: 上传包
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: Onekey_v${{ env.PACKAGE_VERSION }}.exe
|
||||||
|
path: build/Onekey_v${{ env.PACKAGE_VERSION }}.exe
|
||||||
92
.github/workflows/release.yml
vendored
Normal file
92
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
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@v3
|
||||||
|
with:
|
||||||
|
python-version: 3.13.1
|
||||||
|
|
||||||
|
- name: 安装依赖
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install imageio
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
- name: 编译
|
||||||
|
uses: Nuitka/Nuitka-Action@main
|
||||||
|
with:
|
||||||
|
nuitka-version: main
|
||||||
|
script-name: main.py
|
||||||
|
mode: onefile
|
||||||
|
show-memory: true
|
||||||
|
onefile-tempdir-spec: "%TEMP%\\onekey_%PID%_%TIME%"
|
||||||
|
windows-icon-from-ico: icon.jpg
|
||||||
|
company-name: "ikunshare"
|
||||||
|
product-name: "Onekey"
|
||||||
|
include-data-dir: |
|
||||||
|
./web/templates=web/templates
|
||||||
|
./web/static=web/static
|
||||||
|
file-version: ${{ env.PACKAGE_VERSION }}
|
||||||
|
product-version: ${{ env.PACKAGE_VERSION }}
|
||||||
|
file-description: "Onekey Depot Manifest Downloader."
|
||||||
|
copyright: "Copyright © 2025 ikunshare All Rights Reserved."
|
||||||
|
output-file: Onekey_v${{ env.PACKAGE_VERSION }}.exe
|
||||||
|
assume-yes-for-downloads: true
|
||||||
|
output-dir: build
|
||||||
|
|
||||||
|
- name: 创建标签
|
||||||
|
uses: pkgdeps/git-tag-action@v3
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GH_TOKEN }}
|
||||||
|
github_repo: ikunshare/Onekey
|
||||||
|
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
|
||||||
|
repository: ikunshare/Onekey
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GH_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
|
||||||
171
.gitignore
vendored
Normal file
171
.gitignore
vendored
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
Pipfile.lock
|
||||||
|
|
||||||
|
# PEP 582
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# 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.
|
||||||
83
README.md
83
README.md
@@ -1,4 +1,85 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

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