diff --git a/apps/frontend/src/ui/components/base/FilePicker.tsx b/apps/frontend/src/ui/components/base/FilePicker.tsx new file mode 100644 index 00000000..05458c07 --- /dev/null +++ b/apps/frontend/src/ui/components/base/FilePicker.tsx @@ -0,0 +1,82 @@ +import { type DialogFilter, open } from '@tauri-apps/plugin-dialog'; +import { FilePlus02Icon } from '@untitled-theme/icons-solid'; +import { createSignal, For, Match, onMount, splitProps, Switch } from 'solid-js'; +import Button from './Button'; + +export interface FilePickerOptions { + directory?: boolean; + multiple?: boolean; + defaultPath?: string; + title?: string; + filters?: DialogFilter[]; +}; + +export interface FilePickerRef { + open: () => void; +} + +export default function FilePicker(componentProps: FilePickerOptions & { + onPicked?: (paths: string[]) => void; + ref?: (ref: FilePickerRef) => void; +}) { + const [props, options] = splitProps(componentProps, ['onPicked', 'ref']); + const [file, setFile] = createSignal([]); + + onMount(() => { + if (options.defaultPath) + setFile([options.defaultPath]); + + props.ref?.({ + open: openPicker, + }); + }); + + async function openPicker() { + const path = await open(options) as string[] | string | null; // The type automatically infers from the selected options, but we don't want that here + + if (Array.isArray(path)) + setFile(path); + else if (path === null) + setFile([]); + else + setFile([path]); + + props.onPicked?.(file()); + } + + return ( +
+ +
+ ); +} diff --git a/apps/frontend/src/ui/components/base/SelectList.tsx b/apps/frontend/src/ui/components/base/SelectList.tsx index 6d3db14b..5a85d957 100644 --- a/apps/frontend/src/ui/components/base/SelectList.tsx +++ b/apps/frontend/src/ui/components/base/SelectList.tsx @@ -1,5 +1,5 @@ import { OverlayScrollbarsComponent } from 'overlayscrollbars-solid'; -import { type Accessor, type Context, createContext, createEffect, createSignal, type JSX, type ParentProps, type Setter, splitProps, useContext } from 'solid-js'; +import { type Accessor, type Context, createContext, createEffect, createSignal, type JSX, onMount, type ParentProps, type Setter, splitProps, useContext } from 'solid-js'; import styles from './SelectList.module.scss'; interface SelectListContextHelpers { @@ -83,11 +83,17 @@ function SelectList(props: SelectListProps) { export type SelectListRowProps = JSX.HTMLAttributes & { index: number; + selected?: boolean; }; SelectList.Row = (props: SelectListRowProps) => { const { selected, setSelected, select } = useSelectListContext(); - const [split, rest] = splitProps(props, ['index', 'class', 'onClick']); + const [split, rest] = splitProps(props, ['selected', 'index', 'class', 'onClick']); + + onMount(() => { + if (split.selected) + select(split.index); + }); return (
& { }; function Tooltip(props: TooltipProps) { - const [split, rest] = splitProps(props, ['class']); + const [split, rest] = splitProps(props, ['class', 'text']); return ( -
+
); } diff --git a/apps/frontend/src/ui/components/content/LauncherImportComponent.tsx b/apps/frontend/src/ui/components/content/LauncherImportComponent.tsx new file mode 100644 index 00000000..e1edbf9c --- /dev/null +++ b/apps/frontend/src/ui/components/content/LauncherImportComponent.tsx @@ -0,0 +1,124 @@ +import type { ImportType } from '@onelauncher/client/bindings'; +import { RefreshCw01Icon } from '@untitled-theme/icons-solid'; +import { bridge } from '~imports'; +import Button from '~ui/components/base/Button'; +import FilePicker from '~ui/components/base/FilePicker'; +import SelectList from '~ui/components/base/SelectList'; +import { tryResult } from '~ui/hooks/useCommand'; +import useNotifications from '~ui/hooks/useNotifications'; +import { type Accessor, createEffect, createResource, createSignal, For, type Setter, untrack } from 'solid-js'; +import Spinner from '../Spinner'; + +export interface LauncherImportInformation { + importType: ImportType; + path: string | null; + instances: string[]; +}; + +interface LauncherImportComponentProps { + multiple: boolean; + setCanImport?: Setter; + importInformation: Accessor; + setImportInformation: Setter; +}; + +export default function LauncherImportComponent(props: LauncherImportComponentProps) { + const notifications = useNotifications(); + + // eslint-disable-next-line solid/reactivity -- Its fine + const [selected, setSelected] = createSignal(props.importInformation().instances || []); + // eslint-disable-next-line solid/reactivity -- Its fine + const [customPath, setCustomPath] = createSignal(props.importInformation().path || null); + const [pathChanged, setPathChanged] = createSignal(false); + + const [instances, { refetch: refetchInstances }] = createResource(async () => { + const info = props.importInformation(); + const importType = info.importType; + const path = untrack(customPath) || null; // empty path + + try { + const result = await tryResult(() => bridge.commands.getLauncherInstances(importType, path)); + setCustomPath(result[0]); + + return result[1]; + } + catch (err) { + // If the path is not null this means that the launcher's default location was not found + if (path !== null) + notifications.create({ + title: 'Instance Scan Error', + message: `${err}`, + }); + + return []; + } + }); + + createEffect(() => { + const selectedInstances = selected(); + + props.setCanImport?.(() => selectedInstances.length > 0); + + props.setImportInformation({ + importType: untrack(props.importInformation).importType, + path: untrack(customPath)!, + instances: selectedInstances, + }); + }); + + function onPicked(paths: string[]) { + setCustomPath(paths[0] || ''); + setPathChanged(true); + } + + async function refresh() { + refetchInstances(); + setPathChanged(false); + } + + return ( +
+ +

Path

+
+ { + if (customPath() === null) + ref.open(); + }} + /> +
+ +

Found Instances

+ { + const mapped = instances()?.map((instance, index) => indexes.includes(index) ? instance : undefined).filter((instance): instance is string => !!instance) || []; + setSelected(mapped); + }} + > + + {(instance, index) => { + // console.log(props.importInformation(), props.importInformation().instances.includes(instance)); + return ( + + {instance} + + ); + }} + + +
+
+ ); +} diff --git a/apps/frontend/src/ui/components/overlay/cluster/ClusterImportSelection.tsx b/apps/frontend/src/ui/components/overlay/cluster/ClusterImportSelection.tsx index 95d1f92f..04c54d78 100644 --- a/apps/frontend/src/ui/components/overlay/cluster/ClusterImportSelection.tsx +++ b/apps/frontend/src/ui/components/overlay/cluster/ClusterImportSelection.tsx @@ -1,8 +1,8 @@ import type { ImportType } from '@onelauncher/client/bindings'; +import type { LauncherImportInformation } from '~ui/components/content/LauncherImportComponent'; 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 LauncherImportComponent from '~ui/components/content/LauncherImportComponent'; +import { createSignal, onMount, untrack } from 'solid-js'; import { type ClusterStepProps, createClusterStep } from './ClusterCreationModal'; export default createClusterStep({ @@ -12,67 +12,29 @@ export default createClusterStep({ }); 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(); - - createEffect(() => { - const index = selected(); - - props.setCanGoForward(() => { - if (index === undefined) - return false; - - if (index >= 0) - return true; - - return false; - }); + const [importInfo, setImportInfo] = createSignal({ + // eslint-disable-next-line solid/reactivity -- It works as intended + importType: untrack(props.controller.provider) as ImportType, + instances: [], + path: null, }); onMount(() => { - // eslint-disable-next-line solid/reactivity -- It's fine props.controller.setFinishFunction(() => async () => { - const index = untrack(selected); - if (index === undefined) - return false; + const details = untrack(importInfo); - 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]); + bridge.commands.importInstances(details.importType, details.path!, details.instances); return true; }); }); return ( - setSelected(selected[0])} - > - - {(instance, index) => ( - - {instance} - - )} - - + ); } diff --git a/apps/frontend/src/ui/pages/Home.tsx b/apps/frontend/src/ui/pages/Home.tsx index affe94d8..0466c864 100644 --- a/apps/frontend/src/ui/pages/Home.tsx +++ b/apps/frontend/src/ui/pages/Home.tsx @@ -3,7 +3,6 @@ import { useNavigate } from '@solidjs/router'; import { PlayIcon, PlusIcon, - RefreshCw01Icon, SearchMdIcon, Settings01Icon, } from '@untitled-theme/icons-solid'; @@ -30,7 +29,7 @@ function HomePage() { onMount(() => { bridge.events.clusterPayload.listen(({ payload }) => { - if (payload.event === 'created' || payload.event === 'deleted') + if (payload.event === 'created' || payload.event === 'deleted' || payload.event === 'inserted') refetch(); }); }); @@ -52,14 +51,15 @@ function HomePage() {
- } placeholder="Search for clusters..." /> + } placeholder="Search for clusters..." />
-