mirror of
https://github.com/ikunshare/Onekey.git
synced 2026-01-12 16:25:53 +08:00
新增多语言支持并重构前端资源结构
引入英文和中文多语言支持,web 目录下静态资源和模板按语言分目录存放。新增 src/utils/i18n.py 实现国际化,main.py 增加多语言错误提示。CI/CD 工作流升级 Python 版本与 Nuitka 编译方式,提升兼容性和构建效率。
This commit is contained in:
27
.github/workflows/dev.yml
vendored
27
.github/workflows/dev.yml
vendored
@@ -19,37 +19,20 @@ jobs:
|
||||
echo "PACKAGE_VERSION=$version" >> $env:GITHUB_ENV
|
||||
|
||||
- name: 安装Python
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.13.1
|
||||
python-version: "3.11"
|
||||
|
||||
- name: 安装依赖
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install imageio
|
||||
pip install -r requirements.txt
|
||||
pip install nuitka
|
||||
|
||||
- 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
|
||||
run: |
|
||||
python -m nuitka --standalone --onefile --assume-yes-for-downloads --show-memory --show-progress --onefile-tempdir-spec="%TEMP%\\onekey_%PID%_%TIME%" --windows-icon-from-ico="icon.jpg" --company-name="ikunshare" --product-name="Onekey" --file-version="${{ env.PACKAGE_VERSION }}" --product-version="${{ env.PACKAGE_VERSION }}" --file-description="Onekey Depot Manifest Downloader." --copyright="Copyright © 2025 ikunshare All Rights Reserved." --include-data-dir="web=web" --output-dir="build" --output-filename="Onekey_v${{ env.PACKAGE_VERSION }}.exe" main.py
|
||||
|
||||
- name: 上传包
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
27
.github/workflows/release.yml
vendored
27
.github/workflows/release.yml
vendored
@@ -19,37 +19,20 @@ jobs:
|
||||
echo "PACKAGE_VERSION=$version" >> $env:GITHUB_ENV
|
||||
|
||||
- name: 安装Python
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.13.1
|
||||
python-version: "3.11"
|
||||
|
||||
- name: 安装依赖
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install imageio
|
||||
pip install -r requirements.txt
|
||||
pip install nuitka
|
||||
|
||||
- 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
|
||||
run: |
|
||||
python -m nuitka --standalone --onefile --assume-yes-for-downloads --show-memory --show-progress --onefile-tempdir-spec="%TEMP%\\onekey_%PID%_%TIME%" --windows-icon-from-ico="icon.jpg" --company-name="ikunshare" --product-name="Onekey" --file-version="${{ env.PACKAGE_VERSION }}" --product-version="${{ env.PACKAGE_VERSION }}" --file-description="Onekey Depot Manifest Downloader." --copyright="Copyright © 2025 ikunshare All Rights Reserved." --include-data-dir="web=web" --output-dir="build" --output-filename="Onekey_v${{ env.PACKAGE_VERSION }}.exe" main.py
|
||||
|
||||
- name: 创建标签
|
||||
uses: pkgdeps/git-tag-action@v3
|
||||
|
||||
@@ -47,7 +47,6 @@ git clone https://github.com/ikunshare/Onekey
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
|
||||
## Star 趋势图
|
||||
|
||||
[](https://starchart.cc/ikunshare/Onekey)
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Dict, Optional
|
||||
|
||||
from .constants import CONFIG_FILE
|
||||
from .models import AppConfig
|
||||
from .utils.i18n import t
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"KEY": "",
|
||||
@@ -14,6 +15,7 @@ DEFAULT_CONFIG = {
|
||||
"Logging_Files": True,
|
||||
"Show_Console": False,
|
||||
"Custom_Steam_Path": "",
|
||||
"Language": "zh",
|
||||
}
|
||||
|
||||
|
||||
@@ -32,11 +34,11 @@ class ConfigManager:
|
||||
try:
|
||||
with open(self.config_path, "w", encoding="utf-8") as f:
|
||||
json.dump(DEFAULT_CONFIG, f, indent=2, ensure_ascii=False)
|
||||
print("配置文件已生成")
|
||||
print(t("config.generated"))
|
||||
os.system("pause")
|
||||
sys.exit(1)
|
||||
except IOError as e:
|
||||
print(f"配置文件创建失败: {str(e)}")
|
||||
print(t("config.create_failed", error=str(e)))
|
||||
os.system("pause")
|
||||
sys.exit(1)
|
||||
|
||||
@@ -55,37 +57,40 @@ class ConfigManager:
|
||||
debug_mode=self._config_data.get("Debug_Mode", False),
|
||||
logging_files=self._config_data.get("Logging_Files", True),
|
||||
show_console=self._config_data.get("Show_Console", True),
|
||||
language=self._config_data.get("Language", "zh"),
|
||||
)
|
||||
|
||||
self.steam_path = self._get_steam_path()
|
||||
except json.JSONDecodeError:
|
||||
print("配置文件损坏,正在重新生成...")
|
||||
print(t("config.corrupted"))
|
||||
self._generate_config()
|
||||
print("配置文件已重新生成,使用默认配置继续运行")
|
||||
print(t("config.regenerated"))
|
||||
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),
|
||||
language=DEFAULT_CONFIG.get("Language", "zh"),
|
||||
)
|
||||
try:
|
||||
self.steam_path = self._get_steam_path()
|
||||
except:
|
||||
except Exception:
|
||||
self.steam_path = None
|
||||
except Exception as e:
|
||||
print(f"配置加载失败: {str(e)}")
|
||||
print("使用默认配置继续运行")
|
||||
print(t("config.load_failed", error=str(e)))
|
||||
print(t("config.use_default"))
|
||||
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),
|
||||
language=DEFAULT_CONFIG.get("Language", "zh"),
|
||||
)
|
||||
try:
|
||||
self.steam_path = self._get_steam_path()
|
||||
except:
|
||||
except Exception:
|
||||
self.steam_path = None
|
||||
|
||||
def _get_steam_path(self) -> Optional[Path]:
|
||||
@@ -99,6 +104,6 @@ class ConfigManager:
|
||||
) as key:
|
||||
return Path(winreg.QueryValueEx(key, "SteamPath")[0])
|
||||
except Exception as e:
|
||||
print(f"Steam路径获取失败: {str(e)}")
|
||||
print("程序将继续运行,但部分功能可能不可用")
|
||||
print(t("config.steam_path_failed", error=str(e)))
|
||||
print(t("config.continue_partial"))
|
||||
return None
|
||||
|
||||
106
src/main.py
106
src/main.py
@@ -5,6 +5,7 @@ from .logger import Logger
|
||||
from .models import DepotInfo, ManifestInfo, SteamAppInfo, SteamAppManifestInfo
|
||||
from .network.client import HttpClient
|
||||
from .manifest_handler import ManifestHandler
|
||||
from .utils.i18n import t
|
||||
|
||||
|
||||
class OnekeyApp:
|
||||
@@ -19,28 +20,27 @@ class OnekeyApp:
|
||||
)
|
||||
self.client = HttpClient()
|
||||
|
||||
async def fetch_key(self):
|
||||
trans = {
|
||||
"week": "周卡",
|
||||
"month": "月卡",
|
||||
"year": "年卡",
|
||||
"permanent": "永久卡",
|
||||
}
|
||||
async def fetch_key(self) -> bool:
|
||||
"""获取并验证卡密信息"""
|
||||
try:
|
||||
response = await self.client._client.post(
|
||||
f"{STEAM_API_BASE}/getKeyInfo",
|
||||
json={"key": self.config.app_config.key},
|
||||
)
|
||||
body = response.json()
|
||||
|
||||
if not body["info"]:
|
||||
self.logger.error("卡密不存在")
|
||||
self.logger.error(t("api.key_not_exist"))
|
||||
return False
|
||||
self.logger.info(f"卡密类型: {trans[body['info']['type']]}")
|
||||
if trans[body["info"]["type"]] != "永久卡":
|
||||
self.logger.info(f"卡密过期时间: {body['info']['expiresAt']}")
|
||||
|
||||
key_type = body["info"]["type"]
|
||||
self.logger.info(t("api.key_type", type=t(f"key_type.{key_type}")))
|
||||
|
||||
if key_type != "permanent":
|
||||
self.logger.info(t("api.key_expires", time=body["info"]["expiresAt"]))
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"获取卡密信息失败: {str(e)}")
|
||||
self.logger.error(t("api.key_info_failed", error=str(e)))
|
||||
return True
|
||||
|
||||
async def fetch_app_data(
|
||||
@@ -53,7 +53,7 @@ class OnekeyApp:
|
||||
dlc_manifests = []
|
||||
|
||||
try:
|
||||
self.logger.info(f"正在获取游戏 {app_id} 的信息...")
|
||||
self.logger.info(t("api.fetching_game", app_id=app_id))
|
||||
|
||||
response = await self.client._client.post(
|
||||
f"{STEAM_API_BASE}/getGame",
|
||||
@@ -62,24 +62,26 @@ class OnekeyApp:
|
||||
)
|
||||
|
||||
if response.status_code == 401:
|
||||
self.logger.error("API密钥无效")
|
||||
return []
|
||||
self.logger.error(t("api.invalid_key"))
|
||||
return SteamAppInfo(), SteamAppManifestInfo(mainapp=[], dlcs=[])
|
||||
|
||||
if response.status_code != 200:
|
||||
self.logger.error(f"API请求失败,状态码: {response.status_code}")
|
||||
return []
|
||||
self.logger.error(t("api.request_failed", code=response.status_code))
|
||||
return SteamAppInfo(), SteamAppManifestInfo(mainapp=[], dlcs=[])
|
||||
|
||||
data = response.json()
|
||||
|
||||
if not data:
|
||||
self.logger.error("未找到此游戏的清单信息")
|
||||
return []
|
||||
self.logger.error(t("api.no_manifest"))
|
||||
return SteamAppInfo(), SteamAppManifestInfo(mainapp=[], dlcs=[])
|
||||
|
||||
self.logger.info(f"游戏名称: {data.get('name', 'Unknown')}")
|
||||
self.logger.info(f"Depot数量: {data.get('depotCount', 'Unknown')}")
|
||||
self.logger.info(t("api.game_name", name=data.get("name", "Unknown")))
|
||||
self.logger.info(
|
||||
t("api.depot_count", count=data.get("depotCount", "Unknown"))
|
||||
)
|
||||
|
||||
if and_dlc:
|
||||
for item in data["gameManifests"]:
|
||||
for item in data.get("gameManifests", []):
|
||||
manifest = ManifestInfo(
|
||||
app_id=item["app_id"],
|
||||
depot_id=item["depot_id"],
|
||||
@@ -89,10 +91,14 @@ class OnekeyApp:
|
||||
)
|
||||
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"]:
|
||||
for item in data.get("dlcManifests", []):
|
||||
self.logger.info(
|
||||
t("api.dlc_name", name=item.get("dlcName", "Unknown"))
|
||||
)
|
||||
self.logger.info(
|
||||
t("api.dlc_appid", id=item.get("dlcId", "Unknown"))
|
||||
)
|
||||
for manifests in item.get("manifests", []):
|
||||
manifest = ManifestInfo(
|
||||
app_id=manifests["app_id"],
|
||||
depot_id=manifests["depot_id"],
|
||||
@@ -102,7 +108,7 @@ class OnekeyApp:
|
||||
)
|
||||
dlc_manifests.append(manifest)
|
||||
else:
|
||||
for item in data["manifests"]:
|
||||
for item in data.get("manifests", []):
|
||||
manifest = ManifestInfo(
|
||||
app_id=item["app_id"],
|
||||
depot_id=item["depot_id"],
|
||||
@@ -111,24 +117,26 @@ class OnekeyApp:
|
||||
url=item["url"],
|
||||
)
|
||||
main_app_manifests.append(manifest)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"获取主游戏信息失败: {str(e)}")
|
||||
return SteamAppManifestInfo(mainapp=[], dlcs=[])
|
||||
self.logger.error(t("api.fetch_failed", error=str(e)))
|
||||
return SteamAppInfo(), SteamAppManifestInfo(mainapp=[], dlcs=[])
|
||||
|
||||
return SteamAppInfo(
|
||||
app_id,
|
||||
data["name"],
|
||||
data.get("name", ""),
|
||||
data.get("totalDLCCount", data.get("dlcCount", 0)),
|
||||
data["depotCount"],
|
||||
data.get("depotCount", 0),
|
||||
data.get("workshopDecryptionKey", "None"),
|
||||
), SteamAppManifestInfo(mainapp=main_app_manifests, dlcs=dlc_manifests)
|
||||
|
||||
def prepare_depot_data(
|
||||
self, manifests: List[ManifestInfo]
|
||||
) -> tuple[List[DepotInfo], Dict[str, List[str]]]:
|
||||
) -> 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] = {
|
||||
@@ -147,34 +155,43 @@ class OnekeyApp:
|
||||
|
||||
return depot_data, depot_dict
|
||||
|
||||
async def run(self, app_id: str, tool_type: str, dlc: bool):
|
||||
async def run(self, app_id: str, tool_type: str, dlc: bool) -> bool:
|
||||
"""
|
||||
为Web版本提供的运行方法。
|
||||
|
||||
Args:
|
||||
app_id: Steam应用ID
|
||||
tool_type: 解锁工具类型 (steamtools/greenluma)
|
||||
dlc: 是否包含DLC
|
||||
|
||||
Returns:
|
||||
是否成功执行
|
||||
"""
|
||||
try:
|
||||
if not self.config.steam_path:
|
||||
self.logger.error("Steam路径未配置或无效,无法继续")
|
||||
self.logger.error(t("task.no_steam_path"))
|
||||
return False
|
||||
|
||||
await self.fetch_key()
|
||||
|
||||
manifests = []
|
||||
|
||||
app_info, manifests = await self.fetch_app_data(app_id, dlc)
|
||||
|
||||
if not manifests:
|
||||
if not manifests.mainapp and not manifests.dlcs:
|
||||
return False
|
||||
|
||||
manifest_handler = ManifestHandler(
|
||||
self.client, self.logger, self.config.steam_path
|
||||
)
|
||||
|
||||
processed_manifests = await manifest_handler.process_manifests(manifests)
|
||||
|
||||
if not processed_manifests:
|
||||
self.logger.error("没有成功处理的清单")
|
||||
self.logger.error(t("task.no_manifest_processed"))
|
||||
return False
|
||||
|
||||
depot_data, _ = self.prepare_depot_data(processed_manifests)
|
||||
self.logger.info(f"选择的解锁工具: {tool_type}")
|
||||
self.logger.info(t("tool.selected", tool=tool_type))
|
||||
|
||||
if tool_type == "steamtools":
|
||||
from .tools.steamtools import SteamTools
|
||||
|
||||
@@ -186,18 +203,19 @@ class OnekeyApp:
|
||||
tool = GreenLuma(self.config.steam_path)
|
||||
success = await tool.setup(depot_data, app_id)
|
||||
else:
|
||||
self.logger.error("无效的工具选择")
|
||||
self.logger.error(t("tool.invalid_selection"))
|
||||
return False
|
||||
|
||||
if success:
|
||||
self.logger.info("游戏解锁配置成功!")
|
||||
self.logger.info("重启Steam后生效")
|
||||
self.logger.info(t("tool.config_success"))
|
||||
self.logger.info(t("tool.restart_steam"))
|
||||
return True
|
||||
else:
|
||||
self.logger.error("配置失败")
|
||||
self.logger.error(t("tool.config_failed"))
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"运行错误: {e}")
|
||||
self.logger.error(t("task.run_error", error=str(e)))
|
||||
return False
|
||||
finally:
|
||||
await self.client.close()
|
||||
|
||||
@@ -6,6 +6,7 @@ from .constants import STEAM_CACHE_CDN_LIST
|
||||
from .models import ManifestInfo, SteamAppManifestInfo
|
||||
from .logger import Logger
|
||||
from .network.client import HttpClient
|
||||
from .utils.i18n import t
|
||||
|
||||
|
||||
class ManifestHandler:
|
||||
@@ -28,7 +29,7 @@ class ManifestHandler:
|
||||
if r.status_code == 200:
|
||||
return r.content
|
||||
except Exception as e:
|
||||
self.logger.debug(f"从 {url} 下载失败: {str(e)}")
|
||||
self.logger.debug(t("manifest.download.failed", url=url, error=e))
|
||||
|
||||
def process_manifest(
|
||||
self, manifest_data: bytes, manifest_info: ManifestInfo, remove_old: bool = True
|
||||
@@ -62,7 +63,7 @@ class ManifestHandler:
|
||||
and parts[1] != str(manifest_id)
|
||||
):
|
||||
file.unlink(missing_ok=True)
|
||||
self.logger.info(f"删除旧清单: {file.name}")
|
||||
self.logger.info(t("manifest.delete_old", name=file.name))
|
||||
|
||||
with open(manifest_path, "wb") as f:
|
||||
f.write(manifest.serialize(compress=False))
|
||||
@@ -70,11 +71,17 @@ class ManifestHandler:
|
||||
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")
|
||||
self.logger.info(
|
||||
t(
|
||||
"manifest.process.success",
|
||||
depot_id=depot_id,
|
||||
manifest_id=manifest_id,
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"处理清单时出错: {str(e)}")
|
||||
self.logger.error(t("manifest.process.failed", error=e))
|
||||
return False
|
||||
|
||||
async def process_manifests(
|
||||
@@ -93,12 +100,16 @@ class ManifestHandler:
|
||||
)
|
||||
|
||||
if manifest_path.exists():
|
||||
self.logger.warning(f"清单已存在: {manifest_path.name}")
|
||||
self.logger.warning(t("manifest.exists", name=manifest_path.name))
|
||||
processed.append(manifest_info)
|
||||
continue
|
||||
|
||||
self.logger.info(
|
||||
f"正在下载清单: {manifest_info.depot_id}_{manifest_info.manifest_id}"
|
||||
t(
|
||||
"manifest.downloading",
|
||||
depot_id=manifest_info.depot_id,
|
||||
manifest_id=manifest_info.manifest_id,
|
||||
)
|
||||
)
|
||||
manifest_data = await self.download_manifest(manifest_info)
|
||||
|
||||
@@ -107,11 +118,19 @@ class ManifestHandler:
|
||||
processed.append(manifest_info)
|
||||
else:
|
||||
self.logger.error(
|
||||
f"处理清单失败: {manifest_info.depot_id}_{manifest_info.manifest_id}"
|
||||
t(
|
||||
"manifest.downloading.failed",
|
||||
depot_id=manifest_info.depot_id,
|
||||
manifest_id=manifest_info.manifest_id,
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.logger.error(
|
||||
f"下载清单失败: {manifest_info.depot_id}_{manifest_info.manifest_id}"
|
||||
t(
|
||||
"manifest.process.failed2",
|
||||
depot_id=manifest_info.depot_id,
|
||||
manifest_id=manifest_info.manifest_id,
|
||||
)
|
||||
)
|
||||
|
||||
return processed
|
||||
|
||||
@@ -50,3 +50,4 @@ class AppConfig:
|
||||
debug_mode: bool = False
|
||||
logging_files: bool = True
|
||||
show_console: bool = True
|
||||
language: str = "zh"
|
||||
|
||||
243
src/utils/i18n.py
Normal file
243
src/utils/i18n.py
Normal file
@@ -0,0 +1,243 @@
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class I18n:
|
||||
"""国际化管理类"""
|
||||
|
||||
def __init__(self, default_lang: str = "zh"):
|
||||
self.current_lang = default_lang
|
||||
self.translations: Dict[str, Dict[str, str]] = {}
|
||||
self._load_translations()
|
||||
|
||||
def _load_translations(self):
|
||||
"""加载所有语言的翻译"""
|
||||
# 中文翻译
|
||||
self.translations["zh"] = {
|
||||
# 系统托盘
|
||||
"tray.open_browser": "打开浏览器",
|
||||
"tray.show_console": "显示控制台",
|
||||
"tray.exit": "退出程序",
|
||||
# 主程序
|
||||
"main.starting": "正在启动Onekey...",
|
||||
"main.tray_created": "系统托盘已创建",
|
||||
"main.browser_opened": "浏览器已自动打开",
|
||||
"main.browser_open_failed": "无法自动打开浏览器,请手动访问: http://localhost:5000",
|
||||
"main.exit": "程序已退出",
|
||||
"main.start_error": "启动错误: {error}",
|
||||
"main.press_enter": "按回车键退出...",
|
||||
"main.startup_failed": "启动失败: {error}",
|
||||
# 配置
|
||||
"config.generated": "配置文件已生成",
|
||||
"config.create_failed": "配置文件创建失败: {error}",
|
||||
"config.corrupted": "配置文件损坏,正在重新生成...",
|
||||
"config.regenerated": "配置文件已重新生成,使用默认配置继续运行",
|
||||
"config.load_failed": "配置加载失败: {error}",
|
||||
"config.use_default": "使用默认配置继续运行",
|
||||
"config.steam_path_failed": "Steam路径获取失败: {error}",
|
||||
"config.continue_partial": "程序将继续运行,但部分功能可能不可用",
|
||||
# API相关
|
||||
"api.key_not_exist": "卡密不存在",
|
||||
"api.key_type": "卡密类型: {type}",
|
||||
"api.key_expires": "卡密过期时间: {time}",
|
||||
"api.key_info_failed": "获取卡密信息失败: {error}",
|
||||
"api.fetching_game": "正在获取游戏 {app_id} 的信息...",
|
||||
"api.invalid_key": "API密钥无效",
|
||||
"api.request_failed": "API请求失败,状态码: {code}",
|
||||
"api.no_manifest": "未找到此游戏的清单信息",
|
||||
"api.game_name": "游戏名称: {name}",
|
||||
"api.depot_count": "Depot数量: {count}",
|
||||
"api.dlc_name": "DLC名称: {name}",
|
||||
"api.dlc_appid": "DLC AppId: {id}",
|
||||
"api.fetch_failed": "获取主游戏信息失败: {error}",
|
||||
# 工具相关
|
||||
"tool.selected": "选择的解锁工具: {tool}",
|
||||
"tool.invalid_selection": "无效的工具选择",
|
||||
"tool.config_success": "游戏解锁配置成功!",
|
||||
"tool.restart_steam": "重启Steam后生效",
|
||||
"tool.config_failed": "配置失败",
|
||||
# 任务相关
|
||||
"task.no_steam_path": "Steam路径未配置或无效,无法继续",
|
||||
"task.no_manifest_processed": "没有成功处理的清单",
|
||||
"task.run_error": "运行错误: {error}",
|
||||
# 卡密类型
|
||||
"key_type.week": "周卡",
|
||||
"key_type.month": "月卡",
|
||||
"key_type.year": "年卡",
|
||||
"key_type.permanent": "永久卡",
|
||||
# 错误消息
|
||||
"error.load_icon": "加载图标失败: {error}",
|
||||
"error.import": "导入错误: {error}",
|
||||
"error.ensure_root": "请确保在项目根目录中运行此程序",
|
||||
# Web相关
|
||||
"web.starting": "启动Onekey Web GUI...",
|
||||
"web.visit": "请在浏览器中访问: http://localhost:5000",
|
||||
"web.task_running": "已有任务正在运行",
|
||||
"web.invalid_appid": "请输入有效的App ID",
|
||||
"web.invalid_format": "App ID格式无效",
|
||||
"web.task_started": "任务已开始",
|
||||
"web.task_failed": "任务执行失败: {error}",
|
||||
"web.config_saved": "配置已保存",
|
||||
"web.config_save_failed": "保存配置失败: {error}",
|
||||
"web.config_reset": "配置已重置为默认值",
|
||||
"web.config_reset_failed": "重置配置失败: {error}",
|
||||
"web.key_empty": "卡密不能为空",
|
||||
"web.key_service_unavailable": "卡密验证服务不可用",
|
||||
"web.verify_timeout": "验证超时,请检查网络连接",
|
||||
"web.verify_failed": "验证失败: {error}",
|
||||
"web.connected": "已连接到服务器",
|
||||
"web.client_disconnected": "客户端断开连接",
|
||||
"web.websocket_error": "WebSocket 错误: {error}",
|
||||
"web.invalid_config_data": "无效的配置数据",
|
||||
# 清单处理
|
||||
"manifest.download.failed": "从 {url} 下载失败: {error}",
|
||||
"manifest.delete_old": "删除旧清单: {name}",
|
||||
"manifest.process.success": "清单处理成功: {depot_id}_{manifest_id}.manifest",
|
||||
"manifest.process.failed": "处理清单时出错: {error}",
|
||||
"manifest.process.failed2": "处理清单失败: {depot_id}_{manifest_id}",
|
||||
"manifest.exists": "清单已存在: {name}",
|
||||
"manifest.downloading": "正在下载清单: {depot_id}_{manifest_id}",
|
||||
"manifest.downloading.failed": "下载清单失败: {depot_id}_{manifest_id}",
|
||||
}
|
||||
|
||||
# 英文翻译
|
||||
self.translations["en"] = {
|
||||
# System tray
|
||||
"tray.open_browser": "Open Browser",
|
||||
"tray.show_console": "Show Console",
|
||||
"tray.exit": "Exit",
|
||||
# Main program
|
||||
"main.starting": "Starting Onekey...",
|
||||
"main.tray_created": "System tray created",
|
||||
"main.browser_opened": "Browser opened automatically",
|
||||
"main.browser_open_failed": "Failed to open browser automatically, please visit: http://localhost:5000",
|
||||
"main.exit": "Program exited",
|
||||
"main.start_error": "Startup error: {error}",
|
||||
"main.press_enter": "Press Enter to exit...",
|
||||
"main.startup_failed": "Startup failed: {error}",
|
||||
# Configuration
|
||||
"config.generated": "Configuration file generated",
|
||||
"config.create_failed": "Failed to create configuration file: {error}",
|
||||
"config.corrupted": "Configuration file corrupted, regenerating...",
|
||||
"config.regenerated": "Configuration file regenerated, continuing with default config",
|
||||
"config.load_failed": "Failed to load configuration: {error}",
|
||||
"config.use_default": "Continuing with default configuration",
|
||||
"config.steam_path_failed": "Failed to get Steam path: {error}",
|
||||
"config.continue_partial": "Program will continue but some features may be unavailable",
|
||||
# API related
|
||||
"api.key_not_exist": "Key does not exist",
|
||||
"api.key_type": "Key type: {type}",
|
||||
"api.key_expires": "Key expires at: {time}",
|
||||
"api.key_info_failed": "Failed to get key info: {error}",
|
||||
"api.fetching_game": "Fetching game {app_id} information...",
|
||||
"api.invalid_key": "Invalid API key",
|
||||
"api.request_failed": "API request failed with status code: {code}",
|
||||
"api.no_manifest": "No manifest found for this game",
|
||||
"api.game_name": "Game name: {name}",
|
||||
"api.depot_count": "Depot count: {count}",
|
||||
"api.dlc_name": "DLC name: {name}",
|
||||
"api.dlc_appid": "DLC AppId: {id}",
|
||||
"api.fetch_failed": "Failed to fetch main game info: {error}",
|
||||
# Tool related
|
||||
"tool.selected": "Selected unlock tool: {tool}",
|
||||
"tool.invalid_selection": "Invalid tool selection",
|
||||
"tool.config_success": "Game unlock configuration successful!",
|
||||
"tool.restart_steam": "Restart Steam to take effect",
|
||||
"tool.config_failed": "Configuration failed",
|
||||
# Task related
|
||||
"task.no_steam_path": "Steam path not configured or invalid, cannot continue",
|
||||
"task.no_manifest_processed": "No manifests successfully processed",
|
||||
"task.run_error": "Run error: {error}",
|
||||
# Key types
|
||||
"key_type.week": "Weekly",
|
||||
"key_type.month": "Monthly",
|
||||
"key_type.year": "Yearly",
|
||||
"key_type.permanent": "Permanent",
|
||||
# Error messages
|
||||
"error.load_icon": "Failed to load icon: {error}",
|
||||
"error.import": "Import error: {error}",
|
||||
"error.ensure_root": "Please ensure running this program from project root",
|
||||
# Web related
|
||||
"web.starting": "Starting Onekey Web GUI...",
|
||||
"web.visit": "Please visit: http://localhost:5000",
|
||||
"web.task_running": "A task is already running",
|
||||
"web.invalid_appid": "Please enter a valid App ID",
|
||||
"web.invalid_format": "Invalid App ID format",
|
||||
"web.task_started": "Task started",
|
||||
"web.task_failed": "Task execution failed: {error}",
|
||||
"web.config_saved": "Configuration saved",
|
||||
"web.config_save_failed": "Failed to save configuration: {error}",
|
||||
"web.config_reset": "Configuration reset to default",
|
||||
"web.config_reset_failed": "Failed to reset configuration: {error}",
|
||||
"web.key_empty": "Key cannot be empty",
|
||||
"web.key_service_unavailable": "Key verification service unavailable",
|
||||
"web.verify_timeout": "Verification timeout, please check network connection",
|
||||
"web.verify_failed": "Verification failed: {error}",
|
||||
"web.connected": "Connected to server",
|
||||
"web.client_disconnected": "Client disconnected",
|
||||
"web.websocket_error": "WebSocket error: {error}",
|
||||
"web.invalid_config_data": "Invalid configuration data",
|
||||
# List processing
|
||||
"manifest.download.failed": "Downloading from {url} failed: {error}",
|
||||
"manifest.delete_old": "Delete old manifest: {name}",
|
||||
"manifest.process.success": "Manifest processing successful: {depot_id}_{manifest_id}.manifest",
|
||||
"manifest.process.failed": "Error while processing manifest: {error}",
|
||||
"manifest.process.failed2": "Manifest processing failed: {depot_id}_{manifest_id}",
|
||||
"manifest.exists": "Manifest already exists: {name}",
|
||||
"manifest.downloading": "Downloading manifest: {depot_id}_{manifest_id}",
|
||||
"manifest.downloading.failed": "Manifest download failed: {depot_id}_{manifest_id}",
|
||||
}
|
||||
|
||||
def set_language(self, lang: str):
|
||||
"""设置当前语言"""
|
||||
if lang in self.translations:
|
||||
self.current_lang = lang
|
||||
else:
|
||||
raise ValueError(f"Unsupported language: {lang}")
|
||||
|
||||
def t(self, key: str, **kwargs) -> str:
|
||||
"""
|
||||
获取翻译文本
|
||||
|
||||
Args:
|
||||
key: 翻译键
|
||||
**kwargs: 格式化参数
|
||||
|
||||
Returns:
|
||||
翻译后的文本
|
||||
"""
|
||||
lang_dict = self.translations.get(self.current_lang, {})
|
||||
text = lang_dict.get(key, key)
|
||||
|
||||
# 格式化文本
|
||||
if kwargs:
|
||||
try:
|
||||
text = text.format(**kwargs)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return text
|
||||
|
||||
def get_all_translations(self, lang: str = None) -> Dict[str, str]:
|
||||
"""获取指定语言的所有翻译"""
|
||||
lang = lang or self.current_lang
|
||||
return self.translations.get(lang, {})
|
||||
|
||||
|
||||
# 全局i18n实例
|
||||
_i18n_instance = None
|
||||
|
||||
|
||||
def get_i18n() -> I18n:
|
||||
"""获取全局i18n实例"""
|
||||
global _i18n_instance
|
||||
if _i18n_instance is None:
|
||||
from ..config import ConfigManager
|
||||
|
||||
config = ConfigManager()
|
||||
_i18n_instance = I18n(config.app_config.language)
|
||||
return _i18n_instance
|
||||
|
||||
|
||||
def t(key: str, **kwargs) -> str:
|
||||
"""便捷的翻译函数"""
|
||||
return get_i18n().t(key, **kwargs)
|
||||
27
web/app.py
27
web/app.py
@@ -13,8 +13,8 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from src import constants
|
||||
from src.constants import STEAM_API_BASE
|
||||
from src.utils.i18n import t
|
||||
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
@@ -38,8 +38,8 @@ try:
|
||||
from src.main import OnekeyApp
|
||||
from src.config import ConfigManager
|
||||
except ImportError as e:
|
||||
print(f"导入错误: {e}")
|
||||
print("请确保在项目根目录中运行此程序")
|
||||
print(t("error.import", error=str(e)))
|
||||
print(t("error.ensure_root"))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -199,8 +199,15 @@ app.add_middleware(
|
||||
manager = ConnectionManager()
|
||||
|
||||
# 修复:为静态文件路由添加name参数
|
||||
app.mount("/static", StaticFiles(directory=f"{base_path}/static"), name="static")
|
||||
templates = Jinja2Templates(directory=f"{base_path}/templates")
|
||||
config = ConfigManager()
|
||||
app.mount(
|
||||
"/static",
|
||||
StaticFiles(directory=f"{base_path}/{config.app_config.language}/static"),
|
||||
name="static",
|
||||
)
|
||||
templates = Jinja2Templates(
|
||||
directory=f"{base_path}/{config.app_config.language}/templates"
|
||||
)
|
||||
|
||||
# 创建Web应用实例
|
||||
web_app = WebOnekeyApp(manager)
|
||||
@@ -326,6 +333,7 @@ async def update_config(request: Request):
|
||||
"Debug_Mode": data.get("debug_mode", False),
|
||||
"Logging_Files": data.get("logging_files", True),
|
||||
"Show_Console": data.get("show_console", True),
|
||||
"Language": data.get("language", "zh"),
|
||||
}
|
||||
|
||||
# 保存配置
|
||||
@@ -376,6 +384,7 @@ async def get_detailed_config():
|
||||
config.steam_path.exists() if config.steam_path else False
|
||||
),
|
||||
"key": getattr(config.app_config, "key", ""),
|
||||
"language": config.app_config.language,
|
||||
},
|
||||
}
|
||||
except Exception as e:
|
||||
@@ -428,11 +437,11 @@ async def websocket_endpoint(websocket: WebSocket):
|
||||
)
|
||||
except WebSocketDisconnect:
|
||||
manager.disconnect(websocket)
|
||||
print("客户端断开连接")
|
||||
print(t("web.client_disconnected"))
|
||||
except Exception as e:
|
||||
print(f"WebSocket 错误: {e}")
|
||||
print(t("web.websocket_error", error=str(e)))
|
||||
manager.disconnect(websocket)
|
||||
|
||||
|
||||
print("启动Onekey Web GUI...")
|
||||
print("请在浏览器中访问: http://localhost:5000")
|
||||
print(t("web.starting"))
|
||||
print(t("web.visit"))
|
||||
|
||||
685
web/en/static/js/app.js
Normal file
685
web/en/static/js/app.js
Normal file
@@ -0,0 +1,685 @@
|
||||
class OnekeyWebApp {
|
||||
constructor() {
|
||||
this.socket = null;
|
||||
this.taskStatus = "idle";
|
||||
this.reconnectTimer = null;
|
||||
this.reconnectAttempts = 0;
|
||||
this.maxReconnectAttempts = 5;
|
||||
this.reconnectDelay = 2000;
|
||||
this.initializeSocket();
|
||||
this.initializeEventListeners();
|
||||
this.checkConfig();
|
||||
}
|
||||
|
||||
initializeSocket() {
|
||||
this.connectWebSocket();
|
||||
}
|
||||
|
||||
connectWebSocket() {
|
||||
try {
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws`;
|
||||
|
||||
this.socket = new WebSocket(wsUrl);
|
||||
|
||||
this.socket.onopen = () => {
|
||||
console.log("Connected to server");
|
||||
this.showSnackbar("Connected to server", "success");
|
||||
this.reconnectAttempts = 0;
|
||||
|
||||
this.startHeartbeat();
|
||||
};
|
||||
|
||||
this.socket.onclose = (event) => {
|
||||
console.log("Disconnected from server", event);
|
||||
this.showSnackbar("Disconnected from server", "error");
|
||||
|
||||
this.stopHeartbeat();
|
||||
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.reconnectAttempts++;
|
||||
console.log(
|
||||
`Trying reconnect... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`,
|
||||
);
|
||||
this.connectWebSocket();
|
||||
}, this.reconnectDelay);
|
||||
}
|
||||
};
|
||||
|
||||
this.socket.onerror = (error) => {
|
||||
console.error("WebSocket error:", error);
|
||||
};
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
this.handleMessage(message);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse message:", e);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to connect WebSocket:", error);
|
||||
this.showSnackbar("Failed to connect WebSocket", "error");
|
||||
}
|
||||
}
|
||||
|
||||
handleMessage(message) {
|
||||
switch (message.type) {
|
||||
case "connected":
|
||||
console.log(message.data.message);
|
||||
break;
|
||||
case "task_progress":
|
||||
this.addLogEntry(message.data.type, message.data.message);
|
||||
break;
|
||||
case "pong":
|
||||
break;
|
||||
default:
|
||||
console.log("Unknown message type:", message.type);
|
||||
}
|
||||
}
|
||||
|
||||
startHeartbeat() {
|
||||
this.heartbeatInterval = setInterval(() => {
|
||||
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
||||
this.socket.send(JSON.stringify({ type: "ping" }));
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
stopHeartbeat() {
|
||||
if (this.heartbeatInterval) {
|
||||
clearInterval(this.heartbeatInterval);
|
||||
this.heartbeatInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
this.stopHeartbeat();
|
||||
if (this.socket) {
|
||||
this.socket.close();
|
||||
this.socket = null;
|
||||
}
|
||||
}
|
||||
|
||||
initializeEventListeners() {
|
||||
const unlockForm = document.getElementById("unlockForm");
|
||||
unlockForm.addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
this.startUnlockTask();
|
||||
});
|
||||
|
||||
const resetBtn = document.getElementById("resetBtn");
|
||||
resetBtn.addEventListener("click", () => {
|
||||
this.resetForm();
|
||||
});
|
||||
|
||||
const clearLogBtn = document.getElementById("clearLogBtn");
|
||||
clearLogBtn.addEventListener("click", () => {
|
||||
this.clearLogs();
|
||||
});
|
||||
|
||||
const snackbarClose = document.getElementById("snackbarClose");
|
||||
snackbarClose.addEventListener("click", () => {
|
||||
this.hideSnackbar();
|
||||
});
|
||||
|
||||
window.addEventListener("beforeunload", () => {
|
||||
this.disconnect();
|
||||
});
|
||||
}
|
||||
|
||||
async checkConfig() {
|
||||
const configStatus = document.getElementById("configStatus");
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/config");
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
configStatus.innerHTML = this.generateConfigStatusHTML(data.config);
|
||||
} else {
|
||||
configStatus.innerHTML = `
|
||||
<div class="status-item">
|
||||
<span class="material-icons status-icon error">error</span>
|
||||
<span class="status-text">Load config failed: ${data.message}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
configStatus.innerHTML = `
|
||||
<div class="status-item">
|
||||
<span class="material-icons status-icon error">error</span>
|
||||
<span class="status-text">Failed to connect WebSocket</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
generateConfigStatusHTML(config) {
|
||||
const items = [];
|
||||
|
||||
if (config.steam_path) {
|
||||
items.push(`
|
||||
<div class="status-item">
|
||||
<span class="material-icons status-icon success">check_circle</span>
|
||||
<span class="status-text">Steam Path: ${config.steam_path}</span>
|
||||
</div>
|
||||
`);
|
||||
} else {
|
||||
items.push(`
|
||||
<div class="status-item">
|
||||
<span class="material-icons status-icon error">error</span>
|
||||
<span class="status-text">Steam path not found</span>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
if (config.debug_mode) {
|
||||
items.push(`
|
||||
<div class="status-item">
|
||||
<span class="material-icons status-icon warning">bug_report</span>
|
||||
<span class="status-text">Debug mode is enable</span>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
return items.join("");
|
||||
}
|
||||
|
||||
toggleAndDLC() {
|
||||
document.getElementById("+DLC").checked = true;
|
||||
}
|
||||
|
||||
async startUnlockTask() {
|
||||
if (this.taskStatus === "running") {
|
||||
this.showSnackbar("There is already a task running", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData(document.getElementById("unlockForm"));
|
||||
const appId = formData.get("appId").trim();
|
||||
const toolType = formData.get("toolType");
|
||||
const ADLC = formData.get("+DLC") === "on";
|
||||
|
||||
if (!appId) {
|
||||
this.showSnackbar("Please enter App ID", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const appIdPattern = /^[\d-]+$/;
|
||||
if (!appIdPattern.test(appId)) {
|
||||
this.showSnackbar(
|
||||
"Invalid App ID format, should be a number or numbers separated by -",
|
||||
"error",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.taskStatus = "running";
|
||||
this.updateUIForRunningTask();
|
||||
this.clearLogs();
|
||||
this.addLogEntry("info", `Start working on the game ${appId}...`);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/start_unlock", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
app_id: appId,
|
||||
tool_type: toolType,
|
||||
dlc: ADLC,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showSnackbar("Mission has started", "success");
|
||||
this.startStatusPolling();
|
||||
} else {
|
||||
this.taskStatus = "idle";
|
||||
this.updateUIForIdleTask();
|
||||
this.showSnackbar(data.message, "error");
|
||||
this.addLogEntry("error", data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
this.taskStatus = "idle";
|
||||
this.updateUIForIdleTask();
|
||||
this.showSnackbar("Failed to start task", "error");
|
||||
this.addLogEntry("error", `Failed to start task: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
startStatusPolling() {
|
||||
const pollInterval = setInterval(async () => {
|
||||
try {
|
||||
const response = await fetch("/api/task_status");
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === "completed") {
|
||||
clearInterval(pollInterval);
|
||||
this.taskStatus = "completed";
|
||||
this.updateUIForIdleTask();
|
||||
|
||||
if (data.result && data.result.success) {
|
||||
this.showSnackbar(data.result.message, "success");
|
||||
this.addLogEntry("info", data.result.message);
|
||||
} else if (data.result) {
|
||||
this.showSnackbar(data.result.message, "error");
|
||||
this.addLogEntry("error", data.result.message);
|
||||
}
|
||||
} else if (data.status === "error") {
|
||||
clearInterval(pollInterval);
|
||||
this.taskStatus = "error";
|
||||
this.updateUIForIdleTask();
|
||||
|
||||
if (data.result) {
|
||||
this.showSnackbar(data.result.message, "error");
|
||||
this.addLogEntry("error", data.result.message);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Status polling error:", error);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
updateUIForRunningTask() {
|
||||
const unlockBtn = document.getElementById("unlockBtn");
|
||||
const resetBtn = document.getElementById("resetBtn");
|
||||
const appIdInput = document.getElementById("appId");
|
||||
const toolTypeRadios = document.querySelectorAll('input[name="toolType"]');
|
||||
|
||||
unlockBtn.disabled = true;
|
||||
unlockBtn.innerHTML = `
|
||||
<span class="material-icons">hourglass_empty</span>
|
||||
Executing...
|
||||
`;
|
||||
|
||||
resetBtn.disabled = true;
|
||||
appIdInput.disabled = true;
|
||||
toolTypeRadios.forEach((radio) => (radio.disabled = true));
|
||||
}
|
||||
|
||||
updateUIForIdleTask() {
|
||||
const unlockBtn = document.getElementById("unlockBtn");
|
||||
const resetBtn = document.getElementById("resetBtn");
|
||||
const appIdInput = document.getElementById("appId");
|
||||
const toolTypeRadios = document.querySelectorAll('input[name="toolType"]');
|
||||
|
||||
unlockBtn.disabled = false;
|
||||
unlockBtn.innerHTML = `
|
||||
<span class="material-icons">play_arrow</span>
|
||||
Start unlocking
|
||||
`;
|
||||
|
||||
resetBtn.disabled = false;
|
||||
appIdInput.disabled = false;
|
||||
toolTypeRadios.forEach((radio) => (radio.disabled = false));
|
||||
}
|
||||
|
||||
resetForm() {
|
||||
if (this.taskStatus === "running") {
|
||||
this.showSnackbar("The task is running and cannot be reset.", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("unlockForm").reset();
|
||||
document.querySelector(
|
||||
'input[name="toolType"][value="steamtools"]',
|
||||
).checked = true;
|
||||
this.clearLogs();
|
||||
this.showSnackbar("Form has been reset", "success");
|
||||
}
|
||||
|
||||
addLogEntry(type, message) {
|
||||
const progressContainer = document.getElementById("progressContainer");
|
||||
const placeholder = progressContainer.querySelector(
|
||||
".progress-placeholder",
|
||||
);
|
||||
|
||||
if (placeholder) {
|
||||
placeholder.remove();
|
||||
}
|
||||
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const logEntry = document.createElement("div");
|
||||
logEntry.className = `log-entry ${type}`;
|
||||
logEntry.innerHTML = `
|
||||
<span class="log-timestamp">${timestamp}</span>
|
||||
<span class="log-message">${this.escapeHtml(message)}</span>
|
||||
`;
|
||||
|
||||
progressContainer.appendChild(logEntry);
|
||||
progressContainer.scrollTop = progressContainer.scrollHeight;
|
||||
}
|
||||
|
||||
clearLogs() {
|
||||
const progressContainer = document.getElementById("progressContainer");
|
||||
progressContainer.innerHTML = `
|
||||
<div class="progress-placeholder">
|
||||
<span class="material-icons">info</span>
|
||||
<p>Wait for task to start...</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
showSnackbar(message, type = "info") {
|
||||
const snackbar = document.getElementById("snackbar");
|
||||
const snackbarMessage = document.getElementById("snackbarMessage");
|
||||
|
||||
snackbarMessage.textContent = message;
|
||||
snackbar.className = `snackbar ${type}`;
|
||||
|
||||
snackbar.offsetHeight;
|
||||
|
||||
snackbar.classList.add("show");
|
||||
|
||||
setTimeout(() => {
|
||||
this.hideSnackbar();
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
hideSnackbar() {
|
||||
const snackbar = document.getElementById("snackbar");
|
||||
snackbar.classList.remove("show");
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
@keyframes iconRotate {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
|
||||
anchor.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute("href"));
|
||||
if (target) {
|
||||
target.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const cards = document.querySelectorAll(".card");
|
||||
|
||||
cards.forEach((card) => {
|
||||
card.addEventListener("mousemove", (e) => {
|
||||
const rect = card.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
const centerX = rect.width / 2;
|
||||
const centerY = rect.height / 2;
|
||||
|
||||
const percentX = (x - centerX) / centerX;
|
||||
const percentY = (y - centerY) / centerY;
|
||||
|
||||
const rotateX = percentY * 5;
|
||||
const rotateY = percentX * 5;
|
||||
|
||||
card.style.transform = `perspective(1000px) rotateX(${-rotateX}deg) rotateY(${rotateY}deg) translateZ(10px)`;
|
||||
});
|
||||
|
||||
card.addEventListener("mouseleave", () => {
|
||||
card.style.transform = "";
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function typeWriter(element, text, speed = 50) {
|
||||
let i = 0;
|
||||
element.textContent = "";
|
||||
|
||||
function type() {
|
||||
if (i < text.length) {
|
||||
element.textContent += text.charAt(i);
|
||||
i++;
|
||||
setTimeout(type, speed);
|
||||
}
|
||||
}
|
||||
|
||||
type();
|
||||
}
|
||||
|
||||
function animateValue(element, start, end, duration) {
|
||||
const range = end - start;
|
||||
const increment = range / (duration / 16);
|
||||
let current = start;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
current += increment;
|
||||
if (
|
||||
(increment > 0 && current >= end) ||
|
||||
(increment < 0 && current <= end)
|
||||
) {
|
||||
current = end;
|
||||
clearInterval(timer);
|
||||
}
|
||||
element.textContent = Math.round(current);
|
||||
}, 16);
|
||||
}
|
||||
|
||||
document.querySelectorAll(".btn").forEach((button) => {
|
||||
button.addEventListener("mousemove", (e) => {
|
||||
const rect = button.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left - rect.width / 2;
|
||||
const y = e.clientY - rect.top - rect.height / 2;
|
||||
|
||||
button.style.transform = `translate(${x * 0.1}px, ${y * 0.1}px)`;
|
||||
});
|
||||
|
||||
button.addEventListener("mouseleave", () => {
|
||||
button.style.transform = "";
|
||||
});
|
||||
});
|
||||
|
||||
function createParticles() {
|
||||
const particlesContainer = document.createElement("div");
|
||||
particlesContainer.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
`;
|
||||
document.body.appendChild(particlesContainer);
|
||||
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const particle = document.createElement("div");
|
||||
particle.style.cssText = `
|
||||
position: absolute;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: rgba(94, 53, 177, 0.3);
|
||||
border-radius: 50%;
|
||||
top: ${Math.random() * 100}%;
|
||||
left: ${Math.random() * 100}%;
|
||||
animation: floatParticle ${
|
||||
10 + Math.random() * 20
|
||||
}s linear infinite;
|
||||
`;
|
||||
particlesContainer.appendChild(particle);
|
||||
}
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
@keyframes floatParticle {
|
||||
0% {
|
||||
transform: translateY(100vh) rotate(0deg);
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
opacity: 1;
|
||||
}
|
||||
90% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-100vh) rotate(720deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
const observerOptions = {
|
||||
threshold: 0.1,
|
||||
rootMargin: "0px 0px -50px 0px",
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.style.opacity = "1";
|
||||
entry.target.style.transform = "translateY(0)";
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
document.querySelectorAll(".card").forEach((card) => {
|
||||
card.style.opacity = "0";
|
||||
card.style.transform = "translateY(20px)";
|
||||
card.style.transition = "opacity 0.6s ease, transform 0.6s ease";
|
||||
observer.observe(card);
|
||||
});
|
||||
|
||||
document.addEventListener("mousemove", (e) => {
|
||||
const light = document.createElement("div");
|
||||
light.style.cssText = `
|
||||
position: fixed;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background: radial-gradient(circle, rgba(94, 53, 177, 0.1) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
transform: translate(-50%, -50%);
|
||||
transition: opacity 0.3s ease;
|
||||
`;
|
||||
light.style.left = e.clientX + "px";
|
||||
light.style.top = e.clientY + "px";
|
||||
document.body.appendChild(light);
|
||||
|
||||
setTimeout(() => {
|
||||
light.style.opacity = "0";
|
||||
setTimeout(() => light.remove(), 300);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
document.querySelectorAll(".status-icon").forEach((icon) => {
|
||||
if (icon.classList.contains("success")) {
|
||||
icon.style.animation = "pulse-icon 2s ease-in-out infinite";
|
||||
}
|
||||
});
|
||||
|
||||
const pulseStyle = document.createElement("style");
|
||||
pulseStyle.textContent = `
|
||||
@keyframes pulse-icon {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(pulseStyle);
|
||||
|
||||
const originalShowSnackbar = window.showSnackbar;
|
||||
if (typeof originalShowSnackbar === "function") {
|
||||
window.showSnackbar = function (message, type = "info") {
|
||||
originalShowSnackbar(message, type);
|
||||
|
||||
if ("vibrate" in navigator) {
|
||||
if (type === "error") {
|
||||
navigator.vibrate([100, 50, 100]);
|
||||
} else {
|
||||
navigator.vibrate(50);
|
||||
}
|
||||
}
|
||||
|
||||
const audio = new Audio(
|
||||
`data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBSuBzvLZiTYIG2m98OScTgwOUarm7blmFgU7k9n1unEiBC13yO/eizEIHWq+8+OWT` +
|
||||
`BEFS6Xj67xqGAU+lNr1unIiBCx0xvDdiTYIHWu+8+OWT`,
|
||||
);
|
||||
if (type === "success") {
|
||||
audio.volume = 0.1;
|
||||
audio.play().catch(() => {});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
document.querySelectorAll(".text-field").forEach((input) => {
|
||||
input.addEventListener("focus", (e) => {
|
||||
const ripple = document.createElement("div");
|
||||
ripple.style.cssText = `
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border: 2px solid var(--md-sys-color-primary);
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
animation: inputRipple 0.6s ease-out;
|
||||
`;
|
||||
|
||||
const wrapper = input.parentElement;
|
||||
wrapper.style.position = "relative";
|
||||
wrapper.appendChild(ripple);
|
||||
|
||||
setTimeout(() => ripple.remove(), 600);
|
||||
});
|
||||
});
|
||||
|
||||
const inputRippleStyle = document.createElement("style");
|
||||
inputRippleStyle.textContent = `
|
||||
@keyframes inputRipple {
|
||||
0% {
|
||||
transform: scale(0.8);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(inputRippleStyle);
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
createParticles();
|
||||
document.body.classList.add("loaded");
|
||||
console.log("UI enhancements loaded ✨");
|
||||
|
||||
new OnekeyWebApp();
|
||||
});
|
||||
180
web/en/static/js/project-info.js
Normal file
180
web/en/static/js/project-info.js
Normal file
@@ -0,0 +1,180 @@
|
||||
class ProjectInfoEnhancer {
|
||||
constructor() {
|
||||
this.initializeProjectInfo();
|
||||
}
|
||||
|
||||
initializeProjectInfo() {
|
||||
this.addProjectLinkTracking();
|
||||
|
||||
this.addVersionClickEaster();
|
||||
|
||||
this.addLogoClickEffect();
|
||||
}
|
||||
|
||||
addProjectLinkTracking() {
|
||||
const projectLinks = document.querySelectorAll(".project-link");
|
||||
projectLinks.forEach((link) => {
|
||||
link.addEventListener("click", (e) => {
|
||||
const linkType = link.classList.contains("github")
|
||||
? "GitHub repository"
|
||||
: link.classList.contains("releases")
|
||||
? "Download release version"
|
||||
: link.classList.contains("docs")
|
||||
? "Use documentation"
|
||||
: link.classList.contains("issues")
|
||||
? "Problem feedback"
|
||||
: "unknown link";
|
||||
|
||||
console.log(`User clicked on ${linkType} link`);
|
||||
|
||||
link.style.transform = "scale(0.95)";
|
||||
setTimeout(() => {
|
||||
link.style.transform = "";
|
||||
}, 150);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
addVersionClickEaster() {
|
||||
const versionLabels = document.querySelectorAll(".version-label");
|
||||
let clickCount = 0;
|
||||
|
||||
versionLabels.forEach((label) => {
|
||||
label.addEventListener("click", () => {
|
||||
clickCount++;
|
||||
|
||||
if (clickCount === 5) {
|
||||
this.showEasterEgg();
|
||||
clickCount = 0;
|
||||
}
|
||||
|
||||
label.style.animation = "pulse 0.3s ease";
|
||||
setTimeout(() => {
|
||||
label.style.animation = "";
|
||||
}, 300);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
addLogoClickEffect() {
|
||||
const logos = document.querySelectorAll(".project-logo");
|
||||
|
||||
logos.forEach((logo) => {
|
||||
logo.addEventListener("click", () => {
|
||||
logo.style.transform = "rotate(360deg)";
|
||||
logo.style.transition = "transform 0.6s ease";
|
||||
|
||||
setTimeout(() => {
|
||||
logo.style.transform = "";
|
||||
logo.style.transition = "";
|
||||
}, 600);
|
||||
|
||||
this.showTooltip(logo, "🎮 Onekey - Steam unlocking made easy!");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
showEasterEgg() {
|
||||
const messages = [
|
||||
"🎉 You found a hidden easter egg!",
|
||||
"🚀 Thank you for using Onekey Tools!",
|
||||
"⭐ Don’t forget to give the project a star!",
|
||||
"🎮 Happy gaming!",
|
||||
|
||||
"🔓 Unlock it with one click and enjoy the game!",
|
||||
];
|
||||
|
||||
const randomMessage = messages[Math.floor(Math.random() * messages.length)];
|
||||
|
||||
const easterEgg = document.createElement("div");
|
||||
easterEgg.className = "easter-egg";
|
||||
easterEgg.textContent = randomMessage;
|
||||
easterEgg.style.cssText = `
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: linear-gradient(45deg, #6750a4, #7d5260);
|
||||
color: white;
|
||||
padding: 20px 30px;
|
||||
border-radius: 15px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
z-index: 9999;
|
||||
animation: easterEggBounce 0.6s ease-out;
|
||||
`;
|
||||
|
||||
if (!document.getElementById("easter-egg-styles")) {
|
||||
const style = document.createElement("style");
|
||||
style.id = "easter-egg-styles";
|
||||
style.textContent = `
|
||||
@keyframes easterEggBounce {
|
||||
0% { transform: translate(-50%, -50%) scale(0); opacity: 0; }
|
||||
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 1; }
|
||||
100% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
|
||||
}
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
document.body.appendChild(easterEgg);
|
||||
|
||||
setTimeout(() => {
|
||||
easterEgg.style.animation = "easterEggBounce 0.3s ease-in reverse";
|
||||
setTimeout(() => {
|
||||
if (easterEgg.parentNode) {
|
||||
easterEgg.parentNode.removeChild(easterEgg);
|
||||
}
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
showTooltip(element, message) {
|
||||
const tooltip = document.createElement("div");
|
||||
tooltip.className = "custom-tooltip";
|
||||
tooltip.textContent = message;
|
||||
tooltip.style.cssText = `
|
||||
position: absolute;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
tooltip.style.left = rect.left + rect.width / 2 + "px";
|
||||
tooltip.style.top = rect.bottom + 10 + "px";
|
||||
tooltip.style.transform = "translateX(-50%)";
|
||||
|
||||
document.body.appendChild(tooltip);
|
||||
|
||||
setTimeout(() => {
|
||||
tooltip.style.opacity = "1";
|
||||
}, 10);
|
||||
|
||||
setTimeout(() => {
|
||||
tooltip.style.opacity = "0";
|
||||
setTimeout(() => {
|
||||
if (tooltip.parentNode) {
|
||||
tooltip.parentNode.removeChild(tooltip);
|
||||
}
|
||||
}, 300);
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
new ProjectInfoEnhancer();
|
||||
});
|
||||
677
web/en/static/js/settings.js
Normal file
677
web/en/static/js/settings.js
Normal file
@@ -0,0 +1,677 @@
|
||||
class SettingsManager {
|
||||
constructor() {
|
||||
this.currentConfig = {};
|
||||
this.currentKeyInfo = null;
|
||||
this.newKeyData = null;
|
||||
this.initializeEventListeners();
|
||||
this.loadConfig();
|
||||
this.loadKeyInfo();
|
||||
}
|
||||
|
||||
initializeEventListeners() {
|
||||
document.getElementById("saveConfig").addEventListener("click", () => {
|
||||
this.saveConfig();
|
||||
});
|
||||
|
||||
document.getElementById("resetConfig").addEventListener("click", () => {
|
||||
this.showConfirmDialog(
|
||||
"Reset configuration",
|
||||
"Are you sure you want to reset all configurations to default? This operation is irreversible.",
|
||||
() => this.resetConfig(),
|
||||
);
|
||||
});
|
||||
|
||||
document.getElementById("testConfig").addEventListener("click", () => {
|
||||
this.testConfig();
|
||||
});
|
||||
|
||||
document.getElementById("detectSteamPath").addEventListener("click", () => {
|
||||
this.detectSteamPath();
|
||||
});
|
||||
|
||||
document.getElementById("steamPath").addEventListener("input", () => {
|
||||
this.validateSteamPath();
|
||||
});
|
||||
|
||||
document.getElementById("verifyNewKey").addEventListener("click", () => {
|
||||
this.verifyNewKey();
|
||||
});
|
||||
|
||||
document.getElementById("changeKey").addEventListener("click", () => {
|
||||
this.showConfirmDialog(
|
||||
"Change card password",
|
||||
"Are you sure you want to change to a new card password? Re-verification is required after replacement.",
|
||||
() => this.changeKey(),
|
||||
);
|
||||
});
|
||||
|
||||
document.getElementById("newKey").addEventListener("input", () => {
|
||||
this.resetNewKeyStatus();
|
||||
});
|
||||
|
||||
document.getElementById("newKey").addEventListener("keypress", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
this.verifyNewKey();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("dialogCancel").addEventListener("click", () => {
|
||||
this.hideConfirmDialog();
|
||||
});
|
||||
|
||||
document.getElementById("dialogConfirm").addEventListener("click", () => {
|
||||
this.executeConfirmAction();
|
||||
});
|
||||
|
||||
document.getElementById("snackbarClose").addEventListener("click", () => {
|
||||
this.hideSnackbar();
|
||||
});
|
||||
}
|
||||
|
||||
async loadConfig() {
|
||||
try {
|
||||
const response = await fetch("/api/config/detailed");
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.currentConfig = data.config;
|
||||
this.populateForm();
|
||||
this.updateConfigStatus();
|
||||
} else {
|
||||
this.showSnackbar(
|
||||
"Failed to load configuration: " + data.message,
|
||||
"error",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.showSnackbar("Unable to connect to server", "error");
|
||||
console.error("Load config error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadKeyInfo() {
|
||||
const keyInfoSection = document.getElementById("keyInfoSection");
|
||||
|
||||
try {
|
||||
const configResponse = await fetch("/api/config/detailed");
|
||||
const configData = await configResponse.json();
|
||||
|
||||
if (!configData.success || !configData.config.key) {
|
||||
keyInfoSection.innerHTML = `
|
||||
<div class="expiry-warning">
|
||||
<span class="material-icons">warning</span>
|
||||
<div>
|
||||
<strong>No card password set</strong><br>
|
||||
Please enter your authorization card password below
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const keyResponse = await fetch("/api/getKeyInfo", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ key: configData.config.key }),
|
||||
});
|
||||
|
||||
const keyData = await keyResponse.json();
|
||||
|
||||
if (keyData.key && keyData.info) {
|
||||
this.currentKeyInfo = keyData.info;
|
||||
this.displayKeyInfo(keyData.info);
|
||||
} else {
|
||||
keyInfoSection.innerHTML = `
|
||||
<div class="expiry-warning">
|
||||
<span class="material-icons">error</span>
|
||||
<div>
|
||||
<strong>Card password verification failed</strong><br>
|
||||
The current card password is invalid or expired, please replace it with a new one
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
keyInfoSection.innerHTML = `
|
||||
<div class="expiry-warning">
|
||||
<span class="material-icons">error</span>
|
||||
<div>
|
||||
<strong>Failed to obtain card password information</strong><br>
|
||||
Please check the network connection or contact customer service
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
console.error("Load key info error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
displayKeyInfo(keyInfo) {
|
||||
const keyInfoSection = document.getElementById("keyInfoSection");
|
||||
const expiresAt = new Date(keyInfo.expiresAt);
|
||||
const createdAt = new Date(keyInfo.createdAt);
|
||||
const firstUsedAt = keyInfo.firstUsedAt
|
||||
? new Date(keyInfo.firstUsedAt)
|
||||
: null;
|
||||
const now = new Date();
|
||||
|
||||
const isExpired = expiresAt < now;
|
||||
const daysLeft = Math.ceil((expiresAt - now) / (1000 * 60 * 60 * 24));
|
||||
const isExpiringSoon = daysLeft <= 7 && daysLeft > 0;
|
||||
|
||||
const typeNames = {
|
||||
day: "day card",
|
||||
week: "Weekly card",
|
||||
month: "monthly card",
|
||||
year: "Annual Pass",
|
||||
permanent: "permanent card",
|
||||
};
|
||||
|
||||
let statusBadge = "";
|
||||
if (isExpired && keyInfo.type != "permanent") {
|
||||
statusBadge =
|
||||
'<span class="key-status-badge expired"><span class="material-icons" style="font-size: 14px;">cancel</span>Expired</span>';
|
||||
} else if (!keyInfo.isActive) {
|
||||
statusBadge =
|
||||
'<span class="key-status-badge inactive"><span class="material-icons" style="font-size: 14px;">pause</span>Not activated</span>';
|
||||
} else {
|
||||
statusBadge =
|
||||
'<span class="key-status-badge active"><span class="material-icons" style="font-size: 14px;">check_circle</span>normal</span>';
|
||||
}
|
||||
|
||||
let warningSection = "";
|
||||
if (isExpiringSoon) {
|
||||
warningSection = `
|
||||
<div class="expiry-warning">
|
||||
<span class="material-icons">schedule</span>
|
||||
<div>
|
||||
<strong>upcoming expiry reminder</strong><br>
|
||||
Your card password will expire in ${daysLeft} days, please renew in time
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
keyInfoSection.innerHTML = `
|
||||
<div class="key-info-grid">
|
||||
<div class="key-info-card">
|
||||
<span class="material-icons key-info-icon">fingerprint</span>
|
||||
<div class="key-info-content">
|
||||
<div class="key-info-label">Cardamom</div>
|
||||
<div class="key-info-value">${keyInfo.key.substring(
|
||||
0,
|
||||
8,
|
||||
)}...${keyInfo.key.substring(
|
||||
keyInfo.key.length - 8,
|
||||
)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="key-info-card">
|
||||
<span class="material-icons key-info-icon">label</span>
|
||||
<div class="key-info-content">
|
||||
<div class="key-info-label">Type</div>
|
||||
<div class="key-info-value">${
|
||||
typeNames[keyInfo.type] || keyInfo.type
|
||||
}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="key-info-card">
|
||||
<span class="material-icons key-info-icon">toggle_on</span>
|
||||
<div class="key-info-content">
|
||||
<div class="key-info-label">State</div>
|
||||
<div class="key-info-value">${statusBadge}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="key-info-card">
|
||||
<span class="material-icons key-info-icon">event</span>
|
||||
<div class="key-info-content">
|
||||
<div class="key-info-label">Expiration time</div>
|
||||
<div class="key-info-value">${expiresAt.toLocaleDateString()} ${expiresAt
|
||||
.toLocaleTimeString()
|
||||
.substring(0, 5)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="key-info-card">
|
||||
<span class="material-icons key-info-icon">analytics</span>
|
||||
<div class="key-info-content">
|
||||
<div class="key-info-label">Number of uses</div>
|
||||
<div class="key-info-value">${keyInfo.usageCount} / ${
|
||||
keyInfo.totalUsage || "∞"
|
||||
}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="key-info-card">
|
||||
<span class="material-icons key-info-icon">schedule</span>
|
||||
<div class="key-info-content">
|
||||
<div class="key-info-label">Creation time</div>
|
||||
<div class="key-info-value">${createdAt.toLocaleDateString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${warningSection}
|
||||
`;
|
||||
}
|
||||
|
||||
async verifyNewKey() {
|
||||
const newKeyInput = document.getElementById("newKey");
|
||||
const key = newKeyInput.value.trim();
|
||||
|
||||
if (!key) {
|
||||
this.showSnackbar("Please enter new card password", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!key.match(/^[A-Z0-9_-]+$/)) {
|
||||
this.showSnackbar("The card password format is incorrect", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const verifyBtn = document.getElementById("verifyNewKey");
|
||||
const changeBtn = document.getElementById("changeKey");
|
||||
|
||||
verifyBtn.disabled = true;
|
||||
verifyBtn.innerHTML =
|
||||
'<span class="material-icons">hourglass_empty</span>Verifying...';
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/getKeyInfo", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ key: key }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.key && data.info) {
|
||||
this.newKeyData = data.info;
|
||||
this.showSnackbar(
|
||||
"New card password verification successful!",
|
||||
"success",
|
||||
);
|
||||
|
||||
changeBtn.style.display = "flex";
|
||||
verifyBtn.style.display = "none";
|
||||
|
||||
const typeNames = {
|
||||
day: "day card",
|
||||
week: "Weekly card",
|
||||
month: "Monthly card",
|
||||
year: "Annual Pass",
|
||||
permanent: "permanent card",
|
||||
};
|
||||
|
||||
const expiresAt = new Date(data.info.expiresAt);
|
||||
this.showSnackbar(
|
||||
`Verification successful! New card password type:
|
||||
${typeNames[data.info.type]}
|
||||
}, valid until:${expiresAt.toLocaleDateString()}`,
|
||||
"success",
|
||||
);
|
||||
} else {
|
||||
this.showSnackbar(
|
||||
"The new card password is invalid or expired",
|
||||
"error",
|
||||
);
|
||||
this.newKeyData = null;
|
||||
}
|
||||
} catch (error) {
|
||||
this.showSnackbar(
|
||||
"Verification failed, please check network connection",
|
||||
"error",
|
||||
);
|
||||
console.error("New key verification error:", error);
|
||||
} finally {
|
||||
verifyBtn.disabled = false;
|
||||
verifyBtn.innerHTML = '<span class="material-icons">check</span>Verify';
|
||||
}
|
||||
}
|
||||
|
||||
async changeKey() {
|
||||
if (!this.newKeyData) {
|
||||
this.showSnackbar("Please verify the new card password first", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newKey = document.getElementById("newKey").value.trim();
|
||||
|
||||
const updateData = {
|
||||
key: newKey,
|
||||
steam_path: this.currentConfig.steam_path || "",
|
||||
debug_mode: this.currentConfig.debug_mode || false,
|
||||
logging_files: this.currentConfig.logging_files !== false,
|
||||
show_console: this.currentConfig.show_console !== false,
|
||||
};
|
||||
|
||||
const response = await fetch("/api/config/update", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(updateData),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showSnackbar("Card secret changed successfully!", "success");
|
||||
|
||||
await this.loadKeyInfo();
|
||||
|
||||
this.resetNewKeyStatus();
|
||||
document.getElementById("newKey").value = "";
|
||||
} else {
|
||||
this.showSnackbar("Replacement failed: " + data.message, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
this.showSnackbar(
|
||||
"An error occurred while changing the card password",
|
||||
"error",
|
||||
);
|
||||
console.error("Change key error:", error);
|
||||
}
|
||||
|
||||
this.hideConfirmDialog();
|
||||
}
|
||||
|
||||
resetNewKeyStatus() {
|
||||
const verifyBtn = document.getElementById("verifyNewKey");
|
||||
const changeBtn = document.getElementById("changeKey");
|
||||
|
||||
verifyBtn.style.display = "flex";
|
||||
changeBtn.style.display = "none";
|
||||
this.newKeyData = null;
|
||||
}
|
||||
|
||||
populateForm() {
|
||||
document.getElementById("steamPath").value =
|
||||
this.currentConfig.steam_path || "";
|
||||
document.getElementById("debugMode").checked =
|
||||
this.currentConfig.debug_mode || false;
|
||||
document.getElementById("loggingFiles").checked =
|
||||
this.currentConfig.logging_files !== false;
|
||||
document.getElementById("showConsole").checked =
|
||||
this.currentConfig.show_console !== false;
|
||||
|
||||
this.validateSteamPath();
|
||||
}
|
||||
|
||||
async saveConfig() {
|
||||
try {
|
||||
const config = {
|
||||
key: this.currentConfig.key || "",
|
||||
steam_path: document.getElementById("steamPath").value.trim(),
|
||||
debug_mode: document.getElementById("debugMode").checked,
|
||||
logging_files: document.getElementById("loggingFiles").checked,
|
||||
show_console: document.getElementById("showConsole").checked,
|
||||
};
|
||||
|
||||
const response = await fetch("/api/config/update", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showSnackbar("Configuration saved", "success");
|
||||
await this.loadConfig();
|
||||
} else {
|
||||
this.showSnackbar("Save failed: " + data.message, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
this.showSnackbar(
|
||||
"An error occurred while saving the configuration",
|
||||
"error",
|
||||
);
|
||||
console.error("Save config error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async resetConfig() {
|
||||
try {
|
||||
const response = await fetch("/api/config/reset", {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showSnackbar(
|
||||
"The configuration has been reset (the card password remains unchanged)",
|
||||
"success",
|
||||
);
|
||||
await this.loadConfig();
|
||||
} else {
|
||||
this.showSnackbar("Reset failed: " + data.message, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
this.showSnackbar(
|
||||
"An error occurred while resetting the configuration",
|
||||
"error",
|
||||
);
|
||||
console.error("Reset config error:", error);
|
||||
}
|
||||
|
||||
this.hideConfirmDialog();
|
||||
}
|
||||
|
||||
async testConfig() {
|
||||
this.showSnackbar("Testing configuration...", "info");
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/config");
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
let messages = [];
|
||||
|
||||
if (data.config.steam_path) {
|
||||
messages.push("✓ Steam Path configuration is normal");
|
||||
} else {
|
||||
messages.push("✗ Steam Abnormal path configuration");
|
||||
}
|
||||
|
||||
if (this.currentKeyInfo) {
|
||||
const expiresAt = new Date(this.currentKeyInfo.expiresAt);
|
||||
let isExpired = expiresAt < new Date();
|
||||
|
||||
if ((this.currentKeyInfo.type = "permanent")) {
|
||||
isExpired = false;
|
||||
}
|
||||
|
||||
if (this.currentKeyInfo.isActive && !isExpired) {
|
||||
messages.push("✓ Card secret status is normal");
|
||||
} else {
|
||||
messages.push("✗ Abnormal card secret status");
|
||||
}
|
||||
}
|
||||
|
||||
this.showSnackbar(
|
||||
`Configuration test completed: ${messages.join(", ")}`,
|
||||
"success",
|
||||
);
|
||||
} else {
|
||||
this.showSnackbar(
|
||||
"Configuration test failed: " + data.message,
|
||||
"error",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.showSnackbar(
|
||||
"An error occurred while configuring the test",
|
||||
"error",
|
||||
);
|
||||
console.error("Test config error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
detectSteamPath() {
|
||||
const commonPaths = [
|
||||
"C:\\Program Files (x86)\\Steam",
|
||||
"C:\\Program Files\\Steam",
|
||||
"D:\\Steam",
|
||||
"E:\\Steam",
|
||||
];
|
||||
|
||||
const suggestedPath = commonPaths[0];
|
||||
document.getElementById("steamPath").value = suggestedPath;
|
||||
|
||||
this.validateSteamPath();
|
||||
this.showSnackbar(
|
||||
"It has been set as a common path, please confirm whether it is correct",
|
||||
"info",
|
||||
);
|
||||
}
|
||||
|
||||
validateSteamPath() {
|
||||
const steamPath = document.getElementById("steamPath").value.trim();
|
||||
const statusElement = document.getElementById("steamPathStatus");
|
||||
|
||||
if (!steamPath) {
|
||||
statusElement.className = "status-indicator";
|
||||
statusElement.innerHTML = `
|
||||
<span class="material-icons status-icon">info</span>
|
||||
<span class="status-text">The automatically detected path will be used</span>
|
||||
`;
|
||||
} else {
|
||||
if (steamPath.toLowerCase().includes("steam")) {
|
||||
statusElement.className = "status-indicator success";
|
||||
statusElement.innerHTML = `
|
||||
<span class="material-icons status-icon">check_circle</span>
|
||||
<span class="status-text">The path format looks correct</span>
|
||||
`;
|
||||
} else {
|
||||
statusElement.className = "status-indicator warning";
|
||||
statusElement.innerHTML = `
|
||||
<span class="material-icons status-icon">warning</span>
|
||||
<span class="status-text">The path may be incorrect, please confirm</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateConfigStatus() {
|
||||
const statusGrid = document.getElementById("configStatusGrid");
|
||||
const config = this.currentConfig;
|
||||
|
||||
const statusCards = [];
|
||||
|
||||
if (config.steam_path && config.steam_path_exists) {
|
||||
statusCards.push({
|
||||
type: "success",
|
||||
icon: "folder",
|
||||
title: "Steam path",
|
||||
description: `Path is valid: ${config.steam_path}`,
|
||||
});
|
||||
} else if (config.steam_path) {
|
||||
statusCards.push({
|
||||
type: "warning",
|
||||
icon: "folder_off",
|
||||
title: "Steam path",
|
||||
description: "Path is set but may be invalid",
|
||||
});
|
||||
} else {
|
||||
statusCards.push({
|
||||
type: "error",
|
||||
icon: "error",
|
||||
title: "Steam path",
|
||||
description: "Not set or auto-detection failed",
|
||||
});
|
||||
}
|
||||
|
||||
if (config.debug_mode) {
|
||||
statusCards.push({
|
||||
type: "warning",
|
||||
icon: "bug_report",
|
||||
title: "Debug mode",
|
||||
description: "Enabled, detailed logs will be output",
|
||||
});
|
||||
}
|
||||
|
||||
if (config.logging_files) {
|
||||
statusCards.push({
|
||||
type: "success",
|
||||
icon: "description",
|
||||
title: "Log file",
|
||||
description: "Enabled, logs will be saved to file",
|
||||
});
|
||||
}
|
||||
|
||||
statusGrid.innerHTML = statusCards
|
||||
.map(
|
||||
(card) => `
|
||||
<div class="status-card ${card.type}">
|
||||
<span class="material-icons status-card-icon">${card.icon}</span>
|
||||
<div class="status-card-content">
|
||||
<div class="status-card-title">${card.title}</div>
|
||||
<div class="status-card-description">${card.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
showConfirmDialog(title, message, confirmAction) {
|
||||
document.getElementById("dialogTitle").textContent = title;
|
||||
document.getElementById("dialogMessage").textContent = message;
|
||||
this.confirmAction = confirmAction;
|
||||
|
||||
const dialog = document.getElementById("confirmDialog");
|
||||
dialog.classList.add("show");
|
||||
}
|
||||
|
||||
hideConfirmDialog() {
|
||||
const dialog = document.getElementById("confirmDialog");
|
||||
dialog.classList.remove("show");
|
||||
this.confirmAction = null;
|
||||
}
|
||||
|
||||
executeConfirmAction() {
|
||||
if (this.confirmAction) {
|
||||
this.confirmAction();
|
||||
}
|
||||
this.hideConfirmDialog();
|
||||
}
|
||||
|
||||
showSnackbar(message, type = "info") {
|
||||
const snackbar = document.getElementById("snackbar");
|
||||
const snackbarMessage = document.getElementById("snackbarMessage");
|
||||
|
||||
snackbarMessage.textContent = message;
|
||||
snackbar.className = `snackbar ${type}`;
|
||||
|
||||
snackbar.offsetHeight;
|
||||
|
||||
snackbar.classList.add("show");
|
||||
|
||||
setTimeout(() => {
|
||||
this.hideSnackbar();
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
hideSnackbar() {
|
||||
const snackbar = document.getElementById("snackbar");
|
||||
snackbar.classList.remove("show");
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
window.location.href = "/";
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
new SettingsManager();
|
||||
});
|
||||
178
web/en/static/js/theme.js
Normal file
178
web/en/static/js/theme.js
Normal file
@@ -0,0 +1,178 @@
|
||||
class ThemeManager {
|
||||
constructor() {
|
||||
this.themeToggle = document.getElementById("themeToggle");
|
||||
this.currentTheme = this.getStoredTheme() || this.getPreferredTheme();
|
||||
this.isTransitioning = false;
|
||||
|
||||
this.createThemeIndicator();
|
||||
this.applyTheme(this.currentTheme, false);
|
||||
this.initializeEventListeners();
|
||||
}
|
||||
|
||||
getStoredTheme() {
|
||||
return localStorage.getItem("theme");
|
||||
}
|
||||
|
||||
getPreferredTheme() {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
|
||||
createThemeIndicator() {
|
||||
const indicator = document.createElement("div");
|
||||
indicator.className = "theme-indicator";
|
||||
indicator.id = "themeIndicator";
|
||||
document.body.appendChild(indicator);
|
||||
this.themeIndicator = indicator;
|
||||
}
|
||||
|
||||
showThemeIndicator(message) {
|
||||
this.themeIndicator.textContent = message;
|
||||
this.themeIndicator.classList.add("show");
|
||||
|
||||
setTimeout(() => {
|
||||
this.themeIndicator.classList.remove("show");
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
applyTheme(theme, animate = true) {
|
||||
if (this.isTransitioning) return;
|
||||
|
||||
this.isTransitioning = true;
|
||||
|
||||
if (animate) {
|
||||
document.body.classList.add("theme-transitioning");
|
||||
}
|
||||
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
this.updateToggleButton(theme);
|
||||
localStorage.setItem("theme", theme);
|
||||
this.currentTheme = theme;
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("themechange", {
|
||||
detail: { theme, animated: animate },
|
||||
}),
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.classList.remove("theme-transitioning");
|
||||
this.isTransitioning = false;
|
||||
}, 600);
|
||||
}
|
||||
|
||||
updateToggleButton(theme) {
|
||||
if (this.themeToggle) {
|
||||
const icon = this.themeToggle.querySelector(".material-icons");
|
||||
icon.textContent = theme === "dark" ? "dark_mode" : "light_mode";
|
||||
this.themeToggle.title =
|
||||
theme === "dark" ? "Switch to light mode" : "Switch to dark mode";
|
||||
|
||||
icon.style.animation = "none";
|
||||
icon.offsetHeight;
|
||||
icon.style.animation = "iconRotate 300ms ease";
|
||||
}
|
||||
}
|
||||
|
||||
toggleTheme(event) {
|
||||
const newTheme = this.currentTheme === "dark" ? "light" : "dark";
|
||||
|
||||
if (event && event.currentTarget) {
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
const x = ((rect.left + rect.width / 2) / window.innerWidth) * 100;
|
||||
const y = ((rect.top + rect.height / 2) / window.innerHeight) * 100;
|
||||
|
||||
document.documentElement.style.setProperty("--x", `${x}%`);
|
||||
document.documentElement.style.setProperty("--y", `${y}%`);
|
||||
}
|
||||
|
||||
if ("vibrate" in navigator) {
|
||||
navigator.vibrate(50);
|
||||
}
|
||||
|
||||
this.applyTheme(newTheme);
|
||||
|
||||
this.logThemeSwitch(newTheme);
|
||||
}
|
||||
|
||||
logThemeSwitch(theme) {
|
||||
console.log(`Topic switch to: ${theme}`);
|
||||
}
|
||||
|
||||
initializeEventListeners() {
|
||||
if (this.themeToggle) {
|
||||
this.themeToggle.addEventListener("click", (e) => this.toggleTheme(e));
|
||||
|
||||
this.themeToggle.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
this.toggleTheme(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
mediaQuery.addEventListener("change", (e) => {
|
||||
if (!this.getStoredTheme()) {
|
||||
this.applyTheme(e.matches ? "dark" : "light");
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === "T") {
|
||||
e.preventDefault();
|
||||
this.toggleTheme();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("storage", (e) => {
|
||||
if (e.key === "theme" && e.newValue) {
|
||||
this.applyTheme(e.newValue, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getThemePalette() {
|
||||
const computedStyle = getComputedStyle(document.documentElement);
|
||||
return {
|
||||
primary: computedStyle.getPropertyValue("--md-sys-color-primary").trim(),
|
||||
secondary: computedStyle
|
||||
.getPropertyValue("--md-sys-color-secondary")
|
||||
.trim(),
|
||||
surface: computedStyle.getPropertyValue("--md-sys-color-surface").trim(),
|
||||
background: computedStyle
|
||||
.getPropertyValue("--md-sys-color-background")
|
||||
.trim(),
|
||||
onBackground: computedStyle
|
||||
.getPropertyValue("--md-sys-color-on-background")
|
||||
.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
shouldUseDarkMode() {
|
||||
const hour = new Date().getHours();
|
||||
return hour >= 18 || hour < 6;
|
||||
}
|
||||
|
||||
enableAutoThemeSwitch() {
|
||||
const checkTime = () => {
|
||||
if (!this.getStoredTheme()) {
|
||||
const shouldBeDark = this.shouldUseDarkMode();
|
||||
const currentTheme = this.currentTheme;
|
||||
|
||||
if (
|
||||
(shouldBeDark && currentTheme === "light") ||
|
||||
(!shouldBeDark && currentTheme === "dark")
|
||||
) {
|
||||
this.applyTheme(shouldBeDark ? "dark" : "light");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setInterval(checkTime, 60000);
|
||||
checkTime();
|
||||
}
|
||||
}
|
||||
|
||||
window.ThemeManager = new ThemeManager();
|
||||
172
web/en/templates/about.html
Normal file
172
web/en/templates/about.html
Normal file
@@ -0,0 +1,172 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Onekey - About</title>
|
||||
|
||||
<!-- Material Design 3 -->
|
||||
<link
|
||||
href="https://cdn.jsdmirror.com/gh/ikun0014/font@main/LXGWWenKaiMono-Regular/result.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Custom Styles -->
|
||||
<link rel="stylesheet" href="/static/css/style.css" />
|
||||
</head>
|
||||
|
||||
<div class="footer-content">
|
||||
<!-- Project Info Card -->
|
||||
<div class="project-info-card">
|
||||
<div class="project-header">
|
||||
<div class="project-logo">
|
||||
<span class="material-icons">extension</span>
|
||||
</div>
|
||||
<div class="project-details">
|
||||
<h3 class="project-name">Onekey</h3>
|
||||
<p class="project-subtitle">
|
||||
Intuitive, elegant game unlock solution
|
||||
</p>
|
||||
</div>
|
||||
<div class="project-version">
|
||||
<span class="version-label">v2.0.8</span>
|
||||
<span class="version-type">Web UI</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-links">
|
||||
<a
|
||||
href="https://github.com/ikunshare/Onekey"
|
||||
target="_blank"
|
||||
class="project-link github"
|
||||
>
|
||||
<span class="material-icons">code</span>
|
||||
<div class="link-content">
|
||||
<span class="link-title">GitHub Repository</span>
|
||||
<span class="link-url">github.com/ikunshare/Onekey</span>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/ikunshare/Onekey/releases"
|
||||
target="_blank"
|
||||
class="project-link releases"
|
||||
>
|
||||
<span class="material-icons">file_download</span>
|
||||
<div class="link-content">
|
||||
<span class="link-title">Download Release</span>
|
||||
<span class="link-url">Get Latest Version</span>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="https://shop.ikunshare.com"
|
||||
target="_blank"
|
||||
class="project-link buy_cdk"
|
||||
>
|
||||
<span class="material-icons">shopping_cart</span>
|
||||
<div class="link-content">
|
||||
<span class="link-title">Purchase Link</span>
|
||||
<span class="link-url">Buy Activation Key</span>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/qwq-xinkeng"
|
||||
target="_blank"
|
||||
class="project-link author"
|
||||
>
|
||||
<span class="material-icons">person</span>
|
||||
<div class="link-content">
|
||||
<span class="link-title">Author Page</span>
|
||||
<span class="link-url">github.com/qwq-xinkeng</span>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/ikun0014"
|
||||
target="_blank"
|
||||
class="project-link author"
|
||||
>
|
||||
<span class="material-icons">person</span>
|
||||
<div class="link-content">
|
||||
<span class="link-title">Author Page</span>
|
||||
<span class="link-url">github.com/ikun0014</span>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/ikunshare/Onekey/issues"
|
||||
target="_blank"
|
||||
class="project-link issues"
|
||||
>
|
||||
<span class="material-icons">bug_report</span>
|
||||
<div class="link-content">
|
||||
<span class="link-title">Issue Feedback</span>
|
||||
<span class="link-url">Report Bugs or Suggestions</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Technical Information -->
|
||||
<div class="tech-info">
|
||||
<div class="tech-header">
|
||||
<span class="material-icons">code</span>
|
||||
<h4>Technical Information</h4>
|
||||
</div>
|
||||
<div class="tech-content">
|
||||
<div class="tech-item">
|
||||
<strong>🐍 Backend Technology</strong>
|
||||
<span>Python 3.8+ • FastAPI • AsyncIO • HTTPX</span>
|
||||
</div>
|
||||
<div class="tech-item">
|
||||
<strong>🌐 Frontend Technology</strong>
|
||||
<span>HTML5 • CSS3 • JavaScript ES6+ • Material Design 3.0</span>
|
||||
</div>
|
||||
<div class="tech-item">
|
||||
<strong>🔧 Supported Tools</strong>
|
||||
<span>SteamTools • GreenLuma</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Usage Notice -->
|
||||
<div class="usage-notice">
|
||||
<div class="notice-header">
|
||||
<span class="material-icons">info</span>
|
||||
<h4>Usage Notice</h4>
|
||||
</div>
|
||||
<div class="notice-content">
|
||||
<p>
|
||||
<strong>🖥️ System Requirements</strong> - Please ensure Windows 10/11
|
||||
is installed and Steam client is properly configured
|
||||
</p>
|
||||
<p>
|
||||
<strong>🛠️ Tool Preparation</strong> - Please install SteamTools or
|
||||
GreenLuma unlock tool before use
|
||||
</p>
|
||||
<p>
|
||||
<strong>🔒 Disclaimer</strong> - This tool is for educational and
|
||||
communication purposes only, users assume all related risks
|
||||
</p>
|
||||
<p>
|
||||
<strong>⭐ Support Project</strong> - If this tool helps you, feel
|
||||
free to star the project on GitHub
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Copyright Information -->
|
||||
<div class="copyright">
|
||||
<p>© 2025 Onekey Steam Unlock Tool • Authors: qwq-xinkeng && ikun0014</p>
|
||||
<p>
|
||||
Project URL:
|
||||
<a href="https://github.com/ikunshare/Onekey" target="_blank"
|
||||
>https://github.com/ikunshare/Onekey</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', path='js/theme.js') }}"></script>
|
||||
</div>
|
||||
</html>
|
||||
180
web/en/templates/index.html
Normal file
180
web/en/templates/index.html
Normal file
@@ -0,0 +1,180 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Onekey - Home</title>
|
||||
|
||||
<!-- Material Design 3 -->
|
||||
<link
|
||||
href="https://cdn.jsdmirror.com/gh/ikun0014/font@main/LXGWWenKaiMono-Regular/result.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Custom Styles -->
|
||||
<link rel="stylesheet" href="/static/css/style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- Top App Bar -->
|
||||
<header class="app-bar">
|
||||
<div class="app-bar-content">
|
||||
<span class="material-icons app-icon">games</span>
|
||||
<h1 class="app-title">Onekey</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="theme-toggle"
|
||||
id="themeToggle"
|
||||
title="Toggle Theme"
|
||||
>
|
||||
<span class="material-icons">light_mode</span>
|
||||
</button>
|
||||
<a href="/settings" class="btn btn-text settings-link">
|
||||
<span class="material-icons">settings</span>
|
||||
<span class="settings-text">Settings</span>
|
||||
</a>
|
||||
<a href="/about" class="btn btn-text about-link">
|
||||
<span class="material-icons">info</span>
|
||||
<span class="about-text">About</span>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="main-content">
|
||||
<!-- Configuration Status Card -->
|
||||
<div class="card config-card">
|
||||
<div class="card-header">
|
||||
<span class="material-icons">settings</span>
|
||||
<h2>Configuration Status</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="config-status" id="configStatus">
|
||||
<div class="loading">Checking configuration...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Unlock Card -->
|
||||
<div class="card unlock-card">
|
||||
<div class="card-header">
|
||||
<span class="material-icons">lock_open</span>
|
||||
<h2>Game Unlock</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<form id="unlockForm" class="unlock-form">
|
||||
<div class="input-group">
|
||||
<label for="appId" class="input-label">Steam App ID</label>
|
||||
<input
|
||||
type="text"
|
||||
id="appId"
|
||||
name="appId"
|
||||
class="text-field"
|
||||
placeholder="Enter game App ID"
|
||||
inputmode="numeric"
|
||||
autocomplete="off"
|
||||
autofocus
|
||||
required
|
||||
/>
|
||||
<div class="input-feedback" id="appIdFeedback"></div>
|
||||
<div class="input-helper">Example: 730 (CS2), 570 (Dota 2)</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label class="input-label">Unlock Tool</label>
|
||||
<div class="radio-group">
|
||||
<label class="radio-item">
|
||||
<input
|
||||
type="radio"
|
||||
name="toolType"
|
||||
value="steamtools"
|
||||
checked
|
||||
/>
|
||||
<span class="radio-button"></span>
|
||||
<span class="radio-label"
|
||||
>SteamTools (actively updated, recommended)</span
|
||||
>
|
||||
</label>
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="toolType" value="greenluma" />
|
||||
<span class="radio-button"></span>
|
||||
<span class="radio-label"
|
||||
>GreenLuma (updated yearly, no GUI)</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group" id="+DLCGroup">
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" id="+DLC" name="+DLC" />
|
||||
<span class="checkbox-button"></span>
|
||||
<span class="checkbox-label"
|
||||
>Retrieve and include all DLCs</span
|
||||
>
|
||||
</label>
|
||||
<div class="input-helper">
|
||||
Note: Some DLC depots are bundled with the base game and not
|
||||
separated
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button type="submit" class="btn btn-primary" id="unlockBtn">
|
||||
<span class="material-icons">play_arrow</span>
|
||||
Start Unlock
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" id="resetBtn">
|
||||
<span class="material-icons">refresh</span>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Log Card -->
|
||||
<div class="card progress-card">
|
||||
<div class="card-header">
|
||||
<span class="material-icons">timeline</span>
|
||||
<h2>Execution Log</h2>
|
||||
<div class="card-actions">
|
||||
<button class="btn btn-text" id="clearLogBtn">
|
||||
<span class="material-icons">clear_all</span>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="progress-container" id="progressContainer">
|
||||
<div class="progress-placeholder">
|
||||
<span class="material-icons">info</span>
|
||||
<p>Waiting for task to start...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Snackbar -->
|
||||
<div id="snackbar" class="snackbar">
|
||||
<div class="snackbar-content">
|
||||
<span id="snackbarMessage"></span>
|
||||
<button id="snackbarClose" class="snackbar-action">
|
||||
<span class="material-icons">close</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="{{ url_for('static', path='js/app.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='js/theme.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='js/project-info.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
501
web/en/templates/oobe.html
Normal file
501
web/en/templates/oobe.html
Normal file
@@ -0,0 +1,501 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Onekey - First Time Setup Wizard</title>
|
||||
|
||||
<!-- Material Design 3 -->
|
||||
<link
|
||||
href="https://cdn.jsdmirror.com/gh/ikun0014/font@main/LXGWWenKaiMono-Regular/result.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Custom Styles -->
|
||||
<link rel="stylesheet" href="/static/css/style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="oobe-container">
|
||||
<!-- Top App Bar -->
|
||||
<div class="oobe-card">
|
||||
<div class="oobe-header">
|
||||
<button
|
||||
type="button"
|
||||
class="theme-toggle"
|
||||
id="themeToggle"
|
||||
title="Toggle Theme"
|
||||
>
|
||||
<span class="material-icons">light_mode</span>
|
||||
</button>
|
||||
<div class="oobe-logo">
|
||||
<span class="material-icons" style="font-size: inherit"
|
||||
>extension</span
|
||||
>
|
||||
</div>
|
||||
<h1 class="oobe-title">Welcome to Onekey</h1>
|
||||
<p class="oobe-subtitle">One-click unlock, enjoy gaming experience</p>
|
||||
</div>
|
||||
|
||||
<div class="oobe-content">
|
||||
<div class="step-indicator">
|
||||
<div class="step-dot active" data-step="0"></div>
|
||||
<div class="step-dot" data-step="1"></div>
|
||||
<div class="step-dot" data-step="2"></div>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Welcome -->
|
||||
<div class="oobe-step active" data-step="0">
|
||||
<div class="welcome-text">
|
||||
<h3>🎮 Welcome to the Onekey World</h3>
|
||||
<p>
|
||||
Onekey is a powerful Steam game unlock tool that helps you
|
||||
easily manage and unlock games.
|
||||
</p>
|
||||
<p>
|
||||
Before getting started, we need to verify your activation key.
|
||||
</p>
|
||||
<p><strong>Features:</strong></p>
|
||||
<p>• Supports both SteamTools and GreenLuma unlock methods</p>
|
||||
<p>• Intuitive web interface, simple operation</p>
|
||||
<p>• Real-time log display, transparent process</p>
|
||||
<p>
|
||||
• Frontend code completely open source, absolutely no account
|
||||
theft/mining
|
||||
</p>
|
||||
<a href="https://shop.ikunshare.com" target="_blank"
|
||||
>• Click here to purchase activation key</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Key Verification -->
|
||||
<div class="oobe-step" data-step="1">
|
||||
<div class="welcome-text">
|
||||
<h3>🔑 Activate Your Key</h3>
|
||||
<p>
|
||||
Please enter your activation key to activate the Onekey tool.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="key-input-section">
|
||||
<div class="input-group">
|
||||
<label for="activationKey" class="input-label"
|
||||
>Activation Key</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="activationKey"
|
||||
class="text-field"
|
||||
placeholder="Please enter your key"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="input-helper">
|
||||
Key format: [PREFIX]_XXXXXXXX-XXXXXXXXXXXXXXXX
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="key-status" id="keyStatus">
|
||||
<div class="status-header">
|
||||
<span class="material-icons" id="statusIcon">info</span>
|
||||
<span id="statusMessage">Verifying...</span>
|
||||
</div>
|
||||
<div class="key-info" id="keyInfo"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 4: Complete -->
|
||||
<div class="oobe-step" data-step="3">
|
||||
<div class="welcome-text">
|
||||
<h3>🎉 Setup Complete</h3>
|
||||
<p>
|
||||
Congratulations! You have successfully activated the Onekey
|
||||
tool.
|
||||
</p>
|
||||
<p>Now you can start using all features.</p>
|
||||
<div
|
||||
class="key-info"
|
||||
id="finalKeyInfo"
|
||||
style="margin-top: 24px"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="oobe-actions">
|
||||
<button
|
||||
type="button"
|
||||
id="prevBtn"
|
||||
class="btn btn-text"
|
||||
style="display: none"
|
||||
>
|
||||
<span class="material-icons">arrow_back</span>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
id="nextBtn"
|
||||
class="btn btn-primary btn-large"
|
||||
>
|
||||
<span class="material-icons">arrow_forward</span>
|
||||
Next
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
id="verifyBtn"
|
||||
class="btn btn-primary btn-large"
|
||||
style="display: none"
|
||||
>
|
||||
<span class="material-icons">verified</span>
|
||||
Verify Key
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
id="finishBtn"
|
||||
class="btn btn-primary btn-large"
|
||||
style="display: none"
|
||||
>
|
||||
<span class="material-icons">check</span>
|
||||
Start Using
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="loading-overlay" id="loadingOverlay">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Snackbar -->
|
||||
<div id="snackbar" class="snackbar">
|
||||
<div class="snackbar-content">
|
||||
<span id="snackbarMessage"></span>
|
||||
<button id="snackbarClose" class="snackbar-action">
|
||||
<span class="material-icons">close</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
class OOBEManager {
|
||||
constructor() {
|
||||
this.currentStep = 0;
|
||||
this.totalSteps = 3;
|
||||
this.keyData = null;
|
||||
|
||||
this.initializeEventListeners();
|
||||
this.updateStepDisplay();
|
||||
}
|
||||
|
||||
initializeEventListeners() {
|
||||
document.getElementById("nextBtn").addEventListener("click", () => {
|
||||
this.nextStep();
|
||||
});
|
||||
|
||||
document.getElementById("prevBtn").addEventListener("click", () => {
|
||||
this.prevStep();
|
||||
});
|
||||
|
||||
document.getElementById("verifyBtn").addEventListener("click", () => {
|
||||
this.verifyKey();
|
||||
});
|
||||
|
||||
document.getElementById("finishBtn").addEventListener("click", () => {
|
||||
this.finishSetup();
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("activationKey")
|
||||
.addEventListener("input", () => {
|
||||
this.resetKeyStatus();
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("activationKey")
|
||||
.addEventListener("keypress", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
this.verifyKey();
|
||||
}
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("snackbarClose")
|
||||
.addEventListener("click", () => {
|
||||
this.hideSnackbar();
|
||||
});
|
||||
}
|
||||
|
||||
nextStep() {
|
||||
if (this.currentStep < this.totalSteps - 1) {
|
||||
this.currentStep++;
|
||||
this.updateStepDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
prevStep() {
|
||||
if (this.currentStep > 0) {
|
||||
this.currentStep--;
|
||||
this.updateStepDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
updateStepDisplay() {
|
||||
document.querySelectorAll(".step-dot").forEach((dot, index) => {
|
||||
dot.classList.remove("active", "completed");
|
||||
if (index < this.currentStep) {
|
||||
dot.classList.add("completed");
|
||||
} else if (index === this.currentStep) {
|
||||
dot.classList.add("active");
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll(".oobe-step").forEach((step, index) => {
|
||||
step.classList.toggle("active", index === this.currentStep);
|
||||
});
|
||||
|
||||
this.updateButtons();
|
||||
}
|
||||
|
||||
updateButtons() {
|
||||
const prevBtn = document.getElementById("prevBtn");
|
||||
const nextBtn = document.getElementById("nextBtn");
|
||||
const verifyBtn = document.getElementById("verifyBtn");
|
||||
const finishBtn = document.getElementById("finishBtn");
|
||||
|
||||
[prevBtn, nextBtn, verifyBtn, finishBtn].forEach((btn) => {
|
||||
btn.style.display = "none";
|
||||
});
|
||||
|
||||
if (this.currentStep > 0) {
|
||||
prevBtn.style.display = "flex";
|
||||
}
|
||||
|
||||
switch (this.currentStep) {
|
||||
case 0:
|
||||
nextBtn.style.display = "flex";
|
||||
break;
|
||||
case 1:
|
||||
verifyBtn.style.display = "flex";
|
||||
break;
|
||||
case 2:
|
||||
finishBtn.style.display = "flex";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
resetKeyStatus() {
|
||||
const keyStatus = document.getElementById("keyStatus");
|
||||
keyStatus.classList.remove("show", "success", "error");
|
||||
}
|
||||
|
||||
async verifyKey() {
|
||||
const keyInput = document.getElementById("activationKey");
|
||||
const key = keyInput.value.trim();
|
||||
|
||||
if (!key) {
|
||||
this.showSnackbar("Please enter the key", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!key.match(/^[A-Z0-9_-]+$/)) {
|
||||
this.showKeyStatus("error", "Invalid key format", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
this.showLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/getKeyInfo", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ key: key }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.key && data.info) {
|
||||
this.keyData = data.info;
|
||||
this.showKeyStatus(
|
||||
"success",
|
||||
"Key verified successfully!",
|
||||
"check_circle",
|
||||
);
|
||||
this.displayKeyInfo(data.info);
|
||||
|
||||
setTimeout(() => {
|
||||
this.nextStep();
|
||||
this.showFinalKeyInfo(data.info);
|
||||
}, 2000);
|
||||
} else {
|
||||
this.showKeyStatus(
|
||||
"error",
|
||||
data.message || "Key does not exist or has expired",
|
||||
"error",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.showKeyStatus(
|
||||
"error",
|
||||
"Verification failed, please check network connection",
|
||||
"error",
|
||||
);
|
||||
console.error("Key verification error:", error);
|
||||
} finally {
|
||||
this.showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
showKeyStatus(type, message, icon) {
|
||||
const keyStatus = document.getElementById("keyStatus");
|
||||
const statusIcon = document.getElementById("statusIcon");
|
||||
const statusMessage = document.getElementById("statusMessage");
|
||||
|
||||
statusIcon.textContent = icon;
|
||||
statusMessage.textContent = message;
|
||||
|
||||
keyStatus.className = `key-status show ${type}`;
|
||||
}
|
||||
|
||||
displayKeyInfo(keyInfo) {
|
||||
const keyInfoContainer = document.getElementById("keyInfo");
|
||||
const expiresAt = new Date(keyInfo.expiresAt);
|
||||
const isExpired = expiresAt < new Date();
|
||||
|
||||
const typeNames = {
|
||||
day: "Daily",
|
||||
week: "Weekly",
|
||||
month: "Monthly",
|
||||
year: "Yearly",
|
||||
permanent: "Permanent",
|
||||
};
|
||||
|
||||
keyInfoContainer.innerHTML = `
|
||||
<div class="key-info-item">
|
||||
<span class="material-icons">label</span>
|
||||
<span>Type: ${typeNames[keyInfo.type] || keyInfo.type}</span>
|
||||
</div>
|
||||
<div class="key-info-item">
|
||||
<span class="material-icons">schedule</span>
|
||||
<span>Expires: ${expiresAt.toLocaleDateString()}</span>
|
||||
</div>
|
||||
<div class="key-info-item">
|
||||
<span class="material-icons">analytics</span>
|
||||
<span>Usage Count: ${keyInfo.usageCount}</span>
|
||||
</div>
|
||||
<div class="key-info-item">
|
||||
<span class="material-icons">${keyInfo.isActive && !isExpired ? "check_circle" : "cancel"}</span>
|
||||
<span>Status: ${keyInfo.isActive && !isExpired ? "Valid" : "Invalid"}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
showFinalKeyInfo(keyInfo) {
|
||||
const finalKeyInfo = document.getElementById("finalKeyInfo");
|
||||
const expiresAt = new Date(keyInfo.expiresAt);
|
||||
|
||||
const typeNames = {
|
||||
day: "Daily",
|
||||
week: "Weekly",
|
||||
month: "Monthly",
|
||||
year: "Yearly",
|
||||
permanent: "Permanent",
|
||||
};
|
||||
|
||||
finalKeyInfo.innerHTML = `
|
||||
<div class="key-info-item">
|
||||
<span class="material-icons">verified_user</span>
|
||||
<span><strong>Key Type: </strong>${typeNames[keyInfo.type] || keyInfo.type}</span>
|
||||
</div>
|
||||
<div class="key-info-item">
|
||||
<span class="material-icons">event</span>
|
||||
<span><strong>Valid Until: </strong>${expiresAt.toLocaleDateString()} ${expiresAt.toLocaleTimeString()}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async finishSetup() {
|
||||
if (!this.keyData) {
|
||||
this.showSnackbar("Key data lost, please verify again", "error");
|
||||
this.currentStep = 1;
|
||||
this.updateStepDisplay();
|
||||
return;
|
||||
}
|
||||
|
||||
this.showLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/config/update", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
key: document.getElementById("activationKey").value.trim(),
|
||||
steam_path: "",
|
||||
debug_mode: false,
|
||||
logging_files: true,
|
||||
show_console: false,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showSnackbar(
|
||||
"Configuration saved successfully, redirecting...",
|
||||
"success",
|
||||
);
|
||||
setTimeout(() => {
|
||||
window.location.href = "/";
|
||||
}, 1500);
|
||||
} else {
|
||||
throw new Error(data.message || "Failed to save configuration");
|
||||
}
|
||||
} catch (error) {
|
||||
this.showSnackbar(
|
||||
"Failed to save configuration: " + error.message,
|
||||
"error",
|
||||
);
|
||||
console.error("Save config error:", error);
|
||||
} finally {
|
||||
this.showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
showLoading(show) {
|
||||
const overlay = document.getElementById("loadingOverlay");
|
||||
overlay.classList.toggle("show", show);
|
||||
}
|
||||
|
||||
showSnackbar(message, type = "info") {
|
||||
const snackbar = document.getElementById("snackbar");
|
||||
const snackbarMessage = document.getElementById("snackbarMessage");
|
||||
|
||||
snackbarMessage.textContent = message;
|
||||
snackbar.className = `snackbar ${type} show`;
|
||||
|
||||
setTimeout(() => {
|
||||
this.hideSnackbar();
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
hideSnackbar() {
|
||||
const snackbar = document.getElementById("snackbar");
|
||||
snackbar.classList.remove("show");
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
new OOBEManager();
|
||||
});
|
||||
</script>
|
||||
<script src="{{ url_for('static', path='js/theme.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
292
web/en/templates/settings.html
Normal file
292
web/en/templates/settings.html
Normal file
@@ -0,0 +1,292 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Onekey - Settings</title>
|
||||
|
||||
<!-- Material Design 3 -->
|
||||
<link
|
||||
href="https://cdn.jsdmirror.com/gh/ikun0014/font@main/LXGWWenKaiMono-Regular/result.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Custom Styles -->
|
||||
<link rel="stylesheet" href="/static/css/style.css" />
|
||||
<link rel="stylesheet" href="/static/css/settings.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- Top App Bar -->
|
||||
<header class="app-bar">
|
||||
<div class="app-bar-content">
|
||||
<button class="btn btn-text" onclick="goBack()">
|
||||
<span class="material-icons">arrow_back</span>
|
||||
</button>
|
||||
<span class="material-icons app-icon">settings</span>
|
||||
<h1 class="app-title">Settings</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="main-content settings-main">
|
||||
<!-- Key Management Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="material-icons">verified</span>
|
||||
<h2>Key Management</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="settings-section">
|
||||
<div id="keyInfoSection">
|
||||
<div class="loading">Loading key information...</div>
|
||||
</div>
|
||||
|
||||
<div class="key-change-section">
|
||||
<h4
|
||||
style="
|
||||
margin: 0 0 16px 0;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
"
|
||||
>
|
||||
<span
|
||||
class="material-icons"
|
||||
style="vertical-align: middle; margin-right: 8px"
|
||||
>swap_horiz</span
|
||||
>
|
||||
Change Key
|
||||
</h4>
|
||||
<div class="key-input-group">
|
||||
<div class="input-group" style="flex: 1; margin: 0">
|
||||
<label for="newKey" class="input-label">New Key</label>
|
||||
<input
|
||||
type="text"
|
||||
id="newKey"
|
||||
class="text-field"
|
||||
placeholder="Please enter new key"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="input-helper">
|
||||
Format: [PREFIX]_XXXXXXXX-XXXXXXXXXXXXXXXX
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
id="verifyNewKey"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
<span class="material-icons">check</span>
|
||||
Verify
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
id="changeKey"
|
||||
class="btn btn-primary"
|
||||
style="display: none"
|
||||
>
|
||||
<span class="material-icons">save</span>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Steam Configuration Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="material-icons">games</span>
|
||||
<h2>Steam Configuration</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="settings-section">
|
||||
<div class="input-group">
|
||||
<label for="steamPath" class="input-label"
|
||||
>Steam Installation Path</label
|
||||
>
|
||||
<div class="path-input-group">
|
||||
<input
|
||||
type="text"
|
||||
id="steamPath"
|
||||
class="text-field"
|
||||
placeholder="Leave blank for auto-detect, or manually enter Steam installation path"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
id="detectSteamPath"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
<span class="material-icons">search</span>
|
||||
Auto Detect
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-helper">
|
||||
The program will attempt to automatically detect the Steam
|
||||
installation path. If detection fails, please enter it
|
||||
manually. Usually located at: C:\Program Files (x86)\Steam
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-indicator" id="steamPathStatus">
|
||||
<span class="material-icons status-icon">info</span>
|
||||
<span class="status-text">Waiting for detection...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Application Configuration Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="material-icons">tune</span>
|
||||
<h2>Application Configuration</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="settings-section">
|
||||
<div class="setting-item">
|
||||
<label class="setting-label">Language Selection</label>
|
||||
<div class="radio-group">
|
||||
<label class="radio-item">
|
||||
<input
|
||||
type="radio"
|
||||
name="language"
|
||||
value="zh"
|
||||
id="language-zh"
|
||||
/>
|
||||
<span class="radio-button"></span>
|
||||
<span class="radio-label">Simplified Chinese</span>
|
||||
</label>
|
||||
<label class="radio-item">
|
||||
<input
|
||||
type="radio"
|
||||
name="language"
|
||||
value="en"
|
||||
id="language-en"
|
||||
checked
|
||||
/>
|
||||
<span class="radio-button"></span>
|
||||
<span class="radio-label">English</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" id="debugMode" />
|
||||
<span class="checkbox-button"></span>
|
||||
<div class="checkbox-content">
|
||||
<span class="checkbox-label">Debug Mode</span>
|
||||
<span class="checkbox-description"
|
||||
>Enable detailed debug log output</span
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" id="loggingFiles" />
|
||||
<span class="checkbox-button"></span>
|
||||
<div class="checkbox-content">
|
||||
<span class="checkbox-label">Save Log Files</span>
|
||||
<span class="checkbox-description"
|
||||
>Save logs to files for troubleshooting</span
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" id="showConsole" />
|
||||
<span class="checkbox-button"></span>
|
||||
<div class="checkbox-content">
|
||||
<span class="checkbox-label">Show Console Window</span>
|
||||
<span class="checkbox-description"
|
||||
>Display console window and log output on startup</span
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons Card -->
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="action-buttons">
|
||||
<button type="button" id="saveConfig" class="btn btn-primary">
|
||||
<span class="material-icons">save</span>
|
||||
Save Configuration
|
||||
</button>
|
||||
<button type="button" id="resetConfig" class="btn btn-secondary">
|
||||
<span class="material-icons">restore</span>
|
||||
Reset to Default
|
||||
</button>
|
||||
<button type="button" id="testConfig" class="btn btn-secondary">
|
||||
<span class="material-icons">check_circle</span>
|
||||
Test Configuration
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuration Information Display Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="material-icons">info</span>
|
||||
<h2>Configuration Status</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="config-status-grid" id="configStatusGrid">
|
||||
<div class="loading">Loading configuration status...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Confirmation Dialog -->
|
||||
<div id="confirmDialog" class="dialog-overlay">
|
||||
<div class="dialog">
|
||||
<div class="dialog-header">
|
||||
<h3 id="dialogTitle">Confirm Action</h3>
|
||||
</div>
|
||||
<div class="dialog-content">
|
||||
<p id="dialogMessage">
|
||||
Are you sure you want to perform this action?
|
||||
</p>
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<button type="button" id="dialogCancel" class="btn btn-text">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" id="dialogConfirm" class="btn btn-primary">
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Snackbar -->
|
||||
<div id="snackbar" class="snackbar">
|
||||
<div class="snackbar-content">
|
||||
<span id="snackbarMessage"></span>
|
||||
<button id="snackbarClose" class="snackbar-action">
|
||||
<span class="material-icons">close</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="{{ url_for('static', path='js/settings.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='js/theme.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='js/project-info.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,139 +0,0 @@
|
||||
<!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.8</span>
|
||||
<span class="version-type">Web UI</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-links">
|
||||
<a href="https://github.com/ikunshare/Onekey" target="_blank" class="project-link github">
|
||||
<span class="material-icons">code</span>
|
||||
<div class="link-content">
|
||||
<span class="link-title">GitHub 仓库</span>
|
||||
<span class="link-url">github.com/ikunshare/Onekey</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href="https://github.com/ikunshare/Onekey/releases" target="_blank" class="project-link releases">
|
||||
<span class="material-icons">file_download</span>
|
||||
<div class="link-content">
|
||||
<span class="link-title">下载发布版</span>
|
||||
<span class="link-url">获取最新版本</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href="https://shop.ikunshare.com" target="_blank" class="project-link buy_cdk">
|
||||
<span class="material-icons">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> - 请确保已安装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>
|
||||
@@ -1,150 +0,0 @@
|
||||
<!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>
|
||||
277
web/zh/static/css/animations.css
Normal file
277
web/zh/static/css/animations.css
Normal file
@@ -0,0 +1,277 @@
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInDown {
|
||||
from {
|
||||
transform: translateY(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
transform: translateY(60px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes subtle-rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% center;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% center;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
10%,
|
||||
30%,
|
||||
50%,
|
||||
70%,
|
||||
90% {
|
||||
transform: translateX(-5px);
|
||||
}
|
||||
20%,
|
||||
40%,
|
||||
60%,
|
||||
80% {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes success-pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.4);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 0 10px rgba(76, 175, 80, 0);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 rgba(76, 175, 80, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loading-bounce {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: scale(0);
|
||||
opacity: 0.5;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
.loading::before {
|
||||
content: "";
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--md-sys-color-primary-container);
|
||||
border-top: 2px solid var(--md-sys-color-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s cubic-bezier(0.68, -0.55, 0.265, 1.55) infinite;
|
||||
}
|
||||
|
||||
.loading-dots {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.loading-dots span {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--md-sys-color-primary);
|
||||
animation: loading-bounce 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.loading-dots span:nth-child(1) {
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
.loading-dots span:nth-child(2) {
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
.skeleton-loader {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--md-sys-color-surface-container) 25%,
|
||||
var(--md-sys-color-surface-container-high) 50%,
|
||||
var(--md-sys-color-surface-container) 75%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
.error-state {
|
||||
animation: shake 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.success-state {
|
||||
animation: success-pulse 0.5s ease-out;
|
||||
}
|
||||
|
||||
.ripple {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ripple::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: var(--ripple-y, 50%);
|
||||
left: var(--ripple-x, 50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
transform: translate(-50%, -50%);
|
||||
transition:
|
||||
width 0.6s ease,
|
||||
height 0.6s ease,
|
||||
opacity 0.6s ease;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.ripple:active::before {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fadeIn {
|
||||
animation: fadeIn var(--transition-medium) ease;
|
||||
}
|
||||
|
||||
.animate-slideUp {
|
||||
animation: slideUp var(--transition-medium) ease-out;
|
||||
}
|
||||
|
||||
.animate-slideIn {
|
||||
animation: slideIn var(--transition-medium) ease-out;
|
||||
}
|
||||
|
||||
.animate-pulse {
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-float {
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
165
web/zh/static/css/base.css
Normal file
165
web/zh/static/css/base.css
Normal file
@@ -0,0 +1,165 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
transition:
|
||||
background-color var(--transition-medium) ease,
|
||||
color var(--transition-medium) ease,
|
||||
border-color var(--transition-medium) ease;
|
||||
}
|
||||
|
||||
:root {
|
||||
transition:
|
||||
background-color 0.3s ease,
|
||||
color 0.3s ease;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family:
|
||||
"LXGW Wenkai Mono",
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
background-color: var(--md-sys-color-background);
|
||||
color: var(--md-sys-color-on-background);
|
||||
line-height: 1.5;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
top: -50%;
|
||||
right: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 30% 80%,
|
||||
rgba(103, 80, 164, 0.05) 0%,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 80% 20%,
|
||||
rgba(0, 188, 212, 0.05) 0%,
|
||||
transparent 50%
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] body::before {
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 30% 80%,
|
||||
rgba(208, 188, 255, 0.03) 0%,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 80% 20%,
|
||||
rgba(77, 208, 225, 0.03) 0%,
|
||||
transparent 50%
|
||||
);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--md-sys-color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--md-sys-color-primary);
|
||||
color: var(--md-sys-color-on-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] ::selection {
|
||||
background: var(--md-sys-color-primary);
|
||||
color: var(--md-sys-color-on-primary);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--md-sys-color-surface-container);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--md-sys-color-primary);
|
||||
border-radius: 6px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--gradient-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"]::-webkit-scrollbar-track {
|
||||
background: var(--md-sys-color-surface-container);
|
||||
}
|
||||
|
||||
[data-theme="dark"]::-webkit-scrollbar-thumb {
|
||||
background: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"]::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--gradient-primary);
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
font-family:
|
||||
"LXGW Wenkai Mono",
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
font-size: 12px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
[data-theme="dark"] input:-webkit-autofill,
|
||||
[data-theme="dark"] input:-webkit-autofill:hover,
|
||||
[data-theme="dark"] input:-webkit-autofill:focus {
|
||||
-webkit-text-fill-color: var(--md-sys-color-on-surface);
|
||||
-webkit-box-shadow: 0 0 0px 1000px var(--md-sys-color-surface-container) inset;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
|
||||
.btn,
|
||||
.card,
|
||||
.theme-toggle {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.btn:not(:hover),
|
||||
.card:not(:hover),
|
||||
.theme-toggle:not(:hover) {
|
||||
will-change: auto;
|
||||
}
|
||||
|
||||
button:active,
|
||||
.btn:active,
|
||||
.card:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
* {
|
||||
transition:
|
||||
background-color var(--transition-medium) ease,
|
||||
color var(--transition-medium) ease,
|
||||
border-color var(--transition-medium) ease,
|
||||
box-shadow var(--transition-medium) ease;
|
||||
}
|
||||
459
web/zh/static/css/components.css
Normal file
459
web/zh/static/css/components.css
Normal file
@@ -0,0 +1,459 @@
|
||||
.card {
|
||||
background: var(--md-sys-color-surface);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-radius: var(--md-sys-shape-corner-large);
|
||||
box-shadow: var(--md-sys-elevation-level1);
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
overflow: hidden;
|
||||
transition: all var(--transition-medium) cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
--hover-scale: 1.02;
|
||||
--hover-shadow: var(--md-sys-elevation-level3);
|
||||
animation: slideUp 0.6s ease-out backwards;
|
||||
}
|
||||
|
||||
.card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: var(--gradient-primary);
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition-medium) ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px) scale(var(--hover-scale, 1));
|
||||
box-shadow: var(--hover-shadow);
|
||||
}
|
||||
|
||||
.card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.card:nth-child(1) {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
.card:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
.card:nth-child(3) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
background: var(--md-sys-color-surface-container-low);
|
||||
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
||||
}
|
||||
|
||||
.card-header .material-icons {
|
||||
font-size: 28px;
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 14px 28px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
transition: all var(--transition-medium) cubic-bezier(0.4, 0, 0.2, 1);
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translate(-50%, -50%);
|
||||
transition:
|
||||
width var(--transition-slow) ease,
|
||||
height var(--transition-slow) ease;
|
||||
}
|
||||
|
||||
.btn:active::before {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--md-sys-color-primary);
|
||||
color: var(--md-sys-color-on-primary);
|
||||
box-shadow: var(--md-sys-elevation-level1);
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--gradient-primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--md-sys-elevation-level3);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--md-sys-color-secondary-container);
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: var(--md-sys-color-secondary);
|
||||
color: var(--md-sys-color-on-secondary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--md-sys-elevation-level2);
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
background-color: transparent;
|
||||
color: var(--md-sys-color-primary);
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.btn-text:hover:not(:disabled) {
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.text-field {
|
||||
padding: 16px;
|
||||
border: 2px solid var(--md-sys-color-outline-variant);
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
background-color: var(--md-sys-color-surface-container-low);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
font-size: 16px;
|
||||
transition: all var(--transition-fast) ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.text-field:hover {
|
||||
border-color: var(--md-sys-color-outline);
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
}
|
||||
|
||||
.text-field:focus {
|
||||
outline: none;
|
||||
border-color: var(--md-sys-color-primary);
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
box-shadow: 0 0 0 3px rgba(103, 80, 164, 0.15);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .text-field:focus {
|
||||
box-shadow: 0 0 0 3px rgba(208, 188, 255, 0.15);
|
||||
}
|
||||
|
||||
.input-helper {
|
||||
font-size: 12px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.radio-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
transition: background-color var(--transition-fast) ease;
|
||||
}
|
||||
|
||||
.radio-item:hover {
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
}
|
||||
|
||||
.radio-item input[type="radio"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.radio-button {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--md-sys-color-outline);
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
transition: all var(--transition-fast) ease;
|
||||
}
|
||||
|
||||
.radio-button::after {
|
||||
content: "";
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: var(--md-sys-color-primary);
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
transition: transform var(--transition-fast)
|
||||
cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
}
|
||||
|
||||
.radio-item input[type="radio"]:checked + .radio-button {
|
||||
border-color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.radio-item input[type="radio"]:checked + .radio-button::after {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
transition: background-color var(--transition-fast) ease;
|
||||
}
|
||||
|
||||
.checkbox-item:hover {
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
}
|
||||
|
||||
.checkbox-item input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.checkbox-button {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--md-sys-color-outline);
|
||||
border-radius: var(--md-sys-shape-corner-extra-small);
|
||||
position: relative;
|
||||
transition: all var(--transition-fast) ease;
|
||||
}
|
||||
|
||||
.checkbox-button::after {
|
||||
content: "✓";
|
||||
color: var(--md-sys-color-on-primary);
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
transition: transform var(--transition-fast)
|
||||
cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
}
|
||||
|
||||
.checkbox-item input[type="checkbox"]:checked + .checkbox-button {
|
||||
background: var(--md-sys-color-primary);
|
||||
border-color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.checkbox-item input[type="checkbox"]:checked + .checkbox-button::after {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: var(--md-sys-color-surface-container);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all var(--transition-fast) ease;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
background: var(--md-sys-color-primary-container);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.theme-toggle .material-icons {
|
||||
font-size: 20px;
|
||||
transition: transform var(--transition-medium) ease;
|
||||
}
|
||||
|
||||
.theme-toggle:active .material-icons {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.snackbar {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(100px);
|
||||
background: var(--md-sys-color-inverse-surface);
|
||||
color: var(--md-sys-color-inverse-on-surface);
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
box-shadow: var(--md-sys-elevation-level4);
|
||||
z-index: 2000;
|
||||
max-width: 560px;
|
||||
min-width: 344px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all var(--transition-medium) cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.snackbar.show {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.snackbar.success {
|
||||
background: var(--md-sys-color-success);
|
||||
color: var(--md-sys-color-on-success);
|
||||
}
|
||||
|
||||
.snackbar.error {
|
||||
background: var(--md-sys-color-error);
|
||||
color: var(--md-sys-color-on-error);
|
||||
}
|
||||
|
||||
.snackbar.warning {
|
||||
background: var(--md-sys-color-warning);
|
||||
color: var(--md-sys-color-on-warning);
|
||||
}
|
||||
|
||||
.snackbar.info {
|
||||
background: var(--md-sys-color-primary);
|
||||
color: var(--md-sys-color-on-primary);
|
||||
}
|
||||
|
||||
.snackbar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.snackbar-content span {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.snackbar-action {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 50%;
|
||||
transition: all var(--transition-fast) ease;
|
||||
}
|
||||
|
||||
.snackbar-action:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
background: var(--md-sys-color-surface-container);
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
transition: all var(--transition-fast) ease;
|
||||
}
|
||||
|
||||
.status-item:hover {
|
||||
transform: translateX(4px);
|
||||
border-color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 20px;
|
||||
transition: transform var(--transition-fast) ease;
|
||||
}
|
||||
|
||||
.status-item:hover .status-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.status-icon.success {
|
||||
color: var(--md-sys-color-success);
|
||||
}
|
||||
.status-icon.error {
|
||||
color: var(--md-sys-color-error);
|
||||
}
|
||||
.status-icon.warning {
|
||||
color: var(--md-sys-color-warning);
|
||||
}
|
||||
|
||||
.status-text {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
@media (prefers-contrast: high) {
|
||||
.card {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: 2px solid currentColor;
|
||||
}
|
||||
|
||||
.text-field {
|
||||
border-width: 3px;
|
||||
}
|
||||
}
|
||||
218
web/zh/static/css/layout.css
Normal file
218
web/zh/static/css/layout.css
Normal file
@@ -0,0 +1,218 @@
|
||||
.app-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 24px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.app-bar {
|
||||
background: var(--md-sys-color-surface);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: var(--md-sys-elevation-level1);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
transition: all var(--transition-medium) ease;
|
||||
}
|
||||
|
||||
.app-bar:hover {
|
||||
box-shadow: var(--md-sys-elevation-level2);
|
||||
}
|
||||
|
||||
.app-bar-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 16px 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
background: var(--gradient-primary);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
font-size: 32px;
|
||||
animation: subtle-rotate 20s linear infinite;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.app-version {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--md-sys-color-primary);
|
||||
background: var(--md-sys-color-primary-container);
|
||||
padding: 6px 16px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
border: 1px solid var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 32px 24px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.app-footer {
|
||||
background: var(--md-sys-color-surface);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
margin-top: 32px;
|
||||
border-top: 1px solid var(--md-sys-color-outline-variant);
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 32px 24px;
|
||||
}
|
||||
|
||||
.copyright {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
border-top: 1px solid var(--md-sys-color-outline-variant);
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.copyright p {
|
||||
font-size: 13px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
margin: 4px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.copyright a {
|
||||
color: var(--md-sys-color-primary);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.copyright a::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: var(--md-sys-color-primary);
|
||||
transition: width var(--transition-medium) ease;
|
||||
}
|
||||
|
||||
.copyright a:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.main-content {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.progress-card {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.app-bar-content {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.project-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.project-version {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.project-links {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.button-group .btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settings-text,
|
||||
.about-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.project-info-card {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.tech-info,
|
||||
.usage-notice {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.app-bar,
|
||||
.theme-toggle,
|
||||
.settings-link,
|
||||
.about-link,
|
||||
.btn,
|
||||
.snackbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body {
|
||||
background: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: none;
|
||||
border: 1px solid #ccc;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
:root {
|
||||
--md-sys-color-background: #ffffff !important;
|
||||
--md-sys-color-on-background: #000000 !important;
|
||||
--md-sys-color-surface: #ffffff !important;
|
||||
--md-sys-color-on-surface: #000000 !important;
|
||||
}
|
||||
}
|
||||
230
web/zh/static/css/oobe.css
Normal file
230
web/zh/static/css/oobe.css
Normal file
@@ -0,0 +1,230 @@
|
||||
.oobe-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--md-sys-color-primary-container) 0%,
|
||||
var(--md-sys-color-secondary-container) 100%
|
||||
);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.oobe-card {
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
background: var(--md-sys-color-surface);
|
||||
border-radius: var(--md-sys-shape-corner-extra-large);
|
||||
box-shadow: var(--md-sys-elevation-level3);
|
||||
overflow: hidden;
|
||||
animation: slideInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
.oobe-header {
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
var(--md-sys-color-primary),
|
||||
var(--md-sys-color-tertiary)
|
||||
);
|
||||
color: var(--md-sys-color-on-primary);
|
||||
padding: 40px 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.oobe-logo {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.oobe-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px 0;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.oobe-subtitle {
|
||||
font-size: 16px;
|
||||
opacity: 0.9;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.oobe-content {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.oobe-step {
|
||||
display: none;
|
||||
animation: fadeIn 0.4s ease-out;
|
||||
}
|
||||
|
||||
.oobe-step.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.step-indicator {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.step-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--md-sys-color-outline);
|
||||
margin: 0 6px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.step-dot.active {
|
||||
background: var(--md-sys-color-primary);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.step-dot.completed {
|
||||
background: var(--md-sys-color-tertiary);
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.welcome-text h3 {
|
||||
color: var(--md-sys-color-on-surface);
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.welcome-text p {
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
margin: 0 0 12px 0;
|
||||
line-height: 1.5;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.welcome-text a {
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
margin: 0 0 12px 0;
|
||||
line-height: 1.5;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.key-input-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.key-status {
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
background: var(--md-sys-color-surface-variant);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.key-status.show {
|
||||
display: block;
|
||||
animation: slideInDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
.key-status.success {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
border: 1px solid rgba(76, 175, 80, 0.3);
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.key-status.error {
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
border: 1px solid rgba(244, 67, 54, 0.3);
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.key-info {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.key-info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.key-info-item .material-icons {
|
||||
font-size: 18px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.oobe-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.btn-large {
|
||||
padding: 16px 32px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--md-sys-shape-corner-extra-large);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.loading-overlay.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid var(--md-sys-color-outline);
|
||||
border-top: 4px solid var(--md-sys-color-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.oobe-container {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.oobe-header {
|
||||
padding: 32px 24px;
|
||||
}
|
||||
|
||||
.oobe-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.key-info {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.oobe-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
314
web/zh/static/css/project-info.css
Normal file
314
web/zh/static/css/project-info.css
Normal file
@@ -0,0 +1,314 @@
|
||||
.project-info-card {
|
||||
background: var(--gradient-primary);
|
||||
border-radius: var(--md-sys-shape-corner-extra-large);
|
||||
padding: 32px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: var(--md-sys-elevation-level3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .project-info-card {
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--md-sys-color-primary-container) 0%,
|
||||
var(--md-sys-color-secondary-container) 100%
|
||||
);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
|
||||
.project-info-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
right: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(255, 255, 255, 0.1) 0%,
|
||||
transparent 70%
|
||||
);
|
||||
animation: rotate 30s linear infinite;
|
||||
}
|
||||
|
||||
.project-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.project-logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: var(--md-sys-shape-corner-large);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||
transition: transform var(--transition-medium) ease;
|
||||
}
|
||||
|
||||
.project-logo:hover {
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
}
|
||||
|
||||
.project-logo .material-icons {
|
||||
color: white;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.project-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.project-name {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
margin: 0 0 4px 0;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .project-info-card .project-name {
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
|
||||
.project-subtitle {
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .project-info-card .project-subtitle {
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
|
||||
.project-version {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.version-label {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
color: white;
|
||||
padding: 6px 16px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .project-info-card .version-label {
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
|
||||
.version-type {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.project-description {
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.project-description p {
|
||||
font-size: 15px;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.project-links {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 12px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.project-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(10px);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
transition: all var(--transition-medium) ease;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .project-link {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
|
||||
.project-link:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .project-link:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.project-link .material-icons {
|
||||
font-size: 24px;
|
||||
transition: transform var(--transition-fast) ease;
|
||||
}
|
||||
|
||||
.project-link:hover .material-icons {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.link-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.link-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.link-url {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.tech-info {
|
||||
background: var(--md-sys-color-surface-container);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
border-radius: var(--md-sys-shape-corner-large);
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: var(--md-sys-elevation-level1);
|
||||
transition: all var(--transition-medium) ease;
|
||||
}
|
||||
|
||||
.tech-info:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--md-sys-elevation-level3);
|
||||
}
|
||||
|
||||
.tech-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.tech-header .material-icons {
|
||||
font-size: 28px;
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.tech-header h4 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tech-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.tech-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 12px;
|
||||
background: var(--md-sys-color-surface-container-low);
|
||||
border-radius: var(--md-sys-shape-corner-small);
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
transition: all var(--transition-fast) ease;
|
||||
}
|
||||
|
||||
.tech-item:hover {
|
||||
transform: translateX(4px);
|
||||
border-color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.tech-item strong {
|
||||
color: var(--md-sys-color-on-surface);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tech-item span {
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.usage-notice {
|
||||
background: var(--md-sys-color-warning-container);
|
||||
border: 1px solid var(--md-sys-color-warning);
|
||||
border-radius: var(--md-sys-shape-corner-large);
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: var(--md-sys-elevation-level1);
|
||||
}
|
||||
|
||||
.notice-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.notice-header .material-icons {
|
||||
color: var(--md-sys-color-warning);
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.notice-header h4 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--md-sys-color-on-warning-container);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.notice-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.notice-content p {
|
||||
font-size: 14px;
|
||||
color: var(--md-sys-color-on-warning-container);
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
background: var(--md-sys-color-surface);
|
||||
border-radius: var(--md-sys-shape-corner-small);
|
||||
border-left: 3px solid var(--md-sys-color-warning);
|
||||
}
|
||||
|
||||
.notice-content strong {
|
||||
color: var(--md-sys-color-on-warning-container);
|
||||
font-weight: 600;
|
||||
}
|
||||
299
web/zh/static/css/settings.css
Normal file
299
web/zh/static/css/settings.css
Normal file
@@ -0,0 +1,299 @@
|
||||
.settings-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.checkbox-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
.checkbox-description {
|
||||
font-size: 13px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
#keyInfoSection {
|
||||
padding: 20px;
|
||||
background: var(--md-sys-color-surface-container-low);
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.key-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.key-info-card {
|
||||
background: var(--md-sys-color-surface-variant);
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.key-info-card:hover {
|
||||
background: var(--md-sys-color-primary-container);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.key-info-icon {
|
||||
color: var(--md-sys-color-primary);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.key-info-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.key-info-label {
|
||||
font-size: 12px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.key-info-value {
|
||||
font-size: 16px;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
font-weight: 500;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.key-status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--md-sys-shape-corner-small);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.key-status-badge.active {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.key-status-badge.expired {
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.key-status-badge.inactive {
|
||||
background: rgba(158, 158, 158, 0.1);
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
.key-change-section {
|
||||
margin-top: 24px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--md-sys-color-outline);
|
||||
}
|
||||
|
||||
.key-input-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.key-input-group .text-field {
|
||||
flex: 1;
|
||||
font-family: monospace;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.path-input-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.path-input-group .text-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: var(--md-sys-color-surface-container-low);
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.status-indicator .status-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.status-indicator .status-text {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.config-status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.config-status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: var(--md-sys-color-surface-container-low);
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
}
|
||||
|
||||
.config-status-item .material-icons {
|
||||
font-size: 24px;
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.config-status-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.config-status-label {
|
||||
font-size: 12px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.config-status-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1100;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all var(--transition-medium) ease;
|
||||
}
|
||||
|
||||
.dialog-overlay.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background: var(--md-sys-color-surface);
|
||||
border-radius: var(--md-sys-shape-corner-extra-large);
|
||||
box-shadow: var(--md-sys-elevation-level5);
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
transform: scale(0.9);
|
||||
transition: transform var(--transition-medium) ease;
|
||||
}
|
||||
|
||||
.dialog-overlay.show .dialog {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
padding: 24px 24px 16px;
|
||||
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
||||
}
|
||||
|
||||
.dialog-header h3 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.dialog-content p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
padding: 16px 24px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
border-top: 1px solid var(--md-sys-color-outline-variant);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.key-input-group {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.path-input-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.action-buttons .btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.config-status-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.btn.btn-text:first-child {
|
||||
margin-right: 8px;
|
||||
}
|
||||
8
web/zh/static/css/style.css
Normal file
8
web/zh/static/css/style.css
Normal file
@@ -0,0 +1,8 @@
|
||||
@import url("./variables.css");
|
||||
@import url("./base.css");
|
||||
@import url("./layout.css");
|
||||
@import url("./components.css");
|
||||
@import url("./animations.css");
|
||||
@import url("./oobe.css");
|
||||
@import url("./project-info.css");
|
||||
@import url("./utilities.css");
|
||||
222
web/zh/static/css/utilities.css
Normal file
222
web/zh/static/css/utilities.css
Normal file
@@ -0,0 +1,222 @@
|
||||
.progress-container {
|
||||
background: var(--md-sys-color-surface-container-low);
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
min-height: 200px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
}
|
||||
|
||||
.progress-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.progress-container::-webkit-scrollbar-track {
|
||||
background: var(--md-sys-color-surface-container);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.progress-container::-webkit-scrollbar-thumb {
|
||||
background: var(--md-sys-color-primary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.progress-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.progress-placeholder .material-icons {
|
||||
font-size: 48px;
|
||||
opacity: 0.3;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
||||
font-family: "Courier New", monospace;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
transition: background-color var(--transition-fast) ease;
|
||||
animation: slideIn var(--transition-medium) ease-out;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.log-entry::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background: var(--md-sys-color-primary);
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition-fast) ease;
|
||||
}
|
||||
|
||||
.log-entry:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.log-entry:hover {
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .log-entry {
|
||||
background: var(--md-sys-color-surface-container);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .log-entry:hover {
|
||||
background: var(--md-sys-color-surface-container-high);
|
||||
}
|
||||
|
||||
.log-entry:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.log-entry.warning {
|
||||
background-color: var(--md-sys-color-warning-container);
|
||||
border-left: 3px solid var(--md-sys-color-warning);
|
||||
}
|
||||
|
||||
.log-entry.error {
|
||||
background-color: var(--md-sys-color-error-container);
|
||||
border-left: 3px solid var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
.log-timestamp {
|
||||
font-family:
|
||||
"LXGW Wenkai Mono",
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font-size: 11px;
|
||||
min-width: 60px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
font-family:
|
||||
"LXGW Wenkai Mono",
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
.config-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.settings-link,
|
||||
.about-link {
|
||||
color: var(--md-sys-color-on-surface) !important;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
background: var(--md-sys-color-surface-container);
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
transition: all var(--transition-medium) ease;
|
||||
}
|
||||
|
||||
.settings-link:hover,
|
||||
.about-link:hover {
|
||||
background: var(--md-sys-color-primary-container);
|
||||
border-color: var(--md-sys-color-primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--md-sys-elevation-level2);
|
||||
}
|
||||
|
||||
.settings-link:hover .material-icons,
|
||||
.about-link:hover .material-icons {
|
||||
animation: rotate 1s ease-in-out;
|
||||
}
|
||||
|
||||
.settings-text,
|
||||
.about-text {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[data-tooltip] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[data-tooltip]::after {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-8px);
|
||||
background: var(--md-sys-color-inverse-surface);
|
||||
color: var(--md-sys-color-inverse-on-surface);
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--md-sys-shape-corner-small);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all var(--transition-fast) ease;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
[data-tooltip]:hover::after {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
|
||||
.unlock-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (prefers-contrast: high) {
|
||||
.card {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: 2px solid currentColor;
|
||||
}
|
||||
|
||||
.text-field {
|
||||
border-width: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) and (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) .card {
|
||||
box-shadow: var(--md-sys-elevation-level2);
|
||||
}
|
||||
}
|
||||
206
web/zh/static/css/variables.css
Normal file
206
web/zh/static/css/variables.css
Normal file
@@ -0,0 +1,206 @@
|
||||
:root {
|
||||
--transition-fast: 200ms;
|
||||
--transition-medium: 300ms;
|
||||
--transition-slow: 400ms;
|
||||
|
||||
--md-sys-color-primary: #6750a4;
|
||||
--md-sys-color-on-primary: #ffffff;
|
||||
--md-sys-color-primary-container: #e9ddff;
|
||||
--md-sys-color-on-primary-container: #22005d;
|
||||
|
||||
--md-sys-color-secondary: #00bcd4;
|
||||
--md-sys-color-on-secondary: #ffffff;
|
||||
--md-sys-color-secondary-container: #b2ebf2;
|
||||
--md-sys-color-on-secondary-container: #00363d;
|
||||
|
||||
--md-sys-color-tertiary: #ff6f00;
|
||||
--md-sys-color-on-tertiary: #ffffff;
|
||||
--md-sys-color-tertiary-container: #ffe0b2;
|
||||
--md-sys-color-on-tertiary-container: #4a1c00;
|
||||
|
||||
--md-sys-color-error: #dc2626;
|
||||
--md-sys-color-on-error: #ffffff;
|
||||
--md-sys-color-error-container: #fee2e2;
|
||||
--md-sys-color-on-error-container: #7f1d1d;
|
||||
|
||||
--md-sys-color-background: #fdfcff;
|
||||
--md-sys-color-on-background: #1a1c1e;
|
||||
|
||||
--md-sys-color-surface: #fdfcff;
|
||||
--md-sys-color-on-surface: #1a1c1e;
|
||||
--md-sys-color-surface-variant: #e7e0ec;
|
||||
--md-sys-color-on-surface-variant: #49454e;
|
||||
--md-sys-color-surface-container-lowest: #ffffff;
|
||||
--md-sys-color-surface-container-low: #f7f2fa;
|
||||
--md-sys-color-surface-container: #f1ecf4;
|
||||
--md-sys-color-surface-container-high: #ebe6ee;
|
||||
--md-sys-color-surface-container-highest: #e6e0e9;
|
||||
|
||||
--md-sys-color-outline: #79747e;
|
||||
--md-sys-color-outline-variant: #cac4cf;
|
||||
|
||||
--md-sys-color-success: #16a34a;
|
||||
--md-sys-color-on-success: #ffffff;
|
||||
--md-sys-color-success-container: #dcfce7;
|
||||
|
||||
--md-sys-color-warning: #f59e0b;
|
||||
--md-sys-color-on-warning: #ffffff;
|
||||
--md-sys-color-warning-container: #fef3c7;
|
||||
|
||||
--md-sys-color-surface-tint: #6750a4;
|
||||
--md-sys-color-scrim: #000000;
|
||||
--md-sys-color-inverse-surface: #313033;
|
||||
--md-sys-color-inverse-on-surface: #f4eff4;
|
||||
--md-sys-color-inverse-primary: #d0bcff;
|
||||
|
||||
--md-sys-elevation-level0: none;
|
||||
--md-sys-elevation-level1:
|
||||
0px 1px 3px 1px rgba(0, 0, 0, 0.15), 0px 1px 2px 0px rgba(0, 0, 0, 0.3);
|
||||
--md-sys-elevation-level2:
|
||||
0px 2px 6px 2px rgba(0, 0, 0, 0.15), 0px 1px 2px 0px rgba(0, 0, 0, 0.3);
|
||||
--md-sys-elevation-level3:
|
||||
0px 4px 8px 3px rgba(0, 0, 0, 0.15), 0px 1px 3px 0px rgba(0, 0, 0, 0.3);
|
||||
--md-sys-elevation-level4:
|
||||
0px 6px 10px 4px rgba(0, 0, 0, 0.15), 0px 2px 3px 0px rgba(0, 0, 0, 0.3);
|
||||
--md-sys-elevation-level5:
|
||||
0px 8px 12px 6px rgba(0, 0, 0, 0.15), 0px 4px 4px 0px rgba(0, 0, 0, 0.3);
|
||||
|
||||
--md-sys-shape-corner-none: 0px;
|
||||
--md-sys-shape-corner-extra-small: 4px;
|
||||
--md-sys-shape-corner-small: 8px;
|
||||
--md-sys-shape-corner-medium: 12px;
|
||||
--md-sys-shape-corner-large: 16px;
|
||||
--md-sys-shape-corner-extra-large: 28px;
|
||||
--md-sys-shape-corner-full: 999px;
|
||||
|
||||
--gradient-primary: linear-gradient(135deg, #6750a4 0%, #8b7cc4 100%);
|
||||
--gradient-secondary: linear-gradient(135deg, #00bcd4 0%, #4dd0e1 100%);
|
||||
--gradient-surface: linear-gradient(135deg, #fdfcff 0%, #f7f2fa 100%);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--md-sys-color-primary: #d0bcff;
|
||||
--md-sys-color-on-primary: #381e72;
|
||||
--md-sys-color-primary-container: #4f378a;
|
||||
--md-sys-color-on-primary-container: #e9ddff;
|
||||
|
||||
--md-sys-color-secondary: #4dd0e1;
|
||||
--md-sys-color-on-secondary: #00363d;
|
||||
--md-sys-color-secondary-container: #005662;
|
||||
--md-sys-color-on-secondary-container: #b2ebf2;
|
||||
|
||||
--md-sys-color-tertiary: #ffb74d;
|
||||
--md-sys-color-on-tertiary: #4a1c00;
|
||||
--md-sys-color-tertiary-container: #6a2c00;
|
||||
--md-sys-color-on-tertiary-container: #ffe0b2;
|
||||
|
||||
--md-sys-color-error: #f87171;
|
||||
--md-sys-color-on-error: #7f1d1d;
|
||||
--md-sys-color-error-container: #991b1b;
|
||||
--md-sys-color-on-error-container: #fee2e2;
|
||||
|
||||
--md-sys-color-background: #1a1c1e;
|
||||
--md-sys-color-on-background: #e3e2e6;
|
||||
|
||||
--md-sys-color-surface: #1a1c1e;
|
||||
--md-sys-color-on-surface: #e3e2e6;
|
||||
--md-sys-color-surface-variant: #49454e;
|
||||
--md-sys-color-on-surface-variant: #cac4cf;
|
||||
--md-sys-color-surface-container-lowest: #0e0f11;
|
||||
--md-sys-color-surface-container-low: #1a1c1e;
|
||||
--md-sys-color-surface-container: #1e2022;
|
||||
--md-sys-color-surface-container-high: #282a2d;
|
||||
--md-sys-color-surface-container-highest: #333538;
|
||||
|
||||
--md-sys-color-outline: #938f99;
|
||||
--md-sys-color-outline-variant: #49454e;
|
||||
|
||||
--md-sys-color-success: #4ade80;
|
||||
--md-sys-color-on-success: #14532d;
|
||||
--md-sys-color-success-container: #166534;
|
||||
|
||||
--md-sys-color-warning: #fbbf24;
|
||||
--md-sys-color-on-warning: #451a03;
|
||||
--md-sys-color-warning-container: #78350f;
|
||||
|
||||
--md-sys-color-surface-tint: #d0bcff;
|
||||
--md-sys-color-scrim: #000000;
|
||||
--md-sys-color-inverse-surface: #e6e0e9;
|
||||
--md-sys-color-inverse-on-surface: #313033;
|
||||
--md-sys-color-inverse-primary: #6750a4;
|
||||
|
||||
--md-sys-elevation-level0: none;
|
||||
--md-sys-elevation-level1:
|
||||
0px 1px 2px 0px rgba(0, 0, 0, 0.3), 0px 1px 3px 1px rgba(0, 0, 0, 0.15);
|
||||
--md-sys-elevation-level2:
|
||||
0px 1px 2px 0px rgba(0, 0, 0, 0.3), 0px 2px 6px 2px rgba(0, 0, 0, 0.15);
|
||||
--md-sys-elevation-level3:
|
||||
0px 1px 3px 0px rgba(0, 0, 0, 0.3), 0px 4px 8px 3px rgba(0, 0, 0, 0.15);
|
||||
--md-sys-elevation-level4:
|
||||
0px 2px 3px 0px rgba(0, 0, 0, 0.3), 0px 6px 10px 4px rgba(0, 0, 0, 0.15);
|
||||
--md-sys-elevation-level5:
|
||||
0px 4px 4px 0px rgba(0, 0, 0, 0.3), 0px 8px 12px 6px rgba(0, 0, 0, 0.15);
|
||||
|
||||
--gradient-primary: linear-gradient(135deg, #4f378a 0%, #6750a4 100%);
|
||||
--gradient-secondary: linear-gradient(135deg, #005662 0%, #00838f 100%);
|
||||
--gradient-surface: linear-gradient(135deg, #1a1c1e 0%, #1e2022 100%);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) {
|
||||
--md-sys-color-primary: #d0bcff;
|
||||
--md-sys-color-on-primary: #381e72;
|
||||
--md-sys-color-primary-container: #4f378a;
|
||||
--md-sys-color-on-primary-container: #e9ddff;
|
||||
--md-sys-color-secondary: #4dd0e1;
|
||||
--md-sys-color-on-secondary: #00363d;
|
||||
--md-sys-color-secondary-container: #005662;
|
||||
--md-sys-color-on-secondary-container: #b2ebf2;
|
||||
--md-sys-color-tertiary: #ffb74d;
|
||||
--md-sys-color-on-tertiary: #4a1c00;
|
||||
--md-sys-color-tertiary-container: #6a2c00;
|
||||
--md-sys-color-on-tertiary-container: #ffe0b2;
|
||||
--md-sys-color-error: #f87171;
|
||||
--md-sys-color-on-error: #7f1d1d;
|
||||
--md-sys-color-error-container: #991b1b;
|
||||
--md-sys-color-on-error-container: #fee2e2;
|
||||
--md-sys-color-background: #1a1c1e;
|
||||
--md-sys-color-on-background: #e3e2e6;
|
||||
--md-sys-color-surface: #1a1c1e;
|
||||
--md-sys-color-on-surface: #e3e2e6;
|
||||
--md-sys-color-surface-variant: #49454e;
|
||||
--md-sys-color-on-surface-variant: #cac4cf;
|
||||
--md-sys-color-surface-container-lowest: #0e0f11;
|
||||
--md-sys-color-surface-container-low: #1a1c1e;
|
||||
--md-sys-color-surface-container: #1e2022;
|
||||
--md-sys-color-surface-container-high: #282a2d;
|
||||
--md-sys-color-surface-container-highest: #333538;
|
||||
--md-sys-color-outline: #938f99;
|
||||
--md-sys-color-outline-variant: #49454e;
|
||||
--md-sys-color-success: #4ade80;
|
||||
--md-sys-color-on-success: #14532d;
|
||||
--md-sys-color-success-container: #166534;
|
||||
--md-sys-color-warning: #fbbf24;
|
||||
--md-sys-color-on-warning: #451a03;
|
||||
--md-sys-color-warning-container: #78350f;
|
||||
--md-sys-color-surface-tint: #d0bcff;
|
||||
--md-sys-color-scrim: #000000;
|
||||
--md-sys-color-inverse-surface: #e6e0e9;
|
||||
--md-sys-color-inverse-on-surface: #313033;
|
||||
--md-sys-color-inverse-primary: #6750a4;
|
||||
--md-sys-elevation-level0: none;
|
||||
--md-sys-elevation-level1:
|
||||
0px 1px 2px 0px rgba(0, 0, 0, 0.3), 0px 1px 3px 1px rgba(0, 0, 0, 0.15);
|
||||
--md-sys-elevation-level2:
|
||||
0px 1px 2px 0px rgba(0, 0, 0, 0.3), 0px 2px 6px 2px rgba(0, 0, 0, 0.15);
|
||||
--md-sys-elevation-level3:
|
||||
0px 1px 3px 0px rgba(0, 0, 0, 0.3), 0px 4px 8px 3px rgba(0, 0, 0, 0.15);
|
||||
--md-sys-elevation-level4:
|
||||
0px 2px 3px 0px rgba(0, 0, 0, 0.3), 0px 6px 10px 4px rgba(0, 0, 0, 0.15);
|
||||
--md-sys-elevation-level5:
|
||||
0px 4px 4px 0px rgba(0, 0, 0, 0.3), 0px 8px 12px 6px rgba(0, 0, 0, 0.15);
|
||||
--gradient-primary: linear-gradient(135deg, #4f378a 0%, #6750a4 100%);
|
||||
--gradient-secondary: linear-gradient(135deg, #005662 0%, #00838f 100%);
|
||||
--gradient-surface: linear-gradient(135deg, #1a1c1e 0%, #1e2022 100%);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ class SettingsManager {
|
||||
this.showConfirmDialog(
|
||||
"重置配置",
|
||||
"确定要重置所有配置为默认值吗?此操作不可恢复。",
|
||||
() => this.resetConfig()
|
||||
() => this.resetConfig(),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@ class SettingsManager {
|
||||
this.showConfirmDialog(
|
||||
"更换卡密",
|
||||
"确定要更换为新的卡密吗?更换后需要重新验证。",
|
||||
() => this.changeKey()
|
||||
() => this.changeKey(),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -198,10 +198,10 @@ class SettingsManager {
|
||||
<div class="key-info-label">卡密</div>
|
||||
<div class="key-info-value">${keyInfo.key.substring(
|
||||
0,
|
||||
8
|
||||
8,
|
||||
)}...${keyInfo.key.substring(
|
||||
keyInfo.key.length - 8
|
||||
)}</div>
|
||||
keyInfo.key.length - 8,
|
||||
)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -228,8 +228,8 @@ class SettingsManager {
|
||||
<div class="key-info-content">
|
||||
<div class="key-info-label">到期时间</div>
|
||||
<div class="key-info-value">${expiresAt.toLocaleDateString()} ${expiresAt
|
||||
.toLocaleTimeString()
|
||||
.substring(0, 5)}</div>
|
||||
.toLocaleTimeString()
|
||||
.substring(0, 5)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -238,8 +238,8 @@ class SettingsManager {
|
||||
<div class="key-info-content">
|
||||
<div class="key-info-label">使用次数</div>
|
||||
<div class="key-info-value">${keyInfo.usageCount} / ${
|
||||
keyInfo.totalUsage || "∞"
|
||||
}</div>
|
||||
keyInfo.totalUsage || "∞"
|
||||
}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -307,7 +307,7 @@ class SettingsManager {
|
||||
`验证成功!新卡密类型:${
|
||||
typeNames[data.info.type]
|
||||
},有效期至:${expiresAt.toLocaleDateString()}`,
|
||||
"success"
|
||||
"success",
|
||||
);
|
||||
} else {
|
||||
this.showSnackbar("新卡密无效或已过期", "error");
|
||||
@@ -583,7 +583,7 @@ class SettingsManager {
|
||||
<div class="status-card-description">${card.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
169
web/zh/templates/about.html
Normal file
169
web/zh/templates/about.html
Normal file
@@ -0,0 +1,169 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Onekey - About</title>
|
||||
|
||||
<!-- Material Design 3 -->
|
||||
<link
|
||||
href="https://cdn.jsdmirror.com/gh/ikun0014/font@main/LXGWWenKaiMono-Regular/result.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- 自定义样式 -->
|
||||
<link rel="stylesheet" href="/static/css/style.css" />
|
||||
</head>
|
||||
|
||||
<div class="footer-content">
|
||||
<!-- 项目介绍卡片 -->
|
||||
<div class="project-info-card">
|
||||
<div class="project-header">
|
||||
<div class="project-logo">
|
||||
<span class="material-icons">extension</span>
|
||||
</div>
|
||||
<div class="project-details">
|
||||
<h3 class="project-name">Onekey</h3>
|
||||
<p class="project-subtitle">直观,优雅的游戏解锁解决方案</p>
|
||||
</div>
|
||||
<div class="project-version">
|
||||
<span class="version-label">v2.0.8</span>
|
||||
<span class="version-type">Web UI</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-links">
|
||||
<a
|
||||
href="https://github.com/ikunshare/Onekey"
|
||||
target="_blank"
|
||||
class="project-link github"
|
||||
>
|
||||
<span class="material-icons">code</span>
|
||||
<div class="link-content">
|
||||
<span class="link-title">GitHub 仓库</span>
|
||||
<span class="link-url">github.com/ikunshare/Onekey</span>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/ikunshare/Onekey/releases"
|
||||
target="_blank"
|
||||
class="project-link releases"
|
||||
>
|
||||
<span class="material-icons">file_download</span>
|
||||
<div class="link-content">
|
||||
<span class="link-title">下载发布版</span>
|
||||
<span class="link-url">获取最新版本</span>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="https://shop.ikunshare.com"
|
||||
target="_blank"
|
||||
class="project-link buy_cdk"
|
||||
>
|
||||
<span class="material-icons">shopping_cart</span>
|
||||
<div class="link-content">
|
||||
<span class="link-title">购卡链接</span>
|
||||
<span class="link-url">购买卡密</span>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/qwq-xinkeng"
|
||||
target="_blank"
|
||||
class="project-link author"
|
||||
>
|
||||
<span class="material-icons">person</span>
|
||||
<div class="link-content">
|
||||
<span class="link-title">作者主页</span>
|
||||
<span class="link-url">github.com/qwq-xinkeng</span>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/ikun0014"
|
||||
target="_blank"
|
||||
class="project-link author"
|
||||
>
|
||||
<span class="material-icons">person</span>
|
||||
<div class="link-content">
|
||||
<span class="link-title">作者主页</span>
|
||||
<span class="link-url">github.com/ikun0014</span>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/ikunshare/Onekey/issues"
|
||||
target="_blank"
|
||||
class="project-link issues"
|
||||
>
|
||||
<span class="material-icons">bug_report</span>
|
||||
<div class="link-content">
|
||||
<span class="link-title">问题反馈</span>
|
||||
<span class="link-url">报告Bug或建议</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 技术信息 -->
|
||||
<div class="tech-info">
|
||||
<div class="tech-header">
|
||||
<span class="material-icons">code</span>
|
||||
<h4>技术信息</h4>
|
||||
</div>
|
||||
<div class="tech-content">
|
||||
<div class="tech-item">
|
||||
<strong>🐍 后端技术</strong>
|
||||
<span>Python 3.8+ • FastAPI • AsyncIO • HTTPX</span>
|
||||
</div>
|
||||
<div class="tech-item">
|
||||
<strong>🌐 前端技术</strong>
|
||||
<span>HTML5 • CSS3 • JavaScript ES6+ • Material Design 3.0</span>
|
||||
</div>
|
||||
<div class="tech-item">
|
||||
<strong>🔧 支持工具</strong>
|
||||
<span>SteamTools • GreenLuma</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 使用须知 -->
|
||||
<div class="usage-notice">
|
||||
<div class="notice-header">
|
||||
<span class="material-icons">info</span>
|
||||
<h4>使用须知</h4>
|
||||
</div>
|
||||
<div class="notice-content">
|
||||
<p>
|
||||
<strong>🖥️ 系统要求</strong> - 请确保已安装Windows
|
||||
10/11并正确配置Steam客户端
|
||||
</p>
|
||||
<p>
|
||||
<strong>🛠️ 工具准备</strong> -
|
||||
使用前请先安装SteamTools或GreenLuma解锁工具
|
||||
</p>
|
||||
<p>
|
||||
<strong>🔒 免责声明</strong> -
|
||||
本工具仅供学习交流使用,使用者需自行承担相关风险
|
||||
</p>
|
||||
<p>
|
||||
<strong>⭐ 支持项目</strong> -
|
||||
如果此工具对您有帮助,欢迎在GitHub上给项目点Star支持
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 版权信息 -->
|
||||
<div class="copyright">
|
||||
<p>© 2025 Onekey Steam解锁工具 • 作者:qwq-xinkeng && ikun0014</p>
|
||||
<p>
|
||||
项目地址:<a href="https://github.com/ikunshare/Onekey" target="_blank"
|
||||
>https://github.com/ikunshare/Onekey</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', path='js/theme.js') }}"></script>
|
||||
</div>
|
||||
</html>
|
||||
175
web/zh/templates/index.html
Normal file
175
web/zh/templates/index.html
Normal file
@@ -0,0 +1,175 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Onekey - Home</title>
|
||||
|
||||
<!-- Material Design 3 -->
|
||||
<link
|
||||
href="https://cdn.jsdmirror.com/gh/ikun0014/font@main/LXGWWenKaiMono-Regular/result.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- 自定义样式 -->
|
||||
<link rel="stylesheet" href="/static/css/style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- 顶部应用栏 -->
|
||||
<header class="app-bar">
|
||||
<div class="app-bar-content">
|
||||
<span class="material-icons app-icon">games</span>
|
||||
<h1 class="app-title">Onekey</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="theme-toggle"
|
||||
id="themeToggle"
|
||||
title="切换主题"
|
||||
>
|
||||
<span class="material-icons">light_mode</span>
|
||||
</button>
|
||||
<a href="/settings" class="btn btn-text settings-link">
|
||||
<span class="material-icons">settings</span>
|
||||
<span class="settings-text">设置</span>
|
||||
</a>
|
||||
<a href="/about" class="btn btn-text about-link">
|
||||
<span class="material-icons">info</span>
|
||||
<span class="about-text">关于本项目</span>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="main-content">
|
||||
<!-- 配置状态卡片 -->
|
||||
<div class="card config-card">
|
||||
<div class="card-header">
|
||||
<span class="material-icons">settings</span>
|
||||
<h2>配置状态</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="config-status" id="configStatus">
|
||||
<div class="loading">正在检查配置...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 游戏解锁卡片 -->
|
||||
<div class="card unlock-card">
|
||||
<div class="card-header">
|
||||
<span class="material-icons">lock_open</span>
|
||||
<h2>游戏解锁</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<form id="unlockForm" class="unlock-form">
|
||||
<div class="input-group">
|
||||
<label for="appId" class="input-label">Steam App ID</label>
|
||||
<input
|
||||
type="text"
|
||||
id="appId"
|
||||
name="appId"
|
||||
class="text-field"
|
||||
placeholder="请输入游戏的App ID"
|
||||
inputmode="numeric"
|
||||
autocomplete="off"
|
||||
autofocus
|
||||
required
|
||||
/>
|
||||
<div class="input-feedback" id="appIdFeedback"></div>
|
||||
<div class="input-helper">例如: 730 (CS2), 570 (Dota 2)</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label class="input-label">解锁工具</label>
|
||||
<div class="radio-group">
|
||||
<label class="radio-item">
|
||||
<input
|
||||
type="radio"
|
||||
name="toolType"
|
||||
value="steamtools"
|
||||
checked
|
||||
/>
|
||||
<span class="radio-button"></span>
|
||||
<span class="radio-label"
|
||||
>SteamTools(更新积极, 推荐使用)</span
|
||||
>
|
||||
</label>
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="toolType" value="greenluma" />
|
||||
<span class="radio-button"></span>
|
||||
<span class="radio-label">GreenLuma(一年一更, 无GUI)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group" id="+DLCGroup">
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" id="+DLC" name="+DLC" />
|
||||
<span class="checkbox-button"></span>
|
||||
<span class="checkbox-label">检索并入库所有DLC</span>
|
||||
</label>
|
||||
<div class="input-helper">
|
||||
需要注意: 有些DLC的Depot与游戏本体在一起, 不会分离
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button type="submit" class="btn btn-primary" id="unlockBtn">
|
||||
<span class="material-icons">play_arrow</span>
|
||||
开始解锁
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" id="resetBtn">
|
||||
<span class="material-icons">refresh</span>
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度日志卡片 -->
|
||||
<div class="card progress-card">
|
||||
<div class="card-header">
|
||||
<span class="material-icons">timeline</span>
|
||||
<h2>执行日志</h2>
|
||||
<div class="card-actions">
|
||||
<button class="btn btn-text" id="clearLogBtn">
|
||||
<span class="material-icons">clear_all</span>
|
||||
清空
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="progress-container" id="progressContainer">
|
||||
<div class="progress-placeholder">
|
||||
<span class="material-icons">info</span>
|
||||
<p>等待任务开始...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 提示框 -->
|
||||
<div id="snackbar" class="snackbar">
|
||||
<div class="snackbar-content">
|
||||
<span id="snackbarMessage"></span>
|
||||
<button id="snackbarClose" class="snackbar-action">
|
||||
<span class="material-icons">close</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 脚本 -->
|
||||
<script src="{{ url_for('static', path='js/app.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='js/theme.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='js/project-info.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -62,7 +62,9 @@
|
||||
<p>• 直观的 Web 界面,操作简单</p>
|
||||
<p>• 实时日志显示,过程透明</p>
|
||||
<p>• 前端代码完全开源, 绝对不盗号/挖矿</p>
|
||||
<a href="https://shop.ikunshare.com" target="_blank">• 点我购买卡密</a>
|
||||
<a href="https://shop.ikunshare.com" target="_blank"
|
||||
>• 点我购买卡密</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -147,6 +147,33 @@
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="settings-section">
|
||||
<div class="setting-item">
|
||||
<label class="setting-label">语言选择</label>
|
||||
<div class="radio-group">
|
||||
<label class="radio-item">
|
||||
<input
|
||||
type="radio"
|
||||
name="language"
|
||||
value="zh"
|
||||
id="language-zh"
|
||||
checked
|
||||
/>
|
||||
<span class="radio-button"></span>
|
||||
<span class="radio-label">简体中文</span>
|
||||
</label>
|
||||
<label class="radio-item">
|
||||
<input
|
||||
type="radio"
|
||||
name="language"
|
||||
value="en"
|
||||
id="language-en"
|
||||
/>
|
||||
<span class="radio-button"></span>
|
||||
<span class="radio-label">English</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" id="debugMode" />
|
||||
Reference in New Issue
Block a user