feat: home page animations
This commit is contained in:
parent
fcab0ba6ed
commit
aa72db1e0b
6 changed files with 180 additions and 93 deletions
|
|
@ -167,7 +167,7 @@ export type SettingProfileModel = { name: string; java_id: number | null; res: R
|
|||
|
||||
export type Settings = { global_game_settings: SettingProfileModel; allow_parallel_running_clusters: boolean; enable_gamemode: boolean; discord_enabled: boolean; max_concurrent_requests: number; settings_version: number; native_window_frame: boolean }
|
||||
|
||||
export type SettingsOsExtra = Record<string, never>
|
||||
export type SettingsOsExtra = { enable_gamemode: boolean | null }
|
||||
|
||||
export type Sort = "Relevance" | "Downloads" | "Newest" | "Updated"
|
||||
|
||||
|
|
@ -230,14 +230,11 @@ export type VersionType =
|
|||
*/
|
||||
"old_beta"
|
||||
|
||||
const ARGS_MAP = { 'core':'{"getPackageBody":["provider","body"],"getLogByName":["id","name"],"getClusters":[],"updateClusterProfile":["name","profile"],"getPackage":["provider","slug"],"getClusterById":["id"],"getGameVersions":[],"getUsers":[],"setDefaultUser":["uuid"],"openMsaLogin":[],"removeCluster":["id"],"getScreenshots":["id"],"removeUser":["uuid"],"searchPackages":["provider","query"],"getUsersFromAuthor":["provider","author"],"launchCluster":["id","uuid"],"getLogs":["id"],"updateClusterById":["id","request"],"getLoadersForVersion":["mc_version"],"getUser":["uuid"],"getProfileOrDefault":["name"],"fetchMinecraftProfile":["uuid"],"getWorlds":["id"],"writeSettings":["setting"],"getDefaultUser":["fallback"],"readSettings":[],"getGlobalProfile":[],"getMultiplePackages":["provider","slugs"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"createCluster":["options"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'events':'{"message":["event"],"ingress":["event"],"process":["event"]}', 'oneclient':'{"getClustersGroupedByMajor":[],"openDevTools":[]}' }
|
||||
export type Router = { 'folders': { fromCluster: (folderName: string) => Promise<string>,
|
||||
openCluster: (folderName: string) => Promise<null> },
|
||||
'oneclient': { openDevTools: () => Promise<void>,
|
||||
const ARGS_MAP = { 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'events':'{"message":["event"],"ingress":["event"],"process":["event"]}', 'oneclient':'{"openDevTools":[],"getClustersGroupedByMajor":[]}', 'core':'{"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getProfileOrDefault":["name"],"updateClusterById":["id","request"],"createCluster":["options"],"getLogByName":["id","name"],"getGameVersions":[],"getScreenshots":["id"],"writeSettings":["setting"],"getMultiplePackages":["provider","slugs"],"getLogs":["id"],"removeUser":["uuid"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getDefaultUser":["fallback"],"getLoadersForVersion":["mc_version"],"getClusters":[],"setDefaultUser":["uuid"],"getGlobalProfile":[],"getClusterById":["id"],"updateClusterProfile":["name","profile"],"getUsers":[],"getUsersFromAuthor":["provider","author"],"openMsaLogin":[],"launchCluster":["id","uuid"],"searchPackages":["provider","query"],"getWorlds":["id"],"getUser":["uuid"],"getPackageBody":["provider","body"],"readSettings":[],"removeCluster":["id"],"getPackage":["provider","slug"],"fetchMinecraftProfile":["uuid"]}' }
|
||||
export type Router = { 'oneclient': { openDevTools: () => Promise<void>,
|
||||
getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>> },
|
||||
'events': { ingress: (event: IngressPayload) => Promise<void>,
|
||||
message: (event: MessagePayload) => Promise<void>,
|
||||
process: (event: ProcessPayload) => Promise<void> },
|
||||
'folders': { fromCluster: (folderName: string) => Promise<string>,
|
||||
openCluster: (folderName: string) => Promise<null> },
|
||||
'core': { getClusters: () => Promise<ClusterModel[]>,
|
||||
getClusterById: (id: number) => Promise<ClusterModel | null>,
|
||||
removeCluster: (id: number) => Promise<null>,
|
||||
|
|
@ -268,7 +265,10 @@ getMultiplePackages: (provider: Provider, slugs: string[]) => Promise<ManagedPac
|
|||
getPackageVersions: (provider: Provider, slug: string, mcVersion: string | null, loader: GameLoader | null, offset: number, limit: number) => Promise<Paginated<ManagedVersion>>,
|
||||
downloadPackage: (provider: Provider, packageId: string, versionId: string, clusterId: number, skipCompatibility: boolean | null) => Promise<PackageModel>,
|
||||
getUsersFromAuthor: (provider: Provider, author: PackageAuthor) => Promise<ManagedUser[]>,
|
||||
fetchMinecraftProfile: (uuid: string) => Promise<MojangPlayerProfile> } };
|
||||
fetchMinecraftProfile: (uuid: string) => Promise<MojangPlayerProfile> },
|
||||
'events': { ingress: (event: IngressPayload) => Promise<void>,
|
||||
message: (event: MessagePayload) => Promise<void>,
|
||||
process: (event: ProcessPayload) => Promise<void> } };
|
||||
|
||||
|
||||
export type { InferCommandOutput }
|
||||
|
|
|
|||
33
apps/oneclient/frontend/src/hooks/useClusters.ts
Normal file
33
apps/oneclient/frontend/src/hooks/useClusters.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { bindings } from '@/main';
|
||||
import useAppShellStore from '@/stores/appShellStore';
|
||||
import { useCommandSuspense } from '@onelauncher/common';
|
||||
|
||||
export function useLastPlayedClusters() {
|
||||
return useCommandSuspense(
|
||||
['getClusters', 'sortedByLastPlayed'],
|
||||
bindings.core.getClusters,
|
||||
{
|
||||
select: data => data
|
||||
.sort((a, b) => {
|
||||
if (a.last_played && b.last_played)
|
||||
return new Date(b.last_played).getTime() - new Date(a.last_played).getTime();
|
||||
else if (a.last_played)
|
||||
return -1; // a has last_played, b does not
|
||||
else if (b.last_played)
|
||||
return 1; // b has last_played, a does not
|
||||
|
||||
const aVersion = a.mc_version.replaceAll('.', '');
|
||||
const bVersion = b.mc_version.replaceAll('.', '');
|
||||
return Number.parseInt(bVersion) - Number.parseInt(aVersion);
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function useActiveCluster() {
|
||||
const clusterId = useAppShellStore(state => state.activeClusterId);
|
||||
|
||||
const { data: clusters } = useLastPlayedClusters();
|
||||
|
||||
return clusters.find(c => c.id === clusterId) ?? clusters.at(0);
|
||||
}
|
||||
|
|
@ -1,16 +1,17 @@
|
|||
import type { GameLoader } from '@/bindings.gen';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import type { ClusterModel, GameLoader } from '@/bindings.gen';
|
||||
import type { ButtonProps } from 'react-aria-components';
|
||||
import { GameBackground } from '@/components';
|
||||
import { useActiveCluster, useLastPlayedClusters } from '@/hooks/useClusters';
|
||||
import { bindings } from '@/main';
|
||||
import useAppShellStore from '@/stores/appShellStore';
|
||||
import { prettifyLoader } from '@/utils/loaders';
|
||||
import { animations, transitions } from '@/utils/motion';
|
||||
import { getVersionInfo } from '@/utils/versionMap';
|
||||
import { useCommandSuspense } from '@onelauncher/common';
|
||||
import { getVersionInfo, getVersionInfoOrDefault } from '@/utils/versionMap';
|
||||
import { Button } from '@onelauncher/common/components';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { DotsGridIcon, PlusIcon, Settings04Icon } from '@untitled-theme/icons-react';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { DotsGridIcon, Settings04Icon } from '@untitled-theme/icons-react';
|
||||
import { motion } from 'motion/react';
|
||||
import { MouseParallax } from 'react-just-parallax';
|
||||
import { Button as AriaButton } from 'react-aria-components';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export const Route = createFileRoute('/app/')({
|
||||
|
|
@ -18,64 +19,35 @@ export const Route = createFileRoute('/app/')({
|
|||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { data: clusters } = useCommandSuspense(
|
||||
['getClusters'],
|
||||
bindings.core.getClusters,
|
||||
{
|
||||
select(data) {
|
||||
const sorted = data
|
||||
.filter(cluster => cluster.last_played !== null)
|
||||
.sort((a, b) => {
|
||||
if (a.last_played && b.last_played)
|
||||
return new Date(b.last_played).getTime() - new Date(a.last_played).getTime();
|
||||
else if (a.last_played)
|
||||
return -1; // a has last_played, b does not
|
||||
else if (b.last_played)
|
||||
return 1; // b has last_played, a does not
|
||||
const { data: lastPlayedClusters } = useLastPlayedClusters();
|
||||
|
||||
const aVersion = a.mc_version.replaceAll('.', '');
|
||||
const bVersion = b.mc_version.replaceAll('.', '');
|
||||
return Number.parseInt(bVersion) - Number.parseInt(aVersion);
|
||||
});
|
||||
const setActiveClusterId = useAppShellStore(state => state.setActiveClusterId);
|
||||
const activeCluster = useActiveCluster();
|
||||
|
||||
return [
|
||||
sorted[0],
|
||||
sorted[1],
|
||||
sorted[3],
|
||||
];
|
||||
},
|
||||
},
|
||||
);
|
||||
const navigate = useNavigate({
|
||||
from: Route.id,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col justify-center p-12">
|
||||
<motion.div {...animations.slideInLeft} className="flex flex-1 flex-col justify-center items-start gap-2" transition={{ ...transitions.spring, delay: 0.2 }}>
|
||||
<h1 className="text-6xl font-bold text-fg-primary">1.8.9</h1>
|
||||
<p className="text-lg font-medium text-fg-secondary">The Bountiful Update</p>
|
||||
|
||||
<div className="flex flex-row justify-center items-center gap-2">
|
||||
<Button size="large">Launch</Button>
|
||||
<Button color="ghost" size="iconLarge">
|
||||
<Settings04Icon />
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
<ActiveClusterInfo cluster={activeCluster} />
|
||||
|
||||
<motion.div {...animations.slideInUp} className="flex flex-row transition-[height] h-52 gap-6">
|
||||
{clusters.map((cluster) => {
|
||||
// eslint-disable-next-line ts/no-unnecessary-condition -- rule broken
|
||||
return cluster === undefined
|
||||
? (
|
||||
<Card blur className="group flex justify-center items-center" key={`undefined_${Math.random()}`}>
|
||||
<PlusIcon className="text-fg-secondary opacity-40 stroke-2 group-hover:text-fg-primary group-hover:opacity-90" height={64} width={64} />
|
||||
</Card>
|
||||
)
|
||||
: (
|
||||
<RecentsCard key={cluster.folder_name} loader={cluster.mc_loader} version={cluster.mc_version} />
|
||||
);
|
||||
})}
|
||||
{lastPlayedClusters.slice(0, 3).map(cluster => (
|
||||
<RecentsCard
|
||||
active={activeCluster?.id === cluster.id}
|
||||
key={cluster.folder_name}
|
||||
loader={cluster.mc_loader}
|
||||
onPress={() => setActiveClusterId(cluster.id)}
|
||||
version={cluster.mc_version}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Card blur className="flex flex-col justify-center items-center max-w-24">
|
||||
<Card
|
||||
blur
|
||||
className="flex flex-col justify-center items-center max-w-24"
|
||||
onPress={() => navigate({ to: '/app/clusters' })}
|
||||
>
|
||||
<DotsGridIcon height={48} width={48} />
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
|
@ -83,24 +55,81 @@ function RouteComponent() {
|
|||
);
|
||||
}
|
||||
|
||||
function ActiveClusterInfo({
|
||||
cluster,
|
||||
}: {
|
||||
cluster: ClusterModel | undefined;
|
||||
}) {
|
||||
const versionInfo = getVersionInfoOrDefault(cluster?.mc_version);
|
||||
const navigate = useNavigate({
|
||||
from: Route.id,
|
||||
});
|
||||
|
||||
const launch = () => {
|
||||
if (!cluster)
|
||||
return;
|
||||
|
||||
bindings.core.launchCluster(cluster.id, null);
|
||||
};
|
||||
|
||||
const viewCluster = () => {
|
||||
if (!cluster)
|
||||
return;
|
||||
|
||||
navigate({
|
||||
to: '/app/cluster/overview',
|
||||
search: {
|
||||
clusterId: cluster.id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
{...animations.slideInLeft}
|
||||
className="flex flex-1 flex-col justify-center items-start gap-2"
|
||||
key={(cluster?.mc_version ?? Math.random()) + (cluster?.mc_loader ?? '')}
|
||||
transition={{ ...transitions.spring, delay: 0.2 }}
|
||||
>
|
||||
<h1 className="text-6xl font-bold text-fg-primary">{cluster?.mc_version} {prettifyLoader(cluster?.mc_loader ?? 'vanilla')}</h1>
|
||||
<p className="text-lg font-medium text-fg-secondary">{versionInfo.shortDescription}</p>
|
||||
|
||||
<div className="flex flex-row justify-center items-center gap-2">
|
||||
<Button onPress={launch} size="large">Launch</Button>
|
||||
<Button color="ghost" onPress={viewCluster} size="iconLarge">
|
||||
<Settings04Icon />
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
interface RecentsCardProps {
|
||||
version: string;
|
||||
loader: GameLoader;
|
||||
onPress: () => void;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
const BLUR = '30px';
|
||||
function RecentsCard({ version, loader }: RecentsCardProps) {
|
||||
function RecentsCard({
|
||||
version,
|
||||
loader,
|
||||
onPress,
|
||||
active,
|
||||
}: RecentsCardProps) {
|
||||
const versionInfo = getVersionInfo(version);
|
||||
|
||||
if (!versionInfo)
|
||||
return <Card blur>Unknown Version</Card>;
|
||||
return (
|
||||
<Card blur>
|
||||
<p className="text-lg font-medium text-fg-secondary">Unknown Version</p>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex w-full h-full justify-start items-end px-6 py-3">
|
||||
<MouseParallax isAbsolutelyPositioned strength={0.005} zIndex={-10}>
|
||||
<GameBackground className="absolute left-0 top-0 w-full h-full scale-110" name={versionInfo.backgroundName} />
|
||||
</MouseParallax>
|
||||
<Card className={twMerge(active && 'outline-2 outline-brand')} onPress={onPress}>
|
||||
<div className="flex w-full h-full justify-start items-end px-6 py-3 hover:brightness-80">
|
||||
<GameBackground className="absolute -z-10 left-0 top-0 w-full h-full scale-110" name={versionInfo.backgroundName} />
|
||||
|
||||
<div
|
||||
className="absolute top-0 left-0 -z-10 w-full h-full"
|
||||
|
|
@ -110,30 +139,33 @@ function RecentsCard({ version, loader }: RecentsCardProps) {
|
|||
>
|
||||
</div>
|
||||
|
||||
<h4 className="text-3xl font-semibold">{version} {prettifyLoader(loader)}</h4>
|
||||
<h4 className="text-2xl font-semibold">{version} {prettifyLoader(loader)}</h4>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const BLUR = '30px';
|
||||
function Card({
|
||||
blur = true,
|
||||
children,
|
||||
className,
|
||||
style,
|
||||
onPress,
|
||||
}: {
|
||||
blur?: boolean;
|
||||
children?: React.ReactNode;
|
||||
} & HTMLAttributes<HTMLDivElement>) {
|
||||
className?: string | undefined;
|
||||
} & ButtonProps & React.RefAttributes<HTMLButtonElement>) {
|
||||
return (
|
||||
<div
|
||||
<AriaButton
|
||||
className={twMerge(
|
||||
'relative overflow-hidden flex-1 rounded-xl inset-ring-2 inset-ring-component-border',
|
||||
'relative overflow-hidden flex-1 rounded-xl outline outline-component-border',
|
||||
blur
|
||||
? 'bg-white/5 hover:bg-white/15 active:bg-white/20'
|
||||
: 'hover:bg-ghost-overlay-hover active:bg-ghost-overlay-pressed',
|
||||
className,
|
||||
)}
|
||||
onPress={onPress}
|
||||
style={blur
|
||||
? {
|
||||
// shitty hack because webkit breaks with css variables in its backdrop filter
|
||||
|
|
@ -144,6 +176,6 @@ function Card({
|
|||
: style}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</AriaButton>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import type { PropsWithChildren } from 'react';
|
||||
import { LoaderSuspense, Navbar } from '@/components';
|
||||
import { GameBackground } from '@/components/GameBackground';
|
||||
import useAppShellStore from '@/stores/appShellStore';
|
||||
import { useActiveCluster } from '@/hooks/useClusters';
|
||||
import { getVersionInfoOrDefault } from '@/utils/versionMap';
|
||||
import { createFileRoute, Outlet } from '@tanstack/react-router';
|
||||
import { motion } from 'motion/react';
|
||||
import { MouseParallax } from 'react-just-parallax';
|
||||
|
||||
export const Route = createFileRoute('/app')({
|
||||
|
|
@ -50,10 +52,8 @@ function AppShell({
|
|||
}
|
||||
|
||||
function BackgroundGradient() {
|
||||
const background = useAppShellStore(state => state.background);
|
||||
|
||||
if (background === 'none')
|
||||
return undefined;
|
||||
const cluster = useActiveCluster();
|
||||
const versionInfo = getVersionInfoOrDefault(cluster?.mc_version);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
|
|
@ -83,10 +83,23 @@ function BackgroundGradient() {
|
|||
</div>
|
||||
|
||||
<MouseParallax isAbsolutelyPositioned strength={0.01} zIndex={-50}>
|
||||
<GameBackground
|
||||
className="absolute left-0 top-0 w-screen h-screen scale-110"
|
||||
name="HypixelSkyblockHub"
|
||||
/>
|
||||
<motion.div
|
||||
animate={{
|
||||
opacity: 1,
|
||||
left: '0',
|
||||
}}
|
||||
className="relative scale-105"
|
||||
initial={{
|
||||
opacity: 0,
|
||||
left: '-10%',
|
||||
}}
|
||||
key={(cluster?.mc_version ?? Math.random()) + (cluster?.mc_loader ?? '')}
|
||||
>
|
||||
<GameBackground
|
||||
className="absolute left-0 top-0 w-screen h-screen scale-110"
|
||||
name={versionInfo.backgroundName}
|
||||
/>
|
||||
</motion.div>
|
||||
</MouseParallax>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import { create } from 'zustand';
|
||||
|
||||
export interface AppShellStore {
|
||||
background: 'gradientOverlay' | 'none';
|
||||
activeClusterId: number;
|
||||
}
|
||||
|
||||
export interface AppShellStoreActions {
|
||||
setBackground: (background: AppShellStore['background']) => void;
|
||||
setActiveClusterId: (cluster: AppShellStore['activeClusterId']) => void;
|
||||
}
|
||||
|
||||
export const useAppShellStore = create<AppShellStore & AppShellStoreActions>()(set => ({
|
||||
background: 'gradientOverlay',
|
||||
activeClusterId: 0,
|
||||
|
||||
setBackground: background => set({ background }),
|
||||
setActiveClusterId: cluster => set({ activeClusterId: cluster }),
|
||||
}));
|
||||
|
||||
export default useAppShellStore;
|
||||
|
|
|
|||
|
|
@ -41,4 +41,13 @@ export const animations = {
|
|||
bottom: '0',
|
||||
},
|
||||
},
|
||||
|
||||
fadeIn: {
|
||||
initial: {
|
||||
opacity: 0,
|
||||
},
|
||||
animate: {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
} as const satisfies Record<string, MotionProps>;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue