feat: Finish off onboarding instance import frontend + multi-select lists
This commit is contained in:
parent
23ff953f64
commit
709d2a0136
5 changed files with 166 additions and 31 deletions
|
|
@ -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<number | undefined>;
|
||||
interface SelectListContextHelpers {
|
||||
selected: Accessor<number[]>;
|
||||
setSelected: Setter<number[]>;
|
||||
select: (index: number) => void;
|
||||
}
|
||||
const SelectListContext = createContext<SelectListContextHelpers>() as Context<SelectListContextHelpers>;
|
||||
|
||||
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<number | undefined>();
|
||||
// eslint-disable-next-line solid/reactivity -- -
|
||||
const [selected, setSelected] = createSignal<number[]>(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 (
|
||||
<SelectListContext.Provider value={[selected, setSelected]}>
|
||||
<SelectListContext.Provider value={ctx}>
|
||||
{props.children}
|
||||
</SelectListContext.Provider>
|
||||
);
|
||||
|
|
@ -26,13 +56,20 @@ function useSelectListContext() {
|
|||
}
|
||||
|
||||
export type SelectListProps = Omit<JSX.HTMLAttributes<HTMLDivElement>, '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 (
|
||||
<SelectListContextProvider onChanged={props.onChange}>
|
||||
<SelectListContextProvider
|
||||
multiple={props.multiple}
|
||||
onChanged={props.onChange}
|
||||
selected={props.selected}
|
||||
>
|
||||
<div {...rest} class={`${styles.select_list} ${split.class || ''}`}>
|
||||
<OverlayScrollbarsComponent class="max-h-full">
|
||||
<div>
|
||||
|
|
@ -45,20 +82,33 @@ function SelectList(props: SelectListProps) {
|
|||
}
|
||||
|
||||
export type SelectListRowProps = JSX.HTMLAttributes<HTMLDivElement> & {
|
||||
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 (
|
||||
<div
|
||||
{...rest}
|
||||
class={`${styles.row} ${props.index !== undefined && selected() === props.index ? styles.selected : ''} ${split.class || ''}`}
|
||||
class={`${styles.row} ${props.index !== undefined && selected().includes(props.index) ? styles.selected : ''} ${split.class || ''}`}
|
||||
onClick={(e) => {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ Modal.Simple = function (props: ModalSimpleProps) {
|
|||
<div class="mt-2 flex flex-row gap-x-4 [&>*]:flex-1">
|
||||
<For each={split.buttons}>
|
||||
{button => (
|
||||
<ModalButton props={button} />
|
||||
<ModalButton hide={props.hide} props={button} />
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
|
@ -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'
|
||||
? (
|
||||
<Button buttonStyle="secondary">
|
||||
<Button buttonStyle="secondary" onClick={button.hide}>
|
||||
{button.props}
|
||||
</Button>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ function VersionSelector(props: { setVersion: (version: string) => void }) {
|
|||
<div class="flex flex-1 flex-row gap-2">
|
||||
<SelectList
|
||||
class="max-h-40 min-w-3/5"
|
||||
onChange={setVersion}
|
||||
onChange={indexes => setVersion(indexes[0])}
|
||||
>
|
||||
<Show when={filteredVersions() !== undefined}>
|
||||
<Index each={filteredVersions()}>
|
||||
|
|
|
|||
|
|
@ -30,9 +30,19 @@ export const enum OnboardingTaskStage {
|
|||
Completed = 2,
|
||||
}
|
||||
|
||||
interface ImportInstancesType {
|
||||
basePath: string;
|
||||
instances: string[];
|
||||
}
|
||||
|
||||
type InstancesImportMapType = Map<ImportType, ImportInstancesType>;
|
||||
|
||||
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<Language>('en');
|
||||
const [importTypes, setImportTypes] = createSignal<Map<ImportType, string[]>>(new Map());
|
||||
const [importInstances, setImportInstances] = createSignal<InstancesImportMapType>(new Map());
|
||||
const [tasksStage, setTasksStage] = createSignal<OnboardingTaskStage>(OnboardingTaskStage.NotStarted);
|
||||
const [tasksMessage, setTasksMessage] = createSignal<string>('');
|
||||
|
||||
|
|
@ -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,
|
||||
|
||||
|
|
|
|||
|
|
@ -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<ImportType>();
|
||||
|
||||
const modal = createModal((props) => {
|
||||
|
||||
});
|
||||
const modal = createModal(props => (
|
||||
<InstancesPickerModal
|
||||
{...props}
|
||||
launcher={launcher()!}
|
||||
onSelected={(basePath, instance) => {
|
||||
ctx.setImportInstances(launcher()!, basePath, instance);
|
||||
}}
|
||||
selected={ctx.importInstances(launcher()!)?.instances}
|
||||
/>
|
||||
));
|
||||
|
||||
function displayImport(type: ImportType) {
|
||||
|
||||
setLauncher(type);
|
||||
modal.show();
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<OnboardingStep
|
||||
illustration={<Illustration />}
|
||||
|
|
@ -30,7 +42,10 @@ function OnboardingImport() {
|
|||
<div class="grid grid-cols-3">
|
||||
<For each={LAUNCHER_IMPORT_TYPES}>
|
||||
{type => (
|
||||
<button class="flex flex-col items-center justify-center gap-y-4 rounded-md p-4 active:bg-gray-10 hover:bg-gray-05" onClick={() => displayImport(type)}>
|
||||
<button
|
||||
class={`flex flex-col items-center justify-center gap-y-4 rounded-md p-4 active:bg-gray-10 hover:bg-gray-05 ${(ctx.importInstances(type)?.instances.length || 0) > 0 ? 'bg-success hover:bg-success/70' : ''}`}
|
||||
onClick={() => displayImport(type)}
|
||||
>
|
||||
<LauncherIcon class="h-16 max-w-22 min-w-16" launcher={type} />
|
||||
<span class="text-lg font-medium">{type}</span>
|
||||
</button>
|
||||
|
|
@ -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<number[]>([]);
|
||||
|
||||
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 (
|
||||
<Modal.Simple
|
||||
{...props}
|
||||
buttons={[
|
||||
'Cancel',
|
||||
<Button
|
||||
buttonStyle="primary"
|
||||
children={`Import (${selected().length || 'None'})`}
|
||||
disabled={(instances()?.length || 0) <= 0}
|
||||
onClick={() => {
|
||||
props.onSelected(instances()![0], getSelectedInstances());
|
||||
props.hide();
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
title="Import Instances"
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<SelectList
|
||||
class="h-52 max-h-52"
|
||||
multiple
|
||||
onChange={setSelected}
|
||||
selected={selected()}
|
||||
>
|
||||
<For each={instances()?.[1]}>
|
||||
{(instance, index) => (
|
||||
<SelectList.Row index={index()}>
|
||||
{instance}
|
||||
</SelectList.Row>
|
||||
)}
|
||||
</For>
|
||||
</SelectList>
|
||||
</div>
|
||||
</Modal.Simple>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue