227 lines
5.2 KiB
TypeScript
227 lines
5.2 KiB
TypeScript
import type { IngressType } from '@/bindings.gen';
|
|
import type { ToastData, ToastId } from '@/utils/toast';
|
|
import type { HTMLAttributes } from 'react';
|
|
import type { ToastContentProps, ToastOptions } from 'react-toastify';
|
|
import { Spinner } from '@/components';
|
|
import { bindings } from '@/main';
|
|
import { toastUpdate, useToast } from '@/utils/toast';
|
|
import { Button } from '@onelauncher/common/components';
|
|
import { AlertCircleIcon, AlertTriangleIcon, CheckCircleIcon, InfoCircleIcon, XCloseIcon } from '@untitled-theme/icons-react';
|
|
import { useEffect, useRef } from 'react';
|
|
import { cssTransition, ToastContainer } from 'react-toastify';
|
|
import { tv } from 'tailwind-variants';
|
|
import styles from './Toasts.module.css';
|
|
|
|
const toastVariants = tv({
|
|
slots: {
|
|
container: [
|
|
'relative h-full flex flex-row w-full justify-start items-center gap-3 py-4 px-4 rounded-lg overflow-hidden',
|
|
'inset-ring inset-ring-component-border bg-component-bg',
|
|
],
|
|
closeBtn: [
|
|
'absolute top-2 right-2',
|
|
],
|
|
},
|
|
variants: {
|
|
clickable: {
|
|
true: {
|
|
container: [
|
|
'hover:bg-component-bg-hover active:bg-component-bg-pressed',
|
|
],
|
|
},
|
|
false: {
|
|
container: [],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
export function Toast({
|
|
closeToast,
|
|
data,
|
|
toastProps,
|
|
isPaused,
|
|
}: ToastContentProps<ToastData>) {
|
|
const { container, closeBtn } = toastVariants({
|
|
clickable: toastProps.closeOnClick || typeof toastProps.onClick !== 'undefined',
|
|
});
|
|
|
|
return (
|
|
<div className={container()}>
|
|
<ToastIcon isLoading={toastProps.isLoading} type={data.type} />
|
|
|
|
<div className="flex flex-col">
|
|
<h4 className="text-lg font-medium">{data.title}</h4>
|
|
{data.message && <p className="text-sm text-fg-secondary">{data.message}</p>}
|
|
</div>
|
|
|
|
<div className={closeBtn()}>
|
|
<Button
|
|
color="ghost"
|
|
onClick={closeToast}
|
|
size="icon"
|
|
>
|
|
<XCloseIcon />
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="w-full h-1 absolute left-0 right-0 bottom-0">
|
|
<ToastProgressBar
|
|
closeToast={closeToast}
|
|
data={data}
|
|
isPaused={isPaused}
|
|
toastProps={toastProps}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ToastProgressBar({
|
|
isPaused,
|
|
toastProps,
|
|
closeToast,
|
|
data,
|
|
}: {
|
|
toastProps: ToastOptions;
|
|
isPaused: boolean;
|
|
closeToast: () => void;
|
|
data: ToastData;
|
|
}) {
|
|
let progress = toastProps.progress;
|
|
|
|
if (data.shouldAutoClose === false) {
|
|
isPaused = true;
|
|
progress = 0;
|
|
}
|
|
|
|
const attrs: HTMLAttributes<HTMLDivElement> = {};
|
|
|
|
if (typeof progress === 'number') {
|
|
attrs.style = {
|
|
transition: 'width 0.2s ease-in-out',
|
|
width: `${progress * 100}%`,
|
|
};
|
|
|
|
if (progress >= 1)
|
|
attrs.onTransitionEnd = closeToast;
|
|
}
|
|
else {
|
|
attrs.style = {
|
|
animationName: styles['custom-anim-progress-bar'],
|
|
animationDuration: `${toastProps.autoClose}ms`,
|
|
animationPlayState: isPaused ? 'paused' : 'running',
|
|
animationTimingFunction: 'linear',
|
|
animationDelay: '500ms',
|
|
};
|
|
|
|
attrs.onAnimationEnd = closeToast;
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="h-1 bg-brand"
|
|
{...attrs}
|
|
>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
type UnionToIntersection<T> =
|
|
(T extends any ? (x: T) => void : never) extends ((x: infer I) => void) ? I : never;
|
|
|
|
const ingressNames: Record<(IngressType & string) | (keyof UnionToIntersection<IngressType & object>), string> = {
|
|
JavaPrepare: 'Preparing Java',
|
|
JavaCheck: 'Checking Java',
|
|
JavaLocate: 'Locating Java',
|
|
MinecraftDownload: 'Downloading Minecraft',
|
|
Download: 'Downloading',
|
|
PrepareCluster: 'Preparing Cluster',
|
|
};
|
|
|
|
export function Toasts() {
|
|
const toastIngressMapRef = useRef(new Map<string, ToastId>());
|
|
const listenerRef = useRef(false);
|
|
const toast = useToast();
|
|
|
|
useEffect(() => {
|
|
if (listenerRef.current)
|
|
return;
|
|
|
|
listenerRef.current = true;
|
|
|
|
let unlisten: (() => void) | undefined;
|
|
|
|
bindings.events.ingress.on((e) => {
|
|
const toastIngressMap = toastIngressMapRef.current;
|
|
|
|
const ingressType = typeof e.ingress_type === 'string'
|
|
? e.ingress_type
|
|
: Object.keys(e.ingress_type)[0] ?? 'Info';
|
|
|
|
const title = ingressNames[ingressType as keyof typeof ingressNames] ?? ingressType;
|
|
|
|
const existingToastId = toastIngressMap.get(e.id);
|
|
|
|
if (existingToastId) {
|
|
toastUpdate(existingToastId, {
|
|
progress: e.percent ?? 1,
|
|
isLoading: true,
|
|
message: e.message,
|
|
title,
|
|
});
|
|
}
|
|
else {
|
|
const toastId = toast({
|
|
type: 'info',
|
|
title,
|
|
progress: e.percent ?? 1,
|
|
isLoading: true,
|
|
message: e.message,
|
|
});
|
|
|
|
toastIngressMap.set(e.id, toastId);
|
|
}
|
|
}).then((fn) => {
|
|
unlisten = fn;
|
|
});
|
|
|
|
return () => {
|
|
if (unlisten)
|
|
unlisten();
|
|
};
|
|
}, [toast]);
|
|
|
|
return (
|
|
<ToastContainer
|
|
newestOnTop
|
|
transition={cssTransition({
|
|
enter: 'animate-fade animate-duration-75',
|
|
exit: 'animate-fade animate-duration-75 animate-reverse',
|
|
})}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function ToastIcon({
|
|
isLoading,
|
|
type,
|
|
}: {
|
|
isLoading: boolean | undefined;
|
|
type: ToastData['type'];
|
|
}) {
|
|
if (isLoading)
|
|
return <Spinner size="extraSmall" />;
|
|
|
|
switch (type) {
|
|
case 'success':
|
|
return <CheckCircleIcon className="w-6 h-6 text-success" />;
|
|
case 'warning':
|
|
return <AlertTriangleIcon className="w-6 h-6 text-code-warn" />;
|
|
case 'error':
|
|
return <AlertCircleIcon className="w-6 h-6 text-danger" />;
|
|
case 'info':
|
|
default:
|
|
return <InfoCircleIcon className="w-6 h-6 text-code-info" />;
|
|
}
|
|
}
|