diff --git a/apps/frontend/src/ui/components/base/SelectList.tsx b/apps/frontend/src/ui/components/base/SelectList.tsx index 42c25d43..6d3db14b 100644 --- a/apps/frontend/src/ui/components/base/SelectList.tsx +++ b/apps/frontend/src/ui/components/base/SelectList.tsx @@ -1,21 +1,51 @@ import { OverlayScrollbarsComponent } from 'overlayscrollbars-solid'; -import { type Context, createContext, createEffect, createSignal, type JSX, type ParentProps, type Signal, splitProps, useContext } from 'solid-js'; +import { type Accessor, type Context, createContext, createEffect, createSignal, type JSX, type ParentProps, type Setter, splitProps, useContext } from 'solid-js'; import styles from './SelectList.module.scss'; -type SelectListContextHelpers = Signal; +interface SelectListContextHelpers { + selected: Accessor; + setSelected: Setter; + select: (index: number) => void; +} const SelectListContext = createContext() as Context; function SelectListContextProvider(props: ParentProps & { - onChanged?: ((index: number | undefined) => void) | undefined; + onChanged?: ((index: number[]) => void) | undefined; + multiple?: boolean | undefined; + selected?: number[] | undefined; }) { - const [selected, setSelected] = createSignal(); + // eslint-disable-next-line solid/reactivity -- - + const [selected, setSelected] = createSignal(props.selected ?? []); createEffect(() => { props.onChanged?.(selected()); }); + createEffect(() => { + if (props.selected !== undefined) + setSelected(props.selected); + }); + + const ctx = { + selected, + setSelected, + select(index: number) { + setSelected((prev) => { + if (props.multiple) { + if (prev.includes(index)) + return prev.filter(i => i !== index); + + return [...prev, index]; + } + else { + return [index]; + } + }); + }, + }; + return ( - + {props.children} ); @@ -26,13 +56,20 @@ function useSelectListContext() { } export type SelectListProps = Omit, 'onChange'> & { - onChange?: (index: number | undefined) => void; + onChange?: (index: number[]) => void; + multiple?: boolean | undefined; + selected?: number[] | undefined; }; function SelectList(props: SelectListProps) { const [split, rest] = splitProps(props, ['class', 'onChange']); + return ( - +
@@ -45,20 +82,33 @@ function SelectList(props: SelectListProps) { } export type SelectListRowProps = JSX.HTMLAttributes & { - index?: number; + index: number; }; SelectList.Row = (props: SelectListRowProps) => { - const [selected, setSelected] = useSelectListContext(); + const { selected, setSelected, select } = useSelectListContext(); const [split, rest] = splitProps(props, ['index', 'class', 'onClick']); return (
{ if (props.index !== undefined) - setSelected(props.index); + if (e.shiftKey) { + const indexes = selected(); + const last = indexes[indexes.length - 1]!; + const start = last !== undefined ? Math.min(last, props.index) : props.index; + const end = Math.max(last, props.index) || start; + const newIndexes = Array.from({ length: end - start + 1 }, (_, i) => i + start); + setSelected(prev => [ + ...prev.filter(i => !newIndexes.includes(i)), + ...newIndexes, + ]); + } + else { + select(split.index); + } if (typeof split.onClick === 'function') split.onClick(e); diff --git a/apps/frontend/src/ui/components/overlay/Modal.tsx b/apps/frontend/src/ui/components/overlay/Modal.tsx index 9dfe7b69..50e76f39 100644 --- a/apps/frontend/src/ui/components/overlay/Modal.tsx +++ b/apps/frontend/src/ui/components/overlay/Modal.tsx @@ -156,7 +156,7 @@ Modal.Simple = function (props: ModalSimpleProps) {
{button => ( - + )}
@@ -304,12 +304,12 @@ Modal.Delete = function (props: ModalDeleteProps) { ); }; -function ModalButton(button: { props: ModalButtonProps }) { +function ModalButton(button: { hide: () => void; props: ModalButtonProps }) { return ( <> {typeof button.props === 'string' ? ( - ) diff --git a/apps/frontend/src/ui/components/overlay/cluster/ClusterGameSetup.tsx b/apps/frontend/src/ui/components/overlay/cluster/ClusterGameSetup.tsx index 1eaadab1..27778213 100644 --- a/apps/frontend/src/ui/components/overlay/cluster/ClusterGameSetup.tsx +++ b/apps/frontend/src/ui/components/overlay/cluster/ClusterGameSetup.tsx @@ -129,7 +129,7 @@ function VersionSelector(props: { setVersion: (version: string) => void }) {
setVersion(indexes[0])} > diff --git a/apps/frontend/src/ui/pages/onboarding/Onboarding.tsx b/apps/frontend/src/ui/pages/onboarding/Onboarding.tsx index 01bd0191..b6915b9a 100644 --- a/apps/frontend/src/ui/pages/onboarding/Onboarding.tsx +++ b/apps/frontend/src/ui/pages/onboarding/Onboarding.tsx @@ -30,9 +30,19 @@ export const enum OnboardingTaskStage { Completed = 2, } +interface ImportInstancesType { + basePath: string; + instances: string[]; +} + +type InstancesImportMapType = Map; + interface OnboardingContextType { setLanguage: (language: Language) => void; - setImportTypeInstances: (type: ImportType, instances: string[]) => void; + language: () => Language; + + setImportInstances: (type: ImportType, basePath: string, instances: string[]) => void; + importInstances: (type: ImportType) => ImportInstancesType | undefined; getTasks: () => string[]; @@ -51,7 +61,7 @@ function Onboarding(props: ParentProps) { const [forwardButtonEnabled, setForwardButtonEnabled] = createSignal(true); const [language, setLanguage] = createSignal('en'); - const [importTypes, setImportTypes] = createSignal>(new Map()); + const [importInstances, setImportInstances] = createSignal(new Map()); const [tasksStage, setTasksStage] = createSignal(OnboardingTaskStage.NotStarted); const [tasksMessage, setTasksMessage] = createSignal(''); @@ -131,7 +141,7 @@ function Onboarding(props: ParentProps) { tasks.push(`Set language to ${LanguagesList[language()][0]}`); - importTypes().forEach((type) => { + importInstances().forEach((type) => { tasks.push(`Import profiles from ${type}`); }); @@ -140,13 +150,19 @@ function Onboarding(props: ParentProps) { const ctx: OnboardingContextType = { setLanguage, - setImportTypeInstances(type, instances) { - setImportTypes((importTypes) => { + language, + + setImportInstances(type, basePath, instances) { + setImportInstances((importTypes) => { const newMap = new Map(importTypes); - newMap.set(type, instances); + newMap.set(type, { + basePath, + instances, + }); return newMap; }); }, + importInstances: type => importInstances().get(type), getTasks, diff --git a/apps/frontend/src/ui/pages/onboarding/OnboardingImport.tsx b/apps/frontend/src/ui/pages/onboarding/OnboardingImport.tsx index e55bb6e2..d99de6d6 100644 --- a/apps/frontend/src/ui/pages/onboarding/OnboardingImport.tsx +++ b/apps/frontend/src/ui/pages/onboarding/OnboardingImport.tsx @@ -1,24 +1,36 @@ import type { ImportType } from '@onelauncher/client/bindings'; import Illustration from '~assets/illustrations/onboarding/import_from_others.svg?component-solid'; +import { bridge } from '~imports'; +import Button from '~ui/components/base/Button'; +import SelectList from '~ui/components/base/SelectList'; import LauncherIcon from '~ui/components/content/LauncherIcon'; +import Modal, { createModal, type ModalProps } from '~ui/components/overlay/Modal'; +import useCommand from '~ui/hooks/useCommand'; import { LAUNCHER_IMPORT_TYPES } from '~utils'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-solid'; -import { For } from 'solid-js'; +import { createEffect, createSignal, For } from 'solid-js'; import Onboarding, { OnboardingStep } from './Onboarding'; -import { createModal } from '~ui/components/overlay/Modal'; function OnboardingImport() { const ctx = Onboarding.useContext(); + const [launcher, setLauncher] = createSignal(); - const modal = createModal((props) => { - - }); + const modal = createModal(props => ( + { + ctx.setImportInstances(launcher()!, basePath, instance); + }} + selected={ctx.importInstances(launcher()!)?.instances} + /> + )); function displayImport(type: ImportType) { - + setLauncher(type); + modal.show(); } - return ( } @@ -30,7 +42,10 @@ function OnboardingImport() {
{type => ( - @@ -45,6 +60,60 @@ function OnboardingImport() { export default OnboardingImport; -function InstancesPickerModal() { - +type InstancesPickerModalProps = ModalProps & { + launcher: ImportType; + selected: string[] | undefined; + onSelected: (basePath: string, instances: string[]) => void; +}; + +function InstancesPickerModal(props: InstancesPickerModalProps) { + const [instances] = useCommand(() => bridge.commands.getLauncherInstances(props.launcher, null)); + const [selected, setSelected] = createSignal([]); + + const getSelectedInstances = () => selected().map(i => instances()?.[1][i]).filter(v => v !== undefined); + + createEffect(() => { + const selected = props.selected; + const instancesList = instances()?.[1]; + if (selected !== undefined && instancesList !== undefined) { + const indexes = selected.map(instance => instancesList.indexOf(instance)).filter(i => i !== undefined); + setSelected(indexes); + } + }); + + return ( + { + props.onSelected(instances()![0], getSelectedInstances()); + props.hide(); + }} + />, + ]} + title="Import Instances" + > +
+ + + {(instance, index) => ( + + {instance} + + )} + + +
+
+ ); }