feat: context stuff
i dont know what to name this tbh
This commit is contained in:
parent
b7cac50020
commit
86c3cc4f40
9 changed files with 297 additions and 128 deletions
|
|
@ -41,8 +41,6 @@ export type JavaError = { ParseVersion: [string] } | "Execute" | "MissingJava"
|
|||
|
||||
export type LauncherError = { type: "DirError"; data: DirectoryError } | { type: "IOError"; data: IOError } | { type: "IngressError"; data: IngressError } | { type: "JavaError"; data: JavaError } | { type: "CryptoError"; data: CryptoError } | { type: "DiscordError"; data: DiscordError } | { type: "MetadataError"; data: MetadataError } | { type: "ClusterError"; data: ClusterError } | { type: "MinecraftAuthError"; data: MinecraftAuthError } | { type: "ProcessError"; data: ProcessError } | { type: "PackageError"; data: PackageError } | { type: "DaoError"; data: DaoError } | { type: "SerdeError" } | { type: "AnyhowError" } | { type: "DbError" } | { type: "ReqwestError" } | { type: "InterpulseError" } | { type: "RegexError" } | { type: "SemaphoreError" } | { type: "UrlError" } | { type: "TauriError" }
|
||||
|
||||
export type LauncherEvent = { Ingress: IngressPayload } | { Message: MessagePayload } | { Process: ProcessPayload }
|
||||
|
||||
export type MessageLevel = "Info" | "Warn" | "Error"
|
||||
|
||||
export type MessagePayload = { level: MessageLevel; message: string }
|
||||
|
|
@ -92,7 +90,7 @@ export type Process = { pid: number; started_at: string; cluster_id: bigint; pos
|
|||
|
||||
export type ProcessError = { HookUnsuccessful: number } | "NoPid"
|
||||
|
||||
export type ProcessPayload = { Starting: { command: string } } | { Started: { process: Process } } | { Stopped: { pid: number; exit_code: number } } | { Output: { pid: number; output: string } }
|
||||
export type ProcessPayload = { type: "Starting"; command: string } | { type: "Started"; process: Process } | { type: "Stopped"; pid: number; exit_code: number } | { type: "Output"; pid: number; output: string }
|
||||
|
||||
export type Resolution = { width: number; height: number }
|
||||
|
||||
|
|
@ -100,36 +98,26 @@ export type SettingProfileModel = { name: string; java_id: bigint | null; res: R
|
|||
|
||||
export type SettingsOsExtra = Record<string, never>
|
||||
|
||||
const ARGS_MAP = { 'onelauncher':'{"return_error":[],"open_dev_tools":[]}', 'core':'{"get_users":[],"get_user":["uuid"],"get_default_user":["fallback"],"get_cluster":["id"],"create_cluster":["options"],"get_profile_or_default":["name"],"remove_cluster":["id"],"set_default_user":["uuid"],"open_msa_login":[],"get_global_profile":[],"remove_user":["uuid"],"get_clusters":[]}', '':'{"send_event":["event"]}' }
|
||||
const ARGS_MAP = { 'core':'{"getClusters":[],"getGlobalProfile":[],"getClusterById":["id"],"getProfileOrDefault":["name"],"removeCluster":["id"],"openMsaLogin":[],"createCluster":["options"],"getUser":["uuid"],"getDefaultUser":["fallback"],"setDefaultUser":["uuid"],"launchCluster":["id","uuid"],"getUsers":[],"removeUser":["uuid"]}', 'onelauncher':'{"open_dev_tools":[],"return_error":[]}', 'events':'{"ingress":["event"],"process":["event"],"message":["event"]}' }
|
||||
export type Router = { 'onelauncher': { return_error: () => Promise<null>,
|
||||
open_dev_tools: () => Promise<void> },
|
||||
'core': { get_clusters: () => Promise<Model[]>,
|
||||
get_cluster: (id: bigint) => Promise<Model | null>,
|
||||
remove_cluster: (id: bigint) => Promise<null>,
|
||||
create_cluster: (options: CreateCluster) => Promise<Model>,
|
||||
get_profile_or_default: (name: string | null) => Promise<SettingProfileModel>,
|
||||
get_global_profile: () => Promise<SettingProfileModel>,
|
||||
get_users: () => Promise<MinecraftCredentials[]>,
|
||||
get_user: (uuid: string) => Promise<MinecraftCredentials | null>,
|
||||
remove_user: (uuid: string) => Promise<null>,
|
||||
get_default_user: (fallback: boolean | null) => Promise<MinecraftCredentials | null>,
|
||||
set_default_user: (uuid: string | null) => Promise<null>,
|
||||
open_msa_login: () => Promise<MinecraftCredentials | null> },
|
||||
'': { send_event: (event: LauncherEvent) => Promise<void> } };
|
||||
'events': { ingress: (event: IngressPayload) => Promise<void>,
|
||||
message: (event: MessagePayload) => Promise<void>,
|
||||
process: (event: ProcessPayload) => Promise<void> },
|
||||
'core': { getClusters: () => Promise<Model[]>,
|
||||
getClusterById: (id: bigint) => Promise<Model | null>,
|
||||
removeCluster: (id: bigint) => Promise<null>,
|
||||
createCluster: (options: CreateCluster) => Promise<Model>,
|
||||
launchCluster: (id: bigint, uuid: string | null) => Promise<null>,
|
||||
getProfileOrDefault: (name: string | null) => Promise<SettingProfileModel>,
|
||||
getGlobalProfile: () => Promise<SettingProfileModel>,
|
||||
getUsers: () => Promise<MinecraftCredentials[]>,
|
||||
getUser: (uuid: string) => Promise<MinecraftCredentials | null>,
|
||||
removeUser: (uuid: string) => Promise<null>,
|
||||
getDefaultUser: (fallback: boolean | null) => Promise<MinecraftCredentials | null>,
|
||||
setDefaultUser: (uuid: string | null) => Promise<null>,
|
||||
openMsaLogin: () => Promise<MinecraftCredentials | null> } };
|
||||
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< Updated upstream
|
||||
const ARGS_MAP = { '': '{"send_event":["event"]}', 'onelauncher': '{"return_error":[],"open_dev_tools":[]}', 'core': '{"get_user":["uuid"],"get_default_user":["fallback"],"set_default_user":["uuid"],"get_global_profile":[],"get_clusters":[],"remove_cluster":["id"],"get_profile_or_default":["name"],"get_users":[],"remove_user":["uuid"],"open_msa_login":[],"create_cluster":["options"],"get_cluster":["id"]}' };
|
||||
=======
|
||||
const ARGS_MAP = { 'core': '{"getGlobalProfile":[],"getUsers":[],"removeUser":["uuid"],"getDefaultUser":["fallback"],"getUser":["uuid"],"getClusterById":["id"],"setDefaultUser":["uuid"],"openMsaLogin":[],"removeCluster":["id"],"getProfileOrDefault":["name"],"getClusters":[],"createCluster":["options"]}', 'onelauncher': '{"open_dev_tools":[],"return_error":[]}', '': '{"send_event":["event"]}' };
|
||||
>>>>>>> 17734ca1b9033a35ee6a581cc55e25fa7fc932b3
|
||||
export interface Router {
|
||||
'onelauncher': { return_error: () => Promise<null>; open_dev_tools: () => Promise<void> };
|
||||
'': { send_event: (event: LauncherEvent) => Promise<void> };
|
||||
'core': { getClusters: () => Promise<Array<Model>>; getClusterById: (id: bigint) => Promise<Model | null>; removeCluster: (id: bigint) => Promise<null>; createCluster: (options: CreateCluster) => Promise<Model>; getProfileOrDefault: (name: string | null) => Promise<SettingProfileModel>; getGlobalProfile: () => Promise<SettingProfileModel>; getUsers: () => Promise<Array<MinecraftCredentials>>; getUser: (uuid: string) => Promise<MinecraftCredentials | null>; removeUser: (uuid: string) => Promise<null>; getDefaultUser: (fallback: boolean | null) => Promise<MinecraftCredentials | null>; setDefaultUser: (uuid: string | null) => Promise<null>; openMsaLogin: () => Promise<MinecraftCredentials | null> };
|
||||
}
|
||||
=======
|
||||
>>>>>>> Stashed changes
|
||||
|
||||
export type { InferCommandOutput }
|
||||
export const createTauRPCProxy = () => createProxy<Router>(ARGS_MAP)
|
||||
|
|
|
|||
|
|
@ -124,8 +124,6 @@ function Banner({
|
|||
// props.setNewCover(selected);
|
||||
// }
|
||||
|
||||
const { setNewName, editMode, cluster } = props;
|
||||
|
||||
function updateName(name: string) {
|
||||
if (name.length > 30 || name.length <= 0)
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ function RouteComponent() {
|
|||
const { id } = Route.useSearch();
|
||||
|
||||
const cluster = useCommand('getClusterById', () => bindings.core.getClusterById(Number(id.toString()) as unknown as bigint));
|
||||
const result = useCommand('getProfileOrDefault', () => bindings.core.getProfileOrDefault(cluster.data?.name as string), {
|
||||
const _result = useCommand('getProfileOrDefault', () => bindings.core.getProfileOrDefault(cluster.data?.name as string), {
|
||||
enabled: !!cluster.data?.name,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import type { Model } from '@/bindings.gen';
|
||||
import DefaultBanner from '@/assets/images/default_banner.png';
|
||||
import HeaderImage from '@/assets/images/default_banner.png';
|
||||
import DefaultInstancePhoto from '@/assets/images/default_instance_cover.jpg';
|
||||
import Modal from '@/components/overlay/Modal';
|
||||
import { bindings } from '@/main';
|
||||
import { useCommand } from '@onelauncher/common';
|
||||
import { Button, TextField } from '@onelauncher/common/components';
|
||||
import { createFileRoute, Link } from '@tanstack/react-router';
|
||||
import { Server01Icon } from '@untitled-theme/icons-react';
|
||||
import { PlayIcon, Server01Icon } from '@untitled-theme/icons-react';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
|
||||
export const Route = createFileRoute('/app/')({
|
||||
|
|
@ -17,6 +16,8 @@ export const Route = createFileRoute('/app/')({
|
|||
/*
|
||||
Please note this route has a very big issue related to scrolling
|
||||
and i am very angry rn so i will not be fixing it rn
|
||||
|
||||
hey future sassan here i guess the issue is solved idk
|
||||
*/
|
||||
function RouteComponent() {
|
||||
const result = useCommand('getClusters', bindings.core.getClusters);
|
||||
|
|
@ -111,11 +112,11 @@ function Banner() {
|
|||
|
||||
function ClusterCreate() {
|
||||
const result = useCommand('createCluster', () => bindings.core.createCluster({
|
||||
icon: 'asd',
|
||||
icon: null,
|
||||
mc_loader: 'vanilla',
|
||||
mc_version: '1.20.1',
|
||||
name: 'Test Cluster',
|
||||
mc_loader_version: '0.13.5',
|
||||
mc_loader_version: null,
|
||||
}), {
|
||||
enabled: false,
|
||||
subscribed: false,
|
||||
|
|
@ -125,7 +126,7 @@ function ClusterCreate() {
|
|||
result.refetch();
|
||||
|
||||
if (result.isError)
|
||||
alert(result.error.message);
|
||||
console.error(result.error.message);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -140,7 +141,7 @@ function ClusterCreate() {
|
|||
<div className="min-w-sm flex flex-col rounded-lg bg-page text-center">
|
||||
<div className="theme-OneLauncher-Dark relative h-25 flex">
|
||||
<div className="absolute left-0 top-0 h-full w-full">
|
||||
<img alt="Header Image" className="h-full w-full rounded-t-lg" src={HeaderImage} />
|
||||
<img alt="Header Image" className="h-full w-full rounded-t-lg" src={DefaultBanner} />
|
||||
</div>
|
||||
<div
|
||||
className="absolute left-0 top-0 h-full flex flex-row items-center justify-start gap-x-4 bg-[radial-gradient(at_center,#00000077,transparent)] px-10"
|
||||
|
|
@ -209,6 +210,18 @@ function ClusterCard({
|
|||
mc_loader,
|
||||
mc_version,
|
||||
}: Model) {
|
||||
const launch = useCommand('launchCluster', () => bindings.core.launchCluster(id, null), {
|
||||
enabled: false,
|
||||
subscribed: false,
|
||||
});
|
||||
|
||||
const handleLaunch = () => {
|
||||
launch.refetch();
|
||||
|
||||
if (launch.error)
|
||||
console.error(launch.error.message);
|
||||
};
|
||||
|
||||
return (
|
||||
<Link
|
||||
search={{
|
||||
|
|
@ -241,6 +254,7 @@ function ClusterCard({
|
|||
</div>
|
||||
|
||||
{/* <LaunchButton cluster={props} iconOnly /> */}
|
||||
<Button onClick={handleLaunch} size="icon"><PlayIcon /></Button>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export const Route = createFileRoute('/app/settings/')({
|
|||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const result = useCommand('getGlobalProfile', bindings.core.getGlobalProfile);
|
||||
const _result = useCommand('getGlobalProfile', bindings.core.getGlobalProfile);
|
||||
|
||||
return (
|
||||
<Sidebar.Page>
|
||||
|
|
|
|||
|
|
@ -1,23 +1,15 @@
|
|||
import Illustration from '@/assets/illustrations/onboarding/language.svg';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { OnboardingStep } from './route';
|
||||
import { languageList, OnboardingStep } from './route';
|
||||
|
||||
export const Route = createFileRoute('/onboarding/language')({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
const languages = [
|
||||
{
|
||||
lang: 'English',
|
||||
percentage: 100,
|
||||
},
|
||||
];
|
||||
|
||||
function RouteComponent() {
|
||||
return (
|
||||
<>
|
||||
<OnboardingStep
|
||||
// importing and using it as a react component was not working
|
||||
illustration={<img src={Illustration} />}
|
||||
paragraph="Choose your preferred language."
|
||||
title="Language"
|
||||
|
|
@ -27,7 +19,7 @@ function RouteComponent() {
|
|||
<div className="max-h-84 overflow-hidden">
|
||||
<div className="flex flex-col gap-y-1 p-2">
|
||||
<aside>
|
||||
{languages.map(x => (
|
||||
{languageList.map(x => (
|
||||
<div
|
||||
className="flex flex-row items-center rounded-lg px-6 py-5"
|
||||
key={x.lang}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@ import { useCommand } from '@onelauncher/common';
|
|||
import { Button, Show } from '@onelauncher/common/components';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { useState } from 'react';
|
||||
import { useOnboardingContext } from './route';
|
||||
|
||||
export const Route = createFileRoute('/onboarding/login')({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const ctx = useOnboardingContext();
|
||||
const [profile, setProfile] = useState<MinecraftCredentials>();
|
||||
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||
const result = useCommand('openMsaLogin', bindings.core.openMsaLogin, {
|
||||
|
|
@ -19,19 +21,21 @@ function RouteComponent() {
|
|||
});
|
||||
|
||||
function beginMsAuthFlow() {
|
||||
result.refetch();
|
||||
result.refetch().then((data) => {
|
||||
if (data.isError) {
|
||||
setErrorMessage(data.error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.isError) {
|
||||
setErrorMessage(result.error.message);
|
||||
return;
|
||||
}
|
||||
if (!data.data) {
|
||||
setErrorMessage('No account was found. Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.data) {
|
||||
setErrorMessage('No account was found. Please try again.');
|
||||
return;
|
||||
}
|
||||
setProfile(data.data);
|
||||
|
||||
setProfile(result.data);
|
||||
ctx.setIsForwardButtonEnabled(false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -51,12 +55,10 @@ function RouteComponent() {
|
|||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Button
|
||||
children="Login with Microsoft"
|
||||
onPress={beginMsAuthFlow}
|
||||
/>
|
||||
|
||||
<p className="text-danger">{errorMessage}</p>
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,108 +1,277 @@
|
|||
import type { JSX } from 'react';
|
||||
/* eslint-disable no-console -- we dont have a seperate logger for now */
|
||||
import type { ReactNode } from 'react';
|
||||
import { pluralize } from '@/utils';
|
||||
import { Button } from '@onelauncher/common/components';
|
||||
import { createFileRoute, Outlet, useNavigate, useRouterState } from '@tanstack/react-router';
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
export const Route = createFileRoute('/onboarding')({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
const steps = [
|
||||
'/onboarding', // done
|
||||
'/onboarding/language', // done (only ui)
|
||||
'/onboarding/login', // wip
|
||||
'/onboarding/import', // wip
|
||||
|
||||
'/onboarding/summary', // wip
|
||||
'/onboarding/complete', // wip
|
||||
'/onboarding',
|
||||
'/onboarding/language',
|
||||
'/onboarding/login',
|
||||
'/onboarding/import',
|
||||
'/onboarding/summary',
|
||||
'/onboarding/complete',
|
||||
] as const;
|
||||
|
||||
interface Language {
|
||||
lang: string;
|
||||
code: string;
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
export const languageList: Array<Language> = [
|
||||
{
|
||||
lang: 'English',
|
||||
code: 'en',
|
||||
percentage: 100,
|
||||
},
|
||||
];
|
||||
|
||||
enum OnboardingTaskStage {
|
||||
NotStarted = 0,
|
||||
Running = 1,
|
||||
Completed = 2,
|
||||
}
|
||||
|
||||
interface ImportInstancesType {
|
||||
basePath: string;
|
||||
instances: Array<string>;
|
||||
}
|
||||
|
||||
interface _OnboardingContextType {
|
||||
interface OnboardingContextType {
|
||||
setLanguage: (language: Language) => void;
|
||||
language: () => Language;
|
||||
language: Language | undefined;
|
||||
|
||||
setImportInstances: (type: string, basePath: string, instances: Array<string>) => void;
|
||||
importInstances: (type: string) => ImportInstancesType | undefined;
|
||||
getImportInstance: (type: string) => ImportInstancesType | undefined;
|
||||
|
||||
setForwardButtonEnabled: (enabled: boolean) => void;
|
||||
setIsForwardButtonEnabled: (enabled: boolean) => void;
|
||||
|
||||
getTasks: () => Array<string>;
|
||||
|
||||
tasksStage: OnboardingTaskStage;
|
||||
tasksMessage: string;
|
||||
}
|
||||
|
||||
const OnboardingContext = createContext<OnboardingContextType | undefined>(undefined);
|
||||
|
||||
export function useOnboardingContext() {
|
||||
const context = useContext(OnboardingContext);
|
||||
if (context === undefined)
|
||||
throw new Error('useOnboardingContext must be used within an OnboardingProvider (RouteComponent)');
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
function RouteComponent() {
|
||||
const navigate = useNavigate();
|
||||
const routerState = useRouterState();
|
||||
const navigate = useNavigate({ from: Route.fullPath });
|
||||
const pathname = useRouterState({ select: s => s.location.pathname });
|
||||
|
||||
const currentPath = routerState.location.pathname;
|
||||
const currentStepIndex = steps.findIndex(path => currentPath === path || currentPath.startsWith(path));
|
||||
const currentStepIndex = useMemo(() => {
|
||||
return steps.findIndex(stepPath => stepPath === pathname);
|
||||
}, [pathname]);
|
||||
|
||||
const progressPercentage = currentStepIndex >= 0
|
||||
? (currentStepIndex / (steps.length - 1)) * 100
|
||||
: 0;
|
||||
const [isBackButtonEnabled, setIsBackButtonEnabled] = useState(false);
|
||||
const [isForwardButtonEnabled, setIsForwardButtonEnabled] = useState(true);
|
||||
|
||||
const handleBack = () => {
|
||||
const [language, setLanguage] = useState<Language>();
|
||||
const [importInstancesMap, setImportInstancesMap] = useState<Map<string, ImportInstancesType>>(new Map());
|
||||
const [tasksStage, setTasksStage] = useState<OnboardingTaskStage>(OnboardingTaskStage.NotStarted);
|
||||
const [tasksMessage, setTasksMessage] = useState<string>('');
|
||||
|
||||
const tasksCompleted = tasksStage === OnboardingTaskStage.Completed;
|
||||
|
||||
const percentage = (currentStepIndex / (steps.length - 1)) * 100;
|
||||
|
||||
const shallRunTasks = !tasksCompleted && currentStepIndex === steps.length - 2;
|
||||
|
||||
useEffect(() => {
|
||||
const isSummaryStep = currentStepIndex === steps.length - 2;
|
||||
const isCompleteStep = currentStepIndex === steps.length - 1;
|
||||
|
||||
setIsBackButtonEnabled(currentStepIndex > 0 && tasksStage !== OnboardingTaskStage.Running);
|
||||
|
||||
if (tasksStage === OnboardingTaskStage.Running)
|
||||
setIsForwardButtonEnabled(false);
|
||||
else if (tasksCompleted)
|
||||
setIsForwardButtonEnabled(isSummaryStep || isCompleteStep);
|
||||
else
|
||||
setIsForwardButtonEnabled(true);
|
||||
}, [currentStepIndex, tasksStage, tasksCompleted]);
|
||||
|
||||
const next = useCallback(async () => {
|
||||
const isLast = currentStepIndex === steps.length - 1;
|
||||
|
||||
if (isLast) {
|
||||
navigate({ to: '/app' });
|
||||
}
|
||||
else {
|
||||
const nextPath = steps[currentStepIndex + 1];
|
||||
|
||||
navigate({ to: nextPath });
|
||||
}
|
||||
}, [currentStepIndex, navigate]);
|
||||
|
||||
const previous = useCallback(() => {
|
||||
if (currentStepIndex > 0) {
|
||||
const previousStep = steps[currentStepIndex - 1];
|
||||
navigate({ to: previousStep as any });
|
||||
}
|
||||
};
|
||||
const prevPath = steps[currentStepIndex - 1];
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentStepIndex === -1) {
|
||||
navigate({ to: steps[0] as any });
|
||||
return;
|
||||
navigate({ to: prevPath });
|
||||
}
|
||||
}, [currentStepIndex, navigate]);
|
||||
|
||||
const getButtonText = useMemo(() => {
|
||||
if (shallRunTasks)
|
||||
return 'Setup';
|
||||
if (currentStepIndex === steps.length - 1)
|
||||
return 'Finish';
|
||||
return 'Next';
|
||||
}, [shallRunTasks, currentStepIndex]);
|
||||
|
||||
const runTasks = useCallback(async () => {
|
||||
setIsBackButtonEnabled(false);
|
||||
setIsForwardButtonEnabled(false);
|
||||
setTasksStage(OnboardingTaskStage.Running);
|
||||
setTasksMessage('Starting setup...');
|
||||
|
||||
const tasksToRun = [
|
||||
async () => {
|
||||
setTasksMessage(`Setting language to ${language?.lang}...`);
|
||||
|
||||
console.log('Language task completed for:', language?.lang);
|
||||
},
|
||||
async () => {
|
||||
if (importInstancesMap.size === 0) {
|
||||
console.log('No instances selected for import.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [launcher, importData] of importInstancesMap.entries()) {
|
||||
if (importData.instances.length === 0)
|
||||
continue;
|
||||
|
||||
setTasksMessage(`Importing ${importData.instances.length} ${pluralize(importData.instances.length, 'instance')} from ${launcher}...`);
|
||||
try {
|
||||
console.log(`Simulating import for ${launcher}:`, importData);
|
||||
}
|
||||
catch (e) {
|
||||
console.error(`Failed to import from ${launcher}:`, e);
|
||||
setTasksMessage(`Failed to import ${pluralize(importData.instances.length, 'instance')} from ${launcher}...`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
const results = await Promise.allSettled(tasksToRun.map(task => task()));
|
||||
const errorCount = results.filter(({ status }) => status === 'rejected').length;
|
||||
|
||||
if (errorCount > 0) {
|
||||
console.error('Onboarding tasks completed with error count:', errorCount);
|
||||
setTasksMessage(`Setup completed with ${errorCount} ${pluralize(errorCount, 'error')}. Check console for details.`);
|
||||
}
|
||||
else {
|
||||
setTasksMessage('Setup completed successfully!');
|
||||
}
|
||||
|
||||
if (currentStepIndex < steps.length - 1) {
|
||||
const nextStep = steps[currentStepIndex + 1];
|
||||
navigate({ to: nextStep as any });
|
||||
}
|
||||
else if (currentStepIndex === steps.length - 1) {
|
||||
navigate({ to: '/app' as any });
|
||||
}
|
||||
};
|
||||
setTasksStage(OnboardingTaskStage.Completed);
|
||||
// setIsBackButtonEnabled(true);
|
||||
setIsForwardButtonEnabled(true);
|
||||
next();
|
||||
}, [importInstancesMap, next, language?.lang, setIsBackButtonEnabled, setIsForwardButtonEnabled, setTasksStage, setTasksMessage]);
|
||||
|
||||
const getTasks = useCallback((): Array<string> => {
|
||||
const tasksStrings: Array<string> = [];
|
||||
const selectedLangInfo = languageList.find(l => l.lang === language?.lang);
|
||||
const langDisplayName = selectedLangInfo
|
||||
? `${selectedLangInfo.lang} (Coverage: ${selectedLangInfo.percentage}%)`
|
||||
: language?.lang;
|
||||
tasksStrings.push(`Set language to ${langDisplayName}`);
|
||||
|
||||
importInstancesMap.forEach((importData, launcher) => {
|
||||
if (importData.instances.length === 0)
|
||||
return;
|
||||
|
||||
let builder = `Import ${importData.instances.length} ${pluralize(importData.instances.length, 'instance')} from ${launcher} (Source: ${importData.basePath}):\n`;
|
||||
importData.instances.forEach((instance, index) => {
|
||||
builder += ` ${index + 1}. ${instance}\n`;
|
||||
});
|
||||
tasksStrings.push(builder.trim());
|
||||
});
|
||||
|
||||
return tasksStrings;
|
||||
}, [language, importInstancesMap]);
|
||||
|
||||
const handleSetImportInstances = useCallback((type: string, newBasePath: string, instances: Array<string>) => {
|
||||
setImportInstancesMap((prevMap) => {
|
||||
const newMap = new Map(prevMap);
|
||||
newMap.set(type, { basePath: newBasePath, instances });
|
||||
return newMap;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const getImportInstance = useCallback((type: string) => {
|
||||
return importInstancesMap.get(type);
|
||||
}, [importInstancesMap]);
|
||||
|
||||
const ctxValue: OnboardingContextType = useMemo(() => ({
|
||||
setLanguage,
|
||||
language,
|
||||
setImportInstances: handleSetImportInstances,
|
||||
getImportInstance,
|
||||
setIsForwardButtonEnabled,
|
||||
getTasks,
|
||||
tasksStage,
|
||||
tasksMessage,
|
||||
}), [
|
||||
language,
|
||||
handleSetImportInstances,
|
||||
getImportInstance,
|
||||
setIsForwardButtonEnabled,
|
||||
getTasks,
|
||||
tasksStage,
|
||||
tasksMessage,
|
||||
]);
|
||||
|
||||
return (
|
||||
// remind me 2 hours! i'll fix this
|
||||
// update: it's fixed!
|
||||
<div className="w-full flex flex-col items-center h-screen">
|
||||
<div className="h-0.5 w-full">
|
||||
<div
|
||||
className="h-full rounded-lg bg-brand transition-all"
|
||||
style={{
|
||||
width: `${progressPercentage}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<OnboardingContext value={ctxValue}>
|
||||
<div className="w-full flex flex-col items-center h-screen bg-background-primary text-text-primary">
|
||||
<div className="h-0.5 w-full">
|
||||
<div
|
||||
className="h-full rounded-lg bg-brand transition-all duration-300 ease-in-out"
|
||||
style={{
|
||||
width: `${percentage}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 max-w-280 w-full flex flex-col gap-y-4 p-8">
|
||||
<Outlet />
|
||||
</div>
|
||||
<div className="flex-1 max-w-5xl w-full flex flex-col gap-y-4 p-8 overflow-y-auto">
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-280 p-8">
|
||||
<div className="w-1/3 flex flex-row items-stretch gap-x-8 [&>*]:w-full ml-auto">
|
||||
<Button onClick={handleBack}>Back</Button>
|
||||
<Button onClick={handleNext}>{currentStepIndex === steps.length - 1 ? 'Finish' : 'Next'}</Button>
|
||||
<div className="w-full max-w-5xl p-8">
|
||||
<div className="w-1/3 flex flex-row items-stretch gap-x-8 [&>*]:w-full ml-auto">
|
||||
<Button color="secondary" isDisabled={!isBackButtonEnabled} onClick={previous}>Back</Button>
|
||||
<Button color="primary" isDisabled={!isForwardButtonEnabled} onClick={shallRunTasks ? runTasks : next}>
|
||||
{getButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</OnboardingContext>
|
||||
);
|
||||
}
|
||||
|
||||
export interface OnboardingStepProps {
|
||||
title: string;
|
||||
paragraph: string;
|
||||
illustration: JSX.Element;
|
||||
children: JSX.Element;
|
||||
illustration?: ReactNode;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function OnboardingStep({
|
||||
|
|
@ -112,18 +281,18 @@ export function OnboardingStep({
|
|||
children,
|
||||
}: OnboardingStepProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 h-full w-full gap-x-16">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
{illustration}
|
||||
</div>
|
||||
|
||||
<div className={`grid ${illustration ? 'grid-cols-1 md:grid-cols-2' : 'grid-cols-1'} h-full w-full gap-x-16`}>
|
||||
{illustration && (
|
||||
<div className="hidden md:flex flex-col items-center justify-center p-4">
|
||||
{illustration}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col justify-center gap-y-4">
|
||||
<div className="w-full flex flex-col gap-y-2">
|
||||
<h1 className="text-2xl">{title}</h1>
|
||||
<p className="text-lg text-fg-secondary line-height-normal">{paragraph}</p>
|
||||
<h1 className="text-3xl font-semibold text-text-primary">{title}</h1>
|
||||
<p className="text-lg text-text-secondary leading-relaxed">{paragraph}</p>
|
||||
</div>
|
||||
|
||||
<div className="max-h-96 w-full flex flex-1 flex-col gap-y-2">
|
||||
<div className="max-h-[30rem] w-full flex-1 flex flex-col gap-y-2 overflow-y-auto pr-2">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,3 +9,9 @@ export const LAUNCHER_IMPORT_TYPES: Array<string> = [
|
|||
// 'TLauncher',
|
||||
// 'Technic'
|
||||
] as const;
|
||||
|
||||
export function pluralize(n: number, word: string, locale: string = 'en'): string {
|
||||
const pluralRules = new Intl.PluralRules(locale);
|
||||
const pluralForm = pluralRules.select(n);
|
||||
return pluralForm === 'one' ? word : `${word}s`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue