feat: match notification style and re-enable notifications
This commit is contained in:
parent
65cceba2b3
commit
ad45798ea2
7 changed files with 86 additions and 63 deletions
|
|
@ -2,10 +2,9 @@ import type { ToOptions } from '@tanstack/react-router';
|
|||
import useNotifications from '@/hooks/useNotification';
|
||||
import { bindings } from '@/main';
|
||||
import { useCommand } from '@onelauncher/common';
|
||||
import { Button, Menu, Popup } from '@onelauncher/common/components';
|
||||
import { Button, Popup } from '@onelauncher/common/components';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { Bell01Icon, Cloud01Icon, TerminalBrowserIcon } from '@untitled-theme/icons-react';
|
||||
import { MenuTrigger } from 'react-aria-components';
|
||||
import OneLauncherText from '../assets/logos/onelauncher_text.svg';
|
||||
import PlayerHead from './content/PlayerHead';
|
||||
import AccountPopup from './overlay/popups/AccountPopup';
|
||||
|
|
@ -49,6 +48,7 @@ function Navbar() {
|
|||
</span>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Popup>
|
||||
<NotificationPopup />
|
||||
</Popup>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import type { GameLoader, Version, VersionType } from '@/bindings.gen';
|
||||
import type { JSX, RefAttributes } from 'react';
|
||||
import DefaultBanner from '@/assets/images/default_banner.png';
|
||||
import DefaultClusterBanner from '@/assets/images/default_instance_cover.jpg';
|
||||
import LauncherIcon from '@/components/content/LauncherIcon';
|
||||
import Modal from '@/components/overlay/Modal';
|
||||
import ScrollableContainer from '@/components/ScrollableContainer';
|
||||
|
|
@ -9,6 +8,7 @@ import { bindings } from '@/main';
|
|||
import { LAUNCHER_IMPORT_TYPES, LOADERS, upperFirst } from '@/utils';
|
||||
import { useCommand, useCommandMut } from '@onelauncher/common';
|
||||
import { Button, Dropdown, SelectList, TextField } from '@onelauncher/common/components';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { ArrowRightIcon, PlusIcon, SearchMdIcon, Server01Icon, TextInputIcon, User03Icon } from '@untitled-theme/icons-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Checkbox } from 'react-aria-components';
|
||||
|
|
@ -25,24 +25,37 @@ const STEPS = [
|
|||
},
|
||||
] as const;
|
||||
|
||||
interface ClusterFormData {
|
||||
loader: GameLoader;
|
||||
clusterName: string;
|
||||
clusterVersion: string;
|
||||
clusterProvider: string;
|
||||
}
|
||||
|
||||
export function NewClusterCreate() {
|
||||
const queryClient = useQueryClient();
|
||||
const [currentStepIndex, setCurrentStepIndex] = useState(0);
|
||||
const [formData, setFormData] = useState({
|
||||
loader: '',
|
||||
const [formData, setFormData] = useState<ClusterFormData>({
|
||||
loader: 'vanilla',
|
||||
clusterName: '',
|
||||
clusterVersion: '',
|
||||
clusterProvider: '',
|
||||
});
|
||||
|
||||
const create = useCommandMut(() => bindings.core.createCluster({
|
||||
icon: DefaultClusterBanner,
|
||||
mc_loader: formData.loader as GameLoader,
|
||||
icon: null,
|
||||
mc_loader: formData.loader,
|
||||
mc_loader_version: null,
|
||||
mc_version: formData.clusterVersion,
|
||||
name: formData.clusterName,
|
||||
}));
|
||||
}), {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['getClusters'] });
|
||||
},
|
||||
});
|
||||
|
||||
function handleCreate() {
|
||||
if (!isLastInputStep)
|
||||
if (!isLastInputStep && !formData.clusterName)
|
||||
return;
|
||||
|
||||
create.mutate();
|
||||
|
|
@ -70,10 +83,7 @@ export function NewClusterCreate() {
|
|||
switch (currentStepConfig.id) {
|
||||
case 'loader': {
|
||||
stepContent = (
|
||||
<ClusterLoader
|
||||
onSelectLoader={loader => setFormData(prev => ({ ...prev, loader }))}
|
||||
selectedLoader={formData.loader}
|
||||
/>
|
||||
<ClusterLoader onSelectProvider={provider => setFormData(prev => ({ ...prev, clusterProvider: provider }))} selectedProvider={formData.clusterProvider} />
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
|
@ -81,6 +91,7 @@ export function NewClusterCreate() {
|
|||
case 'info': {
|
||||
stepContent = (
|
||||
<ClusterInformation
|
||||
clusterLoader={formData.loader}
|
||||
clusterName={formData.clusterName}
|
||||
clusterVersion={formData.clusterVersion}
|
||||
onClusterLoaderChange={loader => setFormData(prev => ({ ...prev, loader }))}
|
||||
|
|
@ -98,7 +109,7 @@ export function NewClusterCreate() {
|
|||
const nextButtonText = isLastInputStep ? 'Create' : 'Next';
|
||||
|
||||
let isNextDisabled = false;
|
||||
if (currentStepConfig.id === 'loader' && !formData.loader)
|
||||
if (currentStepConfig.id === 'loader' && !formData.clusterProvider)
|
||||
isNextDisabled = true;
|
||||
else if (
|
||||
currentStepConfig.id === 'info'
|
||||
|
|
@ -167,19 +178,19 @@ export function NewClusterCreate() {
|
|||
}
|
||||
|
||||
interface ClusterLoaderProps {
|
||||
selectedLoader: string;
|
||||
onSelectLoader: (loader: string) => void;
|
||||
selectedProvider: string;
|
||||
onSelectProvider: (provider: string) => void;
|
||||
}
|
||||
|
||||
function ClusterLoader({ selectedLoader, onSelectLoader }: ClusterLoaderProps) {
|
||||
function ClusterLoader({ selectedProvider, onSelectProvider }: ClusterLoaderProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<ProviderCard
|
||||
icon={<User03Icon />}
|
||||
name="New"
|
||||
selected={selectedLoader === 'new'}
|
||||
setSelected={() => onSelectLoader('new')}
|
||||
selected={selectedProvider === 'new'}
|
||||
setSelected={() => onSelectProvider('new')}
|
||||
/>
|
||||
|
||||
{LAUNCHER_IMPORT_TYPES.map((data, i) => (
|
||||
|
|
@ -187,8 +198,8 @@ function ClusterLoader({ selectedLoader, onSelectLoader }: ClusterLoaderProps) {
|
|||
icon={<LauncherIcon launcher={data} />}
|
||||
key={i}
|
||||
name={data}
|
||||
selected={selectedLoader === data}
|
||||
setSelected={() => onSelectLoader(data)}
|
||||
selected={selectedProvider === data}
|
||||
setSelected={() => onSelectProvider(data)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -224,6 +235,7 @@ interface ClusterInformationProps {
|
|||
onClusterNameChange: (name: string) => void;
|
||||
clusterVersion: string;
|
||||
onClusterVersionChange: (version: string) => void;
|
||||
clusterLoader: GameLoader;
|
||||
onClusterLoaderChange: (loader: GameLoader) => void;
|
||||
}
|
||||
|
||||
|
|
@ -232,6 +244,7 @@ function ClusterInformation({
|
|||
onClusterNameChange,
|
||||
clusterVersion,
|
||||
onClusterVersionChange,
|
||||
clusterLoader,
|
||||
onClusterLoaderChange,
|
||||
}: ClusterInformationProps) {
|
||||
const versions = useCommand('getGameVersions', bindings.core.getGameVersions);
|
||||
|
|
@ -244,6 +257,7 @@ function ClusterInformation({
|
|||
iconLeft={<TextInputIcon className="size-4" />}
|
||||
onChange={e => onClusterNameChange(e.target.value)}
|
||||
placeholder="Name"
|
||||
required
|
||||
value={clusterName}
|
||||
/>
|
||||
</Option>
|
||||
|
|
@ -254,7 +268,7 @@ function ClusterInformation({
|
|||
|
||||
<Option header="Loader">
|
||||
{/* TODO: fixme */}
|
||||
<Dropdown defaultSelectedKey="vanilla" onSelectionChange={e => onClusterLoaderChange(e.toString() as GameLoader)}>
|
||||
<Dropdown defaultSelectedKey="vanilla" onSelectionChange={e => onClusterLoaderChange(e.toString() as GameLoader)} selectedKey={clusterLoader}>
|
||||
{LOADERS.map(loader => (
|
||||
<Dropdown.Item id={loader} key={loader}>
|
||||
<div className="flex flex-row gap-2">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { NotificationData } from '@/hooks/useNotification';
|
||||
import useNotifications from '@/hooks/useNotification';
|
||||
import { Button } from '@onelauncher/common/components';
|
||||
import { XIcon } from '@untitled-theme/icons-react';
|
||||
import { Button, Show } from '@onelauncher/common/components';
|
||||
import { InfoCircleIcon, XIcon } from '@untitled-theme/icons-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
|
|
@ -15,6 +15,8 @@ interface NotificationToastProps {
|
|||
onRemove: (id: string) => void;
|
||||
}
|
||||
|
||||
const fractionEnded = (fraction: number | undefined) => fraction !== undefined && fraction >= 0.97;
|
||||
|
||||
function NotificationToast({ notification, onRemove }: NotificationToastProps) {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isRemoving, setIsRemoving] = useState(false);
|
||||
|
|
@ -41,22 +43,13 @@ function NotificationToast({ notification, onRemove }: NotificationToastProps) {
|
|||
}, [notification.fraction, handleRemove]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
'transform transition-all duration-300 ease-in-out',
|
||||
'bg-component-bg border border-component-border rounded-lg shadow-lg',
|
||||
'p-4 mb-3 max-w-sm',
|
||||
isVisible && !isRemoving ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-semibold text-fg-primary text-sm truncate">
|
||||
{notification.title}
|
||||
</h4>
|
||||
<p className="text-fg-secondary text-xs mt-1 break-words">
|
||||
{notification.message}
|
||||
</p>
|
||||
<div className={twMerge('flex flex-col gap-y-1 p-2 bg-component-bg border border-component-border rounded-lg shadow-lg', isVisible && !isRemoving ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0')}>
|
||||
<div className="grid grid-cols-[24px_1fr_auto] min-h-10 place-items-center gap-3">
|
||||
<InfoCircleIcon className="h-6 w-6 text-fg-primary" />
|
||||
|
||||
<div className="w-full flex flex-col gap-y-1">
|
||||
<span className="text-fg-primary font-medium capitalize">{notification.title}</span>
|
||||
<span className="text-sm text-fg-secondary/60 capitalize">{notification.message}</span>
|
||||
</div>
|
||||
<Button
|
||||
className="flex-shrink-0 h-6 w-6 p-0"
|
||||
|
|
@ -68,14 +61,16 @@ function NotificationToast({ notification, onRemove }: NotificationToastProps) {
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
{notification.fraction !== undefined && (
|
||||
<div className="w-full bg-component-bg-hover rounded-full h-2 mt-3">
|
||||
<Show when={notification.fraction !== undefined && !fractionEnded(notification.fraction)}>
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-brand-disabled/10">
|
||||
<div
|
||||
className="bg-brand h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${Math.max(0, Math.min(100, notification.fraction))}%` }}
|
||||
className="h-full max-w-full min-w-0 rounded-full bg-brand transition-width"
|
||||
style={{
|
||||
width: `${Math.floor(notification.fraction! * 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import type { NotificationData } from '@/hooks/useNotification';
|
||||
import useNotifications from '@/hooks/useNotification';
|
||||
import { Button, Menu, Popup } from '@onelauncher/common/components';
|
||||
import { Button, Popup } from '@onelauncher/common/components';
|
||||
import { XIcon } from '@untitled-theme/icons-react';
|
||||
import { Popover } from 'react-aria-components';
|
||||
|
||||
interface NotificationItemProps {
|
||||
id: string;
|
||||
|
|
@ -60,7 +59,7 @@ function NotificationPopup() {
|
|||
);
|
||||
|
||||
return (
|
||||
<Popup className="min-w-80 w-80 max-w-96 max-h-96 overflow-y-auto">
|
||||
<div className="min-w-80 w-80 max-w-96 max-h-96 overflow-y-auto">
|
||||
<div className="flex items-center justify-between border-b border-component-border">
|
||||
<h3 className="font-semibold text-fg-primary">Notifications</h3>
|
||||
{notificationEntries.length > 0 && (
|
||||
|
|
@ -86,7 +85,7 @@ function NotificationPopup() {
|
|||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Popup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ interface NotificationContextValue {
|
|||
|
||||
const defaultContextValue: NotificationContextValue = {
|
||||
notifications: {},
|
||||
setNotifications: () => {},
|
||||
setNotifications: () => { },
|
||||
};
|
||||
|
||||
const NotificationContext = createContext<NotificationContextValue>(defaultContextValue);
|
||||
|
|
@ -42,13 +42,29 @@ export const NotificationProvider = memo(({ children }: NotificationProviderProp
|
|||
let cleanup: (() => void) | undefined;
|
||||
|
||||
bindings.events.ingress.on((e) => {
|
||||
setNotifications(prev => ({
|
||||
...prev,
|
||||
[e.id]: {
|
||||
title: `${e.ingress_type}`,
|
||||
message: e.message,
|
||||
},
|
||||
}));
|
||||
if (typeof e.ingress_type === 'object') {
|
||||
if ('PrepareCluster' in e.ingress_type) {
|
||||
const { cluster_name } = e.ingress_type.PrepareCluster;
|
||||
setNotifications(prev => ({
|
||||
...prev,
|
||||
[e.id]: {
|
||||
title: `Preparing ${cluster_name}`,
|
||||
message: e.message,
|
||||
fraction: e.percent ?? undefined,
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
else {
|
||||
setNotifications(prev => ({
|
||||
...prev,
|
||||
[e.id]: {
|
||||
title: `${e.ingress_type}`,
|
||||
message: e.message,
|
||||
fraction: e.percent ?? undefined,
|
||||
},
|
||||
}));
|
||||
}
|
||||
}).then((u) => {
|
||||
cleanup = u;
|
||||
}).catch(console.error);
|
||||
|
|
@ -81,10 +97,10 @@ function useNotifications(): HookReturn {
|
|||
console.warn('useNotifications: Using default context (likely due to HMR or missing provider)');
|
||||
return {
|
||||
list: {},
|
||||
set: () => {},
|
||||
create: () => {},
|
||||
remove: () => {},
|
||||
clear: () => {},
|
||||
set: () => { },
|
||||
create: () => { },
|
||||
remove: () => { },
|
||||
clear: () => { },
|
||||
};
|
||||
}
|
||||
throw new Error('useNotifications must be used within a NotificationProvider');
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { createFileRoute } from '@tanstack/react-router';
|
|||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import { Edit02Icon, FolderIcon, ImagePlusIcon, Share07Icon, Tool02Icon, Trash01Icon } from '@untitled-theme/icons-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import Sidebar from '../settings/route';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import { createFileRoute, Link } from '@tanstack/react-router';
|
|||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import { PlayIcon } from '@untitled-theme/icons-react';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const Route = createFileRoute('/app/')({
|
||||
component: RouteComponent,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue