feat: Make onboarding experience slightly better + fade into main App after completion
This commit is contained in:
parent
fd9a910b12
commit
305bed88ad
6 changed files with 133 additions and 55 deletions
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue