feat(cluster): add settings, screenshots, and worlds pages
This commit is contained in:
parent
e92508c8de
commit
46cca44837
17 changed files with 530 additions and 32 deletions
|
|
@ -22,6 +22,7 @@ macro_rules! collect_commands {
|
|||
cluster::get_cluster_by_id,
|
||||
|
||||
settings::get_global_profile,
|
||||
settings::get_profile_or_default,
|
||||
|
||||
other::open_dev_tools,
|
||||
]
|
||||
|
|
|
|||
|
|
@ -7,4 +7,12 @@ use crate::api::error::SerializableResult;
|
|||
#[tauri::command]
|
||||
pub async fn get_global_profile() -> SerializableResult<Model> {
|
||||
Ok(setting_profiles::get_global_profile().await)
|
||||
}
|
||||
|
||||
#[specta::specta]
|
||||
#[tauri::command]
|
||||
pub async fn get_profile_or_default(
|
||||
name: Option<String>,
|
||||
) -> SerializableResult<Model> {
|
||||
Ok(setting_profiles::dao::get_profile_or_default(name.as_ref()).await?)
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ interface ScrollableContainerProps {
|
|||
|
||||
function ScrollableContainer(props: ScrollableContainerProps) {
|
||||
return (
|
||||
<div className="h-full h-screen w-[calc(100%+14px)] flex flex-1 flex-col overflow-hidden">
|
||||
<div className="h-full h-screen w-full flex flex-1 flex-col overflow-hidden">
|
||||
<OverlayScrollbarsComponent className="os-hide-horizontal-scrollbar relative flex-1">
|
||||
<div className="absolute h-full w-[calc(100%-14px)] flex flex-col gap-2">
|
||||
{props.children}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ function SettingsRow(props: SettingsRowProps) {
|
|||
return (
|
||||
<div
|
||||
className={`
|
||||
flex flex-row items-center gap-3.5 rounded-xl p-4
|
||||
flex flex-row items-center gap-3.5 rounded-xl p-4 my-2
|
||||
${props.disabled ? 'bg-component-bg-disabled' : 'bg-page-elevated hover:bg-component-bg-hover'}
|
||||
`}
|
||||
>
|
||||
|
|
|
|||
62
apps/onelauncher/frontend/src/components/base/Dropdown.tsx
Normal file
62
apps/onelauncher/frontend/src/components/base/Dropdown.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import type { ClassNameString } from '@/types/global';
|
||||
import type {
|
||||
SelectProps as AriaComboBoxProps,
|
||||
ListBoxItemProps as AriaListBoxItemProps
|
||||
} from 'react-aria-components';
|
||||
import {
|
||||
Select as AriaComboBox,
|
||||
Label as AriaLabel,
|
||||
ListBox as AriaListBox,
|
||||
ListBoxItem as AriaListBoxItem,
|
||||
Popover as AriaPopover,
|
||||
SelectValue
|
||||
} from 'react-aria-components';
|
||||
import { tv } from 'tailwind-variants';
|
||||
import Button from './Button';
|
||||
|
||||
interface ComboBoxWrapperProps<T extends object> extends Omit<AriaComboBoxProps<T>, 'className'>, ClassNameString {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
const dropdownStyles = tv({
|
||||
base: [
|
||||
"flex flex-col border border-component-bg-hover w-fit rounded-lg focus:outline-none"
|
||||
]
|
||||
})
|
||||
|
||||
function Dropdown<T extends object>({
|
||||
className,
|
||||
label,
|
||||
children,
|
||||
...rest
|
||||
}: ComboBoxWrapperProps<T>) {
|
||||
return (
|
||||
<AriaComboBox
|
||||
className={dropdownStyles({ className })}
|
||||
{...rest}
|
||||
>
|
||||
{label && <AriaLabel>{label}</AriaLabel>}
|
||||
<div className="flex">
|
||||
<SelectValue />
|
||||
<Button className="size-6">▼</Button>
|
||||
</div>
|
||||
<AriaPopover className="border border-orange-400">
|
||||
<AriaListBox className="bg-blue-500 p-2 w-38">
|
||||
{children}
|
||||
</AriaListBox>
|
||||
</AriaPopover>
|
||||
</AriaComboBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default Dropdown;
|
||||
|
||||
Dropdown.Item = ({
|
||||
className,
|
||||
...rest
|
||||
}: AriaListBoxItemProps & ClassNameString) => (
|
||||
<AriaListBoxItem
|
||||
className={className}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import type { Model, GameLoader } from '@/bindings.gen';
|
||||
import FabricImage from '@/assets/logos/loaders/fabric.png';
|
||||
import ForgeImage from '@/assets/logos/loaders/forge.png';
|
||||
import NeoForgeImage from '@/assets/logos/loaders/neoforge.png';
|
||||
import QuiltImage from '@/assets/logos/loaders/quilt.png';
|
||||
import VanillaImage from '@/assets/logos/minecraft.png';
|
||||
import type { RefAttributes } from 'react';
|
||||
|
||||
export function getLoaderLogoSrc(loader: Model | GameLoader): string {
|
||||
const loaderName = (typeof loader === 'string' ? loader : loader.mc_loader)?.toLowerCase() as GameLoader;
|
||||
|
||||
const mapping: Record<GameLoader, string> = {
|
||||
vanilla: VanillaImage,
|
||||
fabric: FabricImage,
|
||||
legacyfabric: FabricImage,
|
||||
forge: ForgeImage,
|
||||
neoforge: NeoForgeImage,
|
||||
quilt: QuiltImage,
|
||||
};
|
||||
|
||||
return mapping[loaderName];
|
||||
}
|
||||
|
||||
type LoaderIconProp = RefAttributes<HTMLImageElement> & {
|
||||
loader: GameLoader | undefined;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function LoaderIcon(props: LoaderIconProp) {
|
||||
return (
|
||||
<img
|
||||
{...props}
|
||||
alt={`${props.loader}'s logo`}
|
||||
src={props.loader ? getLoaderLogoSrc(props.loader) : VanillaImage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoaderIcon;
|
||||
|
|
@ -11,7 +11,7 @@ interface ModalProps extends DialogProps, ClassNameString { }
|
|||
function Modal({ className, ...props }: ModalProps) {
|
||||
return (
|
||||
<ModalOverlay isDismissable className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
|
||||
<AriaDialog 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" {...props} />
|
||||
<AriaDialog className="" {...props} />
|
||||
</ModalOverlay>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
42
apps/onelauncher/frontend/src/hooks/useCluster.ts
Normal file
42
apps/onelauncher/frontend/src/hooks/useCluster.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { bindings } from "@/main";
|
||||
import useCommand from "./useCommand";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { Model } from "@/bindings.gen";
|
||||
|
||||
function useRecentCluster() {
|
||||
const result = useCommand("getClusters", bindings.commands.getClusters);
|
||||
const [cluster, setCluster] = useState<Model | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!result.data) return;
|
||||
|
||||
let mostRecentCluster: Model | undefined;
|
||||
|
||||
for (const c of result.data) {
|
||||
if (!mostRecentCluster) {
|
||||
mostRecentCluster = c;
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentPlayed = mostRecentCluster.last_played;
|
||||
const newPlayed = c.last_played;
|
||||
|
||||
if (typeof currentPlayed !== 'string' && typeof newPlayed === 'string') {
|
||||
mostRecentCluster = c;
|
||||
} else if (typeof currentPlayed === 'string' && typeof newPlayed === 'string') {
|
||||
const playedAt = new Date(currentPlayed);
|
||||
const clusterPlayedAt = new Date(newPlayed);
|
||||
|
||||
if (clusterPlayedAt > playedAt) {
|
||||
mostRecentCluster = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setCluster(mostRecentCluster);
|
||||
}, [result.data]);
|
||||
|
||||
return cluster;
|
||||
}
|
||||
|
||||
export default useRecentCluster;
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import Button from '@/components/base/Button';
|
||||
import Dropdown from '@/components/base/Dropdown';
|
||||
import { TextField } from '@/components/base/TextField';
|
||||
import Modal from '@/components/overlay/Modal';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
|
@ -22,6 +23,11 @@ function RouteComponent() {
|
|||
<p>sadsadsad</p>
|
||||
</Modal>
|
||||
</Modal.Trigger>
|
||||
|
||||
{/* <Dropdown>
|
||||
<Dropdown.Item>Slmalr</Dropdown.Item>
|
||||
<Dropdown.Item>Slmalr 2</Dropdown.Item>
|
||||
</Dropdown> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,16 @@ import useCommand from '@/hooks/useCommand'
|
|||
import { bindings } from '@/main'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import Sidebar from '../settings/route'
|
||||
import Button from '@/components/base/Button'
|
||||
import { Edit02Icon, FolderIcon, ImagePlusIcon, Share07Icon, Tool02Icon, Trash01Icon } from '@untitled-theme/icons-react'
|
||||
import LoaderIcon from '@/components/launcher/LoaderIcon'
|
||||
import { Show } from '@/components/base/Show'
|
||||
import { TextField } from '@/components/base/TextField'
|
||||
import type { Model } from '@/bindings.gen'
|
||||
import ScrollableContainer from '@/components/ScrollableContainer'
|
||||
import { useState, type Dispatch, type SetStateAction } from 'react'
|
||||
import SettingsRow from '@/components/SettingsRow'
|
||||
import ToggleButton from '@/components/base/ToggleButton'
|
||||
|
||||
export const Route = createFileRoute('/app/cluster/')({
|
||||
component: RouteComponent
|
||||
|
|
@ -9,15 +19,183 @@ export const Route = createFileRoute('/app/cluster/')({
|
|||
|
||||
function RouteComponent() {
|
||||
const { id } = Route.useSearch()
|
||||
const [newCover, setNewCover] = useState<string>("")
|
||||
const [newName, setNewName] = useState<string>("")
|
||||
|
||||
// dumbass fix ik
|
||||
const cluster = useCommand("getClusterById", () => bindings.commands.getClusterById(Number(id.toString()) as unknown as bigint))
|
||||
|
||||
return (
|
||||
<Sidebar.Page>
|
||||
<div className="h-full">
|
||||
<pre>{JSON.stringify(cluster, null, 2)}</pre>
|
||||
</div>
|
||||
<h1>Overview</h1>
|
||||
<ScrollableContainer>
|
||||
<div className='h-full'>
|
||||
<Banner
|
||||
cluster={cluster.data}
|
||||
newCover={newCover}
|
||||
setNewCover={setNewCover}
|
||||
editMode={false}
|
||||
newName={newName}
|
||||
setNewName={setNewName}
|
||||
refetch={cluster.refetch}
|
||||
/>
|
||||
|
||||
<SettingsRow.Header>Folders and Files</SettingsRow.Header>
|
||||
<SettingsRow
|
||||
children={(
|
||||
<Button
|
||||
color="primary"
|
||||
children="Open"
|
||||
isDisabled={false}
|
||||
/>
|
||||
)}
|
||||
description={"burada path var işte yersen"}
|
||||
disabled={false}
|
||||
icon={<FolderIcon />}
|
||||
title="Cluster Folder"
|
||||
/>
|
||||
|
||||
<SettingsRow.Header>Cluster Actions</SettingsRow.Header>
|
||||
<SettingsRow
|
||||
children={(
|
||||
<ToggleButton color='secondary'>Edit</ToggleButton>
|
||||
)}
|
||||
description="Edit the cluster name and cover image."
|
||||
icon={<Edit02Icon />}
|
||||
title="Edit Cluster"
|
||||
/>
|
||||
<SettingsRow
|
||||
children={(
|
||||
<Button
|
||||
color="danger"
|
||||
children="Delete"
|
||||
isDisabled={false}
|
||||
/>
|
||||
)}
|
||||
description="Delete this cluster and all its data."
|
||||
disabled={false}
|
||||
icon={<Trash01Icon />}
|
||||
title="Delete Cluster"
|
||||
/>
|
||||
<SettingsRow
|
||||
children={(
|
||||
<Button
|
||||
color="secondary"
|
||||
children="Repair"
|
||||
/>
|
||||
)}
|
||||
description="Verifies whether all assets, libraries and natives were properly installed."
|
||||
icon={<Tool02Icon />}
|
||||
title="Verify Cluster"
|
||||
/>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
</Sidebar.Page>
|
||||
)
|
||||
}
|
||||
|
||||
interface BannerProps {
|
||||
cluster: Model | null | undefined;
|
||||
editMode: boolean;
|
||||
newName: string;
|
||||
setNewName: Dispatch<SetStateAction<string>>;
|
||||
newCover: string;
|
||||
setNewCover: Dispatch<SetStateAction<string>>;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
function Banner(props: BannerProps) {
|
||||
// async function launchFilePicker() {
|
||||
// const selected = await dialog.open({
|
||||
// multiple: false,
|
||||
// directory: false,
|
||||
// filters: [{
|
||||
// name: 'Image',
|
||||
// extensions: ['png', 'jpg', 'jpeg', 'webp'],
|
||||
// }],
|
||||
// });
|
||||
|
||||
// if (selected === null)
|
||||
// return;
|
||||
|
||||
// props.setNewCover(selected);
|
||||
// }
|
||||
|
||||
function updateName(name: string) {
|
||||
if (name.length > 30 || name.length <= 0)
|
||||
return;
|
||||
|
||||
props.setNewName(name);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-37 flex flex-row gap-x-2.5 rounded-xl bg-page-elevated p-2.5">
|
||||
<div className="relative aspect-ratio-video h-full min-w-57 w-57 overflow-hidden border border-component-bg/10 rounded-lg">
|
||||
<Show when={props.editMode}>
|
||||
<div
|
||||
className="absolute z-1 h-full w-full flex items-center justify-center bg-black/50 opacity-50 hover:opacity-100"
|
||||
// onClick={launchFilePicker}
|
||||
>
|
||||
<ImagePlusIcon className="h-12 w-12" />
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* <ClusterCover class="h-full w-full object-cover" cluster={props.cluster} override={props.newCover()} /> */}
|
||||
<img src='https://github.com/emirsassan.png' />
|
||||
</div>
|
||||
|
||||
<div className="w-full flex flex-col justify-between gap-y-.5 overflow-hidden text-fg-primary">
|
||||
<div>
|
||||
<Show
|
||||
fallback={
|
||||
<h2 className="break-words text-wrap text-2xl">{props.cluster?.name}</h2>
|
||||
}
|
||||
when={props.editMode}
|
||||
>
|
||||
<TextField
|
||||
className="text-xl font-bold"
|
||||
onChange={e => updateName(e.target.value)}
|
||||
placeholder={props.cluster?.name}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 flex-row">
|
||||
<div
|
||||
className={"flex flex-1 flex-col items-start justify-between " + props.editMode ? "text-fg-primary-disabled" : ""}
|
||||
>
|
||||
<span className="flex flex-row items-center gap-x-1">
|
||||
<LoaderIcon
|
||||
className="w-5"
|
||||
loader={props.cluster?.mc_loader}
|
||||
/>
|
||||
<span>{props.cluster?.mc_version}</span>
|
||||
<span>{props.cluster?.mc_loader || 'unknown'}</span>
|
||||
{props.cluster?.mc_loader_version && <span>{props.cluster.mc_loader_version}</span>}
|
||||
</span>
|
||||
<span
|
||||
className={"text-xs text-fg-secondary " + props.editMode ? "text-fg-secondary-disabled" : ""}
|
||||
>
|
||||
Played for
|
||||
{' '}
|
||||
<b>{props.cluster?.overall_played || 0}</b>
|
||||
.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-end gap-x-2.5 *:h-8">
|
||||
<Button children="Launch" />
|
||||
|
||||
<Button
|
||||
color="secondary"
|
||||
children={<Share07Icon />}
|
||||
// disabled={props.editMode()}
|
||||
isDisabled={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { createFileRoute, Outlet } from '@tanstack/react-router'
|
||||
import Sidebar from '../settings/route'
|
||||
import { EyeIcon, File06Icon, Globe04Icon, Image03Icon, PackagePlusIcon, Settings04Icon } from '@untitled-theme/icons-react'
|
||||
import useCommand from '@/hooks/useCommand'
|
||||
import { bindings } from '@/main'
|
||||
|
||||
type ClusterSearch = {
|
||||
id: bigint
|
||||
|
|
@ -16,6 +18,12 @@ export const Route = createFileRoute('/app/cluster')({
|
|||
})
|
||||
|
||||
function RouteComponent() {
|
||||
const { id } = Route.useSearch()
|
||||
|
||||
const cluster = useCommand("getClusterById", () => bindings.commands.getClusterById(Number(id.toString()) as unknown as bigint))
|
||||
|
||||
const isModded = cluster.data?.mc_loader != "vanilla"
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-row overflow-hidden">
|
||||
<div className="flex-shrink-0 w-72 flex flex-col pt-8 pr-7 pb-8">
|
||||
|
|
@ -23,12 +31,13 @@ function RouteComponent() {
|
|||
base="/app/cluster"
|
||||
links={{
|
||||
'Cluster Settings': [
|
||||
[<EyeIcon />, 'Overview', '/'],
|
||||
[<PackagePlusIcon />, 'Mods', '/mods'],
|
||||
[<Image03Icon />, 'Screenshots', '/screenshots'],
|
||||
[<Globe04Icon />, 'Worlds', '/worlds'],
|
||||
[<File06Icon />, 'Logs', '/logs'],
|
||||
[<Settings04Icon />, 'Game Settings', '/settings'],
|
||||
// if someone has a better way to do this, please let me know
|
||||
[<EyeIcon />, 'Overview', '/?id=' + id],
|
||||
(isModded ? [<PackagePlusIcon />, 'Mods', '/mods?id=' + id] : undefined),
|
||||
[<Image03Icon />, 'Screenshots', '/screenshots?id=' + id],
|
||||
[<Globe04Icon />, 'Worlds', '/worlds?id=' + id],
|
||||
[<File06Icon />, 'Logs', '/logs?id=' + id],
|
||||
[<Settings04Icon />, 'Game Settings', '/settings?id=' + id],
|
||||
],
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import Sidebar from '../settings/route';
|
||||
import ScrollableContainer from '@/components/ScrollableContainer';
|
||||
import { Show } from '@/components/base/Show';
|
||||
|
||||
export const Route = createFileRoute('/app/cluster/screenshots')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
const arr = [1, 2, 3, 4, 5, 6]
|
||||
return (
|
||||
<Sidebar.Page>
|
||||
<h1>Screenshots</h1>
|
||||
<ScrollableContainer>
|
||||
<div className="h-full">
|
||||
<Show
|
||||
fallback={<div className="text-border/400">No screenshots found. Press F2 in game to take a screenshot!</div>}
|
||||
when={true}
|
||||
>
|
||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(350px,1fr))] w-full transform-gpu gap-2">
|
||||
{arr.map((data) => (
|
||||
<ScreenshotEntry key={data} path={`screenshot-${data}.png`} cluster_path="cluster" />
|
||||
))}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
</Sidebar.Page>
|
||||
)
|
||||
}
|
||||
|
||||
function ScreenshotEntry(props: { path: string; cluster_path: string }) {
|
||||
function onClick() {
|
||||
//screen shot viewer overlay
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col items-center gap-3 rounded-xl bg-component-bg p-3 active:bg-component-bg-pressed hover:bg-component-bg-hover hover:opacity-80"
|
||||
onClick={onClick}
|
||||
>
|
||||
<img alt={props.path} className="aspect-video w-full rounded-lg" src="https://github.com/emirsassan.png" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import Sidebar from '../settings/route'
|
||||
import ScrollableContainer from '@/components/ScrollableContainer'
|
||||
import useCommand from '@/hooks/useCommand'
|
||||
import { bindings } from '@/main'
|
||||
|
||||
export const Route = createFileRoute('/app/cluster/settings')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
const { id } = Route.useSearch()
|
||||
|
||||
const result = useCommand("getProfileOrDefault", () => bindings.commands.getProfileOrDefault(id.toString()))
|
||||
|
||||
return (
|
||||
<Sidebar.Page>
|
||||
<h1>Settings</h1>
|
||||
<ScrollableContainer>
|
||||
<div className="h-full">
|
||||
<pre>{JSON.stringify(result.data, null, 2)}</pre>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
</Sidebar.Page>
|
||||
)
|
||||
}
|
||||
20
apps/onelauncher/frontend/src/routes/app/cluster/worlds.tsx
Normal file
20
apps/onelauncher/frontend/src/routes/app/cluster/worlds.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import Sidebar from '../settings/route'
|
||||
import ScrollableContainer from '@/components/ScrollableContainer'
|
||||
|
||||
export const Route = createFileRoute('/app/cluster/worlds')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return (
|
||||
<Sidebar.Page>
|
||||
<h1>Worlds</h1>
|
||||
<ScrollableContainer>
|
||||
<div className="h-full">
|
||||
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
</Sidebar.Page>
|
||||
)
|
||||
}
|
||||
|
|
@ -3,10 +3,12 @@ import type { Model } from '@/bindings.gen';
|
|||
import Button from '@/components/base/Button';
|
||||
import useCommand from '@/hooks/useCommand';
|
||||
import { bindings } from '@/main';
|
||||
import { useIsFetching, useQueryClient } from '@tanstack/react-query';
|
||||
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
import DefaultInstancePhoto from "@/assets/images/default_instance_cover.jpg"
|
||||
import Modal from '@/components/overlay/Modal';
|
||||
import { Server01Icon } from '@untitled-theme/icons-react';
|
||||
import HeaderImage from "@/assets/images/default_instance_cover.jpg";
|
||||
|
||||
export const Route = createFileRoute('/app/')({
|
||||
component: RouteComponent,
|
||||
|
|
@ -30,7 +32,7 @@ function RouteComponent() {
|
|||
</div>
|
||||
|
||||
<div className='flex flex-col'>
|
||||
<ClusterGroup clusters={result.data} />
|
||||
<ClusterGroup clusters={result.data} isFetching={result.isFetching} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -108,8 +110,6 @@ function Banner() {
|
|||
}
|
||||
|
||||
function ClusterCreate() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const result = useCommand("createCluster", () => bindings.commands.createCluster({
|
||||
icon_url: "asd",
|
||||
mc_loader: "vanilla",
|
||||
|
|
@ -128,30 +128,59 @@ function ClusterCreate() {
|
|||
alert(result.error.message)
|
||||
return;
|
||||
}
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["getClusters"],
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
children="New Cluster"
|
||||
onClick={testThingy}
|
||||
/>
|
||||
<Modal.Trigger>
|
||||
<Button
|
||||
children="New Cluster"
|
||||
// onClick={testThingy}
|
||||
/>
|
||||
|
||||
<Modal>
|
||||
<div className="min-w-sm flex flex-col rounded-lg bg-page text-center">
|
||||
<div className="theme-OneLauncher-Dark relative h-25 flex">
|
||||
<div className="absolute left-0 top-0 h-full w-full">
|
||||
<img alt="Header Image" className="h-full w-full rounded-t-lg" src={HeaderImage} />
|
||||
</div>
|
||||
<div
|
||||
className="absolute left-0 top-0 h-full flex flex-row items-center justify-start gap-x-4 bg-[radial-gradient(at_center,#00000077,transparent)] px-10"
|
||||
>
|
||||
<Server01Icon className="h-8 w-8 text-fg-primary" />
|
||||
<div className="flex flex-col items-start justify-center">
|
||||
<h1 className="h-10 text-fg-primary -mt-2">New Cluster</h1>
|
||||
<span className="text-fg-primary">asd</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col border border-white/5 rounded-b-lg">
|
||||
<div className="p-3">
|
||||
<pre>asdasdasdasds</pre>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row justify-end gap-x-2 p-3 pt-0">
|
||||
<Button
|
||||
color="primary"
|
||||
children="Create"
|
||||
onClick={testThingy}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Modal.Trigger>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface ClusterGroupProps {
|
||||
clusters: Model[] | undefined;
|
||||
isFetching?: boolean;
|
||||
}
|
||||
|
||||
function ClusterGroup(props: ClusterGroupProps) {
|
||||
const isFetching = useIsFetching();
|
||||
|
||||
if (isFetching) {
|
||||
if (props.isFetching) {
|
||||
return (
|
||||
<div className='flex items-center justify-center h-full'>
|
||||
<div className="w-8 h-8 border-4 border-brand rounded-full border-t-transparent animate-spin" />
|
||||
|
|
|
|||
|
|
@ -66,10 +66,8 @@ function Sidebar(props: SidebarProps) {
|
|||
navigate({ to: url })
|
||||
}
|
||||
|
||||
function isActive(link: string, params: URLSearchParams | undefined) {
|
||||
const pathnameMatch = location === `${props.base}${link}` || `${location}/` === `${props.base}${link}`;
|
||||
const paramsMatch = params ? location.endsWith(params.toString()) : true;
|
||||
return pathnameMatch && paramsMatch;
|
||||
function isActive(link: string, _params: URLSearchParams | undefined) {
|
||||
return location === `${props.base}${link}` || `${location}/` === `${props.base}${link}`;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -109,7 +107,7 @@ function Sidebar(props: SidebarProps) {
|
|||
|
||||
interface SidebarPageProps {
|
||||
className?: string;
|
||||
children: JSX.Element;
|
||||
children: JSX.Element | JSX.Element[];
|
||||
}
|
||||
|
||||
Sidebar.Page = function (props: SidebarPageProps) {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use crate::store::State;
|
|||
|
||||
pub mod dao {
|
||||
use onelauncher_entity::setting_profiles;
|
||||
use sea_orm::IntoActiveModel;
|
||||
use sea_orm::{ActiveModelTrait, EntityTrait};
|
||||
|
||||
use crate::error::{DaoError, LauncherResult};
|
||||
|
|
@ -57,6 +58,39 @@ pub mod dao {
|
|||
Ok(profile.insert(db).await?)
|
||||
}
|
||||
|
||||
pub async fn update_profile_by_name<B>(
|
||||
name: &str,
|
||||
block: B,
|
||||
) -> LauncherResult<setting_profiles::Model>
|
||||
where B: AsyncFnOnce(setting_profiles::ActiveModel) -> LauncherResult<setting_profiles::ActiveModel> {
|
||||
let state = State::get().await?;
|
||||
let db = &state.db;
|
||||
|
||||
let model = get_profile_by_name(name).await?.ok_or(DaoError::NotFound)?;
|
||||
let model = block(model.into_active_model()).await?;
|
||||
let model = model.update(db).await?;
|
||||
|
||||
Ok(model)
|
||||
}
|
||||
|
||||
/// Updates an existing profile in the database.
|
||||
pub async fn update_profile<B>(
|
||||
profile: &mut setting_profiles::Model,
|
||||
block: B,
|
||||
) -> LauncherResult<&mut setting_profiles::Model>
|
||||
where B: AsyncFnOnce(setting_profiles::ActiveModel) -> LauncherResult<setting_profiles::ActiveModel> {
|
||||
let state = State::get().await?;
|
||||
let db = &state.db;
|
||||
|
||||
let model = profile.clone().into_active_model();
|
||||
let model = block(model).await?;
|
||||
let model = model.update(db).await?;
|
||||
|
||||
*profile = model;
|
||||
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
pub async fn delete_profile_by_name(name: &str) -> LauncherResult<()> {
|
||||
let state = State::get().await?;
|
||||
let db = &state.db;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue