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

This commit is contained in:
emirsassan 2025-06-18 09:39:39 +03:00
commit 816be606ff
29 changed files with 570 additions and 70 deletions

View file

@ -20,6 +20,12 @@ pub struct SingleInstancePayload {
#[tracing::instrument]
async fn initialize_core() -> LauncherResult<()> {
let level = if cfg!(debug_assertions) {
tracing::Level::DEBUG
} else {
tracing::Level::INFO
};
let opts = CoreOptions {
curseforge_api_key: Some(constants::CURSEFORGE_API_KEY.to_string()),
launcher_name: "OneClient".to_string(),
@ -27,6 +33,9 @@ async fn initialize_core() -> LauncherResult<()> {
launcher_website: "https://polyfrost.org/".to_string(),
discord_client_id: None, //Some(constants::DISCORD_CLIENT_ID.to_string()), // disabled for now
fetch_attempts: 3,
logger_filter: Some(
format!("{}={level},onelauncher_core={level}", env!("CARGO_PKG_NAME")),
),
..Default::default()
};

View file

@ -1,3 +1,4 @@
import type { CSSProperties } from 'react';
import { twMerge } from 'tailwind-merge';
import HypixelSkyblockHub from '../assets/backgrounds/HypixelSkyblockHub.png';
@ -5,20 +6,21 @@ const GameBackgrounds = {
HypixelSkyblockHub,
};
type GameBackgroundName = keyof typeof GameBackgrounds;
export type GameBackgroundName = keyof typeof GameBackgrounds;
interface GameBackgroundProps {
name: GameBackgroundName;
className?: string;
style?: CSSProperties;
}
export function GameBackground({ name, className = '' }: GameBackgroundProps) {
export function GameBackground({ name, className = '', style = undefined }: GameBackgroundProps) {
const BackgroundImage = GameBackgrounds[name];
return (
<div
className={twMerge(`absolute -z-50 pointer-events-none inset-0 bg-cover bg-center`, className)}
style={{ backgroundImage: `url(${BackgroundImage})` }}
style={{ backgroundImage: `url(${BackgroundImage})`, ...style }}
/>
);
}

View file

@ -0,0 +1,68 @@
import type { HTMLAttributes, ReactNode } from 'react';
import type { SpinnerProps } from './Spinner';
import { motion } from 'motion/react';
import { Suspense } from 'react';
import { twMerge } from 'tailwind-merge';
import { Spinner } from './Spinner';
interface LoaderProps extends HTMLAttributes<HTMLDivElement> {
children?: ReactNode;
spinner?: SpinnerProps;
}
export function LoaderContainer({
loading = false,
children,
spinner,
className,
}: LoaderProps & {
loading?: boolean;
}) {
return loading ? <Fallback className={className} spinner={spinner} /> : <LoadedChildren className={className}>{children}</LoadedChildren>;
}
export function LoaderSuspense({
children,
spinner,
className,
}: LoaderProps) {
return (
<Suspense fallback={<Fallback className={className} spinner={spinner} />}>
<LoadedChildren className={className}>
{children}
</LoadedChildren>
</Suspense>
);
}
function Fallback({
spinner,
className,
}: {
spinner?: SpinnerProps;
className?: string;
}) {
return (
<div className={twMerge('w-full h-full flex justify-center items-center', className)}>
<Spinner {...spinner} />
</div>
);
}
function LoadedChildren({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return (
<motion.div
animate={{ opacity: 1 }}
className={twMerge('w-full h-full', className)}
initial={{ opacity: 0 }}
>
{children}
</motion.div>
);
}

View file

@ -20,11 +20,11 @@ export function Navbar() {
<div className="flex flex-1 items-center justify-center pointer-events-none gap-6">
<NavbarLink to="/app">Home</NavbarLink>
<NavbarLink to="/app/test">Clusters</NavbarLink>
<NavbarLink to="/app/clusters">Clusters</NavbarLink>
<NavbarLink to="/">Accounts</NavbarLink>
</div>
<div className="flex flex-1 items-center justify-end gap-2">
<div className="flex flex-1 items-center justify-end gap-2 pointer-events-none">
<NavbarButton
children={<MinusIcon />}
onClick={onMinimize}

View file

@ -0,0 +1,32 @@
import type { HTMLAttributes } from 'react';
import type { VariantProps } from 'tailwind-variants';
import { tv } from 'tailwind-variants';
const spinnerVariants = tv({
base: 'inline-block relative opacity-0 animate-delay-1000 animate-fade animate-fill-forwards',
variants: {
size: {
small: 'w-8 h-8',
medium: 'w-12 h-12',
large: 'w-16 h-16',
},
},
});
type SpinnerVariantsProps = VariantProps<typeof spinnerVariants>;
export interface SpinnerProps extends HTMLAttributes<HTMLDivElement> {
size?: SpinnerVariantsProps['size'];
};
export function Spinner({
size = 'medium',
className,
}: SpinnerProps) {
return (
<div className={spinnerVariants({ size, className })}>
<div className="absolute w-full h-full rounded-full border-2 border-fg-primary animate-fade-scale"></div>
<div className="absolute w-full h-full rounded-full border-2 border-fg-primary animate-fade-scale animate-delay-500"></div>
</div>
);
}

View file

@ -1,2 +1,4 @@
export * from './GameBackground';
export * from './Loader';
export * from './Navbar';
export * from './Spinner';

View file

@ -1,6 +1,5 @@
import type { QueryClient } from '@tanstack/react-query';
import type { NavigateOptions, ToOptions } from '@tanstack/react-router';
import { Navbar } from '@/components';
import { AnimatedOutletProvider } from '@onelauncher/common/components';
import { createRootRouteWithContext, Outlet, useRouter } from '@tanstack/react-router';
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools';
@ -30,8 +29,6 @@ function RootRoute() {
useHref={to => router.buildLocation({ to }).href}
>
<div className="h-screen flex flex-col overflow-hidden text-fg-primary px-12">
<Navbar />
<Outlet />
<TanStackRouterDevtools />

View file

@ -1,9 +1,9 @@
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/app/test')({
export const Route = createFileRoute('/app/clusters/')({
component: RouteComponent,
});
function RouteComponent() {
return <div>Hello "/app/test"!</div>;
return <div>Hello "/app/clusters/"!</div>;
}

View file

@ -0,0 +1,57 @@
import { AnimatedOutlet, AnimatedOutletProvider } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/app/clusters')({
component: RouteComponent,
});
function RouteComponent() {
return (
<div className="flex w-full h-full">
<BackgroundGradients />
<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() {
return (
<div className="relative">
<div style={{
background: 'rgba(0, 0, 0, 0.80)',
}}
>
</div>
<div style={{
background: 'linear-gradient(180deg, rgba(17, 23, 28, 0.00) 0%, #11171C 60.1%)',
}}
>
</div>
</div>
);
}

View file

@ -1,21 +1,140 @@
import type { ClusterModel } from '@/bindings.gen';
import type { HTMLAttributes } from 'react';
import { GameBackground } from '@/components';
import { bindings } from '@/main';
import { animations, transitions } from '@/utils/motion';
import { getVersionInfo } from '@/utils/versionMap';
import { useCommandSuspense } from '@onelauncher/common';
import { Button } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router';
import { DotsGridIcon, Settings04Icon } from '@untitled-theme/icons-react';
import { motion } from 'motion/react';
import { twMerge } from 'tailwind-merge';
export const Route = createFileRoute('/app/')({
component: RouteComponent,
});
const clusters = [
{
mc_version: '1.8.9',
},
undefined,
{
mc_version: '1.21.4',
},
];
function RouteComponent() {
// const { data: clusters } = useCommandSuspense<Array<ClusterModel | undefined>>(
// 'getClusters',
// bindings.core.getClusters,
// {
// select: ((data: Array<ClusterModel>) => {
// const sorted = 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);
// });
// return [
// sorted[0],
// sorted[1],
// undefined,
// ];
// }) as (data: Array<ClusterModel | undefined>) => Array<ClusterModel | undefined>,
// },
// );
return (
<div className="flex h-full w-full flex-col justify-center">
<div className="flex flex-1 flex-col justify-center">
<h1 className="text-5xl font-semibold text-fg-primary">1.8.9</h1>
</div>
<div className="flex flex-row h-56">
<div className="flex-1 rounded-xl border border-solid border-component-border bg-page-overlay">
<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>
</div>
</motion.div>
<motion.div {...animations.slideInUp} className="flex flex-row transition-[height] max-h-84 h-[28vh] max-xl:h-52 gap-6">
{clusters.map(cluster => (
<RecentsCard key={cluster?.mc_version} version={cluster?.mc_version} />
))}
<Card blur className="flex flex-col justify-center items-center max-w-24">
<DotsGridIcon height={48} width={48} />
</Card>
</motion.div>
</div>
);
}
interface RecentsCardProps {
version: string | undefined;
}
const BLUR = '30px';
function RecentsCard({ version }: RecentsCardProps) {
const versionInfo = getVersionInfo(version);
return (
<Card blur={versionInfo === undefined}>
{versionInfo && (
<div className="flex w-full h-full justify-start items-end px-6 py-3">
<GameBackground className="-z-10" name={versionInfo.backgroundName} />
<div
className="absolute top-0 left-0 -z-10 w-full h-full"
style={{
background: 'linear-gradient(180deg, rgba(25, 25, 25, 0.00) 24.52%, rgba(17, 17, 21, 0.75) 65%)',
}}
>
</div>
<h4 className="text-3xl font-semibold">{version}</h4>
</div>
)}
</Card>
);
}
function Card({
blur = true,
children,
className,
style,
}: {
blur?: boolean;
children?: React.ReactNode;
} & HTMLAttributes<HTMLDivElement>) {
return (
<div
className={twMerge(
'relative overflow-hidden flex-1 rounded-xl inset-ring-2 inset-ring-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,
)}
style={blur
? {
// shitty hack because webkit breaks with css variables in its backdrop filter
backdropFilter: `blur(${BLUR})`,
WebkitBackdropFilter: `blur(${BLUR})`,
...style,
}
: style}
>
{children}
</div>
);
}

View file

@ -1,3 +1,4 @@
import { LoaderSuspense, Navbar } from '@/components';
import { GameBackground } from '@/components/GameBackground';
import { AnimatedOutlet } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router';
@ -8,24 +9,28 @@ export const Route = createFileRoute('/app')({
function RouteComponent() {
return (
<div className="h-full w-full">
<Background />
<LoaderSuspense spinner={{ size: 'large' }}>
<div className="flex flex-col h-full w-full">
<Navbar />
<div className="h-full w-full">
<AnimatedOutlet
enter={{
initial: { opacity: 0, transform: 'translateX(-20%)' },
animate: { opacity: 1, transform: 'translateX(0)' },
}}
exit={{
initial: { opacity: 1, transform: 'translateX(0)' },
animate: { opacity: 0, transform: 'translateX(-20%)' },
}}
from={Route.id}
transition={{ duration: 0.3, bounce: 0.1, power: 0.2, type: 'spring' }}
/>
<Background />
<div className="h-full w-full pb-12">
<AnimatedOutlet
enter={{
initial: { opacity: 0 },
animate: { opacity: 1 },
}}
exit={{
initial: { opacity: 1 },
animate: { opacity: 0 },
}}
from={Route.id}
transition={{ duration: 0.3, bounce: 0.1, power: 0.2, type: 'spring' }}
/>
</div>
</div>
</div>
</LoaderSuspense>
);
}

View file

@ -2,17 +2,27 @@ import type { ClusterModel } from '@/bindings.gen';
import type { FileRoutesByFullPath } from '@/routeTree.gen';
import { create } from 'zustand';
interface AppFrameStore {
previousPage: FileRoutesByFullPath | null;
interface AppStore {
activeCluster: ClusterModel | null;
previousPage: FileRoutesByFullPath | null;
}
const useAppFrameStore = create<AppFrameStore>()(set => ({
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 useAppFrameStore;
export default useAppStore;

View file

@ -0,0 +1,18 @@
@layer utilities {
@keyframes custom-anim-fade-scale {
0% {
transform: scale(0);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 0;
}
}
}
@utility animate-fade-scale {
transform: scale(0);
opacity: 1;
animation: custom-anim-fade-scale var(--default-animation-duration, 1s) var(--default-animation-timing-function, cubic-bezier(0, 0, 0.2, 1)) var(--default-animation-delay, 0s) forwards;
}

View file

@ -2,6 +2,7 @@
@plugin "tailwindcss-react-aria-components";
@plugin "tailwindcss-animated";
@import "../../node_modules/@onelauncher/common/dist/theme.css";
@import "./animations.css";
@utility partial-underline-* {
position: relative;
@ -18,3 +19,16 @@
border-radius: 10px;
}
}
@theme {
/** border */
--color-component-border: #ffffff1A;
/** rounding */
--radius-sm: 0.25rem;
--radius-md: 0.438rem;
--radius-lg: 0.625rem;
--radius-xl: 1rem;
--radius-2xl: 1.5rem;
--radius-3xl: 2rem;
}

View file

@ -0,0 +1,9 @@
import type { bindings } from '@/main';
export type BindingCommands = (keyof typeof bindings.core) | (keyof typeof bindings.oneclient);
declare module '@onelauncher/common' {
interface Register {
commands: Array<BindingCommands>;
}
}

View file

@ -0,0 +1,44 @@
import type { MotionProps } from 'motion/react';
export const transitions = {
spring: {
duration: 0.3,
bounce: 0.1,
power: 0.2,
type: 'spring',
},
} as const satisfies Record<string, MotionProps['transition']>;
export const animations = {
slideInRight: {
initial: {
right: '-100%',
},
animate: {
right: '0',
},
transition: transitions.spring,
},
slideInLeft: {
initial: {
position: 'relative',
left: '-100%',
},
animate: {
position: 'relative',
left: '0',
},
},
slideInUp: {
initial: {
position: 'relative',
bottom: '-100%',
},
animate: {
position: 'relative',
bottom: '0',
},
},
} as const satisfies Record<string, MotionProps>;

View file

@ -0,0 +1,40 @@
import type { GameBackgroundName } from '@/components';
export interface VersionInfo {
backgroundName: GameBackgroundName;
description: string;
}
const VERSION_MAP: Record<number, VersionInfo> = {
[-1]: {
backgroundName: 'HypixelSkyblockHub',
description: 'Unknown Version',
},
8: {
backgroundName: 'HypixelSkyblockHub',
description: 'The Bountiful Update',
},
} as const;
export function getVersionInfo(version: number | string | null | undefined): VersionInfo | undefined {
if (version === undefined || version === null)
return undefined;
let majorVersion;
if (typeof version === 'string') {
const parts = version.split('.');
if (parts.length >= 2)
majorVersion = Number.parseInt(parts[1], 10);
}
if (majorVersion)
return VERSION_MAP[majorVersion];
else
return undefined;
}
export function getVersionInfoOrDefault(version: number | string): VersionInfo {
const info = getVersionInfo(version);
return info ?? VERSION_MAP[-1];
}

View file

@ -20,6 +20,12 @@ pub struct SingleInstancePayload {
#[tracing::instrument]
async fn initialize_core() -> LauncherResult<()> {
let level = if cfg!(debug_assertions) {
tracing::Level::DEBUG
} else {
tracing::Level::INFO
};
let opts = CoreOptions {
curseforge_api_key: Some(constants::CURSEFORGE_API_KEY.to_string()),
launcher_name: "OneLauncher".to_string(),
@ -27,6 +33,9 @@ async fn initialize_core() -> LauncherResult<()> {
launcher_website: "https://polyfrost.org/".to_string(),
discord_client_id: Some(constants::DISCORD_CLIENT_ID.to_string()),
fetch_attempts: 3,
logger_filter: Some(
format!("{}={level},onelauncher_core={level}", env!("CARGO_PKG_NAME")),
),
..Default::default()
};

View file

@ -2,3 +2,14 @@
@plugin "tailwindcss-react-aria-components";
@plugin "tailwindcss-animated";
@import "../../node_modules/@onelauncher/common/dist/theme.css";
@theme {
--radius-xs: 0.125rem;
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--radius-2xl: 1rem;
--radius-3xl: 1.5rem;
--radius-4xl: 2rem;
}

View file

@ -12,6 +12,9 @@ pub struct ProxyTauri {
impl ProxyTauri {
#[must_use]
pub fn new(handle: AppHandle) -> Self {
tracing::debug!("using tauri bridge");
tracing::debug!("webview version: {}", tauri::webview_version().unwrap_or("unknown".into()));
Self {
emitter: LauncherEventEmitter::new(handle.clone()),
handle,

View file

@ -143,8 +143,8 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
.await
.map_err(|e| LauncherError::from(e))?;
Ok(())
}
// Ok(())
// }
// Setting Profiles

View file

@ -6,7 +6,15 @@ use crate::store::Core;
#[cfg(debug_assertions)]
pub async fn start_logger() {
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(format!("{}=info", env!("CARGO_PKG_NAME"))));
.unwrap_or_else(|_| {
tracing_subscriber::EnvFilter::new(
Core::get().logger_filter
.as_ref()
.unwrap_or(
&format!("{}=info", env!("CARGO_PKG_NAME"))
)
)
});
let mut fmt_layer = tracing_subscriber::fmt::layer()
.with_ansi(true)
@ -16,7 +24,7 @@ pub async fn start_logger() {
.with_thread_names(true)
.pretty();
if let Some(span) = Core::get().span_formatting.clone() {
if let Some(span) = Core::get().logger_span_formatting.clone() {
fmt_layer = fmt_layer.with_span_events(span);
}
@ -62,7 +70,15 @@ pub async fn start_logger() {
};
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(format!("{}=info", env!("CARGO_PKG_NAME"))));
.unwrap_or_else(|_| {
tracing_subscriber::EnvFilter::new(
Core::get().logger_filter
.as_ref()
.unwrap_or(
&format!("{}=info", env!("CARGO_PKG_NAME"))
)
)
});
let mut fmt_layer = tracing_subscriber::fmt::layer()
.compact()

View file

@ -16,11 +16,12 @@ pub struct CoreOptions {
pub launcher_version: String,
pub launcher_website: String,
pub fetch_attempts: usize,
pub span_formatting: Option<FmtSpan>,
/// Default client id is the same as the one used by the official launcher
pub msa_client_id: String,
pub msa_redirect_uri: String,
pub curseforge_api_key: Option<String>,
pub logger_span_formatting: Option<FmtSpan>,
pub logger_filter: Option<String>,
}
impl Default for CoreOptions {
@ -31,10 +32,11 @@ impl Default for CoreOptions {
launcher_version: String::from(env!("CARGO_PKG_VERSION")),
launcher_website: String::from("https://polyfrost.org/"),
fetch_attempts: 3,
span_formatting: None,
msa_client_id: String::from("00000000402b5328"),
msa_redirect_uri: String::from("https://login.live.com/oauth20_desktop.srf"),
curseforge_api_key: None,
logger_span_formatting: None,
logger_filter: None,
}
}
}

View file

@ -24,6 +24,7 @@
"peerDependencies": {
"@tanstack/react-query": "^5.66.5",
"@tanstack/react-router": "^1.114.3",
"@untitled-theme/icons-react": "^0.14.0",
"motion": "^12.10.5",
"react": "^19.0.0",
"react-aria-components": "^1.8.0",

View file

@ -7,7 +7,7 @@
import type { AnyRoute } from '@tanstack/react-router';
import type { MotionProps } from 'motion/react';
import { Outlet, useMatch, useRouter } from '@tanstack/react-router';
import { motion } from 'motion/react';
import { AnimatePresence, motion } from 'motion/react';
import { createContext, useContext, useEffect, useRef, useState } from 'react';
interface TransitionProps {

View file

@ -29,8 +29,9 @@ export const buttonVariants = tv({
},
size: {
normal: 'py-1.5 px-3',
large: 'py-2 px-6 text-lg',
icon: 'p-1.5 aspect-square box-border w-8 h-8',
iconLarge: 'w-9 h-9 [&>*]:p-0.5',
iconLarge: 'w-10 h-10 [&>*]:p-0.5',
},
},
});

View file

@ -1,5 +1,6 @@
import type { UndefinedInitialDataOptions, UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
/* eslint-disable query/exhaustive-deps -- False */
import type { UndefinedInitialDataOptions, UseMutationOptions, UseMutationResult, UseQueryResult, UseSuspenseQueryOptions, UseSuspenseQueryResult } from '@tanstack/react-query';
import { useMutation, useQuery, useSuspenseQuery } from '@tanstack/react-query';
// this gets overwritten by the consumer project
export interface Register {
@ -10,24 +11,52 @@ type RegisterConfig = Register extends {
commands: infer Keys;
} ? Keys : never;
export function useCommand<T>(cacheKey: RegisterConfig[number] | (string & {}) | false, command: () => Promise<T>, options?: Omit<UndefinedInitialDataOptions<T>, 'queryKey' | 'queryFn'>): UseQueryResult<T, Error> {
async function fetchCommand<T>(command: () => Promise<T>): Promise<T> {
try {
const result = await command();
return result;
}
// TODO: (lynith) handle errors better (requires modifications to TauRPC)
catch (e) {
console.error(e);
if (e instanceof Error)
return Promise.reject(e);
else
return Promise.reject(new Error('Unknown error'));
}
}
export function useCommand<T>(
cacheKey: RegisterConfig[number] | (string & {}) | false,
command: () => Promise<T>,
options?: Omit<UndefinedInitialDataOptions<T>, 'queryKey' | 'queryFn'>,
): UseQueryResult<T, Error> {
return useQuery({
queryKey: cacheKey === false ? [] : [cacheKey],
queryFn: async () => {
try {
const result = await command();
return result;
}
// TODO: (lynith) handle errors better (requires modifications to TauRPC)
catch (e) {
console.error(e);
if (e instanceof Error)
return Promise.reject(e);
else
return Promise.reject(new Error('Unknown error'));
}
},
queryFn: () => fetchCommand(command),
enabled: true,
...options,
});
}
export function useCommandSuspense<T>(
cacheKey: RegisterConfig[number] | (string & {}) | false,
command: () => Promise<T>,
options?: Omit<UseSuspenseQueryOptions<T>, 'queryKey' | 'queryFn'>,
): UseSuspenseQueryResult<T, Error> {
return useSuspenseQuery({
queryKey: cacheKey === false ? [] : [cacheKey],
queryFn: () => fetchCommand(command),
...options,
});
}
export function useCommandMut<TData, TError = Error, TVariables = void, TContext = unknown>(
command: () => Promise<TData>,
options?: Omit<UseMutationOptions<TData, TError, TVariables, TContext>, 'mutationFn'>,
): UseMutationResult<TData, TError, TVariables, TContext> {
return useMutation<TData, TError, TVariables, TContext>({
mutationFn: () => fetchCommand(command),
...options,
});
}

View file

@ -3,7 +3,7 @@
@plugin "tailwindcss-animated";
@theme {
/* foreground colors */
/** foreground colors */
--color-fg-primary: #d5dbff;
--color-fg-primary-hover: #dae0ff;
--color-fg-primary-pressed: #e1e5ff;
@ -19,7 +19,7 @@
--color-link-pressed: #72afff;
--color-link-disabled: #7597b3;
/* background colors */
/** background colors */
--color-brand: #2b4bff;
--color-brand-hover: #2843dd;
--color-brand-pressed: #3957ff;
@ -35,7 +35,7 @@
--color-page-overlay: #ffffff1a;
--color-window-frame: #151b20;
/* general colors */
/** general colors */
--color-danger: #ff4444;
--color-danger-hover: #d63434;
--color-danger-pressed: #ff5656;
@ -52,12 +52,12 @@
--color-code-debug: #2b4bff;
--color-code-trace: #fdfdfd;
/* border */
/** border */
--color-component-border: rgba(255, 255, 255, 0.05);
--color-component-border-hover: rgba(255, 255, 255, 0.10);
--color-component-border-pressed: rgba(255, 255, 255, 0.15);
/* overlay */
/** overlay */
--color-ghost-overlay: rgba(255, 255, 255, 0.05);
--color-ghost-overlay-hover: rgba(255, 255, 255, 0.10);
--color-ghost-overlay-pressed: rgba(255, 255, 255, 0.15);
@ -66,11 +66,11 @@
--color-scrollbar-bg-hover: rgba(255, 255, 255, 0.10);
--color-scrollbar-bg-pressed: rgba(255, 255, 255, 0.15);
/* other */
/** other */
--font-sans: "Poppins", sans-serif;
--text-xs: 0.688rem;
--text-sm: 0.75rem;
--text-base: 0.875rem;
--text-base: 0.875rem; /** body text is 14px */
--text-lg: 1rem;
--text-2lg: 1.125rem;
--text-xl: 1.25rem;
@ -83,6 +83,7 @@
* {
user-select: none !important;
-webkit-user-select: none !important;
-webkit-user-drag: none !important;
cursor: default !important;
}

View file

@ -44,6 +44,7 @@ export default defineConfig({
'tailwindcss',
'tailwind-merge',
'tailwind-variants',
'@untitled-theme/icons-react',
// 'tailwindcss-animated',
// 'tailwindcss-react-aria-components',
],