diff --git a/apps/onelauncher/frontend/src/bindings.gen.ts b/apps/onelauncher/frontend/src/bindings.gen.ts index 0803d759..66ad0339 100644 --- a/apps/onelauncher/frontend/src/bindings.gen.ts +++ b/apps/onelauncher/frontend/src/bindings.gen.ts @@ -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 -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, open_dev_tools: () => Promise }, -'core': { get_clusters: () => Promise, -get_cluster: (id: bigint) => Promise, -remove_cluster: (id: bigint) => Promise, -create_cluster: (options: CreateCluster) => Promise, -get_profile_or_default: (name: string | null) => Promise, -get_global_profile: () => Promise, -get_users: () => Promise, -get_user: (uuid: string) => Promise, -remove_user: (uuid: string) => Promise, -get_default_user: (fallback: boolean | null) => Promise, -set_default_user: (uuid: string | null) => Promise, -open_msa_login: () => Promise }, -'': { send_event: (event: LauncherEvent) => Promise } }; +'events': { ingress: (event: IngressPayload) => Promise, +message: (event: MessagePayload) => Promise, +process: (event: ProcessPayload) => Promise }, +'core': { getClusters: () => Promise, +getClusterById: (id: bigint) => Promise, +removeCluster: (id: bigint) => Promise, +createCluster: (options: CreateCluster) => Promise, +launchCluster: (id: bigint, uuid: string | null) => Promise, +getProfileOrDefault: (name: string | null) => Promise, +getGlobalProfile: () => Promise, +getUsers: () => Promise, +getUser: (uuid: string) => Promise, +removeUser: (uuid: string) => Promise, +getDefaultUser: (fallback: boolean | null) => Promise, +setDefaultUser: (uuid: string | null) => Promise, +openMsaLogin: () => Promise } }; -<<<<<<< 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; open_dev_tools: () => Promise }; - '': { send_event: (event: LauncherEvent) => Promise }; - 'core': { getClusters: () => Promise>; getClusterById: (id: bigint) => Promise; removeCluster: (id: bigint) => Promise; createCluster: (options: CreateCluster) => Promise; getProfileOrDefault: (name: string | null) => Promise; getGlobalProfile: () => Promise; getUsers: () => Promise>; getUser: (uuid: string) => Promise; removeUser: (uuid: string) => Promise; getDefaultUser: (fallback: boolean | null) => Promise; setDefaultUser: (uuid: string | null) => Promise; openMsaLogin: () => Promise }; -} -======= ->>>>>>> Stashed changes export type { InferCommandOutput } export const createTauRPCProxy = () => createProxy(ARGS_MAP) diff --git a/apps/onelauncher/frontend/src/routes/app/cluster/index.tsx b/apps/onelauncher/frontend/src/routes/app/cluster/index.tsx index e058f6e9..cfeadd8b 100644 --- a/apps/onelauncher/frontend/src/routes/app/cluster/index.tsx +++ b/apps/onelauncher/frontend/src/routes/app/cluster/index.tsx @@ -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; diff --git a/apps/onelauncher/frontend/src/routes/app/cluster/settings.tsx b/apps/onelauncher/frontend/src/routes/app/cluster/settings.tsx index 82b98d12..2efdc652 100644 --- a/apps/onelauncher/frontend/src/routes/app/cluster/settings.tsx +++ b/apps/onelauncher/frontend/src/routes/app/cluster/settings.tsx @@ -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, }); diff --git a/apps/onelauncher/frontend/src/routes/app/index.tsx b/apps/onelauncher/frontend/src/routes/app/index.tsx index b46af92d..aa9062e1 100644 --- a/apps/onelauncher/frontend/src/routes/app/index.tsx +++ b/apps/onelauncher/frontend/src/routes/app/index.tsx @@ -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() {
- Header Image + Header Image
bindings.core.launchCluster(id, null), { + enabled: false, + subscribed: false, + }); + + const handleLaunch = () => { + launch.refetch(); + + if (launch.error) + console.error(launch.error.message); + }; + return ( {/* */} +
diff --git a/apps/onelauncher/frontend/src/routes/app/settings/index.tsx b/apps/onelauncher/frontend/src/routes/app/settings/index.tsx index f944466a..4d720b45 100644 --- a/apps/onelauncher/frontend/src/routes/app/settings/index.tsx +++ b/apps/onelauncher/frontend/src/routes/app/settings/index.tsx @@ -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 ( diff --git a/apps/onelauncher/frontend/src/routes/onboarding/language.tsx b/apps/onelauncher/frontend/src/routes/onboarding/language.tsx index 267057aa..916f1df2 100644 --- a/apps/onelauncher/frontend/src/routes/onboarding/language.tsx +++ b/apps/onelauncher/frontend/src/routes/onboarding/language.tsx @@ -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 ( <> } paragraph="Choose your preferred language." title="Language" @@ -27,7 +19,7 @@ function RouteComponent() {
-
diff --git a/apps/onelauncher/frontend/src/routes/onboarding/route.tsx b/apps/onelauncher/frontend/src/routes/onboarding/route.tsx index e7cc7ef0..11e68ae1 100644 --- a/apps/onelauncher/frontend/src/routes/onboarding/route.tsx +++ b/apps/onelauncher/frontend/src/routes/onboarding/route.tsx @@ -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 = [ + { + lang: 'English', + code: 'en', + percentage: 100, + }, +]; + +enum OnboardingTaskStage { + NotStarted = 0, + Running = 1, + Completed = 2, +} + interface ImportInstancesType { basePath: string; instances: Array; } -interface _OnboardingContextType { +interface OnboardingContextType { setLanguage: (language: Language) => void; - language: () => Language; + language: Language | undefined; setImportInstances: (type: string, basePath: string, instances: Array) => void; - importInstances: (type: string) => ImportInstancesType | undefined; + getImportInstance: (type: string) => ImportInstancesType | undefined; - setForwardButtonEnabled: (enabled: boolean) => void; + setIsForwardButtonEnabled: (enabled: boolean) => void; getTasks: () => Array; + + tasksStage: OnboardingTaskStage; + tasksMessage: string; +} + +const OnboardingContext = createContext(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(); + const [importInstancesMap, setImportInstancesMap] = useState>(new Map()); + const [tasksStage, setTasksStage] = useState(OnboardingTaskStage.NotStarted); + const [tasksMessage, setTasksMessage] = useState(''); + + 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 => { + const tasksStrings: Array = []; + 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) => { + 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! -
-
-
-
+ +
+
+
+
-
- -
+
+ +
-
-
- - +
+
+ + +
-
+ ); } 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 ( -
-
- {illustration} -
- +
+ {illustration && ( +
+ {illustration} +
+ )}
-

{title}

-

{paragraph}

+

{title}

+

{paragraph}

- -
+
{children}
diff --git a/apps/onelauncher/frontend/src/utils/index.ts b/apps/onelauncher/frontend/src/utils/index.ts index ea104d2c..498f81ca 100644 --- a/apps/onelauncher/frontend/src/utils/index.ts +++ b/apps/onelauncher/frontend/src/utils/index.ts @@ -9,3 +9,9 @@ export const LAUNCHER_IMPORT_TYPES: Array = [ // '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`; +}