feat(ui): Create cluster modal now supports importing instances
This commit is contained in:
parent
623632a4fe
commit
db6e995836
4 changed files with 229 additions and 91 deletions
|
|
@ -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;
|
||||
|
|
@ -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 = {
|
||||
|
|
@ -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();
|
||||
},
|
||||
|
|
@ -83,24 +83,17 @@ export function ClusterModalControllerProvider(props: ParentProps) {
|
|||
modal.hide();
|
||||
},
|
||||
|
||||
async finish() {
|
||||
const untracked = untrack(partialCluster);
|
||||
finish() {
|
||||
const fn = finishFunction()();
|
||||
|
||||
for (const prop of requiredProps)
|
||||
if (!untracked[prop])
|
||||
throw new Error(`Missing required property ${prop}`);
|
||||
|
||||
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();
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -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',
|
||||
});
|
||||
|
||||
const requiredProps: (keyof PartialCluster)[] = ['name', 'mc_version', 'mod_loader'];
|
||||
|
||||
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 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);
|
||||
};
|
||||
|
||||
createEffect(check);
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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 (
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,82 +1,102 @@
|
|||
import { Upload01Icon, User03Icon } from '@untitled-theme/icons-solid';
|
||||
import CurseforgeIcon from '~assets/logos/curseforge.svg?component-solid';
|
||||
import ModrinthIcon from '~assets/logos/modrinth.svg?component-solid';
|
||||
import { createEffect, createSignal, Index, type JSX } from 'solid-js';
|
||||
import type { ImportType } from '@onelauncher/client/bindings';
|
||||
import { User03Icon } from '@untitled-theme/icons-solid';
|
||||
import LauncherIcon from '~ui/components/content/LauncherIcon';
|
||||
import { LAUNCHER_IMPORT_TYPES } from '~utils';
|
||||
import { createEffect, createSignal, For, type JSX } from 'solid-js';
|
||||
import { type ClusterStepProps, createClusterStep, CreationStage } from './ClusterCreationModal';
|
||||
|
||||
const providers: Omit<ProviderCardProps, 'selected' | 'setSelected'>[] = [
|
||||
{
|
||||
name: 'New',
|
||||
icon: <User03Icon />,
|
||||
},
|
||||
{
|
||||
name: 'Modrinth',
|
||||
icon: <ModrinthIcon color="#1bd96a" />,
|
||||
},
|
||||
{
|
||||
name: 'Curseforge',
|
||||
icon: <CurseforgeIcon color="#F16436" />,
|
||||
},
|
||||
{
|
||||
name: 'Import',
|
||||
icon: <Upload01Icon />,
|
||||
},
|
||||
];
|
||||
|
||||
export default createClusterStep({
|
||||
message: 'Select Provider',
|
||||
buttonType: 'next',
|
||||
Component: ClusterProviderSelection,
|
||||
});
|
||||
|
||||
export type ClusterCreationProvider = ImportType | 'New';
|
||||
|
||||
function ClusterProviderSelection(props: ClusterStepProps) {
|
||||
const [selected, setSelected] = createSignal<number>();
|
||||
|
||||
createEffect(() => {
|
||||
// TODO: Add more stages
|
||||
// eslint-disable-next-line solid/reactivity -- It's fine
|
||||
props.setCanGoForward(() => {
|
||||
const isTrue = selected() !== undefined;
|
||||
const index = selected();
|
||||
if (index === undefined)
|
||||
return false;
|
||||
|
||||
if (isTrue)
|
||||
if (index === -1) {
|
||||
props.controller.setProvider('New');
|
||||
props.setNextStage(CreationStage.GAME_SETUP);
|
||||
}
|
||||
else if (index >= 0) {
|
||||
props.controller.setProvider(LAUNCHER_IMPORT_TYPES[index]);
|
||||
props.setNextStage(CreationStage.IMPORT_SELECTION);
|
||||
}
|
||||
|
||||
return isTrue;
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<Index each={providers}>
|
||||
<ProviderCard
|
||||
icon={<User03Icon />}
|
||||
name="New"
|
||||
selected={selected() === -1}
|
||||
setSelected={() => setSelected(-1)}
|
||||
/>
|
||||
|
||||
<For each={LAUNCHER_IMPORT_TYPES}>
|
||||
{(provider, index) => (
|
||||
<ProviderCard
|
||||
{...provider()}
|
||||
selected={index === selected()}
|
||||
importType={provider}
|
||||
selected={index() === selected()}
|
||||
setSelected={() => setSelected(index)}
|
||||
/>
|
||||
)}
|
||||
</Index>
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ProviderCardProps {
|
||||
interface ProviderLauncherCardProps {
|
||||
importType: ImportType;
|
||||
}
|
||||
|
||||
interface ProviderCustomCardProps {
|
||||
icon: JSX.Element;
|
||||
name: string;
|
||||
}
|
||||
|
||||
type ProviderCardProps = (ProviderLauncherCardProps | ProviderCustomCardProps) & {
|
||||
setSelected: () => void;
|
||||
selected: boolean;
|
||||
};
|
||||
|
||||
function ProviderCard(props: ProviderCardProps) {
|
||||
const Icon = () => {
|
||||
if ('icon' in props)
|
||||
// eslint-disable-next-line solid/components-return-once -- ok
|
||||
return props.icon;
|
||||
|
||||
return <LauncherIcon launcher={props.importType} />;
|
||||
};
|
||||
|
||||
const Name = () => {
|
||||
if ('name' in props)
|
||||
return props.name;
|
||||
|
||||
return props.importType;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
class={`flex flex-col justify-center items-center gap-y-2 py-2 px-4 hover:bg-component-bg-hover active:bg-component-bg-pressed rounded-lg ${props.selected ? 'bg-component-bg' : ''}`}
|
||||
class={`flex flex-col justify-center items-center gap-y-3 py-2 px-4 hover:bg-component-bg-hover active:bg-component-bg-pressed rounded-lg ${props.selected ? 'bg-component-bg' : ''}`}
|
||||
onClick={() => props.setSelected()}
|
||||
>
|
||||
<div class="[&>svg]:(h-8 w-8)">
|
||||
{props.icon}
|
||||
<div class="h-8 w-8 flex items-center justify-center [&>svg]:(w-8 h-8!)">
|
||||
<Icon />
|
||||
</div>
|
||||
<span>{props.name}</span>
|
||||
<span>{Name()}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue