Skip to content

Commit

Permalink
feat(ui): Create cluster modal now supports importing instances
Browse files Browse the repository at this point in the history
  • Loading branch information
LynithDev committed Nov 29, 2024
1 parent 623632a commit db6e995
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 90 deletions.
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
import type { CreateCluster } from '@onelauncher/client/bindings';
import { ArrowRightIcon, Server01Icon } from '@untitled-theme/icons-solid';
import { bridge } from '~imports';
import Button from '~ui/components/base/Button';
import { type Accessor, type Context, createContext, createSignal, type JSX, type ParentProps, type Setter, Show, untrack, useContext } from 'solid-js';
import { type Accessor, type Context, createContext, createSignal, type JSX, type ParentProps, type Setter, Show, useContext } from 'solid-js';
import { createStore } from 'solid-js/store';
import HeaderImage from '../../../../assets/images/header.png';
import { createModal } from '../Modal';
import ClusterGameSetup from './ClusterGameSetup';
import ClusterProviderSelection from './ClusterProviderSelection';
import ClusterImportSelection from './ClusterImportSelection';
import ClusterProviderSelection, { type ClusterCreationProvider } from './ClusterProviderSelection';

export enum CreationStage {
PROVIDER_SELECTION = 0,
GAME_SETUP = 1,
IMPORT_SELECTION = 2,
PROVIDER_SELECTION,
GAME_SETUP,
IMPORT_SELECTION,
}

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

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

partialCluster: Accessor<PartialCluster>;
setPartialCluster: Setter<PartialCluster>;
updatePartialCluster: PartialClusterUpdateFunc;
provider: Accessor<ClusterCreationProvider | undefined>;
setProvider: Setter<ClusterCreationProvider | undefined>;

start: () => Promise<void>;
finishFunction: Accessor<FinishFn>;
setFinishFunction: Setter<FinishFn>;

start: () => void;
previous: () => void;
cancel: () => void;
finish: () => void;
Expand All @@ -35,14 +35,13 @@ interface ClusterModalController {
const ClusterModalContext = createContext<ClusterModalController>() as Context<ClusterModalController>;

export function ClusterModalControllerProvider(props: ParentProps) {
const [partialCluster, setPartialCluster] = createSignal<PartialCluster>({});
const requiredProps: (keyof PartialCluster)[] = ['name', 'mc_version', 'mod_loader'];

const [provider, setProvider] = createSignal<ClusterCreationProvider | undefined>();
const [finishFunction, setFinishFunction] = createSignal<FinishFn>(() => true);
const [steps, setSteps] = createSignal<CreationStage[]>([]);
const [stepComponents] = createStore<{ [key in CreationStage]: () => JSX.Element }>({
[CreationStage.PROVIDER_SELECTION]: ClusterProviderSelection,
[CreationStage.GAME_SETUP]: ClusterGameSetup,
[CreationStage.IMPORT_SELECTION]: () => <></>,
[CreationStage.IMPORT_SELECTION]: ClusterImportSelection,
});

const controller: ClusterModalController = {
Expand All @@ -64,12 +63,13 @@ export function ClusterModalControllerProvider(props: ParentProps) {
});
},

partialCluster,
setPartialCluster,
updatePartialCluster: (key, value) => setPartialCluster(prev => ({ ...prev, [key]: value })),
provider,
setProvider,

async start() {
setPartialCluster({});
finishFunction,
setFinishFunction,

start() {
controller.setStep(CreationStage.PROVIDER_SELECTION);
modal.show();
},
Expand All @@ -83,24 +83,17 @@ export function ClusterModalControllerProvider(props: ParentProps) {
modal.hide();
},

async finish() {
const untracked = untrack(partialCluster);

for (const prop of requiredProps)
if (!untracked[prop])
throw new Error(`Missing required property ${prop}`);
finish() {
const fn = finishFunction()();

modal.hide();

await bridge.commands.createCluster({
icon: null,
icon_url: null,
loader_version: null,
package_data: null,
skip: null,
skip_watch: null,
...untracked,
} as CreateCluster);
if (fn instanceof Promise)
fn.then((res) => {
if (res === true)
modal.hide();
});
else
if (fn === true)
modal.hide();
},
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Loader, VersionType } from '@onelauncher/client/bindings';
import type { CreateCluster, Loader, VersionType } from '@onelauncher/client/bindings';
import { TextInputIcon } from '@untitled-theme/icons-solid';
import { bridge } from '~imports';
import Checkbox from '~ui/components/base/Checkbox';
Expand All @@ -8,32 +8,79 @@ import TextField from '~ui/components/base/TextField';
import LoaderIcon from '~ui/components/game/LoaderIcon';
import useCommand from '~ui/hooks/useCommand';
import { formatVersionRelease, LOADERS } from '~utils';
import { createEffect, createSignal, For, Index, type JSX, onMount, Show, splitProps, untrack } from 'solid-js';
import { createEffect, createMemo, createSignal, For, Index, type JSX, onMount, Show, splitProps, untrack } from 'solid-js';
import { type ClusterStepProps, createClusterStep } from './ClusterCreationModal';

type PartialCluster = Partial<CreateCluster>;
type PartialClusterUpdateFunc = <K extends keyof PartialCluster>(key: K, value: PartialCluster[K]) => void;

export default createClusterStep({
message: 'Game Setup',
buttonType: 'create',
Component: ClusterGameSetup,
});

function _epicHardcodedLoaderVersionFilter(loader: Loader, version: string): boolean {
if (loader === 'vanilla')
return true;

const split = version.split('.')[1];
if (split === undefined)
return true;

const minor = Number.parseInt(split);
if (minor < 13)
return loader === 'forge' || loader === 'legacyfabric';
else
return loader === 'forge' || loader === 'fabric' || loader === 'neoforge' || loader === 'quilt';
}

function ClusterGameSetup(props: ClusterStepProps) {
const check = () => {
const hasName = (props.controller.partialCluster().name?.length ?? 0) > 0;
const hasVersion = (props.controller.partialCluster().mc_version?.length ?? 0) > 0;
const hasLoader = (props.controller.partialCluster().mod_loader?.length ?? 0) > 0;
const [partialCluster, setPartialCluster] = createSignal<PartialCluster>({
mod_loader: 'vanilla',
});

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

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

const setName = (name: string) => props.controller.updatePartialCluster('name', name);
const setVersion = (version: string) => props.controller.updatePartialCluster('mc_version', version);
const setLoader = (loader: Loader | string) => props.controller.updatePartialCluster('mod_loader', loader.toLowerCase() as Loader);
const getLoaders = createMemo(() => {
const version = partialCluster().mc_version ?? '';

return LOADERS.filter(loader => _epicHardcodedLoaderVersionFilter(loader, version));
});

createEffect(() => {
const hasName = (partialCluster().name?.length ?? 0) > 0;
const hasVersion = (partialCluster().mc_version?.length ?? 0) > 0;
const hasLoader = (partialCluster().mod_loader?.length ?? 0) > 0;

props.setCanGoForward(hasName && hasVersion && hasLoader);
});

onMount(() => {
setLoader('vanilla');
props.controller.setFinishFunction(() => async () => {
const untracked = untrack(partialCluster);

for (const prop of requiredProps)
if (!untracked[prop])
throw new Error(`Missing required property ${prop}`);

await bridge.commands.createCluster({
icon: null,
icon_url: null,
loader_version: null,
package_data: null,
skip: null,
skip_watch: null,
...untracked,
} as CreateCluster);

return true;
});
});

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

<Option header="Loader">
<Dropdown onChange={index => setLoader(LOADERS[index] || 'vanilla')}>
<For each={LOADERS}>
<Dropdown onChange={index => setLoader(getLoaders()[index] || 'vanilla')}>
<For each={getLoaders()}>
{loader => (
<Dropdown.Row>
<div class="flex flex-row gap-x-2">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { ImportType } from '@onelauncher/client/bindings';
import { bridge } from '~imports';
import SelectList from '~ui/components/base/SelectList';
import useCommand from '~ui/hooks/useCommand';
import { createEffect, createSignal, For, onMount, untrack } from 'solid-js';
import { type ClusterStepProps, createClusterStep } from './ClusterCreationModal';

export default createClusterStep({
message: 'Import Launcher',
buttonType: 'create',
Component: ClusterImportSelection,
});

function ClusterImportSelection(props: ClusterStepProps) {
const [instances] = useCommand(() => {
const importType = props.controller.provider();
if (importType === undefined || importType === 'New')
return [];

return bridge.commands.getLauncherInstances(importType, null);
});

const [selected, setSelected] = createSignal<number>();

createEffect(() => {
const index = selected();

props.setCanGoForward(() => {
if (index === undefined)
return false;

if (index >= 0)
return true;

return false;
});
});

onMount(() => {
// eslint-disable-next-line solid/reactivity -- It's fine
props.controller.setFinishFunction(() => async () => {
const index = untrack(selected);
if (index === undefined)
return false;

const list = untrack(instances);
if (list === undefined)
return false;

const instance = list[1][index];
if (instance === undefined)
return false;

const basePath = list[0];

const importType = untrack(props.controller.provider) as ImportType;

await bridge.commands.importInstances(importType, basePath, [instance]);

return true;
});
});

return (
<SelectList
class="h-52 max-h-52"
onChange={selected => setSelected(selected[0])}
>
<For each={instances()?.[1]}>
{(instance, index) => (
<SelectList.Row index={index()}>
{instance}
</SelectList.Row>
)}
</For>
</SelectList>
);
}
Loading

0 comments on commit db6e995

Please sign in to comment.