feat: Create clusters!
This commit is contained in:
parent
a98eda07a7
commit
316f1fc48a
18 changed files with 247 additions and 103 deletions
|
|
@ -12,6 +12,7 @@ fn main() {
|
|||
"remove_user",
|
||||
"launch_cluster",
|
||||
"create_cluster",
|
||||
"remove_cluster",
|
||||
"get_clusters",
|
||||
"get_cluster",
|
||||
"get_manifest",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ permissions = [
|
|||
"allow-remove-user",
|
||||
"allow-launch-cluster",
|
||||
"allow-create-cluster",
|
||||
"allow-remove-cluster",
|
||||
"allow-get-clusters",
|
||||
"allow-get-cluster",
|
||||
"allow-get-manifest",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ macro_rules! collect_commands {
|
|||
get_user,
|
||||
remove_user,
|
||||
create_cluster,
|
||||
remove_cluster,
|
||||
get_cluster,
|
||||
get_clusters,
|
||||
get_settings,
|
||||
|
|
@ -71,6 +72,13 @@ pub async fn create_cluster(props: CreateCluster) -> Result<Uuid, String> {
|
|||
}
|
||||
}
|
||||
|
||||
#[specta::specta]
|
||||
#[tauri::command]
|
||||
pub async fn remove_cluster(uuid: Uuid) -> Result<(), String> {
|
||||
let path = cluster::get_by_uuid(uuid, None).await?.ok_or("Cluster does not exist")?.cluster_path();
|
||||
Ok(cluster::remove(&path).await?)
|
||||
}
|
||||
|
||||
// #[specta::specta]
|
||||
// #[tauri::command]
|
||||
// pub fn update_cluster(cluster: Cluster) -> Result<(), String> {
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ function WindowFrame() {
|
|||
title="Close OneLauncher?"
|
||||
visible={isModalVisible}
|
||||
setVisible={setModalVisible}
|
||||
zIndex={999999}
|
||||
buttons={[
|
||||
<Button buttonStyle="secondary" onClick={() => setModalVisible(false)}>No</Button>,
|
||||
<Button buttonStyle="danger" onClick={() => kill()}>Yes</Button>,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
@apply bg-component-bg hover:bg-component-bg-hover active:bg-component-bg-pressed disabled:bg-component-bg-disabled;
|
||||
}
|
||||
|
||||
&__danger {
|
||||
&__danger, &__iconDanger {
|
||||
@apply bg-danger hover:bg-danger-hover active:bg-danger-pressed disabled:bg-danger-disabled;
|
||||
}
|
||||
|
||||
|
|
@ -20,8 +20,8 @@
|
|||
@apply hover:bg-gray-05 [&[aria-checked="true"]]:bg-gray-10 active:bg-gray-10;
|
||||
}
|
||||
|
||||
&__icon, &__iconSecondary {
|
||||
@apply p-1.5 hover:bg-gray-05 [&[aria-checked="true"]]:bg-gray-10 active:bg-gray-10 aspect-square box-border w-8 h-8;
|
||||
&__icon, &__iconSecondary, &__iconDanger {
|
||||
@apply p-1.5 aspect-square box-border w-8 h-8;
|
||||
|
||||
&.large {
|
||||
@apply w-9 h-9;
|
||||
|
|
@ -33,6 +33,10 @@
|
|||
& > * {
|
||||
@apply w-full h-full stroke-fg-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon, &__iconSecondary {
|
||||
@apply hover:bg-gray-05 [&[aria-checked="true"]]:bg-gray-10 active:bg-gray-10;
|
||||
|
||||
&:hover > * {
|
||||
@apply stroke-fg-primary-hover;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import styles from './Button.module.scss';
|
|||
type ButtonProps = JSX.ButtonHTMLAttributes<HTMLButtonElement> & ParentProps & {
|
||||
iconLeft?: JSX.Element;
|
||||
iconRight?: JSX.Element;
|
||||
buttonStyle?: 'primary' | 'secondary' | 'danger' | 'icon' | 'iconSecondary' | 'ghost';
|
||||
buttonStyle?: 'primary' | 'secondary' | 'danger' | 'icon' | 'iconSecondary' | 'iconDanger' | 'ghost';
|
||||
large?: boolean;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -25,10 +25,8 @@ function FullscreenOverlay(props: FullscreenOverlayProps) {
|
|||
style={{ 'z-index': props.zIndex || 1000 }}
|
||||
class={`fixed top-0 left-0 w-screen h-screen bg-black/60 backdrop-blur-sm backdrop-grayscale transition-opacity ${props.visible() ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}`}
|
||||
>
|
||||
{/* New element to prevent from having to call stopPropagation in child elements */}
|
||||
{/* <div class="absolute w-full h-full" onClick={() => onBackdropClick()} /> */}
|
||||
|
||||
<div class="absolute top-0 left-0 w-screen h-screen" onClick={onBackdropClick}>
|
||||
<div class="absolute top-0 left-0 w-screen h-screen flex justify-center items-center" onClick={onBackdropClick}>
|
||||
{props.children}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -27,11 +27,11 @@ function Modal(props: ModalProps) {
|
|||
mount={props.mount}
|
||||
zIndex={props.zIndex || 1000}
|
||||
>
|
||||
<div ref={mergeRefs(props.ref)} class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
<div class="bg-primary border border-white/5 p-4 rounded-lg text-center flex flex-col gap-y-2 min-w-xs">
|
||||
{props.children}
|
||||
</div>
|
||||
{/* <div ref={mergeRefs(props.ref)} class="absolute top-0 bottom-0 left-1/2 flex items-center"> */}
|
||||
<div ref={mergeRefs(props.ref)} class="bg-primary border border-white/5 p-4 rounded-lg text-center flex flex-col gap-y-2 min-w-xs">
|
||||
{props.children}
|
||||
</div>
|
||||
{/* </div> */}
|
||||
</FullscreenOverlay>
|
||||
);
|
||||
}
|
||||
|
|
@ -56,7 +56,7 @@ Modal.Simple = function (props: ModalSimpleProps) {
|
|||
<div class="flex flex-col">
|
||||
{props.children}
|
||||
</div>
|
||||
<div class="flex flex-row gap-x-4 [&>*]:flex-1">
|
||||
<div class="flex flex-row gap-x-4 [&>*]:flex-1 mt-2">
|
||||
<For each={props.buttons}>
|
||||
{button => (
|
||||
<ModalButton props={button} />
|
||||
|
|
@ -72,7 +72,7 @@ function ModalButton(button: { props: ModalButtonProps }) {
|
|||
<>
|
||||
{typeof button.props === 'string'
|
||||
? (
|
||||
<Button>
|
||||
<Button buttonStyle="secondary">
|
||||
{button.props}
|
||||
</Button>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ interface ClusterModalContextFunc {
|
|||
partialCluster: Accessor<PartialCluster>;
|
||||
setPartialCluster: Setter<PartialCluster>;
|
||||
updatePartialCluster: PartialClusterUpdateFunc;
|
||||
start: () => Promise<void>; // Returns true if its finished (created) successfully
|
||||
finish: () => void;
|
||||
visible: Accessor<boolean>;
|
||||
setVisible: Setter<boolean>;
|
||||
|
|
@ -37,6 +38,8 @@ export function ClusterModalController(props: ParentProps) {
|
|||
const [step, setStep] = createSignal<number>(ClusterModalStages.STAGE_1_PROVIDER);
|
||||
const [visible, setVisible] = createSignal(false);
|
||||
const [partialCluster, setPartialCluster] = createSignal<PartialCluster>({});
|
||||
const [inProgress, setInProgress] = createSignal(false);
|
||||
const [error, setError] = createSignal<string | undefined>(undefined);
|
||||
|
||||
const updatePartialCluster: PartialClusterUpdateFunc = (key, value) => {
|
||||
setPartialCluster((prev) => {
|
||||
|
|
@ -47,24 +50,54 @@ export function ClusterModalController(props: ParentProps) {
|
|||
});
|
||||
};
|
||||
|
||||
const finish = () => {
|
||||
const start = () => {
|
||||
setPartialCluster({});
|
||||
setStep(ClusterModalStages.STAGE_1_PROVIDER);
|
||||
setError(undefined);
|
||||
setInProgress(true);
|
||||
setVisible(true);
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
createEffect(() => {
|
||||
const result = error();
|
||||
if (inProgress() === true)
|
||||
return;
|
||||
|
||||
if (result === undefined || result.length === 0)
|
||||
resolve();
|
||||
else
|
||||
reject(result);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const finish = async () => {
|
||||
const untracked = untrack(partialCluster);
|
||||
|
||||
if (untracked.name === undefined || untracked.mc_version === undefined || untracked.mod_loader === undefined)
|
||||
throw new Error('Cluster is missing required fields');
|
||||
|
||||
bridge.commands.createCluster({
|
||||
name: untracked.name!,
|
||||
mod_loader: untracked.mod_loader!,
|
||||
mc_version: untracked.mc_version!,
|
||||
// TODO: Implement the rest of the fields
|
||||
icon: null,
|
||||
icon_url: null,
|
||||
loader_version: null,
|
||||
package_data: null,
|
||||
skip: null,
|
||||
skip_watch: null,
|
||||
});
|
||||
setVisible(false);
|
||||
|
||||
try {
|
||||
await bridge.commands.createCluster({
|
||||
name: untracked.name!,
|
||||
mod_loader: untracked.mod_loader!,
|
||||
mc_version: untracked.mc_version!,
|
||||
// TODO: Implement the rest of the fields
|
||||
icon: null,
|
||||
icon_url: null,
|
||||
loader_version: null,
|
||||
package_data: null,
|
||||
skip: null,
|
||||
skip_watch: null,
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
setInProgress(false);
|
||||
};
|
||||
|
||||
const stepper: ClusterModalContextFunc = {
|
||||
|
|
@ -73,6 +106,7 @@ export function ClusterModalController(props: ParentProps) {
|
|||
partialCluster,
|
||||
setPartialCluster,
|
||||
updatePartialCluster,
|
||||
start,
|
||||
finish,
|
||||
visible,
|
||||
setVisible,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,14 @@ function HomePage() {
|
|||
const controller = useClusterModalController();
|
||||
|
||||
async function newCluster() {
|
||||
controller.setVisible(true);
|
||||
try {
|
||||
await controller.start();
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
refetch();
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
153
apps/desktop/src/ui/pages/cluster/ClusterOverview.tsx
Normal file
153
apps/desktop/src/ui/pages/cluster/ClusterOverview.tsx
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import { PlayIcon, Share07Icon, Trash01Icon } from '@untitled-theme/icons-solid';
|
||||
import { type Accessor, type Setter, createEffect, createSignal, on } from 'solid-js';
|
||||
import { useNavigate } from '@solidjs/router';
|
||||
import ClusterCover from '../../components/game/ClusterCover';
|
||||
import LoaderIcon from '../../components/game/LoaderIcon';
|
||||
import Button from '../../components/base/Button';
|
||||
import ScrollableContainer from '~ui/components/ScrollableContainer';
|
||||
import Sidebar from '~ui/components/Sidebar';
|
||||
import type { Cluster } from '~bindings';
|
||||
import { upperFirst } from '~utils/primitives';
|
||||
import useClusterContext from '~ui/hooks/useCluster';
|
||||
import Modal from '~ui/components/overlay/Modal';
|
||||
import { bridge } from '~imports';
|
||||
|
||||
function ClusterOverview() {
|
||||
const cluster = useClusterContext();
|
||||
const [deleteVisible, setDeleteVisible] = createSignal(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
async function deleteCluster() {
|
||||
await bridge.commands.removeCluster(cluster.uuid);
|
||||
navigate('/');
|
||||
}
|
||||
|
||||
return (
|
||||
<Sidebar.Page>
|
||||
<h1>Overview</h1>
|
||||
<ScrollableContainer>
|
||||
<Banner {...cluster} showDeletePrompt={() => setDeleteVisible(true)} />
|
||||
</ScrollableContainer>
|
||||
|
||||
<DeleteModal
|
||||
visible={deleteVisible}
|
||||
setVisible={setDeleteVisible}
|
||||
deleteCluster={deleteCluster}
|
||||
/>
|
||||
</Sidebar.Page>
|
||||
);
|
||||
}
|
||||
|
||||
interface DeleteModalProps {
|
||||
visible: Accessor<boolean>;
|
||||
setVisible: Setter<boolean>;
|
||||
deleteCluster: () => void;
|
||||
};
|
||||
function DeleteModal(props: DeleteModalProps) {
|
||||
const [timeLeft, setTimeLeft] = createSignal(3);
|
||||
|
||||
createEffect(on(() => props.visible(), (visible) => {
|
||||
if (visible !== true)
|
||||
return;
|
||||
|
||||
setTimeLeft(3);
|
||||
const interval = setInterval(() => {
|
||||
setTimeLeft((prev) => {
|
||||
const next = prev - 1;
|
||||
|
||||
if (next <= 0)
|
||||
clearInterval(interval);
|
||||
|
||||
return next;
|
||||
});
|
||||
}, 1000);
|
||||
}));
|
||||
|
||||
return (
|
||||
<Modal.Simple
|
||||
visible={props.visible}
|
||||
setVisible={props.setVisible}
|
||||
title="Delete Cluster"
|
||||
buttons={[
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
children="Cancel"
|
||||
onClick={() => props.setVisible(false)}
|
||||
/>,
|
||||
<Button
|
||||
buttonStyle="danger"
|
||||
children={`Delete${timeLeft() > 0 ? ` (${timeLeft()}s)` : ''}`}
|
||||
disabled={timeLeft() > 0}
|
||||
onClick={props.deleteCluster}
|
||||
/>,
|
||||
]}
|
||||
>
|
||||
<div class="flex flex-col justify-center items-center gap-y-3">
|
||||
<p>Are you sure you want to delete this cluster?</p>
|
||||
<p class="text-danger uppercase max-w-82 line-height-normal">
|
||||
Doing this will
|
||||
{' '}
|
||||
<span class="underline font-bold">delete</span>
|
||||
{' '}
|
||||
your entire cluster data
|
||||
{' '}
|
||||
<span class="underline font-bold">FOREVER</span>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</Modal.Simple>
|
||||
);
|
||||
}
|
||||
|
||||
function Banner(props: Cluster & { showDeletePrompt: () => void }) {
|
||||
return (
|
||||
<div class="flex flex-row bg-component-bg rounded-xl p-2.5 gap-x-2.5">
|
||||
<div class="w-48 rounded-lg overflow-hidden border border-gray-10">
|
||||
<ClusterCover cluster={props} />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col flex-1 text-fg-primary">
|
||||
<div class="flex-1">
|
||||
<h2 class="text-2xl">{props.meta.name}</h2>
|
||||
<span class="flex flex-row items-center gap-x-1">
|
||||
<LoaderIcon loader={props.meta.loader} class="w-5" />
|
||||
<span>{upperFirst(props.meta.loader || 'unknown')}</span>
|
||||
{props.meta.loader_version && <span>{props.meta.loader_version.id}</span>}
|
||||
<span>{props.meta.mc_version}</span>
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-xs text-fg-secondary">
|
||||
Played for
|
||||
{' '}
|
||||
<b>{((props.meta.overall_played || 0n)).toString()}</b>
|
||||
{' '}
|
||||
hours
|
||||
{/* TODO: Ask Pauline if this is measured in seconds or milliseconds */}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row items-end gap-x-2.5 *:h-8">
|
||||
<Button
|
||||
buttonStyle="iconDanger"
|
||||
children={<Trash01Icon />}
|
||||
onClick={props.showDeletePrompt}
|
||||
/>
|
||||
|
||||
<Button
|
||||
buttonStyle="iconSecondary"
|
||||
children={<Share07Icon />}
|
||||
/>
|
||||
|
||||
<Button
|
||||
buttonStyle="primary"
|
||||
iconLeft={<PlayIcon />}
|
||||
children="Launch"
|
||||
class="!w-auto"
|
||||
onClick={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClusterOverview;
|
||||
|
|
@ -4,11 +4,11 @@ import { EyeIcon, File06Icon, Globe04Icon, Image03Icon, Settings04Icon } from '@
|
|||
import Sidebar from '../../components/Sidebar';
|
||||
import AnimatedRoutes from '../../components/AnimatedRoutes';
|
||||
import ErrorBoundary from '../../components/ErrorBoundary';
|
||||
import ClusterOverview from './cluster/ClusterOverview';
|
||||
import ClusterLogs from './cluster/ClusterLogs';
|
||||
import ClusterMods from './cluster/ClusterMods';
|
||||
import ClusterScreenshots from './cluster/ClusterScreenshots';
|
||||
import ClusterSettings from './cluster/ClusterSettings';
|
||||
import ClusterOverview from './ClusterOverview';
|
||||
import ClusterLogs from './ClusterLogs';
|
||||
import ClusterMods from './ClusterMods';
|
||||
import ClusterScreenshots from './ClusterScreenshots';
|
||||
import ClusterSettings from './ClusterSettings';
|
||||
import useClusterContext, { ClusterProvider } from '~ui/hooks/useCluster';
|
||||
|
||||
function ClusterRoutes() {
|
||||
|
|
|
|||
|
|
@ -1,68 +0,0 @@
|
|||
import { PlayIcon, Share07Icon } from '@untitled-theme/icons-solid';
|
||||
import ClusterCover from '../../../components/game/ClusterCover';
|
||||
import LoaderIcon from '../../../components/game/LoaderIcon';
|
||||
import Button from '../../../components/base/Button';
|
||||
import ScrollableContainer from '~ui/components/ScrollableContainer';
|
||||
import Sidebar from '~ui/components/Sidebar';
|
||||
import type { Cluster } from '~bindings';
|
||||
import { upperFirst } from '~utils/primitives';
|
||||
import useClusterContext from '~ui/hooks/useCluster';
|
||||
|
||||
function ClusterOverview() {
|
||||
const cluster = useClusterContext();
|
||||
|
||||
return (
|
||||
<Sidebar.Page>
|
||||
<h1>Overview</h1>
|
||||
<ScrollableContainer>
|
||||
<Banner {...cluster} />
|
||||
</ScrollableContainer>
|
||||
</Sidebar.Page>
|
||||
);
|
||||
}
|
||||
|
||||
function Banner(props: Cluster) {
|
||||
return (
|
||||
<div class="flex flex-row bg-component-bg rounded-xl p-2.5 gap-x-2.5">
|
||||
<div class="w-48 rounded-lg overflow-hidden border border-gray-10">
|
||||
<ClusterCover cluster={props} />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col flex-1 text-fg-primary">
|
||||
<div class="flex-1">
|
||||
<h2 class="text-2xl">{props.meta.name}</h2>
|
||||
<span class="flex flex-row items-center gap-x-1">
|
||||
<LoaderIcon loader={props.meta.loader} class="w-5" />
|
||||
<span>{upperFirst(props.meta.loader || 'unknown')}</span>
|
||||
{props.meta.loader_version && <span>{props.meta.loader_version.id}</span>}
|
||||
<span>{props.meta.mc_version}</span>
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-xs text-fg-secondary">
|
||||
Played for
|
||||
{' '}
|
||||
<b>{((props.meta.overall_played || 0n)).toString()}</b>
|
||||
{' '}
|
||||
hours
|
||||
{/* TODO: Ask Pauline if this is measured in seconds or milliseconds */}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row items-end gap-x-2.5 *:h-8">
|
||||
<Button
|
||||
buttonStyle="iconSecondary"
|
||||
children={<Share07Icon />}
|
||||
/>
|
||||
<Button
|
||||
buttonStyle="primary"
|
||||
iconLeft={<PlayIcon />}
|
||||
children="Launch"
|
||||
class="!w-auto"
|
||||
onClick={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClusterOverview;
|
||||
|
|
@ -107,9 +107,14 @@ pub async fn create_cluster(
|
|||
.await?;
|
||||
}
|
||||
|
||||
// if !skip.unwrap_or(false) {
|
||||
// crate::game::install_minecraft(&cluster, None, false).await?;
|
||||
// }
|
||||
if !skip.unwrap_or(false) {
|
||||
let clone = cluster.clone();
|
||||
tokio::task::spawn(async move {
|
||||
if let Err(err) = crate::game::install_minecraft(&clone, None, false).await {
|
||||
tracing::error!("failed to install Minecraft while creating cluster {}: {}", cluster.uuid, err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
State::sync().await?;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue