diff --git a/apps/frontend/src/ui/App.tsx b/apps/frontend/src/ui/App.tsx index 6eedb5d1..6c3bf1df 100644 --- a/apps/frontend/src/ui/App.tsx +++ b/apps/frontend/src/ui/App.tsx @@ -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) { + ); @@ -51,7 +52,7 @@ function GlobalContexts(props: ParentProps) { ModalProvider, SettingsProvider, AccountControllerProvider, - ClusterModalController, + ClusterModalControllerProvider, ]} > {props.children} diff --git a/apps/frontend/src/ui/components/game/LoaderIcon.tsx b/apps/frontend/src/ui/components/game/LoaderIcon.tsx index b4a5d7a5..7035ecc7 100644 --- a/apps/frontend/src/ui/components/game/LoaderIcon.tsx +++ b/apps/frontend/src/ui/components/game/LoaderIcon.tsx @@ -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 = { + vanilla: VanillaImage, + fabric: FabricImage, + legacyfabric: FabricImage, + forge: ForgeImage, + neoforge: ForgeImage, + quilt: QuiltImage, + }; + + return mapping[loaderName]; } type LoaderIconProp = JSX.HTMLAttributes & { @@ -26,7 +27,7 @@ type LoaderIconProp = JSX.HTMLAttributes & { function LoaderIcon(props: LoaderIconProp) { const [{ loader = 'vanilla' }, rest] = splitProps(props, ['loader']); return ( - {loader} + {`${loader}'s ); } diff --git a/apps/frontend/src/ui/components/overlay/Modal.tsx b/apps/frontend/src/ui/components/overlay/Modal.tsx index 2977f693..6f0fe895 100644 --- a/apps/frontend/src/ui/components/overlay/Modal.tsx +++ b/apps/frontend/src/ui/components/overlay/Modal.tsx @@ -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; -type ModalsList = (() => JSX.Element)[]; export function ModalProvider(props: ParentProps) { const [modals, setModals] = createStore([]); 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 ( {props.children} - modals.length > 0} - setVisible={(value) => { - if (value === false) - context.closeModal(); - }} - > - - <>{modals.length > 0 && modals[modals.length - 1]!()} - - ); } +export function ModalRenderer() { + const controller = useModalController(); + + return ( + controller.modals().length > 0} + setVisible={(value) => { + if (value === false) + controller.closeModal(); + }} + > + + 0}> + {controller.modals()[controller.modals().length - 1]!()} + + + + ); +} + 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(); - 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 | undefined; -} & ParentProps & CreateModal; +} & ParentProps & ModalController; function Modal(props: ModalProps) { return ( @@ -110,7 +128,7 @@ function Modal(props: ModalProps) { ); } -Modal.SplitProps = function (props: T) { +Modal.SplitProps = function (props: T) { return splitProps(props, ['hide', 'show']); }; diff --git a/apps/frontend/src/ui/components/overlay/cluster/ClusterCreationModal.tsx b/apps/frontend/src/ui/components/overlay/cluster/ClusterCreationModal.tsx index b6daf8e1..0a77053f 100644 --- a/apps/frontend/src/ui/components/overlay/cluster/ClusterCreationModal.tsx +++ b/apps/frontend/src/ui/components/overlay/cluster/ClusterCreationModal.tsx @@ -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; type PartialClusterUpdateFunc = (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; - setStep: Setter; +interface ClusterModalController { + step: Accessor; + setStep: (step: CreationStage | undefined) => void; + partialCluster: Accessor; setPartialCluster: Setter; updatePartialCluster: PartialClusterUpdateFunc; - start: () => Promise; // Returns true if its finished (created) successfully + + start: () => Promise; + previous: () => void; + cancel: () => void; finish: () => void; } -const ClusterModalContext = createContext() as Context; +const ClusterModalContext = createContext() as Context; -export enum ClusterModalStages { - STAGE_1_PROVIDER = 0, - STAGE_2_GAME_SETUP = 1, -} - -type ClusterModalStagesLength = UnionToArray['length']; - -export function ClusterModalController(props: ParentProps) { - const [step, setStep] = createSignal(ClusterModalStages.STAGE_1_PROVIDER); +export function ClusterModalControllerProvider(props: ParentProps) { const [partialCluster, setPartialCluster] = createSignal({}); - const [inProgress, setInProgress] = createSignal(false); - const [error, setError] = createSignal(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([]); + 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 - - )); + 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((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 ( - + {props.children} ); } -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; setCanGoForward: Setter; - controller: Accessor; + setNextStage: Setter; + 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 = [ - 'Provider selection', - 'Game Setup', - ]; +export function createClusterStep(props: CreateClusterStepType): () => JSX.Element { + return () => { + const controller = useClusterCreator(); + const [canGoForward, setCanGoForward] = createSignal(false); + const [nextStage, setNextStage] = createSignal(); - 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 ( -
-
-
- Header Image + return ( +
+
+
+ Header Image +
+
+ +
+

New Cluster

+ {props.message} +
+
-
- -
- {/** weird positioning?? */} -

New Cluster

- {messages[props.controller.step()]} +
+
+ +
+ +
+
-
-
- -
- -
- - -
-
-
- ); + ); + }; } diff --git a/apps/frontend/src/ui/components/overlay/cluster/ClusterStepTwo.tsx b/apps/frontend/src/ui/components/overlay/cluster/ClusterGameSetup.tsx similarity index 69% rename from apps/frontend/src/ui/components/overlay/cluster/ClusterStepTwo.tsx rename to apps/frontend/src/ui/components/overlay/cluster/ClusterGameSetup.tsx index 0db0c04b..94e03b3e 100644 --- a/apps/frontend/src/ui/components/overlay/cluster/ClusterStepTwo.tsx +++ b/apps/frontend/src/ui/components/overlay/cluster/ClusterGameSetup.tsx @@ -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: () => , - }, - { - name: 'Fabric', - icon: () => , - }, - { - name: 'Forge', - icon: () => , - }, - { - name: 'Quilt', - icon: () => , - }, -]; +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) {