feat: add cluster management functionality

only creating for now lol
This commit is contained in:
emirsassan 2025-05-22 22:36:11 +03:00
parent 13fb769208
commit 5d88f9a5c1
10 changed files with 277 additions and 26 deletions

View file

@ -16,6 +16,9 @@ macro_rules! collect_commands {
users::set_default_user,
users::begin_ms_flow,
cluster::create_cluster,
cluster::get_clusters,
other::open_dev_tools,
]
}};

View file

@ -214,4 +214,32 @@
// cluster::repair_cluster(&cluster.cluster_path()).await?;
// Ok(())
// }
// }
use onelauncher_core::{api::cluster, entity::{clusters::Model, icon::Icon, loader::GameLoader}};
use serde::{Deserialize, Serialize};
use specta::Type;
use crate::api::error::SerializableResult;
#[derive(Serialize, Deserialize, Type)]
pub struct CreateCluster {
name: String,
mc_version: String,
mc_loader: GameLoader,
mc_loader_version: Option<String>,
icon_url: Option<Icon>
}
#[specta::specta]
#[tauri::command]
pub async fn create_cluster(options: CreateCluster) -> SerializableResult<Model> {
let thing = cluster::create_cluster(&options.name, &options.mc_version, options.mc_loader, options.mc_loader_version.as_deref(), options.icon_url).await?;
Ok(thing)
}
#[specta::specta]
#[tauri::command]
pub async fn get_clusters() -> SerializableResult<Vec<Model>> {
Ok(cluster::dao::get_all_clusters().await?)
}

View file

@ -0,0 +1,45 @@
import { Input } from 'react-aria-components';
import type { RefAttributes } from 'react';
import type { InputProps as AriaInputProps } from 'react-aria-components';
import type { VariantProps } from 'tailwind-variants';
import { tv } from 'tailwind-variants';
export const textFieldVariants = tv({
base: [
'bg-component-bg',
'[&:not(:focus-within)]:hover:bg-component-bg-hover',
'focus-within:bg-component-bg-pressed',
'disabled:bg-component-bg-disabled',
'border-component-border border fill-fg-primary',
'rounded-lg px-2.5 py-1.5 min-h-8',
'flex flex-row items-center justify-start gap-x-1.5',
'text-sm text-fg-primary font-medium',
'bg-transparent outline-none appearance-none p-0 m-0 box-border w-full font-medium',
'[&::selection]:bg-brand/30',
'placeholder:text-fg-secondary/40 placeholder:font-normal'
],
variants: {
invalid: {
true: {
base: 'border-danger'
}
}
}
})
export type TextFieldVariants = VariantProps<typeof textFieldVariants>;
export type TextFieldProps = AriaInputProps & RefAttributes<HTMLInputElement> & {
className?: string;
};
export function TextField({
className,
...props
}: TextFieldProps) {
return (
<>
<Input className={textFieldVariants({ className })} {...props} />
</>
)
}

View file

@ -13,7 +13,7 @@ function PlayerHead({
}: PlayerHeadProps & ImgHTMLAttributes<HTMLImageElement>) {
const src = useMemo(() => {
return uuid
? `https://crafatar.com/avatars/${uuid}?overlay&size=8`
? `https://crafatar.com/avatars/${uuid}?overlay&size=64`
: SteveHead;
}, [uuid]);

View file

@ -0,0 +1,33 @@
import { Dialog, DialogTrigger, Heading, Input, Label, Modal as AriaModal, TextField, ModalOverlay } from 'react-aria-components';
import Button from '../base/Button';
// just testing stuff
export function Modal() {
return (
<>
<DialogTrigger>
<Button className={"focus:outline-none"}>Slmlar</Button>
<ModalOverlay isDismissable className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<AriaModal>
<Dialog className="min-w-md flex flex-col gap-y-2 border border-white/5 rounded-lg bg-page p-4 text-center focus:outline-none">
<form>
<Heading slot="title">Sign up</Heading>
<TextField autoFocus>
<Label>First Name:</Label>
<Input />
</TextField>
<TextField>
<Label>Last Name:</Label>
<Input />
</TextField>
<Button slot="close">
Submit
</Button>
</form>
</Dialog>
</AriaModal>
</ModalOverlay>
</DialogTrigger>
</>
)
}

View file

@ -29,7 +29,6 @@ function RootRoute() {
navigate={(to, options) => router.navigate({ to, ...options })}
useHref={to => router.buildLocation({ to }).href}
>
{/* questionable fix copied from old frontend but it works */}
<main className='max-h-screen min-h-screen flex flex-col'>
<WindowFrame />

View file

@ -1,3 +1,5 @@
import { TextField } from '@/components/base/TextField';
import { Modal } from '@/components/overlay/Modal';
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/app/browser/')({
@ -5,5 +7,13 @@ export const Route = createFileRoute('/app/browser/')({
});
function RouteComponent() {
return <div>Hello "/app/browser/"!</div>;
return (
<div>
<p>asdasdas</p>
<TextField />
<Modal />
</div>
);
}

View file

@ -1,4 +1,9 @@
import DefaultBanner from '@/assets/images/default_banner.png';
import type { Model } from '@/bindings.gen';
import Button from '@/components/base/Button';
import useCommand from '@/hooks/useCommand';
import { bindings } from '@/main';
import { useIsFetching } from '@tanstack/react-query';
import { createFileRoute, useNavigate } from '@tanstack/react-router';
export const Route = createFileRoute('/app/')({
@ -6,9 +11,21 @@ export const Route = createFileRoute('/app/')({
});
function RouteComponent() {
const result = useCommand("getClusters", bindings.commands.getClusters)
return (
<div className="h-full flex flex-col gap-y-4 text-fg-primary">
<Banner />
<div className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center gap-x-2">
<ClusterCreate />
</div>
</div>
<div className='h-96 overflow-y-auto'>
<ClusterGroup clusters={result.data} />
</div>
</div>
);
}
@ -83,3 +100,97 @@ function Banner() {
</div>
);
}
function ClusterCreate() {
const result = useCommand("createCluster", () => bindings.commands.createCluster({
icon_url: "asd",
mc_loader: "vanilla",
mc_version: "1.20.1",
name: "Test Cluster",
mc_loader_version: "0.13.5",
}), {
enabled: false,
subscribed: false
})
const testThingy = () => {
result.refetch()
if (result.isError) {
alert(result.error.message)
return;
}
return;
}
return (
<>
<Button
children="New Cluster"
onClick={testThingy}
/>
</>
)
}
interface ClusterGroupProps {
clusters: Model[] | undefined;
}
function ClusterGroup(props: ClusterGroupProps) {
const isFetching = useIsFetching();
if (isFetching) {
return (
<div className='flex items-center justify-center h-fit'>
<div className="w-8 h-8 border-4 border-brand rounded-full border-t-transparent animate-spin" />
</div>
)
}
return (
<>
<div className='grid grid-cols-4 gap-2'>
{props.clusters?.map((data) => (
<ClusterCard key={data.id} {...data} />
))}
</div>
</>
)
}
function ClusterCard(props: Model) {
return (
<>
<div
className="group relative h-[152px] flex flex-col rounded-xl border border-component-border/5 bg-component-bg active:bg-component-bg-pressed hover:bg-component-bg-hover"
>
<div className="relative flex-1 overflow-hidden rounded-t-xl">
<div
className="absolute h-full w-full transition-transform group-hover:!scale-110"
>
<img
className="h-full w-full object-cover"
src='https://github.com/emirsassan.png'
/>
</div>
</div>
<div className="z-10 flex flex-row items-center justify-between gap-x-3 p-3">
<div className="h-full flex flex-col gap-1.5 overflow-hidden">
<p className="h-4 text-ellipsis whitespace-nowrap font-medium">{props.name}</p>
<p className="h-4 text-xs">
{props.mc_loader}
{' '}
{props.mc_version}
{/* {' '}
{props.packages.mods && `• ${props.mods} mods`} */}
</p>
</div>
{/* <LaunchButton cluster={props} iconOnly /> */}
</div>
</div>
</>
);
}

View file

@ -27,7 +27,7 @@ function RouteComponent() {
<small className="pt-2 text-fg-secondary">
Want to contribute a launcher import? Click
{' '}
<a href="https://github.com/Polyfrost/OneLauncher">here</a>
<a className='text-brand-pressed' href="https://github.com/Polyfrost/OneLauncher">here</a>
.
</small>
</div>

View file

@ -1,6 +1,6 @@
import Button from '@/components/base/Button'
import { createFileRoute, Outlet, useNavigate, useRouterState } from '@tanstack/react-router'
import type { JSX } from 'react';
import { createContext, useState, type JSX } from 'react';
export const Route = createFileRoute('/onboarding')({
component: RouteComponent,
@ -16,6 +16,28 @@ const steps = [
'/onboarding/complete', // wip
] as const;
type Language = {
lang: string,
percentage: number
}
interface ImportInstancesType {
basePath: string;
instances: string[];
}
interface OnboardingContextType {
setLanguage: (language: Language) => void;
language: () => Language;
setImportInstances: (type: string, basePath: string, instances: string[]) => void;
importInstances: (type: string) => ImportInstancesType | undefined;
setForwardButtonEnabled: (enabled: boolean) => void;
getTasks: () => string[];
}
function RouteComponent() {
const navigate = useNavigate()
const routerState = useRouterState()
@ -61,7 +83,7 @@ function RouteComponent() {
console.log("Bir sonraki adım belirlenemedi.");
}
};
return (
// remind me 2 hours! i'll fix this
// update: it's fixed!
@ -90,29 +112,29 @@ function RouteComponent() {
}
export interface OnboardingStepProps {
title: string;
paragraph: string;
illustration: JSX.Element;
title: string;
paragraph: string;
illustration: JSX.Element;
children: JSX.Element;
}
export function OnboardingStep(props: OnboardingStepProps) {
return (
<div className="grid grid-cols-2 h-full w-full gap-x-16">
<div className="flex flex-col items-center justify-center">
{props.illustration}
</div>
return (
<div className="grid grid-cols-2 h-full w-full gap-x-16">
<div className="flex flex-col items-center justify-center">
{props.illustration}
</div>
<div className="flex flex-col justify-center gap-y-4">
<div className="w-full flex flex-col gap-y-2">
<h1 className="text-2xl">{props.title}</h1>
<p className="text-lg text-fg-secondary line-height-normal">{props.paragraph}</p>
</div>
<div className="flex flex-col justify-center gap-y-4">
<div className="w-full flex flex-col gap-y-2">
<h1 className="text-2xl">{props.title}</h1>
<p className="text-lg text-fg-secondary line-height-normal">{props.paragraph}</p>
</div>
<div className="max-h-96 w-full flex flex-1 flex-col gap-y-2">
{props.children}
</div>
</div>
</div>
);
<div className="max-h-96 w-full flex flex-1 flex-col gap-y-2">
{props.children}
</div>
</div>
</div>
);
}