feat: add cluster management functionality
only creating for now lol
This commit is contained in:
parent
13fb769208
commit
5d88f9a5c1
10 changed files with 277 additions and 26 deletions
|
|
@ -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,
|
||||
]
|
||||
}};
|
||||
|
|
|
|||
|
|
@ -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?)
|
||||
}
|
||||
45
apps/onelauncher/frontend/src/components/base/TextField.tsx
Normal file
45
apps/onelauncher/frontend/src/components/base/TextField.tsx
Normal 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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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]);
|
||||
|
||||
|
|
|
|||
33
apps/onelauncher/frontend/src/components/overlay/Modal.tsx
Normal file
33
apps/onelauncher/frontend/src/components/overlay/Modal.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 />
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue