feat: Make onboarding experience slightly better + fade into main App after completion

This commit is contained in:
LynithDev 2024-09-14 19:46:13 +02:00
parent fd9a910b12
commit 305bed88ad
6 changed files with 133 additions and 55 deletions

View file

@ -1,4 +1,5 @@
import { PROGRAM_INFO } from '@onelauncher/client/bindings';
import AnimatedRoutes from '~ui/components/AnimatedRoutes';
import { onMount, type ParentProps } from 'solid-js';
import { MultiProvider } from './ui/components/MultiProvider';
import { AccountControllerProvider } from './ui/components/overlay/account/AddAccountModal';
@ -20,7 +21,9 @@ function RootLayout(props: ParentProps) {
<main class="h-screen max-h-screen min-h-screen w-full flex flex-col overflow-hidden bg-page text-fg-primary">
<WindowFrame />
{props.children}
<AnimatedRoutes animation="fade" appear>
{props.children}
</AnimatedRoutes>
<NotificationOverlay />
<ModalRenderer />

View file

@ -2,18 +2,36 @@ import useSettings from '~ui/hooks/useSettings';
import { type ParentProps, Show } from 'solid-js';
import { Transition, type TransitionProps } from 'solid-transition-group';
function AnimatedRoutes(props: TransitionProps & ParentProps) {
type AnimationTypes = 'default' | 'fade';
export interface AnimatedProps {
animation?: AnimationTypes;
};
const animations: { [key in AnimationTypes]: { before: Keyframe; after: Keyframe } } = {
default: {
before: {
transform: 'translateX(-85px)',
opacity: 0,
},
after: {
transform: 'translateX(0)',
opacity: 1,
},
},
fade: {
before: {
opacity: 0,
},
after: {
opacity: 1,
},
},
};
function AnimatedRoutes(props: AnimatedProps & TransitionProps & ParentProps) {
const { settings } = useSettings();
const before: Keyframe = {
transform: 'translateX(-85px)',
opacity: 0,
};
const after: Keyframe = {
transform: 'translateX(0)',
opacity: 1,
};
const animation = () => animations[props.animation ?? 'default'];
return (
<Show
@ -27,8 +45,8 @@ function AnimatedRoutes(props: TransitionProps & ParentProps) {
}
el.animate([
before,
after,
animation().before,
animation().after,
], {
duration: 90,
easing: 'cubic-bezier(0.16, 1, 0.3, 1)',
@ -43,8 +61,8 @@ function AnimatedRoutes(props: TransitionProps & ParentProps) {
}
el.animate([
after,
before,
animation().after,
animation().before,
], {
duration: 95,
easing: 'cubic-bezier(0.16, 1, 0.3, 1)',
@ -56,9 +74,7 @@ function AnimatedRoutes(props: TransitionProps & ParentProps) {
{props.children}
</Transition>
)}
fallback={(
<>{props.children}</>
)}
fallback={(<>{props.children}</>)}
when={settings().disable_animations !== true}
/>
);

View file

@ -3,7 +3,6 @@ import useSettings from '~ui/hooks/useSettings';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-solid';
import type { ParentProps } from 'solid-js';
import AnimatedRoutes from '../components/AnimatedRoutes';
import ErrorBoundary from '../components/ErrorBoundary';
import Navbar from '../components/Navbar';
function App(props: ParentProps) {
@ -14,25 +13,23 @@ function App(props: ParentProps) {
navigate('/onboarding');
return (
<>
<div class="h-full flex flex-col">
<div class="flex flex-col px-8">
<Navbar />
</div>
<div class="relative h-full w-full overflow-hidden">
<div class="absolute left-0 top-0 h-full w-full flex flex-col overflow-x-hidden">
<ErrorBoundary>
<OverlayScrollbarsComponent class="os-hide-horizontal-scrollbar absolute left-0 top-0 h-full w-full flex flex-col overflow-x-hidden overflow-y-auto px-8 pb-8">
<div class="h-full flex-1">
<AnimatedRoutes>
{props.children}
</AnimatedRoutes>
</div>
</OverlayScrollbarsComponent>
</ErrorBoundary>
<div class="h-full w-full overflow-hidden">
<div class="relative h-full w-full flex flex-col overflow-x-hidden">
<OverlayScrollbarsComponent class="os-hide-horizontal-scrollbar absolute left-0 top-0 h-full w-full flex flex-col overflow-x-hidden overflow-y-auto px-8 pb-8">
<div class="h-full flex-1">
<AnimatedRoutes>
{props.children}
</AnimatedRoutes>
</div>
</OverlayScrollbarsComponent>
</div>
</div>
</>
</div>
);
}

View file

@ -1,14 +1,14 @@
import { Route, useLocation, useNavigate } from '@solidjs/router';
import { Route, useBeforeLeave, 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 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 Accessor, type Context, createContext, createEffect, createSignal, type JSX, on, type ParentProps, useContext } from 'solid-js';
import type { ImportType } from '@onelauncher/client/bindings';
import OnboardingComplete from './OnboardingComplete';
import OnboardingImport from './OnboardingImport';
import OnboardingLanguage, { type Language } from './OnboardingLanguage';
import OnboardingLanguage, { type Language, LanguagesList } from './OnboardingLanguage';
import OnboardingSummary from './OnboardingSummary';
import OnboardingWelcome from './OnboardingWelcome';
@ -34,7 +34,10 @@ interface OnboardingContextType {
setLanguage: (language: Language) => void;
setImportTypes: (types: ImportType[]) => void;
getTasks: () => string[];
tasksStage: Accessor<OnboardingTaskStage>;
tasksMessage: Accessor<string>;
}
const OnboardingContext = createContext() as Context<OnboardingContextType>;
@ -50,6 +53,7 @@ function Onboarding(props: ParentProps) {
const [language, setLanguage] = createSignal<Language>('en');
const [importTypes, setImportTypes] = createSignal<ImportType[]>([]);
const [tasksStage, setTasksStage] = createSignal<OnboardingTaskStage>(OnboardingTaskStage.NotStarted);
const [tasksMessage, setTasksMessage] = createSignal<string>('');
const tasksCompleted = () => tasksStage() === OnboardingTaskStage.Completed;
@ -57,8 +61,13 @@ function Onboarding(props: ParentProps) {
const step = () => OnboardingSteps.findIndex(([path]) => (basePath + path).startsWith(location.pathname));
const shallRunTasks = () => !tasksCompleted() && step() === OnboardingSteps.length - 2;
createEffect(() => {
createEffect(on(() => location.pathname, () => {
setBackButtonEnabled(step() > 0 && !tasksCompleted());
}));
useBeforeLeave((e) => {
if (tasksCompleted() && step() === OnboardingSteps.length - 1 && !settings().onboarding_completed)
e.preventDefault();
});
const next = () => {
@ -94,26 +103,49 @@ function Onboarding(props: ParentProps) {
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;
async () => {
setTasksMessage('Setting language');
await setAsyncTimeout(500);
},
async () => {
setTasksMessage('Importing data');
await setAsyncTimeout(1000);
},
].map(task => task());
const errorCount = (await Promise.allSettled(tasks)).filter(({ status }) => status === 'rejected').length;
if (errorCount > 0)
console.error('Error count:', errorCount); // TODO: Show a toast or something idk
setForwardButtonEnabled(true);
setTasksStage(OnboardingTaskStage.Completed);
next(); // Go to Completion page
};
const getTasks = () => {
const tasks = [];
tasks.push(`Set language to ${LanguagesList[language()][0]}`);
importTypes().forEach((type) => {
tasks.push(`Import profiles from ${type}`);
});
return tasks;
};
const ctx: OnboardingContextType = {
setLanguage,
setImportTypes,
getTasks,
tasksStage,
tasksMessage,
};
return (

View file

@ -1,20 +1,40 @@
import Onboarding from './Onboarding';
import AnimatedRoutes from '~ui/components/AnimatedRoutes';
import { For, Match, Switch } from 'solid-js';
import Onboarding, { OnboardingTaskStage } from './Onboarding';
function OnboardingSummary() {
const ctx = Onboarding.useContext();
return (
<div class="grid grid-cols-2 h-full w-full flex flex-col items-center justify-center gap-x-16">
<div class="grid grid-cols-2 h-full w-full flex flex-col items-start justify-center gap-x-16 gap-y-2">
<h1 class="text-6xl">
Chosen setup tasks
Prepare OneLauncher
</h1>
<h3>blah blah.</h3>
<h3>Are you sure you want to proceed with the following tasks?</h3>
<span>
Task Stage =
{ctx.tasksStage()}
</span>
<AnimatedRoutes>
<Switch>
<Match when={ctx.tasksStage() === OnboardingTaskStage.NotStarted}>
<div class="my-8 max-h-36 w-full flex flex-1 flex-col gap-y-2 rounded-lg bg-page-elevated p-4">
<For each={ctx.getTasks()}>
{task => (
<span class="text-lg text-fg-primary">
{task}
</span>
)}
</For>
</div>
</Match>
<Match when>
<div class="my-8 max-h-36 w-full flex flex-1 flex-col gap-y-2">
<div class="h-full w-full flex items-center justify-center">
<h2>Preparing...</h2>
</div>
</div>
</Match>
</Switch>
</AnimatedRoutes>
</div>
);
}

View file

@ -2,11 +2,21 @@ 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<T>(callback: () => T, ms: number): Promise<T> {
return new Promise((resolve) => {
export function setAsyncTimeout(ms: number): Promise<void>;
export function setAsyncTimeout(callback: () => any, ms: number): Promise<void>;
export function setAsyncTimeout(
callback: (() => any) | number,
ms?: number,
): Promise<void> {
const _ms = typeof callback === 'number' ? callback : ms;
return new Promise<void>((resolve) => {
setTimeout(() => {
resolve(callback());
}, ms);
if (typeof callback === 'function')
callback();
resolve();
}, _ms);
});
}