Skip to content

Commit db6e995

Browse files
committed
feat(ui): Create cluster modal now supports importing instances
1 parent 623632a commit db6e995

File tree

4 files changed

+228
-90
lines changed

4 files changed

+228
-90
lines changed

apps/frontend/src/ui/components/overlay/cluster/ClusterCreationModal.tsx

Lines changed: 32 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,32 @@
1-
import type { CreateCluster } from '@onelauncher/client/bindings';
21
import { ArrowRightIcon, Server01Icon } from '@untitled-theme/icons-solid';
3-
import { bridge } from '~imports';
42
import Button from '~ui/components/base/Button';
5-
import { type Accessor, type Context, createContext, createSignal, type JSX, type ParentProps, type Setter, Show, untrack, useContext } from 'solid-js';
3+
import { type Accessor, type Context, createContext, createSignal, type JSX, type ParentProps, type Setter, Show, useContext } from 'solid-js';
64
import { createStore } from 'solid-js/store';
75
import HeaderImage from '../../../../assets/images/header.png';
86
import { createModal } from '../Modal';
97
import ClusterGameSetup from './ClusterGameSetup';
10-
import ClusterProviderSelection from './ClusterProviderSelection';
8+
import ClusterImportSelection from './ClusterImportSelection';
9+
import ClusterProviderSelection, { type ClusterCreationProvider } from './ClusterProviderSelection';
1110

1211
export enum CreationStage {
13-
PROVIDER_SELECTION = 0,
14-
GAME_SETUP = 1,
15-
IMPORT_SELECTION = 2,
12+
PROVIDER_SELECTION,
13+
GAME_SETUP,
14+
IMPORT_SELECTION,
1615
}
1716

18-
type PartialCluster = Partial<CreateCluster>;
19-
type PartialClusterUpdateFunc = <K extends keyof PartialCluster>(key: K, value: PartialCluster[K]) => void;
17+
type FinishFn = () => Promise<boolean> | boolean;
2018

