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
42 changes: 42 additions & 0 deletions packages/docs/content/en/manual/features/instances.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
title: Instance Library
description: Search, inspect, launch, migrate, and recover isolated Minecraft environments
---

# Instance Library

The instance library is the operating surface for every isolated Minecraft environment in DropOut. Selecting an instance also makes it the active instance used by the Home launch command center, so the library and launch state never point at different environments.

## Find an instance

Use the search field to match an instance name, Minecraft version, loader, or notes. Sort the result by:

- **Last played** for the environments you use most often;
- **Name** for large, structured libraries;
- **Created** for recently added or imported environments.

List view keeps dense libraries scannable. Grid view gives smaller libraries more visual separation. DropOut remembers the selected presentation on the current device.

## Read launch state

Each visible instance is checked progressively so a large library does not issue hundreds of runtime probes at once. The readiness marker uses the same backend check as the Home command center and considers the selected Minecraft version, local game files, compatible Java runtime, account, and memory override.

The active workspace on the right shows:

- Minecraft version, loader, and launch readiness;
- local file and Java availability;
- memory and Java overrides;
- enabled and disabled mod count;
- notes, creation date, and the managed game directory.

Use **Launch active instance** as the primary action. Opening another card changes the active instance everywhere, including Home.

## Manage and move environments

Secondary commands are grouped under the instance menu. From there you can open the game directory, edit metadata and runtime overrides, duplicate the environment, manage mods, browse content, export a ZIP, or delete it. The active workspace repeats the most useful shortcuts without hiding launch state.

Use **Library actions** to import a ZIP, migrate instances from Prism/MultiMC or PCL/HMCL, or repair the instance index from directories on disk.

## Delete safely

Deletion removes the managed game directory and cannot be undone from DropOut. Export the instance first when you may need its saves, mods, resource packs, or configuration later. Cancel the confirmation dialog if the target name or recovery plan is not correct.
2 changes: 1 addition & 1 deletion packages/docs/content/en/manual/features/meta.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"title": "Features",
"pages": ["index", "authentication", "java", "mod-loaders"]
"pages": ["index", "instances", "authentication", "java", "mod-loaders"]
}
42 changes: 42 additions & 0 deletions packages/docs/content/zh/manual/features/instances.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
title: 实例库
description: 搜索、检查、启动、迁移并修复隔离的 Minecraft 环境
---

# 实例库

实例库用于管理 DropOut 中所有隔离的 Minecraft 环境。选择某个实例时,它也会成为主页启动指挥中心使用的当前实例,因此实例库与启动状态不会指向不同环境。

## 查找实例

搜索框可匹配实例名称、Minecraft 版本、加载器或备注。可以按以下方式排序:

- **最近游玩**:优先显示常用环境;
- **名称**:便于浏览大型、结构化实例库;
- **创建时间**:优先显示最近创建或导入的环境。

列表视图适合高密度实例库,网格视图让较小的实例库更易区分。DropOut 会在当前设备上记住所选展示方式。

## 查看启动状态

可见实例会被渐进检查,避免大型实例库一次发起数百个运行时探测。就绪标记与主页使用同一个后端检查,并综合已选 Minecraft 版本、本地游戏文件、兼容 Java、账户和内存覆盖。

右侧当前工作区会显示:

- Minecraft 版本、加载器和启动就绪度;
- 本地文件与 Java 可用性;
- 内存与 Java 覆盖;
- 已启用和已禁用的模组数量;
- 备注、创建时间和托管游戏目录。

将 **启动当前实例** 作为主要操作。打开其他实例卡片会同步修改全局当前实例,包括主页。

## 管理与迁移环境

次要命令统一收进实例菜单,可打开游戏目录、编辑元数据和运行时覆盖、复制环境、管理模组、浏览内容、导出 ZIP 或删除实例。当前工作区也会重复最常用快捷操作,同时保留启动状态。

通过 **实例库操作** 可以导入 ZIP、从 Prism/MultiMC 或 PCL/HMCL 迁移实例,或根据磁盘目录修复实例索引。

## 安全删除

删除会移除托管的游戏目录,且无法从 DropOut 撤销。如果以后可能需要存档、模组、资源包或配置,请先导出实例。如果目标名称或恢复方案不正确,请取消确认对话框。
2 changes: 1 addition & 1 deletion packages/docs/content/zh/manual/features/meta.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"title": "功能特性",
"pages": ["index", "authentication", "java", "mod-loaders"]
"pages": ["index", "instances", "authentication", "java", "mod-loaders"]
}
7 changes: 7 additions & 0 deletions packages/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ Available fixture states are:
| `stopped` | Cleanly ended session with retained logs and relaunch action |
| `failed` | Failed session with captured diagnostic output |
| `error` | Instance-index failure and recovery feedback |
| `instances-empty` | Empty instance library onboarding |
| `instances-single` | One-instance library and active detail workspace |
| `instances-20` | Medium library for grid/list and visual review |
| `instances-grid` | Medium library with the grid presentation preselected |
| `instances-100` | Large library for search, sorting, and keyboard flows |
| `instances-loading` | Pending instance-index read |
| `instances-error` | Failed instance-index read with retry and repair actions |
| `migration` | Detected Prism/MultiMC and PCL/HMCL import sources |

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.
Expand Down
42 changes: 42 additions & 0 deletions packages/ui/src/components/instance-editor-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export function InstanceEditorModal({ open, instance, onOpenChange }: Props) {
// Settings tab fields
const [editMemoryMin, setEditMemoryMin] = useState<number>(0);
const [editMemoryMax, setEditMemoryMax] = useState<number>(0);
const [editJavaPath, setEditJavaPath] = useState<string>("");
const [editJavaArgs, setEditJavaArgs] = useState<string>("");
const [editServerAddress, setEditServerAddress] = useState<string>("");
const [editSkinPath, setEditSkinPath] = useState<string>("");
Expand All @@ -82,6 +83,7 @@ export function InstanceEditorModal({ open, instance, onOpenChange }: Props) {
config?.maxMemory ??
2048,
);
setEditJavaPath(instance.javaPathOverride ?? "");
setEditJavaArgs(instance.jvmArgsOverride ?? "");
setEditServerAddress(instance.serverAddress ?? "");
setEditSkinPath(instance.skinPath ?? "");
Expand Down Expand Up @@ -181,6 +183,7 @@ export function InstanceEditorModal({ open, instance, onOpenChange }: Props) {
min: editMemoryMin,
max: editMemoryMax,
},
javaPathOverride: editJavaPath.trim() ? editJavaPath.trim() : null,
jvmArgsOverride: editJavaArgs.trim() ? editJavaArgs.trim() : null,
serverAddress: editServerAddress.trim()
? editServerAddress.trim()
Expand Down Expand Up @@ -466,6 +469,45 @@ export function InstanceEditorModal({ open, instance, onOpenChange }: Props) {

{activeTab === "settings" && (
<div className="space-y-4">
<div>
<label
htmlFor="java-path-edit"
className="block text-sm font-medium mb-2"
>
{t("editor.javaPath")}
</label>
<div className="flex gap-2">
<Input
id="java-path-edit"
value={editJavaPath}
onChange={(e) => setEditJavaPath(e.target.value)}
placeholder={t("editor.javaPathPlaceholder")}
disabled={saving}
className="flex-1"
/>
<Button
type="button"
variant="outline"
size="icon"
disabled={saving}
aria-label={t("editor.chooseJavaPath")}
onClick={async () => {
const selected = await openFileDialog({
multiple: false,
});
if (typeof selected === "string") {
setEditJavaPath(selected);
}
}}
>
<Folder />
</Button>
</div>
<p className="text-xs text-zinc-400 mt-1">
{t("editor.javaPathHint")}
</p>
</div>

<div>
<label
htmlFor="server-address-edit"
Expand Down
4 changes: 4 additions & 0 deletions packages/ui/src/fixtures/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export function bootstrapLauncherFixture() {
document.documentElement.dataset.launcherFixtureLocale =
getLauncherFixtureLocale() ?? "en";

if (fixtureName === "instances-grid") {
window.localStorage.setItem("dropout.instanceLibraryView", "grid");
}

if (fixtureName === "downloading") {
useDownloadStore.setState({
active: true,
Expand Down
89 changes: 83 additions & 6 deletions packages/ui/src/fixtures/launcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
JavaInstallation,
LauncherConfig,
LaunchReadiness,
ModInfo,
Version,
} from "@/types";
import type { GameExitedEvent } from "@/types/bindings/core";
Expand Down Expand Up @@ -71,6 +72,39 @@ const vanillaInstance: Instance = {
memoryOverride: null,
};

function createLibraryInstances(count: number): Instance[] {
return Array.from({ length: count }, (_, index) => {
if (index === 0) return readyInstance;

const ordinal = index + 1;
const padded = String(ordinal).padStart(3, "0");
const loader =
index % 3 === 0 ? null : index % 3 === 1 ? "fabric" : "forge";
const name =
ordinal === 97
? "Redstone Archive 097"
: `${["Alpine", "Copper", "Deep Dark", "Skyblock", "Workshop"][index % 5]} ${padded}`;

return {
...readyInstance,
id: `fixture-library-${padded}`,
name,
gameDir: `/fixtures/dropout/instances/library-${padded}`,
versionId:
index % 13 === 0 ? null : index % 4 === 0 ? "1.20.6" : "1.21.1",
createdAt: BigInt(1_735_689_600_000 - index * 86_400_000),
lastPlayed:
index % 9 === 0 ? null : BigInt(1_786_464_000_000 - index * 7_200_000),
notes:
index % 4 === 0 ? "Automation and progression test environment." : null,
modLoader: loader,
modLoaderVersion:
loader === "fabric" ? "0.16.14" : loader === "forge" ? "52.0.16" : null,
memoryOverride: index % 5 === 0 ? { min: 3072, max: 8192 } : null,
};
});
}

const settings: LauncherConfig = {
minMemory: 1024,
maxMemory: 8192,
Expand Down Expand Up @@ -205,8 +239,17 @@ const fixtureState: FixtureState = {
function fixturesForCurrentScenario() {
const name = getLauncherFixtureName() ?? "ready";
const noAccount = name === "empty" || name === "no-account";
const noInstances = name === "empty" || name === "no-instance";
const instances = noInstances ? [] : [readyInstance, vanillaInstance];
const noInstances =
name === "empty" || name === "no-instance" || name === "instances-empty";
const instances = noInstances
? []
: name === "instances-single"
? [readyInstance]
: name === "instances-20" || name === "instances-grid"
? createLibraryInstances(20)
: name === "instances-100"
? createLibraryInstances(100)
: [readyInstance, vanillaInstance];
const activeInstance =
instances.find(
(instance) => instance.id === fixtureState.activeInstanceId,
Expand Down Expand Up @@ -267,10 +310,17 @@ export async function fixtureInvoke<T>(
): Promise<T> {
const fixture = fixturesForCurrentScenario();

if (fixture.name === "error" && command === "list_instances") {
if (
(fixture.name === "error" || fixture.name === "instances-error") &&
command === "list_instances"
) {
throw new Error("Fixture: instance index could not be read");
}

if (fixture.name === "instances-loading" && command === "list_instances") {
return new Promise<T>(() => undefined);
}

const result = (() => {
switch (command) {
case "get_active_account":
Expand Down Expand Up @@ -298,12 +348,22 @@ export async function fixtureInvoke<T>(
return fixture.instances;
case "get_versions":
return versions;
case "get_launch_readiness":
case "get_launch_readiness": {
const requestedInstance = fixture.instances.find(
(instance) => instance.id === args.instanceId,
);
return {
versionInstalled: fixture.name !== "downloading",
versionInstalled:
fixture.name !== "downloading" &&
requestedInstance?.versionId !== null,
requiredJavaMajor: 21n,
java: fixture.name === "not-ready" ? null : javaInstallations[0],
java:
fixture.name === "not-ready" ||
String(args.instanceId).endsWith("017")
? null
: javaInstallations[0],
} satisfies LaunchReadiness;
}
case "detect_launchers":
return fixture.name === "migration" ? detectedLaunchers : [];
case "scan_launcher_instances":
Expand Down Expand Up @@ -354,6 +414,23 @@ export async function fixtureInvoke<T>(
}
case "open_file_explorer":
return undefined;
case "scan_instance_mods": {
const libraryOrdinal = String(args.instanceId).match(
/^fixture-library-(\d+)$/,
)?.[1];
const count = libraryOrdinal ? Number(libraryOrdinal) % 18 : 12;
return Array.from({ length: count }, (_, index) => ({
fileName: `fixture-mod-${index + 1}.jar`,
filePath: `/fixtures/mods/fixture-mod-${index + 1}.jar`,
enabled: index % 5 !== 0,
fileSize: BigInt((index + 1) * 1024 * 1024),
modName: `Fixture Mod ${index + 1}`,
modId: `fixture_mod_${index + 1}`,
version: "1.0.0",
description: null,
modLoader: fixture.activeInstance?.modLoader ?? "fabric",
})) satisfies ModInfo[];
}
case "stop_game": {
const active = fixturesForCurrentScenario().activeInstance;
queueMicrotask(() => {
Expand Down
7 changes: 7 additions & 0 deletions packages/ui/src/lib/launcher-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ export const launcherFixtureNames = [
"stopped",
"failed",
"error",
"instances-empty",
"instances-single",
"instances-20",
"instances-grid",
"instances-100",
"instances-loading",
"instances-error",
"migration",
] as const;

Expand Down
Loading
Loading