Merge branch 'oneclient/main' of https://github.com/Polyfrost/OneLauncher into refactor/frontend

This commit is contained in:
emirsassan 2025-06-28 09:41:03 +03:00
commit f025673d9d
12 changed files with 312 additions and 126 deletions

View file

@ -13,7 +13,7 @@ export function Navbar() {
const onClose = () => Window.getCurrent().close(); const onClose = () => Window.getCurrent().close();
return ( return (
<nav className="flex flex-row items-center justify-between h-20" data-tauri-drag-region="true"> <nav className="flex flex-row items-center justify-between h-20 px-12 z-50" data-tauri-drag-region="true">
<div className="flex flex-1 pointer-events-none"> <div className="flex flex-1 pointer-events-none">
<LauncherLogo height={47} width={230} /> <LauncherLogo height={47} width={230} />
</div> </div>

View file

@ -0,0 +1,131 @@
import type { MotionValue } from 'motion/react';
import useAppShellStore from '@/stores/appShellStore';
import { getLocationName } from '@/utils/locationMapping';
import { Button } from '@onelauncher/common/components';
import { Outlet, useNavigate } from '@tanstack/react-router';
import { ArrowLeftIcon } from '@untitled-theme/icons-react';
import { motion, useScroll, useTransform } from 'motion/react';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import React, { useRef } from 'react';
export function SheetPage({
headerSmall,
headerLarge,
}: {
headerSmall: React.ReactNode;
headerLarge: React.ReactNode;
}) {
const scrollContainer = useRef<HTMLElement>(null);
const headerLargeRef = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
axis: 'y',
container: scrollContainer,
target: headerLargeRef,
layoutEffect: false,
offset: ['end start', 'start start'],
});
const smallTop = useTransform(scrollYProgress, [0, 1], ['0', '100%']);
const smallOpacity = useTransform(scrollYProgress, [0, 1], ['1', '0']);
const largeOpacity = useTransform(scrollYProgress, [0, 1], ['0', '1']);
return (
<div className="h-full">
<HeaderBackgrounds scrollYProgress={scrollYProgress} />
<div className="flex flex-col w-full min-h-full h-0">
<div className="sticky top-0 left-0 mx-12 flex flex-row gap-4 h-8 overflow-hidden">
<GoBackButton />
<motion.div
className="relative w-full h-full left-0" style={{
top: smallTop,
opacity: smallOpacity,
}}
>
{headerSmall}
</motion.div>
</div>
<OverlayScrollbarsComponent className="flex flex-col flex-1 mask-t-from-97% pt-4 relative" ref={ref => scrollContainer.current = ref?.osInstance()?.elements().content ?? null}>
<div className="flex flex-col mx-12 gap-4">
<motion.div className="w-full" ref={headerLargeRef} style={{ opacity: largeOpacity }}>
{/* <headerLarge ref={headerLargeRef as React.RefObject<HTMLDivElement>} /> */}
{headerLarge}
</motion.div>
<motion.div
animate={{ bottom: 0 }}
className="relative flex flex-col px-10 py-6 mb-8 bg-page-elevated rounded-2xl"
initial={{ bottom: '-80px' }}
>
<Outlet />
</motion.div>
</div>
</OverlayScrollbarsComponent>
</div>
</div>
);
}
function GoBackButton() {
const prevLocation = useAppShellStore(state => state.prevLocation);
const navigate = useNavigate();
const onClick = () => {
if (!prevLocation)
return;
navigate({
to: prevLocation.pathname,
search: prevLocation.search,
hash: prevLocation.hash,
state: prevLocation.state,
});
};
if (!prevLocation)
return undefined;
return (
<Button color="ghost" onClick={onClick}>
<ArrowLeftIcon height={20} />
<span className="h-5">{getLocationName(prevLocation.pathname)}</span>
</Button>
);
}
function HeaderBackgrounds({
scrollYProgress,
}: {
scrollYProgress: MotionValue<number>;
}) {
const top = useTransform(scrollYProgress, [1, 0], ['0px', '-60px']);
return (
<motion.div
className="absolute left-0 h-full w-full"
style={{
top,
}}
>
<div
className="absolute -z-10 -top-20 h-50 left-0 w-screen animate-fade animate-duration-200" style={{
background: 'rgba(0, 0, 0, 0.20)',
}}
>
</div>
<div
className="relative h-50 top-0 left-0 w-screen animate-fade animate-duration-200"
style={{
background: 'linear-gradient(180deg, rgba(17, 23, 28, 0.00) 0%, #11171C 60.1%)',
}}
>
</div>
<div className="relative top-0 left-0 w-full h-full bg-page animate-fade animate-duration-200"></div>
</motion.div>
);
}

View file

@ -5,7 +5,7 @@ import { createRootRouteWithContext, Outlet, useRouter } from '@tanstack/react-r
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'; import { TanStackRouterDevtools } from '@tanstack/react-router-devtools';
import { RouterProvider } from 'react-aria-components'; import { RouterProvider } from 'react-aria-components';
interface MyRouterContext { interface AppRouterContext {
queryClient: QueryClient; queryClient: QueryClient;
} }
@ -16,7 +16,7 @@ declare module 'react-aria-components' {
} }
} }
export const Route = createRootRouteWithContext<MyRouterContext>()({ export const Route = createRootRouteWithContext<AppRouterContext>()({
component: RootRoute, component: RootRoute,
}); });
@ -28,7 +28,7 @@ function RootRoute() {
navigate={(to, options) => router.navigate({ to, ...options })} navigate={(to, options) => router.navigate({ to, ...options })}
useHref={to => router.buildLocation({ to }).href} useHref={to => router.buildLocation({ to }).href}
> >
<div className="h-screen flex flex-col overflow-hidden text-fg-primary px-12"> <div className="h-screen flex flex-col overflow-hidden text-fg-primary">
<Outlet /> <Outlet />
<TanStackRouterDevtools /> <TanStackRouterDevtools />

View file

@ -5,5 +5,13 @@ export const Route = createFileRoute('/app/clusters/')({
}); });
function RouteComponent() { function RouteComponent() {
return <div>Hello "/app/clusters/"!</div>; return (
<div>
<div>Hello "/app/clusters/"!</div>
<div className="w-px h-screen bg-red-100/10"></div>
<div>Hello "/app/clusters/"!</div>
</div>
);
} }

View file

@ -1,4 +1,5 @@
import { AnimatedOutlet, AnimatedOutletProvider } from '@onelauncher/common/components'; import { SheetPage } from '@/components/SheetPage';
import { Button } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/app/clusters')({ export const Route = createFileRoute('/app/clusters')({
@ -7,51 +8,38 @@ export const Route = createFileRoute('/app/clusters')({
function RouteComponent() { function RouteComponent() {
return ( return (
<div className="flex w-full h-full"> <SheetPage
<BackgroundGradients /> headerLarge={<HeaderLarge />}
headerSmall={<HeaderSmall />}
<AnimatedOutletProvider> />
<AnimatedOutlet
enter={{
initial: {
bottom: '-100%',
opacity: 0,
},
animate: {
bottom: 0,
opacity: 1,
},
}}
exit={{
initial: {
bottom: 0,
opacity: 1,
},
animate: {
bottom: '-100%',
opacity: 0,
},
}}
from={Route.id}
/>
</AnimatedOutletProvider>
</div>
); );
} }
function BackgroundGradients() { function HeaderLarge({ ref }: { ref?: React.Ref<HTMLDivElement> }) {
return ( return (
<div className="relative"> <div className="flex flex-row justify-between items-end" ref={ref}>
<div style={{ <div className="flex flex-col">
background: 'rgba(0, 0, 0, 0.80)', <h1 className="text-3xl font-semibold">Header</h1>
}} <p className="text-md font-medium text-fg-secondary">Tailored for Hypixel Skyblock.</p>
>
</div>
<div style={{
background: 'linear-gradient(180deg, rgba(17, 23, 28, 0.00) 0%, #11171C 60.1%)',
}}
>
</div> </div>
<Button color="primary" size="large">
Launch
</Button>
</div>
);
}
function HeaderSmall() {
return (
<div className="flex flex-row w-full items-center justify-between">
<div className="flex flex-row items-center gap-4">
<h1 className="text-2lg h-full font-medium">Header</h1>
</div>
<Button color="primary">
Launch
</Button>
</div> </div>
); );
} }

View file

@ -15,46 +15,36 @@ export const Route = createFileRoute('/app/')({
component: RouteComponent, component: RouteComponent,
}); });
const clusters = [
{
mc_version: '1.8.9',
},
undefined,
{
mc_version: '1.21.4',
},
];
function RouteComponent() { function RouteComponent() {
// const { data: clusters } = useCommandSuspense<Array<ClusterModel | undefined>>( const { data: clusters } = useCommandSuspense<Array<ClusterModel | undefined>>(
// 'getClusters', 'getClusters',
// bindings.core.getClusters, bindings.core.getClusters,
// { {
// select: ((data: Array<ClusterModel>) => { select: ((data: Array<ClusterModel>) => {
// const sorted = data.sort((a, b) => { const sorted = data.sort((a, b) => {
// if (a.last_played && b.last_played) if (a.last_played && b.last_played)
// return new Date(b.last_played).getTime() - new Date(a.last_played).getTime(); return new Date(b.last_played).getTime() - new Date(a.last_played).getTime();
// else if (a.last_played) else if (a.last_played)
// return -1; // a has last_played, b does not return -1; // a has last_played, b does not
// else if (b.last_played) else if (b.last_played)
// return 1; // b has last_played, a does not return 1; // b has last_played, a does not
// const aVersion = a.mc_version.replaceAll('.', ''); const aVersion = a.mc_version.replaceAll('.', '');
// const bVersion = b.mc_version.replaceAll('.', ''); const bVersion = b.mc_version.replaceAll('.', '');
// return Number.parseInt(bVersion) - Number.parseInt(aVersion); return Number.parseInt(bVersion) - Number.parseInt(aVersion);
// }); });
// return [ return [
// sorted[0], sorted[0],
// sorted[1], sorted[1],
// undefined, undefined,
// ]; ];
// }) as (data: Array<ClusterModel | undefined>) => Array<ClusterModel | undefined>, }) as (data: Array<ClusterModel | undefined>) => Array<ClusterModel | undefined>,
// }, },
// ); );
return ( return (
<div className="flex h-full w-full flex-col justify-center"> <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 }}> <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> <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> <p className="text-lg font-medium text-fg-secondary">The Bountiful Update</p>
@ -65,9 +55,9 @@ function RouteComponent() {
</div> </div>
</motion.div> </motion.div>
<motion.div {...animations.slideInUp} className="flex flex-row transition-[height] max-h-84 h-[28vh] max-xl:h-52 gap-6"> <motion.div {...animations.slideInUp} className="flex flex-row transition-[height] h-52 gap-6">
{clusters.map(cluster => ( {clusters.map((cluster, index) => (
<RecentsCard key={cluster?.mc_version} version={cluster?.mc_version} /> <RecentsCard key={cluster?.folder_name ?? index} 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">

View file

@ -1,7 +1,10 @@
import { useEffect, type PropsWithChildren } from 'react';
import { LoaderSuspense, Navbar } from '@/components'; import { LoaderSuspense, Navbar } from '@/components';
import { GameBackground } from '@/components/GameBackground'; import { GameBackground } from '@/components/GameBackground';
import useAppShellStore from '@/stores/appShellStore';
import { AnimatedOutlet } from '@onelauncher/common/components'; import { AnimatedOutlet } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router'; import { createFileRoute, useRouter } from '@tanstack/react-router';
import { AnimatePresence } from 'motion/react';
export const Route = createFileRoute('/app')({ export const Route = createFileRoute('/app')({
component: RouteComponent, component: RouteComponent,
@ -10,12 +13,8 @@ export const Route = createFileRoute('/app')({
function RouteComponent() { function RouteComponent() {
return ( return (
<LoaderSuspense spinner={{ size: 'large' }}> <LoaderSuspense spinner={{ size: 'large' }}>
<div className="flex flex-col h-full w-full"> <AppShell>
<Navbar /> <div className="h-full w-full">
<Background />
<div className="h-full w-full pb-12">
<AnimatedOutlet <AnimatedOutlet
enter={{ enter={{
initial: { opacity: 0 }, initial: { opacity: 0 },
@ -29,15 +28,49 @@ function RouteComponent() {
transition={{ duration: 0.3, bounce: 0.1, power: 0.2, type: 'spring' }} transition={{ duration: 0.3, bounce: 0.1, power: 0.2, type: 'spring' }}
/> />
</div> </div>
</div> </AppShell>
</LoaderSuspense> </LoaderSuspense>
); );
} }
function Background() { function AppShell({
children,
}: PropsWithChildren) {
const router = useRouter();
const setPrevLocation = useAppShellStore(state => state.setPrevLocation);
useEffect(() => {
const unsub = router.subscribe("onBeforeNavigate", (e) => {
setPrevLocation(e.fromLocation ?? null);
});
return () => unsub();
}, [router]);
return (
<div className="flex flex-col h-full w-full">
<AnimatePresence>
<BackgroundGradient />
</AnimatePresence>
<Navbar />
<div className="flex flex-col w-full h-full">
{children}
</div>
</div>
);
}
function BackgroundGradient() {
const background = useAppShellStore(state => state.background);
if (background === 'none')
return undefined;
return ( return (
<div> <div>
{/* Linear gradient: left -> right */} {/* Linear black gradient: left -> right */}
<div <div
className="absolute top-0 left-0 w-screen h-screen -z-10" className="absolute top-0 left-0 w-screen h-screen -z-10"
style={{ style={{
@ -46,7 +79,7 @@ function Background() {
> >
</div> </div>
{/* Radial Gradient */} {/* Radial black gradient */}
<div <div
className="absolute top-0 left-0 w-screen h-screen -z-10" style={{ className="absolute top-0 left-0 w-screen h-screen -z-10" style={{
background: 'radial-gradient(48.29% 48.29% at 77.29% 50%, rgba(0, 0, 0, 0.00) 0%, rgba(0, 0, 0, 0.64) 100%)', background: 'radial-gradient(48.29% 48.29% at 77.29% 50%, rgba(0, 0, 0, 0.00) 0%, rgba(0, 0, 0, 0.64) 100%)',
@ -54,7 +87,7 @@ function Background() {
> >
</div> </div>
{/* Linear gradient: bottom -> 200 px up */} {/* Linear black gradient: bottom -> 200 px up */}
<div <div
className="absolute bottom-0 left-0 w-screen h-50 -z-10" style={{ className="absolute bottom-0 left-0 w-screen h-50 -z-10" style={{
background: 'linear-gradient(180deg, rgba(17, 23, 28, 0.00) 0%, rgba(0, 0, 0, 0.68) 60%)', background: 'linear-gradient(180deg, rgba(17, 23, 28, 0.00) 0%, rgba(0, 0, 0, 0.68) 60%)',

View file

@ -0,0 +1,22 @@
import type { ParsedLocation } from '@tanstack/react-router';
import { create } from 'zustand';
export interface AppShellStore {
background: 'gradientOverlay' | 'none';
prevLocation: ParsedLocation | null;
}
export interface AppShellStoreActions {
setBackground: (background: AppShellStore['background']) => void;
setPrevLocation: (location: ParsedLocation | null) => void;
}
export const useAppShellStore = create<AppShellStore & AppShellStoreActions>()(set => ({
background: 'gradientOverlay',
prevLocation: null,
setBackground: background => set({ background }),
setPrevLocation: location => set({ prevLocation: location }),
}));
export default useAppShellStore;

View file

@ -1,28 +0,0 @@
import type { ClusterModel } from '@/bindings.gen';
import type { FileRoutesByFullPath } from '@/routeTree.gen';
import { create } from 'zustand';
interface AppStore {
activeCluster: ClusterModel | null;
previousPage: FileRoutesByFullPath | null;
}
interface AppStoreActions {
setInitialData: (data: AppStore) => void;
setActiveCluster: (cluster: ClusterModel | null) => void;
setPreviousPage: (page: FileRoutesByFullPath) => void;
}
const useAppStore = create<AppStore & AppStoreActions>()(set => ({
activeCluster: null,
previousPage: null,
setInitialData: (data: AppStore) => set({
activeCluster: data.activeCluster,
previousPage: data.previousPage,
}),
setPreviousPage: (page: FileRoutesByFullPath) => set({ previousPage: page }),
setActiveCluster: (cluster: ClusterModel | null) => set({ activeCluster: cluster }),
}));
export default useAppStore;

View file

@ -0,0 +1,39 @@
import type { FileRouteTypes } from '@/routeTree.gen';
const mapping: Partial<Record<FileRouteTypes['to'], string>> = {
'/app': 'Home',
'/app/clusters': 'Clusters',
};
export function getLocationName(
to: FileRouteTypes['to'] | (string & {}) | undefined | null,
): string | undefined {
if (!to)
return undefined;
if (to in mapping)
return mapping[to as keyof typeof mapping];
const mostSimilarPath = getMostSimilarPathMapping(to);
return mostSimilarPath;
}
function getMostSimilarPathMapping(
to: string,
): FileRouteTypes['to'] | undefined {
let path = to;
while (path !== '') {
if (Object.prototype.hasOwnProperty.call(mapping, path))
return path as FileRouteTypes['to'];
const lastSlash = path.lastIndexOf('/');
if (lastSlash === 0)
return Object.prototype.hasOwnProperty.call(mapping, '/') ? '/' : undefined;
path = path.substring(0, lastSlash);
}
return undefined;
}

View file

@ -31,6 +31,7 @@ export default defineConfig(
'ts/no-use-before-define': 'off', 'ts/no-use-before-define': 'off',
'style/function-paren-newline': ['error', 'consistent'], 'style/function-paren-newline': ['error', 'consistent'],
'react/no-context-provider': 'off', 'react/no-context-provider': 'off',
"prefer-const": "off" // disabled due to maximum call stack size
}, },
}, },
); );

View file

@ -7,7 +7,7 @@
import type { AnyRoute } from '@tanstack/react-router'; import type { AnyRoute } from '@tanstack/react-router';
import type { MotionProps } from 'motion/react'; import type { MotionProps } from 'motion/react';
import { Outlet, useMatch, useRouter } from '@tanstack/react-router'; import { Outlet, useMatch, useRouter } from '@tanstack/react-router';
import { AnimatePresence, motion } from 'motion/react'; import { motion } from 'motion/react';
import { createContext, useContext, useEffect, useRef, useState } from 'react'; import { createContext, useContext, useEffect, useRef, useState } from 'react';
interface TransitionProps { interface TransitionProps {
@ -21,6 +21,7 @@ interface AnimatedOutletProps {
transition?: MotionProps['transition']; transition?: MotionProps['transition'];
from: AnyRoute['id']; from: AnyRoute['id'];
clone?: boolean; clone?: boolean;
children?: (children: React.ReactNode) => React.ReactNode;
} }
interface AnimatedOutletWrapperProps { interface AnimatedOutletWrapperProps {
@ -82,6 +83,7 @@ export function AnimatedOutlet({
transition = { duration: 0.3 }, transition = { duration: 0.3 },
from, from,
clone = true, clone = true,
children,
}: AnimatedOutletProps) { }: AnimatedOutletProps) {
const [snapshots, setSnapshots] = useState<Array<{ node: HTMLElement; id: number }>>([]); const [snapshots, setSnapshots] = useState<Array<{ node: HTMLElement; id: number }>>([]);
const [pathname, setPathname] = useState<string | null>(null); const [pathname, setPathname] = useState<string | null>(null);
@ -148,7 +150,7 @@ export function AnimatedOutlet({
transition={transition} transition={transition}
> >
<div className="w-full h-full"> <div className="w-full h-full">
<Outlet /> {children?.(<Outlet />) ?? <Outlet />}
</div> </div>
</motion.div> </motion.div>
</div> </div>