Repository Wiki
isHarryh/Ark-Pets

后台任务与下载对话框

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 承担三项职责:

  1. 呈现并切换下载源:根据本地是否已保存 CDK(app.config.getMcCdk() != null)决定显示 Mirror 酱指示器还是公共源指示器;用户点击 psConfirm 即切回公共源(将 CDK 置空并保存)。
  2. CDK 验证与持久化:对用户输入做本地正则校验(长度 4–256 的 \w- 字符),然后启动后台任务 McCheckAppUpdateTask 向 Mirror 酱接口发起校验;通过后把 CDK 写回本地加密配置。
  3. 衔接后续流程:对话框可携带一个 Runnable(afterConfirm)被弹出,CDK 验证成功后经确认弹窗继续执行该回调——这是 ModelsModule 在下载模型前"二次确认数据源"的实现手段。

关键设计意图:

  • UI 线程零阻塞:网络校验被封装为 GuiTask 后台任务(McCheckAppUpdateTask),通过 onReceivedData(JSONObject) 回调把结果交还 UI 层,避免在 JavaFX Application Thread 上执行 HTTP 请求。
  • 状态外露:DownloadDialog 通过 BooleanProperty isMc 把"当前是否为 Mirror 酱源"暴露为可观察属性,SettingsModule 直接监听它来更新设置页的网络源文案,避免轮询或重复读取配置。
  • 验证即保存:只有服务端校验通过的 CDK 才会被 setMcCdk + save() 持久化,防止无效凭证污染本地配置。

Architecture

Loading diagram...

架构要点说明:

  • 注册与弹出分离: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 → 后台校验 → 保存 → 继续流程" 过程。

Loading diagram...

流程细节与设计意图:

  1. 入口多样化:mcCdkInput.setOnKeyPressed 监听回车、mcConfirm.setOnAction 监听按钮,两条路径都收敛到同一个 submitMcCdk(),避免重复逻辑。
  2. 空输入复用旧 CDK:submitMcCdk() 里 String cdk = input.isEmpty() && oldCdk != null ? oldCdk : input;——当输入为空但本地已有 CDK 时,会拿旧 CDK 重新验证(配合提示语 "点击下方按钮以验证当前 CDK"),方便用户主动刷新凭证状态或过期时间。
  3. 轻量级本地校验先行:cdkPattern = "[\\w\\-]{4,256}" 在发起网络请求前过滤掉格式明显非法的输入,减少无效请求;命中失败时仅清空输入并写 debug 日志,不打扰用户。
  4. 回调中的异常分类处理:onReceivedData 里区分 McQueryVersion.McException(业务层错误,CDK 无效)与 GeneralSecurityException(本地保存失败),分别弹出错误对话框并记录 error 日志,互不掩盖。
  5. 成功后的两种呈现:若 afterConfirm != null(即由模型下载流程触发),弹出带"是否继续下一步?"的确认弹窗,用户确认后才执行回调;否则仅弹出普通成功提示。这保证 CDK 验证与业务流程解耦,对话框可独立用于设置场景。

数据源切换:initPs()

选择公共源是一条更短的路径:点击 psConfirm 后同步执行 setMcCdk("") + save() + isMc.setValue(false),然后立即触发返回回调并执行 afterConfirm。GeneralSecurityException 在此被忽略(吞掉异常)——因为置空 CDK 失败并不影响用户继续使用公共源。

Usage Examples

弹出下载对话框并携带后续回调(ModelsModule 中的真实用法)

模型下载流程在"二次确认"场景下按名字弹出对话框,并把要继续的动作作为 Runnable 传入:

java
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)

java
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)

java
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 核心)

java
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)

java
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)

java
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 / LabelMirror 酱区域控件
psIndicator / psConfirm@FXML Label / Button公共源区域控件
afterConfirmprivate Runnable确认后要执行的回调(可空)
isMcprivate BooleanProperty当前是否为 Mirror 酱源的可观察状态
cdkPatternprivate 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 中的使用方式(匿名子类):

java
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.McExceptionLogger.error + GuiPrefabs.Dialogs.createErrorDialog(...).show();不保存 CDK,isMc 不变
本地加密保存失败app.config.save() 抛 GeneralSecurityExceptionLogger.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,可全局统一调整。
  • 源码:DownloadDialog.java
  • 源码:DownloadDialog.fxml
  • 源码:RootModule.java(对话框注册)
  • 源码:ModelsModule.java(弹出对话框并携带回调)
  • 源码:SettingsModule.java(监听 isMc 属性)
  • 相关主题:模型列表加载与资源下载流程(ModelsModule.assertDownloadSource)→ 见模型管理页面
  • 相关主题:网络层 Mirror 酱 API(McQueryVersion)→ 见网络与数据源页面
  • 相关主题:GuiTask 后台任务框架与 GuiTaskStyle → 见 guitasks 框架页面
  • 相关主题:本地配置加密存储(ArkConfig)→ 见配置系统页面

Sources

(1 files)