后台任务与下载对话框
Ark-Pets 桌面端启动器在下载模型资源时支持两种数据源(公共源 / Mirror 酱),本页讲解承载该选择的 DownloadDialog(下载对话框),以及它如何借助启动器的 GuiTask 后台任务框架 在不阻塞 JavaFX UI 线程的前提下完成 CDK 网络校验。
Purpose and Scope
本页覆盖以下内容(围绕目录 7-launcher-gui/7.4-background-tasks-and-dialogs):
DownloadDialog控制器的完整实现:对话框生命周期、公共源切换、Mirror 酱 CDK 的输入校验、网络验证与持久化。DownloadDialog与启动器对话框管理器(app.dialogs的registerDialog/popDialog/getDialogController)的协作方式。DownloadDialog如何通过McCheckAppUpdateTask(GuiTask的匿名子类 +onReceivedData回调 +start())执行后台网络任务,并用GuiPrefabs.Dialogs呈现确认/错误弹窗。
以下相关主题有意留给兄弟页面,本页仅在必要处引用:
- 对话框管理器与各对话框(公告、日志等)的通用注册/弹出框架本身 —— 见启动器 GUI 相关页面。
- 模型列表加载与资源下载的完整流程(
ModelsModule的assertDownloadSource等)—— 见模型管理相关页面。 - 网络层
McQueryVersion/ Mirror 酱 API 的请求构造细节 —— 见网络与数据源相关页面。 - 启动器配置文件(
ArkConfig)的加密存储机制 —— 见配置系统相关页面。
Overview
Ark-Pets 的模型资源(干员模型等)可以来自两种下载源:
| 数据源 | 标识 | 说明 |
|---|---|---|
| 公共源 | psIndicator / psConfirm | 免费公共镜像,无需凭证 |
| Mirror 酱 | mcIndicator / mcPurchase / mcCdkInput / mcConfirm | 付费加速源,需要输入 CDK(授权码)验证后方可使用 |
DownloadDialog 承担三项职责:
- 呈现并切换下载源:根据本地是否已保存 CDK(
app.config.getMcCdk() != null)决定显示 Mirror 酱指示器还是公共源指示器;用户点击psConfirm即切回公共源(将 CDK 置空并保存)。 - CDK 验证与持久化:对用户输入做本地正则校验(长度 4–256 的
\w-字符),然后启动后台任务McCheckAppUpdateTask向 Mirror 酱接口发起校验;通过后把 CDK 写回本地加密配置。 - 衔接后续流程:对话框可携带一个
Runnable(afterConfirm)被弹出,CDK 验证成功后经确认弹窗继续执行该回调——这是ModelsModule在下载模型前"二次确认数据源"的实现手段。
关键设计意图:
- UI 线程零阻塞:网络校验被封装为
GuiTask后台任务(McCheckAppUpdateTask),通过onReceivedData(JSONObject)回调把结果交还 UI 层,避免在 JavaFX Application Thread 上执行 HTTP 请求。 - 状态外露:
DownloadDialog通过BooleanProperty isMc把"当前是否为 Mirror 酱源"暴露为可观察属性,SettingsModule直接监听它来更新设置页的网络源文案,避免轮询或重复读取配置。 - 验证即保存:只有服务端校验通过的 CDK 才会被
setMcCdk+save()持久化,防止无效凭证污染本地配置。
Architecture
架构要点说明:
- 注册与弹出分离:
RootModule在初始化时调用app.dialogs.registerDialog("downloadDialog", body, backgroundNodes, "/UI/DownloadDialog.fxml")完成一次性注册;业务模块(如ModelsModule)随后按名字popDialog("downloadDialog", data)弹出并传入载荷(这里是后续要执行的Runnable)。控件与控制器由DownloadDialog.fxml通过@FXML注入绑定。 - 回调式后台任务:
McCheckAppUpdateTask以匿名类形式在DownloadDialog内创建,构造参数为(app.body, GuiTaskStyle.COMMON, cdk),随后.start()启动。任务完成时回调onReceivedData(JSONObject json),控制器在该回调里完成 JSON→McQueryVersion转换、错误码检查、配置保存与弹窗提示。 - 状态广播:
isMc是SimpleBooleanProperty,SettingsModule通过getIsMcProperty()订阅它,实时把设置页configNetworkSource文案切换为 "Mirror 酱" 或 "公共源"。
说明:
GuiTask基类、McCheckAppUpdateTask与app.dialogs管理器的内部实现未包含在本页读取的源文件中;上图中与其相关的边(start()、onReceivedData、registerDialog、popDialog)均依据DownloadDialog.java及各调用点中的真实调用签名描述,未对其内部行为做推测。
Core Flow:CDK 验证与数据源切换
下面按真实控制流梳理一次完整的 "输入 CDK → 后台校验 → 保存 → 继续流程" 过程。
流程细节与设计意图:
- 入口多样化:
mcCdkInput.setOnKeyPressed监听回车、mcConfirm.setOnAction监听按钮,两条路径都收敛到同一个submitMcCdk(),避免重复逻辑。 - 空输入复用旧 CDK:
submitMcCdk()里String cdk = input.isEmpty() && oldCdk != null ? oldCdk : input;——当输入为空但本地已有 CDK 时,会拿旧 CDK 重新验证(配合提示语 "点击下方按钮以验证当前 CDK"),方便用户主动刷新凭证状态或过期时间。 - 轻量级本地校验先行:
cdkPattern = "[\\w\\-]{4,256}"在发起网络请求前过滤掉格式明显非法的输入,减少无效请求;命中失败时仅清空输入并写 debug 日志,不打扰用户。 - 回调中的异常分类处理:
onReceivedData里区分McQueryVersion.McException(业务层错误,CDK 无效)与GeneralSecurityException(本地保存失败),分别弹出错误对话框并记录 error 日志,互不掩盖。 - 成功后的两种呈现:若
afterConfirm != null(即由模型下载流程触发),弹出带"是否继续下一步?"的确认弹窗,用户确认后才执行回调;否则仅弹出普通成功提示。这保证 CDK 验证与业务流程解耦,对话框可独立用于设置场景。
数据源切换:initPs()
选择公共源是一条更短的路径:点击 psConfirm 后同步执行 setMcCdk("") + save() + isMc.setValue(false),然后立即触发返回回调并执行 afterConfirm。GeneralSecurityException 在此被忽略(吞掉异常)——因为置空 CDK 失败并不影响用户继续使用公共源。
Usage Examples
弹出下载对话框并携带后续回调(ModelsModule 中的真实用法)
模型下载流程在"二次确认"场景下按名字弹出对话框,并把要继续的动作作为 Runnable 传入:
1// ModelsModule.java — doDoubleCheck 为真时,先让用户确认/配置数据源,再继续 onDone
2if (doDoubleCheck)
3 app.dialogs.popDialog("downloadDialog", (Runnable) () -> assertDownloadSource(false, onDone));
4else if (onDone != null)
5 onDone.run();Source: ModelsModule.java
popDialog 的第二个参数最终会以 Object data 的形式到达 notifyDialogOpened,因此同一处调用也兼容其他数据类型(当前实现只接受 Runnable,否则抛出 IllegalArgumentException)。
Source: DownloadDialog.java
启动时注册对话框(RootModule)
app.dialogs.registerDialog("announceDialog", body, backgroundNodes, "/UI/AnnounceDialog.fxml");
app.dialogs.registerDialog("downloadDialog", body, backgroundNodes, "/UI/DownloadDialog.fxml");
app.dialogs.registerDialog("logDialog", body, backgroundNodes, "/UI/LogDialog.fxml");Source: RootModule.java
注册发生在启动阶段;backgroundNodes 参数用于在对话框打开时对背景节点做统一的视觉处理(如禁用/变暗),由对话框管理器统一管理。
对话框打开时按本地 CDK 状态刷新 UI(notifyDialogOpened)
1@Override
2public void notifyDialogOpened(Object data) {
3 afterConfirm = null;
4 if (data != null) {
5 if (data instanceof Runnable runnable) {
6 // The runnable will be started if any confirm action is triggered
7 afterConfirm = runnable;
8 } else {
9 throw new IllegalArgumentException("Invalid data type");
10 }
11 }
12 if (app.config.getMcCdk() != null) {
13 isMc.setValue(true);
14 mcIndicator.setManaged(true);
15 mcIndicator.setVisible(true);
16 psIndicator.setManaged(false);
17 psIndicator.setVisible(false);
18 mcCdkInput.setPromptText("点击下方按钮以验证当前 CDK");
19 } else {
20 isMc.setValue(false);
21 mcIndicator.setManaged(false);
22 mcIndicator.setVisible(false);
23 psIndicator.setManaged(true);
24 psIndicator.setVisible(true);
25 mcCdkInput.setPromptText("请在这里输入或粘贴 CDK");
26 }
27}Source: DownloadDialog.java
注意 setManaged 与 setVisible 同时设置:JavaFX 中仅 setVisible(false) 仍会保留布局占位,成对调用才能让指示器真正收起,避免对话框内出现空白区域。
发起 CDK 后台校验任务并处理回调(submitMcCdk 核心)
1Logger.info("DownloadDialog", "Verifying CDK");
2new McCheckAppUpdateTask(app.body, GuiTaskStyle.COMMON, cdk) {
3 @Override
4 protected void onReceivedData(JSONObject json) {
5 McQueryVersion value = json.toJavaObject(McQueryVersion.class);
6 try {
7 value.raiseForCode();
8 app.config.setMcCdk(cdk);
9 app.config.save();
10 isMc.setValue(true);
11
12 String exp = StringUtils.getSimpleTimeString(value.getCDKExpiredTime());
13 Logger.info("DownloadDialog", "Accepted CDK, expires at: " + exp);
14 mcCdkInput.clear();
15
16 if (afterConfirm != null) {
17 GuiPrefabs.Dialogs.createConfirmDialog(app.body,
18 GuiPrefabs.Icons.getIcon(GuiPrefabs.Icons.SVG_SUCCESS_ALT, GuiPrefabs.COLOR_SUCCESS),
19 "Mirror 酱准备就绪",
20 "CDK 验证完成!是否继续下一步?",
21 "CDK 会在本地加密存储,此 CDK 的有效期至 " + exp,
22 afterConfirm).show();
23 } else {
24 GuiPrefabs.Dialogs.createCommonDialog(app.body,
25 GuiPrefabs.Icons.getIcon(GuiPrefabs.Icons.SVG_SUCCESS_ALT, GuiPrefabs.COLOR_SUCCESS),
26 "Mirror 酱准备就绪",
27 "CDK 验证完成!以后下载资源会自动采用此 CDK",
28 "CDK 会在本地加密存储,此 CDK 的有效期至 " + exp,
29 null).show();
30 }
31
32 triggerReturnActionCallback(new ActionEvent(this, null));
33 } catch (McQueryVersion.McException e) {
34 Logger.error("DownloadDialog", "Invalid CDK, " + e.getMessage());
35 GuiPrefabs.Dialogs.createErrorDialog(app.body, e).show();
36 } catch (GeneralSecurityException e) {
37 Logger.error("DownloadDialog", "Failed to save CDK");
38 GuiPrefabs.Dialogs.createErrorDialog(app.body, e).show();
39 }
40 }
41}.start();Source: DownloadDialog.java
要点:McCheckAppUpdateTask 的任务样式为 GuiTaskStyle.COMMON(通用样式),任务接收原始 JSONObject,由控制器负责反序列化到 McQueryVersion 并调用 raiseForCode() 检查业务错误码;getCDKExpiredTime() 提供的到期时间既用于日志,也用于给用户展示。
跳转 Mirror 酱购买页(initMc)
1mcPurchase.setOnMouseClicked(e -> app.popBrowser(
2 new StringUtils.URLStringBuilder(Const.PathConfig.urlMirrorChyan)
3 .addQuery("source", Const.mirrorChyanAID)
4 .toString()
5));Source: DownloadDialog.java
购买链接通过 StringUtils.URLStringBuilder 拼接 source=mirrorChyanAID 查询参数(推荐来源追踪),再用 app.popBrowser 唤起系统浏览器打开。
订阅数据源状态(SettingsModule)
1private void initNetwork() {
2 BooleanProperty isMc = ((DownloadDialog) app.dialogs.getDialogController("downloadDialog")).getIsMcProperty();
3 isMc.addListener(observable -> configNetworkSource.setText(isMc.get() ? "Mirror 酱" : "公共源"));
4}Source: SettingsModule.java
设置页通过 getDialogController("downloadDialog") 取回已注册的控制器实例并强制转型为 DownloadDialog,随后监听 isMc 属性——数据源切换会立即反映到设置页文案,无需重新读取配置文件。
API Reference
以下签名均取自实际源码。
DownloadDialog implements DialogController<ArkHomeFX>
desktop/src/cn/harryh/arkpets/controllers/DownloadDialog.java
控制器为 final 类,FXML 控件通过 @FXML 注入。
| 成员 / 方法 | 类型 / 签名 | 说明 |
|---|---|---|
dialog | @FXML AnchorPane | 对话框根容器 |
dialogReturn | @FXML Button | 返回按钮 |
mcIndicator / mcCdkInput / mcConfirm / mcPurchase | @FXML Label / TextField / Button / Label | Mirror 酱区域控件 |
psIndicator / psConfirm | @FXML Label / Button | 公共源区域控件 |
afterConfirm | private Runnable | 确认后要执行的回调(可空) |
isMc | private BooleanProperty | 当前是否为 Mirror 酱源的可观察状态 |
cdkPattern | private static final Pattern | [\w\-]{4,256},CDK 本地格式校验 |
initializeWith(app) | void initializeWith(ArkHomeFX) | 注入应用实例,初始化 isMc(初值 = config.getMcCdk() != null),并调用 initPs() / initMc() 绑定事件 |
getDialogPane() | AnchorPane getDialogPane() | 返回对话框根节点(DialogController 接口方法) |
getReturnButton() | Button getReturnButton() | 返回返回按钮(DialogController 接口方法) |
notifyDialogOpen(Object) | void notifyDialogOpened(Object data) | 每次弹出时调用:接收 Runnable(否则抛 IllegalArgumentException("Invalid data type"))、重置 afterConfirm、按本地 CDK 状态刷新指示器与输入提示 |
getIsMcProperty() | BooleanProperty getIsMcProperty() | 暴露 isMc 供外部(设置页)监听 |
initPs() | private void initPs() | 绑定公共源确认按钮:清空并保存 CDK、置 isMc=false、触发返回回调、执行 afterConfirm |
initMc() | private void initMc() | 绑定购买跳转、回车提交、确认按钮提交 |
submitMcCdk() | private void submitMcCdk() | 本地校验并启动 McCheckAppUpdateTask 后台验证 |
Source: DownloadDialog.java
McCheckAppUpdateTask(后台任务,回调式)
在 DownloadDialog 中的使用方式(匿名子类):
1new McCheckAppUpdateTask(app.body, GuiTaskStyle.COMMON, cdk) {
2 @Override
3 protected void onReceivedData(JSONObject json) { /* 见上文示例 */ }
4}.start();Source: DownloadDialog.java
- 构造参数:
app.body(承载任务 UI 的窗口/面板)、GuiTaskStyle.COMMON(任务展示样式)、cdk(待验证字符串)。 - 回调:
onReceivedData(JSONObject json)—— 任务收到响应 JSON 后回调;由调用方自行json.toJavaObject(McQueryVersion.class)并raiseForCode()。 - 启动:
.start(),非阻塞,UI 线程立即返回。 - 其基类
GuiTask与GuiTaskStyle的完整实现(源文件desktop/src/cn/harryh/arkpets/guitasks/)未包含在本页读取范围内,此处仅文档化DownloadDialog实际依赖的调用契约。
对话框管理器调用契约(app.dialogs)
| 调用 | 出现位置 | 说明 |
|---|---|---|
registerDialog("downloadDialog", body, backgroundNodes, "/UI/DownloadDialog.fxml") | RootModule 启动初始化 | 按名字注册对话框及其 FXML、背景节点 |
popDialog("downloadDialog", data) | ModelsModule | 按名字弹出,data 传给 notifyDialogOpened |
getDialogController("downloadDialog") | SettingsModule | 按名字取回控制器实例 |
Source: RootModule.java · Sources: ModelsModule.java · SettingsModule.java
Failure Modes, Edge Cases & Concurrency
依据源码可确认的边界与失败处理:
| 场景 | 触发点 | 行为 |
|---|---|---|
| 空 CDK 输入(且本地无旧 CDK) | submitMcCdk() | 清空输入,Logger.debug("Empty CDK input"),直接返回,不发起网络请求 |
CDK 格式非法(不匹配 [\w\-]{4,256}) | submitMcCdk() | 清空输入,Logger.debug("Invalid CDK input"),直接返回 |
| 服务端返回业务错误码 | value.raiseForCode() 抛 McQueryVersion.McException | Logger.error + GuiPrefabs.Dialogs.createErrorDialog(...).show();不保存 CDK,isMc 不变 |
| 本地加密保存失败 | app.config.save() 抛 GeneralSecurityException | Logger.error("Failed to save CDK") + 错误弹窗;此时 isMc 已被置 true 之前即抛出——见下方并发说明 |
popDialog 传入非 Runnable 数据 | notifyDialogOpened | 抛出 IllegalArgumentException("Invalid data type")(快速失败,暴露调用方 bug) |
| 选择公共源时保存失败 | initPs() 的 catch (GeneralSecurityException ignored) | 异常被显式忽略(注释级别的设计取舍:切换公共源失败不阻断流程),随后照常执行返回回调与 afterConfirm |
| 重复弹出对话框 | notifyDialogOpened 每次都先 afterConfirm = null 再重新赋值 | 保证上一次未消费的回调不会泄漏到下一次打开 |
并发 / 线程模型说明(基于源码可见证据):
McCheckAppUpdateTask.start()是异步的;onReceivedData回调中直接操作了mcCdkInput.clear()、isMc.setValue(true)以及多个GuiPrefabs.Dialogs弹窗调用,从代码写法看其契约是回调发生在 JavaFX UI 线程(否则这些 UI 操作会抛出 FX 线程异常)。GuiTask基类如何在 worker 线程与 FX 线程之间切换未在本页读取范围内,实现细节请参见 guitasks 框架页面。- 回调内对
app.config的写入与isMc属性更新为顺序执行;BooleanProperty的监听器(如设置页)在setValue后立即触发,因此设置页文案与对话框状态保持同步。 - 源码中未对
mcConfirm/ 回车提交做防重入处理——理论上网络校验进行中再次点击可并发启动第二个任务。实际影响有限(后完成者覆盖isMc),但这是扩展时的注意点。
Extension Points
- 新增数据源:当前对话框把"源"抽象成
isMc一个布尔值(true= Mirror 酱,false= 公共源)。若要接入第三种源,需要把BooleanProperty升级为承载源枚举的属性,并同步修改SettingsModule的监听逻辑与 FXML 中的指示器/控件区。 - 自定义后续动作:任何调用方都可以在
popDialog("downloadDialog", runnable)时注入自己的Runnable,无需修改DownloadDialog本身——这是该对话框最主要的扩展入口(模型下载流程即通过它衔接assertDownloadSource)。 - 调整 CDK 约束:
cdkPattern是私有静态常量,修改校验规则(长度/字符集)只需改这一处正则。 - 弹窗文案与图标:成功提示使用
GuiPrefabs.Icons.SVG_SUCCESS_ALT与GuiPrefabs.COLOR_SUCCESS,颜色/图标集中在GuiPrefabs,可全局统一调整。
Related Links
- 源码:DownloadDialog.java
- 源码:DownloadDialog.fxml
- 源码:RootModule.java(对话框注册)
- 源码:ModelsModule.java(弹出对话框并携带回调)
- 源码:SettingsModule.java(监听
isMc属性) - 相关主题:模型列表加载与资源下载流程(
ModelsModule.assertDownloadSource)→ 见模型管理页面 - 相关主题:网络层 Mirror 酱 API(
McQueryVersion)→ 见网络与数据源页面 - 相关主题:
GuiTask后台任务框架与GuiTaskStyle→ 见 guitasks 框架页面 - 相关主题:本地配置加密存储(
ArkConfig)→ 见配置系统页面