refactor: ClusterCreationModal, prepare for multi stage onboarding and integrate with ModalController
This commit is contained in:
parent
7eb2f74297
commit
fcb7df0430
9 changed files with 278 additions and 282 deletions
|
|
@ -6,10 +6,10 @@ import ErrorBoundary from './components/ErrorBoundary';
|
|||
import AnimatedRoutes from './components/AnimatedRoutes';
|
||||
import NotificationOverlay from './components/overlay/notifications/NotificationOverlay';
|
||||
import { SettingsProvider } from './hooks/useSettings';
|
||||
import { ClusterModalController } from './components/overlay/cluster/ClusterCreationModal';
|
||||
import { ClusterModalControllerProvider } from './components/overlay/cluster/ClusterCreationModal';
|
||||
import { MultiProvider } from './components/MultiProvider';
|
||||
import { AccountControllerProvider } from './components/overlay/account/AddAccountModal';
|
||||
import { ModalProvider } from './components/overlay/Modal';
|
||||
import { ModalProvider, ModalRenderer } from './components/overlay/Modal';
|
||||
import { PROGRAM_INFO } from '~bindings';
|
||||
|
||||
function App(props: ParentProps) {
|
||||
|
|
@ -37,6 +37,7 @@ function App(props: ParentProps) {
|
|||
</div>
|
||||
|
||||
<NotificationOverlay />
|
||||
<ModalRenderer />
|
||||
</main>
|
||||
</GlobalContexts>
|
||||
);
|
||||
|
|
@ -51,7 +52,7 @@ function GlobalContexts(props: ParentProps) {
|
|||
ModalProvider,
|
||||
SettingsProvider,
|
||||
AccountControllerProvider,
|
||||
ClusterModalController,
|
||||
ClusterModalControllerProvider,
|
||||
]}
|
||||
>
|
||||
{props.children}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,23 @@
|
|||
import { type JSX, splitProps } from 'solid-js';
|
||||
import fabric from '~assets/logos/fabric.png';
|
||||
import forge from '~assets/logos/forge.png';
|
||||
import quilt from '~assets/logos/quilt.png';
|
||||
import vanilla from '~assets/logos/vanilla.png';
|
||||
import type { Loader } from '~bindings';
|
||||
import FabricImage from '~assets/logos/fabric.png';
|
||||
import ForgeImage from '~assets/logos/forge.png';
|
||||
import QuiltImage from '~assets/logos/quilt.png';
|
||||
import VanillaImage from '~assets/logos/vanilla.png';
|
||||
import type { Cluster, Loader } from '~bindings';
|
||||
|
||||
export function getLoaderIcon(loader: Loader): string {
|
||||
switch (loader) {
|
||||
case 'forge':
|
||||
return forge;
|
||||
case 'fabric':
|
||||
return fabric;
|
||||
case 'quilt':
|
||||
return quilt;
|
||||
case 'vanilla':
|
||||
default:
|
||||
return vanilla;
|
||||
}
|
||||
export function getLoaderLogoSrc(loader: Cluster | Loader): string {
|
||||
const loaderName = (typeof loader === 'string' ? loader : loader.meta.loader)?.toLowerCase() as Loader;
|
||||
|
||||
const mapping: Record<Loader, string> = {
|
||||
vanilla: VanillaImage,
|
||||
fabric: FabricImage,
|
||||
legacyfabric: FabricImage,
|
||||
forge: ForgeImage,
|
||||
neoforge: ForgeImage,
|
||||
quilt: QuiltImage,
|
||||
};
|
||||
|
||||
return mapping[loaderName];
|
||||
}
|
||||
|
||||
type LoaderIconProp = JSX.HTMLAttributes<HTMLImageElement> & {
|
||||
|
|
@ -26,7 +27,7 @@ type LoaderIconProp = JSX.HTMLAttributes<HTMLImageElement> & {
|
|||
function LoaderIcon(props: LoaderIconProp) {
|
||||
const [{ loader = 'vanilla' }, rest] = splitProps(props, ['loader']);
|
||||
return (
|
||||
<img {...rest} src={getLoaderIcon(loader)} alt={loader} />
|
||||
<img {...rest} src={getLoaderLogoSrc(loader)} alt={`${loader}'s logo`} />
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import type {
|
|||
ParentProps,
|
||||
Ref,
|
||||
} from 'solid-js';
|
||||
import { For, createContext, createSignal, onCleanup, onMount, splitProps, useContext } from 'solid-js';
|
||||
import { For, Show, createContext, createSignal, onCleanup, onMount, splitProps, useContext } from 'solid-js';
|
||||
import { mergeRefs } from '@solid-primitives/refs';
|
||||
import { createStore } from 'solid-js/store';
|
||||
import { Transition } from 'solid-transition-group';
|
||||
|
|
@ -12,18 +12,23 @@ import Button from '../base/Button';
|
|||
import FullscreenOverlay from './FullscreenOverlay';
|
||||
import type { MakeOptional } from '~types.ts';
|
||||
|
||||
type ModalsList = (() => JSX.Element)[];
|
||||
|
||||
interface ModalContextType {
|
||||
modals: () => ModalsList;
|
||||
displayModal: (modal: () => JSX.Element) => number;
|
||||
closeModal: (index?: number) => void;
|
||||
isVisible: (index: number) => boolean;
|
||||
};
|
||||
|
||||
const ModalContext = createContext() as Context<ModalContextType>;
|
||||
type ModalsList = (() => JSX.Element)[];
|
||||
|
||||
export function ModalProvider(props: ParentProps) {
|
||||
const [modals, setModals] = createStore<ModalsList>([]);
|
||||
|
||||
const context: ModalContextType = {
|
||||
modals: () => modals,
|
||||
|
||||
displayModal: (modal) => {
|
||||
setModals(prev => [...prev, modal]);
|
||||
return modals.length - 1;
|
||||
|
|
@ -40,34 +45,47 @@ export function ModalProvider(props: ParentProps) {
|
|||
|
||||
setModals(prev => prev.slice(0, -1));
|
||||
},
|
||||
|
||||
isVisible: (index) => {
|
||||
return modals.length > index;
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalContext.Provider value={context}>
|
||||
{props.children}
|
||||
<FullscreenOverlay
|
||||
visible={() => modals.length > 0}
|
||||
setVisible={(value) => {
|
||||
if (value === false)
|
||||
context.closeModal();
|
||||
}}
|
||||
>
|
||||
<Transition
|
||||
mode="outin"
|
||||
enterClass="modal-animation-enter"
|
||||
enterActiveClass="modal-animation-enter-active"
|
||||
enterToClass="modal-animation-enter-to"
|
||||
exitClass="modal-animation-leave"
|
||||
exitActiveClass="modal-animation-leave-active"
|
||||
exitToClass="modal-animation-leave-to"
|
||||
>
|
||||
<>{modals.length > 0 && modals[modals.length - 1]!()}</>
|
||||
</Transition>
|
||||
</FullscreenOverlay>
|
||||
</ModalContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function ModalRenderer() {
|
||||
const controller = useModalController();
|
||||
|
||||
return (
|
||||
<FullscreenOverlay
|
||||
visible={() => controller.modals().length > 0}
|
||||
setVisible={(value) => {
|
||||
if (value === false)
|
||||
controller.closeModal();
|
||||
}}
|
||||
>
|
||||
<Transition
|
||||
mode="outin"
|
||||
enterClass="modal-animation-enter"
|
||||
enterActiveClass="modal-animation-enter-active"
|
||||
enterToClass="modal-animation-enter-to"
|
||||
exitClass="modal-animation-leave"
|
||||
exitActiveClass="modal-animation-leave-active"
|
||||
exitToClass="modal-animation-leave-to"
|
||||
>
|
||||
<Show when={controller.modals().length > 0}>
|
||||
{controller.modals()[controller.modals().length - 1]!()}
|
||||
</Show>
|
||||
</Transition>
|
||||
</FullscreenOverlay>
|
||||
);
|
||||
}
|
||||
|
||||
export function useModalController() {
|
||||
const context = useContext(ModalContext);
|
||||
|
||||
|
|
@ -77,16 +95,16 @@ export function useModalController() {
|
|||
return context;
|
||||
}
|
||||
|
||||
interface CreateModal {
|
||||
export interface ModalController {
|
||||
show: () => void;
|
||||
hide: () => void;
|
||||
};
|
||||
|
||||
export function createModal(el: (props: CreateModal) => JSX.Element): CreateModal {
|
||||
export function createModal(el: (props: ModalController) => JSX.Element): ModalController {
|
||||
const controller = useModalController();
|
||||
const [index, setIndex] = createSignal<number>();
|
||||
|
||||
const ctx: CreateModal = {
|
||||
const ctx: ModalController = {
|
||||
show: () => {
|
||||
setIndex(controller.displayModal(() => el(ctx)));
|
||||
},
|
||||
|
|
@ -100,7 +118,7 @@ export function createModal(el: (props: CreateModal) => JSX.Element): CreateModa
|
|||
|
||||
export type ModalProps = {
|
||||
ref?: Ref<HTMLDivElement> | undefined;
|
||||
} & ParentProps & CreateModal;
|
||||
} & ParentProps & ModalController;
|
||||
|
||||
function Modal(props: ModalProps) {
|
||||
return (
|
||||
|
|
@ -110,7 +128,7 @@ function Modal(props: ModalProps) {
|
|||
);
|
||||
}
|
||||
|
||||
Modal.SplitProps = function <T extends CreateModal>(props: T) {
|
||||
Modal.SplitProps = function <T extends ModalController>(props: T) {
|
||||
return splitProps(props, ['hide', 'show']);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,238 +1,223 @@
|
|||
import { type Accessor, type Context, Match, type ParentProps, type Setter, Switch, createContext, createEffect, createSignal, onCleanup, untrack, useContext } from 'solid-js';
|
||||
import { type Accessor, type Context, type JSX, type ParentProps, type Setter, Show, createContext, createEffect, createSignal, onMount, untrack, useContext } from 'solid-js';
|
||||
import { ArrowRightIcon, Server01Icon } from '@untitled-theme/icons-solid';
|
||||
import { createStore } from 'solid-js/store';
|
||||
import HeaderImage from '../../../../assets/images/header.png';
|
||||
import { createModal } from '../Modal';
|
||||
import ClusterStepOne from './ClusterStepOne';
|
||||
import { ClusterStepTwo } from './ClusterStepTwo';
|
||||
import ClusterGameSetup from './ClusterGameSetup';
|
||||
import ClusterProviderSelection from './ClusterProviderSelection';
|
||||
import Button from '~ui/components/base/Button';
|
||||
import type { CreateCluster } from '~bindings';
|
||||
import { bridge } from '~imports';
|
||||
import type { FixedArray, UnionToArray } from '~types.ts';
|
||||
|
||||
export enum CreationStage {
|
||||
PROVIDER_SELECTION = 0,
|
||||
GAME_SETUP = 1,
|
||||
IMPORT_SELECTION = 2,
|
||||
}
|
||||
|
||||
type PartialCluster = Partial<CreateCluster>;
|
||||
type PartialClusterUpdateFunc = <K extends keyof PartialCluster>(key: K, value: PartialCluster[K]) => any;
|
||||
|
||||
// Why the fuck do I need to use a context for all this???
|
||||
// TODO: Rewrite for use with the new Modal stacking system
|
||||
interface ClusterModalContextFunc {
|
||||
step: Accessor<number>;
|
||||
setStep: Setter<number>;
|
||||
interface ClusterModalController {
|
||||
step: Accessor<CreationStage | undefined>;
|
||||
setStep: (step: CreationStage | undefined) => void;
|
||||
|
||||
partialCluster: Accessor<PartialCluster>;
|
||||
setPartialCluster: Setter<PartialCluster>;
|
||||
updatePartialCluster: PartialClusterUpdateFunc;
|
||||
start: () => Promise<void>; // Returns true if its finished (created) successfully
|
||||
|
||||
start: () => Promise<void>;
|
||||
previous: () => void;
|
||||
cancel: () => void;
|
||||
finish: () => void;
|
||||
}
|
||||
|
||||
const ClusterModalContext = createContext<ClusterModalContextFunc>() as Context<ClusterModalContextFunc>;
|
||||
const ClusterModalContext = createContext() as Context<ClusterModalController>;
|
||||
|
||||
export enum ClusterModalStages {
|
||||
STAGE_1_PROVIDER = 0,
|
||||
STAGE_2_GAME_SETUP = 1,
|
||||
}
|
||||
|
||||
type ClusterModalStagesLength = UnionToArray<keyof typeof ClusterModalStages>['length'];
|
||||
|
||||
export function ClusterModalController(props: ParentProps) {
|
||||
const [step, setStep] = createSignal<number>(ClusterModalStages.STAGE_1_PROVIDER);
|
||||
export function ClusterModalControllerProvider(props: ParentProps) {
|
||||
const [partialCluster, setPartialCluster] = createSignal<PartialCluster>({});
|
||||
const [inProgress, setInProgress] = createSignal(false);
|
||||
const [error, setError] = createSignal<string | undefined>(undefined);
|
||||
const requiredProps: (keyof PartialCluster)[] = ['name', 'mc_version', 'mod_loader'];
|
||||
|
||||
const updatePartialCluster: PartialClusterUpdateFunc = (key, value) => {
|
||||
setPartialCluster((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
[key]: value,
|
||||
};
|
||||
});
|
||||
};
|
||||
const [steps, setSteps] = createSignal<CreationStage[]>([]);
|
||||
const [stepComponents] = createStore<{ [key in CreationStage]: () => JSX.Element }>({
|
||||
[CreationStage.PROVIDER_SELECTION]: ClusterProviderSelection,
|
||||
[CreationStage.GAME_SETUP]: ClusterGameSetup,
|
||||
[CreationStage.IMPORT_SELECTION]: () => <></>,
|
||||
});
|
||||
|
||||
const modal = createModal(() => (
|
||||
// eslint-disable-next-line ts/no-use-before-define -- circular dependency should be refactored later
|
||||
<ClusterCreationModal controller={context} />
|
||||
));
|
||||
const controller: ClusterModalController = {
|
||||
step: () => {
|
||||
return steps()[steps().length - 1];
|
||||
},
|
||||
setStep: (stage) => {
|
||||
setSteps((prev) => {
|
||||
if (stage === undefined)
|
||||
return prev.slice(0, -1);
|
||||
|
||||
return [...prev, stage];
|
||||
});
|
||||
},
|
||||
|
||||
const context: ClusterModalContextFunc = {
|
||||
step,
|
||||
setStep,
|
||||
partialCluster,
|
||||
setPartialCluster,
|
||||
updatePartialCluster,
|
||||
|
||||
start() {
|
||||
setPartialCluster({});
|
||||
setStep(ClusterModalStages.STAGE_1_PROVIDER);
|
||||
setError(undefined);
|
||||
setInProgress(true);
|
||||
modal.show();
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
createEffect(() => {
|
||||
const result = error();
|
||||
if (inProgress() === true)
|
||||
return;
|
||||
|
||||
if (result === undefined || result.length === 0)
|
||||
resolve();
|
||||
else
|
||||
reject(result);
|
||||
});
|
||||
updatePartialCluster: (key, value) => {
|
||||
setPartialCluster((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
[key]: value,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
async start() {
|
||||
setPartialCluster({});
|
||||
controller.setStep(CreationStage.PROVIDER_SELECTION);
|
||||
},
|
||||
|
||||
previous() {
|
||||
controller.setStep(undefined);
|
||||
},
|
||||
|
||||
cancel() {
|
||||
setSteps([]);
|
||||
},
|
||||
|
||||
async finish() {
|
||||
const untracked = untrack(partialCluster);
|
||||
|
||||
if (untracked.name === undefined || untracked.mc_version === undefined || untracked.mod_loader === undefined)
|
||||
throw new Error('Cluster is missing required fields');
|
||||
|
||||
modal.hide();
|
||||
for (const prop of requiredProps)
|
||||
if (!untracked[prop])
|
||||
throw new Error(`Missing required property ${prop}`);
|
||||
|
||||
// @ts-expect-error -- TODO: Do this properly
|
||||
await bridge.commands.createCluster({
|
||||
name: untracked.name!,
|
||||
mod_loader: untracked.mod_loader!,
|
||||
mc_version: untracked.mc_version!,
|
||||
// TODO: Implement the rest of the fields
|
||||
icon: null,
|
||||
icon_url: null,
|
||||
loader_version: null,
|
||||
package_data: null,
|
||||
skip: null,
|
||||
skip_watch: null,
|
||||
...untracked,
|
||||
});
|
||||
|
||||
setInProgress(false);
|
||||
},
|
||||
};
|
||||
|
||||
onCleanup(() => {
|
||||
setStep(ClusterModalStages.STAGE_1_PROVIDER);
|
||||
const modal = createModal(() => (
|
||||
<>{stepComponents[controller.step()!]()}</>
|
||||
));
|
||||
|
||||
createEffect(() => {
|
||||
if (steps().length > 0)
|
||||
modal.show();
|
||||
else
|
||||
modal.hide();
|
||||
});
|
||||
|
||||
return (
|
||||
<ClusterModalContext.Provider value={context}>
|
||||
<ClusterModalContext.Provider value={controller}>
|
||||
{props.children}
|
||||
</ClusterModalContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useClusterModalController() {
|
||||
export function useClusterCreator() {
|
||||
const ctx = useContext(ClusterModalContext);
|
||||
|
||||
if (!ctx)
|
||||
throw new Error('useClusterModalController must be used within a ClusterModalController');
|
||||
throw new Error('useClusterCreator must be used within a ClusterModalControllerProvider');
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
type ButtonType = 'next' | 'create';
|
||||
|
||||
export interface ClusterStep {
|
||||
stage: CreationStage;
|
||||
component: (props: ClusterStepProps) => JSX.Element;
|
||||
}
|
||||
|
||||
export interface ClusterStepProps {
|
||||
isVisible: Accessor<boolean>;
|
||||
setCanGoForward: Setter<boolean>;
|
||||
controller: Accessor<ClusterModalContextFunc>;
|
||||
setNextStage: Setter<CreationStage | undefined>;
|
||||
controller: ClusterModalController;
|
||||
};
|
||||
|
||||
function ClusterCreationModal(props: { controller: ClusterModalContextFunc }) {
|
||||
const buttonIsNext = () => props.controller.step() !== ((Object.keys(ClusterModalStages).length / 2) - 1);
|
||||
interface CreateClusterStepType {
|
||||
message: string;
|
||||
buttonType: ButtonType;
|
||||
Component: (props: ClusterStepProps) => JSX.Element;
|
||||
}
|
||||
|
||||
const [canGoForward, setCanGoForward] = createSignal(false);
|
||||
// Indexed by the current step
|
||||
const messages: FixedArray<string, ClusterModalStagesLength> = [
|
||||
'Provider selection',
|
||||
'Game Setup',
|
||||
];
|
||||
export function createClusterStep(props: CreateClusterStepType): () => JSX.Element {
|
||||
return () => {
|
||||
const controller = useClusterCreator();
|
||||
const [canGoForward, setCanGoForward] = createSignal(false);
|
||||
const [nextStage, setNextStage] = createSignal<CreationStage | undefined>();
|
||||
|
||||
const stepComponents = [
|
||||
ClusterStepOne,
|
||||
ClusterStepTwo,
|
||||
].map((Component, index) => (
|
||||
Component({
|
||||
setCanGoForward,
|
||||
isVisible: () => props.controller.step() === index,
|
||||
controller: () => props.controller,
|
||||
})
|
||||
));
|
||||
const previousButtonText = () => {
|
||||
if (controller.step() === CreationStage.PROVIDER_SELECTION)
|
||||
return 'Cancel';
|
||||
else
|
||||
return 'Previous';
|
||||
};
|
||||
|
||||
const Step = (props: { step: number }) => <>{stepComponents[props.step]}</>;
|
||||
function previousBtnClick() {
|
||||
if (controller.step() === CreationStage.PROVIDER_SELECTION)
|
||||
controller.cancel();
|
||||
else
|
||||
controller.previous();
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
// controller.setVisible(false);
|
||||
}
|
||||
function btnClick() {
|
||||
if (props.buttonType === 'create')
|
||||
controller.finish();
|
||||
else if (props.buttonType === 'next')
|
||||
controller.setStep(nextStage());
|
||||
}
|
||||
|
||||
function prev() {
|
||||
props.controller.setStep(prev => prev - 1);
|
||||
}
|
||||
|
||||
function next() {
|
||||
props.controller.setStep(prev => prev + 1);
|
||||
}
|
||||
|
||||
function finish() {
|
||||
props.controller.finish();
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="bg-primary rounded-lg text-center flex flex-col min-w-sm">
|
||||
<div class="flex relative h-25">
|
||||
<div class="absolute top-0 left-0 w-full h-full">
|
||||
<img class="h-full w-full rounded-t-lg" src={HeaderImage} alt="Header Image" />
|
||||
return (
|
||||
<div class="bg-primary rounded-lg text-center flex flex-col min-w-sm">
|
||||
<div class="flex relative h-25">
|
||||
<div class="absolute top-0 left-0 w-full h-full">
|
||||
<img class="h-full w-full rounded-t-lg" src={HeaderImage} alt="Header Image" />
|
||||
</div>
|
||||
<div
|
||||
class="absolute top-0 left-0 px-10 h-full flex flex-row justify-start items-center gap-x-4 bg-[radial-gradient(at_center,#00000077,transparent)]"
|
||||
>
|
||||
<Server01Icon class="w-8 h-8" />
|
||||
<div class="flex flex-col items-start justify-center">
|
||||
<h1 class="h-10 -mt-2">New Cluster</h1>
|
||||
<span>{props.message}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="absolute top-0 left-0 px-10 h-full flex flex-row justify-start items-center gap-x-4 bg-[radial-gradient(at_center,#00000077,transparent)]"
|
||||
>
|
||||
<Server01Icon class="w-8 h-8" />
|
||||
<div class="flex flex-col items-start justify-center">
|
||||
{/** weird positioning?? */}
|
||||
<h1 class="h-10 -mt-2">New Cluster</h1>
|
||||
<span>{messages[props.controller.step()]}</span>
|
||||
<div class="flex flex-col border border-white/5 rounded-b-lg">
|
||||
<div class="p-3">
|
||||
<props.Component
|
||||
controller={controller}
|
||||
setCanGoForward={setCanGoForward}
|
||||
setNextStage={setNextStage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-2 justify-end pt-0 p-3">
|
||||
<Button
|
||||
class="capitalize"
|
||||
children={previousButtonText()}
|
||||
buttonStyle="ghost"
|
||||
onClick={previousBtnClick}
|
||||
/>
|
||||
|
||||
<Button
|
||||
children={props.buttonType}
|
||||
class="capitalize"
|
||||
buttonStyle="primary"
|
||||
disabled={canGoForward() !== true}
|
||||
iconRight={<ArrowRightIcon />}
|
||||
onClick={btnClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col border border-white/5 rounded-b-lg">
|
||||
<div class="p-3">
|
||||
<Step step={props.controller.step()} />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-2 justify-end pt-0 p-3">
|
||||
<Switch>
|
||||
<Match when={props.controller.step() === 0}>
|
||||
<Button
|
||||
children="Cancel"
|
||||
buttonStyle="ghost"
|
||||
onClick={cancel}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={props.controller.step() >= 1}>
|
||||
<Button
|
||||
children="Previous"
|
||||
buttonStyle="ghost"
|
||||
onClick={prev}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
<Switch>
|
||||
<Match when={buttonIsNext() === true}>
|
||||
<Button
|
||||
children="Next"
|
||||
buttonStyle="primary"
|
||||
disabled={!canGoForward()}
|
||||
iconRight={<ArrowRightIcon />}
|
||||
onClick={next}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={buttonIsNext() === false}>
|
||||
<Button
|
||||
children="Create"
|
||||
buttonStyle="primary"
|
||||
disabled={!canGoForward()}
|
||||
iconRight={<ArrowRightIcon />}
|
||||
onClick={finish}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,59 +1,40 @@
|
|||
import { TextInputIcon } from '@untitled-theme/icons-solid';
|
||||
import { For, Index, type JSX, Show, createEffect, createSignal, on, onMount, splitProps, untrack } from 'solid-js';
|
||||
import type { ClusterStepProps } from './ClusterCreationModal';
|
||||
import { For, Index, type JSX, Show, createEffect, createSignal, onMount, splitProps, untrack } from 'solid-js';
|
||||
import { type ClusterStepProps, createClusterStep } from './ClusterCreationModal';
|
||||
import Dropdown from '~ui/components/base/Dropdown';
|
||||
import TextField from '~ui/components/base/TextField';
|
||||
import VanillaImage from '~assets/logos/vanilla.png';
|
||||
import FabricImage from '~assets/logos/fabric.png';
|
||||
import ForgeImage from '~assets/logos/forge.png';
|
||||
import QuiltImage from '~assets/logos/quilt.png';
|
||||
import useCommand from '~ui/hooks/useCommand';
|
||||
import { bridge } from '~imports';
|
||||
import SelectList from '~ui/components/base/SelectList';
|
||||
import Checkbox from '~ui/components/base/Checkbox';
|
||||
import type { Loader, VersionType } from '~bindings';
|
||||
import { formatVersionRelease } from '~utils';
|
||||
import { formatVersionRelease, getLoaders } from '~utils';
|
||||
import LoaderIcon from '~ui/components/game/LoaderIcon';
|
||||
|
||||
const loaders: {
|
||||
name: string;
|
||||
icon: () => JSX.Element;
|
||||
}[] = [
|
||||
{
|
||||
name: 'Vanilla',
|
||||
icon: () => <img src={VanillaImage} />,
|
||||
},
|
||||
{
|
||||
name: 'Fabric',
|
||||
icon: () => <img src={FabricImage} />,
|
||||
},
|
||||
{
|
||||
name: 'Forge',
|
||||
icon: () => <img src={ForgeImage} />,
|
||||
},
|
||||
{
|
||||
name: 'Quilt',
|
||||
icon: () => <img src={QuiltImage} />,
|
||||
},
|
||||
];
|
||||
export default createClusterStep({
|
||||
message: 'Game Setup',
|
||||
buttonType: 'create',
|
||||
Component: ClusterGameSetup,
|
||||
});
|
||||
|
||||
export function ClusterStepTwo(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;
|
||||
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;
|
||||
|
||||
props.setCanGoForward(hasName && hasVersion && hasLoader);
|
||||
};
|
||||
// props.setCanGoForward(hasName && hasVersion && hasLoader);
|
||||
// };
|
||||
|
||||
createEffect(check);
|
||||
createEffect(on(() => props.isVisible(), (curr: boolean) => {
|
||||
if (curr)
|
||||
check();
|
||||
}));
|
||||
// createEffect(check);
|
||||
// createEffect(on(() => props.isVisible(), (curr: boolean) => {
|
||||
// if (curr)
|
||||
// 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);
|
||||
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');
|
||||
|
|
@ -74,15 +55,15 @@ export function ClusterStepTwo(props: ClusterStepProps) {
|
|||
</Option>
|
||||
|
||||
<Option header="Loader">
|
||||
<Dropdown onChange={index => setLoader(loaders[index]?.name || '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">
|
||||
<div class="w-4 h-4">
|
||||
<loader.icon />
|
||||
<LoaderIcon loader={loader} />
|
||||
</div>
|
||||
{loader.name}
|
||||
{loader}
|
||||
</div>
|
||||
</Dropdown.Row>
|
||||
)}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { Upload01Icon, User03Icon } from '@untitled-theme/icons-solid';
|
||||
import { Index, type JSX, createEffect, createSignal, on } from 'solid-js';
|
||||
import type { ClusterStepProps } from './ClusterCreationModal';
|
||||
import { Index, type JSX, createSignal } from 'solid-js';
|
||||
import { type ClusterStepProps, createClusterStep } from './ClusterCreationModal';
|
||||
import ModrinthIcon from '~assets/logos/modrinth.svg?component-solid';
|
||||
import CurseforgeIcon from '~assets/logos/curseforge.svg?component-solid';
|
||||
|
||||
|
|
@ -23,18 +23,24 @@ const providers: Omit<ProviderCardProps, 'selected' | 'setSelected'>[] = [
|
|||
},
|
||||
];
|
||||
|
||||
export default function ClusterStepOne(props: ClusterStepProps) {
|
||||
export default createClusterStep({
|
||||
message: 'Select Provider',
|
||||
buttonType: 'next',
|
||||
Component: ClusterProviderSelection,
|
||||
});
|
||||
|
||||
function ClusterProviderSelection(_props: ClusterStepProps) {
|
||||
const [selected, setSelected] = createSignal<number>();
|
||||
|
||||
const check = () => {
|
||||
props.setCanGoForward(selected() !== undefined);
|
||||
};
|
||||
// const check = () => {
|
||||
// props.setCanGoForward(selected() !== undefined);
|
||||
// };
|
||||
|
||||
createEffect(check);
|
||||
createEffect(on(() => props.isVisible(), (curr: boolean) => {
|
||||
if (curr)
|
||||
check();
|
||||
}));
|
||||
// createEffect(check);
|
||||
// createEffect(on(() => props.isVisible(), (curr: boolean) => {
|
||||
// if (curr)
|
||||
// check();
|
||||
// }));
|
||||
|
||||
return (
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
|
|
@ -17,13 +17,13 @@ import type { Cluster } from '~bindings';
|
|||
import useCommand from '~ui/hooks/useCommand';
|
||||
import { bridge } from '~imports';
|
||||
import { secondsToWords, upperFirst } from '~utils';
|
||||
import { useClusterModalController } from '~ui/components/overlay/cluster/ClusterCreationModal';
|
||||
import { useClusterCreator } from '~ui/components/overlay/cluster/ClusterCreationModal';
|
||||
|
||||
type GroupedClusters = Record<string, Cluster[]>;
|
||||
|
||||
function HomePage() {
|
||||
const [clusters, { refetch }] = useCommand<GroupedClusters>(bridge.commands.getClustersGrouped);
|
||||
const controller = useClusterModalController();
|
||||
const controller = useClusterCreator();
|
||||
|
||||
const containerIds = (list: GroupedClusters | undefined) => Object.keys(list || []);
|
||||
|
||||
|
|
|
|||
|
|
@ -96,3 +96,7 @@ export function asEnvVariables(str: string): [string, string][] {
|
|||
})
|
||||
.filter(pair => pair !== null);
|
||||
}
|
||||
|
||||
export function getLoaders(): Loader[] {
|
||||
return ['vanilla', 'fabric', 'forge', 'neoforge', 'quilt'];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,33 +111,33 @@ pub async fn import_instances(import: ImportType, path: PathBuf) -> crate::Resul
|
|||
pub async fn import_instance(
|
||||
cluster_path: ClusterPath,
|
||||
import: ImportType,
|
||||
path: PathBuf,
|
||||
base_path: PathBuf,
|
||||
instance_path: String,
|
||||
) -> crate::Result<()> {
|
||||
tracing::debug!("importing instance from {instance_path}");
|
||||
|
||||
let result = match import {
|
||||
ImportType::MultiMC | ImportType::PrismLauncher => {
|
||||
multibased::import_mmc(path, instance_path, cluster_path.clone()).await
|
||||
multibased::import_mmc(base_path, instance_path, cluster_path.clone()).await
|
||||
}
|
||||
ImportType::ATLauncher => {
|
||||
atlauncher::import_atlauncher(path, instance_path, cluster_path.clone()).await
|
||||
atlauncher::import_atlauncher(base_path, instance_path, cluster_path.clone()).await
|
||||
}
|
||||
ImportType::GDLauncher => {
|
||||
gdlauncher::import_gdlauncher(
|
||||
path.join("instances").join(instance_path),
|
||||
base_path.join("instances").join(instance_path),
|
||||
cluster_path.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
ImportType::Curseforge => {
|
||||
curseforge::import_curseforge(
|
||||
path.join("Instances").join(instance_path),
|
||||
base_path.join("Instances").join(instance_path),
|
||||
cluster_path.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
// ImportType::Modrinth => modrinth::import_modrinth(path.join("profiles").join(instance_path), cluster_path.clone()).await,
|
||||
ImportType::Modrinth => modrinth::import_modrinth(base_path.join("profiles").join(instance_path), cluster_path.clone()).await,
|
||||
// ImportType::TLauncher => tlauncher::import_tlauncher(path.join("instances").join(instance_path), cluster_path.clone()).await,
|
||||
// ImportType::Technic => technic::import_technic(path.join("instances").join(instance_path), cluster_path.clone()).await,
|
||||
// ImportType::FTBLauncher => ftb::import_ftb(path.join("instances").join(instance_path), cluster_path.clone()).await,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue