Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ jobs:
- name: Run UI fixture regression suite
run: pnpm -C packages/ui test:ui

- name: Upload Linux screenshot candidates
if: failure()
uses: actions/upload-artifact@v4
with:
name: linux-screenshot-candidates
path: packages/ui/test-results/**/*-actual.png
if-no-files-found: error
compression-level: 0
retention-days: 7

- name: Upload UI regression diagnostics
if: failure()
uses: actions/upload-artifact@v4
Expand Down
11 changes: 11 additions & 0 deletions packages/docs/content/en/development/implementation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,17 @@ For 1.17+ versions and high-load mod environments, DropOut injects a concise G1G
-XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC
```

### 3.3 Launch Readiness and Recovery APIs

The home command center calls `get_launch_readiness(instance_id, version_id)` before enabling launch. The command checks the version metadata/client files and calls the same Java resolver used by `start_game` (instance override → global path → preferred managed runtime → automatic detection). This keeps the displayed Java result and the real launch decision consistent.

| Command / Event | Type | Purpose |
| :--- | :--- | :--- |
| `get_launch_readiness` | Invoke | Return installed-file state, required Java major version, and the resolved compatible runtime |
| `download-start` / `download-progress` / `download-complete` | Event | Drive the live aggregate download card |
| `launcher-log` / `game-stdout` / `game-stderr` | Event | Preserve recent launch and game output for recovery |
| `game-exited` | Event | Distinguish stopped, clean exit, and failed session states |

---

## 4. Mod Loader Mechanism
Expand Down
20 changes: 12 additions & 8 deletions packages/docs/content/en/manual/getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,18 @@ When you first launch DropOut, you'll need to:

### 3. Launch the Game

1. Go to the **Home** tab
2. Select your desired version from the dropdown
3. Adjust settings if needed:
- Memory allocation (RAM)
- Window resolution
- Java path
4. Click **"Launch Game"**
5. Monitor the launch process in the console
1. Go to **Overview** and choose an instance from **Active instance**.
2. Read the **Launch readiness** checks for account, instance, version, loader, Java, memory, and local game files.
3. Use the single primary action. It changes with the current state:
- sign in or create/import an instance;
- configure a missing version or repair Java/memory;
- download required game files;
- launch, stop, or launch again;
- inspect a failed session.
4. During preparation, the download card shows file count, current file, bytes, and overall progress.
5. During and after a session, use **Live launch log** or **Open Minecraft logs** to inspect output without searching for the instance directory.

DropOut calculates the Java and file checks with the same backend rules used by the real launch command. A green **ready** state therefore means the launch-critical checks passed, not merely that fields were filled in.

## Next Steps

Expand Down
11 changes: 11 additions & 0 deletions packages/docs/content/en/manual/troubleshooting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,17 @@ sudo pacman -S webkit2gtk gtk3

## Game Launch Issues

### Use the Home Recovery Action First

The Overview page summarizes the active account, instance, version, loader, Java runtime, memory allocation, and local files. Follow its primary action before changing configuration manually:

- **Repair Java** opens the runtime settings when no compatible installation can be resolved.
- **Download & prepare** installs or repairs missing version files and keeps progress visible.
- **Inspect failure** keeps the captured launcher/stdout/stderr lines on screen; **Open Minecraft logs** opens the full log directory.
- **Retry checks** reloads account, settings, and instance data after a read error.

If the page reports **ready** but launch still fails, preserve the failure details and full Minecraft log when reporting the problem.

### Java Not Found

**Issue:** "No Java installation found"
Expand Down
11 changes: 11 additions & 0 deletions packages/docs/content/zh/development/implementation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,17 @@ fn check_java_installation(path: &PathBuf) -> Option<JavaInstallation> {
-XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC
```

### 3.3 启动就绪与恢复接口

首页指挥台会在启用启动操作前调用 `get_launch_readiness(instance_id, version_id)`。该命令检查版本元数据和客户端文件,并调用与 `start_game` 相同的 Java 解析器(实例覆盖 → 全局路径 → 首选托管运行时 → 自动检测),确保页面显示的 Java 结果与真实启动判断一致。

| 命令 / 事件 | 类型 | 用途 |
| :--- | :--- | :--- |
| `get_launch_readiness` | Invoke | 返回文件安装状态、所需 Java 主版本和已解析的兼容运行时 |
| `download-start` / `download-progress` / `download-complete` | Event | 驱动实时汇总下载卡片 |
| `launcher-log` / `game-stdout` / `game-stderr` | Event | 保留最近的启动和游戏输出,用于失败恢复 |
| `game-exited` | Event | 区分主动停止、正常退出和失败会话 |

---

## 4. 模组加载机制 (Mod Loaders)
Expand Down
20 changes: 12 additions & 8 deletions packages/docs/content/zh/manual/getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,18 @@ chmod +x dropout_*.AppImage

### 3. 启动游戏

1. 转到**主页**标签
2. 从下拉菜单中选择你想要的版本
3. 如果需要,调整设置:
- 内存分配(RAM)
- 窗口分辨率
- Java 路径
4. 点击**启动游戏**
5. 在控制台中监视启动过程
1. 进入**概览**,在**当前实例**中选择要游玩的实例。
2. 查看**启动就绪检查**中的账户、实例、版本、加载器、Java、内存和本地游戏文件状态。
3. 使用唯一的主操作按钮;它会根据当前状态自动切换:
- 登录,或创建/导入实例;
- 配置缺失版本,或修复 Java/内存;
- 下载所需游戏文件;
- 启动、停止或再次启动;
- 查看失败会话的详情。
4. 准备期间,下载卡片会显示文件数、当前文件、字节数和总体进度。
5. 会话运行期间或结束后,可通过**实时启动日志**或**打开 Minecraft 日志**查看输出,无需手动查找实例目录。

DropOut 使用与真实启动命令相同的后端规则计算 Java 和文件状态。因此,绿色的**已就绪**表示启动关键检查确实通过,而不只是配置字段已经填写。

## 下一步

Expand Down
11 changes: 11 additions & 0 deletions packages/docs/content/zh/manual/troubleshooting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,17 @@ sudo pacman -S webkit2gtk gtk3

## 游戏启动问题

### 优先使用首页恢复操作

概览页会汇总当前账户、实例、版本、加载器、Java 运行时、内存分配和本地文件状态。手动修改配置前,请先执行页面给出的主操作:

- **修复 Java**:无法解析兼容运行时时,直接进入运行时设置。
- **下载并准备**:安装或修复缺失的版本文件,并持续显示进度。
- **查看失败详情**:在页面保留已捕获的启动器、标准输出和错误输出;**打开 Minecraft 日志**可进入完整日志目录。
- **重新检查**:读取错误后,重新加载账户、设置和实例数据。

如果页面显示**已就绪**但启动仍然失败,请在反馈问题时保留失败详情和完整 Minecraft 日志。

### 找不到 Java

**问题:** "未找到 Java 安装"
Expand Down
22 changes: 16 additions & 6 deletions packages/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,30 @@ Open a fixture with query parameters before the hash route:
```text
http://127.0.0.1:1420/?fixture=ready&theme=dark#/
http://127.0.0.1:1420/?fixture=migration&theme=light#/instances/import
http://127.0.0.1:1420/?fixture=failed&theme=dark&locale=zh-CN#/
```

Available fixture states are:

| Fixture | Purpose |
|---|---|
| `empty` | No account or instances |
| `ready` | Account, active Fabric instance, versions, Java, and settings |
| `downloading` | Stable active-download presentation |
| `running` | Active game process and stop action |
| `empty` | Legacy combined empty state |
| `no-account` | Instance exists but authentication blocks launch |
| `no-instance` | Account exists but no instance has been created or imported |
| `not-ready` | Active instance has no compatible Java runtime |
| `ready` | All account, instance, version, Java, memory, and file checks pass |
| `downloading` | Live game-file progress in the home command center |
| `java-download-progress` | Java runtime event progress with bounded percentage and byte formatting |
| `launching` | Launch command is assembling the runtime and process |
| `running` | Active game process with the stop action |
| `stopped` | Cleanly ended session with retained logs and relaunch action |
| `failed` | Failed session with captured diagnostic output |
| `error` | Instance-index failure and recovery feedback |
| `migration` | Detected Prism/MultiMC and PCL/HMCL import sources |

Use `theme=dark` or `theme=light`. Fixture activation is guarded by `import.meta.env.DEV`; production builds ignore fixture parameters and continue to call Tauri directly.
Use `theme=dark` or `theme=light`, and `locale=en` or `locale=zh-CN`. Fixture activation is guarded by `import.meta.env.DEV`; production builds ignore fixture parameters and continue to call Tauri directly.

The home command center does not infer readiness from frontend configuration alone. `get_launch_readiness` reuses the backend's real Java priority and compatibility rules, while `download-start`, `download-progress`, `download-complete`, `launcher-log`, `game-stdout`, `game-stderr`, and `game-exited` events keep progress and recovery details live.

## Regression Tests

Expand All @@ -40,7 +50,7 @@ pnpm -C packages/ui exec playwright install chromium
pnpm -C packages/ui test:ui
```

The suite verifies both supported launcher window sizes (`1024x768` and `905x575`), both themes, deterministic screenshots, accessible names/image alternatives/unique IDs on critical routes, and a keyboard-only launch flow. Import and raw-config editor surfaces have independent lazy routes so their heavier dependencies do not inflate the initial route.
The suite verifies both supported launcher window sizes (`1024x768` and `905x575`), both themes, every launch/recovery state, deterministic screenshots, English and Chinese rendering, reduced motion, accessible names/image alternatives/unique IDs on critical routes, and keyboard-only navigation and launch flows. Import and raw-config editor surfaces have independent lazy routes so their heavier dependencies do not inflate the initial route.

When an intentional visual change has been reviewed at both window sizes, update and inspect the baselines before committing them:

Expand Down
11 changes: 11 additions & 0 deletions packages/ui/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type {
JavaDownloadInfo,
JavaInstallation,
LauncherConfig,
LaunchReadiness,
Message,
MigrationResult,
ModelInfo,
Expand Down Expand Up @@ -269,6 +270,16 @@ export function getInstance(instanceId: string): Promise<Instance> {
});
}

export function getLaunchReadiness(
instanceId: string,
versionId: string,
): Promise<LaunchReadiness> {
return invoke<LaunchReadiness>("get_launch_readiness", {
instanceId,
versionId,
});
}

export function getPendingJavaDownloads(): Promise<PendingJavaDownload[]> {
return invoke<PendingJavaDownload[]>("get_pending_java_downloads");
}
Expand Down
136 changes: 84 additions & 52 deletions packages/ui/src/components/download-monitor.tsx
Original file line number Diff line number Diff line change
@@ -1,69 +1,101 @@
import { X } from "lucide-react";
import { useState } from "react";
import { Download, FileDown } from "lucide-react";
import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils";
import { useDownloadStore } from "@/models/downloads";

export function DownloadMonitor() {
const [isVisible, setIsVisible] = useState(true);
function formatBytes(value: number) {
if (!Number.isFinite(value) || value <= 0) return "—";
const units = ["B", "KB", "MB", "GB"];
const unit = Math.min(
Math.floor(Math.log(value) / Math.log(1024)),
units.length - 1,
);
return `${(value / 1024 ** unit).toFixed(unit > 1 ? 1 : 0)} ${units[unit]}`;
}

export function DownloadMonitor({ className }: { className?: string }) {
const { t } = useTranslation();
const download = useDownloadStore();

if (!download.active && download.totalFiles === 0) return null;

if (!isVisible) return null;
const percentage = Math.round(download.percentage);

return (
<div className="bg-card/95 backdrop-blur-md border border-border rounded-lg shadow-2xl overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 bg-muted/50 border-b border-border">
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-emerald-500 rounded-full animate-pulse"></div>
<span className="text-sm font-medium text-foreground">
{t("downloadMonitor.title")}
<section
id="download-monitor"
tabIndex={-1}
aria-labelledby="download-monitor-title"
aria-live="polite"
className={cn(
"border-border/80 bg-card/90 focus-visible:ring-ring overflow-hidden border shadow-sm backdrop-blur-xl focus-visible:ring-2 focus-visible:outline-none",
className,
)}
data-testid="download-monitor"
>
<div className="border-border/70 flex items-center justify-between border-b px-3 py-2.5">
<div className="flex min-w-0 items-center gap-2">
<span className="bg-primary/10 text-primary flex size-7 shrink-0 items-center justify-center rounded-md">
<Download className="size-3.5" aria-hidden="true" />
</span>
<div className="min-w-0">
<h2 id="download-monitor-title" className="text-xs font-semibold">
{t("downloadMonitor.title")}
</h2>
<p className="text-muted-foreground truncate text-[10px]">
{download.kind === "java"
? t("downloadMonitor.javaRuntime")
: t("downloadMonitor.gameFiles")}
</p>
</div>
</div>
<button
type="button"
aria-label={t("downloadMonitor.close")}
onClick={() => setIsVisible(false)}
className="text-muted-foreground hover:text-foreground transition-colors p-1"
>
<X size={16} />
</button>
<span className="text-primary font-mono text-xs font-bold">
{percentage}%
</span>
</div>

{/* Content */}
<div className="p-4">
<div className="space-y-3">
{/* Download Item */}
<div className="space-y-1">
<div className="flex justify-between text-xs">
<span className="text-foreground">Minecraft 1.20.4</span>
<span className="text-muted-foreground">65%</span>
</div>
<div className="h-1.5 bg-muted rounded-full overflow-hidden">
<div
className="h-full bg-emerald-500 rounded-full transition-all duration-300"
style={{ width: "65%" }}
></div>
</div>
<div className="flex justify-between text-[10px] text-muted-foreground">
<span>142 MB / 218 MB</span>
<span>2.1 MB/s • 36s remaining</span>
</div>
</div>
<div className="space-y-3 p-3">
<div
className="bg-muted h-1.5 overflow-hidden rounded-full"
role="progressbar"
aria-label={t("downloadMonitor.progress")}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={percentage}
>
<div
className="bg-primary h-full rounded-full transition-[width] duration-300 motion-reduce:transition-none"
style={{ width: `${percentage}%` }}
/>
</div>

{/* Download Item */}
<div className="space-y-1">
<div className="flex justify-between text-xs">
<span className="text-foreground">Java 17</span>
<span className="text-muted-foreground">100%</span>
</div>
<div className="h-1.5 bg-muted rounded-full overflow-hidden">
<div className="h-full bg-emerald-500 rounded-full"></div>
</div>
<div className="text-[10px] text-emerald-700 dark:text-emerald-400">
{t("downloadMonitor.completed")}
<div className="flex items-start gap-2">
<FileDown
className="text-muted-foreground mt-0.5 size-3.5 shrink-0"
aria-hidden="true"
/>
<div className="min-w-0 flex-1">
<p className="truncate font-mono text-[11px]">
{download.currentFile ?? t("downloadMonitor.preparing")}
</p>
<div className="text-muted-foreground mt-1 flex justify-between gap-2 text-[10px]">
<span>
{download.totalFiles > 0
? t("downloadMonitor.files", {
completed: download.completedFiles,
total: download.totalFiles,
})
: download.status}
</span>
<span>
{download.totalBytes > 0
? `${formatBytes(download.downloadedBytes)} / ${formatBytes(download.totalBytes)}`
: download.status}
</span>
</div>
</div>
</div>
</div>
</div>
</section>
);
}
Loading
Loading