Files
Unlucky-Launcher/crates/app/ui/widgets/wizard.slint
T
AskaEth d95c9664c6 fix(app): M45 r1 两条 P2(向导拒装文案 + 处理器段进度满格)
P2-1:wizard.slint 的 Forge/NeoForge 提示还写着「需要 installer processors:本轮请用 ull install」,
改成「需要 installer processors(首装会外调 java,约 1–3 分钟)」;全仓再扫一遍
「ull install / 显式拒装 / 终端跑」等措辞,user-facing 文案已无残留(余下命中全是
OptiFine/LiteLoader 的正确说明与测试注释)。

P2-2:进度条在处理器依赖库下完那一刻就满格(36/36),随后加载器库的 29 个文件才被计入
→ 100% 回落再涨回,且 bytes_total 用 .max() 与文件数对不上。
- Meter 新增 expect(files, bytes):文件数与字节都 saturating_add;
- install_loader 在处理器段之前就把加载器库的文件/字节一次预记账(两段之和),
  加载器库阶段只剩切阶段文案,不再记账;
- Reporter::expect 改为委托 Meter::expect,处理器依赖库沿用同一条曲线;
- 回归测试钉住:预记账后 files_total=95、bytes_total=17MB、ratio 不再满格,
  处理器库下完仍是中间态,加载器库下完才到 100%。

真机重截 + 识图:D:/tmp/ull-m45-shots/forge-mid-fixed-25s.png —— 同一时刻面板
36/65 文件、3.4 MB/15.3 MB、进度条只填了约一半(旧图 36/36 + 满格)。
2026-11-01 09:14:02 +08:00

302 lines
12 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 新建实例向导(G5,全屏模态):版本 → 加载器 → 名字 → 确认 → 进度。
// 步骤推进/校验在 Rust(`crate::wizard`),这里只画当前步与提交按钮。
import { Theme } from "../theme/tokens.slint";
import { Metrics } from "../theme/metrics.slint";
import { UiVersion, VersionKind } from "../models.slint";
import { PageState } from "../page_state.slint";
import { UiText, TextRole } from "../components/label.slint";
import { Badge } from "../components/badge.slint";
import { AppIcon, IconKind } from "../components/icons.slint";
import { MetroButton, ButtonKind } from "../widgets/button.slint";
import { SearchField } from "../widgets/inputs.slint";
import { InstallPanel } from "../components/install-panel.slint";
import { ScrollView } from "std-widgets.slint";
component StepDot inherits Rectangle {
in property <int> index: 0;
in property <string> label: "";
height: 28px;
background: transparent;
HorizontalLayout {
spacing: Metrics.gap-1;
alignment: center;
Rectangle {
width: 18px;
height: 18px;
border-radius: 9px;
background: PageState.wizard.step >= root.index ? Theme.accent : Theme.bg-hover;
UiText {
role: TextRole.caption;
text: root.index + 1;
horizontal-alignment: center;
color: PageState.wizard.step >= root.index ? Theme.fg-on-accent : Theme.fg-secondary;
}
}
UiText {
role: TextRole.caption;
text: root.label;
color: PageState.wizard.step >= root.index ? Theme.fg-primary : Theme.fg-disabled;
}
}
}
component VersionChoice inherits Rectangle {
in property <UiVersion> data;
callback picked();
height: 36px;
background: PageState.wizard.version == root.data.id ? Theme.accent-soft
: (touch.has-hover ? Theme.bg-hover : transparent);
HorizontalLayout {
padding-left: Metrics.gap-3;
padding-right: Metrics.gap-3;
spacing: Metrics.gap-2;
UiText { role: TextRole.body; text: root.data.id; width: 160px; }
Badge {
text: root.data.kind == VersionKind.release ? "正式版" : "预览版";
tint: Theme.fg-secondary;
}
UiText { role: TextRole.caption; text: root.data.released; horizontal-stretch: 1; }
if PageState.wizard.version == root.data.id : AppIcon {
kind: IconKind.check;
icon-size: 14px;
tint: Theme.accent;
}
}
touch := TouchArea {
mouse-cursor: pointer;
clicked => { root.picked(); }
}
}
component LoaderChoice inherits Rectangle {
in property <string> label: "";
in property <string> hint: "";
in property <int> index: 0;
callback picked();
height: 48px;
background: PageState.wizard.loader == root.index ? Theme.accent-soft
: (touch.has-hover ? Theme.bg-hover : transparent);
HorizontalLayout {
padding-left: Metrics.gap-3;
padding-right: Metrics.gap-3;
spacing: Metrics.gap-2;
VerticalLayout {
vertical-stretch: 1;
spacing: 0px;
UiText { role: TextRole.body; text: root.label; }
UiText { role: TextRole.caption; text: root.hint; horizontal-stretch: 1; }
}
if PageState.wizard.loader == root.index : AppIcon {
kind: IconKind.check;
icon-size: 14px;
tint: Theme.accent;
}
}
touch := TouchArea {
mouse-cursor: pointer;
clicked => { root.picked(); }
}
}
export component NewInstanceWizard inherits Rectangle {
// 版本列表由 shell 传入(与下载页共用一份,避免二次拉取)
in property <[UiVersion]> versions;
in-out property <string> query: "";
visible: PageState.wizard.open;
background: rgba(0, 0, 0, 0.45);
// 吃掉底层点击(模态)
TouchArea { }
Rectangle {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: min(parent.width - 80px, 720px);
height: min(parent.height - 60px, 560px);
background: Theme.bg-surface;
border-radius: Metrics.radius-md;
border-width: 1px;
border-color: Theme.stroke;
VerticalLayout {
// 标题栏
Rectangle {
height: 52px;
background: transparent;
HorizontalLayout {
padding-left: Metrics.gap-4;
padding-right: Metrics.gap-3;
spacing: Metrics.gap-2;
UiText {
role: TextRole.subtitle;
text: "新建实例 · " + PageState.wizard.step_label;
horizontal-stretch: 1;
}
MetroButton {
kind: ButtonKind.subtle;
text: "关闭";
button-height: 24px;
clicked => { PageState.wizard-close(); }
}
}
}
// 步骤指示
HorizontalLayout {
padding-left: Metrics.gap-4;
spacing: Metrics.gap-3;
StepDot { index: 0; label: "版本"; }
StepDot { index: 1; label: "加载器"; }
StepDot { index: 2; label: "名字"; }
StepDot { index: 3; label: "确认"; }
StepDot { index: 4; label: "进度"; }
}
Rectangle { height: 1px; background: Theme.stroke; }
// 步骤内容
Rectangle {
vertical-stretch: 1;
background: transparent;
// ① 选版本(只列正式版/快照,远古版走下载页的显式入口)
if PageState.wizard.step == 0 : VerticalLayout {
padding: Metrics.gap-3;
spacing: Metrics.gap-2;
SearchField {
width: 220px;
text <=> root.query;
placeholder: "筛选版本号";
}
ScrollView {
vertical-stretch: 1;
VerticalLayout {
for v in root.versions : VersionChoice {
visible: !v.ancient;
data: v;
picked => { PageState.wizard-select-version(v.id); }
}
}
}
}
// ② 选加载器
if PageState.wizard.step == 1 : VerticalLayout {
padding: Metrics.gap-3;
spacing: 0px;
LoaderChoice { index: 0; label: "原版(Vanilla)"; hint: "不装加载器,最快最稳"; picked => { PageState.wizard-select-loader(0); } }
LoaderChoice { index: 1; label: "Fabric"; hint: "轻量模组加载器(GUI 可直接装)"; picked => { PageState.wizard-select-loader(1); } }
LoaderChoice { index: 2; label: "Quilt"; hint: "Fabric 的分支(GUI 可直接装)"; picked => { PageState.wizard-select-loader(2); } }
LoaderChoice { index: 3; label: "Forge"; hint: "需要 installer processors(首装会外调 java,约 1–3 分钟)"; picked => { PageState.wizard-select-loader(3); } }
LoaderChoice { index: 4; label: "NeoForge"; hint: "需要 installer processors(首装会外调 java,约 1–3 分钟)"; picked => { PageState.wizard-select-loader(4); } }
}
// ③ 名字
if PageState.wizard.step == 2 : VerticalLayout {
padding: Metrics.gap-4;
spacing: Metrics.gap-2;
UiText { role: TextRole.body; text: "实例名(默认用版本号)"; }
SearchField {
width: 320px;
text: PageState.wizard.name;
placeholder: "例如:生存 1.20.4";
edited(t) => { PageState.wizard-name-changed(t); }
}
if PageState.wizard.error != "" : UiText {
role: TextRole.caption;
text: PageState.wizard.error;
color: Theme.state-danger;
}
UiText {
role: TextRole.caption;
text: "不能包含 / \\ : * ? \" < > |,最多 64 个字符。";
}
}
// ④ 确认
if PageState.wizard.step == 3 : VerticalLayout {
padding: Metrics.gap-4;
spacing: Metrics.gap-2;
UiText { role: TextRole.bodyStrong; text: "将创建实例"; }
UiText { role: TextRole.body; text: "版本 " + PageState.wizard.version; }
UiText { role: TextRole.body; text: "加载器 " + PageState.wizard.loader_label; }
UiText { role: TextRole.body; text: "实例名 " + PageState.wizard.name; }
UiText {
role: TextRole.caption;
text: "确认后会按 core 安装管线下载:版本 JSON → 客户端 jar → 库 → 资源索引 → 登记实例。";
wrap: word-wrap;
}
if PageState.wizard.error != "" : UiText {
role: TextRole.caption;
text: PageState.wizard.error;
color: Theme.state-danger;
}
}
// ⑤ 进度
if PageState.wizard.step == 4 : VerticalLayout {
padding: Metrics.gap-3;
spacing: Metrics.gap-2;
InstallPanel {
data: PageState.install;
cancel => { PageState.install-cancel(); }
}
UiText {
role: TextRole.caption;
text: "装完会自动跳到实例页;失败原因会显示在上面。";
}
}
}
Rectangle { height: 1px; background: Theme.stroke; }
// 底部按钮
Rectangle {
height: 56px;
background: transparent;
HorizontalLayout {
padding: Metrics.gap-4;
spacing: Metrics.gap-2;
Rectangle { horizontal-stretch: 1; background: transparent; }
MetroButton {
kind: ButtonKind.subtle;
text: "上一步";
button-height: 30px;
enabled: PageState.wizard.step > 0 && PageState.wizard.step < 4;
clicked => { PageState.wizard-back(); }
}
if PageState.wizard.step < 3 : MetroButton {
kind: ButtonKind.accent;
text: "下一步";
button-height: 30px;
enabled: PageState.wizard.can-next;
clicked => { PageState.wizard-next(); }
}
if PageState.wizard.step == 3 : MetroButton {
kind: ButtonKind.accent;
text: "开始安装";
button-height: 30px;
enabled: PageState.wizard.can-next && !PageState.wizard.busy;
clicked => { PageState.wizard-confirm(); }
}
if PageState.wizard.step == 4 : MetroButton {
kind: ButtonKind.standard;
text: "后台继续";
button-height: 30px;
clicked => { PageState.wizard-close(); }
}
}
}
}
}
}