mirror of
https://github.com/dream-pep/koring-launcher.git
synced 2026-09-12 05:45:18 +08:00
feat: 发布 v1.2.0 版本,新增多项核心功能
- 引入 electron-updater 实现自动更新功能 - 新增 Java 环境扫描与校验的 IPC 处理逻辑 - 实现离线账号登录功能 - 新增配置变更跨进程广播机制 - 重构游戏启动逻辑,使用主进程内存配置作为唯一权威来源 - 新增界面显示与语言设置的配置页面 - 添加 Windows 平台自动发布 CI 流水线 - 迁移旧版配置/认证文件到用户数据目录 - 修复崩溃日志路径、表单控件等多项 bug - 重构设置页组件系统统一界面样式
This commit is contained in:
@@ -0,0 +1,85 @@
|
|||||||
|
# Koring Launcher Windows 发布流水线:构建 → SignPath 签名 → 发布 GitHub Releases
|
||||||
|
#
|
||||||
|
# 触发方式:
|
||||||
|
# 1) 推送 tag v*(推荐):先本地升版(node scripts/version.js 1.2.1)并打 tag v1.2.1,
|
||||||
|
# 推送后本流水线构建、签名并把产物发布到该 tag 对应的 GitHub Release。
|
||||||
|
# 2) 手动触发 workflow_dispatch:填版本号,构建产物发布到 v<版本号> Release。
|
||||||
|
#
|
||||||
|
# 需要的仓库 Secrets(Settings -> Secrets and variables -> Actions):
|
||||||
|
# SIGNPATH_API_TOKEN SignPath API Token(必填;放在 Environment "BUILDER" 的环境 Secrets 中,
|
||||||
|
# 本 job 声明 environment: BUILDER 后才能读取)
|
||||||
|
# SIGNPATH_ARTIFACT_CONFIG_SLUG 产物配置 slug(项目只有一个配置时可留空;仓库级 Secret)
|
||||||
|
#
|
||||||
|
# 组织 ID / slug 非密钥,直接写在下方 env(SignPath 后台确认值):
|
||||||
|
# OrganizationId 31ecd033-d59e-492b-a70b-b00a54bbc7c2
|
||||||
|
# ProjectSlug Koring_Launcher
|
||||||
|
# SigningPolicySlug Koring_Launcher_Dev_builder
|
||||||
|
#
|
||||||
|
# ⚠️ 签名策略说明:
|
||||||
|
# - 审批流程:SignPath 后台已关闭人工审批(自动批准),CI 可全自动。
|
||||||
|
# - 证书:当前策略 Purpose 为 Release signing(测试证书),用户机器默认不信任
|
||||||
|
# (SmartScreen / 杀软警告),正式对外发布需生产证书(OV/EV)+ 对应生产签名策略,
|
||||||
|
# 届时只需更换下方 SIGNPATH_SIGNING_POLICY_SLUG。
|
||||||
|
#
|
||||||
|
# 说明:
|
||||||
|
# - electron-builder 的 win.sign 会调用 scripts/signpath-sign.js 逐个文件远程签名,
|
||||||
|
# 签名后自动生成的 latest.yml / blockmap 哈希即对应签名后的安装包。
|
||||||
|
# - 发布使用 GITHUB_TOKEN(自动注入,需 contents: write 权限创建/更新 Release)。
|
||||||
|
|
||||||
|
name: Release Windows (SignPath)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Version to build and publish (e.g. 1.2.1)'
|
||||||
|
required: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-sign-publish:
|
||||||
|
runs-on: windows-latest
|
||||||
|
# 声明使用 Environment "BUILDER",才能读取其中的环境 Secret SIGNPATH_API_TOKEN
|
||||||
|
environment: BUILDER
|
||||||
|
env:
|
||||||
|
SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||||
|
SIGNPATH_ORG_ID: 31ecd033-d59e-492b-a70b-b00a54bbc7c2
|
||||||
|
SIGNPATH_PROJECT_SLUG: Koring_Launcher
|
||||||
|
SIGNPATH_SIGNING_POLICY_SLUG: Koring_Launcher_Dev_builder
|
||||||
|
SIGNPATH_ARTIFACT_CONFIG_SLUG: ${{ secrets.SIGNPATH_ARTIFACT_CONFIG_SLUG }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 11.7.0
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
# 手动触发时按输入升版(tag 触发时版本已在 package.json)
|
||||||
|
- name: Set version (manual runs only)
|
||||||
|
if: github.event_name == 'workflow_dispatch' && inputs.version != ''
|
||||||
|
run: node scripts/version.js ${{ inputs.version }}
|
||||||
|
|
||||||
|
- name: Build renderer + main (production)
|
||||||
|
run: pnpm build:run
|
||||||
|
|
||||||
|
- name: Switch production icons
|
||||||
|
run: pnpm icon:run
|
||||||
|
|
||||||
|
# 关键步骤:win.sign 自定义签名(SignPath)在打包过程中逐个签名内部 exe 与 setup.exe
|
||||||
|
- name: Package, sign and publish to GitHub Releases
|
||||||
|
run: pnpm exec electron-builder --win --publish always
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -13,6 +13,9 @@ dist-ssr
|
|||||||
dist-electron
|
dist-electron
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
|
# pnpm 11 project-local store
|
||||||
|
.pnpm-store/
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
|||||||
@@ -98,6 +98,15 @@ koring-launcher/
|
|||||||
│ │ │ ├── TaskButton.tsx # 标题栏任务指示器
|
│ │ │ ├── TaskButton.tsx # 标题栏任务指示器
|
||||||
│ │ │ └── TaskCard.tsx # 单个任务卡片
|
│ │ │ └── TaskCard.tsx # 单个任务卡片
|
||||||
│ │ ├── ui/ # shadcn/ui 组件
|
│ │ ├── ui/ # shadcn/ui 组件
|
||||||
|
│ │ ├── setting/ # 设置页原语(HeroUI 统一)
|
||||||
|
│ │ │ ├── SettingSurface.tsx # 卡片唯一原语(HeroUI Surface + 磨砂 + 圆角)
|
||||||
|
│ │ │ ├── SettingCard.tsx # 设置卡片(SettingSurface + 标准 padding)
|
||||||
|
│ │ │ ├── SettingRow.tsx # 设置行(label/desc + 控件)
|
||||||
|
│ │ │ ├── SettingBadge.tsx # 统一徽章(neutral/primary/success/warning/info/error/violet)
|
||||||
|
│ │ │ ├── SettingListItem.tsx # 列表项行(版本行/扫描行,支持选中态)
|
||||||
|
│ │ │ ├── SectionTitle.tsx # PageHeader/SectionTitle(HeroUI Typography)
|
||||||
|
│ │ │ ├── controls.tsx # 设置控件(Select/NumberField/Switch/Radio/TextArea/FilePicker + fieldCls)
|
||||||
|
│ │ │ └── Surface.tsx # Surface 兼容别名(旧 API,样式与卡片统一)
|
||||||
│ │ ├── VersionCard.tsx # 版本/更新卡片
|
│ │ ├── VersionCard.tsx # 版本/更新卡片
|
||||||
│ │ ├── UnderConstruction.tsx # "装修中" 占位组件
|
│ │ ├── UnderConstruction.tsx # "装修中" 占位组件
|
||||||
│ │ └── StartupPopup.tsx # 启动弹窗
|
│ │ └── StartupPopup.tsx # 启动弹窗
|
||||||
@@ -117,11 +126,12 @@ koring-launcher/
|
|||||||
│ │ └── devStore.ts # 开发者调试
|
│ │ └── devStore.ts # 开发者调试
|
||||||
│ ├── api/
|
│ ├── api/
|
||||||
│ │ ├── ipc.ts # 核心 IPC 工具 (invoke, onIpcEvent)
|
│ │ ├── ipc.ts # 核心 IPC 工具 (invoke, onIpcEvent)
|
||||||
│ │ ├── config.ts # AppConfig 读写
|
│ │ ├── config.ts # AppConfig 读写 + updateConfig(section, patch)
|
||||||
│ │ ├── auth.ts # 登录 API
|
│ │ ├── auth.ts # 登录 API
|
||||||
│ │ ├── background.ts # 背景控制
|
│ │ ├── background.ts # 背景控制
|
||||||
│ │ ├── install.ts # Minecraft 安装
|
│ │ ├── install.ts # Minecraft 安装
|
||||||
│ │ ├── launch.ts # 游戏启动
|
│ │ ├── launch.ts # 统一游戏启动 (launchGame / onGameEvent)
|
||||||
|
│ │ ├── java.ts # Java 检测 (scanJava / resolveJava)
|
||||||
│ │ ├── mods.ts # Mod 搜索
|
│ │ ├── mods.ts # Mod 搜索
|
||||||
│ │ ├── instance.ts # 实例 API
|
│ │ ├── instance.ts # 实例 API
|
||||||
│ │ └── update.ts # 应用更新
|
│ │ └── update.ts # 应用更新
|
||||||
@@ -142,15 +152,18 @@ koring-launcher/
|
|||||||
│ │ ├── auth.ts # Microsoft OAuth, Xbox Live, MC auth
|
│ │ ├── auth.ts # Microsoft OAuth, Xbox Live, MC auth
|
||||||
│ │ ├── installer.ts # @xmcl/installer
|
│ │ ├── installer.ts # @xmcl/installer
|
||||||
│ │ ├── launcher.ts # @xmcl/core 游戏启动
|
│ │ ├── launcher.ts # @xmcl/core 游戏启动
|
||||||
|
│ │ ├── launch-options.ts # 配置→LaunchOption 映射 (parseArgs/buildLaunchOptions/resolveJavaPath)
|
||||||
|
│ │ ├── paths.ts # 相对 gameDir 归一化 (resolveGamePath)
|
||||||
│ │ ├── modrinth.ts # Modrinth/CurseForge API
|
│ │ ├── modrinth.ts # Modrinth/CurseForge API
|
||||||
│ │ └── instance.ts # 实例管理
|
│ │ └── instance.ts # 实例管理(含 importExistingInstance sourceGamePath)
|
||||||
│ ├── handlers/ # IPC 处理器
|
│ ├── handlers/ # IPC 处理器
|
||||||
│ │ ├── config.ts # 配置读写
|
│ │ ├── config.ts # 配置读写 + config:update/config:changed
|
||||||
│ │ ├── auth.ts # 认证操作
|
│ │ ├── auth.ts # 认证操作
|
||||||
│ │ ├── install.ts # 安装操作
|
│ │ ├── install.ts # 安装操作
|
||||||
│ │ ├── launch.ts # 游戏启动
|
│ │ ├── launch.ts # 统一游戏启动 + afterLaunch
|
||||||
|
│ │ ├── java.ts # Java 检测 (java:scan / java:resolve)
|
||||||
│ │ ├── mods.ts # Mod 操作
|
│ │ ├── mods.ts # Mod 操作
|
||||||
│ │ ├── instance.ts # 实例操作
|
│ │ ├── instance.ts # 实例操作(gamePath 统一 resolveGamePath)
|
||||||
│ │ ├── background.ts # 背景操作
|
│ │ ├── background.ts # 背景操作
|
||||||
│ │ ├── task.ts # 任务系统
|
│ │ ├── task.ts # 任务系统
|
||||||
│ │ ├── system.ts # 系统信息
|
│ │ ├── system.ts # 系统信息
|
||||||
@@ -185,14 +198,37 @@ koring-launcher/
|
|||||||
|
|
||||||
| 数据类型 | 存储位置 | 格式 | 说明 |
|
| 数据类型 | 存储位置 | 格式 | 说明 |
|
||||||
|---------|---------|------|------|
|
|---------|---------|------|------|
|
||||||
| 用户设置 | 程序目录 `Koring.yml` | YAML | 所有可配置项 |
|
| 用户设置 | 打包:`app.getPath('userData')/Koring.yml`;开发:项目根 `Koring.yml` | YAML | 所有可配置项 |
|
||||||
| 认证数据 | 程序目录 `koring-auth.json` | JSON | token/xboxProfile |
|
| 认证数据 | 打包:`userData/koring-auth.json`;开发:项目根 | JSON | token/xboxProfile |
|
||||||
|
| 崩溃日志 | 打包:`userData/koring-crash.log`;开发:项目根 | JSONL | 崩溃记录,max 1000 行 |
|
||||||
| 任务历史 | localStorage `koring-task-history` | JSON | 临时,max 50 |
|
| 任务历史 | localStorage `koring-task-history` | JSON | 临时,max 50 |
|
||||||
|
|
||||||
|
> 打包模式统一使用系统用户数据目录,避免安装到 Program Files 等只读目录时写入失败。
|
||||||
|
> 旧版「exe 旁」文件在启动时由 `migrateLegacyFiles()`(`electron/main.ts`)自动复制迁移,复制不删除。
|
||||||
|
|
||||||
|
### 主进程权威写入模型(single source of truth)
|
||||||
|
|
||||||
|
```
|
||||||
|
渲染进程 setX(patch) ──config:update {section, patch}──▶ 主进程 updateConfig()
|
||||||
|
├─ 深度合并到内存缓存(getConfig())
|
||||||
|
├─ 300ms debounce 稀疏写盘(saveConfig)
|
||||||
|
└─ 广播 config:changed(完整配置)
|
||||||
|
渲染进程 onConfigChanged ──▶ configStore.applyChanged() 覆盖本地镜像
|
||||||
|
```
|
||||||
|
|
||||||
|
- 主进程内存缓存(`electron/config.ts` 的 `current`)是唯一权威;**渲染进程不直接写盘**。
|
||||||
|
- 启动游戏(`launch:launch`)直接读内存 `getConfig()`,保证永远用最新配置,无磁盘竞争。
|
||||||
|
- 退出时 `flushConfig()`(`window-all-closed`)强制写盘。
|
||||||
|
- 认证(Koring 账户)同步进配置时同样走 `updateConfig({ koringUser })` / `deleteConfigKey('koringUser')`。
|
||||||
|
|
||||||
### Koring.yml 结构
|
### Koring.yml 结构
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: 1
|
version: 1
|
||||||
|
oobe: true # 首次启动引导完成标记
|
||||||
|
|
||||||
|
app:
|
||||||
|
language: zh-CN # zh-CN | en-US(语言包开发中,仅保存偏好 + <html lang>)
|
||||||
|
|
||||||
theme:
|
theme:
|
||||||
darkMode: auto # auto | light | dark
|
darkMode: auto # auto | light | dark
|
||||||
@@ -215,6 +251,7 @@ game:
|
|||||||
resourceDir: ""
|
resourceDir: ""
|
||||||
savesDir: ""
|
savesDir: ""
|
||||||
instancesDir: .minecraft/instances
|
instancesDir: .minecraft/instances
|
||||||
|
gameDirs: [] # 已添加的游戏目录列表
|
||||||
|
|
||||||
java:
|
java:
|
||||||
javaPath: ""
|
javaPath: ""
|
||||||
@@ -231,6 +268,9 @@ advanced:
|
|||||||
gameArgs: ""
|
gameArgs: ""
|
||||||
preLaunchCmd: ""
|
preLaunchCmd: ""
|
||||||
debugMode: false
|
debugMode: false
|
||||||
|
server: # 快速进入服务器(ip 空则不自动加入)
|
||||||
|
ip: ""
|
||||||
|
port: 25565
|
||||||
|
|
||||||
download:
|
download:
|
||||||
fileSource: mirror # mirror | official | official-only
|
fileSource: mirror # mirror | official | official-only
|
||||||
@@ -242,15 +282,20 @@ network:
|
|||||||
securityId:
|
securityId:
|
||||||
enabled: false
|
enabled: false
|
||||||
authUrl: ""
|
authUrl: ""
|
||||||
|
|
||||||
|
ui:
|
||||||
|
showInstanceTitle: true # 首页实例标题
|
||||||
|
showTaskButton: true # 标题栏任务队列按钮
|
||||||
```
|
```
|
||||||
|
|
||||||
### 向上兼容策略
|
### 向上兼容策略
|
||||||
|
|
||||||
1. **版本号** — `version` 字段,每次结构变更递增
|
1. **版本号** — `version` 字段,每次结构变更递增
|
||||||
2. **默认值填充** — 加载时缺失字段自动补全,不丢数据
|
2. **默认值填充** — 加载时缺失字段自动补全,不丢数据(`loadConfig` 与 `DEFAULTS` 深度合并)
|
||||||
3. **迁移函数** — `migrate_v0_to_v1()` 等,按版本链执行
|
3. **迁移函数** — 结构变更时在 `migrate()` 中按版本链执行
|
||||||
4. **未知字段保留** — YAML 解析器保留不认识的字段
|
4. **未知字段保留** — YAML 解析器保留不认识的字段
|
||||||
5. **Debounce 写入** — 300ms debounce 避免频繁 IO
|
5. **Debounce 写入** — 主进程 300ms debounce 稀疏写盘,避免频繁 IO
|
||||||
|
6. **稀疏保存** — `diffValue(config, DEFAULTS)` 只写非默认值;默认值改回后从文件移除
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -295,33 +340,67 @@ win.mainWindow?.webContents.send('install:progress', data);
|
|||||||
|
|
||||||
| 频道 | 说明 |
|
| 频道 | 说明 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `config:get` / `config:save` | 配置读写 |
|
| `config:get` / `config:update` / `config:save` | 配置读写;`config:update` 提交 `{section, patch}`,主进程合并 + debounce 写盘 + 广播 `config:changed` |
|
||||||
|
| `config:changed`(事件) | 主进程广播完整配置,渲染端 `configStore.applyChanged` 覆盖镜像 |
|
||||||
|
| `config:preload`(事件) | 启动时推送初始配置 + isFirstLaunch |
|
||||||
| `auth:offline-login` / `auth:get` / `auth:save` / `auth:delete` | 认证操作 |
|
| `auth:offline-login` / `auth:get` / `auth:save` / `auth:delete` | 认证操作 |
|
||||||
| `install:minecraft` / `install:mod-loader` / `install:version-list` | 安装操作 |
|
| `install:minecraft` / `install:mod-loader` / `install:version-list` | 安装操作 |
|
||||||
| `launch:launch` / `launch:diagnose` | 游戏启动 |
|
| `launch:launch` / `launch:diagnose` | 统一游戏启动:`{instanceName, gamePath, profile, server?}`;主进程读权威配置 → `buildLaunchOptions` → `@xmcl/core launch`;事件经 `launch:event` 推送(stdout/stderr/window-ready/exit) |
|
||||||
|
| `java:scan` / `java:resolve` | Java 环境检测(JAVA_HOME/PATH/常见目录)/ 路径校验 |
|
||||||
| `mods:search` / `mods:install` | Mod 操作 |
|
| `mods:search` / `mods:install` | Mod 操作 |
|
||||||
| `instance:create` / `instance:list` / `instance:delete` | 实例操作 |
|
| `instance:create` / `instance:list` / `instance:delete` / `instance:install` / `instance:import` | 实例操作(启动统一走 `launch:launch`) |
|
||||||
| `background:set-image` / `background:set-color` / `background:reset` | 背景操作 |
|
| `background:set-image` / `background:set-color` / `background:reset` | 背景操作 |
|
||||||
| `task:progress` / `task:completed` | 任务进度 |
|
| `task:progress` / `task:completed` | 任务进度 |
|
||||||
| `system:info` | 系统信息 |
|
| `system:info` / `system:open-path` | 系统信息 / 打开路径 |
|
||||||
| `window:minimize` / `window:maximize` / `window:close` | 窗口控制 |
|
| `window:minimize` / `window:maximize` / `window:close` | 窗口控制 |
|
||||||
| `window:openSplash` / `window:closeSplash` | Splash 管理 |
|
| `window:openSplash` / `window:closeSplash` | Splash 管理 |
|
||||||
| `dialog:openFile` | 文件选择器 |
|
| `dialog:openFile` / `dialog:openFolder` | 文件/文件夹选择器 |
|
||||||
|
|
||||||
|
### 游戏启动链路(配置驱动)
|
||||||
|
|
||||||
|
```
|
||||||
|
launchStore.launch(instanceName, gamePath)
|
||||||
|
→ launchGame({instanceName, gamePath, profile, server?}) # src/api/launch.ts
|
||||||
|
→ ipc launch:launch # electron/handlers/launch.ts
|
||||||
|
→ getConfig() # 主进程内存权威配置
|
||||||
|
→ getInstanceInfo() # 实例 runtime / path / 健康检查
|
||||||
|
→ resolveJavaPath() # config.javaPath → 系统扫描 → PATH
|
||||||
|
→ buildLaunchOptions() # electron/core/launch-options.ts(配置→LaunchOption 映射)
|
||||||
|
→ @xmcl/core launch() + createMinecraftProcessWatcher
|
||||||
|
→ 事件 launch:event(stdout/stderr/window-ready/exit)+ playtime 累计
|
||||||
|
→ window-ready 时按 advanced.afterLaunch 处理启动器窗口(close/minimize/keep)
|
||||||
|
```
|
||||||
|
|
||||||
|
**配置 → 启动参数映射**(`buildLaunchOptions`):
|
||||||
|
|
||||||
|
| 配置字段 | 映射 |
|
||||||
|
|---|---|
|
||||||
|
| `java.memMode=auto` | 实例 minMemory/maxMemory(未设则 1024/4096) |
|
||||||
|
| `java.memMode=custom` | `min=min(2,memGB)G`,`max=memGB G` |
|
||||||
|
| `java.gc=zgc/g1` | `-XX:+UseZGC` / `-XX:+UseG1GC` |
|
||||||
|
| `java.jvmArgs` | 引号感知 `parseArgs` 并入 `extraJVMArgs` |
|
||||||
|
| `advanced.gameArgs` | `parseArgs` 并入 `extraMCArgs` |
|
||||||
|
| `advanced.winMode` | `resolution`(fullscreen / custom 宽高) |
|
||||||
|
| `advanced.preLaunchCmd` | `prependCommand`(Windows 批处理需 `cmd /c` 前缀) |
|
||||||
|
| `advanced.debugMode` | `-Dkoring.debugMode=true` + 日志流 |
|
||||||
|
| profile / server | `gameProfile`+`accessToken` / `server` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Zustand Stores
|
## Zustand Stores
|
||||||
|
|
||||||
### configStore (统一配置中心)
|
### configStore (主进程权威模型的渲染端镜像)
|
||||||
|
|
||||||
所有用户设置的单一数据源。读写通过 IPC 与 `Koring.yml` 同步。
|
配置的渲染端镜像。所有 setter 只向主进程提交 `{section, patch}`(`config:update`),不直接写盘;主进程合并后广播 `config:changed`,`applyChanged` 以广播为准覆盖本地。
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
config: AppConfig // 完整配置
|
config: AppConfig // 完整配置(镜像)
|
||||||
loaded: boolean // 是否已加载
|
loaded: boolean // 是否已加载
|
||||||
|
|
||||||
init() // 从主进程加载配置
|
init() // 兜底:从主进程加载配置(config:get)
|
||||||
setTheme(patch) // 部分更新 + debounce 300ms 写回
|
applyPreloaded() // 启动预载(config:preload)
|
||||||
|
applyChanged() // 主进程广播覆盖(config:changed)
|
||||||
|
setTheme(patch) // 乐观更新本地 + submit("theme", patch)
|
||||||
setA11y(patch)
|
setA11y(patch)
|
||||||
setBackground(patch)
|
setBackground(patch)
|
||||||
setGame(patch)
|
setGame(patch)
|
||||||
@@ -329,6 +408,8 @@ setJava(patch)
|
|||||||
setAdvanced(patch)
|
setAdvanced(patch)
|
||||||
setDownload(patch)
|
setDownload(patch)
|
||||||
setNetwork(patch)
|
setNetwork(patch)
|
||||||
|
setInstances(list) // 数组整体替换
|
||||||
|
setOobe(value)
|
||||||
```
|
```
|
||||||
|
|
||||||
### themeStore (委托 configStore)
|
### themeStore (委托 configStore)
|
||||||
@@ -432,3 +513,52 @@ z-200 StartupPopup 启动弹窗 (环境变量控制)
|
|||||||
- AbortController 取消机制
|
- AbortController 取消机制
|
||||||
- localStorage 持久化历史 (max 50)
|
- localStorage 持久化历史 (max 50)
|
||||||
- 任务类型: `install` / `download` / `update` / `launch` / `auth` / `sync` / `custom`
|
- 任务类型: `install` / `download` / `update` / `launch` / `auth` / `sync` / `custom`
|
||||||
|
|
||||||
|
### 设置原语与控件规范(HeroUI 3)
|
||||||
|
|
||||||
|
所有设置子页面共用 `src/components/setting/` 原语,**一套样式组合**:
|
||||||
|
|
||||||
|
| 原语 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| `SettingSurface` | 卡片唯一原语(HeroUI Surface + 磨砂 blur + rounded-xl + 统一边框/背景) |
|
||||||
|
| `SettingCard` | 设置卡片(= SettingSurface + px-5 py-4) |
|
||||||
|
| `SettingRow` | 设置行(label/desc 用 HeroUI Typography + 右侧控件) |
|
||||||
|
| `SectionTitle` / `PageHeader` | HeroUI Typography.Heading / Paragraph |
|
||||||
|
| `SettingBadge` | 统一徽章:neutral / primary / success / warning / info / error / violet |
|
||||||
|
| `SettingListItem` | 列表项行(版本行/扫描行,支持 selected 高亮) |
|
||||||
|
| `SettingSelect` / `SettingNumberField` / `SettingSwitch` / `SettingRadioGroup` / `SettingTextArea` / `SettingFilePicker` | HeroUI 控件封装 |
|
||||||
|
| `fieldCls` | 输入框统一样式类(见下方注意) |
|
||||||
|
|
||||||
|
**HeroUI 3 控件要点(易踩坑)**:
|
||||||
|
|
||||||
|
1. **事件用 `onChange` 而非 `onValueChange`**——HeroUI 3 基于 react-aria,`onValueChange` 不存在且静默失效(全项目已统一修复)。
|
||||||
|
2. **Radio 必须显式渲染圆点**:`<Radio.Control><Radio.Indicator /></Radio.Control>`,否则无选中指示器。
|
||||||
|
3. **field 默认无边框**:HeroUI 默认主题 `--field-border-width: 0px`,Input/TextArea/NumberField/Radio 需叠加 `fieldCls`(或等价边框类)保证视觉完整;Select.Trigger 已内置边框类。
|
||||||
|
4. **Select 单选**:`selectedKey` + `onSelectionChange`(回调可能是 `Key | null` 或 `Set<Key>`,控件内已做兼容)。
|
||||||
|
5. **NumberField**:Group 内需显式渲染 Increment/Decrement 按钮。
|
||||||
|
|
||||||
|
### 游戏目录版本识别(设置页 → 导入)
|
||||||
|
|
||||||
|
- 扫描任意目录(主目录 / 已添加目录)→ `instance:scan-dir` → `scanGameDirectories`(读 `versions/` 子目录,检测 JSON/JAR 健康度与 Forge/Fabric/Quilt/OptiFine 加载器)。
|
||||||
|
- **导入源 = 当前扫描目录**:`handleImport`/`handleImportAll` 传 `scanTarget` 作为 `sourceGamePath`,实例仍创建在主库 `gameDir/instances/`。
|
||||||
|
- **主目录变更自动重扫**(`useEffect([gameDir])`),旧结果先清空。
|
||||||
|
- **批量导入幂等**:先取 `listInstances` 名集合,已存在实例跳过并计入「跳过」。
|
||||||
|
- **相对 gameDir 归一化**:`electron/core/paths.ts` 的 `resolveGamePath` 在 IPC 边界(instance/launch/task)统一应用,相对路径按 exe 目录(打包)/ 项目根(开发)解析,与 `.minecraft` 创建位置一致。
|
||||||
|
|
||||||
|
### 设置子页面状态(参考 PCL2 组织)
|
||||||
|
|
||||||
|
| 分组 | 页面 | 状态 | 对接 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 通用 | 主页 / Koring 账户 / 关于 / 版权 | 保留 | — |
|
||||||
|
| 游戏 | 游戏账户&档案 | 实装 | `auth:offline-login`(离线账号);微软按钮占位 |
|
||||||
|
| 游戏 | Java虚拟机与内存 | 实装 | `java:scan` / `java:resolve` / `java.*` |
|
||||||
|
| 游戏 | 游戏目录 | 实装 | `instance:scan-dir` / `instance:import`(sourceGamePath) |
|
||||||
|
| 游戏 | 高级设置 | 实装 | `advanced.*`(含快速进入服务器 `advanced.server`) |
|
||||||
|
| 个性化 | 主题与背景 | 实装 | `theme.*` / `background.*` |
|
||||||
|
| 个性化 | 主界面 | 实装 | `ui.showInstanceTitle` / `ui.showTaskButton`(应用到 InstanceTitle / WindowControls) |
|
||||||
|
| 个性化 | 语言 | 实装 | `app.language`(保存 + `<html lang>`;语言包开发中) |
|
||||||
|
| 个性化 | 辅助功能 | 实装 | `a11y.*` |
|
||||||
|
| 网络 | 下载 | 实装 | `download.*` |
|
||||||
|
| 网络 | 安全识别服务 | 实装 | `network.securityId` |
|
||||||
|
| 网络 | 以太联机 / 陶瓦联机 | 占位 | 无后端接口 |
|
||||||
|
| 其他 | 服务与反馈 / 赞助我们 / 开发者选项 | 保留 | — |
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ build/ Build resources (generated, gitignored)
|
|||||||
**IPC Flow:**
|
**IPC Flow:**
|
||||||
Frontend → `ipcRenderer.invoke()` → `ipcMain.handle()` → main process → `webContents.send()` → Frontend
|
Frontend → `ipcRenderer.invoke()` → `ipcMain.handle()` → main process → `webContents.send()` → Frontend
|
||||||
|
|
||||||
|
**Config storage (main-process authoritative):**
|
||||||
|
- `Koring.yml` — 打包后存 `app.getPath('userData')`(`%APPDATA%/Koring Launcher/`),开发模式在项目根目录;旧版 exe 旁文件首次启动自动迁移
|
||||||
|
- 主进程内存缓存为唯一权威:渲染进程通过 `config:update` 提交补丁,主进程深度合并 → 300ms debounce 稀疏写盘 → 广播 `config:changed` 同步渲染端镜像
|
||||||
|
- `koring-auth.json` / `koring-crash.log` 与配置同策略(打包后 userData)
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -64,19 +69,21 @@ src/
|
|||||||
electron/
|
electron/
|
||||||
├── main.ts # Electron entry, window management, splash→main transition
|
├── main.ts # Electron entry, window management, splash→main transition
|
||||||
├── preload.ts # Context bridge (window.electronAPI)
|
├── preload.ts # Context bridge (window.electronAPI)
|
||||||
├── config.ts # YAML config management
|
├── config.ts # YAML config management (main-process authoritative, debounce sparse save)
|
||||||
├── auth.ts # Auth data persistence
|
├── auth.ts # Auth data persistence
|
||||||
├── core/ # @xmcl/* integrations
|
├── core/ # @xmcl/* integrations
|
||||||
│ ├── auth.ts # Microsoft OAuth, Xbox Live, MC auth
|
│ ├── auth.ts # Microsoft OAuth, Xbox Live, MC auth
|
||||||
│ ├── installer.ts # @xmcl/installer
|
│ ├── installer.ts # @xmcl/installer
|
||||||
│ ├── launcher.ts # @xmcl/core game launcher
|
│ ├── launcher.ts # Unified game launcher (@xmcl/core launch + config-driven)
|
||||||
|
│ ├── launch-options.ts # Config → LaunchOption mapping (parseArgs/buildLaunchOptions/resolveJavaPath)
|
||||||
│ ├── modrinth.ts # Modrinth/CurseForge API
|
│ ├── modrinth.ts # Modrinth/CurseForge API
|
||||||
│ └── instance.ts # Instance management
|
│ └── instance.ts # Instance management
|
||||||
├── handlers/ # IPC handlers
|
├── handlers/ # IPC handlers
|
||||||
│ ├── config.ts # Config load/save
|
│ ├── config.ts # Config load/save/update (config:get/update/save + config:changed broadcast)
|
||||||
│ ├── auth.ts # Auth operations
|
│ ├── auth.ts # Auth operations
|
||||||
│ ├── install.ts # Install operations
|
│ ├── install.ts # Install operations
|
||||||
│ ├── launch.ts # Game launch
|
│ ├── launch.ts # Unified game launch (launch:launch / launch:diagnose + afterLaunch)
|
||||||
|
│ ├── java.ts # Java detection (java:scan / java:resolve)
|
||||||
│ ├── mods.ts # Mod operations
|
│ ├── mods.ts # Mod operations
|
||||||
│ ├── instance.ts # Instance operations
|
│ ├── instance.ts # Instance operations
|
||||||
│ ├── background.ts # Background operations
|
│ ├── background.ts # Background operations
|
||||||
@@ -146,12 +153,13 @@ import { APP_ICON, DEFAULT_BG, LOGO_SVG, BUILD_MODE, isDev } from "@/lib/mode";
|
|||||||
|
|
||||||
## IPC Handlers
|
## IPC Handlers
|
||||||
|
|
||||||
- `config:*` — Config load/save
|
- `config:get` / `config:update` / `config:save` / `config:changed` — 配置读写(主进程权威:update 深度合并 + debounce 稀疏写盘 + 广播)
|
||||||
- `auth:*` — Microsoft OAuth, offline login
|
- `auth:*` — Microsoft OAuth, offline login
|
||||||
- `install:*` — Minecraft install, mod loader, version lists
|
- `install:*` — Minecraft install, mod loader, version lists
|
||||||
- `launch:*` — Game launch, diagnose
|
- `launch:launch` / `launch:diagnose` — 统一游戏启动:主进程读取权威配置自动应用 Java/内存/GC/JVM/游戏参数/窗口/启动前命令,事件经 `launch:event` 推送,window-ready 时按 `afterLaunch` 处理启动器窗口
|
||||||
|
- `java:scan` / `java:resolve` — Java 环境检测 / 路径校验
|
||||||
- `mods:*` — Modrinth/CurseForge search, install
|
- `mods:*` — Modrinth/CurseForge search, install
|
||||||
- `instance:*` — Instance CRUD
|
- `instance:*` — Instance CRUD(安装/导入/诊断;启动统一走 `launch:launch`)
|
||||||
- `background:*` — Background image/color/blur/animation/theme
|
- `background:*` — Background image/color/blur/animation/theme
|
||||||
- `task:*` — Task system progress
|
- `task:*` — Task system progress
|
||||||
- `system:*` — System info
|
- `system:*` — System info
|
||||||
@@ -166,8 +174,14 @@ import { APP_ICON, DEFAULT_BG, LOGO_SVG, BUILD_MODE, isDev } from "@/lib/mode";
|
|||||||
- **Transparent windows**: `transparent: true` + `frame: false` in BrowserWindow options.
|
- **Transparent windows**: `transparent: true` + `frame: false` in BrowserWindow options.
|
||||||
- **Mutable win ref**: `electron/main.ts` uses a mutable `win` object — all handlers read `win.mainWindow` at runtime (not captured at registration time).
|
- **Mutable win ref**: `electron/main.ts` uses a mutable `win` object — all handlers read `win.mainWindow` at runtime (not captured at registration time).
|
||||||
- **Asset paths**: Use `import.meta.env.BASE_URL` prefix for public assets (e.g., `${import.meta.env.BASE_URL}background.png`). Absolute paths like `/background.png` break in packaged app.
|
- **Asset paths**: Use `import.meta.env.BASE_URL` prefix for public assets (e.g., `${import.meta.env.BASE_URL}background.png`). Absolute paths like `/background.png` break in packaged app.
|
||||||
- **Config**: YAML format (`Koring.yml`) stored next to executable. Sparse save (only non-default values).
|
- **Config**: YAML format (`Koring.yml`). 打包后存 `app.getPath('userData')`(开发模式在项目根目录);主进程内存缓存为唯一权威,渲染进程经 `config:update` 提交、`config:changed` 同步,不在渲染端直接写盘。Sparse save(只写非默认值)。
|
||||||
- **Auth**: JSON file (`koring-auth.json`) stored next to executable.
|
- **Auth**: JSON file (`koring-auth.json`),打包后存 userData(与配置同策略)。
|
||||||
|
- **Launch is config-driven**: `launch:launch` 由主进程读取权威配置映射为 `@xmcl/core` 的 `LaunchOption`(`buildLaunchOptions`),前端只传实例名 + 游戏根目录 + 账户档案。
|
||||||
|
- **HeroUI 3 基于 react-aria**:Switch / RadioGroup 等使用 `onChange`(不是 `onValueChange`),`onValueChange` 在 HeroUI 3 中不存在且静默失效。
|
||||||
|
- **设置子页面统一原语**:`src/components/setting/`(SettingCard/SettingRow/SettingBadge/SettingListItem/controls.tsx + `fieldCls`),一套样式组合;Radio 必须显式渲染 `<Radio.Control><Radio.Indicator /></Radio.Control>` 才有圆点;HeroUI field 默认边框宽度为 0,输入框需叠加 `fieldCls`。
|
||||||
|
- **游戏目录导入**:版本从**当前扫描目录**导入(`sourceGamePath`),实例建在主库;主目录变更自动重扫;批量导入幂等(已存在跳过);相对 `gameDir`(默认 `.minecraft`)由主进程 `resolveGamePath` 按 exe 目录/项目根归一化。
|
||||||
|
- **设置页结构(参考 PCL2)**:通用(主页/Koring 账户/关于/版权)与其他(服务与反馈/赞助/开发者)保留;游戏组(账户·离线登录、Java、目录、高级·含快速进入服务器)、个性化组(主题背景/主界面/语言/辅助)、网络组(下载/安全识别)已对接设置接口;以太/陶瓦联机页暂为占位。新增配置段:`app.language`(语言偏好)、`ui.showInstanceTitle/showTaskButton`(主界面元素)、`advanced.server`(快速进入服务器,启动自动加入)。
|
||||||
|
- **离线账号登录**:`auth:offline-login` 处理器生成离线 UUID(MD5(OfflinePlayer:用户名));微软登录 UI 标注"开发中"。
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,277 @@
|
|||||||
|
# Koring Launcher 自动更新方案(v1 规划稿)
|
||||||
|
|
||||||
|
> 状态:**M1 已完成(2026-08-28)**;M2 待开始
|
||||||
|
> 目标平台:**Windows 优先**(NSIS exe 安装包),macOS/Linux 后续复用同一套架构
|
||||||
|
> 决策记录:
|
||||||
|
> - 安装器模式:**保持 assisted 安装器(`oneClick: false` + 可改安装目录)→ 每次更新整包下载**
|
||||||
|
> - 更新托管:**GitHub Releases**
|
||||||
|
> - 当前交付:**M1 基础设施已完成**(electron-updater 依赖 + publish 配置 + 1.2.0 打包验证)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 背景与目标
|
||||||
|
|
||||||
|
- 项目:Electron 33 + React 19 + TS + electron-builder 25(Windows NSIS 安装包,`dist:dev/beta/run` 三模式打包)
|
||||||
|
- 现状:`package.json` 版本 1.1.2,无任何更新机制;用户需手动下载新版安装包
|
||||||
|
- 目标:客户端内自动检查新版本 → 下载 → 静默重装 → 自动重启,先跑通 Windows
|
||||||
|
|
||||||
|
## 2. 技术选型
|
||||||
|
|
||||||
|
**选 electron-updater**(electron-builder 官方配套,与现有打包链无缝衔接)。
|
||||||
|
|
||||||
|
| 方案 | 说明 | 结论 |
|
||||||
|
|---|---|---|
|
||||||
|
| electron-updater | 自动生成 `latest.yml` 清单、SHA512 校验、进度事件、失败重试、断点续传 | ✅ 选用 |
|
||||||
|
| electron-simple-updater | 只支持替换 asar,不支持 NSIS 重装 | ❌ |
|
||||||
|
| update-electron-app | 只面向 GitHub,定制性差 | ❌ |
|
||||||
|
| 自研差分/自建服务 | 灰度、强制更新、统计需要时再评估 | 后期可选 |
|
||||||
|
|
||||||
|
**版本兼容**:electron-builder 25.1.8 ↔ electron-updater ^6.x(官方同仓库发布,6.x 与 24/25/26 打包器配套)。
|
||||||
|
|
||||||
|
## 3. 关键约束(已确认)
|
||||||
|
|
||||||
|
### 3.1 assisted 安装器 → 整包下载
|
||||||
|
|
||||||
|
当前 `electron-builder.yml` 为 assisted 安装器:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
nsis:
|
||||||
|
oneClick: false
|
||||||
|
allowToChangeInstallationDirectory: true
|
||||||
|
```
|
||||||
|
|
||||||
|
electron-updater 的 NSIS **差分更新(blockmap 增量)只支持一键安装(per-user oneClick)**;
|
||||||
|
assisted 安装器安装目录不固定,**退化为每次下载完整 `koring-launcher-<version>-setup.exe`**。
|
||||||
|
|
||||||
|
**已决策:接受整包下载**,不改安装体验。含义:
|
||||||
|
- 每次更新下载全量安装包(预计几十 MB 级别),CDN/带宽按此评估;
|
||||||
|
- 后续若用户量大、包体过大,可再评估改 oneClick 启用差分,或自研增量(代价高,不优先)。
|
||||||
|
|
||||||
|
### 3.2 代码签名(重要)
|
||||||
|
|
||||||
|
Windows 下 electron-updater 会对下载的安装包做 Authenticode 校验:当前 exe 有签名时,要求新安装包发布者一致;
|
||||||
|
当前 exe 未签名时校验会被跳过(记警告)。
|
||||||
|
|
||||||
|
- **未签名**:开发/内测可跑通全流程,但国内杀软对未签名 exe 误报率高;
|
||||||
|
- **正式对外发布前必须做代码签名**(OV 证书起步,EV 更佳),并让发布流水线对 setup.exe 签名;
|
||||||
|
- 签名后 electron-builder 默认会校验一致性,无需额外配置,但要保证**发布流水线签名证书与产物一致**。
|
||||||
|
|
||||||
|
## 4. 整体架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌───────────────────────────── 客户端 ─────────────────────────────┐
|
||||||
|
│ React UI (src/) ⇄ preload.ts (contextBridge) ⇄ 主进程 │
|
||||||
|
│ │ electronAPI.onUpdateStatus() │ │
|
||||||
|
│ └────────── IPC ────────────────┘ │
|
||||||
|
│ 主进程 electron/updater.ts(封装 electron-updater) │
|
||||||
|
│ electron/handlers/update.ts(IPC handler) │
|
||||||
|
└──────────────────────────────┬──────────────────────────────────┘
|
||||||
|
│ HTTPS
|
||||||
|
▼
|
||||||
|
┌───────────────────────────┐
|
||||||
|
│ GitHub Releases (仓库) │
|
||||||
|
│ · koring-launcher-1.2.1 │
|
||||||
|
│ -setup.exe │
|
||||||
|
│ · latest.yml │
|
||||||
|
└───────────────────────────┘
|
||||||
|
(国内网络差 → 见 §5.3 加速对策)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 服务端:GitHub Releases
|
||||||
|
|
||||||
|
### 5.1 发布产物
|
||||||
|
|
||||||
|
electron-builder 配置 `publish: { provider: github }` 后,`electron-builder --publish always`
|
||||||
|
(或 `--publish` 搭配 CI token)会自动:
|
||||||
|
|
||||||
|
1. 打包 Windows 产物;
|
||||||
|
2. 创建/更新 GitHub Release(tag 取自版本号,如 `v1.2.1`);
|
||||||
|
3. 上传 `koring-launcher-1.2.1-setup.exe` + `latest.yml`(+ `latest.yml.blockmap`,assisted 模式下不用但会生成)。
|
||||||
|
|
||||||
|
`latest.yml` 是更新清单(版本、文件路径、大小、sha512),electron-updater 靠它发现新版本并校验完整性:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: 1.2.1
|
||||||
|
files:
|
||||||
|
- url: koring-launcher-1.2.1-setup.exe
|
||||||
|
sha512: <base64-sha512>
|
||||||
|
size: 81234567
|
||||||
|
path: koring-launcher-1.2.1-setup.exe
|
||||||
|
sha512: <base64-sha512>
|
||||||
|
releaseDate: '2025-01-01T00:00:00.000Z'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 配置示例(electron-builder.yml 增加段)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
publish:
|
||||||
|
provider: github
|
||||||
|
owner: <GitHub 用户名/组织>
|
||||||
|
repo: <仓库名>
|
||||||
|
# releaseType: release # 默认 release;beta 通道可设 draft/prerelease
|
||||||
|
```
|
||||||
|
|
||||||
|
打包时 electron-builder 会把 `app-update.yml`(含 provider 信息)写进 `resources/`,
|
||||||
|
electron-updater 在运行时读取它——**没有 publish 配置就不会生成 app-update.yml,更新会直接报错**(M1 验收点)。
|
||||||
|
|
||||||
|
### 5.3 国内网络注意(务必先评估)
|
||||||
|
|
||||||
|
electron-updater 的 GitHub provider 走 `api.github.com`(发现版本)与 `github.com/.../releases/download/...`(下载),
|
||||||
|
国内部分网络环境访问慢或不稳定。对策(按需选):
|
||||||
|
|
||||||
|
- **现状接受**:很多应用直接走 GitHub,配合失败重试 + 手动"下载最新版"兜底按钮;
|
||||||
|
- **CDN 加速(推荐后续做)**:改为 `generic` provider,把 `latest.yml` + exe 同步到
|
||||||
|
OSS/COS + CDN(或 ghproxy 类代理),`app-update.yml` 指向 CDN URL;
|
||||||
|
- **自建/商业 CDN 代理 GitHub Releases**:等用户量上来再评估。
|
||||||
|
|
||||||
|
> 方案设计上保持 provider 可切换:`electron/updater.ts` 只面向 electron-updater 统一 API,
|
||||||
|
> 未来从 `github` 切 `generic` 只需改 electron-builder.yml + 重新打包,业务代码不动。
|
||||||
|
|
||||||
|
## 6. Windows 更新流程(整包下载版)
|
||||||
|
|
||||||
|
```
|
||||||
|
应用启动
|
||||||
|
├─ 加载完成且空闲后(延迟 10~15s)→ 静默 checkForUpdates()
|
||||||
|
│ (避开启动加载与 Minecraft 下载抢带宽;首启/开发模式跳过)
|
||||||
|
├─ 无更新 → 结束,静默
|
||||||
|
└─ 有更新 → 发 update:status{state:'available', version}
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
前端提示"发现新版本 vX.Y.Z"(非强制,可忽略/稍后)
|
||||||
|
│ 用户点"下载更新"
|
||||||
|
▼
|
||||||
|
autoUpdater.downloadUpdate()
|
||||||
|
├─ download-progress → update:status{state:'downloading', percent, speed, ...}
|
||||||
|
├─ 下载完成 → SHA512 校验(latest.yml)→ update:status{state:'downloaded'}
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
前端提示"重启并安装"(可暂缓;退出时 autoInstallOnAppQuit 兜底)
|
||||||
|
│ 用户确认
|
||||||
|
▼
|
||||||
|
autoUpdater.quitAndInstall()
|
||||||
|
├─ 应用退出 → NSIS 静默安装(不弹 UI,沿用原安装目录)
|
||||||
|
└─ 安装完成自动重启 → 运行新版本
|
||||||
|
|
||||||
|
失败路径:
|
||||||
|
· 网络失败 → 提示重试(electron-updater 自带断点续传/重试)
|
||||||
|
· 校验失败 → 清除缓存重下;仍失败则提示手动下载最新版
|
||||||
|
· 静默安装失败 → 提示手动下载;保留旧版本可用
|
||||||
|
```
|
||||||
|
|
||||||
|
### 更新状态机(前端 store 用)
|
||||||
|
|
||||||
|
```
|
||||||
|
idle → checking → available → downloading → downloaded → installing → relaunch
|
||||||
|
└─not-available→ idle └─ error → idle(可重试)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 代码结构规划
|
||||||
|
|
||||||
|
```
|
||||||
|
electron/
|
||||||
|
updater.ts # 新增:封装 electron-updater
|
||||||
|
# · 初始化(读 app-update.yml、设日志)
|
||||||
|
# · 守卫:!app.isPackaged 时跳过(开发模式)
|
||||||
|
# · check() / download() / quitAndInstall() / 状态查询
|
||||||
|
# · 订阅 checking/available/not-available/download-progress/downloaded/error
|
||||||
|
# · 转发为 update:status 事件(带完整 payload)
|
||||||
|
handlers/update.ts # 新增:IPC handler
|
||||||
|
# · update:check → 触发检查
|
||||||
|
# · update:download → 触发下载
|
||||||
|
# · update:quitAndInstall → 重启安装
|
||||||
|
# · update:getState → 查询当前状态/版本信息
|
||||||
|
main.ts # 改:import { registerUpdateHandlers };注册;启动后延迟静默检查
|
||||||
|
preload.ts # 改:暴露
|
||||||
|
# · checkForUpdates() / downloadUpdate() / quitAndInstall()
|
||||||
|
# · onUpdateStatus(cb) → 订阅 update:status(沿用现有 on() 模式)
|
||||||
|
|
||||||
|
src/
|
||||||
|
api/update.ts # 新增:IPC 封装 + TS 类型(UpdateStatus)
|
||||||
|
stores/update.ts # 新增:zustand store,维护状态机
|
||||||
|
components/settings/... # 更新 UI:设置页"关于/更新"区块
|
||||||
|
# · 当前版本号 + 检查更新按钮
|
||||||
|
# · 下载进度条(百分比/速度/剩余大小)
|
||||||
|
# · "重启并安装"确认
|
||||||
|
components/... # 可选:新版可用 toast / 弹窗(非强制打扰)
|
||||||
|
```
|
||||||
|
|
||||||
|
**依赖注意**:`electron-updater` 必须放 **`dependencies`**(不能是 devDependencies),
|
||||||
|
否则 asar 打包后运行时找不到模块——最常见事故,M1 验收必须覆盖。
|
||||||
|
|
||||||
|
## 8. 版本与多通道
|
||||||
|
|
||||||
|
- 沿用现有 `scripts/version.js` 升版(`pnpm version:set 1.2.0`),版本必须严格 semver 递增;
|
||||||
|
- electron-updater 只认 package.json 的 `version`,升版后需重新打包发布;
|
||||||
|
- **dev / beta / run 三模式的通道隔离放加固阶段(M5)**:
|
||||||
|
- GitHub provider 多通道依赖 tag 约定(如 `v1.2.1-beta.1`),行为略绕,先只用默认 latest 通道跑通;
|
||||||
|
- 届时按产品需要决定:beta 走 prerelease/draft release,run 走正式 release,或切 generic + 不同 URL。
|
||||||
|
|
||||||
|
## 9. 里程碑与验收
|
||||||
|
|
||||||
|
| 阶段 | 内容 | 验收标准 |
|
||||||
|
|---|---|---|
|
||||||
|
| **M1 基础设施** | ① `dependencies` 加 electron-updater;② electron-builder.yml 加 publish(github);③ 升版打包 `pnpm dist:run` | 产物目录出现 `latest.yml`;解包 asar 的 `resources/app-update.yml` 存在;electron-updater 在 asar 内可 require |
|
||||||
|
| **M2 主进程** | updater.ts + handlers/update.ts + preload 暴露 | 手动触发 `update:check` 能返回状态;事件能推送到渲染进程(dev 下用 `forceDevUpdateConfig` 或打包版验证) |
|
||||||
|
| **M3 前端 UI** | update store + 设置页更新区块 + 启动静默检查 | 完整交互:检查/提示/进度/重启安装/失败重试 |
|
||||||
|
| **M4 联调冒烟** | 发布 v1.2.0 → 再发布 v1.2.1,真机从 1.2.0 升到 1.2.1 | Windows 真机全流程通过,含取消下载、断网重试、校验失败场景 |
|
||||||
|
| **M5 加固** | ~~代码签名~~(SignPath 远程签名已接入 CI,见 §12);国内加速(CDN/generic)评估;通道隔离;强制更新开关;临时文件清理 | 可对外发布 |
|
||||||
|
|
||||||
|
## 10. 风险与对策
|
||||||
|
|
||||||
|
| 风险 | 影响 | 对策 |
|
||||||
|
|---|---|---|
|
||||||
|
| electron-updater 误放 devDependencies | 打包后更新直接报错 | M1 验收强制检查 asar 内依赖 |
|
||||||
|
| 未配置 publish → 无 app-update.yml | 运行时报 "app-update.yml not found" | M1 验收点;文档示例已给 |
|
||||||
|
| 未签名 exe 被杀软拦截 | 下载/安装被拦,更新失败 | 正式发布前签名(M5);内测接受现状 |
|
||||||
|
| assisted 静默安装弹 UI / 请求管理员 | 更新中断 | M4 用现有 `build/installer-custom.nsh` 专门联调;必要时加 `runAfterFinish`/静默参数处理 |
|
||||||
|
| api.github.com 国内访问差 | 检查/下载慢或失败 | 失败重试 + 手动下载兜底;后续 CDN 加速(§5.3) |
|
||||||
|
| 检查频率过高 | 浪费带宽、触发限流 | 启动延迟 10~15s + 手动按钮;间隔建议 ≥1h |
|
||||||
|
| 磁盘满 / %LOCALAPPDATA% 权限 | 下载/解压失败 | 失败回退提示,清理 `updaterCacheDirName` 缓存 |
|
||||||
|
| 版本号不递增 | 永远查不到更新 | CI/脚本校验 `version:set` 只允许升版 |
|
||||||
|
| 更新失败后应用状态异常 | 用户卡在旧版本 | 保持旧版可用;UI 提供手动下载入口;错误上报(可复用现有 crash 体系) |
|
||||||
|
|
||||||
|
## 11. 后续待决策(不阻塞 M1~M4)
|
||||||
|
|
||||||
|
- 是否需要强制更新 / 最低版本策略
|
||||||
|
- 国内加速方案何时落地(generic + OSS/COS/CDN)
|
||||||
|
- macOS / Linux 更新的排期(架构已预留,届时分别补 zip/dmg 与 AppImage 产物与签名)
|
||||||
|
|
||||||
|
## 12. SignPath 代码签名接入(2026-08-28 落地)
|
||||||
|
|
||||||
|
**方案**:electron-builder 自定义签名(`win.sign: scripts/signpath-sign.js`),
|
||||||
|
构建时对内部 exe(`Koring Launcher.exe`、elevate.exe、卸载器)与最终 `setup.exe`
|
||||||
|
逐个提交 SignPath 远程签名,签名发生在 blockmap / latest.yml 生成之前,
|
||||||
|
**清单 sha512 与 blockmap 自动对应签名后产物**,electron-updater 校验无缝。
|
||||||
|
|
||||||
|
**SignPath 后台准备(已确认值)**:
|
||||||
|
- Organization ID:`31ecd033-d59e-492b-a70b-b00a54bbc7c2`(已写入 workflow env)
|
||||||
|
- API Token:用户详情页生成(CI 用途),放入 GitHub Secrets `SIGNPATH_API_TOKEN`
|
||||||
|
- slug:项目 `Koring_Launcher`;签名策略 `Koring_Launcher_Dev_builder`(均写入 workflow env);
|
||||||
|
产物配置 DEFAULT 在项目仅一个配置时可省略 `SIGNPATH_ARTIFACT_CONFIG_SLUG`
|
||||||
|
|
||||||
|
**⚠️ 待办约束**:
|
||||||
|
- **测试证书**:当前策略 Purpose 为 **Test signing**(测试证书),用户机器默认不信任
|
||||||
|
(SmartScreen / 杀软警告;`signtool verify` 会报"不受信任"——预期现象),
|
||||||
|
正式对外发布需生产证书(OV/EV)+ 对应生产签名策略(届时只换 `SIGNPATH_SIGNING_POLICY_SLUG`)。
|
||||||
|
- 审批流程:已在 SignPath 后台关闭人工审批(自动批准),CI 可全自动;策略 Purpose 已改为 Release signing。
|
||||||
|
|
||||||
|
**新增/改动文件**:
|
||||||
|
- `scripts/signpath-sign.js` — 签名模块(无 token 自动跳过,本地构建不受影响)
|
||||||
|
- `.github/workflows/release.yml` — 推送 tag `v*`(或手动填版本)→ 构建 → 签名 → `electron-builder --publish always` 发布 GitHub Releases
|
||||||
|
- `electron-builder.yml` — `win.sign` 挂载
|
||||||
|
|
||||||
|
**CI Secrets**:仅 `SIGNPATH_API_TOKEN`(必填)+ `SIGNPATH_ARTIFACT_CONFIG_SLUG`(可选);
|
||||||
|
组织 ID 与 slugs 已在 workflow 中写死。
|
||||||
|
|
||||||
|
**2026-08-28 实测结果**(与官方 PowerShell 模块对齐后的真实签名):
|
||||||
|
- 协议:`POST {base}/v1/{orgId}/SigningRequests`(**multipart/form-data**,字段 ProjectSlug /
|
||||||
|
SigningPolicySlug / ArtifactConfigurationSlug? / Description / 文件部件 **Artifact**)→
|
||||||
|
响应 **Location 头** = 请求 URL → 轮询 `status`/`isFinalStatus` → 完成后取 **signedArtifactLink** 下载;
|
||||||
|
认证 `Authorization: Bearer <token>`。
|
||||||
|
- ✅ 98.3MB setup.exe 提交成功(无大小限制问题);自动批准生效(InProgress → Completed 无人工介入)
|
||||||
|
- ✅ 小文件端到端签名成功,`signtool verify` 显示签名链 **Issued to: Lingke Network**
|
||||||
|
- ⚠️ 本机到 SignPath 下载 100MB+ 产物极慢(20min+),CI(GitHub Actions)网络环境不受影响;
|
||||||
|
模块已加下载重试(3 次)+ 临时文件替换
|
||||||
|
|
||||||
|
**验证点(首次 CI 运行)**:确认 SignPath 请求全部 Completed、`latest.yml` 的 sha512
|
||||||
|
与上传的 signed setup.exe 一致、签名链显示 "Lingke Network"。
|
||||||
File diff suppressed because one or more lines are too long
@@ -3,6 +3,10 @@ productName: Koring Launcher
|
|||||||
directories:
|
directories:
|
||||||
output: dist-electron
|
output: dist-electron
|
||||||
buildResources: build
|
buildResources: build
|
||||||
|
publish:
|
||||||
|
provider: github
|
||||||
|
owner: dream-pep
|
||||||
|
repo: koring-launcher
|
||||||
compression: maximum
|
compression: maximum
|
||||||
asar: true
|
asar: true
|
||||||
files:
|
files:
|
||||||
@@ -13,6 +17,9 @@ win:
|
|||||||
icon: build/icon.ico
|
icon: build/icon.ico
|
||||||
artifactName: "koring-launcher-${version}-setup.${ext}"
|
artifactName: "koring-launcher-${version}-setup.${ext}"
|
||||||
requestedExecutionLevel: asInvoker
|
requestedExecutionLevel: asInvoker
|
||||||
|
# SignPath 远程签名:scripts/signpath-sign.js
|
||||||
|
# 无 SIGNPATH_API_TOKEN 时自动跳过(本地构建不受影响)
|
||||||
|
sign: scripts/signpath-sign.js
|
||||||
mac:
|
mac:
|
||||||
target: dmg
|
target: dmg
|
||||||
icon: build/icon.png
|
icon: build/icon.png
|
||||||
|
|||||||
+11
-6
@@ -11,15 +11,20 @@ export interface AuthData {
|
|||||||
xboxProfile: string;
|
xboxProfile: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const authFile = (): string => {
|
/**
|
||||||
|
* 认证文件路径(与配置一致):
|
||||||
|
* - 打包后 → 系统用户数据目录(userData)
|
||||||
|
* - 开发模式 → 项目根目录
|
||||||
|
*/
|
||||||
|
export function authPath(): string {
|
||||||
if (app.isPackaged) {
|
if (app.isPackaged) {
|
||||||
return path.join(path.dirname(app.getPath('exe')), 'koring-auth.json');
|
return path.join(app.getPath('userData'), 'koring-auth.json');
|
||||||
}
|
}
|
||||||
return path.join(__dirname, '..', 'koring-auth.json');
|
return path.join(__dirname, '..', 'koring-auth.json');
|
||||||
};
|
}
|
||||||
|
|
||||||
export function readAuth(): AuthData {
|
export function readAuth(): AuthData {
|
||||||
const filePath = authFile();
|
const filePath = authPath();
|
||||||
if (!fs.existsSync(filePath)) {
|
if (!fs.existsSync(filePath)) {
|
||||||
return { username: '', uuid: '', accessToken: '', refreshToken: '', xboxProfile: '' };
|
return { username: '', uuid: '', accessToken: '', refreshToken: '', xboxProfile: '' };
|
||||||
}
|
}
|
||||||
@@ -32,12 +37,12 @@ export function readAuth(): AuthData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function writeAuth(auth: AuthData): void {
|
export function writeAuth(auth: AuthData): void {
|
||||||
const filePath = authFile();
|
const filePath = authPath();
|
||||||
fs.writeFileSync(filePath, JSON.stringify(auth, null, 2), 'utf-8');
|
fs.writeFileSync(filePath, JSON.stringify(auth, null, 2), 'utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteAuth(): void {
|
export function deleteAuth(): void {
|
||||||
const filePath = authFile();
|
const filePath = authPath();
|
||||||
if (fs.existsSync(filePath)) {
|
if (fs.existsSync(filePath)) {
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
}
|
}
|
||||||
|
|||||||
+99
-3
@@ -7,9 +7,14 @@ const { app } = electron;
|
|||||||
const CONFIG_FILE = 'Koring.yml';
|
const CONFIG_FILE = 'Koring.yml';
|
||||||
const CURRENT_VERSION = 1;
|
const CURRENT_VERSION = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置文件路径:
|
||||||
|
* - 打包后 → 系统用户数据目录(userData),避免安装到 Program Files 等只读目录时写入失败
|
||||||
|
* - 开发模式 → 项目根目录(与旧行为一致,方便调试)
|
||||||
|
*/
|
||||||
export function configPath(): string {
|
export function configPath(): string {
|
||||||
if (app.isPackaged) {
|
if (app.isPackaged) {
|
||||||
return path.join(path.dirname(app.getPath('exe')), CONFIG_FILE);
|
return path.join(app.getPath('userData'), CONFIG_FILE);
|
||||||
}
|
}
|
||||||
return path.join(__dirname, '..', CONFIG_FILE);
|
return path.join(__dirname, '..', CONFIG_FILE);
|
||||||
}
|
}
|
||||||
@@ -38,6 +43,8 @@ export interface GameConfig {
|
|||||||
resourceDir: string;
|
resourceDir: string;
|
||||||
savesDir: string;
|
savesDir: string;
|
||||||
instancesDir: string;
|
instancesDir: string;
|
||||||
|
/** 已添加的游戏目录列表 */
|
||||||
|
gameDirs: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JavaConfig {
|
export interface JavaConfig {
|
||||||
@@ -48,6 +55,11 @@ export interface JavaConfig {
|
|||||||
jvmArgs: string;
|
jvmArgs: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ServerConfig {
|
||||||
|
ip: string;
|
||||||
|
port: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdvancedConfig {
|
export interface AdvancedConfig {
|
||||||
afterLaunch: string;
|
afterLaunch: string;
|
||||||
winMode: string;
|
winMode: string;
|
||||||
@@ -56,6 +68,20 @@ export interface AdvancedConfig {
|
|||||||
gameArgs: string;
|
gameArgs: string;
|
||||||
preLaunchCmd: string;
|
preLaunchCmd: string;
|
||||||
debugMode: boolean;
|
debugMode: boolean;
|
||||||
|
/** 快速进入服务器(启动后自动加入;ip 为空则不自动加入) */
|
||||||
|
server: ServerConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppInfoConfig {
|
||||||
|
/** 界面语言偏好(zh-CN | en-US);语言包开发中,暂仅保存并设置 <html lang> */
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UiConfig {
|
||||||
|
/** 首页实例标题显示 */
|
||||||
|
showInstanceTitle: boolean;
|
||||||
|
/** 标题栏任务队列按钮显示 */
|
||||||
|
showTaskButton: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DownloadConfig {
|
export interface DownloadConfig {
|
||||||
@@ -89,6 +115,7 @@ export interface NetworkConfig {
|
|||||||
export interface AppConfig {
|
export interface AppConfig {
|
||||||
version: number;
|
version: number;
|
||||||
oobe: boolean;
|
oobe: boolean;
|
||||||
|
app: AppInfoConfig;
|
||||||
theme: ThemeConfig;
|
theme: ThemeConfig;
|
||||||
a11y: A11yConfig;
|
a11y: A11yConfig;
|
||||||
background: BackgroundConfig;
|
background: BackgroundConfig;
|
||||||
@@ -97,20 +124,23 @@ export interface AppConfig {
|
|||||||
advanced: AdvancedConfig;
|
advanced: AdvancedConfig;
|
||||||
download: DownloadConfig;
|
download: DownloadConfig;
|
||||||
network: NetworkConfig;
|
network: NetworkConfig;
|
||||||
|
ui: UiConfig;
|
||||||
instances: InstanceMeta[];
|
instances: InstanceMeta[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULTS: AppConfig = {
|
const DEFAULTS: AppConfig = {
|
||||||
version: CURRENT_VERSION,
|
version: CURRENT_VERSION,
|
||||||
oobe: true,
|
oobe: true,
|
||||||
|
app: { language: 'zh-CN' },
|
||||||
theme: { darkMode: 'auto', parallax: true },
|
theme: { darkMode: 'auto', parallax: true },
|
||||||
a11y: { reduceMotion: false, reduceTransparency: false, highContrast: false, contentBlurOpacity: 50 },
|
a11y: { reduceMotion: false, reduceTransparency: false, highContrast: false, contentBlurOpacity: 50 },
|
||||||
background: { bgType: 'image', image: '/background.png', blur: 0, opacity: 100 },
|
background: { bgType: 'image', image: '/background.png', blur: 0, opacity: 100 },
|
||||||
game: { gameDir: '.minecraft', resourceDir: '', savesDir: '', instancesDir: '.minecraft/instances' },
|
game: { gameDir: '.minecraft', resourceDir: '', savesDir: '', instancesDir: '.minecraft/instances', gameDirs: [] },
|
||||||
java: { javaPath: '', memMode: 'auto', memGB: 4, gc: 'auto', jvmArgs: '' },
|
java: { javaPath: '', memMode: 'auto', memGB: 4, gc: 'auto', jvmArgs: '' },
|
||||||
advanced: { afterLaunch: 'close', winMode: 'default', customWidth: 854, customHeight: 480, gameArgs: '', preLaunchCmd: '', debugMode: false },
|
advanced: { afterLaunch: 'close', winMode: 'default', customWidth: 854, customHeight: 480, gameArgs: '', preLaunchCmd: '', debugMode: false, server: { ip: '', port: 25565 } },
|
||||||
download: { fileSource: 'mirror', versionSource: 'mirror', threads: 16, speedLimit: 0 },
|
download: { fileSource: 'mirror', versionSource: 'mirror', threads: 16, speedLimit: 0 },
|
||||||
network: { securityId: { enabled: false, authUrl: '' } },
|
network: { securityId: { enabled: false, authUrl: '' } },
|
||||||
|
ui: { showInstanceTitle: true, showTaskButton: true },
|
||||||
instances: [],
|
instances: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -183,3 +213,69 @@ export function saveConfig(config: AppConfig): void {
|
|||||||
const yamlStr = yaml.dump(sparse, { lineWidth: -1 });
|
const yamlStr = yaml.dump(sparse, { lineWidth: -1 });
|
||||||
fs.writeFileSync(filePath, yamlStr, 'utf-8');
|
fs.writeFileSync(filePath, yamlStr, 'utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 主进程权威配置模型 ====================
|
||||||
|
// 主进程内存缓存是唯一权威(single source of truth):
|
||||||
|
// 渲染进程通过 config:update 提交补丁 → updateConfig 合并 → debounce 稀疏写盘
|
||||||
|
// 启动游戏时直接读内存缓存,保证永远是最新配置(无磁盘竞争)。
|
||||||
|
|
||||||
|
let current: AppConfig | null = null;
|
||||||
|
|
||||||
|
function mergeDeep<T>(base: T, patch: unknown): T {
|
||||||
|
if (patch === null || patch === undefined) return base;
|
||||||
|
if (typeof base !== 'object' || typeof patch !== 'object' || Array.isArray(base) || Array.isArray(patch)) {
|
||||||
|
return patch as T;
|
||||||
|
}
|
||||||
|
const result: Record<string, unknown> = { ...(base as Record<string, unknown>) };
|
||||||
|
for (const key of Object.keys(patch as Record<string, unknown>)) {
|
||||||
|
result[key] = mergeDeep(result[key], (patch as Record<string, unknown>)[key]);
|
||||||
|
}
|
||||||
|
return result as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 读取当前配置(内存优先,未加载则从磁盘读取) */
|
||||||
|
export function getConfig(): AppConfig {
|
||||||
|
if (!current) {
|
||||||
|
current = loadConfig();
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
let saveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
function scheduleSave(): void {
|
||||||
|
if (saveTimer) clearTimeout(saveTimer);
|
||||||
|
saveTimer = setTimeout(() => {
|
||||||
|
saveTimer = null;
|
||||||
|
flushConfig();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 立即将内存配置写盘(应用退出前调用) */
|
||||||
|
export function flushConfig(): void {
|
||||||
|
if (saveTimer) {
|
||||||
|
clearTimeout(saveTimer);
|
||||||
|
saveTimer = null;
|
||||||
|
}
|
||||||
|
if (current) {
|
||||||
|
saveConfig(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 深度合并补丁到内存配置并返回合并结果(300ms debounce 写盘) */
|
||||||
|
export function updateConfig(patch: Record<string, unknown>): AppConfig {
|
||||||
|
const base = getConfig();
|
||||||
|
current = mergeDeep(base, patch) as AppConfig;
|
||||||
|
scheduleSave();
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除内存配置中的指定顶层键(如 koringUser),300ms debounce 写盘 */
|
||||||
|
export function deleteConfigKey(key: string): AppConfig {
|
||||||
|
const base = getConfig();
|
||||||
|
const next = { ...(base as unknown as Record<string, unknown>) };
|
||||||
|
delete next[key];
|
||||||
|
current = next as unknown as AppConfig;
|
||||||
|
scheduleSave();
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const MAX_LINES = 1000;
|
|||||||
|
|
||||||
function logPath(): string {
|
function logPath(): string {
|
||||||
if (app.isPackaged) {
|
if (app.isPackaged) {
|
||||||
return path.join(path.dirname(app.getPath('exe')), LOG_FILE);
|
return path.join(app.getPath('userData'), LOG_FILE);
|
||||||
}
|
}
|
||||||
return path.join(__dirname, '..', LOG_FILE);
|
return path.join(__dirname, '..', LOG_FILE);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { MinecraftFolder, Version, launch, createMinecraftProcessWatcher, type ResolvedVersion } from '@xmcl/core';
|
import { Version, type ResolvedVersion } from '@xmcl/core';
|
||||||
import {
|
import {
|
||||||
install as xmclInstall,
|
install as xmclInstall,
|
||||||
installForge,
|
installForge,
|
||||||
@@ -308,74 +308,6 @@ export async function installInstanceGame(
|
|||||||
return getInstanceInfo(name, gamePath);
|
return getInstanceInfo(name, gamePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function launchInstance(
|
|
||||||
name: string,
|
|
||||||
gamePath: string,
|
|
||||||
options: {
|
|
||||||
username: string;
|
|
||||||
uuid: string;
|
|
||||||
accessToken?: string;
|
|
||||||
javaPath?: string;
|
|
||||||
server?: { host: string; port?: number };
|
|
||||||
onEvent?: (event: { event: string; [key: string]: unknown }) => void;
|
|
||||||
}
|
|
||||||
): Promise<{ pid: number; version: string; username: string }> {
|
|
||||||
const instance = await getInstanceInfo(name, gamePath);
|
|
||||||
const { runtime } = instance.config;
|
|
||||||
|
|
||||||
const resolved: ResolvedVersion = await Version.parse(instance.path, runtime.minecraft);
|
|
||||||
|
|
||||||
const javaPath = options.javaPath || instance.config.java || 'java';
|
|
||||||
|
|
||||||
const mcProcess = await launch({
|
|
||||||
gameProfile: {
|
|
||||||
id: options.uuid,
|
|
||||||
name: options.username,
|
|
||||||
},
|
|
||||||
javaPath,
|
|
||||||
version: resolved,
|
|
||||||
gamePath: instance.path,
|
|
||||||
minMemory: instance.config.minMemory || 1024,
|
|
||||||
maxMemory: instance.config.maxMemory || 4096,
|
|
||||||
extraExecOption: { detached: true, stdio: 'ignore' },
|
|
||||||
server: options.server ? { ip: options.server.host, port: options.server.port } : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const watcher = createMinecraftProcessWatcher(mcProcess);
|
|
||||||
|
|
||||||
watcher.on('minecraft-window-ready', () => {
|
|
||||||
options.onEvent?.({ event: 'window-ready' });
|
|
||||||
});
|
|
||||||
|
|
||||||
watcher.on('minecraft-exit', ({ code }) => {
|
|
||||||
options.onEvent?.({ event: 'exit', code });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update playtime tracking
|
|
||||||
const startTime = Date.now();
|
|
||||||
mcProcess.on('exit', async () => {
|
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
try {
|
|
||||||
const info = await getInstanceInfo(name, gamePath);
|
|
||||||
await updateInstance(name, gamePath, {
|
|
||||||
lastPlayedDate: Date.now(),
|
|
||||||
playtime: (info.config.playtime || 0) + elapsed,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// Ignore errors during playtime update
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update last access date
|
|
||||||
await updateInstance(name, gamePath, { lastAccessDate: Date.now() });
|
|
||||||
|
|
||||||
return {
|
|
||||||
pid: mcProcess.pid || 0,
|
|
||||||
version: runtime.minecraft,
|
|
||||||
username: options.username,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function diagnoseInstance(
|
export async function diagnoseInstance(
|
||||||
name: string,
|
name: string,
|
||||||
gamePath: string
|
gamePath: string
|
||||||
@@ -503,6 +435,8 @@ export async function importExistingInstance(
|
|||||||
java?: string;
|
java?: string;
|
||||||
minMemory?: number;
|
minMemory?: number;
|
||||||
maxMemory?: number;
|
maxMemory?: number;
|
||||||
|
/** 版本文件来源目录(默认 = gamePath);扫描副目录导入时传 scanTarget */
|
||||||
|
sourceGamePath?: string;
|
||||||
}
|
}
|
||||||
): Promise<InstanceInfo> {
|
): Promise<InstanceInfo> {
|
||||||
const instancePath = path.join(gamePath, 'instances', name);
|
const instancePath = path.join(gamePath, 'instances', name);
|
||||||
@@ -510,10 +444,11 @@ export async function importExistingInstance(
|
|||||||
throw new Error(`Instance already exists: ${name}`);
|
throw new Error(`Instance already exists: ${name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查源版本目录是否存在
|
// 源版本目录:默认取 gamePath,扫描副目录时取 sourceGamePath
|
||||||
const srcVersionDir = path.join(gamePath, 'versions', versionId);
|
const srcGamePath = options?.sourceGamePath || gamePath;
|
||||||
|
const srcVersionDir = path.join(srcGamePath, 'versions', versionId);
|
||||||
if (!fs.existsSync(srcVersionDir)) {
|
if (!fs.existsSync(srcVersionDir)) {
|
||||||
throw new Error(`Version directory not found: ${versionId}`);
|
throw new Error(`Version directory not found: ${versionId}(来源:${srcGamePath})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取源版本 JSON 获取类型信息
|
// 读取源版本 JSON 获取类型信息
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
// __ __ __ __ ______ __ __ ______ __ __ ______ ______
|
||||||
|
// /\ \ /\ \ /\ "-.\ \ /\ ___\ /\ \/ / /\ ___\ /\ "-.\ \ /\ ___\ /\__ _\
|
||||||
|
// \ \ \____ \ \ \ \ \ \-. \ \ \ \__ \ \ \ _"-. \ \ __\ \ \ \-. \ \ \ __\ \/_/\ \/
|
||||||
|
// \ \_____\ \ \_\ \ \_\\"\_\ \ \_____\ \ \_\ \_\ \ \_____\ \ \_\\"\_\ \ \_____\ \ \_\
|
||||||
|
// \/_____/ \/_/ \/_/ \/_/ \/_____/ \/_/\/_/ \/_____/ \/_/ \/_/ \/_____/ \/_/
|
||||||
|
//
|
||||||
|
// 所有权利归Lingke Network (china)所有 | dream_pep拥有其艺术改变权力
|
||||||
|
// 未经允许的情况下删除此版权头可能会受到民事指控
|
||||||
|
// 请勿在未经Lingke Network (china)允许的范围内修改代码并分发
|
||||||
|
|
||||||
|
import type { LaunchOption } from '@xmcl/core';
|
||||||
|
import { resolveJava, getPotentialJavaLocations } from '@xmcl/installer';
|
||||||
|
import type { AppConfig } from '../config';
|
||||||
|
import type { InstanceInfo } from './instance';
|
||||||
|
|
||||||
|
/** 游戏启动所需的账户档案 */
|
||||||
|
export interface LaunchProfile {
|
||||||
|
username: string;
|
||||||
|
uuid: string;
|
||||||
|
accessToken?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 快速联机目标服务器 */
|
||||||
|
export interface LaunchServer {
|
||||||
|
ip: string;
|
||||||
|
port?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 引号感知的命令行参数切分器。
|
||||||
|
* 支持双引号 / 单引号包裹的含空格参数与反斜杠转义。
|
||||||
|
* 用于 jvmArgs / gameArgs / preLaunchCmd 的解析。
|
||||||
|
*/
|
||||||
|
export function parseArgs(line: string): string[] {
|
||||||
|
const args: string[] = [];
|
||||||
|
const re = /"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|(\S+)/g;
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
while ((match = re.exec(line)) !== null) {
|
||||||
|
if (match[1] !== undefined) {
|
||||||
|
args.push(match[1].replace(/\\(["\\])/g, '$1'));
|
||||||
|
} else if (match[2] !== undefined) {
|
||||||
|
args.push(match[2].replace(/\\(['\\])/g, '$1'));
|
||||||
|
} else {
|
||||||
|
args.push(match[3]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将启动器配置(AppConfig.java / AppConfig.advanced)+ 实例信息 + 账户档案
|
||||||
|
* 映射为 @xmcl/core 的 LaunchOption。
|
||||||
|
*
|
||||||
|
* 配置 → 启动参数 对应关系:
|
||||||
|
* - java.memMode=auto → 实例 minMemory/maxMemory(未设置则 1024/4096)
|
||||||
|
* - java.memMode=custom → min=min(2,memGB)G,max=memGB G
|
||||||
|
* - java.gc=zgc/g1 → -XX:+UseZGC / -XX:+UseG1GC
|
||||||
|
* - java.jvmArgs → 逐行解析并入 extraJVMArgs
|
||||||
|
* - advanced.gameArgs → 解析并入 extraMCArgs
|
||||||
|
* - advanced.winMode → resolution(fullscreen / custom 宽高)
|
||||||
|
* - advanced.preLaunchCmd → prependCommand(Windows 批处理需 `cmd /c` 前缀)
|
||||||
|
* - advanced.debugMode → -Dkoring.debugMode=true
|
||||||
|
*/
|
||||||
|
export function buildLaunchOptions(
|
||||||
|
config: AppConfig,
|
||||||
|
instance: InstanceInfo,
|
||||||
|
profile: LaunchProfile,
|
||||||
|
javaPath: string,
|
||||||
|
server?: LaunchServer,
|
||||||
|
): LaunchOption {
|
||||||
|
const java = config.java;
|
||||||
|
const adv = config.advanced;
|
||||||
|
|
||||||
|
// ---- 内存 ----
|
||||||
|
let minMemory = instance.config.minMemory ?? 1024;
|
||||||
|
let maxMemory = instance.config.maxMemory ?? 4096;
|
||||||
|
if (java.memMode === 'custom') {
|
||||||
|
const gb = Math.max(1, Math.min(16, java.memGB || 4));
|
||||||
|
minMemory = Math.min(2, gb) * 1024;
|
||||||
|
maxMemory = gb * 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- JVM 参数 ----
|
||||||
|
const extraJVMArgs: string[] = [];
|
||||||
|
if (java.gc === 'zgc') extraJVMArgs.push('-XX:+UseZGC');
|
||||||
|
else if (java.gc === 'g1') extraJVMArgs.push('-XX:+UseG1GC');
|
||||||
|
if (java.jvmArgs?.trim()) {
|
||||||
|
extraJVMArgs.push(...parseArgs(java.jvmArgs));
|
||||||
|
}
|
||||||
|
if (adv.debugMode) {
|
||||||
|
extraJVMArgs.push('-Dkoring.debugMode=true');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 游戏参数 ----
|
||||||
|
const extraMCArgs: string[] = [];
|
||||||
|
if (adv.gameArgs?.trim()) {
|
||||||
|
extraMCArgs.push(...parseArgs(adv.gameArgs));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 窗口 / 分辨率 ----
|
||||||
|
let resolution: { width?: number; height?: number; fullscreen?: boolean } | undefined;
|
||||||
|
if (adv.winMode === 'fullscreen') {
|
||||||
|
resolution = { fullscreen: true };
|
||||||
|
} else if (adv.winMode === 'custom') {
|
||||||
|
resolution = { width: adv.customWidth || 854, height: adv.customHeight || 480 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
gameProfile: { name: profile.username, id: profile.uuid },
|
||||||
|
accessToken: profile.accessToken,
|
||||||
|
javaPath,
|
||||||
|
// version 由调用方在 Version.parse 后覆盖为 ResolvedVersion
|
||||||
|
version: instance.config.runtime.minecraft,
|
||||||
|
gamePath: instance.path,
|
||||||
|
minMemory,
|
||||||
|
maxMemory,
|
||||||
|
resolution,
|
||||||
|
extraJVMArgs,
|
||||||
|
extraMCArgs,
|
||||||
|
server,
|
||||||
|
prependCommand: adv.preLaunchCmd?.trim() ? parseArgs(adv.preLaunchCmd) : undefined,
|
||||||
|
launcherName: 'Koring Launcher',
|
||||||
|
launcherBrand: 'Koring',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 Java 可执行文件路径,优先级:
|
||||||
|
* 1. 用户配置路径(resolveJava 校验,无效则继续向下)
|
||||||
|
* 2. 系统扫描(`where java` / `which java` 结果逐个 resolve)
|
||||||
|
* 3. 兜底 PATH 中的 `java`
|
||||||
|
*/
|
||||||
|
export async function resolveJavaPath(configuredPath: string): Promise<string> {
|
||||||
|
if (configuredPath?.trim()) {
|
||||||
|
const info = await resolveJava(configuredPath.trim()).catch(() => undefined);
|
||||||
|
if (info) return info.path;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const locations = await getPotentialJavaLocations();
|
||||||
|
for (const loc of locations) {
|
||||||
|
const info = await resolveJava(loc).catch(() => undefined);
|
||||||
|
if (info) return info.path;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 扫描失败则继续回退
|
||||||
|
}
|
||||||
|
return 'java';
|
||||||
|
}
|
||||||
+116
-135
@@ -1,147 +1,129 @@
|
|||||||
import { spawn, ChildProcess } from 'child_process';
|
// __ __ __ __ ______ __ __ ______ __ __ ______ ______
|
||||||
|
// /\ \ /\ \ /\ "-.\ \ /\ ___\ /\ \/ / /\ ___\ /\ "-.\ \ /\ ___\ /\__ _\
|
||||||
|
// \ \ \____ \ \ \ \ \ \-. \ \ \ \__ \ \ \ _"-. \ \ __\ \ \ \-. \ \ \ __\ \/_/\ \/
|
||||||
|
// \ \_____\ \ \_\ \ \_\\"\_\ \ \_____\ \ \_\ \_\ \ \_____\ \ \_\\"\_\ \ \_____\ \ \_\
|
||||||
|
// \/_____/ \/_/ \/_/ \/_/ \/_____/ \/_/\/_/ \/_____/ \/_/ \/_/ \/_____/ \/_/
|
||||||
|
//
|
||||||
|
// 所有权利归Lingke Network (china)所有 | dream_pep拥有其艺术改变权力
|
||||||
|
// 未经允许的情况下删除此版权头可能会受到民事指控
|
||||||
|
// 请勿在未经Lingke Network (china)允许的范围内修改代码并分发
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import { Version, launch, createMinecraftProcessWatcher } from '@xmcl/core';
|
||||||
|
import type { AppConfig } from '../config';
|
||||||
|
import { getInstanceInfo, updateInstance } from './instance';
|
||||||
|
import {
|
||||||
|
buildLaunchOptions,
|
||||||
|
resolveJavaPath,
|
||||||
|
type LaunchProfile,
|
||||||
|
type LaunchServer,
|
||||||
|
} from './launch-options';
|
||||||
|
|
||||||
interface LaunchOptions {
|
export interface GameLaunchResult {
|
||||||
gamePath: string;
|
|
||||||
javaPath: string;
|
|
||||||
version: string;
|
|
||||||
username: string;
|
|
||||||
uuid: string;
|
|
||||||
accessToken?: string;
|
|
||||||
memory?: { min?: string; max?: string };
|
|
||||||
jvmArgs?: string[];
|
|
||||||
gameArgs?: string[];
|
|
||||||
server?: { ip: string; port?: number };
|
|
||||||
detached?: boolean;
|
|
||||||
onEvent?: (event: { event: string; [key: string]: unknown }) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LaunchResult {
|
|
||||||
pid: number;
|
pid: number;
|
||||||
version: string;
|
version: string;
|
||||||
username: string;
|
username: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const runningProcesses = new Map<string, ChildProcess>();
|
export interface LaunchEvent {
|
||||||
|
event: string;
|
||||||
export async function launchMinecraft(options: LaunchOptions): Promise<LaunchResult> {
|
[key: string]: unknown;
|
||||||
const versionJsonPath = path.join(options.gamePath, 'versions', options.version, `${options.version}.json`);
|
|
||||||
|
|
||||||
if (!fs.existsSync(versionJsonPath)) {
|
|
||||||
throw new Error(`Version JSON not found: ${versionJsonPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const versionJson = JSON.parse(fs.readFileSync(versionJsonPath, 'utf-8'));
|
|
||||||
const mainClass = versionJson.mainClass;
|
|
||||||
|
|
||||||
if (!mainClass) {
|
|
||||||
throw new Error('Main class not found in version JSON');
|
|
||||||
}
|
|
||||||
|
|
||||||
const args: string[] = [];
|
|
||||||
|
|
||||||
// Memory
|
|
||||||
const minMem = options.memory?.min || '512M';
|
|
||||||
const maxMem = options.memory?.max || '4G';
|
|
||||||
args.push(`-Xms${minMem}`);
|
|
||||||
args.push(`-Xmx${maxMem}`);
|
|
||||||
|
|
||||||
// JVM args
|
|
||||||
if (options.jvmArgs) {
|
|
||||||
args.push(...options.jvmArgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Native libraries path
|
|
||||||
const nativesDir = path.join(options.gamePath, 'versions', options.version, `${options.version}-natives`);
|
|
||||||
if (fs.existsSync(nativesDir)) {
|
|
||||||
args.push(`-Djava.library.path=${nativesDir}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Classpath
|
|
||||||
const libraries = versionJson.libraries || [];
|
|
||||||
const classpath = libraries
|
|
||||||
.filter((lib: { downloads?: { artifact?: { path: string } } }) => lib.downloads?.artifact?.path)
|
|
||||||
.map((lib: { downloads: { artifact: { path: string } } }) => path.join(options.gamePath, 'libraries', lib.downloads.artifact.path));
|
|
||||||
|
|
||||||
const clientJar = path.join(options.gamePath, 'versions', options.version, `${options.version}.jar`);
|
|
||||||
if (fs.existsSync(clientJar)) {
|
|
||||||
classpath.push(clientJar);
|
|
||||||
}
|
|
||||||
|
|
||||||
args.push('-cp');
|
|
||||||
args.push(classpath.join(path.delimiter));
|
|
||||||
|
|
||||||
args.push(mainClass);
|
|
||||||
|
|
||||||
// Game args
|
|
||||||
args.push(`--username`, options.username);
|
|
||||||
args.push(`--version`, options.version);
|
|
||||||
args.push(`--gameDir`, options.gamePath);
|
|
||||||
args.push(`--assetsDir`, path.join(options.gamePath, 'assets'));
|
|
||||||
args.push(`--assetIndex`, versionJson.assetIndex?.id || options.version);
|
|
||||||
args.push(`--uuid`, options.uuid);
|
|
||||||
|
|
||||||
if (options.accessToken) {
|
|
||||||
args.push(`--accessToken`, options.accessToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.server) {
|
|
||||||
args.push(`--server`, options.server.ip);
|
|
||||||
if (options.server.port) {
|
|
||||||
args.push(`--port`, String(options.server.port));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.gameArgs) {
|
|
||||||
args.push(...options.gameArgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
const javaPath = options.javaPath || 'java';
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const child = spawn(javaPath, args, {
|
|
||||||
cwd: options.gamePath,
|
|
||||||
detached: options.detached,
|
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
|
||||||
});
|
|
||||||
|
|
||||||
const requestId = `mc-${Date.now()}`;
|
|
||||||
runningProcesses.set(requestId, child);
|
|
||||||
|
|
||||||
child.stdout?.on('data', (data) => {
|
|
||||||
const line = data.toString().trim();
|
|
||||||
if (line) {
|
|
||||||
options.onEvent?.({ event: 'stdout', message: line });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
child.stderr?.on('data', (data) => {
|
|
||||||
const line = data.toString().trim();
|
|
||||||
if (line) {
|
|
||||||
options.onEvent?.({ event: 'stderr', message: line });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on('error', (err) => {
|
|
||||||
runningProcesses.delete(requestId);
|
|
||||||
options.onEvent?.({ event: 'error', error: String(err) });
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on('exit', (code) => {
|
|
||||||
runningProcesses.delete(requestId);
|
|
||||||
options.onEvent?.({ event: 'exit', code });
|
|
||||||
});
|
|
||||||
|
|
||||||
resolve({
|
|
||||||
pid: child.pid || 0,
|
|
||||||
version: options.version,
|
|
||||||
username: options.username,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LaunchGameOptions {
|
||||||
|
/** 主进程权威配置(内存中最新值) */
|
||||||
|
config: AppConfig;
|
||||||
|
/** 实例名 */
|
||||||
|
instanceName: string;
|
||||||
|
/** 实例父目录(游戏根目录) */
|
||||||
|
gamePath: string;
|
||||||
|
/** 账户档案 */
|
||||||
|
profile: LaunchProfile;
|
||||||
|
/** 快速联机目标服务器 */
|
||||||
|
server?: LaunchServer;
|
||||||
|
/** 事件回调(stdout / stderr / window-ready / exit) */
|
||||||
|
onEvent?: (event: LaunchEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一游戏启动入口:
|
||||||
|
* 实例信息 → 版本解析 → Java 解析 → 配置映射(buildLaunchOptions)→ @xmcl/core launch
|
||||||
|
* 启动后监听 window-ready / exit,并在退出时累计实例 playtime。
|
||||||
|
*/
|
||||||
|
export async function launchGame(options: LaunchGameOptions): Promise<GameLaunchResult> {
|
||||||
|
const { config, instanceName, gamePath, profile, server, onEvent } = options;
|
||||||
|
|
||||||
|
// 1. 读取实例信息并做健康检查
|
||||||
|
const instance = await getInstanceInfo(instanceName, gamePath);
|
||||||
|
if (!instance.healthy) {
|
||||||
|
const detail = instance.issues.join(';') || '未知问题';
|
||||||
|
throw new Error(`实例「${instanceName}」未安装完整:${detail}。请先在资源中心安装或重新安装该实例。`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 解析版本
|
||||||
|
const resolved = await Version.parse(instance.path, instance.config.runtime.minecraft);
|
||||||
|
|
||||||
|
// 3. 解析 Java 路径(配置路径 → 系统扫描 → PATH)
|
||||||
|
const javaPath = await resolveJavaPath(config.java.javaPath);
|
||||||
|
|
||||||
|
// 4. 配置 → LaunchOption(version 覆盖为已解析版本)
|
||||||
|
const launchOption = buildLaunchOptions(config, instance, profile, javaPath, server);
|
||||||
|
launchOption.version = resolved;
|
||||||
|
|
||||||
|
// 5. 启动(detached:启动器关闭后游戏继续运行;pipe:转发 stdout/stderr)
|
||||||
|
const mcProcess = await launch({
|
||||||
|
...launchOption,
|
||||||
|
extraExecOption: { detached: true, stdio: 'pipe' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. 事件监听
|
||||||
|
const watcher = createMinecraftProcessWatcher(mcProcess);
|
||||||
|
watcher.on('minecraft-window-ready', () => {
|
||||||
|
onEvent?.({ event: 'window-ready' });
|
||||||
|
});
|
||||||
|
watcher.on('minecraft-exit', ({ code }) => {
|
||||||
|
onEvent?.({ event: 'exit', code });
|
||||||
|
});
|
||||||
|
|
||||||
|
mcProcess.stdout?.on('data', (chunk: Buffer) => {
|
||||||
|
const message = chunk.toString();
|
||||||
|
if (message.trim()) onEvent?.({ event: 'stdout', message });
|
||||||
|
});
|
||||||
|
mcProcess.stderr?.on('data', (chunk: Buffer) => {
|
||||||
|
const message = chunk.toString();
|
||||||
|
if (message.trim()) onEvent?.({ event: 'stderr', message });
|
||||||
|
});
|
||||||
|
|
||||||
|
// 7. playtime 累计(游戏进程退出时)
|
||||||
|
const startTime = Date.now();
|
||||||
|
mcProcess.on('exit', async () => {
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
try {
|
||||||
|
const info = await getInstanceInfo(instanceName, gamePath);
|
||||||
|
await updateInstance(instanceName, gamePath, {
|
||||||
|
lastPlayedDate: Date.now(),
|
||||||
|
playtime: (info.config.playtime || 0) + elapsed,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// 忽略 playtime 更新失败
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 8. 更新最近访问时间
|
||||||
|
await updateInstance(instanceName, gamePath, { lastAccessDate: Date.now() }).catch(() => {});
|
||||||
|
|
||||||
|
return {
|
||||||
|
pid: mcProcess.pid || 0,
|
||||||
|
version: instance.config.runtime.minecraft,
|
||||||
|
username: profile.username,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 诊断指定游戏目录下某个版本的健康状态(JSON + JAR 是否存在)。
|
||||||
|
* 供 launch:diagnose 使用。
|
||||||
|
*/
|
||||||
export async function diagnoseVersion(gamePath: string, version: string): Promise<Record<string, unknown>> {
|
export async function diagnoseVersion(gamePath: string, version: string): Promise<Record<string, unknown>> {
|
||||||
const issues: string[] = [];
|
const issues: string[] = [];
|
||||||
const versionDir = path.join(gamePath, 'versions', version);
|
const versionDir = path.join(gamePath, 'versions', version);
|
||||||
@@ -151,7 +133,6 @@ export async function diagnoseVersion(gamePath: string, version: string): Promis
|
|||||||
if (!fs.existsSync(versionJsonPath)) {
|
if (!fs.existsSync(versionJsonPath)) {
|
||||||
issues.push(`Version JSON not found: ${versionJsonPath}`);
|
issues.push(`Version JSON not found: ${versionJsonPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(jarPath)) {
|
if (!fs.existsSync(jarPath)) {
|
||||||
issues.push(`Client JAR not found: ${jarPath}`);
|
issues.push(`Client JAR not found: ${jarPath}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// 路径归一化:相对 gameDir(默认 `.minecraft`)在打包后依赖进程 cwd,不可靠。
|
||||||
|
// 统一按与 runStartupChecks 一致的基准解析(打包 → exe 目录;开发 → 项目根)。
|
||||||
|
import * as path from 'path';
|
||||||
|
import electron from 'electron';
|
||||||
|
const { app } = electron;
|
||||||
|
|
||||||
|
function baseDataPath(): string {
|
||||||
|
if (app.isPackaged) {
|
||||||
|
return path.dirname(app.getPath('exe'));
|
||||||
|
}
|
||||||
|
return path.join(__dirname, '..', '..');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 相对路径 → 绝对(基准 = exe 目录/项目根);绝对路径原样返回 */
|
||||||
|
export function resolveGamePath(gamePath: string): string {
|
||||||
|
if (!gamePath || path.isAbsolute(gamePath)) {
|
||||||
|
return gamePath;
|
||||||
|
}
|
||||||
|
return path.join(baseDataPath(), gamePath);
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
mirrorFetch,
|
mirrorFetch,
|
||||||
type InstanceRuntime,
|
type InstanceRuntime,
|
||||||
} from './instance';
|
} from './instance';
|
||||||
|
import { resolveGamePath } from './paths';
|
||||||
|
|
||||||
// 任务执行钩子:主进程用它向渲染进程广播日志
|
// 任务执行钩子:主进程用它向渲染进程广播日志
|
||||||
export interface TaskHooks {
|
export interface TaskHooks {
|
||||||
@@ -204,7 +205,9 @@ executorRegistry.set('install-sim', (raw, hooks) => {
|
|||||||
// 任务树:install.create → install.minecraft → (forge|neoforge|fabric|quilt) → install.dependencies
|
// 任务树:install.create → install.minecraft → (forge|neoforge|fabric|quilt) → install.dependencies
|
||||||
executorRegistry.set('install', (rawParams, hooks) => {
|
executorRegistry.set('install', (rawParams, hooks) => {
|
||||||
const p = rawParams as unknown as InstallTaskParams;
|
const p = rawParams as unknown as InstallTaskParams;
|
||||||
const { name, gamePath, runtime } = p;
|
const { name, runtime } = p;
|
||||||
|
// 相对 gameDir 归一化(与 runStartupChecks 基准一致)
|
||||||
|
const gamePath = resolveGamePath(p.gamePath);
|
||||||
const instancePath = path.join(gamePath, 'instances', name);
|
const instancePath = path.join(gamePath, 'instances', name);
|
||||||
|
|
||||||
return task('install', async function () {
|
return task('install', async function () {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import electron from 'electron';
|
import electron from 'electron';
|
||||||
const { ipcMain } = electron;
|
const { ipcMain } = electron;
|
||||||
import { readAuth, writeAuth, deleteAuth } from '../auth';
|
import { readAuth, writeAuth, deleteAuth } from '../auth';
|
||||||
|
import { offlineLogin } from '../core/auth';
|
||||||
|
|
||||||
export function registerAuthHandlers() {
|
export function registerAuthHandlers() {
|
||||||
ipcMain.handle('auth:get', () => {
|
ipcMain.handle('auth:get', () => {
|
||||||
@@ -29,4 +30,21 @@ export function registerAuthHandlers() {
|
|||||||
return { success: false, data: null, error: String(e) };
|
return { success: false, data: null, error: String(e) };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 离线账号登录(离线模式不需要微软 OAuth,用户名即可生成 UUID)
|
||||||
|
ipcMain.handle('auth:offline-login', async (_event, payload: { username: string }) => {
|
||||||
|
try {
|
||||||
|
const username = (payload?.username || '').trim();
|
||||||
|
if (!username) {
|
||||||
|
return { success: false, data: null, error: '用户名不能为空' };
|
||||||
|
}
|
||||||
|
if (username.length > 16) {
|
||||||
|
return { success: false, data: null, error: '用户名长度不能超过 16 个字符' };
|
||||||
|
}
|
||||||
|
const data = await offlineLogin(username);
|
||||||
|
return { success: true, data, error: null };
|
||||||
|
} catch (e: unknown) {
|
||||||
|
return { success: false, data: null, error: String(e) };
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import electron from 'electron';
|
import electron from 'electron';
|
||||||
const { ipcMain, app } = electron;
|
const { ipcMain, app } = electron;
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import { getConfig, saveConfig, updateConfig, type AppConfig, configPath } from '../config';
|
||||||
import { loadConfig, saveConfig, type AppConfig, configPath } from '../config';
|
|
||||||
|
|
||||||
export function registerConfigHandlers() {
|
interface WinRef {
|
||||||
|
mainWindow: electron.BrowserWindow | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerConfigHandlers(win: WinRef) {
|
||||||
ipcMain.handle('config:get', () => {
|
ipcMain.handle('config:get', () => {
|
||||||
try {
|
try {
|
||||||
const config = loadConfig();
|
const config = getConfig();
|
||||||
return { success: true, data: config, error: null };
|
return { success: true, data: config, error: null };
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
return { success: false, data: null, error: String(e) };
|
return { success: false, data: null, error: String(e) };
|
||||||
@@ -23,6 +26,19 @@ export function registerConfigHandlers() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 主进程权威更新:渲染进程提交 { section, patch } 补丁,
|
||||||
|
// 主进程深度合并到内存配置 → debounce 稀疏写盘 → 广播完整配置给所有渲染进程
|
||||||
|
ipcMain.handle('config:update', (_event, payload: { section: string; patch: unknown }) => {
|
||||||
|
try {
|
||||||
|
const { section, patch } = payload;
|
||||||
|
const config = updateConfig({ [section]: patch } as Record<string, unknown>);
|
||||||
|
win.mainWindow?.webContents.send('config:changed', config);
|
||||||
|
return { success: true, data: config, error: null };
|
||||||
|
} catch (e: unknown) {
|
||||||
|
return { success: false, data: null, error: String(e) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('config:reset', () => {
|
ipcMain.handle('config:reset', () => {
|
||||||
try {
|
try {
|
||||||
const filePath = configPath();
|
const filePath = configPath();
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import electron from 'electron';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { writeCrashLog, readCrashLog, clearCrashLog, type CrashEntry } from '../core/crash-logger';
|
import { writeCrashLog, readCrashLog, clearCrashLog, type CrashEntry } from '../core/crash-logger';
|
||||||
|
import { configPath } from '../config';
|
||||||
|
import { authPath } from '../auth';
|
||||||
|
|
||||||
const { app, ipcMain, BrowserWindow } = electron;
|
const { app, ipcMain, BrowserWindow } = electron;
|
||||||
|
|
||||||
@@ -151,20 +153,16 @@ export function registerCrashHandlers() {
|
|||||||
|
|
||||||
// Factory reset
|
// Factory reset
|
||||||
ipcMain.handle('crash:factoryReset', () => {
|
ipcMain.handle('crash:factoryReset', () => {
|
||||||
const dataPath = app.isPackaged
|
// Delete config(userData / 项目根目录,与 configPath 一致)
|
||||||
? path.dirname(app.getPath('exe'))
|
|
||||||
: path.join(__dirname, '../..');
|
|
||||||
|
|
||||||
// Delete config
|
|
||||||
try {
|
try {
|
||||||
const configPath = path.join(dataPath, 'Koring.yml');
|
const config = configPath();
|
||||||
if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
|
if (fs.existsSync(config)) fs.unlinkSync(config);
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
// Delete auth
|
// Delete auth(userData / 项目根目录,与 authPath 一致)
|
||||||
try {
|
try {
|
||||||
const authPath = path.join(dataPath, 'koring-auth.json');
|
const auth = authPath();
|
||||||
if (fs.existsSync(authPath)) fs.unlinkSync(authPath);
|
if (fs.existsSync(auth)) fs.unlinkSync(auth);
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
// Delete background cache in userData
|
// Delete background cache in userData
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
deleteInstance,
|
deleteInstance,
|
||||||
updateInstance,
|
updateInstance,
|
||||||
installInstanceGame,
|
installInstanceGame,
|
||||||
launchInstance,
|
|
||||||
diagnoseInstance,
|
diagnoseInstance,
|
||||||
getMinecraftVersionList,
|
getMinecraftVersionList,
|
||||||
getForgeVersionList,
|
getForgeVersionList,
|
||||||
@@ -17,6 +16,7 @@ import {
|
|||||||
type InstanceRuntime,
|
type InstanceRuntime,
|
||||||
type InstanceConfig,
|
type InstanceConfig,
|
||||||
} from '../core/instance';
|
} from '../core/instance';
|
||||||
|
import { resolveGamePath } from '../core/paths';
|
||||||
|
|
||||||
const { ipcMain } = electron;
|
const { ipcMain } = electron;
|
||||||
|
|
||||||
@@ -38,9 +38,10 @@ export function registerInstanceHandlers(win: WinRef) {
|
|||||||
mcOptions?: string[];
|
mcOptions?: string[];
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
|
const gamePath = resolveGamePath(payload.gamePath);
|
||||||
const data = await createInstance(
|
const data = await createInstance(
|
||||||
payload.name,
|
payload.name,
|
||||||
payload.gamePath,
|
gamePath,
|
||||||
payload.runtime,
|
payload.runtime,
|
||||||
{
|
{
|
||||||
author: payload.author,
|
author: payload.author,
|
||||||
@@ -60,7 +61,7 @@ export function registerInstanceHandlers(win: WinRef) {
|
|||||||
|
|
||||||
ipcMain.handle('instance:list', async (_event, payload: { gamePath: string }) => {
|
ipcMain.handle('instance:list', async (_event, payload: { gamePath: string }) => {
|
||||||
try {
|
try {
|
||||||
const data = await listInstances(payload.gamePath);
|
const data = await listInstances(resolveGamePath(payload.gamePath));
|
||||||
return { success: true, data, error: null };
|
return { success: true, data, error: null };
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
return { success: false, data: null, error: String(e) };
|
return { success: false, data: null, error: String(e) };
|
||||||
@@ -69,7 +70,7 @@ export function registerInstanceHandlers(win: WinRef) {
|
|||||||
|
|
||||||
ipcMain.handle('instance:info', async (_event, payload: { name: string; gamePath: string }) => {
|
ipcMain.handle('instance:info', async (_event, payload: { name: string; gamePath: string }) => {
|
||||||
try {
|
try {
|
||||||
const data = await getInstanceInfo(payload.name, payload.gamePath);
|
const data = await getInstanceInfo(payload.name, resolveGamePath(payload.gamePath));
|
||||||
return { success: true, data, error: null };
|
return { success: true, data, error: null };
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
return { success: false, data: null, error: String(e) };
|
return { success: false, data: null, error: String(e) };
|
||||||
@@ -78,7 +79,7 @@ export function registerInstanceHandlers(win: WinRef) {
|
|||||||
|
|
||||||
ipcMain.handle('instance:delete', async (_event, payload: { name: string; gamePath: string }) => {
|
ipcMain.handle('instance:delete', async (_event, payload: { name: string; gamePath: string }) => {
|
||||||
try {
|
try {
|
||||||
const data = await deleteInstance(payload.name, payload.gamePath);
|
const data = await deleteInstance(payload.name, resolveGamePath(payload.gamePath));
|
||||||
return { success: true, data, error: null };
|
return { success: true, data, error: null };
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
return { success: false, data: null, error: String(e) };
|
return { success: false, data: null, error: String(e) };
|
||||||
@@ -91,7 +92,7 @@ export function registerInstanceHandlers(win: WinRef) {
|
|||||||
patch: Partial<Omit<InstanceConfig, 'name' | 'creationDate'>>;
|
patch: Partial<Omit<InstanceConfig, 'name' | 'creationDate'>>;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
const data = await updateInstance(payload.name, payload.gamePath, payload.patch);
|
const data = await updateInstance(payload.name, resolveGamePath(payload.gamePath), payload.patch);
|
||||||
return { success: true, data, error: null };
|
return { success: true, data, error: null };
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
return { success: false, data: null, error: String(e) };
|
return { success: false, data: null, error: String(e) };
|
||||||
@@ -101,8 +102,9 @@ export function registerInstanceHandlers(win: WinRef) {
|
|||||||
ipcMain.handle('instance:install', async (_event, payload: { name: string; gamePath: string }) => {
|
ipcMain.handle('instance:install', async (_event, payload: { name: string; gamePath: string }) => {
|
||||||
try {
|
try {
|
||||||
const requestId = `install-${Date.now()}`;
|
const requestId = `install-${Date.now()}`;
|
||||||
|
const gamePath = resolveGamePath(payload.gamePath);
|
||||||
|
|
||||||
installInstanceGame(payload.name, payload.gamePath, {
|
installInstanceGame(payload.name, gamePath, {
|
||||||
onProgress: (progress) => {
|
onProgress: (progress) => {
|
||||||
win.mainWindow?.webContents.send('instance:progress', { requestId, ...progress });
|
win.mainWindow?.webContents.send('instance:progress', { requestId, ...progress });
|
||||||
},
|
},
|
||||||
@@ -118,42 +120,9 @@ export function registerInstanceHandlers(win: WinRef) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('instance:launch', async (_event, payload: {
|
|
||||||
name: string;
|
|
||||||
gamePath: string;
|
|
||||||
username: string;
|
|
||||||
uuid: string;
|
|
||||||
accessToken?: string;
|
|
||||||
javaPath?: string;
|
|
||||||
server?: { host: string; port?: number };
|
|
||||||
}) => {
|
|
||||||
try {
|
|
||||||
const requestId = `launch-${Date.now()}`;
|
|
||||||
|
|
||||||
launchInstance(payload.name, payload.gamePath, {
|
|
||||||
username: payload.username,
|
|
||||||
uuid: payload.uuid,
|
|
||||||
accessToken: payload.accessToken,
|
|
||||||
javaPath: payload.javaPath,
|
|
||||||
server: payload.server,
|
|
||||||
onEvent: (event) => {
|
|
||||||
win.mainWindow?.webContents.send('instance:launch-event', { requestId, ...event });
|
|
||||||
},
|
|
||||||
}).then((data) => {
|
|
||||||
win.mainWindow?.webContents.send('instance:launch-complete', { requestId, data });
|
|
||||||
}).catch((err) => {
|
|
||||||
win.mainWindow?.webContents.send('instance:launch-error', { requestId, error: String(err) });
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true, data: { requestId }, error: null };
|
|
||||||
} catch (e: unknown) {
|
|
||||||
return { success: false, data: null, error: String(e) };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('instance:diagnose', async (_event, payload: { name: string; gamePath: string }) => {
|
ipcMain.handle('instance:diagnose', async (_event, payload: { name: string; gamePath: string }) => {
|
||||||
try {
|
try {
|
||||||
const data = await diagnoseInstance(payload.name, payload.gamePath);
|
const data = await diagnoseInstance(payload.name, resolveGamePath(payload.gamePath));
|
||||||
return { success: true, data, error: null };
|
return { success: true, data, error: null };
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
return { success: false, data: null, error: String(e) };
|
return { success: false, data: null, error: String(e) };
|
||||||
@@ -200,7 +169,7 @@ export function registerInstanceHandlers(win: WinRef) {
|
|||||||
// 扫描游戏目录中的已安装版本
|
// 扫描游戏目录中的已安装版本
|
||||||
ipcMain.handle('instance:scan-dir', async (_event, payload: { gamePath: string }) => {
|
ipcMain.handle('instance:scan-dir', async (_event, payload: { gamePath: string }) => {
|
||||||
try {
|
try {
|
||||||
const versions = scanGameDirectories(payload.gamePath);
|
const versions = scanGameDirectories(resolveGamePath(payload.gamePath));
|
||||||
return { success: true, data: { versions }, error: null };
|
return { success: true, data: { versions }, error: null };
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
return { success: false, data: null, error: String(e) };
|
return { success: false, data: null, error: String(e) };
|
||||||
@@ -216,17 +185,20 @@ export function registerInstanceHandlers(win: WinRef) {
|
|||||||
java?: string;
|
java?: string;
|
||||||
minMemory?: number;
|
minMemory?: number;
|
||||||
maxMemory?: number;
|
maxMemory?: number;
|
||||||
|
/** 版本文件来源目录(扫描副目录导入时传扫描目录) */
|
||||||
|
sourceGamePath?: string;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
const data = await importExistingInstance(
|
const data = await importExistingInstance(
|
||||||
payload.name,
|
payload.name,
|
||||||
payload.gamePath,
|
resolveGamePath(payload.gamePath),
|
||||||
payload.versionId,
|
payload.versionId,
|
||||||
{
|
{
|
||||||
description: payload.description,
|
description: payload.description,
|
||||||
java: payload.java,
|
java: payload.java,
|
||||||
minMemory: payload.minMemory,
|
minMemory: payload.minMemory,
|
||||||
maxMemory: payload.maxMemory,
|
maxMemory: payload.maxMemory,
|
||||||
|
sourceGamePath: payload.sourceGamePath ? resolveGamePath(payload.sourceGamePath) : undefined,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return { success: true, data, error: null };
|
return { success: true, data, error: null };
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import electron from 'electron';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { scanLocalJava, resolveJava } from '@xmcl/installer';
|
||||||
|
|
||||||
|
const { ipcMain } = electron;
|
||||||
|
|
||||||
|
// 常见 Java 安装目录(Windows),按一层子目录枚举 bin/java.exe
|
||||||
|
function scanCommonJavaDirs(): string[] {
|
||||||
|
const exe = process.platform === 'win32' ? 'java.exe' : 'java';
|
||||||
|
const roots = [
|
||||||
|
'C:\\Program Files\\Java',
|
||||||
|
'C:\\Program Files (x86)\\Java',
|
||||||
|
'C:\\Program Files\\Eclipse Adoptium',
|
||||||
|
'C:\\Program Files\\Microsoft',
|
||||||
|
'C:\\Program Files\\Zulu',
|
||||||
|
'C:\\Program Files\\Amazon Corretto',
|
||||||
|
];
|
||||||
|
const out: string[] = [];
|
||||||
|
for (const root of roots) {
|
||||||
|
try {
|
||||||
|
const entries = fs.readdirSync(root);
|
||||||
|
for (const e of entries) {
|
||||||
|
out.push(path.join(root, e, 'bin', exe));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 目录不存在则跳过
|
||||||
|
}
|
||||||
|
out.push(path.join(root, 'bin', exe));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerJavaHandlers() {
|
||||||
|
// 扫描系统已安装的 Java(JAVA_HOME / PATH / 常见安装目录)
|
||||||
|
ipcMain.handle('java:scan', async () => {
|
||||||
|
try {
|
||||||
|
const candidates = scanCommonJavaDirs();
|
||||||
|
const list = await scanLocalJava(candidates);
|
||||||
|
// 按路径去重(同一安装可能被多个来源发现)
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const javaList = list.filter((j) => {
|
||||||
|
if (seen.has(j.path)) return false;
|
||||||
|
seen.add(j.path);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
return { success: true, data: { javaList }, error: null };
|
||||||
|
} catch (e: unknown) {
|
||||||
|
return { success: false, data: null, error: String(e) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 校验指定路径是否为可用的 Java 可执行文件
|
||||||
|
ipcMain.handle('java:resolve', async (_event, payload: { path: string }) => {
|
||||||
|
try {
|
||||||
|
const java = await resolveJava(payload.path);
|
||||||
|
return { success: true, data: { java: java ?? null }, error: null };
|
||||||
|
} catch (e: unknown) {
|
||||||
|
return { success: false, data: null, error: String(e) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
readKoringAuth,
|
readKoringAuth,
|
||||||
deleteKoringAuth,
|
deleteKoringAuth,
|
||||||
} from '../core/koring-auth';
|
} from '../core/koring-auth';
|
||||||
import { loadConfig, saveConfig } from '../config';
|
import { getConfig, updateConfig, deleteConfigKey } from '../config';
|
||||||
|
|
||||||
export function registerKoringAuthHandlers() {
|
export function registerKoringAuthHandlers() {
|
||||||
ipcMain.handle('koring-auth:request-device-code', async () => {
|
ipcMain.handle('koring-auth:request-device-code', async () => {
|
||||||
@@ -25,19 +25,19 @@ export function registerKoringAuthHandlers() {
|
|||||||
const result = await pollForTokenOnce(deviceCode);
|
const result = await pollForTokenOnce(deviceCode);
|
||||||
const user = saveKoringAuth(result);
|
const user = saveKoringAuth(result);
|
||||||
|
|
||||||
// 同时写入配置文件
|
// 同步到配置文件(主进程权威模型:合并内存缓存 + debounce 写盘)
|
||||||
try {
|
try {
|
||||||
const config = loadConfig();
|
updateConfig({
|
||||||
(config as any).koringUser = {
|
koringUser: {
|
||||||
sub: user.sub,
|
sub: user.sub,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
picture: user.picture,
|
picture: user.picture,
|
||||||
accessToken: result.access_token,
|
accessToken: result.access_token,
|
||||||
refreshToken: result.refresh_token,
|
refreshToken: result.refresh_token,
|
||||||
};
|
},
|
||||||
saveConfig(config);
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[koring-auth] failed to save user to config:', e);
|
console.error('[koring-auth] failed to save user to config:', e);
|
||||||
}
|
}
|
||||||
@@ -58,17 +58,17 @@ export function registerKoringAuthHandlers() {
|
|||||||
|
|
||||||
// 同步到配置文件
|
// 同步到配置文件
|
||||||
try {
|
try {
|
||||||
const config = loadConfig();
|
updateConfig({
|
||||||
(config as any).koringUser = {
|
koringUser: {
|
||||||
sub: user.sub,
|
sub: user.sub,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
picture: user.picture,
|
picture: user.picture,
|
||||||
accessToken: result.access_token,
|
accessToken: result.access_token,
|
||||||
refreshToken: result.refresh_token,
|
refreshToken: result.refresh_token,
|
||||||
};
|
},
|
||||||
saveConfig(config);
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
return { success: true, data: { user }, error: null };
|
return { success: true, data: { user }, error: null };
|
||||||
@@ -80,10 +80,10 @@ export function registerKoringAuthHandlers() {
|
|||||||
ipcMain.handle('koring-auth:get-user', () => {
|
ipcMain.handle('koring-auth:get-user', () => {
|
||||||
try {
|
try {
|
||||||
const stored = readKoringAuth();
|
const stored = readKoringAuth();
|
||||||
// 也从配置文件读取
|
// 也从配置文件读取(内存权威)
|
||||||
if (!stored?.user?.sub) {
|
if (!stored?.user?.sub) {
|
||||||
try {
|
try {
|
||||||
const config = loadConfig();
|
const config = getConfig();
|
||||||
const ku = (config as any).koringUser;
|
const ku = (config as any).koringUser;
|
||||||
if (ku?.sub) {
|
if (ku?.sub) {
|
||||||
return { success: true, data: { user: ku, access_token: '', refresh_token: '', id_token: '', expires_at: 0 }, error: null };
|
return { success: true, data: { user: ku, access_token: '', refresh_token: '', id_token: '', expires_at: 0 }, error: null };
|
||||||
@@ -101,9 +101,7 @@ export function registerKoringAuthHandlers() {
|
|||||||
deleteKoringAuth();
|
deleteKoringAuth();
|
||||||
// 清除配置文件中的用户数据
|
// 清除配置文件中的用户数据
|
||||||
try {
|
try {
|
||||||
const config = loadConfig();
|
deleteConfigKey('koringUser');
|
||||||
delete (config as any).koringUser;
|
|
||||||
saveConfig(config);
|
|
||||||
} catch {}
|
} catch {}
|
||||||
return { success: true, data: null, error: null };
|
return { success: true, data: null, error: null };
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
|
|||||||
+33
-22
@@ -1,5 +1,7 @@
|
|||||||
import electron from 'electron';
|
import electron from 'electron';
|
||||||
import { launchMinecraft, diagnoseVersion } from '../core/launcher';
|
import { launchGame, diagnoseVersion } from '../core/launcher';
|
||||||
|
import { resolveGamePath } from '../core/paths';
|
||||||
|
import { getConfig, type AppConfig } from '../config';
|
||||||
|
|
||||||
const { ipcMain } = electron;
|
const { ipcMain } = electron;
|
||||||
|
|
||||||
@@ -7,36 +9,45 @@ interface WinRef {
|
|||||||
mainWindow: electron.BrowserWindow | null;
|
mainWindow: electron.BrowserWindow | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 游戏窗口就绪后按配置处理启动器窗口(afterLaunch)
|
||||||
|
function applyAfterLaunch(config: AppConfig, win: WinRef): void {
|
||||||
|
const mode = config.advanced?.afterLaunch ?? 'close';
|
||||||
|
if (mode === 'close') {
|
||||||
|
win.mainWindow?.close();
|
||||||
|
} else if (mode === 'minimize') {
|
||||||
|
win.mainWindow?.minimize();
|
||||||
|
}
|
||||||
|
// 'keep' → 无操作
|
||||||
|
}
|
||||||
|
|
||||||
export function registerLaunchHandlers(win: WinRef) {
|
export function registerLaunchHandlers(win: WinRef) {
|
||||||
ipcMain.handle('launch:launch', async (_event, payload: {
|
ipcMain.handle('launch:launch', async (_event, payload: {
|
||||||
|
instanceName: string;
|
||||||
gamePath: string;
|
gamePath: string;
|
||||||
javaPath: string;
|
profile: { username: string; uuid: string; accessToken?: string };
|
||||||
version: string;
|
|
||||||
username: string;
|
|
||||||
uuid: string;
|
|
||||||
accessToken?: string;
|
|
||||||
memory?: { min?: string; max?: string };
|
|
||||||
jvmArgs?: string[];
|
|
||||||
gameArgs?: string[];
|
|
||||||
server?: { ip: string; port?: number };
|
server?: { ip: string; port?: number };
|
||||||
detached?: boolean;
|
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
const requestId = `launch-${Date.now()}`;
|
const requestId = `launch-${Date.now()}`;
|
||||||
|
|
||||||
const result = await launchMinecraft({
|
// 使用主进程内存配置(唯一权威,永远是最新值,无磁盘竞争)
|
||||||
gamePath: payload.gamePath,
|
const config = getConfig();
|
||||||
javaPath: payload.javaPath,
|
|
||||||
version: payload.version,
|
// 快速进入服务器:UI 显式传入优先,否则使用配置中保存的 advanced.server
|
||||||
username: payload.username,
|
const server = payload.server
|
||||||
uuid: payload.uuid,
|
?? (config.advanced?.server?.ip ? config.advanced.server : undefined);
|
||||||
accessToken: payload.accessToken,
|
|
||||||
memory: payload.memory,
|
const result = await launchGame({
|
||||||
jvmArgs: payload.jvmArgs,
|
config,
|
||||||
gameArgs: payload.gameArgs,
|
instanceName: payload.instanceName,
|
||||||
server: payload.server,
|
gamePath: resolveGamePath(payload.gamePath),
|
||||||
detached: payload.detached,
|
profile: payload.profile,
|
||||||
|
server,
|
||||||
onEvent: (event) => {
|
onEvent: (event) => {
|
||||||
|
// afterLaunch 副作用:窗口就绪后关闭/最小化启动器
|
||||||
|
if (event.event === 'window-ready') {
|
||||||
|
applyAfterLaunch(config, win);
|
||||||
|
}
|
||||||
win.mainWindow?.webContents.send('launch:event', { requestId, ...event });
|
win.mainWindow?.webContents.send('launch:event', { requestId, ...event });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+34
-5
@@ -13,7 +13,9 @@ import { registerSystemHandlers } from './handlers/system';
|
|||||||
import { registerWindowHandlers } from './handlers/window';
|
import { registerWindowHandlers } from './handlers/window';
|
||||||
import { registerCrashHandlers, setupCrashListeners, testCrashDialog } from './handlers/crash-monitor';
|
import { registerCrashHandlers, setupCrashListeners, testCrashDialog } from './handlers/crash-monitor';
|
||||||
import { registerKoringAuthHandlers } from './handlers/koring-auth';
|
import { registerKoringAuthHandlers } from './handlers/koring-auth';
|
||||||
import { loadConfig, saveConfig, configExists } from './config';
|
import { registerJavaHandlers } from './handlers/java';
|
||||||
|
import { saveConfig, configExists, getConfig, flushConfig, configPath } from './config';
|
||||||
|
import { authPath } from './auth';
|
||||||
|
|
||||||
const { app } = electron;
|
const { app } = electron;
|
||||||
|
|
||||||
@@ -28,8 +30,30 @@ const win: { mainWindow: electron.BrowserWindow | null; splashWindow: electron.B
|
|||||||
splashWindow: null,
|
splashWindow: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 迁移旧版「可执行文件旁」存储 → userData(仅打包模式)。
|
||||||
|
// 复制而非移动,避免破坏用户已有文件;userData 已有目标文件则跳过。
|
||||||
|
function migrateLegacyFiles(): void {
|
||||||
|
if (!app.isPackaged) return;
|
||||||
|
const exeDir = path.dirname(app.getPath('exe'));
|
||||||
|
const pairs: { name: string; dest: string }[] = [
|
||||||
|
{ name: 'Koring.yml', dest: configPath() },
|
||||||
|
{ name: 'koring-auth.json', dest: authPath() },
|
||||||
|
];
|
||||||
|
for (const { name, dest } of pairs) {
|
||||||
|
if (fs.existsSync(dest)) continue;
|
||||||
|
const src = path.join(exeDir, name);
|
||||||
|
if (!fs.existsSync(src)) continue;
|
||||||
|
try {
|
||||||
|
fs.copyFileSync(src, dest);
|
||||||
|
console.log(`[migrate] copied ${name} → ${dest}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[migrate] failed to copy ${name}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Startup checks: .minecraft dir + config file + first launch detection
|
// Startup checks: .minecraft dir + config file + first launch detection
|
||||||
function runStartupChecks(): { isFirstLaunch: boolean; config: ReturnType<typeof loadConfig> } {
|
function runStartupChecks(): { isFirstLaunch: boolean; config: ReturnType<typeof getConfig> } {
|
||||||
const dataPath = app.isPackaged
|
const dataPath = app.isPackaged
|
||||||
? path.dirname(app.getPath('exe'))
|
? path.dirname(app.getPath('exe'))
|
||||||
: path.join(__dirname, '..');
|
: path.join(__dirname, '..');
|
||||||
@@ -44,8 +68,8 @@ function runStartupChecks(): { isFirstLaunch: boolean; config: ReturnType<typeof
|
|||||||
const hasConfig = configExists();
|
const hasConfig = configExists();
|
||||||
const isFirstLaunch = !hasConfig;
|
const isFirstLaunch = !hasConfig;
|
||||||
|
|
||||||
// 3. Load (or create) config
|
// 3. Load (or create) config(getConfig 会缓存到主进程内存,成为唯一权威)
|
||||||
const config = loadConfig();
|
const config = getConfig();
|
||||||
if (isFirstLaunch) {
|
if (isFirstLaunch) {
|
||||||
saveConfig(config);
|
saveConfig(config);
|
||||||
}
|
}
|
||||||
@@ -130,7 +154,7 @@ function createMainWindow(): electron.BrowserWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function registerAllHandlers() {
|
function registerAllHandlers() {
|
||||||
registerConfigHandlers();
|
registerConfigHandlers(win);
|
||||||
registerAuthHandlers();
|
registerAuthHandlers();
|
||||||
registerInstallHandlers(win);
|
registerInstallHandlers(win);
|
||||||
registerLaunchHandlers(win);
|
registerLaunchHandlers(win);
|
||||||
@@ -142,11 +166,15 @@ function registerAllHandlers() {
|
|||||||
registerWindowHandlers(win);
|
registerWindowHandlers(win);
|
||||||
registerCrashHandlers();
|
registerCrashHandlers();
|
||||||
registerKoringAuthHandlers();
|
registerKoringAuthHandlers();
|
||||||
|
registerJavaHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
registerAllHandlers();
|
registerAllHandlers();
|
||||||
|
|
||||||
|
// Migrate legacy exe-dir config/auth to userData before anything reads them
|
||||||
|
migrateLegacyFiles();
|
||||||
|
|
||||||
// Run startup checks before creating windows
|
// Run startup checks before creating windows
|
||||||
const { isFirstLaunch, config } = runStartupChecks();
|
const { isFirstLaunch, config } = runStartupChecks();
|
||||||
|
|
||||||
@@ -194,6 +222,7 @@ app.whenReady().then(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
|
flushConfig();
|
||||||
app.quit();
|
app.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,13 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
return () => ipcRenderer.removeListener('config:preload', handler);
|
return () => ipcRenderer.removeListener('config:preload', handler);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Config changed broadcast (authoritative full config from main process)
|
||||||
|
onConfigChanged: (callback: (config: unknown) => void) => {
|
||||||
|
const handler = (_event: Electron.IpcRendererEvent, config: unknown) => callback(config);
|
||||||
|
ipcRenderer.on('config:changed', handler);
|
||||||
|
return () => ipcRenderer.removeListener('config:changed', handler);
|
||||||
|
},
|
||||||
|
|
||||||
// Background image — pick file, copy to userData, return base64 data URL
|
// Background image — pick file, copy to userData, return base64 data URL
|
||||||
pickBackgroundImage: async (): Promise<string | null> => {
|
pickBackgroundImage: async (): Promise<string | null> => {
|
||||||
const result = await ipcRenderer.invoke('dialog:openFile', {
|
const result = await ipcRenderer.invoke('dialog:openFile', {
|
||||||
|
|||||||
+2
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "koring-launcher",
|
"name": "koring-launcher",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.1.2",
|
"version": "1.2.0",
|
||||||
"description": "Koring Launcher - Minecraft launcher built with Electron + React",
|
"description": "Koring Launcher - Minecraft launcher built with Electron + React",
|
||||||
"author": "Shenzhen Lingke Network Technology Co., Ltd.",
|
"author": "Shenzhen Lingke Network Technology Co., Ltd.",
|
||||||
"license": "LL-1.0",
|
"license": "LL-1.0",
|
||||||
@@ -49,6 +49,7 @@
|
|||||||
"@xmcl/task": "4.1.1",
|
"@xmcl/task": "4.1.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"electron-updater": "^6.8.9",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lucide-react": "^1.21.0",
|
"lucide-react": "^1.21.0",
|
||||||
"motion": "^12.42.2",
|
"motion": "^12.42.2",
|
||||||
|
|||||||
Generated
+218
-155
@@ -22,10 +22,10 @@ importers:
|
|||||||
version: 5.2.9
|
version: 5.2.9
|
||||||
'@fontsource-variable/inter':
|
'@fontsource-variable/inter':
|
||||||
specifier: ^5.2.8
|
specifier: ^5.2.8
|
||||||
version: 5.2.8
|
version: 5.3.0
|
||||||
'@heroui/react':
|
'@heroui/react':
|
||||||
specifier: ^3.2.2
|
specifier: ^3.2.2
|
||||||
version: 3.2.2(@react-spectrum/provider@3.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(tailwindcss@4.3.1)
|
version: 3.2.2(@react-spectrum/provider@3.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/react-dom@19.2.5(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(tailwindcss@4.3.1)
|
||||||
'@heroui/styles':
|
'@heroui/styles':
|
||||||
specifier: ^3.2.2
|
specifier: ^3.2.2
|
||||||
version: 3.2.2(tailwind-merge@3.6.0)(tailwindcss@4.3.1)
|
version: 3.2.2(tailwind-merge@3.6.0)(tailwindcss@4.3.1)
|
||||||
@@ -59,6 +59,9 @@ importers:
|
|||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
|
electron-updater:
|
||||||
|
specifier: ^6.8.9
|
||||||
|
version: 6.8.9
|
||||||
js-yaml:
|
js-yaml:
|
||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.2.0
|
version: 4.2.0
|
||||||
@@ -82,7 +85,7 @@ importers:
|
|||||||
version: 19.2.7(react@19.2.7)
|
version: 19.2.7(react@19.2.7)
|
||||||
sonner:
|
sonner:
|
||||||
specifier: ^2.0.7
|
specifier: ^2.0.7
|
||||||
version: 2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 2.0.8(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^3.6.0
|
specifier: ^3.6.0
|
||||||
version: 3.6.0
|
version: 3.6.0
|
||||||
@@ -107,10 +110,10 @@ importers:
|
|||||||
version: 19.2.17
|
version: 19.2.17
|
||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
specifier: ^19.1.6
|
specifier: ^19.1.6
|
||||||
version: 19.2.3(@types/react@19.2.17)
|
version: 19.2.5(@types/react@19.2.17)
|
||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^4.6.0
|
specifier: ^4.6.0
|
||||||
version: 4.7.0(supports-color@8.1.1)(vite@7.3.5(@types/node@26.0.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4))
|
version: 4.7.0(vite@7.3.5(@types/node@26.0.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4))
|
||||||
concurrently:
|
concurrently:
|
||||||
specifier: ^9.1.0
|
specifier: ^9.1.0
|
||||||
version: 9.2.3
|
version: 9.2.3
|
||||||
@@ -119,7 +122,7 @@ importers:
|
|||||||
version: 33.4.11(supports-color@8.1.1)
|
version: 33.4.11(supports-color@8.1.1)
|
||||||
electron-builder:
|
electron-builder:
|
||||||
specifier: ^25.1.8
|
specifier: ^25.1.8
|
||||||
version: 25.1.8(bluebird@3.7.2)(electron-builder-squirrel-windows@25.1.8)(supports-color@8.1.1)
|
version: 25.1.8(electron-builder-squirrel-windows@25.1.8)
|
||||||
shadcn:
|
shadcn:
|
||||||
specifier: ^4.11.0
|
specifier: ^4.11.0
|
||||||
version: 4.11.0(supports-color@8.1.1)(typescript@5.8.3)
|
version: 4.11.0(supports-color@8.1.1)(typescript@5.8.3)
|
||||||
@@ -705,8 +708,8 @@ packages:
|
|||||||
'@fontsource-variable/geist@5.2.9':
|
'@fontsource-variable/geist@5.2.9':
|
||||||
resolution: {integrity: sha512-TP+QSBG3wxKGPE33CbMy/L0Nu3qvJ6Fy81Yc4LnQ95xH+i+cfEp8fyU8/kfV14YwszxIFPhnoMTbjL71waVpyQ==}
|
resolution: {integrity: sha512-TP+QSBG3wxKGPE33CbMy/L0Nu3qvJ6Fy81Yc4LnQ95xH+i+cfEp8fyU8/kfV14YwszxIFPhnoMTbjL71waVpyQ==}
|
||||||
|
|
||||||
'@fontsource-variable/inter@5.2.8':
|
'@fontsource-variable/inter@5.3.0':
|
||||||
resolution: {integrity: sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==}
|
resolution: {integrity: sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==}
|
||||||
|
|
||||||
'@formatjs/ecma402-abstract@2.3.6':
|
'@formatjs/ecma402-abstract@2.3.6':
|
||||||
resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==}
|
resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==}
|
||||||
@@ -744,9 +747,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
hono: ^4
|
hono: ^4
|
||||||
|
|
||||||
'@internationalized/date@3.12.2':
|
|
||||||
resolution: {integrity: sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==}
|
|
||||||
|
|
||||||
'@internationalized/date@3.12.3':
|
'@internationalized/date@3.12.3':
|
||||||
resolution: {integrity: sha512-fuLX+3ZKLsxI73y8b01EG/WjHb6gE6weCqlfawPO27kBWGMh9G1yH6Csv1uU7/cac9H2GHmOMt6CjmuQ1aia4Q==}
|
resolution: {integrity: sha512-fuLX+3ZKLsxI73y8b01EG/WjHb6gE6weCqlfawPO27kBWGMh9G1yH6Csv1uU7/cac9H2GHmOMt6CjmuQ1aia4Q==}
|
||||||
|
|
||||||
@@ -1322,8 +1322,8 @@ packages:
|
|||||||
'@types/plist@3.0.5':
|
'@types/plist@3.0.5':
|
||||||
resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==}
|
resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==}
|
||||||
|
|
||||||
'@types/react-dom@19.2.3':
|
'@types/react-dom@19.2.5':
|
||||||
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
|
resolution: {integrity: sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/react': ^19.2.0
|
'@types/react': ^19.2.0
|
||||||
|
|
||||||
@@ -1603,6 +1603,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==}
|
resolution: {integrity: sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
builder-util-runtime@9.7.0:
|
||||||
|
resolution: {integrity: sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
builder-util@25.1.7:
|
builder-util@25.1.7:
|
||||||
resolution: {integrity: sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww==}
|
resolution: {integrity: sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww==}
|
||||||
|
|
||||||
@@ -1996,6 +2000,9 @@ packages:
|
|||||||
electron-to-chromium@1.5.376:
|
electron-to-chromium@1.5.376:
|
||||||
resolution: {integrity: sha512-cUVA7/RvbFTEuw/i3obUwDTRIXojaxkResf+ibByPFxjc6XK3VNtcQXV0NSbAlJ0FMjcJGgftVVB4Qo184EXvA==}
|
resolution: {integrity: sha512-cUVA7/RvbFTEuw/i3obUwDTRIXojaxkResf+ibByPFxjc6XK3VNtcQXV0NSbAlJ0FMjcJGgftVVB4Qo184EXvA==}
|
||||||
|
|
||||||
|
electron-updater@6.8.9:
|
||||||
|
resolution: {integrity: sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig==}
|
||||||
|
|
||||||
electron@33.4.11:
|
electron@33.4.11:
|
||||||
resolution: {integrity: sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==}
|
resolution: {integrity: sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==}
|
||||||
engines: {node: '>= 12.20.55'}
|
engines: {node: '>= 12.20.55'}
|
||||||
@@ -2299,19 +2306,23 @@ packages:
|
|||||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
glob@10.4.5:
|
glob@10.5.0:
|
||||||
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
|
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
|
||||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
glob@7.2.0:
|
||||||
|
resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==}
|
||||||
|
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||||
|
|
||||||
glob@7.2.3:
|
glob@7.2.3:
|
||||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
deprecated: Glob versions prior to v9 are no longer supported
|
||||||
|
|
||||||
glob@8.1.0:
|
glob@8.1.0:
|
||||||
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
|
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
deprecated: Glob versions prior to v9 are no longer supported
|
||||||
|
|
||||||
global-agent@3.0.0:
|
global-agent@3.0.0:
|
||||||
resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==}
|
resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==}
|
||||||
@@ -2750,9 +2761,16 @@ packages:
|
|||||||
lodash.difference@4.5.0:
|
lodash.difference@4.5.0:
|
||||||
resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==}
|
resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==}
|
||||||
|
|
||||||
|
lodash.escaperegexp@4.1.2:
|
||||||
|
resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==}
|
||||||
|
|
||||||
lodash.flatten@4.4.0:
|
lodash.flatten@4.4.0:
|
||||||
resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==}
|
resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==}
|
||||||
|
|
||||||
|
lodash.isequal@4.5.0:
|
||||||
|
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
|
||||||
|
deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
|
||||||
|
|
||||||
lodash.isplainobject@4.0.6:
|
lodash.isplainobject@4.0.6:
|
||||||
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
||||||
|
|
||||||
@@ -3420,6 +3438,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
semver@7.7.4:
|
||||||
|
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
semver@7.8.4:
|
semver@7.8.4:
|
||||||
resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==}
|
resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -3505,11 +3528,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==}
|
resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==}
|
||||||
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
|
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
|
||||||
|
|
||||||
sonner@2.0.7:
|
sonner@2.0.8:
|
||||||
resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
|
resolution: {integrity: sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
'@types/react': ^18.0.0 || ^19.0.0
|
||||||
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||||
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
source-map-js@1.2.1:
|
source-map-js@1.2.1:
|
||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
@@ -3647,6 +3674,9 @@ packages:
|
|||||||
tiny-invariant@1.3.3:
|
tiny-invariant@1.3.3:
|
||||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||||
|
|
||||||
|
tiny-typed-emitter@2.1.0:
|
||||||
|
resolution: {integrity: sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==}
|
||||||
|
|
||||||
tinyglobby@0.2.17:
|
tinyglobby@0.2.17:
|
||||||
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
|
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
@@ -3939,7 +3969,7 @@ snapshots:
|
|||||||
|
|
||||||
'@adobe/react-spectrum@3.47.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@adobe/react-spectrum@3.47.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@internationalized/date': 3.12.2
|
'@internationalized/date': 3.12.3
|
||||||
'@react-types/shared': 3.36.0(react@19.2.7)
|
'@react-types/shared': 3.36.0(react@19.2.7)
|
||||||
'@spectrum-icons/ui': 3.7.1(@adobe/react-spectrum@3.47.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@spectrum-icons/ui': 3.7.1(@adobe/react-spectrum@3.47.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@spectrum-icons/workflow': 4.3.1(@adobe/react-spectrum@3.47.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@spectrum-icons/workflow': 4.3.1(@adobe/react-spectrum@3.47.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
@@ -3962,16 +3992,16 @@ snapshots:
|
|||||||
|
|
||||||
'@babel/compat-data@7.29.7': {}
|
'@babel/compat-data@7.29.7': {}
|
||||||
|
|
||||||
'@babel/core@7.29.7(supports-color@8.1.1)':
|
'@babel/core@7.29.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.29.7
|
'@babel/code-frame': 7.29.7
|
||||||
'@babel/generator': 7.29.7
|
'@babel/generator': 7.29.7
|
||||||
'@babel/helper-compilation-targets': 7.29.7
|
'@babel/helper-compilation-targets': 7.29.7
|
||||||
'@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)
|
'@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
|
||||||
'@babel/helpers': 7.29.7
|
'@babel/helpers': 7.29.7
|
||||||
'@babel/parser': 7.29.7
|
'@babel/parser': 7.29.7
|
||||||
'@babel/template': 7.29.7
|
'@babel/template': 7.29.7
|
||||||
'@babel/traverse': 7.29.7(supports-color@8.1.1)
|
'@babel/traverse': 7.29.7
|
||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
'@jridgewell/remapping': 2.3.5
|
'@jridgewell/remapping': 2.3.5
|
||||||
convert-source-map: 2.0.0
|
convert-source-map: 2.0.0
|
||||||
@@ -4002,41 +4032,41 @@ snapshots:
|
|||||||
lru-cache: 5.1.1
|
lru-cache: 5.1.1
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
|
|
||||||
'@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)':
|
'@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7(supports-color@8.1.1)
|
'@babel/core': 7.29.7
|
||||||
'@babel/helper-annotate-as-pure': 7.29.7
|
'@babel/helper-annotate-as-pure': 7.29.7
|
||||||
'@babel/helper-member-expression-to-functions': 7.29.7(supports-color@8.1.1)
|
'@babel/helper-member-expression-to-functions': 7.29.7
|
||||||
'@babel/helper-optimise-call-expression': 7.29.7
|
'@babel/helper-optimise-call-expression': 7.29.7
|
||||||
'@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)
|
'@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7)
|
||||||
'@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@8.1.1)
|
'@babel/helper-skip-transparent-expression-wrappers': 7.29.7
|
||||||
'@babel/traverse': 7.29.7(supports-color@8.1.1)
|
'@babel/traverse': 7.29.7
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@babel/helper-globals@7.29.7': {}
|
'@babel/helper-globals@7.29.7': {}
|
||||||
|
|
||||||
'@babel/helper-member-expression-to-functions@7.29.7(supports-color@8.1.1)':
|
'@babel/helper-member-expression-to-functions@7.29.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/traverse': 7.29.7(supports-color@8.1.1)
|
'@babel/traverse': 7.29.7
|
||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@babel/helper-module-imports@7.29.7(supports-color@8.1.1)':
|
'@babel/helper-module-imports@7.29.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/traverse': 7.29.7(supports-color@8.1.1)
|
'@babel/traverse': 7.29.7
|
||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)':
|
'@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7(supports-color@8.1.1)
|
'@babel/core': 7.29.7
|
||||||
'@babel/helper-module-imports': 7.29.7(supports-color@8.1.1)
|
'@babel/helper-module-imports': 7.29.7
|
||||||
'@babel/helper-validator-identifier': 7.29.7
|
'@babel/helper-validator-identifier': 7.29.7
|
||||||
'@babel/traverse': 7.29.7(supports-color@8.1.1)
|
'@babel/traverse': 7.29.7
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -4046,18 +4076,18 @@ snapshots:
|
|||||||
|
|
||||||
'@babel/helper-plugin-utils@7.29.7': {}
|
'@babel/helper-plugin-utils@7.29.7': {}
|
||||||
|
|
||||||
'@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)':
|
'@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7(supports-color@8.1.1)
|
'@babel/core': 7.29.7
|
||||||
'@babel/helper-member-expression-to-functions': 7.29.7(supports-color@8.1.1)
|
'@babel/helper-member-expression-to-functions': 7.29.7
|
||||||
'@babel/helper-optimise-call-expression': 7.29.7
|
'@babel/helper-optimise-call-expression': 7.29.7
|
||||||
'@babel/traverse': 7.29.7(supports-color@8.1.1)
|
'@babel/traverse': 7.29.7
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@babel/helper-skip-transparent-expression-wrappers@7.29.7(supports-color@8.1.1)':
|
'@babel/helper-skip-transparent-expression-wrappers@7.29.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/traverse': 7.29.7(supports-color@8.1.1)
|
'@babel/traverse': 7.29.7
|
||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -4077,53 +4107,53 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
|
|
||||||
'@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))':
|
'@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7(supports-color@8.1.1)
|
'@babel/core': 7.29.7
|
||||||
'@babel/helper-plugin-utils': 7.29.7
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
'@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))':
|
'@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7(supports-color@8.1.1)
|
'@babel/core': 7.29.7
|
||||||
'@babel/helper-plugin-utils': 7.29.7
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
'@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)':
|
'@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7(supports-color@8.1.1)
|
'@babel/core': 7.29.7
|
||||||
'@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)
|
'@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
|
||||||
'@babel/helper-plugin-utils': 7.29.7
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))':
|
'@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7(supports-color@8.1.1)
|
'@babel/core': 7.29.7
|
||||||
'@babel/helper-plugin-utils': 7.29.7
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
'@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))':
|
'@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7(supports-color@8.1.1)
|
'@babel/core': 7.29.7
|
||||||
'@babel/helper-plugin-utils': 7.29.7
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
'@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)':
|
'@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7(supports-color@8.1.1)
|
'@babel/core': 7.29.7
|
||||||
'@babel/helper-annotate-as-pure': 7.29.7
|
'@babel/helper-annotate-as-pure': 7.29.7
|
||||||
'@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)
|
'@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7)
|
||||||
'@babel/helper-plugin-utils': 7.29.7
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
'@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@8.1.1)
|
'@babel/helper-skip-transparent-expression-wrappers': 7.29.7
|
||||||
'@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))
|
'@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@babel/preset-typescript@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)':
|
'@babel/preset-typescript@7.29.7(@babel/core@7.29.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7(supports-color@8.1.1)
|
'@babel/core': 7.29.7
|
||||||
'@babel/helper-plugin-utils': 7.29.7
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
'@babel/helper-validator-option': 7.29.7
|
'@babel/helper-validator-option': 7.29.7
|
||||||
'@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))
|
'@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7)
|
||||||
'@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)
|
'@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7)
|
||||||
'@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)
|
'@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -4135,7 +4165,7 @@ snapshots:
|
|||||||
'@babel/parser': 7.29.7
|
'@babel/parser': 7.29.7
|
||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
|
|
||||||
'@babel/traverse@7.29.7(supports-color@8.1.1)':
|
'@babel/traverse@7.29.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.29.7
|
'@babel/code-frame': 7.29.7
|
||||||
'@babel/generator': 7.29.7
|
'@babel/generator': 7.29.7
|
||||||
@@ -4208,7 +4238,7 @@ snapshots:
|
|||||||
'@electron/asar@3.4.1':
|
'@electron/asar@3.4.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
commander: 5.1.0
|
commander: 5.1.0
|
||||||
glob: 7.2.3
|
glob: 7.2.0
|
||||||
minimatch: 3.1.5
|
minimatch: 3.1.5
|
||||||
|
|
||||||
'@electron/get@2.0.3(supports-color@8.1.1)':
|
'@electron/get@2.0.3(supports-color@8.1.1)':
|
||||||
@@ -4225,7 +4255,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@electron/notarize@2.5.0(supports-color@8.1.1)':
|
'@electron/notarize@2.5.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
fs-extra: 9.1.0
|
fs-extra: 9.1.0
|
||||||
@@ -4233,7 +4263,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@electron/osx-sign@1.3.1(supports-color@8.1.1)':
|
'@electron/osx-sign@1.3.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
compare-version: 0.1.2
|
compare-version: 0.1.2
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
@@ -4244,7 +4274,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@electron/rebuild@3.6.1(bluebird@3.7.2)(supports-color@8.1.1)':
|
'@electron/rebuild@3.6.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@malept/cross-spawn-promise': 2.0.0
|
'@malept/cross-spawn-promise': 2.0.0
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
@@ -4254,9 +4284,9 @@ snapshots:
|
|||||||
got: 11.8.6
|
got: 11.8.6
|
||||||
node-abi: 3.92.0
|
node-abi: 3.92.0
|
||||||
node-api-version: 0.2.1
|
node-api-version: 0.2.1
|
||||||
node-gyp: 9.4.1(bluebird@3.7.2)(supports-color@8.1.1)
|
node-gyp: 9.4.1
|
||||||
ora: 5.4.1
|
ora: 5.4.1
|
||||||
read-binary-file-arch: 1.0.6(supports-color@8.1.1)
|
read-binary-file-arch: 1.0.6
|
||||||
semver: 7.8.4
|
semver: 7.8.4
|
||||||
tar: 6.2.1
|
tar: 6.2.1
|
||||||
yargs: 17.7.3
|
yargs: 17.7.3
|
||||||
@@ -4264,7 +4294,7 @@ snapshots:
|
|||||||
- bluebird
|
- bluebird
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@electron/universal@2.0.1(supports-color@8.1.1)':
|
'@electron/universal@2.0.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@electron/asar': 3.4.1
|
'@electron/asar': 3.4.1
|
||||||
'@malept/cross-spawn-promise': 2.0.0
|
'@malept/cross-spawn-promise': 2.0.0
|
||||||
@@ -4451,7 +4481,7 @@ snapshots:
|
|||||||
|
|
||||||
'@fontsource-variable/geist@5.2.9': {}
|
'@fontsource-variable/geist@5.2.9': {}
|
||||||
|
|
||||||
'@fontsource-variable/inter@5.2.8': {}
|
'@fontsource-variable/inter@5.3.0': {}
|
||||||
|
|
||||||
'@formatjs/ecma402-abstract@2.3.6':
|
'@formatjs/ecma402-abstract@2.3.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -4481,10 +4511,10 @@ snapshots:
|
|||||||
|
|
||||||
'@gar/promisify@1.1.3': {}
|
'@gar/promisify@1.1.3': {}
|
||||||
|
|
||||||
'@heroui/react@3.2.2(@react-spectrum/provider@3.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(tailwindcss@4.3.1)':
|
'@heroui/react@3.2.2(@react-spectrum/provider@3.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/react-dom@19.2.5(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(tailwindcss@4.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@heroui/styles': 3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.3.1)
|
'@heroui/styles': 3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.3.1)
|
||||||
'@radix-ui/react-avatar': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-avatar': 1.1.11(@types/react-dom@19.2.5(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@react-aria/i18n': 3.13.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@react-aria/i18n': 3.13.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@react-aria/ssr': 3.10.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@react-aria/ssr': 3.10.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@react-aria/utils': 3.34.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@react-aria/utils': 3.34.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
@@ -4524,10 +4554,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
hono: 4.12.26
|
hono: 4.12.26
|
||||||
|
|
||||||
'@internationalized/date@3.12.2':
|
|
||||||
dependencies:
|
|
||||||
'@swc/helpers': 0.5.23
|
|
||||||
|
|
||||||
'@internationalized/date@3.12.3':
|
'@internationalized/date@3.12.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@swc/helpers': 0.5.23
|
'@swc/helpers': 0.5.23
|
||||||
@@ -4577,7 +4603,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
|
|
||||||
'@malept/flatpak-bundler@0.4.0(supports-color@8.1.1)':
|
'@malept/flatpak-bundler@0.4.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
fs-extra: 9.1.0
|
fs-extra: 9.1.0
|
||||||
@@ -4641,10 +4667,10 @@ snapshots:
|
|||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-avatar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-avatar@1.1.11(@types/react-dom@19.2.5(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-context': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-context': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.5(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.17)(react@19.2.7)
|
||||||
@@ -4652,7 +4678,7 @@ snapshots:
|
|||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.5(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.17)(react@19.2.7)':
|
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.17)(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -4666,14 +4692,14 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
|
|
||||||
'@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.5(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-slot': 1.2.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-slot': 1.2.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.5(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-slot@1.2.4(@types/react@19.2.17)(react@19.2.7)':
|
'@radix-ui/react-slot@1.2.4(@types/react@19.2.17)(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5043,7 +5069,7 @@ snapshots:
|
|||||||
xmlbuilder: 15.1.1
|
xmlbuilder: 15.1.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@types/react-dom@19.2.3(@types/react@19.2.17)':
|
'@types/react-dom@19.2.5(@types/react@19.2.17)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
|
|
||||||
@@ -5081,11 +5107,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 26.0.1
|
'@types/node': 26.0.1
|
||||||
|
|
||||||
'@vitejs/plugin-react@4.7.0(supports-color@8.1.1)(vite@7.3.5(@types/node@26.0.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4))':
|
'@vitejs/plugin-react@4.7.0(vite@7.3.5(@types/node@26.0.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7(supports-color@8.1.1)
|
'@babel/core': 7.29.7
|
||||||
'@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))
|
'@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7)
|
||||||
'@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))
|
'@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7)
|
||||||
'@rolldown/pluginutils': 1.0.0-beta.27
|
'@rolldown/pluginutils': 1.0.0-beta.27
|
||||||
'@types/babel__core': 7.20.5
|
'@types/babel__core': 7.20.5
|
||||||
react-refresh: 0.17.0
|
react-refresh: 0.17.0
|
||||||
@@ -5139,7 +5165,7 @@ snapshots:
|
|||||||
mime-types: 3.0.2
|
mime-types: 3.0.2
|
||||||
negotiator: 1.0.0
|
negotiator: 1.0.0
|
||||||
|
|
||||||
agent-base@6.0.2(supports-color@8.1.1):
|
agent-base@6.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -5196,28 +5222,28 @@ snapshots:
|
|||||||
|
|
||||||
app-builder-bin@5.0.0-alpha.10: {}
|
app-builder-bin@5.0.0-alpha.10: {}
|
||||||
|
|
||||||
app-builder-lib@25.1.8(bluebird@3.7.2)(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8)(supports-color@8.1.1):
|
app-builder-lib@25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@develar/schema-utils': 2.6.5
|
'@develar/schema-utils': 2.6.5
|
||||||
'@electron/notarize': 2.5.0(supports-color@8.1.1)
|
'@electron/notarize': 2.5.0
|
||||||
'@electron/osx-sign': 1.3.1(supports-color@8.1.1)
|
'@electron/osx-sign': 1.3.1
|
||||||
'@electron/rebuild': 3.6.1(bluebird@3.7.2)(supports-color@8.1.1)
|
'@electron/rebuild': 3.6.1
|
||||||
'@electron/universal': 2.0.1(supports-color@8.1.1)
|
'@electron/universal': 2.0.1
|
||||||
'@malept/flatpak-bundler': 0.4.0(supports-color@8.1.1)
|
'@malept/flatpak-bundler': 0.4.0
|
||||||
'@types/fs-extra': 9.0.13
|
'@types/fs-extra': 9.0.13
|
||||||
async-exit-hook: 2.0.1
|
async-exit-hook: 2.0.1
|
||||||
bluebird-lst: 1.0.9
|
bluebird-lst: 1.0.9
|
||||||
builder-util: 25.1.7(supports-color@8.1.1)
|
builder-util: 25.1.7
|
||||||
builder-util-runtime: 9.2.10(supports-color@8.1.1)
|
builder-util-runtime: 9.2.10
|
||||||
chromium-pickle-js: 0.2.0
|
chromium-pickle-js: 0.2.0
|
||||||
config-file-ts: 0.2.8-rc1
|
config-file-ts: 0.2.8-rc1
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
dmg-builder: 25.1.8(bluebird@3.7.2)(electron-builder-squirrel-windows@25.1.8)(supports-color@8.1.1)
|
dmg-builder: 25.1.8(electron-builder-squirrel-windows@25.1.8)
|
||||||
dotenv: 16.6.1
|
dotenv: 16.6.1
|
||||||
dotenv-expand: 11.0.7
|
dotenv-expand: 11.0.7
|
||||||
ejs: 3.1.10
|
ejs: 3.1.10
|
||||||
electron-builder-squirrel-windows: 25.1.8(bluebird@3.7.2)(dmg-builder@25.1.8)(supports-color@8.1.1)
|
electron-builder-squirrel-windows: 25.1.8(dmg-builder@25.1.8)
|
||||||
electron-publish: 25.1.7(supports-color@8.1.1)
|
electron-publish: 25.1.7
|
||||||
form-data: 4.0.6
|
form-data: 4.0.6
|
||||||
fs-extra: 10.1.0
|
fs-extra: 10.1.0
|
||||||
hosted-git-info: 4.1.0
|
hosted-git-info: 4.1.0
|
||||||
@@ -5240,7 +5266,7 @@ snapshots:
|
|||||||
|
|
||||||
archiver-utils@2.1.0:
|
archiver-utils@2.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
glob: 7.2.3
|
glob: 7.2.0
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
lazystream: 1.0.1
|
lazystream: 1.0.1
|
||||||
lodash.defaults: 4.2.0
|
lodash.defaults: 4.2.0
|
||||||
@@ -5383,26 +5409,33 @@ snapshots:
|
|||||||
base64-js: 1.5.1
|
base64-js: 1.5.1
|
||||||
ieee754: 1.2.1
|
ieee754: 1.2.1
|
||||||
|
|
||||||
builder-util-runtime@9.2.10(supports-color@8.1.1):
|
builder-util-runtime@9.2.10:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
sax: 1.6.0
|
sax: 1.6.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
builder-util@25.1.7(supports-color@8.1.1):
|
builder-util-runtime@9.7.0:
|
||||||
|
dependencies:
|
||||||
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
|
sax: 1.6.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
builder-util@25.1.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
7zip-bin: 5.2.0
|
7zip-bin: 5.2.0
|
||||||
'@types/debug': 4.1.13
|
'@types/debug': 4.1.13
|
||||||
app-builder-bin: 5.0.0-alpha.10
|
app-builder-bin: 5.0.0-alpha.10
|
||||||
bluebird-lst: 1.0.9
|
bluebird-lst: 1.0.9
|
||||||
builder-util-runtime: 9.2.10(supports-color@8.1.1)
|
builder-util-runtime: 9.2.10
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
fs-extra: 10.1.0
|
fs-extra: 10.1.0
|
||||||
http-proxy-agent: 7.0.2(supports-color@8.1.1)
|
http-proxy-agent: 7.0.2
|
||||||
https-proxy-agent: 7.0.6(supports-color@8.1.1)
|
https-proxy-agent: 7.0.6
|
||||||
is-ci: 3.0.1
|
is-ci: 3.0.1
|
||||||
js-yaml: 4.2.0
|
js-yaml: 4.2.0
|
||||||
source-map-support: 0.5.21
|
source-map-support: 0.5.21
|
||||||
@@ -5417,7 +5450,7 @@ snapshots:
|
|||||||
|
|
||||||
bytes@3.1.2: {}
|
bytes@3.1.2: {}
|
||||||
|
|
||||||
cacache@16.1.3(bluebird@3.7.2):
|
cacache@16.1.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@npmcli/fs': 2.1.2
|
'@npmcli/fs': 2.1.2
|
||||||
'@npmcli/move-file': 2.0.1
|
'@npmcli/move-file': 2.0.1
|
||||||
@@ -5432,7 +5465,7 @@ snapshots:
|
|||||||
minipass-pipeline: 1.2.4
|
minipass-pipeline: 1.2.4
|
||||||
mkdirp: 1.0.4
|
mkdirp: 1.0.4
|
||||||
p-map: 4.0.0
|
p-map: 4.0.0
|
||||||
promise-inflight: 1.0.1(bluebird@3.7.2)
|
promise-inflight: 1.0.1
|
||||||
rimraf: 3.0.2
|
rimraf: 3.0.2
|
||||||
ssri: 9.0.1
|
ssri: 9.0.1
|
||||||
tar: 6.2.1
|
tar: 6.2.1
|
||||||
@@ -5572,7 +5605,7 @@ snapshots:
|
|||||||
|
|
||||||
config-file-ts@0.2.8-rc1:
|
config-file-ts@0.2.8-rc1:
|
||||||
dependencies:
|
dependencies:
|
||||||
glob: 10.4.5
|
glob: 10.5.0
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
|
|
||||||
console-control-strings@1.1.0: {}
|
console-control-strings@1.1.0: {}
|
||||||
@@ -5711,11 +5744,11 @@ snapshots:
|
|||||||
minimatch: 3.1.5
|
minimatch: 3.1.5
|
||||||
p-limit: 3.1.0
|
p-limit: 3.1.0
|
||||||
|
|
||||||
dmg-builder@25.1.8(bluebird@3.7.2)(electron-builder-squirrel-windows@25.1.8)(supports-color@8.1.1):
|
dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8):
|
||||||
dependencies:
|
dependencies:
|
||||||
app-builder-lib: 25.1.8(bluebird@3.7.2)(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8)(supports-color@8.1.1)
|
app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8)
|
||||||
builder-util: 25.1.7(supports-color@8.1.1)
|
builder-util: 25.1.7
|
||||||
builder-util-runtime: 9.2.10(supports-color@8.1.1)
|
builder-util-runtime: 9.2.10
|
||||||
fs-extra: 10.1.0
|
fs-extra: 10.1.0
|
||||||
iconv-lite: 0.6.3
|
iconv-lite: 0.6.3
|
||||||
js-yaml: 4.2.0
|
js-yaml: 4.2.0
|
||||||
@@ -5794,24 +5827,24 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
jake: 10.9.4
|
jake: 10.9.4
|
||||||
|
|
||||||
electron-builder-squirrel-windows@25.1.8(bluebird@3.7.2)(dmg-builder@25.1.8)(supports-color@8.1.1):
|
electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8):
|
||||||
dependencies:
|
dependencies:
|
||||||
app-builder-lib: 25.1.8(bluebird@3.7.2)(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8)(supports-color@8.1.1)
|
app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8)
|
||||||
archiver: 5.3.2
|
archiver: 5.3.2
|
||||||
builder-util: 25.1.7(supports-color@8.1.1)
|
builder-util: 25.1.7
|
||||||
fs-extra: 10.1.0
|
fs-extra: 10.1.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bluebird
|
- bluebird
|
||||||
- dmg-builder
|
- dmg-builder
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
electron-builder@25.1.8(bluebird@3.7.2)(electron-builder-squirrel-windows@25.1.8)(supports-color@8.1.1):
|
electron-builder@25.1.8(electron-builder-squirrel-windows@25.1.8):
|
||||||
dependencies:
|
dependencies:
|
||||||
app-builder-lib: 25.1.8(bluebird@3.7.2)(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8)(supports-color@8.1.1)
|
app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8)
|
||||||
builder-util: 25.1.7(supports-color@8.1.1)
|
builder-util: 25.1.7
|
||||||
builder-util-runtime: 9.2.10(supports-color@8.1.1)
|
builder-util-runtime: 9.2.10
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
dmg-builder: 25.1.8(bluebird@3.7.2)(electron-builder-squirrel-windows@25.1.8)(supports-color@8.1.1)
|
dmg-builder: 25.1.8(electron-builder-squirrel-windows@25.1.8)
|
||||||
fs-extra: 10.1.0
|
fs-extra: 10.1.0
|
||||||
is-ci: 3.0.1
|
is-ci: 3.0.1
|
||||||
lazy-val: 1.0.5
|
lazy-val: 1.0.5
|
||||||
@@ -5822,11 +5855,11 @@ snapshots:
|
|||||||
- electron-builder-squirrel-windows
|
- electron-builder-squirrel-windows
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
electron-publish@25.1.7(supports-color@8.1.1):
|
electron-publish@25.1.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/fs-extra': 9.0.13
|
'@types/fs-extra': 9.0.13
|
||||||
builder-util: 25.1.7(supports-color@8.1.1)
|
builder-util: 25.1.7
|
||||||
builder-util-runtime: 9.2.10(supports-color@8.1.1)
|
builder-util-runtime: 9.2.10
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
fs-extra: 10.1.0
|
fs-extra: 10.1.0
|
||||||
lazy-val: 1.0.5
|
lazy-val: 1.0.5
|
||||||
@@ -5836,6 +5869,19 @@ snapshots:
|
|||||||
|
|
||||||
electron-to-chromium@1.5.376: {}
|
electron-to-chromium@1.5.376: {}
|
||||||
|
|
||||||
|
electron-updater@6.8.9:
|
||||||
|
dependencies:
|
||||||
|
builder-util-runtime: 9.7.0
|
||||||
|
fs-extra: 10.1.0
|
||||||
|
js-yaml: 4.2.0
|
||||||
|
lazy-val: 1.0.5
|
||||||
|
lodash.escaperegexp: 4.1.2
|
||||||
|
lodash.isequal: 4.5.0
|
||||||
|
semver: 7.7.4
|
||||||
|
tiny-typed-emitter: 2.1.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
electron@33.4.11(supports-color@8.1.1):
|
electron@33.4.11(supports-color@8.1.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@electron/get': 2.0.3(supports-color@8.1.1)
|
'@electron/get': 2.0.3(supports-color@8.1.1)
|
||||||
@@ -6034,7 +6080,7 @@ snapshots:
|
|||||||
range-parser: 1.2.1
|
range-parser: 1.2.1
|
||||||
router: 2.2.0(supports-color@8.1.1)
|
router: 2.2.0(supports-color@8.1.1)
|
||||||
send: 1.2.1(supports-color@8.1.1)
|
send: 1.2.1(supports-color@8.1.1)
|
||||||
serve-static: 2.2.1(supports-color@8.1.1)
|
serve-static: 2.2.1
|
||||||
statuses: 2.0.2
|
statuses: 2.0.2
|
||||||
type-is: 2.1.0
|
type-is: 2.1.0
|
||||||
vary: 1.1.2
|
vary: 1.1.2
|
||||||
@@ -6236,7 +6282,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
|
|
||||||
glob@10.4.5:
|
glob@10.5.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
foreground-child: 3.3.1
|
foreground-child: 3.3.1
|
||||||
jackspeak: 3.4.3
|
jackspeak: 3.4.3
|
||||||
@@ -6245,6 +6291,15 @@ snapshots:
|
|||||||
package-json-from-dist: 1.0.1
|
package-json-from-dist: 1.0.1
|
||||||
path-scurry: 1.11.1
|
path-scurry: 1.11.1
|
||||||
|
|
||||||
|
glob@7.2.0:
|
||||||
|
dependencies:
|
||||||
|
fs.realpath: 1.0.0
|
||||||
|
inflight: 1.0.6
|
||||||
|
inherits: 2.0.4
|
||||||
|
minimatch: 3.1.5
|
||||||
|
once: 1.4.0
|
||||||
|
path-is-absolute: 1.0.1
|
||||||
|
|
||||||
glob@7.2.3:
|
glob@7.2.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
fs.realpath: 1.0.0
|
fs.realpath: 1.0.0
|
||||||
@@ -6333,15 +6388,15 @@ snapshots:
|
|||||||
statuses: 2.0.2
|
statuses: 2.0.2
|
||||||
toidentifier: 1.0.1
|
toidentifier: 1.0.1
|
||||||
|
|
||||||
http-proxy-agent@5.0.0(supports-color@8.1.1):
|
http-proxy-agent@5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tootallnate/once': 2.0.1
|
'@tootallnate/once': 2.0.1
|
||||||
agent-base: 6.0.2(supports-color@8.1.1)
|
agent-base: 6.0.2
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
http-proxy-agent@7.0.2(supports-color@8.1.1):
|
http-proxy-agent@7.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 7.1.4
|
agent-base: 7.1.4
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
@@ -6353,14 +6408,14 @@ snapshots:
|
|||||||
quick-lru: 5.1.1
|
quick-lru: 5.1.1
|
||||||
resolve-alpn: 1.2.1
|
resolve-alpn: 1.2.1
|
||||||
|
|
||||||
https-proxy-agent@5.0.1(supports-color@8.1.1):
|
https-proxy-agent@5.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 6.0.2(supports-color@8.1.1)
|
agent-base: 6.0.2
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
https-proxy-agent@7.0.6(supports-color@8.1.1):
|
https-proxy-agent@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 7.1.4
|
agent-base: 7.1.4
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
@@ -6629,8 +6684,12 @@ snapshots:
|
|||||||
|
|
||||||
lodash.difference@4.5.0: {}
|
lodash.difference@4.5.0: {}
|
||||||
|
|
||||||
|
lodash.escaperegexp@4.1.2: {}
|
||||||
|
|
||||||
lodash.flatten@4.4.0: {}
|
lodash.flatten@4.4.0: {}
|
||||||
|
|
||||||
|
lodash.isequal@4.5.0: {}
|
||||||
|
|
||||||
lodash.isplainobject@4.0.6: {}
|
lodash.isplainobject@4.0.6: {}
|
||||||
|
|
||||||
lodash.union@4.6.0: {}
|
lodash.union@4.6.0: {}
|
||||||
@@ -6673,13 +6732,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
make-fetch-happen@10.2.1(bluebird@3.7.2)(supports-color@8.1.1):
|
make-fetch-happen@10.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
agentkeepalive: 4.6.0
|
agentkeepalive: 4.6.0
|
||||||
cacache: 16.1.3(bluebird@3.7.2)
|
cacache: 16.1.3
|
||||||
http-cache-semantics: 4.2.0
|
http-cache-semantics: 4.2.0
|
||||||
http-proxy-agent: 5.0.0(supports-color@8.1.1)
|
http-proxy-agent: 5.0.0
|
||||||
https-proxy-agent: 5.0.1(supports-color@8.1.1)
|
https-proxy-agent: 5.0.1
|
||||||
is-lambda: 1.0.1
|
is-lambda: 1.0.1
|
||||||
lru-cache: 7.18.3
|
lru-cache: 7.18.3
|
||||||
minipass: 3.3.6
|
minipass: 3.3.6
|
||||||
@@ -6689,7 +6748,7 @@ snapshots:
|
|||||||
minipass-pipeline: 1.2.4
|
minipass-pipeline: 1.2.4
|
||||||
negotiator: 0.6.4
|
negotiator: 0.6.4
|
||||||
promise-retry: 2.0.1
|
promise-retry: 2.0.1
|
||||||
socks-proxy-agent: 7.0.0(supports-color@8.1.1)
|
socks-proxy-agent: 7.0.0
|
||||||
ssri: 9.0.1
|
ssri: 9.0.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bluebird
|
- bluebird
|
||||||
@@ -6844,13 +6903,13 @@ snapshots:
|
|||||||
fetch-blob: 3.2.0
|
fetch-blob: 3.2.0
|
||||||
formdata-polyfill: 4.0.10
|
formdata-polyfill: 4.0.10
|
||||||
|
|
||||||
node-gyp@9.4.1(bluebird@3.7.2)(supports-color@8.1.1):
|
node-gyp@9.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
env-paths: 2.2.1
|
env-paths: 2.2.1
|
||||||
exponential-backoff: 3.1.3
|
exponential-backoff: 3.1.3
|
||||||
glob: 7.2.3
|
glob: 7.2.0
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
make-fetch-happen: 10.2.1(bluebird@3.7.2)(supports-color@8.1.1)
|
make-fetch-happen: 10.2.1
|
||||||
nopt: 6.0.0
|
nopt: 6.0.0
|
||||||
npmlog: 6.0.2
|
npmlog: 6.0.2
|
||||||
rimraf: 3.0.2
|
rimraf: 3.0.2
|
||||||
@@ -7057,9 +7116,7 @@ snapshots:
|
|||||||
|
|
||||||
progress@2.0.3: {}
|
progress@2.0.3: {}
|
||||||
|
|
||||||
promise-inflight@1.0.1(bluebird@3.7.2):
|
promise-inflight@1.0.1: {}
|
||||||
optionalDependencies:
|
|
||||||
bluebird: 3.7.2
|
|
||||||
|
|
||||||
promise-retry@2.0.1:
|
promise-retry@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7171,7 +7228,7 @@ snapshots:
|
|||||||
|
|
||||||
react@19.2.7: {}
|
react@19.2.7: {}
|
||||||
|
|
||||||
read-binary-file-arch@1.0.6(supports-color@8.1.1):
|
read-binary-file-arch@1.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -7239,7 +7296,7 @@ snapshots:
|
|||||||
|
|
||||||
rimraf@3.0.2:
|
rimraf@3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
glob: 7.2.3
|
glob: 7.2.0
|
||||||
|
|
||||||
roarr@2.15.4:
|
roarr@2.15.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7321,6 +7378,8 @@ snapshots:
|
|||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
|
|
||||||
|
semver@7.7.4: {}
|
||||||
|
|
||||||
semver@7.8.4: {}
|
semver@7.8.4: {}
|
||||||
|
|
||||||
send@1.2.1(supports-color@8.1.1):
|
send@1.2.1(supports-color@8.1.1):
|
||||||
@@ -7344,7 +7403,7 @@ snapshots:
|
|||||||
type-fest: 0.13.1
|
type-fest: 0.13.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
serve-static@2.2.1(supports-color@8.1.1):
|
serve-static@2.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
encodeurl: 2.0.0
|
encodeurl: 2.0.0
|
||||||
escape-html: 1.0.3
|
escape-html: 1.0.3
|
||||||
@@ -7359,10 +7418,10 @@ snapshots:
|
|||||||
|
|
||||||
shadcn@4.11.0(supports-color@8.1.1)(typescript@5.8.3):
|
shadcn@4.11.0(supports-color@8.1.1)(typescript@5.8.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7(supports-color@8.1.1)
|
'@babel/core': 7.29.7
|
||||||
'@babel/parser': 7.29.7
|
'@babel/parser': 7.29.7
|
||||||
'@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)
|
'@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7)
|
||||||
'@babel/preset-typescript': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)
|
'@babel/preset-typescript': 7.29.7(@babel/core@7.29.7)
|
||||||
'@dotenvx/dotenvx': 1.74.2
|
'@dotenvx/dotenvx': 1.74.2
|
||||||
'@modelcontextprotocol/sdk': 1.29.0(supports-color@8.1.1)(zod@3.25.76)
|
'@modelcontextprotocol/sdk': 1.29.0(supports-color@8.1.1)(zod@3.25.76)
|
||||||
'@types/validate-npm-package-name': 4.0.2
|
'@types/validate-npm-package-name': 4.0.2
|
||||||
@@ -7376,7 +7435,7 @@ snapshots:
|
|||||||
fast-glob: 3.3.3
|
fast-glob: 3.3.3
|
||||||
fs-extra: 11.3.5
|
fs-extra: 11.3.5
|
||||||
fuzzysort: 3.1.0
|
fuzzysort: 3.1.0
|
||||||
https-proxy-agent: 7.0.6(supports-color@8.1.1)
|
https-proxy-agent: 7.0.6
|
||||||
kleur: 4.1.5
|
kleur: 4.1.5
|
||||||
node-fetch: 3.3.2
|
node-fetch: 3.3.2
|
||||||
open: 11.0.0
|
open: 11.0.0
|
||||||
@@ -7453,9 +7512,9 @@ snapshots:
|
|||||||
|
|
||||||
smart-buffer@4.2.0: {}
|
smart-buffer@4.2.0: {}
|
||||||
|
|
||||||
socks-proxy-agent@7.0.0(supports-color@8.1.1):
|
socks-proxy-agent@7.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 6.0.2(supports-color@8.1.1)
|
agent-base: 6.0.2
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
socks: 2.8.9
|
socks: 2.8.9
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -7466,10 +7525,12 @@ snapshots:
|
|||||||
ip-address: 10.2.0
|
ip-address: 10.2.0
|
||||||
smart-buffer: 4.2.0
|
smart-buffer: 4.2.0
|
||||||
|
|
||||||
sonner@2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
sonner@2.0.8(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.17
|
||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
@@ -7605,6 +7666,8 @@ snapshots:
|
|||||||
|
|
||||||
tiny-invariant@1.3.3: {}
|
tiny-invariant@1.3.3: {}
|
||||||
|
|
||||||
|
tiny-typed-emitter@2.1.0: {}
|
||||||
|
|
||||||
tinyglobby@0.2.17:
|
tinyglobby@0.2.17:
|
||||||
dependencies:
|
dependencies:
|
||||||
fdir: 6.5.0(picomatch@4.0.4)
|
fdir: 6.5.0(picomatch@4.0.4)
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
/**
|
||||||
|
* SignPath 远程代码签名模块(electron-builder 自定义签名)
|
||||||
|
*
|
||||||
|
* 挂载方式:electron-builder.yml -> win.sign: scripts/signpath-sign.js
|
||||||
|
*
|
||||||
|
* electron-builder 在构建 Windows 产物时会对每个待签名文件调用本模块,
|
||||||
|
* 本模块把文件提交到 SignPath 签名后,下载签名产物并原位覆盖。
|
||||||
|
*
|
||||||
|
* 实现与 SignPath 官方 PowerShell 模块(Submit-SigningRequest)对齐:
|
||||||
|
* POST {base}/v1/{orgId}/SigningRequests multipart/form-data,响应 Location 头 = 请求 URL
|
||||||
|
* GET {base}/v1/{orgId}/SigningRequests/{id} 轮询 status / isFinalStatus,完成后返回 signedArtifactLink
|
||||||
|
* GET signedArtifactLink 下载签名产物
|
||||||
|
* 认证:Authorization: Bearer <API token>
|
||||||
|
*
|
||||||
|
* 环境变量(CI 中由 GitHub Secrets 注入;本地构建未设置时自动跳过签名):
|
||||||
|
* SIGNPATH_API_TOKEN 必填(API Token)
|
||||||
|
* SIGNPATH_ORG_ID 必填(SignPath 组织 ID)
|
||||||
|
* SIGNPATH_PROJECT_SLUG 可选,默认 Koring_Launcher
|
||||||
|
* SIGNPATH_SIGNING_POLICY_SLUG 签名策略 slug(必填,如 Koring_Launcher_Dev_builder)
|
||||||
|
* SIGNPATH_ARTIFACT_CONFIG_SLUG 产物配置 slug;项目只有一个配置时可省略
|
||||||
|
* SIGNPATH_API_BASE 可选,默认 https://app.signpath.io/api
|
||||||
|
*
|
||||||
|
* 说明:
|
||||||
|
* - 无 SIGNPATH_API_TOKEN / SIGNPATH_ORG_ID 时直接返回(不签名),
|
||||||
|
* 因此本地 `pnpm dist:dev/beta/run` 行为与之前完全一致。
|
||||||
|
* - 签名发生在 electron-builder 生成 blockmap 与 latest.yml 之前,
|
||||||
|
* 所以清单中的 sha512 自动对应签名后的安装包。
|
||||||
|
* - 参考文档:https://docs.signpath.io/build-system-integration
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const API_BASE = process.env.SIGNPATH_API_BASE || 'https://app.signpath.io/api';
|
||||||
|
const PROJECT_SLUG = process.env.SIGNPATH_PROJECT_SLUG || 'Koring_Launcher';
|
||||||
|
const POLL_INTERVAL_MS = 5000;
|
||||||
|
const POLL_TIMEOUT_MS = 15 * 60 * 1000; // 单文件签名最长等待 15 分钟
|
||||||
|
|
||||||
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
async function failWithDetail(res, ctx) {
|
||||||
|
let detail = '';
|
||||||
|
try {
|
||||||
|
detail = await res.text();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
throw new Error(`[signpath-sign] ${ctx} failed (${res.status}): ${detail}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signWithSignPath(filePath) {
|
||||||
|
const token = process.env.SIGNPATH_API_TOKEN;
|
||||||
|
const orgId = process.env.SIGNPATH_ORG_ID;
|
||||||
|
const policySlug = process.env.SIGNPATH_SIGNING_POLICY_SLUG;
|
||||||
|
const artifactSlug = process.env.SIGNPATH_ARTIFACT_CONFIG_SLUG;
|
||||||
|
|
||||||
|
if (!policySlug) {
|
||||||
|
throw new Error('[signpath-sign] SIGNPATH_SIGNING_POLICY_SLUG is required when signing is enabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = { Authorization: `Bearer ${token}` };
|
||||||
|
|
||||||
|
// 1. 提交签名请求(multipart/form-data,与官方 PowerShell 模块一致)
|
||||||
|
const fileBuf = fs.readFileSync(filePath);
|
||||||
|
const form = new FormData();
|
||||||
|
form.append('ProjectSlug', PROJECT_SLUG);
|
||||||
|
form.append('SigningPolicySlug', policySlug);
|
||||||
|
if (artifactSlug) {
|
||||||
|
form.append('ArtifactConfigurationSlug', artifactSlug);
|
||||||
|
}
|
||||||
|
form.append('Description', `electron-builder auto-sign: ${path.basename(filePath)}`);
|
||||||
|
form.append('Artifact', new Blob([fileBuf]), path.basename(filePath));
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[signpath-sign] submitting signing request: ${path.basename(filePath)} (${(fileBuf.length / 1024 / 1024).toFixed(1)} MB)`
|
||||||
|
);
|
||||||
|
const submitRes = await fetch(`${API_BASE}/v1/${orgId}/SigningRequests`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: form,
|
||||||
|
});
|
||||||
|
if (!submitRes.ok) {
|
||||||
|
await failWithDetail(submitRes, 'submit');
|
||||||
|
}
|
||||||
|
const requestUrl = submitRes.headers.get('location');
|
||||||
|
if (!requestUrl) {
|
||||||
|
throw new Error('[signpath-sign] submit response has no Location header');
|
||||||
|
}
|
||||||
|
console.log(`[signpath-sign] request created: ${requestUrl}`);
|
||||||
|
|
||||||
|
// 2. 轮询直到最终状态(isFinalStatus),要求 Completed
|
||||||
|
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
||||||
|
let data = null;
|
||||||
|
for (;;) {
|
||||||
|
await sleep(POLL_INTERVAL_MS);
|
||||||
|
const pollRes = await fetch(requestUrl, { headers });
|
||||||
|
if (!pollRes.ok) {
|
||||||
|
await failWithDetail(pollRes, 'status query');
|
||||||
|
}
|
||||||
|
data = await pollRes.json();
|
||||||
|
console.log(`[signpath-sign] status: ${data.status}${data.workflowStatus ? ` / ${data.workflowStatus}` : ''}`);
|
||||||
|
if (data.isFinalStatus) break;
|
||||||
|
if (Date.now() > deadline) {
|
||||||
|
throw new Error('[signpath-sign] signing timed out');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data.status !== 'Completed') {
|
||||||
|
throw new Error(`[signpath-sign] request ${data.status}${data.workflowStatus ? ` / ${data.workflowStatus}` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 通过 signedArtifactLink 下载签名产物并原位覆盖(网络错误重试)
|
||||||
|
const downloadUrl = data.signedArtifactLink;
|
||||||
|
if (!downloadUrl) {
|
||||||
|
throw new Error('[signpath-sign] completed request has no signedArtifactLink');
|
||||||
|
}
|
||||||
|
const signedBuf = await downloadWithRetry(downloadUrl, headers);
|
||||||
|
const tmpPath = `${filePath}.signing.tmp`;
|
||||||
|
fs.writeFileSync(tmpPath, signedBuf);
|
||||||
|
fs.renameSync(tmpPath, filePath);
|
||||||
|
console.log(`[signpath-sign] signed OK (${(signedBuf.length / 1024 / 1024).toFixed(1)} MB)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const DOWNLOAD_MAX_ATTEMPTS = 3;
|
||||||
|
|
||||||
|
async function downloadWithRetry(downloadUrl, headers) {
|
||||||
|
let lastError = null;
|
||||||
|
for (let attempt = 1; attempt <= DOWNLOAD_MAX_ATTEMPTS; attempt++) {
|
||||||
|
try {
|
||||||
|
const dlRes = await fetch(downloadUrl, { headers });
|
||||||
|
if (!dlRes.ok) {
|
||||||
|
await failWithDetail(dlRes, 'download signed artifact');
|
||||||
|
}
|
||||||
|
return Buffer.from(await dlRes.arrayBuffer());
|
||||||
|
} catch (err) {
|
||||||
|
lastError = err;
|
||||||
|
console.warn(`[signpath-sign] download attempt ${attempt}/${DOWNLOAD_MAX_ATTEMPTS} failed: ${err.message}`);
|
||||||
|
if (attempt < DOWNLOAD_MAX_ATTEMPTS) {
|
||||||
|
await sleep(3000 * attempt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* electron-builder 自定义签名入口。
|
||||||
|
* configuration.path 为待签名文件绝对路径;无 token 时跳过(本地开发构建)。
|
||||||
|
*/
|
||||||
|
module.exports = async function signPathSign(configuration) {
|
||||||
|
if (!process.env.SIGNPATH_API_TOKEN || !process.env.SIGNPATH_ORG_ID) {
|
||||||
|
console.log(`[signpath-sign] no SignPath credentials, skip signing: ${configuration.name}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await signWithSignPath(configuration.path);
|
||||||
|
};
|
||||||
+17
-2
@@ -70,14 +70,17 @@ function App() {
|
|||||||
// Listen for preloaded config from main process
|
// Listen for preloaded config from main process
|
||||||
const unsub = window.electronAPI?.onConfigPreload((data) => {
|
const unsub = window.electronAPI?.onConfigPreload((data) => {
|
||||||
const { config, isFirstLaunch } = data;
|
const { config, isFirstLaunch } = data;
|
||||||
useConfigStore.getState().applyPreloaded(config as AppConfig, isFirstLaunch);
|
const cfg = config as AppConfig;
|
||||||
|
useConfigStore.getState().applyPreloaded(cfg, isFirstLaunch);
|
||||||
|
// 语言偏好 → <html lang>
|
||||||
|
document.documentElement.lang = (cfg as AppConfig).app?.language ?? "zh-CN";
|
||||||
syncThemeFromConfig();
|
syncThemeFromConfig();
|
||||||
syncA11yFromConfig();
|
syncA11yFromConfig();
|
||||||
syncBackgroundFromConfig();
|
syncBackgroundFromConfig();
|
||||||
useAuthStore.getState().initFromRegistry();
|
useAuthStore.getState().initFromRegistry();
|
||||||
useKoringAuthStore.getState().initFromDisk();
|
useKoringAuthStore.getState().initFromDisk();
|
||||||
// Navigate to OOBE on first launch or if oobe not completed
|
// Navigate to OOBE on first launch or if oobe not completed
|
||||||
if (isFirstLaunch || config.oobe) {
|
if (isFirstLaunch || cfg.oobe) {
|
||||||
useRouteStore.getState().navigate("oobe");
|
useRouteStore.getState().navigate("oobe");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -85,6 +88,18 @@ function App() {
|
|||||||
return () => { unsub?.(); };
|
return () => { unsub?.(); };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 主进程权威配置广播 → 覆盖本地镜像并同步派生 store
|
||||||
|
useEffect(() => {
|
||||||
|
const unsub = window.electronAPI?.onConfigChanged((config) => {
|
||||||
|
useConfigStore.getState().applyChanged(config as AppConfig);
|
||||||
|
syncThemeFromConfig();
|
||||||
|
syncA11yFromConfig();
|
||||||
|
syncBackgroundFromConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => { unsub?.(); };
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RootLayout>
|
<RootLayout>
|
||||||
<Page key={current} />
|
<Page key={current} />
|
||||||
|
|||||||
+36
-7
@@ -1,4 +1,4 @@
|
|||||||
import { ipcInvoke } from './ipc';
|
import { ipcInvoke, onIpcEvent } from './ipc';
|
||||||
|
|
||||||
export interface ThemeConfig {
|
export interface ThemeConfig {
|
||||||
darkMode: string;
|
darkMode: string;
|
||||||
@@ -36,6 +36,11 @@ export interface JavaConfig {
|
|||||||
jvmArgs: string;
|
jvmArgs: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ServerConfig {
|
||||||
|
ip: string;
|
||||||
|
port: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdvancedConfig {
|
export interface AdvancedConfig {
|
||||||
afterLaunch: string;
|
afterLaunch: string;
|
||||||
winMode: string;
|
winMode: string;
|
||||||
@@ -44,6 +49,20 @@ export interface AdvancedConfig {
|
|||||||
gameArgs: string;
|
gameArgs: string;
|
||||||
preLaunchCmd: string;
|
preLaunchCmd: string;
|
||||||
debugMode: boolean;
|
debugMode: boolean;
|
||||||
|
/** 快速进入服务器(启动后自动加入;ip 为空则不自动加入) */
|
||||||
|
server: ServerConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppInfoConfig {
|
||||||
|
/** 界面语言偏好(zh-CN | en-US);语言包开发中,暂仅保存并设置 <html lang> */
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UiConfig {
|
||||||
|
/** 首页实例标题显示 */
|
||||||
|
showInstanceTitle: boolean;
|
||||||
|
/** 标题栏任务队列按钮显示 */
|
||||||
|
showTaskButton: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DownloadConfig {
|
export interface DownloadConfig {
|
||||||
@@ -77,6 +96,7 @@ export interface InstanceMeta {
|
|||||||
export interface AppConfig {
|
export interface AppConfig {
|
||||||
version: number;
|
version: number;
|
||||||
oobe: boolean;
|
oobe: boolean;
|
||||||
|
app: AppInfoConfig;
|
||||||
theme: ThemeConfig;
|
theme: ThemeConfig;
|
||||||
a11y: A11yConfig;
|
a11y: A11yConfig;
|
||||||
background: BackgroundConfig;
|
background: BackgroundConfig;
|
||||||
@@ -85,15 +105,10 @@ export interface AppConfig {
|
|||||||
advanced: AdvancedConfig;
|
advanced: AdvancedConfig;
|
||||||
download: DownloadConfig;
|
download: DownloadConfig;
|
||||||
network: NetworkConfig;
|
network: NetworkConfig;
|
||||||
|
ui: UiConfig;
|
||||||
instances: InstanceMeta[];
|
instances: InstanceMeta[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CommandResult {
|
|
||||||
success: boolean;
|
|
||||||
data: unknown;
|
|
||||||
error: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getConfig(): Promise<AppConfig> {
|
export async function getConfig(): Promise<AppConfig> {
|
||||||
const result = await ipcInvoke<AppConfig>('config:get');
|
const result = await ipcInvoke<AppConfig>('config:get');
|
||||||
return result;
|
return result;
|
||||||
@@ -102,3 +117,17 @@ export async function getConfig(): Promise<AppConfig> {
|
|||||||
export async function saveConfig(config: AppConfig): Promise<void> {
|
export async function saveConfig(config: AppConfig): Promise<void> {
|
||||||
await ipcInvoke('config:save', config);
|
await ipcInvoke('config:save', config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主进程权威更新配置:提交 { section, patch } 补丁,
|
||||||
|
* 主进程深度合并、debounce 稀疏写盘并广播 config:changed。
|
||||||
|
* 返回合并后的完整配置。
|
||||||
|
*/
|
||||||
|
export async function updateConfig(section: string, patch: unknown): Promise<AppConfig> {
|
||||||
|
return ipcInvoke<AppConfig>('config:update', { section, patch });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 监听主进程广播的配置变更(完整配置) */
|
||||||
|
export function onConfigChanged(callback: (config: AppConfig) => void): () => void {
|
||||||
|
return onIpcEvent<AppConfig>('config:changed', callback);
|
||||||
|
}
|
||||||
|
|||||||
+2
-26
@@ -97,20 +97,6 @@ export async function installInstance(
|
|||||||
return ipcInvoke<{ requestId: string }>('instance:install', { name, gamePath });
|
return ipcInvoke<{ requestId: string }>('instance:install', { name, gamePath });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function launchInstance(
|
|
||||||
name: string,
|
|
||||||
gamePath: string,
|
|
||||||
options: {
|
|
||||||
username: string;
|
|
||||||
uuid: string;
|
|
||||||
accessToken?: string;
|
|
||||||
javaPath?: string;
|
|
||||||
server?: { host: string; port?: number };
|
|
||||||
}
|
|
||||||
): Promise<{ requestId: string }> {
|
|
||||||
return ipcInvoke<{ requestId: string }>('instance:launch', { name, gamePath, ...options });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function diagnoseInstance(
|
export async function diagnoseInstance(
|
||||||
name: string,
|
name: string,
|
||||||
gamePath: string
|
gamePath: string
|
||||||
@@ -162,6 +148,8 @@ export async function importExistingInstance(
|
|||||||
java?: string;
|
java?: string;
|
||||||
minMemory?: number;
|
minMemory?: number;
|
||||||
maxMemory?: number;
|
maxMemory?: number;
|
||||||
|
/** 版本文件来源目录(默认 = gamePath) */
|
||||||
|
sourceGamePath?: string;
|
||||||
}
|
}
|
||||||
): Promise<InstanceInfo> {
|
): Promise<InstanceInfo> {
|
||||||
return ipcInvoke<InstanceInfo>('instance:import', {
|
return ipcInvoke<InstanceInfo>('instance:import', {
|
||||||
@@ -201,15 +189,3 @@ export function onInstallComplete(callback: (data: { requestId: string; data: In
|
|||||||
export function onInstallError(callback: (data: { requestId: string; error: string }) => void) {
|
export function onInstallError(callback: (data: { requestId: string; error: string }) => void) {
|
||||||
return onIpcEvent('instance:install-error', callback);
|
return onIpcEvent('instance:install-error', callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onLaunchEvent(callback: (data: { requestId: string; event: string; [key: string]: unknown }) => void) {
|
|
||||||
return onIpcEvent('instance:launch-event', callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function onLaunchComplete(callback: (data: { requestId: string; data: { pid: number; version: string; username: string } }) => void) {
|
|
||||||
return onIpcEvent('instance:launch-complete', callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function onLaunchError(callback: (data: { requestId: string; error: string }) => void) {
|
|
||||||
return onIpcEvent('instance:launch-error', callback);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { ipcInvoke } from './ipc';
|
||||||
|
|
||||||
|
export interface JavaInfo {
|
||||||
|
path: string;
|
||||||
|
version: string;
|
||||||
|
majorVersion: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 扫描系统已安装的 Java(JAVA_HOME / PATH / 常见安装目录) */
|
||||||
|
export async function scanJava(): Promise<JavaInfo[]> {
|
||||||
|
const data = await ipcInvoke<{ javaList: JavaInfo[] }>('java:scan');
|
||||||
|
return data?.javaList ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校验指定路径是否为可用的 Java 可执行文件 */
|
||||||
|
export async function resolveJava(path: string): Promise<JavaInfo | null> {
|
||||||
|
const data = await ipcInvoke<{ java: JavaInfo | null }>('java:resolve', { path });
|
||||||
|
return data?.java ?? null;
|
||||||
|
}
|
||||||
+24
-11
@@ -1,17 +1,25 @@
|
|||||||
import { ipcInvoke, onIpcEvent } from './ipc';
|
import { ipcInvoke, onIpcEvent } from './ipc';
|
||||||
|
|
||||||
export interface LaunchOptions {
|
/** 启动所需的账户档案(来自 authStore) */
|
||||||
gamePath: string;
|
export interface LaunchProfile {
|
||||||
javaPath: string;
|
|
||||||
version: string;
|
|
||||||
username: string;
|
username: string;
|
||||||
uuid: string;
|
uuid: string;
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
memory?: { min?: string; max?: string };
|
}
|
||||||
jvmArgs?: string[];
|
|
||||||
gameArgs?: string[];
|
/** 快速联机目标服务器 */
|
||||||
server?: { ip: string; port?: number };
|
export interface LaunchServer {
|
||||||
detached?: boolean;
|
ip: string;
|
||||||
|
port?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 统一启动接口契约:指定实例 + 游戏根目录 + 账户档案(可选快速联机) */
|
||||||
|
export interface LaunchGamePayload {
|
||||||
|
instanceName: string;
|
||||||
|
/** 实例父目录(游戏根目录) */
|
||||||
|
gamePath: string;
|
||||||
|
profile: LaunchProfile;
|
||||||
|
server?: LaunchServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LaunchResult {
|
export interface LaunchResult {
|
||||||
@@ -21,8 +29,12 @@ export interface LaunchResult {
|
|||||||
requestId: string;
|
requestId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function launchGame(options: LaunchOptions): Promise<LaunchResult> {
|
/**
|
||||||
return ipcInvoke<LaunchResult>('launch:launch', options);
|
* 启动游戏。主进程会读取权威配置(Koring.yml 内存缓存)自动应用
|
||||||
|
* Java 路径 / 内存 / GC / JVM 参数 / 游戏参数 / 窗口模式 / 启动前命令等设置。
|
||||||
|
*/
|
||||||
|
export async function launchGame(payload: LaunchGamePayload): Promise<LaunchResult> {
|
||||||
|
return ipcInvoke<LaunchResult>('launch:launch', payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function diagnoseVersion(
|
export async function diagnoseVersion(
|
||||||
@@ -32,6 +44,7 @@ export async function diagnoseVersion(
|
|||||||
return ipcInvoke('launch:diagnose', { gamePath, version });
|
return ipcInvoke('launch:diagnose', { gamePath, version });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 订阅某个启动请求的游戏事件流(stdout / stderr / window-ready / exit) */
|
||||||
export function onGameEvent(
|
export function onGameEvent(
|
||||||
requestId: string,
|
requestId: string,
|
||||||
callback: (event: { event: string; [key: string]: unknown }) => void
|
callback: (event: { event: string; [key: string]: unknown }) => void
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export class ErrorBoundary extends Component<Props, State> {
|
|||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="flat"
|
variant="outline"
|
||||||
onPress={() => this.setState({ hasError: false, errorMsg: "" })}
|
onPress={() => this.setState({ hasError: false, errorMsg: "" })}
|
||||||
>
|
>
|
||||||
重试
|
重试
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
|
import { Typography } from "@heroui/react";
|
||||||
|
|
||||||
export function PageHeader({ title, desc }: { title: string; desc: string }) {
|
export function PageHeader({ title, desc }: { title: string; desc: string }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h2 className="text-xl font-bold text-foreground mb-1">{title}</h2>
|
<Typography.Heading level={2} className="text-xl font-bold text-foreground mb-1">
|
||||||
<p className="text-sm text-muted-foreground mb-6">{desc}</p>
|
{title}
|
||||||
|
</Typography.Heading>
|
||||||
|
<Typography.Paragraph size="sm" className="text-sm text-muted-foreground mb-6">
|
||||||
|
{desc}
|
||||||
|
</Typography.Paragraph>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SectionTitle({ children }: { children: React.ReactNode }) {
|
export function SectionTitle({ children }: { children: React.ReactNode }) {
|
||||||
return <h3 className="text-lg font-bold text-foreground mb-3">{children}</h3>;
|
return (
|
||||||
|
<Typography.Heading level={3} className="text-lg font-bold text-foreground mb-3">
|
||||||
|
{children}
|
||||||
|
</Typography.Heading>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// 设置页统一徽章:版本类型 / 加载器 / 状态标签共用一个组件与样式组合。
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export type SettingBadgeVariant = "neutral" | "primary" | "success" | "warning" | "info" | "error" | "violet";
|
||||||
|
|
||||||
|
const badgeStyles: Record<SettingBadgeVariant, string> = {
|
||||||
|
neutral: "bg-foreground/[0.05] dark:bg-white/[0.05] text-muted-foreground border-border/30 dark:border-white/[0.05]",
|
||||||
|
primary: "bg-primary/10 text-primary border-primary/20",
|
||||||
|
success: "bg-green-500/10 text-green-600 dark:bg-green-500/15 dark:text-green-400 border-green-500/20",
|
||||||
|
warning: "bg-amber-500/10 text-amber-600 dark:bg-amber-500/15 dark:text-amber-400 border-amber-500/20",
|
||||||
|
info: "bg-sky-500/10 text-sky-600 dark:bg-sky-500/15 dark:text-sky-400 border-sky-500/20",
|
||||||
|
error: "bg-red-500/10 text-red-600 dark:bg-red-500/15 dark:text-red-400 border-red-500/20",
|
||||||
|
violet: "bg-violet-500/10 text-violet-600 dark:bg-violet-500/15 dark:text-violet-400 border-violet-500/20",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SettingBadge({
|
||||||
|
variant = "neutral",
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
variant?: SettingBadgeVariant;
|
||||||
|
className?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center gap-1 text-[11px] px-2 py-0.5 rounded-md border font-medium whitespace-nowrap",
|
||||||
|
badgeStyles[variant],
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
import { Card } from "@heroui/react";
|
import { SettingSurface } from "./SettingSurface";
|
||||||
|
|
||||||
export function SettingCard({ children, className }: { children: React.ReactNode; className?: string }) {
|
export function SettingCard({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||||
return (
|
return <SettingSurface className={`px-5 py-4 ${className ?? ""}`}>{children}</SettingSurface>;
|
||||||
<Card variant="transparent" className={`glass-card px-5 py-4 ${className ?? ""}`}>
|
|
||||||
{children}
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// 设置页列表项容器:版本行 / 扫描结果行等可操作列表项的统一外观。
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export function SettingListItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
selected = false,
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
/** 选中态(高亮边框) */
|
||||||
|
selected?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-3 px-3 py-2 rounded-xl border",
|
||||||
|
"bg-foreground/[0.03] dark:bg-white/[0.03]",
|
||||||
|
selected
|
||||||
|
? "border-primary/30 bg-primary/[0.04]"
|
||||||
|
: "border-black/[0.06] dark:border-white/[0.07]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
|
import { Typography } from "@heroui/react";
|
||||||
|
|
||||||
export function SettingRow({ label, desc, children }: { label: string; desc?: string; children: React.ReactNode }) {
|
export function SettingRow({ label, desc, children }: { label: string; desc?: string; children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-medium text-foreground">{label}</p>
|
<Typography.Paragraph className="text-sm font-medium text-foreground">{label}</Typography.Paragraph>
|
||||||
{desc && <p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>}
|
{desc && (
|
||||||
|
<Typography.Paragraph size="xs" className="text-[13px] text-muted-foreground mt-0.5">
|
||||||
|
{desc}
|
||||||
|
</Typography.Paragraph>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0">{children}</div>
|
<div className="shrink-0">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// 设置卡片唯一原语:所有设置子页面卡片统一走这里。
|
||||||
|
// 基于 HeroUI Surface,用显式类保证确定性的磨砂玻璃外观(与既有设计一致)。
|
||||||
|
import { Surface } from "@heroui/react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export interface SettingSurfaceProps {
|
||||||
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
/** 是否启用磨砂玻璃(尊重无障碍 reduce-transparency 的全局降级) */
|
||||||
|
frost?: boolean;
|
||||||
|
/** 阴影级别:raised 默认、flat 无阴影、bordered 仅边框 */
|
||||||
|
variant?: "raised" | "flat" | "bordered";
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantCls = {
|
||||||
|
raised: "shadow-sm",
|
||||||
|
flat: "shadow-none",
|
||||||
|
bordered: "shadow-none",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function SettingSurface({ className, frost = true, variant = "raised", children }: SettingSurfaceProps) {
|
||||||
|
return (
|
||||||
|
<Surface
|
||||||
|
variant="transparent"
|
||||||
|
className={cn(
|
||||||
|
"rounded-xl",
|
||||||
|
"bg-white/85 dark:bg-black/45",
|
||||||
|
"border border-black/[0.06] dark:border-white/[0.07]",
|
||||||
|
variantCls[variant],
|
||||||
|
frost && "backdrop-blur-[12px]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Surface>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,16 +1,10 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
import { SettingSurface } from "./SettingSurface";
|
||||||
|
|
||||||
interface SurfaceProps extends React.HTMLAttributes<HTMLDivElement> {
|
interface SurfaceProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
variant?: "raised" | "flat" | "bordered"
|
variant?: "raised" | "flat" | "bordered";
|
||||||
frost?: "none" | "sm" | "md" | "lg"
|
frost?: "none" | "sm" | "md" | "lg";
|
||||||
padding?: "none" | "sm" | "md" | "lg"
|
padding?: "none" | "sm" | "md" | "lg";
|
||||||
}
|
|
||||||
|
|
||||||
const frostMap = {
|
|
||||||
none: "",
|
|
||||||
sm: "backdrop-blur-[6px]",
|
|
||||||
md: "backdrop-blur-[12px]",
|
|
||||||
lg: "backdrop-blur-[20px]",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const paddingMap = {
|
const paddingMap = {
|
||||||
@@ -18,8 +12,9 @@ const paddingMap = {
|
|||||||
sm: "px-3 py-2.5",
|
sm: "px-3 py-2.5",
|
||||||
md: "px-5 py-4",
|
md: "px-5 py-4",
|
||||||
lg: "px-6 py-5",
|
lg: "px-6 py-5",
|
||||||
}
|
};
|
||||||
|
|
||||||
|
// Surface 是 SettingSurface 的兼容别名(保留原 API,样式与设置卡片统一)
|
||||||
function Surface({
|
function Surface({
|
||||||
variant = "raised",
|
variant = "raised",
|
||||||
frost = "none",
|
frost = "none",
|
||||||
@@ -29,24 +24,19 @@ function Surface({
|
|||||||
...props
|
...props
|
||||||
}: SurfaceProps) {
|
}: SurfaceProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<SettingSurface
|
||||||
data-slot="surface"
|
variant={variant}
|
||||||
|
frost={frost !== "none"}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-xl transition-colors duration-200",
|
|
||||||
"bg-white/85 dark:bg-black/45",
|
|
||||||
"border border-black/[0.06] dark:border-white/[0.07]",
|
|
||||||
frostMap[frost],
|
|
||||||
paddingMap[padding],
|
paddingMap[padding],
|
||||||
variant === "raised" && "shadow-sm",
|
variant === "flat" && "bg-transparent border-0 backdrop-blur-none",
|
||||||
variant === "bordered" && "shadow-none",
|
|
||||||
variant === "flat" && "shadow-none bg-transparent border-0",
|
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</SettingSurface>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SurfaceHeader({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
function SurfaceHeader({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
@@ -61,7 +51,7 @@ function SurfaceHeader({ className, children, ...props }: React.HTMLAttributes<H
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SurfaceContent({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
function SurfaceContent({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
@@ -69,7 +59,7 @@ function SurfaceContent({ className, children, ...props }: React.HTMLAttributes<
|
|||||||
<div data-slot="surface-content" className={cn("space-y-3", className)} {...props}>
|
<div data-slot="surface-content" className={cn("space-y-3", className)} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Surface, SurfaceHeader, SurfaceContent }
|
export { Surface, SurfaceHeader, SurfaceContent }
|
||||||
|
|||||||
@@ -0,0 +1,318 @@
|
|||||||
|
// 可复用设置控件:HeroUI 3 复合组件封装,统一风格,绑定 configStore setter 自动保存。
|
||||||
|
// 每个控件都遵循 label / desc / value / onChange 通用接口。
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
ListBox,
|
||||||
|
ListBoxItem,
|
||||||
|
NumberField,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
Select,
|
||||||
|
Switch,
|
||||||
|
TextArea,
|
||||||
|
} from "@heroui/react";
|
||||||
|
import { Check, ChevronDown, FolderOpen, FolderSearch, Loader2 } from "lucide-react";
|
||||||
|
import { ipcInvoke } from "@/api/ipc";
|
||||||
|
import { SettingRow } from "./SettingRow";
|
||||||
|
|
||||||
|
export interface SettingOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
desc?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeroUI 默认主题 field 边框宽度为 0(--field-border-width: 0px),
|
||||||
|
// 统一补上与 Select.Trigger 一致的显式边框/背景,保证控件视觉完整。
|
||||||
|
export const fieldCls =
|
||||||
|
"rounded-lg border border-border/40 dark:border-white/[0.08] bg-white/60 dark:bg-black/30 text-[13px] text-foreground placeholder:text-muted-foreground/50 focus:border-primary/40 transition-colors";
|
||||||
|
|
||||||
|
// ---------- 下拉选择 ----------
|
||||||
|
export function SettingSelect({
|
||||||
|
label,
|
||||||
|
desc,
|
||||||
|
value,
|
||||||
|
options,
|
||||||
|
onChange,
|
||||||
|
placeholder = "请选择",
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
desc?: string;
|
||||||
|
value: string;
|
||||||
|
options: SettingOption[];
|
||||||
|
onChange: (v: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const selectedKey = options.some((o) => o.value === value) ? value : "__none__";
|
||||||
|
return (
|
||||||
|
<SettingRow label={label} desc={desc}>
|
||||||
|
<Select.Root
|
||||||
|
selectedKey={selectedKey}
|
||||||
|
onSelectionChange={(keys) => {
|
||||||
|
// RAC 单选时可能传 Key | null,也可能传 Set<Key>;两种形状都兼容
|
||||||
|
let v: string | undefined;
|
||||||
|
if (keys === null || keys === undefined) {
|
||||||
|
v = undefined;
|
||||||
|
} else if (typeof keys === "string" || typeof keys === "number") {
|
||||||
|
v = String(keys);
|
||||||
|
} else if ((keys as unknown) instanceof Set) {
|
||||||
|
const arr = Array.from(keys);
|
||||||
|
v = arr.length > 0 ? String(arr[0]) : undefined;
|
||||||
|
}
|
||||||
|
if (v && v !== "__none__") onChange(v);
|
||||||
|
}}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<Select.Trigger className="h-8 rounded-lg border border-border/40 dark:border-white/[0.08] bg-white/60 dark:bg-black/30 px-3 hover:border-primary/30 transition-colors">
|
||||||
|
<Select.Value className="text-[13px] text-foreground">
|
||||||
|
{options.find((o) => o.value === value)?.label ?? placeholder}
|
||||||
|
</Select.Value>
|
||||||
|
<Select.Indicator>
|
||||||
|
<ChevronDown className="w-3.5 h-3.5 text-muted-foreground/60" />
|
||||||
|
</Select.Indicator>
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Popover className="z-50 rounded-xl border border-border/50 dark:border-white/[0.08] bg-background shadow-xl p-1.5 min-w-[10rem]">
|
||||||
|
<ListBox className="max-h-72 overflow-y-auto scroll-area outline-none">
|
||||||
|
{options.map((opt) => (
|
||||||
|
<ListBoxItem
|
||||||
|
key={opt.value}
|
||||||
|
id={opt.value}
|
||||||
|
className="text-[13px] py-1.5 px-2.5 rounded-lg data-[selected=true]:bg-primary/10 data-[selected=true]:text-primary outline-none cursor-pointer"
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
</ListBoxItem>
|
||||||
|
))}
|
||||||
|
</ListBox>
|
||||||
|
</Select.Popover>
|
||||||
|
</Select.Root>
|
||||||
|
</SettingRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 数字输入 ----------
|
||||||
|
export function SettingNumberField({
|
||||||
|
label,
|
||||||
|
desc,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
step = 1,
|
||||||
|
suffix,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
desc?: string;
|
||||||
|
value: number;
|
||||||
|
onChange: (v: number) => void;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
step?: number;
|
||||||
|
suffix?: string;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SettingRow label={label} desc={desc}>
|
||||||
|
<div className={`flex items-center gap-1.5 ${className ?? ""}`}>
|
||||||
|
<NumberField.Root
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
minValue={min}
|
||||||
|
maxValue={max}
|
||||||
|
step={step}
|
||||||
|
className="w-28"
|
||||||
|
>
|
||||||
|
<NumberField.Group className="flex items-center rounded-lg border border-border/40 dark:border-white/[0.08] bg-white/60 dark:bg-black/30 overflow-hidden focus-within:border-primary/40 transition-colors">
|
||||||
|
<NumberField.DecrementButton
|
||||||
|
aria-label="减少"
|
||||||
|
className="flex items-center justify-center w-7 h-8 shrink-0 text-muted-foreground hover:text-foreground hover:bg-foreground/[0.05] cursor-pointer select-none"
|
||||||
|
>
|
||||||
|
−
|
||||||
|
</NumberField.DecrementButton>
|
||||||
|
<NumberField.Input className="w-14 h-8 bg-transparent text-center text-[13px] text-foreground outline-none" />
|
||||||
|
<NumberField.IncrementButton
|
||||||
|
aria-label="增加"
|
||||||
|
className="flex items-center justify-center w-7 h-8 shrink-0 text-muted-foreground hover:text-foreground hover:bg-foreground/[0.05] cursor-pointer select-none"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</NumberField.IncrementButton>
|
||||||
|
</NumberField.Group>
|
||||||
|
</NumberField.Root>
|
||||||
|
{suffix && <span className="text-[13px] text-muted-foreground shrink-0">{suffix}</span>}
|
||||||
|
</div>
|
||||||
|
</SettingRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 开关 ----------
|
||||||
|
export function SettingSwitch({
|
||||||
|
label,
|
||||||
|
desc,
|
||||||
|
checked,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
desc?: string;
|
||||||
|
checked: boolean;
|
||||||
|
onChange: (v: boolean) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SettingRow label={label} desc={desc}>
|
||||||
|
{/* 注:HeroUI 3 基于 react-aria,Switch 使用 onChange 而非 onValueChange */}
|
||||||
|
<Switch isSelected={checked} onChange={onChange}>
|
||||||
|
<Switch.Control>
|
||||||
|
<Switch.Thumb />
|
||||||
|
</Switch.Control>
|
||||||
|
</Switch>
|
||||||
|
</SettingRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 单选组 ----------
|
||||||
|
export function SettingRadioGroup({
|
||||||
|
label,
|
||||||
|
desc,
|
||||||
|
value,
|
||||||
|
options,
|
||||||
|
onChange,
|
||||||
|
horizontal = false,
|
||||||
|
}: {
|
||||||
|
label?: string;
|
||||||
|
desc?: string;
|
||||||
|
value: string;
|
||||||
|
options: SettingOption[];
|
||||||
|
onChange: (v: string) => void;
|
||||||
|
horizontal?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{label && <p className="text-sm font-medium text-foreground">{label}</p>}
|
||||||
|
{desc && <p className="text-[13px] text-muted-foreground mt-0.5 mb-2">{desc}</p>}
|
||||||
|
<RadioGroup
|
||||||
|
value={value}
|
||||||
|
onChange={(v) => onChange(String(v))}
|
||||||
|
className={horizontal ? "flex items-center gap-4" : "space-y-2"}
|
||||||
|
>
|
||||||
|
{options.map((opt) => (
|
||||||
|
<Radio key={opt.value} value={opt.value}>
|
||||||
|
<Radio.Control className="border border-border/40 dark:border-white/[0.08] bg-white/60 dark:bg-black/30">
|
||||||
|
<Radio.Indicator />
|
||||||
|
</Radio.Control>
|
||||||
|
<Radio.Content>{opt.label}</Radio.Content>
|
||||||
|
</Radio>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 多行文本 ----------
|
||||||
|
export function SettingTextArea({
|
||||||
|
label,
|
||||||
|
desc,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
rows = 3,
|
||||||
|
placeholder,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
desc?: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (v: string) => void;
|
||||||
|
rows?: number;
|
||||||
|
placeholder?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{label && <p className="text-sm font-medium text-foreground">{label}</p>}
|
||||||
|
{desc && <p className="text-[13px] text-muted-foreground">{desc}</p>}
|
||||||
|
<TextArea
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
rows={rows}
|
||||||
|
placeholder={placeholder}
|
||||||
|
fullWidth
|
||||||
|
className={fieldCls}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 文本输入 + 浏览按钮 ----------
|
||||||
|
export function SettingFilePicker({
|
||||||
|
label,
|
||||||
|
desc,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
mode = "file",
|
||||||
|
filters,
|
||||||
|
showCheck,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
desc?: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (v: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
/** file:选择文件;folder:选择文件夹 */
|
||||||
|
mode?: "file" | "folder";
|
||||||
|
filters?: { name: string; extensions: string[] }[];
|
||||||
|
/** 显示路径校验通过标记(配合外部 resolveJava 校验) */
|
||||||
|
showCheck?: boolean;
|
||||||
|
}) {
|
||||||
|
const [browsing, setBrowsing] = useState(false);
|
||||||
|
|
||||||
|
const handleBrowse = async () => {
|
||||||
|
setBrowsing(true);
|
||||||
|
try {
|
||||||
|
if (mode === "file") {
|
||||||
|
const result = await ipcInvoke<{ srcPath: string; ext: string } | null>("dialog:openFile", {
|
||||||
|
filters,
|
||||||
|
});
|
||||||
|
if (result) onChange(result.srcPath);
|
||||||
|
} else {
|
||||||
|
const result = await ipcInvoke<{ folderPath: string } | null>("dialog:openFolder");
|
||||||
|
if (result) onChange(result.folderPath);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 对话框取消或失败则忽略
|
||||||
|
} finally {
|
||||||
|
setBrowsing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{label && <p className="text-sm font-medium text-foreground">{label}</p>}
|
||||||
|
{desc && <p className="text-[13px] text-muted-foreground mt-0.5 mb-2">{desc}</p>}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex-1 min-w-0 relative">
|
||||||
|
<Input
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
fullWidth
|
||||||
|
className={fieldCls}
|
||||||
|
/>
|
||||||
|
{showCheck && value && (
|
||||||
|
<Check className="absolute right-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-green-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button size="sm" variant="outline" onPress={handleBrowse} isDisabled={browsing} className="shrink-0">
|
||||||
|
{browsing ? (
|
||||||
|
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||||
|
) : mode === "file" ? (
|
||||||
|
<FolderSearch className="w-3.5 h-3.5" />
|
||||||
|
) : (
|
||||||
|
<FolderOpen className="w-3.5 h-3.5" />
|
||||||
|
)}
|
||||||
|
浏览
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,4 +2,16 @@ export { SettingCard } from "./SettingCard";
|
|||||||
export { SettingRow } from "./SettingRow";
|
export { SettingRow } from "./SettingRow";
|
||||||
export { PageHeader, SectionTitle } from "./SectionTitle";
|
export { PageHeader, SectionTitle } from "./SectionTitle";
|
||||||
export { Surface, SurfaceHeader, SurfaceContent } from "./Surface";
|
export { Surface, SurfaceHeader, SurfaceContent } from "./Surface";
|
||||||
export { Skeleton } from "@/components/ui/skeleton";
|
export { SettingBadge, type SettingBadgeVariant } from "./SettingBadge";
|
||||||
|
export { SettingListItem } from "./SettingListItem";
|
||||||
|
export {
|
||||||
|
SettingSelect,
|
||||||
|
SettingNumberField,
|
||||||
|
SettingSwitch,
|
||||||
|
SettingRadioGroup,
|
||||||
|
SettingTextArea,
|
||||||
|
SettingFilePicker,
|
||||||
|
fieldCls,
|
||||||
|
type SettingOption,
|
||||||
|
} from "./controls";
|
||||||
|
export { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useEffect } from "react";
|
|||||||
import { BUILD_MODE } from "@/lib/mode";
|
import { BUILD_MODE } from "@/lib/mode";
|
||||||
import { TaskButton } from "@/components/task/TaskButton";
|
import { TaskButton } from "@/components/task/TaskButton";
|
||||||
import { useRouteStore } from "@/stores/routeStore";
|
import { useRouteStore } from "@/stores/routeStore";
|
||||||
|
import { useConfigStore } from "@/stores/configStore";
|
||||||
import { Info } from "lucide-react";
|
import { Info } from "lucide-react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ export function WindowControls({
|
|||||||
}: WindowControlsProps) {
|
}: WindowControlsProps) {
|
||||||
const [isMaximized, setIsMaximized] = useState(false);
|
const [isMaximized, setIsMaximized] = useState(false);
|
||||||
const navigate = useRouteStore((s) => s.navigate);
|
const navigate = useRouteStore((s) => s.navigate);
|
||||||
|
const showTaskButton = useConfigStore((s) => s.config.ui?.showTaskButton ?? true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.electronAPI?.isMaximized().then(setIsMaximized);
|
window.electronAPI?.isMaximized().then(setIsMaximized);
|
||||||
@@ -42,7 +44,7 @@ export function WindowControls({
|
|||||||
const showBadge = BUILD_MODE !== "run";
|
const showBadge = BUILD_MODE !== "run";
|
||||||
const badgeLabel = BUILD_MODE === "dev" ? "DEV" : "BETA";
|
const badgeLabel = BUILD_MODE === "dev" ? "DEV" : "BETA";
|
||||||
|
|
||||||
const showTask = !isSub && !isOobe;
|
const showTask = !isSub && !isOobe && showTaskButton;
|
||||||
const showInfo = isOobe;
|
const showInfo = isOobe;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+12
-29
@@ -15,28 +15,28 @@ import { toast } from "sonner";
|
|||||||
import { useInstanceStore } from "@/stores/instanceStore";
|
import { useInstanceStore } from "@/stores/instanceStore";
|
||||||
import { useConfigStore } from "@/stores/configStore";
|
import { useConfigStore } from "@/stores/configStore";
|
||||||
import { useAuthStore } from "@/stores/authStore";
|
import { useAuthStore } from "@/stores/authStore";
|
||||||
|
import { useLaunchStore } from "@/stores/launchStore";
|
||||||
import { useConfirmDialogStore } from "@/stores/confirmDialogStore";
|
import { useConfirmDialogStore } from "@/stores/confirmDialogStore";
|
||||||
import { useRouteStore } from "@/stores/routeStore";
|
import { useRouteStore } from "@/stores/routeStore";
|
||||||
import { onLaunchComplete, onLaunchError } from "@/api/instance";
|
|
||||||
import { openPath } from "@/api/system";
|
import { openPath } from "@/api/system";
|
||||||
import { InstanceList } from "./InstanceList";
|
import { InstanceList } from "./InstanceList";
|
||||||
import { InstanceDetail } from "./InstanceDetail";
|
import { InstanceDetail } from "./InstanceDetail";
|
||||||
import { EditDialog } from "./EditDialog";
|
import { EditDialog } from "./EditDialog";
|
||||||
|
|
||||||
export function Gallery() {
|
export function Gallery() {
|
||||||
const { instances, loading, fetchInstances, remove, launch } = useInstanceStore();
|
const { instances, loading, fetchInstances, remove } = useInstanceStore();
|
||||||
const gameConfig = useConfigStore((s) => s.config.game);
|
const gameConfig = useConfigStore((s) => s.config.game);
|
||||||
const javaConfig = useConfigStore((s) => s.config.java);
|
|
||||||
const configInstances = useConfigStore((s) => s.config.instances);
|
const configInstances = useConfigStore((s) => s.config.instances);
|
||||||
const setInstances = useConfigStore((s) => s.setInstances);
|
const setInstances = useConfigStore((s) => s.setInstances);
|
||||||
const user = useAuthStore((s) => s.user);
|
const user = useAuthStore((s) => s.user);
|
||||||
const openConfirm = useConfirmDialogStore((s) => s.openDialog);
|
const openConfirm = useConfirmDialogStore((s) => s.openDialog);
|
||||||
const navigate = useRouteStore((s) => s.navigate);
|
const navigate = useRouteStore((s) => s.navigate);
|
||||||
const setStoreSection = useRouteStore((s) => s.setStoreSection);
|
const setStoreSection = useRouteStore((s) => s.setStoreSection);
|
||||||
|
const launch = useLaunchStore((s) => s.launch);
|
||||||
|
const launching = useLaunchStore((s) => s.launching);
|
||||||
|
|
||||||
const [selectedName, setSelectedName] = useState<string | null>(null);
|
const [selectedName, setSelectedName] = useState<string | null>(null);
|
||||||
const [editOpen, setEditOpen] = useState(false);
|
const [editOpen, setEditOpen] = useState(false);
|
||||||
const [launching, setLaunching] = useState(false);
|
|
||||||
|
|
||||||
// 跳转到资源中心"原版游戏"分类创建实例
|
// 跳转到资源中心"原版游戏"分类创建实例
|
||||||
const goCreateInstance = useCallback(() => {
|
const goCreateInstance = useCallback(() => {
|
||||||
@@ -63,41 +63,24 @@ export function Gallery() {
|
|||||||
}
|
}
|
||||||
}, [instances, selectedName]);
|
}, [instances, selectedName]);
|
||||||
|
|
||||||
// 监听启动结果
|
|
||||||
useEffect(() => {
|
|
||||||
const offComplete = onLaunchComplete(() => {
|
|
||||||
setLaunching(false);
|
|
||||||
toast.success("游戏启动成功");
|
|
||||||
});
|
|
||||||
const offError = onLaunchError(({ error }) => {
|
|
||||||
setLaunching(false);
|
|
||||||
toast.error(`启动失败: ${error}`);
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
offComplete();
|
|
||||||
offError();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const selected = instances.find((i) => i.name === selectedName) ?? null;
|
const selected = instances.find((i) => i.name === selectedName) ?? null;
|
||||||
|
|
||||||
// 启动游戏
|
// 启动游戏(统一接口:主进程自动应用权威配置)
|
||||||
const handlePlay = async () => {
|
const handlePlay = async () => {
|
||||||
if (!selected) return;
|
if (!selected) return;
|
||||||
if (!user?.username || !user?.uuid) {
|
if (!user?.username || !user?.uuid) {
|
||||||
toast.warning("请先在设置中登录账号");
|
toast.warning("请先在设置中登录账号");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLaunching(true);
|
|
||||||
try {
|
try {
|
||||||
await launch(selected.name, gameConfig.gameDir, {
|
await launch(selected.name, gameConfig.gameDir);
|
||||||
username: user.username,
|
const error = useLaunchStore.getState().error;
|
||||||
uuid: user.uuid,
|
if (error) {
|
||||||
accessToken: user.accessToken,
|
toast.error(`启动失败: ${error}`);
|
||||||
javaPath: javaConfig.javaPath || undefined,
|
} else {
|
||||||
});
|
toast.success("游戏已启动");
|
||||||
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setLaunching(false);
|
|
||||||
toast.error(`启动失败: ${e.message || e}`);
|
toast.error(`启动失败: ${e.message || e}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { useInstanceStore } from "@/stores/instanceStore";
|
import { useInstanceStore } from "@/stores/instanceStore";
|
||||||
import { useRouteStore } from "@/stores/routeStore";
|
import { useRouteStore } from "@/stores/routeStore";
|
||||||
|
import { useConfigStore } from "@/stores/configStore";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
export function InstanceTitle() {
|
export function InstanceTitle() {
|
||||||
const currentInstance = useInstanceStore((s) => s.currentInstance);
|
const currentInstance = useInstanceStore((s) => s.currentInstance);
|
||||||
const navigate = useRouteStore((s) => s.navigate);
|
const navigate = useRouteStore((s) => s.navigate);
|
||||||
|
const showTitle = useConfigStore((s) => s.config.ui?.showInstanceTitle ?? true);
|
||||||
|
|
||||||
|
if (!showTitle) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,13 +1,34 @@
|
|||||||
import { Play, Settings, Package } from "lucide-react";
|
import { Play, Settings, Package, Loader2 } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { useRouteStore } from "@/stores/routeStore";
|
import { useRouteStore } from "@/stores/routeStore";
|
||||||
|
import { useInstanceStore } from "@/stores/instanceStore";
|
||||||
|
import { useConfigStore } from "@/stores/configStore";
|
||||||
|
import { useLaunchStore } from "@/stores/launchStore";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
interface StartCardProps {
|
export function StartCard() {
|
||||||
onSettingsClick?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function StartCard({ onSettingsClick }: StartCardProps) {
|
|
||||||
const navigate = useRouteStore((s) => s.navigate);
|
const navigate = useRouteStore((s) => s.navigate);
|
||||||
|
const currentInstance = useInstanceStore((s) => s.currentInstance);
|
||||||
|
const gameDir = useConfigStore((s) => s.config.game.gameDir);
|
||||||
|
const launching = useLaunchStore((s) => s.launching);
|
||||||
|
const running = useLaunchStore((s) => s.running);
|
||||||
|
const launch = useLaunchStore((s) => s.launch);
|
||||||
|
const clearError = useLaunchStore((s) => s.clearError);
|
||||||
|
|
||||||
|
// 启动游戏:自动携带实例 + 主进程权威配置(Java/内存/GC/窗口等)
|
||||||
|
const handleLaunch = async () => {
|
||||||
|
clearError();
|
||||||
|
if (!currentInstance) {
|
||||||
|
toast.warning("请先选择一个游戏实例");
|
||||||
|
navigate("gallery");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await launch(currentInstance.name, gameDir);
|
||||||
|
const error = useLaunchStore.getState().error;
|
||||||
|
if (error) {
|
||||||
|
toast.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -20,7 +41,7 @@ export function StartCard({ onSettingsClick }: StartCardProps) {
|
|||||||
>
|
>
|
||||||
{/* Settings button */}
|
{/* Settings button */}
|
||||||
<button
|
<button
|
||||||
onClick={onSettingsClick ?? (() => navigate("setting"))}
|
onClick={() => navigate("setting")}
|
||||||
className="flex items-center justify-center w-9 h-9 ml-0.5 text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-all duration-150 cursor-pointer shrink-0 rounded-full"
|
className="flex items-center justify-center w-9 h-9 ml-0.5 text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-all duration-150 cursor-pointer shrink-0 rounded-full"
|
||||||
aria-label="设置"
|
aria-label="设置"
|
||||||
>
|
>
|
||||||
@@ -28,10 +49,18 @@ export function StartCard({ onSettingsClick }: StartCardProps) {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Launch button */}
|
{/* Launch button */}
|
||||||
<button className="flex items-center justify-center h-9 rounded-full cursor-pointer shrink-0 active:scale-[0.97] transition-all duration-150">
|
<button
|
||||||
|
onClick={handleLaunch}
|
||||||
|
disabled={launching}
|
||||||
|
className="flex items-center justify-center h-9 rounded-full cursor-pointer shrink-0 active:scale-[0.97] transition-all duration-150 disabled:cursor-not-allowed disabled:opacity-70"
|
||||||
|
>
|
||||||
<span className="flex items-center gap-1.5 px-4 h-7 rounded-full font-medium text-sm text-primary bg-primary/10 hover:bg-primary/20 transition-colors">
|
<span className="flex items-center gap-1.5 px-4 h-7 rounded-full font-medium text-sm text-primary bg-primary/10 hover:bg-primary/20 transition-colors">
|
||||||
<Play className="w-3.5 h-3.5 fill-current" />
|
{launching ? (
|
||||||
启动游戏
|
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Play className="w-3.5 h-3.5 fill-current" />
|
||||||
|
)}
|
||||||
|
{launching ? "正在启动..." : running ? "游戏中" : "启动游戏"}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
|
import { Input } from "@heroui/react";
|
||||||
import { useConfigStore } from "@/stores/configStore";
|
import { useConfigStore } from "@/stores/configStore";
|
||||||
import { Switch, RadioGroup, Radio, Input, TextArea } from "@heroui/react";
|
import {
|
||||||
import { SettingCard, SettingRow, PageHeader, SectionTitle } from "@/components/setting";
|
SettingCard,
|
||||||
|
SettingSelect,
|
||||||
|
SettingSwitch,
|
||||||
|
SettingNumberField,
|
||||||
|
SettingFilePicker,
|
||||||
|
fieldCls,
|
||||||
|
PageHeader,
|
||||||
|
SectionTitle,
|
||||||
|
} from "@/components/setting";
|
||||||
const launcherBehavior = [
|
const launcherBehavior = [
|
||||||
{ value: "close", label: "关闭启动器" },
|
{ value: "close", label: "关闭启动器" },
|
||||||
{ value: "minimize", label: "最小化到任务栏" },
|
{ value: "minimize", label: "最小化到任务栏" },
|
||||||
@@ -23,69 +31,52 @@ export function AdvancedSetting() {
|
|||||||
<PageHeader title="高级设置" desc="游戏高级启动参数、调试选项与实验性功能" />
|
<PageHeader title="高级设置" desc="游戏高级启动参数、调试选项与实验性功能" />
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
{/* 启动行为 */}
|
||||||
<div>
|
<div>
|
||||||
<SectionTitle>启动行为</SectionTitle>
|
<SectionTitle>启动行为</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<SettingCard>
|
<SettingCard>
|
||||||
<div className="space-y-2">
|
<SettingSelect
|
||||||
<p className="text-sm font-medium text-foreground">启动后启动器行为</p>
|
label="启动后启动器行为"
|
||||||
<p className="text-[13px] text-muted-foreground">游戏启动后启动器的处理方式</p>
|
desc="游戏窗口就绪后启动器的处理方式"
|
||||||
<RadioGroup
|
value={adv.afterLaunch}
|
||||||
value={adv.afterLaunch}
|
options={launcherBehavior}
|
||||||
onValueChange={(v) => setAdvanced({ afterLaunch: v })}
|
onChange={(v) => setAdvanced({ afterLaunch: v })}
|
||||||
className="mt-2 space-y-2"
|
/>
|
||||||
>
|
|
||||||
{launcherBehavior.map((opt) => (
|
|
||||||
<Radio key={opt.value} value={opt.value}>
|
|
||||||
<Radio.Content>{opt.label}</Radio.Content>
|
|
||||||
</Radio>
|
|
||||||
))}
|
|
||||||
</RadioGroup>
|
|
||||||
</div>
|
|
||||||
</SettingCard>
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 窗口设置 */}
|
||||||
<div>
|
<div>
|
||||||
<SectionTitle>窗口设置</SectionTitle>
|
<SectionTitle>窗口设置</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<SettingCard>
|
<SettingCard>
|
||||||
<div className="space-y-3">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<SettingSelect
|
||||||
<p className="text-sm font-medium text-foreground">窗口大小</p>
|
label="窗口大小"
|
||||||
<RadioGroup
|
value={adv.winMode}
|
||||||
value={adv.winMode}
|
options={windowSize}
|
||||||
onValueChange={(v) => setAdvanced({ winMode: v })}
|
onChange={(v) => setAdvanced({ winMode: v })}
|
||||||
className="space-y-2"
|
/>
|
||||||
>
|
|
||||||
{windowSize.map((opt) => (
|
|
||||||
<Radio key={opt.value} value={opt.value}>
|
|
||||||
<Radio.Content>{opt.label}</Radio.Content>
|
|
||||||
</Radio>
|
|
||||||
))}
|
|
||||||
</RadioGroup>
|
|
||||||
</div>
|
|
||||||
{adv.winMode === "custom" && (
|
{adv.winMode === "custom" && (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex items-center gap-2">
|
<SettingNumberField
|
||||||
<span className="text-[13px] text-muted-foreground">宽</span>
|
label="宽"
|
||||||
<Input
|
value={adv.customWidth}
|
||||||
type="number"
|
onChange={(v) => setAdvanced({ customWidth: v })}
|
||||||
value={String(adv.customWidth)}
|
min={320}
|
||||||
onChange={(e) => setAdvanced({ customWidth: Number(e.target.value) })}
|
max={7680}
|
||||||
className="w-20"
|
suffix="px"
|
||||||
/>
|
/>
|
||||||
</div>
|
<SettingNumberField
|
||||||
<span className="text-muted-foreground">×</span>
|
label="高"
|
||||||
<div className="flex items-center gap-2">
|
value={adv.customHeight}
|
||||||
<span className="text-[13px] text-muted-foreground">高</span>
|
onChange={(v) => setAdvanced({ customHeight: v })}
|
||||||
<Input
|
min={240}
|
||||||
type="number"
|
max={4320}
|
||||||
value={String(adv.customHeight)}
|
suffix="px"
|
||||||
onChange={(e) => setAdvanced({ customHeight: Number(e.target.value) })}
|
/>
|
||||||
className="w-20"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -93,53 +84,96 @@ export function AdvancedSetting() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 快速进入服务器 */}
|
||||||
|
<div>
|
||||||
|
<SectionTitle>快速进入服务器</SectionTitle>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<SettingCard>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm font-medium text-foreground">自动加入服务器</p>
|
||||||
|
<p className="text-[13px] text-muted-foreground">
|
||||||
|
启动游戏后自动加入该服务器;IP 留空则不自动加入
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
value={adv.server?.ip ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setAdvanced({ server: { ip: e.target.value, port: adv.server?.port ?? 25565 } })
|
||||||
|
}
|
||||||
|
placeholder="例如 mc.example.com"
|
||||||
|
fullWidth
|
||||||
|
className={fieldCls}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={String(adv.server?.port ?? 25565)}
|
||||||
|
onChange={(e) =>
|
||||||
|
setAdvanced({ server: { ip: adv.server?.ip ?? "", port: Number(e.target.value) || 25565 } })
|
||||||
|
}
|
||||||
|
className={`w-24 shrink-0 ${fieldCls}`}
|
||||||
|
aria-label="服务器端口"
|
||||||
|
/>
|
||||||
|
<span className="text-[13px] text-muted-foreground shrink-0">端口</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 游戏参数 */}
|
||||||
<div>
|
<div>
|
||||||
<SectionTitle>游戏参数</SectionTitle>
|
<SectionTitle>游戏参数</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<SettingCard>
|
<SettingCard>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-medium text-foreground">额外游戏启动参数</p>
|
<p className="text-sm font-medium text-foreground">额外游戏启动参数</p>
|
||||||
<p className="text-[13px] text-muted-foreground">附加到游戏启动命令末尾的参数</p>
|
<p className="text-[13px] text-muted-foreground">
|
||||||
|
附加到游戏启动命令末尾的参数,例如 --demo;支持引号包裹含空格的值
|
||||||
|
</p>
|
||||||
<Input
|
<Input
|
||||||
value={adv.gameArgs}
|
value={adv.gameArgs}
|
||||||
onChange={(e) => setAdvanced({ gameArgs: e.target.value })}
|
onChange={(e) => setAdvanced({ gameArgs: e.target.value })}
|
||||||
placeholder="可选,例如 --demo"
|
placeholder="可选,例如 --demo"
|
||||||
fullWidth
|
fullWidth
|
||||||
|
className={fieldCls}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingCard>
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 启动命令 */}
|
||||||
<div>
|
<div>
|
||||||
<SectionTitle>启动命令</SectionTitle>
|
<SectionTitle>启动命令</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<SettingCard>
|
<SettingCard>
|
||||||
<div className="space-y-2">
|
<SettingFilePicker
|
||||||
<p className="text-sm font-medium text-foreground">启动前执行命令</p>
|
label="启动前执行命令"
|
||||||
<p className="text-[13px] text-muted-foreground">游戏启动前自动执行的命令或程序路径</p>
|
desc="游戏启动前自动执行的命令或程序路径(Windows 批处理需以 cmd /c 开头)"
|
||||||
<Input
|
value={adv.preLaunchCmd}
|
||||||
value={adv.preLaunchCmd}
|
onChange={(v) => setAdvanced({ preLaunchCmd: v })}
|
||||||
onChange={(e) => setAdvanced({ preLaunchCmd: e.target.value })}
|
placeholder="例如 cmd /c D:\scripts\pre-launch.bat"
|
||||||
placeholder="可选,例如 D:\scripts\pre-launch.bat"
|
mode="file"
|
||||||
fullWidth
|
filters={[
|
||||||
/>
|
{ name: "批处理 / 可执行文件", extensions: ["bat", "cmd", "exe"] },
|
||||||
</div>
|
{ name: "所有文件", extensions: ["*"] },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</SettingCard>
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 调试 */}
|
||||||
<div>
|
<div>
|
||||||
<SectionTitle>调试</SectionTitle>
|
<SectionTitle>调试</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<SettingCard>
|
<SettingCard>
|
||||||
<SettingRow label="调试模式" desc="启用后将在控制台输出详细日志,可能影响性能">
|
<SettingSwitch
|
||||||
<Switch isSelected={adv.debugMode} onValueChange={(v) => setAdvanced({ debugMode: v })}>
|
label="调试模式"
|
||||||
<Switch.Control>
|
desc="启用后附加 -Dkoring.debugMode=true 并在控制台输出详细日志,可能影响性能"
|
||||||
<Switch.Thumb />
|
checked={adv.debugMode}
|
||||||
</Switch.Control>
|
onChange={(v) => setAdvanced({ debugMode: v })}
|
||||||
</Switch>
|
/>
|
||||||
</SettingRow>
|
|
||||||
</SettingCard>
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,171 @@
|
|||||||
import { EmptyState } from "@heroui/react";
|
import { useState } from "react";
|
||||||
import { Gamepad2 } from "lucide-react";
|
import { Button, Avatar, Input } from "@heroui/react";
|
||||||
import { PageHeader } from "@/components/setting";
|
import { Gamepad2, LogOut, Loader2, UserRound, Wifi, ShieldQuestion, AlertCircle } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { useAuthStore } from "@/stores/authStore";
|
||||||
|
import {
|
||||||
|
SettingCard,
|
||||||
|
SettingBadge,
|
||||||
|
PageHeader,
|
||||||
|
SectionTitle,
|
||||||
|
fieldCls,
|
||||||
|
} from "@/components/setting";
|
||||||
|
|
||||||
export function GameAccountSetting() {
|
export function GameAccountSetting() {
|
||||||
|
const user = useAuthStore((s) => s.user);
|
||||||
|
const loading = useAuthStore((s) => s.loading);
|
||||||
|
const error = useAuthStore((s) => s.error);
|
||||||
|
const logout = useAuthStore((s) => s.logout);
|
||||||
|
const loginOffline = useAuthStore((s) => s.loginOffline);
|
||||||
|
const clearError = useAuthStore((s) => s.clearError);
|
||||||
|
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
|
||||||
|
// 离线账号登录
|
||||||
|
const handleOfflineLogin = async () => {
|
||||||
|
clearError();
|
||||||
|
const name = username.trim();
|
||||||
|
if (!name) {
|
||||||
|
toast.warning("请输入离线用户名");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await loginOffline(name);
|
||||||
|
if (!useAuthStore.getState().error) {
|
||||||
|
toast.success(`已登录离线账号:${name}`);
|
||||||
|
setUsername("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 退出登录
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await logout();
|
||||||
|
toast.success("已退出游戏账号");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<PageHeader title="游戏账户&档案" desc="管理 Minecraft 游戏内账户、正版验证与游戏档案配置" />
|
<PageHeader title="游戏账户&档案" desc="管理 Minecraft 游戏内账户(离线 / 微软)与登录状态" />
|
||||||
<EmptyState className="py-16">
|
|
||||||
<Gamepad2 className="w-10 h-10 text-muted-foreground/30" />
|
<div className="space-y-6">
|
||||||
<p className="text-sm text-muted-foreground mt-3">该功能正在开发中</p>
|
{/* 当前账号 */}
|
||||||
</EmptyState>
|
<div>
|
||||||
|
<SectionTitle>当前账号</SectionTitle>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<SettingCard>
|
||||||
|
{user ? (
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Avatar size="lg" className="shrink-0">
|
||||||
|
<Avatar.Fallback>
|
||||||
|
<UserRound className="w-7 h-7 text-foreground/40" />
|
||||||
|
</Avatar.Fallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<p className="text-base font-medium text-foreground truncate">{user.username}</p>
|
||||||
|
{user.accessToken ? (
|
||||||
|
<SettingBadge variant="info">微软账号</SettingBadge>
|
||||||
|
) : (
|
||||||
|
<SettingBadge variant="neutral">离线账号</SettingBadge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-[12px] text-muted-foreground/70 mt-0.5 font-mono truncate">UUID: {user.uuid}</p>
|
||||||
|
<p className="text-[12px] text-muted-foreground/60 mt-0.5">
|
||||||
|
启动游戏时将自动使用此账号
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button size="sm" variant="danger-soft" className="shrink-0" onPress={handleLogout} isDisabled={loading}>
|
||||||
|
<LogOut className="w-3.5 h-3.5" />
|
||||||
|
退出登录
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center gap-3 py-10 text-center">
|
||||||
|
<div className="w-14 h-14 rounded-full bg-foreground/[0.06] flex items-center justify-center shrink-0">
|
||||||
|
<Gamepad2 className="w-8 h-8 text-foreground/30" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">尚未登录游戏账号</p>
|
||||||
|
<p className="text-[12px] text-muted-foreground/60 mt-1">
|
||||||
|
使用下方离线登录即可启动游戏
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</SettingCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 登录方式 */}
|
||||||
|
<div>
|
||||||
|
<SectionTitle>登录方式</SectionTitle>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* 离线账号 */}
|
||||||
|
<SettingCard>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-foreground/[0.05] dark:bg-white/[0.05] flex items-center justify-center shrink-0">
|
||||||
|
<Wifi className="w-4 h-4 text-muted-foreground/70" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-foreground">离线账号</p>
|
||||||
|
<p className="text-[12px] text-muted-foreground/70">无需正版验证,输入用户名即可启动</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
placeholder="输入离线用户名(不超过 16 字符)"
|
||||||
|
maxLength={16}
|
||||||
|
fullWidth
|
||||||
|
className={fieldCls}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") handleOfflineLogin();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="primary"
|
||||||
|
className="shrink-0"
|
||||||
|
onPress={handleOfflineLogin}
|
||||||
|
isDisabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <UserRound className="w-3.5 h-3.5" />}
|
||||||
|
登录
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{error && (
|
||||||
|
<p className="text-[12px] text-red-500/80 flex items-center gap-1">
|
||||||
|
<AlertCircle className="w-3.5 h-3.5" />
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SettingCard>
|
||||||
|
|
||||||
|
{/* 微软账号(开发中) */}
|
||||||
|
<SettingCard>
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-foreground/[0.05] dark:bg-white/[0.05] flex items-center justify-center shrink-0">
|
||||||
|
<ShieldQuestion className="w-4 h-4 text-muted-foreground/70" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<p className="text-sm font-medium text-foreground">微软账号</p>
|
||||||
|
<SettingBadge variant="warning">开发中</SettingBadge>
|
||||||
|
</div>
|
||||||
|
<p className="text-[12px] text-muted-foreground/70">正版验证登录,支持皮肤与存档同步</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button size="sm" variant="outline" className="shrink-0" isDisabled>
|
||||||
|
登录
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</SettingCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,30 +8,30 @@
|
|||||||
// 未经允许的情况下删除此版权头可能会受到民事指控
|
// 未经允许的情况下删除此版权头可能会受到民事指控
|
||||||
// 请勿在未经Lingke Network (china)允许的范围内修改代码并分发
|
// 请勿在未经Lingke Network (china)允许的范围内修改代码并分发
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useRef, useCallback } from "react";
|
||||||
import { Button, Skeleton } from "@heroui/react";
|
import { Button, Skeleton } from "@heroui/react";
|
||||||
import { RefreshCw, FolderOpen, Trash2, Search, Plus, CircleCheck, CircleAlert, Home, Check, Download, Loader2 } from "lucide-react";
|
import { RefreshCw, FolderOpen, Trash2, Search, Plus, CircleCheck, CircleAlert, Home, Check, Download, Loader2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useConfigStore } from "@/stores/configStore";
|
import { useConfigStore } from "@/stores/configStore";
|
||||||
import { useInstanceStore } from "@/stores/instanceStore";
|
import { useInstanceStore } from "@/stores/instanceStore";
|
||||||
import { scanGameDir, selectFolder, importExistingInstance, type ScannedVersion } from "@/api/instance";
|
import { listInstances, scanGameDir, selectFolder, importExistingInstance, type ScannedVersion } from "@/api/instance";
|
||||||
import { SettingCard, PageHeader, SectionTitle } from "@/components/setting";
|
import { SettingCard, SettingBadge, SettingListItem, PageHeader, SectionTitle, type SettingBadgeVariant } from "@/components/setting";
|
||||||
|
|
||||||
// 版本类型标签样式
|
// 版本类型标签(统一徽章变体)
|
||||||
const TYPE_BADGE: Record<string, { label: string; cls: string }> = {
|
const TYPE_BADGE: Record<string, { label: string; variant: SettingBadgeVariant }> = {
|
||||||
release: { label: "正式版", cls: "bg-green-500/10 text-green-600 dark:text-green-400" },
|
release: { label: "正式版", variant: "success" },
|
||||||
snapshot: { label: "预览版", cls: "bg-amber-500/10 text-amber-600 dark:text-amber-400" },
|
snapshot: { label: "预览版", variant: "warning" },
|
||||||
"old_alpha": { label: "Alpha", cls: "bg-purple-500/10 text-purple-600 dark:text-purple-400" },
|
old_alpha: { label: "Alpha", variant: "violet" },
|
||||||
"old_beta": { label: "Beta", cls: "bg-indigo-500/10 text-indigo-600 dark:text-indigo-400" },
|
old_beta: { label: "Beta", variant: "info" },
|
||||||
unknown: { label: "未知", cls: "bg-foreground/[0.06] dark:bg-white/[0.06] text-muted-foreground" },
|
unknown: { label: "未知", variant: "neutral" },
|
||||||
};
|
};
|
||||||
|
|
||||||
// 加载器标签样式
|
// 加载器标签(统一徽章变体)
|
||||||
const LOADER_BADGE: Record<string, { label: string; cls: string }> = {
|
const LOADER_BADGE: Record<string, { label: string; variant: SettingBadgeVariant }> = {
|
||||||
forge: { label: "Forge", cls: "bg-orange-500/10 text-orange-600 dark:text-orange-400" },
|
forge: { label: "Forge", variant: "warning" },
|
||||||
fabric: { label: "Fabric", cls: "bg-sky-500/10 text-sky-600 dark:text-sky-400" },
|
fabric: { label: "Fabric", variant: "info" },
|
||||||
quilt: { label: "Quilt", cls: "bg-pink-500/10 text-pink-600 dark:text-pink-400" },
|
quilt: { label: "Quilt", variant: "violet" },
|
||||||
optifine: { label: "OptiFine", cls: "bg-violet-500/10 text-violet-600 dark:text-violet-400" },
|
optifine: { label: "OptiFine", variant: "violet" },
|
||||||
};
|
};
|
||||||
|
|
||||||
function getBadge(type: string) {
|
function getBadge(type: string) {
|
||||||
@@ -46,10 +46,10 @@ function formatTime(iso?: string): string {
|
|||||||
|
|
||||||
function FileStatus({ ok, label }: { ok: boolean; label: string }) {
|
function FileStatus({ ok, label }: { ok: boolean; label: string }) {
|
||||||
return (
|
return (
|
||||||
<span className={`inline-flex items-center gap-1 text-[11px] ${ok ? "text-green-600 dark:text-green-400" : "text-red-500/70"}`}>
|
<SettingBadge variant={ok ? "success" : "error"}>
|
||||||
{ok ? <CircleCheck className="w-3 h-3" /> : <CircleAlert className="w-3 h-3" />}
|
{ok ? <CircleCheck className="w-3 h-3" /> : <CircleAlert className="w-3 h-3" />}
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</SettingBadge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,21 +90,27 @@ export function GameDirSetting() {
|
|||||||
// 组件挂载时自动扫描主目录
|
// 组件挂载时自动扫描主目录
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleScan(gameDir);
|
handleScan(gameDir);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 主目录变化时自动扫描
|
// 主目录变化时自动重扫新目录(跳过首次挂载)
|
||||||
|
const firstRunRef = useRef(true);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (scanTarget && scanTarget !== gameDir) {
|
if (firstRunRef.current) {
|
||||||
// 目录已变化但还没扫描过新目录
|
firstRunRef.current = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}, [gameDir]);
|
handleScan(gameDir);
|
||||||
|
}, [gameDir, handleScan]);
|
||||||
|
|
||||||
// 导入单个版本
|
// 导入单个版本(来源 = 当前扫描目录,实例建在主库)
|
||||||
const handleImport = async (versionId: string) => {
|
const handleImport = async (versionId: string) => {
|
||||||
|
const source = scanTarget || gameDir;
|
||||||
setImporting(versionId);
|
setImporting(versionId);
|
||||||
try {
|
try {
|
||||||
await importExistingInstance(versionId, gameDir, versionId, {
|
await importExistingInstance(versionId, gameDir, versionId, {
|
||||||
description: `Imported from ${gameDir}`,
|
description: `Imported from ${source}`,
|
||||||
|
sourceGamePath: source,
|
||||||
});
|
});
|
||||||
await fetchInstances(gameDir);
|
await fetchInstances(gameDir);
|
||||||
toast.success(`已导入版本 ${versionId}`);
|
toast.success(`已导入版本 ${versionId}`);
|
||||||
@@ -114,7 +120,7 @@ export function GameDirSetting() {
|
|||||||
setImporting(null);
|
setImporting(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 批量导入所有健康版本
|
// 批量导入所有健康版本(幂等:已存在实例跳过)
|
||||||
const handleImportAll = async () => {
|
const handleImportAll = async () => {
|
||||||
if (!scanResults || scanResults.length === 0) return;
|
if (!scanResults || scanResults.length === 0) return;
|
||||||
const healthy = scanResults.filter((v) => v.healthy);
|
const healthy = scanResults.filter((v) => v.healthy);
|
||||||
@@ -122,13 +128,26 @@ export function GameDirSetting() {
|
|||||||
toast.info("没有可导入的健康版本");
|
toast.info("没有可导入的健康版本");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const source = scanTarget || gameDir;
|
||||||
setImportingBatch(true);
|
setImportingBatch(true);
|
||||||
let success = 0;
|
let success = 0;
|
||||||
let failed = 0;
|
let failed = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
// 已有实例名集合,避免重复导入全部失败
|
||||||
|
let existing = new Set<string>();
|
||||||
|
try {
|
||||||
|
const list = await listInstances(gameDir);
|
||||||
|
existing = new Set(list.map((i) => i.name));
|
||||||
|
} catch {}
|
||||||
for (const v of healthy) {
|
for (const v of healthy) {
|
||||||
|
if (existing.has(v.id)) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await importExistingInstance(v.id, gameDir, v.id, {
|
await importExistingInstance(v.id, gameDir, v.id, {
|
||||||
description: `Imported from ${gameDir}`,
|
description: `Imported from ${source}`,
|
||||||
|
sourceGamePath: source,
|
||||||
});
|
});
|
||||||
success++;
|
success++;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -137,10 +156,15 @@ export function GameDirSetting() {
|
|||||||
}
|
}
|
||||||
await fetchInstances(gameDir);
|
await fetchInstances(gameDir);
|
||||||
setImportingBatch(false);
|
setImportingBatch(false);
|
||||||
if (failed === 0) {
|
const parts = [`成功 ${success}`];
|
||||||
|
if (skipped > 0) parts.push(`跳过 ${skipped}`);
|
||||||
|
if (failed > 0) parts.push(`失败 ${failed}`);
|
||||||
|
if (failed === 0 && skipped === 0) {
|
||||||
toast.success(`成功导入 ${success} 个版本`);
|
toast.success(`成功导入 ${success} 个版本`);
|
||||||
|
} else if (failed === 0) {
|
||||||
|
toast.success(`导入完成:${parts.join(',')}`);
|
||||||
} else {
|
} else {
|
||||||
toast.warning(`导入完成:${success} 成功,${failed} 失败`);
|
toast.warning(`导入完成:${parts.join(',')}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -261,29 +285,24 @@ export function GameDirSetting() {
|
|||||||
const badge = getBadge(v.type);
|
const badge = getBadge(v.type);
|
||||||
const isImporting = importing === v.id;
|
const isImporting = importing === v.id;
|
||||||
return (
|
return (
|
||||||
<div
|
<SettingListItem key={v.id}>
|
||||||
key={v.id}
|
|
||||||
className="flex items-center gap-3 px-3.5 py-2.5 rounded-xl bg-foreground/[0.03] dark:bg-white/[0.03] border border-border/20 dark:border-white/[0.04]"
|
|
||||||
>
|
|
||||||
{/* 版本图标 */}
|
{/* 版本图标 */}
|
||||||
<div className={`w-7 h-7 rounded-lg flex items-center justify-center shrink-0 ${badge.cls}`}>
|
<div className="w-7 h-7 rounded-lg bg-foreground/[0.05] dark:bg-white/[0.05] flex items-center justify-center shrink-0">
|
||||||
<Home className="w-3.5 h-3.5" />
|
<Home className="w-3.5 h-3.5 text-muted-foreground/70" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<p className="text-[13px] font-mono font-semibold text-foreground">{v.id}</p>
|
<p className="text-[13px] font-mono font-semibold text-foreground">{v.id}</p>
|
||||||
<span className={`text-[10px] px-1.5 py-0.5 rounded font-medium ${badge.cls}`}>
|
<SettingBadge variant={badge.variant}>{badge.label}</SettingBadge>
|
||||||
{badge.label}
|
|
||||||
</span>
|
|
||||||
{/* 加载器标签 */}
|
{/* 加载器标签 */}
|
||||||
{v.loaders.map((loader) => (
|
{v.loaders.map((loader) => {
|
||||||
<span
|
const lb = LOADER_BADGE[loader];
|
||||||
key={loader}
|
return lb ? (
|
||||||
className={`text-[10px] px-1.5 py-0.5 rounded font-medium ${LOADER_BADGE[loader]?.cls ?? ""}`}
|
<SettingBadge key={loader} variant={lb.variant}>
|
||||||
>
|
{lb.label}
|
||||||
{LOADER_BADGE[loader]?.label ?? loader}
|
</SettingBadge>
|
||||||
</span>
|
) : null;
|
||||||
))}
|
})}
|
||||||
<span className="text-[11px] text-muted-foreground/60">{formatTime(v.releaseTime)}</span>
|
<span className="text-[11px] text-muted-foreground/60">{formatTime(v.releaseTime)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 mt-1">
|
<div className="flex items-center gap-3 mt-1">
|
||||||
@@ -311,7 +330,7 @@ export function GameDirSetting() {
|
|||||||
)}
|
)}
|
||||||
导入
|
导入
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</SettingListItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -390,10 +409,9 @@ export function GameDirSetting() {
|
|||||||
{scanResults.map((v) => {
|
{scanResults.map((v) => {
|
||||||
const badge = getBadge(v.type);
|
const badge = getBadge(v.type);
|
||||||
return (
|
return (
|
||||||
<span key={v.id} className="inline-flex items-center gap-1 text-[11px] px-2 py-1 rounded-md bg-foreground/[0.04] dark:bg-white/[0.04]">
|
<SettingBadge key={v.id} variant={badge.variant}>
|
||||||
<span className={`w-1.5 h-1.5 rounded-full ${badge.cls.replace(/\/\d+/, "").replace(/\s.*/, "")}`} />
|
|
||||||
{v.id}
|
{v.id}
|
||||||
</span>
|
</SettingBadge>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { Button, Slider, Skeleton } from "@heroui/react";
|
||||||
|
import { Cpu, Loader2, RefreshCw, Check, AlertCircle } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { useConfigStore } from "@/stores/configStore";
|
import { useConfigStore } from "@/stores/configStore";
|
||||||
import { Button, Slider, RadioGroup, Radio, Input, TextArea } from "@heroui/react";
|
import { scanJava, resolveJava, type JavaInfo } from "@/api/java";
|
||||||
import { Cpu, FolderSearch } from "lucide-react";
|
import {
|
||||||
import { SettingCard, SettingRow, PageHeader, SectionTitle } from "@/components/setting";
|
SettingCard,
|
||||||
|
SettingRow,
|
||||||
interface JavaInfo {
|
SettingSelect,
|
||||||
path: string;
|
SettingRadioGroup,
|
||||||
version: string;
|
SettingTextArea,
|
||||||
vendor: string;
|
SettingFilePicker,
|
||||||
}
|
SettingListItem,
|
||||||
|
PageHeader,
|
||||||
const mockJavaList: JavaInfo[] = [
|
SectionTitle,
|
||||||
{ path: "C:\\Program Files\\Java\\jdk-21\\bin\\javaw.exe", version: "21.0.3", vendor: "Oracle OpenJDK" },
|
} from "@/components/setting";
|
||||||
{ path: "C:\\Program Files\\Eclipse Adoptium\\jdk-17.0.10.7-hotspot\\bin\\javaw.exe", version: "17.0.10", vendor: "Eclipse Temurin" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const gcOptions = [
|
const gcOptions = [
|
||||||
{ value: "auto", label: "不指定(由 Java 自动选择)" },
|
{ value: "auto", label: "不指定(由 Java 自动选择)" },
|
||||||
@@ -20,80 +22,179 @@ const gcOptions = [
|
|||||||
{ value: "g1", label: "G1GC(标准,兼容性好)" },
|
{ value: "g1", label: "G1GC(标准,兼容性好)" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 根据路径推断发行版名称(展示用)
|
||||||
|
function javaVendorLabel(j: JavaInfo): string {
|
||||||
|
const p = j.path.toLowerCase();
|
||||||
|
if (p.includes("temurin") || p.includes("adoptium")) return "Eclipse Temurin";
|
||||||
|
if (p.includes("zulu")) return "Azul Zulu";
|
||||||
|
if (p.includes("corretto") || p.includes("amazon")) return "Amazon Corretto";
|
||||||
|
if (p.includes("microsoft")) return "Microsoft OpenJDK";
|
||||||
|
if (p.includes("oracle") || p.includes("jdk") || p.includes("java")) return "OpenJDK";
|
||||||
|
return "Java";
|
||||||
|
}
|
||||||
|
|
||||||
export function JavaMemSetting() {
|
export function JavaMemSetting() {
|
||||||
const java = useConfigStore((s) => s.config.java);
|
const java = useConfigStore((s) => s.config.java);
|
||||||
const setJava = useConfigStore((s) => s.setJava);
|
const setJava = useConfigStore((s) => s.setJava);
|
||||||
|
|
||||||
|
const [scanning, setScanning] = useState(false);
|
||||||
|
const [javaList, setJavaList] = useState<JavaInfo[]>([]);
|
||||||
|
const [validated, setValidated] = useState<JavaInfo | null>(null);
|
||||||
|
const [validating, setValidating] = useState(false);
|
||||||
|
|
||||||
|
// 扫描系统 Java
|
||||||
|
const handleScan = useCallback(async () => {
|
||||||
|
setScanning(true);
|
||||||
|
try {
|
||||||
|
const list = await scanJava();
|
||||||
|
setJavaList(list);
|
||||||
|
if (list.length === 0) {
|
||||||
|
toast.info("未检测到已安装的 Java,请手动指定路径");
|
||||||
|
} else {
|
||||||
|
toast.success(`检测到 ${list.length} 个 Java 环境`);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
toast.error(`检测失败: ${e?.message || e}`);
|
||||||
|
}
|
||||||
|
setScanning(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 配置路径变化后自动校验(700ms debounce,避免每次按键都 spawn java)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!java.javaPath.trim()) {
|
||||||
|
setValidated(null);
|
||||||
|
setValidating(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setValidating(true);
|
||||||
|
const timer = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const info = await resolveJava(java.javaPath.trim());
|
||||||
|
setValidated(info);
|
||||||
|
} catch {
|
||||||
|
setValidated(null);
|
||||||
|
}
|
||||||
|
setValidating(false);
|
||||||
|
}, 700);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [java.javaPath]);
|
||||||
|
|
||||||
|
const isCurrent = (path: string) => java.javaPath === path;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<PageHeader title="Java 虚拟机与内存" desc="配置 Java 运行环境路径、JVM 参数与游戏内存分配" />
|
<PageHeader title="Java 虚拟机与内存" desc="配置 Java 运行环境路径、JVM 参数与游戏内存分配" />
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
{/* Java 环境 */}
|
||||||
<div>
|
<div>
|
||||||
<SectionTitle>Java 环境</SectionTitle>
|
<SectionTitle>Java 环境</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<SettingCard>
|
<SettingCard>
|
||||||
<SettingRow label="自动检测" desc="扫描系统中已安装的 Java 版本">
|
<SettingRow label="自动检测" desc="扫描系统中已安装的 Java(JAVA_HOME / PATH / 常见安装目录)">
|
||||||
<Button size="sm" variant="outline">
|
<Button size="sm" variant="outline" onPress={handleScan} isDisabled={scanning}>
|
||||||
<FolderSearch className="w-3.5 h-3.5 mr-1.5" />
|
{scanning ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <RefreshCw className="w-3.5 h-3.5" />}
|
||||||
检测
|
{scanning ? "检测中..." : "检测"}
|
||||||
</Button>
|
</Button>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</SettingCard>
|
</SettingCard>
|
||||||
|
|
||||||
{mockJavaList.map((j) => (
|
{scanning && (
|
||||||
<SettingCard key={j.path}>
|
<SettingCard>
|
||||||
<div className="flex items-center gap-3">
|
<div className="space-y-2">
|
||||||
<Cpu className="w-4 h-4 text-muted-foreground shrink-0" />
|
{Array.from({ length: 2 }).map((_, i) => (
|
||||||
<div className="flex-1 min-w-0">
|
<Skeleton key={i} className="h-10 w-full rounded-lg" />
|
||||||
<p className="text-sm font-medium text-foreground">
|
))}
|
||||||
{j.vendor} {j.version}
|
|
||||||
</p>
|
|
||||||
<p className="text-[11px] text-muted-foreground/60 mt-0.5 font-mono truncate">
|
|
||||||
{j.path}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button size="sm" variant="outline" className="shrink-0" onPress={() => setJava({ javaPath: j.path })}>
|
|
||||||
使用
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</SettingCard>
|
</SettingCard>
|
||||||
))}
|
)}
|
||||||
|
|
||||||
|
{!scanning && javaList.length > 0 && (
|
||||||
|
<SettingCard>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-[12px] text-muted-foreground/70">检测结果(点击「使用」选择)</p>
|
||||||
|
{javaList.map((j) => {
|
||||||
|
const current = isCurrent(j.path);
|
||||||
|
return (
|
||||||
|
<SettingListItem key={j.path} selected={current}>
|
||||||
|
<Cpu className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium text-foreground">
|
||||||
|
{javaVendorLabel(j)} {j.version}
|
||||||
|
</p>
|
||||||
|
<p className="text-[11px] text-muted-foreground/60 mt-0.5 font-mono truncate">{j.path}</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={current ? "primary" : "outline"}
|
||||||
|
className="shrink-0"
|
||||||
|
isDisabled={current}
|
||||||
|
onPress={() => setJava({ javaPath: j.path })}
|
||||||
|
>
|
||||||
|
{current ? (
|
||||||
|
<>
|
||||||
|
<Check className="w-3.5 h-3.5" />
|
||||||
|
当前
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"使用"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</SettingListItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</SettingCard>
|
||||||
|
)}
|
||||||
|
|
||||||
<SettingCard>
|
<SettingCard>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-medium text-foreground">手动指定路径</p>
|
<p className="text-sm font-medium text-foreground">手动指定路径</p>
|
||||||
<div className="flex items-center gap-2">
|
<p className="text-[13px] text-muted-foreground">
|
||||||
<Input
|
指向 javaw.exe / java.exe 完整路径,输入后自动校验
|
||||||
value={java.javaPath}
|
{validating && "(校验中...)"}
|
||||||
onChange={(e) => setJava({ javaPath: e.target.value })}
|
</p>
|
||||||
placeholder="输入 javaw.exe 完整路径"
|
<SettingFilePicker
|
||||||
fullWidth
|
label=""
|
||||||
/>
|
value={java.javaPath}
|
||||||
<Button size="sm" variant="outline">浏览</Button>
|
onChange={(v) => setJava({ javaPath: v })}
|
||||||
</div>
|
placeholder="例如 C:\Program Files\Java\jdk-21\bin\javaw.exe"
|
||||||
|
mode="file"
|
||||||
|
filters={[{ name: "Java 可执行文件", extensions: ["exe"] }]}
|
||||||
|
showCheck={!!validated}
|
||||||
|
/>
|
||||||
|
{validated && (
|
||||||
|
<p className="text-[12px] text-green-600 dark:text-green-400 flex items-center gap-1">
|
||||||
|
<Check className="w-3.5 h-3.5" />
|
||||||
|
{javaVendorLabel(validated)} {validated.version}(Java {validated.majorVersion})
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{!validating && java.javaPath.trim() && !validated && (
|
||||||
|
<p className="text-[12px] text-red-500/80 flex items-center gap-1">
|
||||||
|
<AlertCircle className="w-3.5 h-3.5" />
|
||||||
|
路径无效或无法解析 Java 版本
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SettingCard>
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 内存分配 */}
|
||||||
<div>
|
<div>
|
||||||
<SectionTitle>内存分配</SectionTitle>
|
<SectionTitle>内存分配</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<SettingCard>
|
<SettingCard>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<RadioGroup
|
<SettingRadioGroup
|
||||||
value={java.memMode}
|
value={java.memMode}
|
||||||
onValueChange={(v) => setJava({ memMode: v })}
|
options={[
|
||||||
className="flex items-center gap-4"
|
{ value: "auto", label: "自动配置" },
|
||||||
>
|
{ value: "custom", label: "自定义" },
|
||||||
<Radio value="auto">
|
]}
|
||||||
<Radio.Content>自动配置</Radio.Content>
|
onChange={(v) => setJava({ memMode: v })}
|
||||||
</Radio>
|
horizontal
|
||||||
<Radio value="custom">
|
/>
|
||||||
<Radio.Content>自定义</Radio.Content>
|
|
||||||
</Radio>
|
|
||||||
</RadioGroup>
|
|
||||||
{java.memMode === "custom" && (
|
{java.memMode === "custom" && (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-1.5">
|
<div className="flex items-center justify-between mb-1.5">
|
||||||
@@ -123,43 +224,34 @@ export function JavaMemSetting() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* JVM 参数 */}
|
||||||
<div>
|
<div>
|
||||||
<SectionTitle>JVM 参数</SectionTitle>
|
<SectionTitle>JVM 参数</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<SettingCard>
|
<SettingCard>
|
||||||
<div className="space-y-2">
|
<SettingTextArea
|
||||||
<p className="text-sm font-medium text-foreground">额外 JVM 启动参数</p>
|
label="额外 JVM 启动参数"
|
||||||
<p className="text-[13px] text-muted-foreground">每行一个参数,例如 -XX:+UseZGC</p>
|
desc="每行一个参数,例如 -XX:+UseZGC;支持引号包裹含空格的值"
|
||||||
<TextArea
|
value={java.jvmArgs}
|
||||||
value={java.jvmArgs}
|
onChange={(v) => setJava({ jvmArgs: v })}
|
||||||
onChange={(e) => setJava({ jvmArgs: e.target.value })}
|
rows={3}
|
||||||
placeholder="可选,留空使用默认参数"
|
placeholder="可选,留空使用默认参数"
|
||||||
rows={3}
|
/>
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingCard>
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 垃圾回收 */}
|
||||||
<div>
|
<div>
|
||||||
<SectionTitle>垃圾回收</SectionTitle>
|
<SectionTitle>垃圾回收</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<SettingCard>
|
<SettingCard>
|
||||||
<div className="space-y-2">
|
<SettingSelect
|
||||||
<p className="text-sm font-medium text-foreground">GC 算法</p>
|
label="GC 算法"
|
||||||
<RadioGroup
|
value={java.gc}
|
||||||
value={java.gc}
|
options={gcOptions}
|
||||||
onValueChange={(v) => setJava({ gc: v })}
|
onChange={(v) => setJava({ gc: v })}
|
||||||
className="space-y-2"
|
/>
|
||||||
>
|
|
||||||
{gcOptions.map((opt) => (
|
|
||||||
<Radio key={opt.value} value={opt.value}>
|
|
||||||
<Radio.Content>{opt.label}</Radio.Content>
|
|
||||||
</Radio>
|
|
||||||
))}
|
|
||||||
</RadioGroup>
|
|
||||||
</div>
|
|
||||||
</SettingCard>
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useConfigStore } from "@/stores/configStore";
|
import { useConfigStore } from "@/stores/configStore";
|
||||||
import { Slider, RadioGroup, Radio, Input } from "@heroui/react";
|
import { Slider, RadioGroup, Radio, Input } from "@heroui/react";
|
||||||
import { SettingCard, PageHeader, SectionTitle } from "@/components/setting";
|
import { SettingCard, PageHeader, SectionTitle, fieldCls } from "@/components/setting";
|
||||||
|
|
||||||
const downloadSources = [
|
const downloadSources = [
|
||||||
{ value: "mirror", label: "尽量使用镜像源(推荐国内用户)" },
|
{ value: "mirror", label: "尽量使用镜像源(推荐国内用户)" },
|
||||||
@@ -26,7 +26,7 @@ export function DownloadSetting() {
|
|||||||
<p className="text-[13px] text-muted-foreground">游戏文件(jar、lib)的下载来源</p>
|
<p className="text-[13px] text-muted-foreground">游戏文件(jar、lib)的下载来源</p>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={dl.fileSource}
|
value={dl.fileSource}
|
||||||
onValueChange={(v) => setDownload({ fileSource: v })}
|
onChange={(v) => setDownload({ fileSource: String(v) })}
|
||||||
className="mt-2 space-y-2"
|
className="mt-2 space-y-2"
|
||||||
>
|
>
|
||||||
{downloadSources.map((opt) => (
|
{downloadSources.map((opt) => (
|
||||||
@@ -44,7 +44,7 @@ export function DownloadSetting() {
|
|||||||
<p className="text-[13px] text-muted-foreground">获取可用游戏版本列表的来源</p>
|
<p className="text-[13px] text-muted-foreground">获取可用游戏版本列表的来源</p>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={dl.versionSource}
|
value={dl.versionSource}
|
||||||
onValueChange={(v) => setDownload({ versionSource: v })}
|
onChange={(v) => setDownload({ versionSource: String(v) })}
|
||||||
className="mt-2 space-y-2"
|
className="mt-2 space-y-2"
|
||||||
>
|
>
|
||||||
{downloadSources.map((opt) => (
|
{downloadSources.map((opt) => (
|
||||||
@@ -103,7 +103,7 @@ export function DownloadSetting() {
|
|||||||
type="number"
|
type="number"
|
||||||
value={String(dl.speedLimit)}
|
value={String(dl.speedLimit)}
|
||||||
onChange={(e) => setDownload({ speedLimit: Number(e.target.value) })}
|
onChange={(e) => setDownload({ speedLimit: Number(e.target.value) })}
|
||||||
className="w-28"
|
className={`w-28 ${fieldCls}`}
|
||||||
/>
|
/>
|
||||||
<span className="text-[13px] text-muted-foreground">KB/s</span>
|
<span className="text-[13px] text-muted-foreground">KB/s</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useCallback } from "react";
|
|||||||
import { useConfigStore } from "@/stores/configStore";
|
import { useConfigStore } from "@/stores/configStore";
|
||||||
import { Switch, Input } from "@heroui/react";
|
import { Switch, Input } from "@heroui/react";
|
||||||
import { ShieldCheck } from "lucide-react";
|
import { ShieldCheck } from "lucide-react";
|
||||||
import { SettingCard, SettingRow, PageHeader, SectionTitle } from "@/components/setting";
|
import { SettingCard, SettingRow, PageHeader, SectionTitle, fieldCls } from "@/components/setting";
|
||||||
|
|
||||||
export function SecurityIdSetting() {
|
export function SecurityIdSetting() {
|
||||||
const enabled = useConfigStore((s) => s.config.network.securityId.enabled);
|
const enabled = useConfigStore((s) => s.config.network.securityId.enabled);
|
||||||
@@ -30,7 +30,7 @@ export function SecurityIdSetting() {
|
|||||||
label="启用第三方认证"
|
label="启用第三方认证"
|
||||||
desc="使用自定义认证服务器替代 Microsoft 认证(适用于离线服务器)"
|
desc="使用自定义认证服务器替代 Microsoft 认证(适用于离线服务器)"
|
||||||
>
|
>
|
||||||
<Switch isSelected={enabled} onValueChange={handleToggle}>
|
<Switch isSelected={enabled} onChange={handleToggle}>
|
||||||
<Switch.Control>
|
<Switch.Control>
|
||||||
<Switch.Thumb />
|
<Switch.Thumb />
|
||||||
</Switch.Control>
|
</Switch.Control>
|
||||||
@@ -50,6 +50,7 @@ export function SecurityIdSetting() {
|
|||||||
onChange={handleUrlChange}
|
onChange={handleUrlChange}
|
||||||
placeholder="https://auth.example.com"
|
placeholder="https://auth.example.com"
|
||||||
fullWidth
|
fullWidth
|
||||||
|
className={fieldCls}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function A11ySetting() {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<SettingCard>
|
<SettingCard>
|
||||||
<SettingRow label="减少动画" desc="关闭页面切换动画和背景动效">
|
<SettingRow label="减少动画" desc="关闭页面切换动画和背景动效">
|
||||||
<Switch isSelected={reduceMotion} onValueChange={setReduceMotion}>
|
<Switch isSelected={reduceMotion} onChange={setReduceMotion}>
|
||||||
<Switch.Control>
|
<Switch.Control>
|
||||||
<Switch.Thumb />
|
<Switch.Thumb />
|
||||||
</Switch.Control>
|
</Switch.Control>
|
||||||
@@ -25,7 +25,7 @@ export function A11ySetting() {
|
|||||||
|
|
||||||
<SettingCard>
|
<SettingCard>
|
||||||
<SettingRow label="减少透明度" desc="将磨砂玻璃效果替换为纯色背景,提升可读性">
|
<SettingRow label="减少透明度" desc="将磨砂玻璃效果替换为纯色背景,提升可读性">
|
||||||
<Switch isSelected={reduceTransparency} onValueChange={setReduceTransparency}>
|
<Switch isSelected={reduceTransparency} onChange={setReduceTransparency}>
|
||||||
<Switch.Control>
|
<Switch.Control>
|
||||||
<Switch.Thumb />
|
<Switch.Thumb />
|
||||||
</Switch.Control>
|
</Switch.Control>
|
||||||
@@ -35,7 +35,7 @@ export function A11ySetting() {
|
|||||||
|
|
||||||
<SettingCard>
|
<SettingCard>
|
||||||
<SettingRow label="高对比度" desc="增强文字与背景的对比度,改善可读性">
|
<SettingRow label="高对比度" desc="增强文字与背景的对比度,改善可读性">
|
||||||
<Switch isSelected={highContrast} onValueChange={setHighContrast}>
|
<Switch isSelected={highContrast} onChange={setHighContrast}>
|
||||||
<Switch.Control>
|
<Switch.Control>
|
||||||
<Switch.Thumb />
|
<Switch.Thumb />
|
||||||
</Switch.Control>
|
</Switch.Control>
|
||||||
|
|||||||
@@ -1,15 +1,63 @@
|
|||||||
import { EmptyState } from "@heroui/react";
|
import { useConfigStore } from "@/stores/configStore";
|
||||||
import { Languages } from "lucide-react";
|
import {
|
||||||
import { PageHeader } from "@/components/setting";
|
SettingCard,
|
||||||
|
SettingSelect,
|
||||||
|
SettingBadge,
|
||||||
|
PageHeader,
|
||||||
|
SectionTitle,
|
||||||
|
} from "@/components/setting";
|
||||||
|
|
||||||
|
const languageOptions = [
|
||||||
|
{ value: "zh-CN", label: "简体中文" },
|
||||||
|
{ value: "en-US", label: "English" },
|
||||||
|
];
|
||||||
|
|
||||||
export function LangSetting() {
|
export function LangSetting() {
|
||||||
|
const language = useConfigStore((s) => s.config.app?.language ?? "zh-CN");
|
||||||
|
const setApp = useConfigStore((s) => s.setApp);
|
||||||
|
|
||||||
|
const handleChange = (v: string) => {
|
||||||
|
setApp({ language: v });
|
||||||
|
document.documentElement.lang = v;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<PageHeader title="语言" desc="选择启动器界面的显示语言与地区偏好" />
|
<PageHeader title="语言" desc="选择启动器界面的显示语言与地区偏好" />
|
||||||
<EmptyState className="py-16">
|
|
||||||
<Languages className="w-10 h-10 text-muted-foreground/30" />
|
<div className="space-y-6">
|
||||||
<p className="text-sm text-muted-foreground mt-3">该功能正在开发中</p>
|
<div>
|
||||||
</EmptyState>
|
<SectionTitle>显示语言</SectionTitle>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<SettingCard>
|
||||||
|
<SettingSelect
|
||||||
|
label="界面语言"
|
||||||
|
desc="切换后保存偏好并更新页面 lang 属性"
|
||||||
|
value={language}
|
||||||
|
options={languageOptions}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</SettingCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<SectionTitle>语言包</SectionTitle>
|
||||||
|
<SettingCard>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<p className="text-sm font-medium text-foreground">语言包开发中</p>
|
||||||
|
<SettingBadge variant="warning">开发中</SettingBadge>
|
||||||
|
</div>
|
||||||
|
<p className="text-[13px] text-muted-foreground mt-0.5">
|
||||||
|
当前界面文案暂为简体中文。所选语言偏好会被保存,后续语言包上线后自动生效。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,14 +12,15 @@ function ThemePreviewCard({ mode, selected, onClick }: { mode: DarkMode; selecte
|
|||||||
const label = isAuto ? "跟随系统" : isDark ? "深色模式" : "浅色模式";
|
const label = isAuto ? "跟随系统" : isDark ? "深色模式" : "浅色模式";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
onClick={onClick}
|
variant="ghost"
|
||||||
|
onPress={onClick}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"relative rounded-md p-1 transition-all duration-200",
|
"relative rounded-md p-1 transition-all duration-200 h-auto min-w-0",
|
||||||
"border-2",
|
"border-2",
|
||||||
selected
|
selected
|
||||||
? "border-primary ring-2 ring-primary/20"
|
? "!border-primary ring-2 ring-primary/20"
|
||||||
: "border-transparent hover:border-muted-foreground/20",
|
: "!border-transparent hover:!border-muted-foreground/20",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="relative w-[140px] h-[96px] rounded overflow-hidden">
|
<div className="relative w-[140px] h-[96px] rounded overflow-hidden">
|
||||||
@@ -77,7 +78,7 @@ function ThemePreviewCard({ mode, selected, onClick }: { mode: DarkMode; selecte
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[11px] text-center mt-1.5 text-muted-foreground">{label}</p>
|
<p className="text-[11px] text-center mt-1.5 text-muted-foreground">{label}</p>
|
||||||
</button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +171,7 @@ export function ThemeBgSetting() {
|
|||||||
|
|
||||||
<SettingCard>
|
<SettingCard>
|
||||||
<SettingRow label="背景图片视差" desc="背景图片随窗口滚动产生视差位移">
|
<SettingRow label="背景图片视差" desc="背景图片随窗口滚动产生视差位移">
|
||||||
<Switch isSelected={parallax} onValueChange={setParallax}>
|
<Switch isSelected={parallax} onChange={setParallax}>
|
||||||
<Switch.Control>
|
<Switch.Control>
|
||||||
<Switch.Thumb />
|
<Switch.Thumb />
|
||||||
</Switch.Control>
|
</Switch.Control>
|
||||||
|
|||||||
@@ -1,15 +1,57 @@
|
|||||||
import { EmptyState } from "@heroui/react";
|
import { useConfigStore } from "@/stores/configStore";
|
||||||
import { Monitor } from "lucide-react";
|
import {
|
||||||
import { PageHeader } from "@/components/setting";
|
SettingCard,
|
||||||
|
SettingSwitch,
|
||||||
|
PageHeader,
|
||||||
|
SectionTitle,
|
||||||
|
} from "@/components/setting";
|
||||||
|
|
||||||
export function UiSetting() {
|
export function UiSetting() {
|
||||||
|
const ui = useConfigStore((s) => s.config.ui);
|
||||||
|
const setUi = useConfigStore((s) => s.setUi);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<PageHeader title="主界面" desc="自定义启动器主界面的布局、模块显示与交互方式" />
|
<PageHeader title="主界面" desc="自定义启动器主界面的元素显示与交互方式" />
|
||||||
<EmptyState className="py-16">
|
|
||||||
<Monitor className="w-10 h-10 text-muted-foreground/30" />
|
<div className="space-y-6">
|
||||||
<p className="text-sm text-muted-foreground mt-3">该功能正在开发中</p>
|
<div>
|
||||||
</EmptyState>
|
<SectionTitle>界面元素</SectionTitle>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<SettingCard>
|
||||||
|
<SettingSwitch
|
||||||
|
label="首页实例标题"
|
||||||
|
desc="在首页左下角显示当前实例的大标题(点击可进入实例管理)"
|
||||||
|
checked={ui?.showInstanceTitle ?? true}
|
||||||
|
onChange={(v) => setUi({ showInstanceTitle: v })}
|
||||||
|
/>
|
||||||
|
</SettingCard>
|
||||||
|
|
||||||
|
<SettingCard>
|
||||||
|
<SettingSwitch
|
||||||
|
label="任务队列按钮"
|
||||||
|
desc="在标题栏右侧显示任务队列入口(安装/下载进度)"
|
||||||
|
checked={ui?.showTaskButton ?? true}
|
||||||
|
onChange={(v) => setUi({ showTaskButton: v })}
|
||||||
|
/>
|
||||||
|
</SettingCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<SectionTitle>说明</SectionTitle>
|
||||||
|
<SettingCard>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<p className="text-[13px] text-muted-foreground">
|
||||||
|
背景图片 / 模糊 / 透明度与视差效果请在「主题与背景」中调整。
|
||||||
|
</p>
|
||||||
|
<p className="text-[13px] text-muted-foreground">
|
||||||
|
动画减弱、透明度减弱与高对比度请在「辅助功能」中调整。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</SettingCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-41
@@ -1,8 +1,9 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import {
|
import {
|
||||||
getConfig,
|
getConfig,
|
||||||
saveConfig,
|
updateConfig,
|
||||||
type AppConfig,
|
type AppConfig,
|
||||||
|
type AppInfoConfig,
|
||||||
type ThemeConfig,
|
type ThemeConfig,
|
||||||
type A11yConfig,
|
type A11yConfig,
|
||||||
type BackgroundConfig,
|
type BackgroundConfig,
|
||||||
@@ -11,19 +12,17 @@ import {
|
|||||||
type AdvancedConfig,
|
type AdvancedConfig,
|
||||||
type DownloadConfig,
|
type DownloadConfig,
|
||||||
type NetworkConfig,
|
type NetworkConfig,
|
||||||
|
type UiConfig,
|
||||||
type InstanceMeta,
|
type InstanceMeta,
|
||||||
} from "@/api/config";
|
} from "@/api/config";
|
||||||
import { DEFAULT_BG } from "@/lib/mode";
|
import { DEFAULT_BG } from "@/lib/mode";
|
||||||
|
|
||||||
let saveTimer: ReturnType<typeof setTimeout> | null = null;
|
/**
|
||||||
function debouncedSave(config: AppConfig) {
|
* 配置 store(主进程权威模型的渲染端镜像):
|
||||||
if (saveTimer) clearTimeout(saveTimer);
|
* - 启动时由 config:preload / config:get 填充
|
||||||
saveTimer = setTimeout(() => {
|
* - 所有 setX 只向主进程提交 { section, patch }(config:update),不直接写盘
|
||||||
saveConfig(config).catch((e) => {
|
* - 主进程合并后广播 config:changed,收到后以广播为准覆盖本地
|
||||||
console.error("[config] save failed:", e);
|
*/
|
||||||
});
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ConfigState {
|
interface ConfigState {
|
||||||
config: AppConfig;
|
config: AppConfig;
|
||||||
@@ -32,6 +31,8 @@ interface ConfigState {
|
|||||||
|
|
||||||
init: () => Promise<void>;
|
init: () => Promise<void>;
|
||||||
applyPreloaded: (config: AppConfig, isFirstLaunch: boolean) => void;
|
applyPreloaded: (config: AppConfig, isFirstLaunch: boolean) => void;
|
||||||
|
applyChanged: (config: AppConfig) => void;
|
||||||
|
setApp: (patch: Partial<AppInfoConfig>) => void;
|
||||||
setTheme: (patch: Partial<ThemeConfig>) => void;
|
setTheme: (patch: Partial<ThemeConfig>) => void;
|
||||||
setA11y: (patch: Partial<A11yConfig>) => void;
|
setA11y: (patch: Partial<A11yConfig>) => void;
|
||||||
setBackground: (patch: Partial<BackgroundConfig>) => void;
|
setBackground: (patch: Partial<BackgroundConfig>) => void;
|
||||||
@@ -40,6 +41,7 @@ interface ConfigState {
|
|||||||
setAdvanced: (patch: Partial<AdvancedConfig>) => void;
|
setAdvanced: (patch: Partial<AdvancedConfig>) => void;
|
||||||
setDownload: (patch: Partial<DownloadConfig>) => void;
|
setDownload: (patch: Partial<DownloadConfig>) => void;
|
||||||
setNetwork: (patch: Partial<NetworkConfig>) => void;
|
setNetwork: (patch: Partial<NetworkConfig>) => void;
|
||||||
|
setUi: (patch: Partial<UiConfig>) => void;
|
||||||
setInstances: (instances: InstanceMeta[]) => void;
|
setInstances: (instances: InstanceMeta[]) => void;
|
||||||
setOobe: (value: boolean) => void;
|
setOobe: (value: boolean) => void;
|
||||||
}
|
}
|
||||||
@@ -47,17 +49,26 @@ interface ConfigState {
|
|||||||
const DEFAULT_CONFIG: AppConfig = {
|
const DEFAULT_CONFIG: AppConfig = {
|
||||||
version: 1,
|
version: 1,
|
||||||
oobe: true,
|
oobe: true,
|
||||||
|
app: { language: "zh-CN" },
|
||||||
theme: { darkMode: "auto", parallax: true },
|
theme: { darkMode: "auto", parallax: true },
|
||||||
a11y: { reduceMotion: false, reduceTransparency: false, highContrast: false, contentBlurOpacity: 50 },
|
a11y: { reduceMotion: false, reduceTransparency: false, highContrast: false, contentBlurOpacity: 50 },
|
||||||
background: { bgType: "image", image: DEFAULT_BG, blur: 0, opacity: 100 },
|
background: { bgType: "image", image: DEFAULT_BG, blur: 0, opacity: 100 },
|
||||||
game: { gameDir: ".minecraft", resourceDir: "", savesDir: "", instancesDir: ".minecraft/instances", gameDirs: [] },
|
game: { gameDir: ".minecraft", resourceDir: "", savesDir: "", instancesDir: ".minecraft/instances", gameDirs: [] },
|
||||||
java: { javaPath: "", memMode: "auto", memGB: 4, gc: "auto", jvmArgs: "" },
|
java: { javaPath: "", memMode: "auto", memGB: 4, gc: "auto", jvmArgs: "" },
|
||||||
advanced: { afterLaunch: "close", winMode: "default", customWidth: 854, customHeight: 480, gameArgs: "", preLaunchCmd: "", debugMode: false },
|
advanced: { afterLaunch: "close", winMode: "default", customWidth: 854, customHeight: 480, gameArgs: "", preLaunchCmd: "", debugMode: false, server: { ip: "", port: 25565 } },
|
||||||
download: { fileSource: "mirror", versionSource: "mirror", threads: 16, speedLimit: 0 },
|
download: { fileSource: "mirror", versionSource: "mirror", threads: 16, speedLimit: 0 },
|
||||||
network: { securityId: { enabled: false, authUrl: "" } },
|
network: { securityId: { enabled: false, authUrl: "" } },
|
||||||
|
ui: { showInstanceTitle: true, showTaskButton: true },
|
||||||
instances: [],
|
instances: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 乐观更新本地 + 提交主进程;主进程广播回来时以广播为准(applyChanged 覆盖)
|
||||||
|
function submit(section: string, patch: unknown) {
|
||||||
|
updateConfig(section, patch).catch((e) => {
|
||||||
|
console.error(`[config] update ${section} failed:`, e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const useConfigStore = create<ConfigState>((set, get) => ({
|
export const useConfigStore = create<ConfigState>((set, get) => ({
|
||||||
config: DEFAULT_CONFIG,
|
config: DEFAULT_CONFIG,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
@@ -67,6 +78,11 @@ export const useConfigStore = create<ConfigState>((set, get) => ({
|
|||||||
set({ config, isFirstLaunch, loaded: true });
|
set({ config, isFirstLaunch, loaded: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 主进程广播的权威配置 → 整体覆盖本地镜像
|
||||||
|
applyChanged: (config) => {
|
||||||
|
set({ config, loaded: true });
|
||||||
|
},
|
||||||
|
|
||||||
init: async () => {
|
init: async () => {
|
||||||
// If already preloaded, skip IPC call
|
// If already preloaded, skip IPC call
|
||||||
if (get().loaded) return;
|
if (get().loaded) return;
|
||||||
@@ -79,73 +95,75 @@ export const useConfigStore = create<ConfigState>((set, get) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setApp: (patch) => {
|
||||||
|
const { config } = get();
|
||||||
|
set({ config: { ...config, app: { ...config.app, ...patch } } });
|
||||||
|
submit("app", patch);
|
||||||
|
},
|
||||||
|
|
||||||
setTheme: (patch) => {
|
setTheme: (patch) => {
|
||||||
const { config } = get();
|
const { config } = get();
|
||||||
const next = { ...config, theme: { ...config.theme, ...patch } };
|
set({ config: { ...config, theme: { ...config.theme, ...patch } } });
|
||||||
set({ config: next });
|
submit("theme", patch);
|
||||||
debouncedSave(next);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setA11y: (patch) => {
|
setA11y: (patch) => {
|
||||||
const { config } = get();
|
const { config } = get();
|
||||||
const next = { ...config, a11y: { ...config.a11y, ...patch } };
|
set({ config: { ...config, a11y: { ...config.a11y, ...patch } } });
|
||||||
set({ config: next });
|
submit("a11y", patch);
|
||||||
debouncedSave(next);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setBackground: (patch) => {
|
setBackground: (patch) => {
|
||||||
const { config } = get();
|
const { config } = get();
|
||||||
const next = { ...config, background: { ...config.background, ...patch } };
|
set({ config: { ...config, background: { ...config.background, ...patch } } });
|
||||||
set({ config: next });
|
submit("background", patch);
|
||||||
debouncedSave(next);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setGame: (patch) => {
|
setGame: (patch) => {
|
||||||
const { config } = get();
|
const { config } = get();
|
||||||
const next = { ...config, game: { ...config.game, ...patch } };
|
set({ config: { ...config, game: { ...config.game, ...patch } } });
|
||||||
set({ config: next });
|
submit("game", patch);
|
||||||
debouncedSave(next);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setJava: (patch) => {
|
setJava: (patch) => {
|
||||||
const { config } = get();
|
const { config } = get();
|
||||||
const next = { ...config, java: { ...config.java, ...patch } };
|
set({ config: { ...config, java: { ...config.java, ...patch } } });
|
||||||
set({ config: next });
|
submit("java", patch);
|
||||||
debouncedSave(next);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setAdvanced: (patch) => {
|
setAdvanced: (patch) => {
|
||||||
const { config } = get();
|
const { config } = get();
|
||||||
const next = { ...config, advanced: { ...config.advanced, ...patch } };
|
set({ config: { ...config, advanced: { ...config.advanced, ...patch } } });
|
||||||
set({ config: next });
|
submit("advanced", patch);
|
||||||
debouncedSave(next);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setDownload: (patch) => {
|
setDownload: (patch) => {
|
||||||
const { config } = get();
|
const { config } = get();
|
||||||
const next = { ...config, download: { ...config.download, ...patch } };
|
set({ config: { ...config, download: { ...config.download, ...patch } } });
|
||||||
set({ config: next });
|
submit("download", patch);
|
||||||
debouncedSave(next);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setNetwork: (patch) => {
|
setNetwork: (patch) => {
|
||||||
const { config } = get();
|
const { config } = get();
|
||||||
const next = { ...config, network: { ...config.network, ...patch } };
|
set({ config: { ...config, network: { ...config.network, ...patch } } });
|
||||||
set({ config: next });
|
submit("network", patch);
|
||||||
debouncedSave(next);
|
},
|
||||||
|
|
||||||
|
setUi: (patch) => {
|
||||||
|
const { config } = get();
|
||||||
|
set({ config: { ...config, ui: { ...config.ui, ...patch } } });
|
||||||
|
submit("ui", patch);
|
||||||
},
|
},
|
||||||
|
|
||||||
setInstances: (instances) => {
|
setInstances: (instances) => {
|
||||||
const { config } = get();
|
const { config } = get();
|
||||||
const next = { ...config, instances };
|
set({ config: { ...config, instances } });
|
||||||
set({ config: next });
|
submit("instances", instances);
|
||||||
debouncedSave(next);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setOobe: (value) => {
|
setOobe: (value) => {
|
||||||
const { config } = get();
|
const { config } = get();
|
||||||
const next = { ...config, oobe: value };
|
set({ config: { ...config, oobe: value } });
|
||||||
set({ config: next });
|
submit("oobe", value);
|
||||||
debouncedSave(next);
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
deleteInstance,
|
deleteInstance,
|
||||||
getInstanceInfo,
|
getInstanceInfo,
|
||||||
installInstance,
|
installInstance,
|
||||||
launchInstance,
|
|
||||||
type InstanceInfo,
|
type InstanceInfo,
|
||||||
type InstanceRuntime,
|
type InstanceRuntime,
|
||||||
} from "../api/instance";
|
} from "../api/instance";
|
||||||
@@ -32,17 +31,6 @@ interface InstanceState {
|
|||||||
remove: (name: string, gamePath: string) => Promise<void>;
|
remove: (name: string, gamePath: string) => Promise<void>;
|
||||||
select: (name: string, gamePath: string) => Promise<void>;
|
select: (name: string, gamePath: string) => Promise<void>;
|
||||||
install: (name: string, gamePath: string) => Promise<string>;
|
install: (name: string, gamePath: string) => Promise<string>;
|
||||||
launch: (
|
|
||||||
name: string,
|
|
||||||
gamePath: string,
|
|
||||||
options: {
|
|
||||||
username: string;
|
|
||||||
uuid: string;
|
|
||||||
accessToken?: string;
|
|
||||||
javaPath?: string;
|
|
||||||
server?: { host: string; port?: number };
|
|
||||||
}
|
|
||||||
) => Promise<string>;
|
|
||||||
clearError: () => void;
|
clearError: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,17 +100,5 @@ export const useInstanceStore = create<InstanceState>((set) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
launch: async (name, gamePath, options) => {
|
|
||||||
set({ loading: true, error: null });
|
|
||||||
try {
|
|
||||||
const { requestId } = await launchInstance(name, gamePath, options);
|
|
||||||
set({ loading: false });
|
|
||||||
return requestId;
|
|
||||||
} catch (e: any) {
|
|
||||||
set({ error: e.message, loading: false });
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
clearError: () => set({ error: null }),
|
clearError: () => set({ error: null }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
+36
-17
@@ -1,6 +1,7 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { launchGame, onGameEvent, diagnoseVersion } from "../api/launch";
|
import { launchGame, onGameEvent, diagnoseVersion } from "../api/launch";
|
||||||
import type { LaunchOptions, LaunchResult } from "../api/launch";
|
import type { LaunchResult, LaunchServer } from "../api/launch";
|
||||||
|
import { useAuthStore } from "./authStore";
|
||||||
|
|
||||||
interface GameEvent {
|
interface GameEvent {
|
||||||
event: string;
|
event: string;
|
||||||
@@ -8,13 +9,19 @@ interface GameEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface LaunchState {
|
interface LaunchState {
|
||||||
|
/** 正在发起启动请求 */
|
||||||
launching: boolean;
|
launching: boolean;
|
||||||
launched: boolean;
|
/** 游戏进程正在运行 */
|
||||||
|
running: boolean;
|
||||||
gameResult: LaunchResult | null;
|
gameResult: LaunchResult | null;
|
||||||
events: GameEvent[];
|
events: GameEvent[];
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
|
||||||
launch: (options: LaunchOptions) => Promise<void>;
|
/**
|
||||||
|
* 统一启动入口:指定实例 + 游戏根目录(可选快速联机)。
|
||||||
|
* 账户档案从 authStore 自动获取;启动参数(Java/内存/GC/窗口等)由主进程读取权威配置自动应用。
|
||||||
|
*/
|
||||||
|
launch: (instanceName: string, gamePath: string, server?: LaunchServer) => Promise<void>;
|
||||||
diagnose: (gamePath: string, version: string) => Promise<void>;
|
diagnose: (gamePath: string, version: string) => Promise<void>;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
clearError: () => void;
|
clearError: () => void;
|
||||||
@@ -22,30 +29,42 @@ interface LaunchState {
|
|||||||
|
|
||||||
export const useLaunchStore = create<LaunchState>((set) => ({
|
export const useLaunchStore = create<LaunchState>((set) => ({
|
||||||
launching: false,
|
launching: false,
|
||||||
launched: false,
|
running: false,
|
||||||
gameResult: null,
|
gameResult: null,
|
||||||
events: [],
|
events: [],
|
||||||
error: null,
|
error: null,
|
||||||
|
|
||||||
launch: async (options: LaunchOptions) => {
|
launch: async (instanceName: string, gamePath: string, server?: LaunchServer) => {
|
||||||
set({ launching: true, error: null, events: [] });
|
const user = useAuthStore.getState().user;
|
||||||
|
if (!user?.username || !user?.uuid) {
|
||||||
|
set({ error: "请先在设置中登录账号", launching: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
set({ launching: true, running: false, error: null, events: [], gameResult: null });
|
||||||
try {
|
try {
|
||||||
const result = await launchGame(options);
|
const result = await launchGame({
|
||||||
set({ gameResult: result, launching: false, launched: true });
|
instanceName,
|
||||||
|
gamePath,
|
||||||
// Listen for game events
|
profile: {
|
||||||
const unlisten = await onGameEvent(result.requestId, (event) => {
|
username: user.username,
|
||||||
set((state) => ({
|
uuid: user.uuid,
|
||||||
events: [...state.events, event],
|
accessToken: user.accessToken || undefined,
|
||||||
}));
|
},
|
||||||
|
server,
|
||||||
|
});
|
||||||
|
set({ gameResult: result, launching: false, running: true });
|
||||||
|
|
||||||
|
// 订阅事件流(stdout / stderr / window-ready / exit)
|
||||||
|
const unlisten = onGameEvent(result.requestId, (event) => {
|
||||||
|
set((state) => ({ events: [...state.events, event] }));
|
||||||
if (event.event === "exit") {
|
if (event.event === "exit") {
|
||||||
set({ launched: false });
|
set({ running: false });
|
||||||
unlisten();
|
unlisten();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
set({ error: e.message, launching: false });
|
set({ error: e?.message || String(e), launching: false, running: false });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -58,7 +77,7 @@ export const useLaunchStore = create<LaunchState>((set) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
reset: () => {
|
reset: () => {
|
||||||
set({ launching: false, launched: false, gameResult: null, events: [] });
|
set({ launching: false, running: false, gameResult: null, events: [], error: null });
|
||||||
},
|
},
|
||||||
|
|
||||||
clearError: () => set({ error: null }),
|
clearError: () => set({ error: null }),
|
||||||
|
|||||||
Vendored
+2
@@ -13,6 +13,8 @@ interface ElectronAPI {
|
|||||||
|
|
||||||
onConfigPreload: (callback: (data: { config: unknown; isFirstLaunch: boolean }) => void) => () => void;
|
onConfigPreload: (callback: (data: { config: unknown; isFirstLaunch: boolean }) => void) => () => void;
|
||||||
|
|
||||||
|
onConfigChanged: (callback: (config: unknown) => void) => () => void;
|
||||||
|
|
||||||
openExternal: (url: string) => Promise<void>;
|
openExternal: (url: string) => Promise<void>;
|
||||||
|
|
||||||
// Crash monitoring
|
// Crash monitoring
|
||||||
|
|||||||
Reference in New Issue
Block a user