diff --git a/.gitignore b/.gitignore index 11de568f..590f3315 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ dev.db dev.db-journal .cargo/config.toml .deps/* +vite.config.ts.timestamp* # Created by https://www.toptal.com/developers/gitignore/api/node,rust,yarn,linux,macos,windows,intellij+all,rust-analyzer,visualstudiocode,intellij,intellij+iml,git # Edit at https://www.toptal.com/developers/gitignore?templates=node,rust,yarn,linux,macos,windows,intellij+all,rust-analyzer,visualstudiocode,intellij,intellij+iml,git diff --git a/apps/frontend/src/ui/hooks/useSettings.tsx b/apps/frontend/src/ui/hooks/useSettings.tsx index e485ee5e..19c440eb 100644 --- a/apps/frontend/src/ui/hooks/useSettings.tsx +++ b/apps/frontend/src/ui/hooks/useSettings.tsx @@ -1,7 +1,7 @@ +import { PROGRAM_INFO, type Settings } from '@onelauncher/client/bindings'; import { useBeforeLeave } from '@solidjs/router'; import { bridge } from '~imports'; import { type Context, createContext, createEffect, type ParentProps, Show, useContext } from 'solid-js'; -import type { Settings } from '@onelauncher/client/bindings'; import useCommand from './useCommand'; /** @@ -14,7 +14,7 @@ export function syncSettings(settings: Settings) { interface SettingsControllerType { settings: () => Settings; saveOnLeave: (settings: () => Partial) => void; - save: (settings: Settings) => void; + save: (settings: Settings) => Promise; } const SettingsContext = createContext() as Context; @@ -37,14 +37,17 @@ export function SettingsProvider(props: ParentProps) { }); }); }, - save: (settings) => { - bridge.commands.setSettings(settings).then(() => { - syncSettings(settings); - refetch(); - }); + save: async (settings) => { + await bridge.commands.setSettings(settings); + syncSettings(settings); + await refetch(); }, }; + if (PROGRAM_INFO.dev_build) + // @ts-expect-error - Expose settings globally for debugging purposes + window.onelauncherSettings = controller; + return ( diff --git a/apps/frontend/src/ui/pages/onboarding/Onboarding.tsx b/apps/frontend/src/ui/pages/onboarding/Onboarding.tsx index d2b3c5c1..3eb9d663 100644 --- a/apps/frontend/src/ui/pages/onboarding/Onboarding.tsx +++ b/apps/frontend/src/ui/pages/onboarding/Onboarding.tsx @@ -2,81 +2,171 @@ import { Route, useLocation, useNavigate } from '@solidjs/router'; import { ChevronLeftIcon, ChevronRightIcon } from '@untitled-theme/icons-solid'; import AnimatedRoutes from '~ui/components/AnimatedRoutes'; import Button from '~ui/components/base/Button'; -import type { JSX, ParentProps } from 'solid-js'; +import useSettings from '~ui/hooks/useSettings'; +import { setAsyncTimeout } from '~utils'; +import { type Accessor, type Context, createContext, createEffect, createSignal, type JSX, type ParentProps, useContext } from 'solid-js'; +import type { ImportType } from '@onelauncher/client/bindings'; import OnboardingComplete from './OnboardingComplete'; import OnboardingImport from './OnboardingImport'; -import OnboardingLanguage from './OnboardingLanguage'; +import OnboardingLanguage, { type Language } from './OnboardingLanguage'; +import OnboardingSummary from './OnboardingSummary'; import OnboardingWelcome from './OnboardingWelcome'; const basePath = '/onboarding'; const OnboardingSteps = [ ['/', OnboardingWelcome], + ['/language', OnboardingLanguage], ['/import', OnboardingImport], - ['/complete', OnboardingComplete], + + ['/summary', OnboardingSummary], // Second to last will always be the summary (which is basically a confirmation for all the tasks that are about to be done) + ['/complete', OnboardingComplete], // Last will always be a "onelauncher is ready to use" page ] as const; +// eslint-disable-next-line no-restricted-syntax -- - +export const enum OnboardingTaskStage { + NotStarted = 0, + Running = 1, + Completed = 2, +} + +interface OnboardingContextType { + setLanguage: (language: Language) => void; + setImportTypes: (types: ImportType[]) => void; + + tasksStage: Accessor; +} + +const OnboardingContext = createContext() as Context; + function Onboarding(props: ParentProps) { const navigate = useNavigate(); const location = useLocation(); + const { settings, save: saveSettings } = useSettings(); - const step = () => OnboardingSteps.findIndex(([path]) => location.pathname === basePath + path); + const [backButtonEnabled, setBackButtonEnabled] = createSignal(false); + const [forwardButtonEnabled, setForwardButtonEnabled] = createSignal(true); - const canGoBack = () => step() > 0; - const canGoForward = () => step() < OnboardingSteps.length - 1; + const [language, setLanguage] = createSignal('en'); + const [importTypes, setImportTypes] = createSignal([]); + const [tasksStage, setTasksStage] = createSignal(OnboardingTaskStage.NotStarted); + + const tasksCompleted = () => tasksStage() === OnboardingTaskStage.Completed; + + const percentage = () => (step() / (OnboardingSteps.length - 1)) * 100; + const step = () => OnboardingSteps.findIndex(([path]) => (basePath + path).startsWith(location.pathname)); + const shallRunTasks = () => !tasksCompleted() && step() === OnboardingSteps.length - 2; + + createEffect(() => { + setBackButtonEnabled(step() > 0 && !tasksCompleted()); + }); const next = () => { - if (canGoForward()) + const isLast = step() === OnboardingSteps.length - 1; + + if (isLast) + saveSettings({ + ...settings(), + onboarding_completed: true, + }).then(() => navigate('/')); + else navigate(basePath + OnboardingSteps[step() + 1]![0]); }; const previous = () => { - if (canGoBack()) + if (step() > 0) navigate(basePath + OnboardingSteps[step() - 1]![0]); }; - const percentage = () => (step() / (OnboardingSteps.length - 1)) * 100; + const getButtonText = () => { + if (shallRunTasks()) + return 'Setup'; + else if (step() === OnboardingSteps.length - 1) + return 'Finish'; + else + return 'Next'; + }; + + const runTasks = async () => { + setBackButtonEnabled(false); + setForwardButtonEnabled(false); + + setTasksStage(OnboardingTaskStage.Running); + + const tasks = [ + // eslint-disable-next-line no-console -- - + setAsyncTimeout(() => console.log('Task 1'), 1500), + // eslint-disable-next-line no-console -- - + setAsyncTimeout(() => console.log('Task 2'), 3500), + ]; + + const promises = Object.values(tasks); + const errorCount = (await Promise.allSettled(promises)).filter(value => value.status === 'rejected').length; + + if (errorCount > 0) + console.error('Error count:', errorCount); // TODO: Show a toast or something idk + + setForwardButtonEnabled(true); + setTasksStage(OnboardingTaskStage.Completed); + }; + + const ctx: OnboardingContextType = { + setLanguage, + setImportTypes, + tasksStage, + }; return ( -
-
-
-
- -
-
- - {props.children} - + +
+
+
-
-
-
-
+ ); } +Onboarding.useContext = () => { + const ctx = useContext(OnboardingContext); + if (!ctx) + throw new Error('Onboarding context not found'); + + return ctx; +}; + Onboarding.Steps = OnboardingSteps; Onboarding.Routes = OnboardingSteps.map(([path, component]) => ( diff --git a/apps/frontend/src/ui/pages/onboarding/OnboardingImport.tsx b/apps/frontend/src/ui/pages/onboarding/OnboardingImport.tsx index 5a175782..0583c1ab 100644 --- a/apps/frontend/src/ui/pages/onboarding/OnboardingImport.tsx +++ b/apps/frontend/src/ui/pages/onboarding/OnboardingImport.tsx @@ -1,11 +1,18 @@ +import { useBeforeLeave } from '@solidjs/router'; import Illustration from '~assets/illustrations/onboarding/import_from_others.svg?component-solid'; import Image from '~assets/logos/vanilla.png'; import { LAUNCHER_IMPORT_TYPES } from '~utils'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-solid'; import { For } from 'solid-js'; -import { OnboardingStep } from './Onboarding'; +import Onboarding, { OnboardingStep } from './Onboarding'; function OnboardingImport() { + const ctx = Onboarding.useContext(); + + useBeforeLeave(() => { + ctx.setImportTypes([]); + }); + return ( } diff --git a/apps/frontend/src/ui/pages/onboarding/OnboardingLanguage.tsx b/apps/frontend/src/ui/pages/onboarding/OnboardingLanguage.tsx index 0a480c8b..846703bc 100644 --- a/apps/frontend/src/ui/pages/onboarding/OnboardingLanguage.tsx +++ b/apps/frontend/src/ui/pages/onboarding/OnboardingLanguage.tsx @@ -1,36 +1,27 @@ import { TRANSLATION_WEBSITE } from '@onelauncher/client'; +import { useBeforeLeave } from '@solidjs/router'; import Illustration from '~assets/illustrations/onboarding/language.svg?component-solid'; import Link from '~ui/components/base/Link'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-solid'; -import { For } from 'solid-js'; -import { OnboardingStep } from './Onboarding'; +import { createSignal, For } from 'solid-js'; +import Onboarding, { OnboardingStep } from './Onboarding'; + +export const LanguagesList = { + en: ['English', 100], +} as const; + +export type Language = keyof typeof LanguagesList; function OnboardingLanguage() { - const languages: [string, number][] = [ - ['English', 100], - ['German', 21], - ['Spanish', 12], - ['Polish', 0], - ['Russian', 0], - ['German', 21], - ['Spanish', 12], - ['Polish', 0], - ['Russian', 0], - ['German', 21], - ['Spanish', 12], - ['Polish', 0], - ['Russian', 0], - ['German', 21], - ['Spanish', 12], - ['Polish', 0], - ['Russian', 0], - ['German', 21], - ['Spanish', 12], - ['Polish', 0], - ['Russian', 0], - ]; + const ctx = Onboarding.useContext(); - const selected = () => languages[0]; + const [selected, setSelected] = createSignal('en'); + + const getSelectedLanguage = () => LanguagesList[selected()]; + + useBeforeLeave(() => { + ctx.setLanguage(selected()); + }); return (
- b[1] - a[1])}> - {language => ( + b[1][1] - a[1][1])}> + {([code, language]) => (
setSelected(code as Language)} >
{language[0]}
@@ -63,7 +55,7 @@ function OnboardingLanguage() {
-
+
Help translate OneLauncher
diff --git a/apps/frontend/src/ui/pages/onboarding/OnboardingSummary.tsx b/apps/frontend/src/ui/pages/onboarding/OnboardingSummary.tsx new file mode 100644 index 00000000..7fdaf803 --- /dev/null +++ b/apps/frontend/src/ui/pages/onboarding/OnboardingSummary.tsx @@ -0,0 +1,22 @@ +import Onboarding from './Onboarding'; + +function OnboardingSummary() { + const ctx = Onboarding.useContext(); + + return ( +
+

+ Chosen setup tasks +

+ +

blah blah.

+ + + Task Stage = + {ctx.tasksStage()} + +
+ ); +} + +export default OnboardingSummary; diff --git a/apps/frontend/src/utils/index.ts b/apps/frontend/src/utils/index.ts index 14966590..1847d34e 100644 --- a/apps/frontend/src/utils/index.ts +++ b/apps/frontend/src/utils/index.ts @@ -2,6 +2,14 @@ import { DurationFormat } from '@formatjs/intl-durationformat'; import { open } from '@tauri-apps/plugin-shell'; import type { Cluster, ImportType, License, Loader, PackageType, Providers, VersionType } from '@onelauncher/client/bindings'; +export function setAsyncTimeout(callback: () => T, ms: number): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve(callback()); + }, ms); + }); +} + export function supportsMods(loader?: Cluster | Loader): boolean { if (loader === undefined) return false;