feat: Add Onboarding "tasks"
This commit is contained in:
parent
08f432c3e6
commit
fd9a910b12
7 changed files with 201 additions and 78 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<Settings>) => void;
|
||||
save: (settings: Settings) => void;
|
||||
save: (settings: Settings) => Promise<void>;
|
||||
}
|
||||
|
||||
const SettingsContext = createContext() as Context<SettingsControllerType>;
|
||||
|
|
@ -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 (
|
||||
<Show when={settings !== undefined && settings() !== undefined}>
|
||||
<SettingsContext.Provider value={controller}>
|
||||
|
|
|
|||
|
|
@ -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<OnboardingTaskStage>;
|
||||
}
|
||||
|
||||
const OnboardingContext = createContext() as Context<OnboardingContextType>;
|
||||
|
||||
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<Language>('en');
|
||||
const [importTypes, setImportTypes] = createSignal<ImportType[]>([]);
|
||||
const [tasksStage, setTasksStage] = createSignal<OnboardingTaskStage>(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 (
|
||||
<div class="h-full max-h-full w-full flex flex-col items-center justify-center">
|
||||
<div class="h-0.5 w-full">
|
||||
<div
|
||||
class="h-full rounded-lg bg-brand transition-all"
|
||||
style={{
|
||||
width: `${percentage()}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="h-full max-w-280 w-full flex flex-col gap-y-4 p-8">
|
||||
<div class="h-full w-full">
|
||||
<AnimatedRoutes>
|
||||
{props.children}
|
||||
</AnimatedRoutes>
|
||||
<OnboardingContext.Provider value={ctx}>
|
||||
<div class="h-full max-h-full w-full flex flex-col items-center justify-center">
|
||||
<div class="h-0.5 w-full">
|
||||
<div
|
||||
class="h-full rounded-lg bg-brand transition-all"
|
||||
style={{
|
||||
width: `${percentage()}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="z-1 w-full flex flex-1 flex-row items-end justify-end">
|
||||
<div class="w-1/3 flex flex-row items-stretch gap-x-8 [&>*]:w-full">
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
children="Previous"
|
||||
disabled={!canGoBack()}
|
||||
iconLeft={<ChevronLeftIcon />}
|
||||
onClick={previous}
|
||||
/>
|
||||
<div class="h-full max-w-280 w-full flex flex-col gap-y-4 p-8">
|
||||
<div class="h-full w-full">
|
||||
<AnimatedRoutes>
|
||||
{props.children}
|
||||
</AnimatedRoutes>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
children={canGoForward() ? 'Next' : 'Finish'}
|
||||
iconRight={<ChevronRightIcon />}
|
||||
onClick={next}
|
||||
/>
|
||||
<div class="z-1 w-full flex flex-1 flex-row items-end justify-end">
|
||||
<div class="w-1/3 flex flex-row items-stretch gap-x-8 [&>*]:w-full">
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
children="Previous"
|
||||
disabled={!backButtonEnabled()}
|
||||
iconLeft={<ChevronLeftIcon />}
|
||||
onClick={previous}
|
||||
/>
|
||||
|
||||
<Button
|
||||
children={getButtonText()}
|
||||
disabled={!forwardButtonEnabled()}
|
||||
iconRight={<ChevronRightIcon />}
|
||||
onClick={shallRunTasks() ? runTasks : next}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</OnboardingContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
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]) => (
|
||||
<Route component={component} path={path} />
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<OnboardingStep
|
||||
illustration={<Illustration />}
|
||||
|
|
|
|||
|
|
@ -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<Language>('en');
|
||||
|
||||
const getSelectedLanguage = () => LanguagesList[selected()];
|
||||
|
||||
useBeforeLeave(() => {
|
||||
ctx.setLanguage(selected());
|
||||
});
|
||||
|
||||
return (
|
||||
<OnboardingStep
|
||||
|
|
@ -42,14 +33,15 @@ function OnboardingLanguage() {
|
|||
<div class="flex flex-col gap-y-2 rounded-lg bg-page-elevated">
|
||||
<OverlayScrollbarsComponent class="max-h-84 overflow-hidden">
|
||||
<div class="flex flex-col gap-y-1 p-2">
|
||||
<For each={languages.sort((a, b) => b[1] - a[1])}>
|
||||
{language => (
|
||||
<For each={Object.entries(LanguagesList).sort((a, b) => b[1][1] - a[1][1])}>
|
||||
{([code, language]) => (
|
||||
<div
|
||||
class="flex flex-row items-center rounded-lg px-6 py-5"
|
||||
classList={{
|
||||
'bg-brand': language === selected(),
|
||||
'hover:bg-gray-05 active:bg-gray-10': language !== selected(),
|
||||
'bg-brand': language === getSelectedLanguage(),
|
||||
'hover:bg-gray-05 active:bg-gray-10': language !== getSelectedLanguage(),
|
||||
}}
|
||||
onClick={() => setSelected(code as Language)}
|
||||
>
|
||||
<div class="flex-1 text-lg font-medium">{language[0]}</div>
|
||||
<div class="flex-1 text-right text-xs">
|
||||
|
|
@ -63,7 +55,7 @@ function OnboardingLanguage() {
|
|||
</OverlayScrollbarsComponent>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex flex-1 flex-row justify-end">
|
||||
<div class="w-full flex flex-row justify-end">
|
||||
<Link class="text-xs" href={TRANSLATION_WEBSITE}>Help translate OneLauncher</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
22
apps/frontend/src/ui/pages/onboarding/OnboardingSummary.tsx
Normal file
22
apps/frontend/src/ui/pages/onboarding/OnboardingSummary.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import Onboarding 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">
|
||||
<h1 class="text-6xl">
|
||||
Chosen setup tasks
|
||||
</h1>
|
||||
|
||||
<h3>blah blah.</h3>
|
||||
|
||||
<span>
|
||||
Task Stage =
|
||||
{ctx.tasksStage()}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default OnboardingSummary;
|
||||
|
|
@ -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<T>(callback: () => T, ms: number): Promise<T> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(callback());
|
||||
}, ms);
|
||||
});
|
||||
}
|
||||
|
||||
export function supportsMods(loader?: Cluster | Loader): boolean {
|
||||
if (loader === undefined)
|
||||
return false;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue