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::set_default_user,
|
||||||
users::begin_ms_flow,
|
users::begin_ms_flow,
|
||||||
|
|
||||||
|
cluster::create_cluster,
|
||||||
|
cluster::get_clusters,
|
||||||
|
|
||||||
other::open_dev_tools,
|
other::open_dev_tools,
|
||||||
]
|
]
|
||||||
}};
|
}};
|
||||||
|
|
|
||||||
|
|
@ -214,4 +214,32 @@
|
||||||
// cluster::repair_cluster(&cluster.cluster_path()).await?;
|
// cluster::repair_cluster(&cluster.cluster_path()).await?;
|
||||||
|
|
||||||
// Ok(())
|
// 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>) {
|
}: PlayerHeadProps & ImgHTMLAttributes<HTMLImageElement>) {
|
||||||
const src = useMemo(() => {
|
const src = useMemo(() => {
|
||||||
return uuid
|
return uuid
|
||||||
? `https://crafatar.com/avatars/${uuid}?overlay&size=8`
|
? `https://crafatar.com/avatars/${uuid}?overlay&size=64`
|
||||||
: SteveHead;
|
: SteveHead;
|
||||||
}, [uuid]);
|
}, [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 })}
|
navigate={(to, options) => router.navigate({ to, ...options })}
|
||||||
useHref={to => router.buildLocation({ to }).href}
|
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'>
|
<main className='max-h-screen min-h-screen flex flex-col'>
|
||||||
<WindowFrame />
|
<WindowFrame />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { TextField } from '@/components/base/TextField';
|
||||||
|
import { Modal } from '@/components/overlay/Modal';
|
||||||
import { createFileRoute } from '@tanstack/react-router';
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
|
|
||||||
export const Route = createFileRoute('/app/browser/')({
|
export const Route = createFileRoute('/app/browser/')({
|
||||||
|
|
@ -5,5 +7,13 @@ export const Route = createFileRoute('/app/browser/')({
|
||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
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 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';
|
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||||
|
|
||||||
export const Route = createFileRoute('/app/')({
|
export const Route = createFileRoute('/app/')({
|
||||||
|
|
@ -6,9 +11,21 @@ export const Route = createFileRoute('/app/')({
|
||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
|
const result = useCommand("getClusters", bindings.commands.getClusters)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col gap-y-4 text-fg-primary">
|
<div className="h-full flex flex-col gap-y-4 text-fg-primary">
|
||||||
<Banner />
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -83,3 +100,97 @@ function Banner() {
|
||||||
</div>
|
</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">
|
<small className="pt-2 text-fg-secondary">
|
||||||
Want to contribute a launcher import? Click
|
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>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import Button from '@/components/base/Button'
|
import Button from '@/components/base/Button'
|
||||||
import { createFileRoute, Outlet, useNavigate, useRouterState } from '@tanstack/react-router'
|
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')({
|
export const Route = createFileRoute('/onboarding')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
|
|
@ -16,6 +16,28 @@ const steps = [
|
||||||
'/onboarding/complete', // wip
|
'/onboarding/complete', // wip
|
||||||
] as const;
|
] 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() {
|
function RouteComponent() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const routerState = useRouterState()
|
const routerState = useRouterState()
|
||||||
|
|
@ -61,7 +83,7 @@ function RouteComponent() {
|
||||||
console.log("Bir sonraki adım belirlenemedi.");
|
console.log("Bir sonraki adım belirlenemedi.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// remind me 2 hours! i'll fix this
|
// remind me 2 hours! i'll fix this
|
||||||
// update: it's fixed!
|
// update: it's fixed!
|
||||||
|
|
@ -90,29 +112,29 @@ function RouteComponent() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OnboardingStepProps {
|
export interface OnboardingStepProps {
|
||||||
title: string;
|
title: string;
|
||||||
paragraph: string;
|
paragraph: string;
|
||||||
illustration: JSX.Element;
|
illustration: JSX.Element;
|
||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OnboardingStep(props: OnboardingStepProps) {
|
export function OnboardingStep(props: OnboardingStepProps) {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-2 h-full w-full gap-x-16">
|
<div className="grid grid-cols-2 h-full w-full gap-x-16">
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
{props.illustration}
|
{props.illustration}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col justify-center gap-y-4">
|
<div className="flex flex-col justify-center gap-y-4">
|
||||||
<div className="w-full flex flex-col gap-y-2">
|
<div className="w-full flex flex-col gap-y-2">
|
||||||
<h1 className="text-2xl">{props.title}</h1>
|
<h1 className="text-2xl">{props.title}</h1>
|
||||||
<p className="text-lg text-fg-secondary line-height-normal">{props.paragraph}</p>
|
<p className="text-lg text-fg-secondary line-height-normal">{props.paragraph}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-h-96 w-full flex flex-1 flex-col gap-y-2">
|
<div className="max-h-96 w-full flex flex-1 flex-col gap-y-2">
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Loading…
Add table
Reference in a new issue