2119
interface ClusterModalController {
2220
step: Accessor<CreationStage | undefined>;
2321
setStep: (step: CreationStage | number | undefined) => void;
2422

25-
partialCluster: Accessor<PartialCluster>;
26-
setPartialCluster: Setter<PartialCluster>;
27-
updatePartialCluster: PartialClusterUpdateFunc;
23+
provider: Accessor<ClusterCreationProvider | undefined>;
24+
setProvider: Setter<ClusterCreationProvider | undefined>;
2825

29-
start: () => Promise<void>;
26+
finishFunction: Accessor<FinishFn>;
27+
setFinishFunction: Setter<FinishFn>;
28+
29+
start: () => void;
3030
previous: () => void;
3131
cancel: () => void;
3232
finish: () => void;
@@ -35,14 +35,13 @@ interface ClusterModalController {
3535
const ClusterModalContext = createContext<ClusterModalController>() as Context<ClusterModalController>;
3636

3737
export function ClusterModalControllerProvider(props: ParentProps) {
38-
const [partialCluster, setPartialCluster] = createSignal<PartialCluster>({});
39-
const requiredProps: (keyof PartialCluster)[] = ['name', 'mc_version', 'mod_loader'];
40-
38+
const [provider, setProvider] = createSignal<ClusterCreationProvider | undefined>();
39+
const [finishFunction, setFinishFunction] = createSignal<FinishFn>(() => true);
4140
const [steps, setSteps] = createSignal<CreationStage[]>([]);
4241
const [stepComponents] = createStore<{ [key in CreationStage]: () => JSX.Element }>({
4342
[CreationStage.PROVIDER_SELECTION]: ClusterProviderSelection,
4443
[CreationStage.GAME_SETUP]: ClusterGameSetup,
45-
[CreationStage.IMPORT_SELECTION]: () => <></>,
44+
[CreationStage.IMPORT_SELECTION]: ClusterImportSelection,
4645
});
4746

4847
const controller: ClusterModalController = {
@@ -64,12 +63,13 @@ export function ClusterModalControllerProvider(props: ParentProps) {
6463
});
6564
},
6665

67-
partialCluster,
68-
setPartialCluster,
69-
updatePartialCluster: (key, value) => setPartialCluster(prev => ({ ...prev, [key]: value })),
66+
provider,
67+
setProvider,
7068

71-
async start() {
72-
setPartialCluster({});
69+
finishFunction,
70+
setFinishFunction,
71+
72+
start() {
7373
controller.setStep(CreationStage.PROVIDER_SELECTION);
7474
modal.show();
7575
},
@@ -83,24 +83,17 @@ export function ClusterModalControllerProvider(props: ParentProps) {
8383
modal.hide();
8484
},
8585

86-
async finish() {
87-
const untracked = untrack(partialCluster);
88-
89-
for (const prop of requiredProps)
90-
if (!untracked[prop])
91-
throw new Error(`Missing required property ${prop}`);
86+
finish() {
87+
const fn = finishFunction()();
9288

93-
modal.hide();
94-
95-
await bridge.commands.createCluster({
96-
icon: null,
97-
icon_url: null,
98-
loader_version: null,
99-
package_data: null,
100-
skip: null,
101-
skip_watch: null,
102-
...untracked,
103-
} as CreateCluster);
89+
if (fn instanceof Promise)
90+
fn.then((res) => {
91+
if (res === true)
92+
modal.hide();
93+
});
94+
else
95+
if (fn === true)
96+
modal.hide();
10497
},
10598
};
10699

apps/frontend/src/ui/components/overlay/cluster/ClusterGameSetup.tsx

Lines changed: 62 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Loader, VersionType } from '@onelauncher/client/bindings';
1+
import type { CreateCluster, Loader, VersionType } from '@onelauncher/client/bindings';
22
import { TextInputIcon } from '@untitled-theme/icons-solid';
33
import { bridge } from '~imports';
44
import Checkbox from '~ui/components/base/Checkbox';
@@ -8,32 +8,79 @@ import TextField from '~ui/components/base/TextField';
88
import LoaderIcon from '~ui/components/game/LoaderIcon';
99
import useCommand from '~ui/hooks/useCommand';
1010
import { formatVersionRelease, LOADERS } from '~utils';
11-
import { createEffect, createSignal, For, Index, type JSX, onMount, Show, splitProps, untrack } from 'solid-js';
11+
import { createEffect, createMemo, createSignal, For, Index, type JSX, onMount, Show, splitProps, untrack } from 'solid-js';
1212
import { type ClusterStepProps, createClusterStep } from './ClusterCreationModal';
1313

14+
type PartialCluster = Partial<CreateCluster>;
15+
type PartialClusterUpdateFunc = <K extends keyof PartialCluster>(key: K, value: PartialCluster[K]) => void;
16+
1417
export default createClusterStep({
1518
message: 'Game Setup',
1619
buttonType: 'create',
1720
Component: ClusterGameSetup,
1821
});
1922

23+
function _epicHardcodedLoaderVersionFilter(loader: Loader, version: string): boolean {
24+
if (loader === 'vanilla')
25+
return true;
26+
27+
const split = version.split('.')[1];
28+
if (split === undefined)
29+
return true;
30+
31+
const minor = Number.parseInt(split);
32+
if (minor < 13)
33+
return loader === 'forge' || loader === 'legacyfabric';
34+
else
35+
return loader === 'forge' || loader === 'fabric' || loader === 'neoforge' || loader === 'quilt';
36+
}
37+
2038
function ClusterGameSetup(props: ClusterStepProps) {
21-
const check = () => {
22-
const hasName = (props.controller.partialCluster().name?.length ?? 0) > 0;
23-
const hasVersion = (props.controller.partialCluster().mc_version?.length ?? 0) > 0;
24-
const hasLoader = (props.controller.partialCluster().mod_loader?.length ?? 0) > 0;
39+
const [partialCluster, setPartialCluster] = createSignal<PartialCluster>({
40+
mod_loader: 'vanilla',
41+
});
2542

26-
props.setCanGoForward(hasName && hasVersion && hasLoader);
27-
};
43+
const requiredProps: (keyof PartialCluster)[] = ['name', 'mc_version', 'mod_loader'];
2844

29-
createEffect(check);
45+
const updatePartialCluster: PartialClusterUpdateFunc = (key, value) => setPartialCluster(prev => ({ ...prev, [key]: value }));
46+
const setName = (name: string) => updatePartialCluster('name', name);
47+
const setVersion = (version: string) => updatePartialCluster('mc_version', version);
48+
const setLoader = (loader: Loader | string) => updatePartialCluster('mod_loader', loader.toLowerCase() as Loader);
3049

31-
const setName = (name: string) => props.controller.updatePartialCluster('name', name);
32-
const setVersion = (version: string) => props.controller.updatePartialCluster('mc_version', version);
33-
const setLoader = (loader: Loader | string) => props.controller.updatePartialCluster('mod_loader', loader.toLowerCase() as Loader);
50+
const getLoaders = createMemo(() => {
51+
const version = partialCluster().mc_version ?? '';
52+
53+
return LOADERS.filter(loader => _epicHardcodedLoaderVersionFilter(loader, version));
54+
});
55+
56+
createEffect(() => {
57+
const hasName = (partialCluster().name?.length ?? 0) > 0;
58+
const hasVersion = (partialCluster().mc_version?.length ?? 0) > 0;
59+
const hasLoader = (partialCluster().mod_loader?.length ?? 0) > 0;
60+
61+
props.setCanGoForward(hasName && hasVersion && hasLoader);
62+
});
3463

3564
onMount(() => {
36-
setLoader('vanilla');
65+
props.controller.setFinishFunction(() => async () => {
66+
const untracked = untrack(partialCluster);
67+
68+
for (const prop of requiredProps)
69+
if (!untracked[prop])
70+
throw new Error(`Missing required property ${prop}`);
71+
72+
await bridge.commands.createCluster({
73+
icon: null,
74+
icon_url: null,
75+
loader_version: null,
76+
package_data: null,
77+
skip: null,
78+
skip_watch: null,
79+
...untracked,
80+
} as CreateCluster);
81+
82+
return true;
83+
});
3784
});
3885

3986
return (
@@ -51,8 +98,8 @@ function ClusterGameSetup(props: ClusterStepProps) {
5198
</Option>
5299

53100
<Option header="Loader">
54-
<Dropdown onChange={index => setLoader(LOADERS[index] || 'vanilla')}>
55-
<For each={LOADERS}>
101+
<Dropdown onChange={index => setLoader(getLoaders()[index] || 'vanilla')}>
102+
<For each={getLoaders()}>
56103
{loader => (
57104
<Dropdown.Row>
58105
<div class="flex flex-row gap-x-2">
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import type { ImportType } from '@onelauncher/client/bindings';
2+
import { bridge } from '~imports';
3+
import SelectList from '~ui/components/base/SelectList';
4+
import useCommand from '~ui/hooks/useCommand';
5+
import { createEffect, createSignal, For, onMount, untrack } from 'solid-js';
6+
import { type ClusterStepProps, createClusterStep } from './ClusterCreationModal';
7+
8+
export default createClusterStep({
9+
message: 'Import Launcher',
10+
buttonType: 'create',
11+
Component: ClusterImportSelection,
12+
});
13+
14+
function ClusterImportSelection(props: ClusterStepProps) {
15+
const [instances] = useCommand(() => {
16+
const importType = props.controller.provider();
17+
if (importType === undefined || importType === 'New')
18+
return [];
19+
20+
return bridge.commands.getLauncherInstances(importType, null);
21+
});
22+
23+
const [selected, setSelected] = createSignal<number>();
24+
25+
createEffect(() => {
26+
const index = selected();
27+
28+
props.setCanGoForward(() => {
29+
if (index === undefined)
30+
return false;
31+
32+
if (index >= 0)
33+
return true;
34+
35+
return false;
36+
});
37+
});
38+
39+
onMount(() => {
40+
// eslint-disable-next-line solid/reactivity -- It's fine
41+
props.controller.setFinishFunction(() => async () => {
42+
const index = untrack(selected);
43+
if (index === undefined)
44+
return false;
45+
46+
const list = untrack(instances);
47+
if (list === undefined)
48+
return false;
49+
50+
const instance = list[1][index];
51+
if (instance === undefined)
52+
return false;
53+
54+
const basePath = list[0];
55+
56+
const importType = untrack(props.controller.provider) as ImportType;
57+
58+
await bridge.commands.importInstances(importType, basePath, [instance]);
59+
60+
return true;
61+
});
62+
});
63+
64+
return (
65+
<SelectList
66+
class="h-52 max-h-52"
67+
onChange={selected => setSelected(selected[0])}
68+
>
69+
<For each={instances()?.[1]}>
70+
{(instance, index) => (
71+
<SelectList.Row index={index()}>
72+
{instance}
73+
</SelectList.Row>
74+
)}
75+
</For>
76+
</SelectList>
77+
);
78+
}

0 commit comments

Comments
 (0)