feat: Better ClusterGame page + Added multi-cluster process support
This commit is contained in:
parent
ca66cf5684
commit
08528ec7c1
19 changed files with 455 additions and 175 deletions
|
|
@ -5,6 +5,7 @@ use std::str::FromStr;
|
|||
use onelauncher::cluster::content::logger;
|
||||
use onelauncher::cluster::{self};
|
||||
use onelauncher::data::{Loader, PackageData};
|
||||
use onelauncher::processor::DetailedProcess;
|
||||
use onelauncher::store::{Cluster, ClusterPath};
|
||||
use onelauncher::State;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -132,21 +133,12 @@ pub async fn remove_cluster(uuid: Uuid) -> Result<(), String> {
|
|||
|
||||
#[specta::specta]
|
||||
#[tauri::command]
|
||||
pub async fn run_cluster(uuid: Uuid) -> Result<(Uuid, u32), String> {
|
||||
pub async fn run_cluster(uuid: Uuid) -> Result<DetailedProcess, String> {
|
||||
let path = ClusterPath::find_by_uuid(uuid).await?;
|
||||
let c_lock = cluster::run_default(&path).await?;
|
||||
let child = &*c_lock.read().await;
|
||||
|
||||
let p_uuid = c_lock.read().await.uuid;
|
||||
let p_pid = c_lock
|
||||
.read()
|
||||
.await
|
||||
.current_child
|
||||
.read()
|
||||
.await
|
||||
.id()
|
||||
.unwrap_or(0);
|
||||
|
||||
Ok((p_uuid, p_pid))
|
||||
Ok(DetailedProcess::from_processor_child(child).await)
|
||||
}
|
||||
|
||||
#[specta::specta]
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ macro_rules! collect_commands {
|
|||
kill_process,
|
||||
is_cluster_running,
|
||||
get_pid_by_uuid,
|
||||
get_user_by_process,
|
||||
get_process_started_at,
|
||||
get_processes_detailed_by_path,
|
||||
get_process_detailed_by_id,
|
||||
// Settings
|
||||
get_settings,
|
||||
set_settings,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use onelauncher::processor;
|
||||
use chrono::{DateTime, Utc};
|
||||
use onelauncher::processor::{self, DetailedProcess};
|
||||
use onelauncher::store::{Cluster, ClusterPath};
|
||||
use uuid::Uuid;
|
||||
|
||||
|
|
@ -20,6 +21,30 @@ pub async fn get_processes_by_path(path: ClusterPath) -> Result<Vec<Uuid>, Strin
|
|||
Ok(processor::get_uuids_by_cluster_path(path).await?)
|
||||
}
|
||||
|
||||
#[specta::specta]
|
||||
#[tauri::command]
|
||||
pub async fn get_processes_detailed_by_path(path: ClusterPath) -> Result<Vec<DetailedProcess>, String> {
|
||||
Ok(processor::get_processes_detailed_by_path(path).await?)
|
||||
}
|
||||
|
||||
#[specta::specta]
|
||||
#[tauri::command]
|
||||
pub async fn get_process_detailed_by_id(uuid: Uuid) -> Result<DetailedProcess, String> {
|
||||
Ok(processor::get_process_detailed_by_id(uuid).await?)
|
||||
}
|
||||
|
||||
#[specta::specta]
|
||||
#[tauri::command]
|
||||
pub async fn get_user_by_process(uuid: Uuid) -> Result<Option<Uuid>, String> {
|
||||
Ok(processor::get_user_by_process(uuid).await?)
|
||||
}
|
||||
|
||||
#[specta::specta]
|
||||
#[tauri::command]
|
||||
pub async fn get_process_started_at(uuid: Uuid) -> Result<DateTime<Utc>, String> {
|
||||
Ok(processor::get_process_started_at(uuid).await?)
|
||||
}
|
||||
|
||||
#[specta::specta]
|
||||
#[tauri::command]
|
||||
pub async fn get_pid_by_uuid(uuid: Uuid) -> Result<u32, String> {
|
||||
|
|
|
|||
|
|
@ -26,8 +26,10 @@ function Sidebar(props: SidebarProps) {
|
|||
navigate(url);
|
||||
}
|
||||
|
||||
function isActive(link: string) {
|
||||
return location.pathname === `${props.base}${link}` || `${location.pathname}/` === `${props.base}${link}`;
|
||||
function isActive(link: string, params: URLSearchParams | undefined) {
|
||||
const pathnameMatch = location.pathname === `${props.base}${link}` || `${location.pathname}/` === `${props.base}${link}`;
|
||||
const paramsMatch = params ? location.search.endsWith(params.toString()) : true;
|
||||
return pathnameMatch && paramsMatch;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -47,7 +49,7 @@ function Sidebar(props: SidebarProps) {
|
|||
<a
|
||||
onClick={() => goto(link[2], link[3])}
|
||||
class={
|
||||
`px-3 py-1 rounded-md text-md hover:bg-component-bg-hover active:bg-component-bg-pressed ${isActive(link[2]) ? 'bg-component-bg' : ''}`
|
||||
`px-3 py-1 rounded-md text-md hover:bg-component-bg-hover active:bg-component-bg-pressed ${isActive(link[2], link[3]) ? 'bg-component-bg' : ''}`
|
||||
}
|
||||
>
|
||||
<span class="flex flex-row items-center gap-x-3 *:w-5">
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ function FormattedLog(props: FormattedLogProps) {
|
|||
const lines = createMemo(() => props.log.split('\n'));
|
||||
|
||||
return (
|
||||
<div class="mt-2 h-full flex flex-1 overflow-auto rounded-md bg-component-bg p-2 font-medium font-mono">
|
||||
<div class="h-full flex flex-1 overflow-auto rounded-md bg-component-bg p-2 font-medium font-mono">
|
||||
<OverlayScrollbarsComponent class="relative h-full flex-1">
|
||||
<code class={styles.log}>
|
||||
<For each={lines()}>
|
||||
|
|
|
|||
|
|
@ -164,14 +164,48 @@ Modal.Simple = function (props: ModalSimpleProps) {
|
|||
);
|
||||
};
|
||||
|
||||
type ModalErrorProps = MakeOptional<ModalSimpleProps, 'title'> & {
|
||||
message?: string | undefined;
|
||||
};
|
||||
|
||||
Modal.Error = function (props: ModalErrorProps) {
|
||||
const [split, rest] = splitProps(props, ['message', 'title', 'buttons', 'children']);
|
||||
|
||||
return (
|
||||
<Modal.Simple
|
||||
{...rest}
|
||||
title={split.title || 'Error'}
|
||||
buttons={split.buttons || [
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
children="Close"
|
||||
onClick={props.hide}
|
||||
/>,
|
||||
]}
|
||||
>
|
||||
<div class="max-w-84 flex flex-col items-center gap-y-2">
|
||||
{split.children || (
|
||||
<span>Something went wrong.</span>
|
||||
)}
|
||||
{split.message && (
|
||||
<code class="rounded-md bg-component-bg p-2 text-left">
|
||||
{split.message}
|
||||
</code>
|
||||
)}
|
||||
</div>
|
||||
</Modal.Simple>
|
||||
);
|
||||
};
|
||||
|
||||
type ModalDeleteProps = MakeOptional<ModalSimpleProps, 'title'> & {
|
||||
onCancel?: () => void;
|
||||
onDelete?: () => void;
|
||||
timeLeft?: number;
|
||||
deleteBtnText?: string;
|
||||
};
|
||||
|
||||
Modal.Delete = function (props: ModalDeleteProps) {
|
||||
const [split, rest] = splitProps(props, ['title', 'buttons', 'onCancel', 'onDelete', 'timeLeft', 'children']);
|
||||
const [split, rest] = splitProps(props, ['title', 'buttons', 'onCancel', 'onDelete', 'timeLeft', 'children', 'deleteBtnText']);
|
||||
const [timeLeft, setTimeLeft] = createSignal(1);
|
||||
const [intervalId, setIntervalId] = createSignal<NodeJS.Timeout | undefined>(undefined);
|
||||
|
||||
|
|
@ -235,7 +269,7 @@ Modal.Delete = function (props: ModalDeleteProps) {
|
|||
/>,
|
||||
<Button
|
||||
buttonStyle="danger"
|
||||
children={`Delete${timeLeft() > 0 ? ` (${timeLeft()}s)` : ''}`}
|
||||
children={(props.deleteBtnText || 'Delete $1').replace('$1', timeLeft() > 0 ? `(${timeLeft()}s)` : '')}
|
||||
disabled={timeLeft() > 0}
|
||||
onClick={onDelete}
|
||||
/>,
|
||||
|
|
|
|||
|
|
@ -18,19 +18,15 @@ export default createClusterStep({
|
|||
});
|
||||
|
||||
function ClusterGameSetup(props: ClusterStepProps) {
|
||||
// const check = () => {
|
||||
// const hasName = (props.controller().partialCluster().name?.length ?? 0) > 0;
|
||||
// const hasVersion = (props.controller().partialCluster().mc_version?.length ?? 0) > 0;
|
||||
// const hasLoader = (props.controller().partialCluster().mod_loader?.length ?? 0) > 0;
|
||||
const check = () => {
|
||||
const hasName = (props.controller.partialCluster().name?.length ?? 0) > 0;
|
||||
const hasVersion = (props.controller.partialCluster().mc_version?.length ?? 0) > 0;
|
||||
const hasLoader = (props.controller.partialCluster().mod_loader?.length ?? 0) > 0;
|
||||
|
||||
// props.setCanGoForward(hasName && hasVersion && hasLoader);
|
||||
// };
|
||||
props.setCanGoForward(hasName && hasVersion && hasLoader);
|
||||
};
|
||||
|
||||
// createEffect(check);
|
||||
// createEffect(on(() => props.isVisible(), (curr: boolean) => {
|
||||
// if (curr)
|
||||
// check();
|
||||
// }));
|
||||
createEffect(check);
|
||||
|
||||
const setName = (name: string) => props.controller.updatePartialCluster('name', name);
|
||||
const setVersion = (version: string) => props.controller.updatePartialCluster('mc_version', version);
|
||||
|
|
@ -63,7 +59,7 @@ function ClusterGameSetup(props: ClusterStepProps) {
|
|||
<div class="h-4 w-4">
|
||||
<LoaderIcon loader={loader} />
|
||||
</div>
|
||||
{loader}
|
||||
<span class="capitalize">{loader}</span>
|
||||
</div>
|
||||
</Dropdown.Row>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { type Context, type ParentProps, type ResourceReturn, Show, createContext, useContext } from 'solid-js';
|
||||
import useCommand from './useCommand';
|
||||
import { type Accessor, type Context, type ParentProps, type ResourceReturn, Show, createContext, createSignal, useContext } from 'solid-js';
|
||||
import { useNavigate } from '@solidjs/router';
|
||||
import useCommand, { tryResult } from './useCommand';
|
||||
import type { Cluster } from '~bindings';
|
||||
import { bridge } from '~imports';
|
||||
import ClusterGame from '~ui/pages/cluster/ClusterGame';
|
||||
import Modal, { createModal } from '~ui/components/overlay/Modal';
|
||||
|
||||
const ClusterContext = createContext() as Context<ResourceReturn<Cluster>>;
|
||||
|
||||
|
|
@ -35,4 +38,32 @@ export function useClusterContext() {
|
|||
return context;
|
||||
}
|
||||
|
||||
export function useLaunchCluster(uuid: string | Accessor<string | undefined> | (() => string | undefined) | undefined) {
|
||||
const navigate = useNavigate();
|
||||
const [error, setError] = createSignal<string | undefined>(undefined);
|
||||
|
||||
const getUuid = () => typeof uuid === 'function' ? uuid() : uuid;
|
||||
|
||||
const modal = createModal(props => (
|
||||
<Modal.Error
|
||||
{...props}
|
||||
message={error()}
|
||||
/>
|
||||
));
|
||||
|
||||
return () => {
|
||||
const uuid = getUuid();
|
||||
|
||||
if (uuid === undefined)
|
||||
return;
|
||||
|
||||
tryResult(bridge.commands.runCluster, uuid).then((details) => {
|
||||
navigate(`/clusters/game?${ClusterGame.buildUrl(uuid, details).toString()}`);
|
||||
}).catch((err) => {
|
||||
setError(err);
|
||||
modal.show();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default useClusterContext;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import useCommand from '~ui/hooks/useCommand';
|
|||
import { bridge } from '~imports';
|
||||
import { secondsToWords, upperFirst } from '~utils';
|
||||
import { useClusterCreator } from '~ui/components/overlay/cluster/ClusterCreationModal';
|
||||
import { useLaunchCluster } from '~ui/hooks/useCluster';
|
||||
|
||||
type GroupedClusters = Record<string, Cluster[]>;
|
||||
|
||||
|
|
@ -183,6 +184,7 @@ function Banner(props: BannerProps) {
|
|||
* If there are no clusters, display a generic background with the button action creating a new cluster.
|
||||
*/
|
||||
const [cluster, setCluster] = createSignal<Cluster>();
|
||||
const launch = useLaunchCluster(() => cluster()?.uuid);
|
||||
const navigate = useNavigate();
|
||||
|
||||
createEffect(() => {
|
||||
|
|
@ -259,7 +261,7 @@ function Banner(props: BannerProps) {
|
|||
buttonStyle="primary"
|
||||
iconLeft={<PlayIcon />}
|
||||
children={`Launch ${cluster()!.meta.mc_version}`}
|
||||
onClick={() => ClusterRoot.launch(navigate, cluster()!.uuid)}
|
||||
onClick={launch}
|
||||
/>
|
||||
<Button
|
||||
buttonStyle="iconSecondary"
|
||||
|
|
@ -291,6 +293,8 @@ function ClusterCard(props: Cluster) {
|
|||
navigate(`/clusters/?id=${props.uuid}`);
|
||||
}
|
||||
|
||||
const launch = useLaunchCluster(() => props.uuid);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
|
|
@ -328,7 +332,7 @@ function ClusterCard(props: Cluster) {
|
|||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
ClusterRoot.launch(navigate, props.uuid);
|
||||
launch();
|
||||
}}
|
||||
>
|
||||
<PlayIcon class="h-4! w-4!" />
|
||||
|
|
|
|||
|
|
@ -1,17 +1,23 @@
|
|||
import { type Params, useNavigate, useSearchParams } from '@solidjs/router';
|
||||
import { createEffect, createSignal, onCleanup, onMount } from 'solid-js';
|
||||
import { createSignal, onCleanup, onMount } from 'solid-js';
|
||||
import type { UnlistenFn } from '@tauri-apps/api/event';
|
||||
import { SlashOctagonIcon } from '@untitled-theme/icons-solid';
|
||||
import ClusterRoot from './ClusterRoot';
|
||||
import { bridge } from '~imports';
|
||||
import Sidebar from '~ui/components/Sidebar';
|
||||
import useClusterContext from '~ui/hooks/useCluster';
|
||||
import { TimeAgo } from '~ui/components/DynamicTime';
|
||||
import Tooltip from '~ui/components/base/Tooltip';
|
||||
import useCommand from '~ui/hooks/useCommand';
|
||||
import FormattedLog from '~ui/components/content/FormattedLog';
|
||||
import Button from '~ui/components/base/Button';
|
||||
import Modal, { createModal } from '~ui/components/overlay/Modal';
|
||||
import type { DetailedProcess } from '~bindings';
|
||||
|
||||
interface ClusterGameParams extends Params {
|
||||
process_uuid: string;
|
||||
started_at: string;
|
||||
pid: string;
|
||||
user: string;
|
||||
};
|
||||
|
||||
function ClusterGame() {
|
||||
|
|
@ -20,22 +26,6 @@ function ClusterGame() {
|
|||
const navigate = useNavigate();
|
||||
|
||||
const [unlisten, setUnlisten] = createSignal<UnlistenFn>();
|
||||
const [startedAt, setStartedAt] = createSignal<number>();
|
||||
const [pid] = useCommand(bridge.commands.getPidByUuid, params.process_uuid || '');
|
||||
|
||||
createEffect(() => {
|
||||
if (params.process_uuid === undefined)
|
||||
throw new Error('process_uuid is required');
|
||||
|
||||
const c = cluster();
|
||||
if (c === undefined)
|
||||
throw new Error('Cluster not found');
|
||||
|
||||
if (typeof c.meta.played_at !== 'string')
|
||||
setStartedAt(Date.now());
|
||||
else
|
||||
setStartedAt(new Date(c.meta.played_at).getTime());
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
const unlisten = await bridge.events.processPayload.listen(({ payload }) => {
|
||||
|
|
@ -50,39 +40,77 @@ function ClusterGame() {
|
|||
unlisten()?.();
|
||||
});
|
||||
|
||||
const startedAtFormatted = (startedAt: number) => {
|
||||
const date = new Date(startedAt);
|
||||
const startedAtFormatted = () => {
|
||||
const date = new Date(params.started_at!);
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
const killModal = createModal(props => (
|
||||
<Modal.Delete
|
||||
{...props}
|
||||
title="Kill Game"
|
||||
children="Are you sure you want to kill the game?"
|
||||
onDelete={() => killProcess(true)}
|
||||
deleteBtnText="Kill $1"
|
||||
/>
|
||||
));
|
||||
|
||||
function killProcess(force: boolean = false) {
|
||||
if (params.process_uuid !== undefined) {
|
||||
if (force === true) {
|
||||
bridge.commands.killProcess(params.process_uuid);
|
||||
return;
|
||||
}
|
||||
|
||||
killModal.show();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Sidebar.Page>
|
||||
<h1>Game Running</h1>
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<div class="flex flex-row">
|
||||
<Tooltip text={startedAtFormatted(startedAt() || 0)}>
|
||||
Started:
|
||||
<div class="flex flex-1 flex-col gap-y-4">
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<div class="flex flex-row">
|
||||
<Tooltip text={startedAtFormatted()}>
|
||||
Started:
|
||||
{' '}
|
||||
<strong>
|
||||
<TimeAgo timestamp={new Date(params.started_at!).getTime()} />
|
||||
</strong>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
PID:
|
||||
{' '}
|
||||
<strong>
|
||||
<TimeAgo timestamp={startedAt() || 0} />
|
||||
</strong>
|
||||
</Tooltip>
|
||||
<strong>{params.pid || 0}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
PID:
|
||||
{' '}
|
||||
<strong>{pid() || 0}</strong>
|
||||
</p>
|
||||
</div>
|
||||
<FormattedLog log="aaa" />
|
||||
|
||||
<FormattedLog log="aaa" />
|
||||
<div class="flex flex-row items-center justify-end gap-x-2">
|
||||
<Button
|
||||
buttonStyle="danger"
|
||||
children="Kill"
|
||||
iconLeft={<SlashOctagonIcon />}
|
||||
onClick={() => killProcess(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Sidebar.Page>
|
||||
);
|
||||
}
|
||||
|
||||
ClusterGame.buildUrl = function (uuid: string, process_uuid: string): URLSearchParams {
|
||||
return new URLSearchParams({ id: uuid, process_uuid });
|
||||
ClusterGame.buildUrl = function (cluster_id: string, detailed: DetailedProcess): URLSearchParams {
|
||||
return new URLSearchParams({
|
||||
id: cluster_id,
|
||||
process_uuid: detailed.uuid,
|
||||
started_at: detailed.started_at,
|
||||
pid: detailed.pid.toString(),
|
||||
...(detailed.user ? { user: detailed.user } : {}),
|
||||
});
|
||||
};
|
||||
|
||||
export default ClusterGame;
|
||||
|
|
|
|||
|
|
@ -7,12 +7,11 @@ import { join } from 'pathe';
|
|||
import ClusterCover from '../../components/game/ClusterCover';
|
||||
import LoaderIcon from '../../components/game/LoaderIcon';
|
||||
import Button from '../../components/base/Button';
|
||||
import ClusterRoot from './ClusterRoot';
|
||||
import ScrollableContainer from '~ui/components/ScrollableContainer';
|
||||
import Sidebar from '~ui/components/Sidebar';
|
||||
import type { Cluster } from '~bindings';
|
||||
import { secondsToWords, upperFirst } from '~utils';
|
||||
import useClusterContext from '~ui/hooks/useCluster';
|
||||
import useClusterContext, { useLaunchCluster } from '~ui/hooks/useCluster';
|
||||
import { bridge } from '~imports';
|
||||
import TextField from '~ui/components/base/TextField';
|
||||
import SettingsRow from '~ui/components/SettingsRow';
|
||||
|
|
@ -233,11 +232,7 @@ interface BannerProps {
|
|||
}
|
||||
|
||||
function Banner(props: BannerProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
async function launch() {
|
||||
ClusterRoot.launch(navigate, props.cluster.uuid);
|
||||
}
|
||||
const launch = useLaunchCluster(() => props.cluster.uuid);
|
||||
|
||||
async function launchFilePicker() {
|
||||
const selected = await dialog.open({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { type Navigator, Route, useIsRouting, useSearchParams } from '@solidjs/router';
|
||||
import { type ParentProps, createEffect, createResource, createSignal, onCleanup, onMount } from 'solid-js';
|
||||
import { type ParentProps, Show, createEffect, createResource, createSignal, onCleanup, onMount } from 'solid-js';
|
||||
import { EyeIcon, File06Icon, Globe04Icon, Image03Icon, PackagePlusIcon, Settings04Icon } from '@untitled-theme/icons-solid';
|
||||
import type { UnlistenFn } from '@tauri-apps/api/event';
|
||||
import Sidebar from '../../components/Sidebar';
|
||||
|
|
@ -27,7 +27,18 @@ function ClusterRoutes() {
|
|||
<Route path="/screenshots" component={ClusterScreenshots} />
|
||||
<Route path="/worlds" component={ClusterWorlds} />
|
||||
<Route path="/settings" component={ClusterSettings} />
|
||||
<Route path="/game" component={ClusterGame} />
|
||||
<Route
|
||||
path="/game"
|
||||
component={() => {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
return (
|
||||
<Show keyed when={searchParams.process_uuid}>
|
||||
<ClusterGame />
|
||||
</Show>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -58,12 +69,6 @@ ClusterRoot.open = function (navigate: Navigator, uuid: string) {
|
|||
navigate(`/clusters/?id=${uuid}`);
|
||||
};
|
||||
|
||||
ClusterRoot.launch = function (navigate: Navigator, uuid: string) {
|
||||
tryResult(bridge.commands.runCluster, uuid).then(([process_uuid]) => {
|
||||
navigate(`/clusters/game?${ClusterGame.buildUrl(uuid, process_uuid).toString()}`);
|
||||
});
|
||||
};
|
||||
|
||||
function ClusterSidebar() {
|
||||
const [cluster, { refetch: refetchCluster }] = useClusterContext();
|
||||
const [listener, setListener] = createSignal<UnlistenFn>();
|
||||
|
|
@ -72,7 +77,7 @@ function ClusterSidebar() {
|
|||
if (!c || typeof c.path !== 'string')
|
||||
return [];
|
||||
|
||||
return tryResult(bridge.commands.getProcessesByPath, c.path);
|
||||
return tryResult(bridge.commands.getProcessesDetailedByPath, c.path);
|
||||
});
|
||||
|
||||
const isRouting = useIsRouting();
|
||||
|
|
@ -106,13 +111,12 @@ function ClusterSidebar() {
|
|||
[<File06Icon />, 'Logs', '/logs'],
|
||||
[<Settings04Icon />, 'Game Settings', '/settings'],
|
||||
],
|
||||
// TODO: Support multiple running clusters and add used account's avatar as the icon
|
||||
...(runningProcesses() && runningProcesses()!.length > 0
|
||||
? {
|
||||
Running: runningProcesses()!.map((process_uuid, index) => {
|
||||
const icon = <PlayerHead uuid={null} />; // : <Settings01Icon />;
|
||||
Running: runningProcesses()!.map((details, index) => {
|
||||
const icon = <PlayerHead uuid={details.user} />; ;
|
||||
|
||||
return [icon, `Process #${index + 1}`, '/game', ClusterGame.buildUrl(cluster()!.uuid, process_uuid)];
|
||||
return [icon, `Process #${index + 1}`, '/game', ClusterGame.buildUrl(cluster()!.uuid, details)];
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
|
|
|
|||
|
|
@ -69,10 +69,35 @@ function PageSettings(cluster: Accessor<Cluster>) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<GameSettings {...{ fullscreen, memory, resolution }} />
|
||||
<LauncherSettings {...{ hideOnLaunch: undefined }} />
|
||||
<ProcessSettings {...{ preCommand, wrapperCommand, postCommand }} />
|
||||
<JvmSettings {...{ javaArgs, envVars }} />
|
||||
<GameSettings
|
||||
{...{
|
||||
fullscreen,
|
||||
memory,
|
||||
resolution,
|
||||
}}
|
||||
/>
|
||||
|
||||
<LauncherSettings
|
||||
{...{
|
||||
hideOnLaunch: undefined,
|
||||
allowParallelClusters: undefined,
|
||||
}}
|
||||
/>
|
||||
|
||||
<ProcessSettings
|
||||
{...{
|
||||
preCommand,
|
||||
wrapperCommand,
|
||||
postCommand,
|
||||
}}
|
||||
/>
|
||||
|
||||
<JvmSettings
|
||||
{...{
|
||||
javaArgs,
|
||||
envVars,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { CpuChip01Icon, Database01Icon, EyeIcon, FilePlus02Icon, FileX02Icon, LayoutTopIcon, Maximize01Icon, ParagraphWrapIcon, VariableIcon, XIcon } from '@untitled-theme/icons-solid';
|
||||
import { ActivityIcon, CpuChip01Icon, Database01Icon, EyeIcon, FilePlus02Icon, FileX02Icon, LayoutTopIcon, Maximize01Icon, ParagraphWrapIcon, VariableIcon, XIcon } from '@untitled-theme/icons-solid';
|
||||
import { type Accessor, type Setter, Show, createSignal, onMount, splitProps, untrack } from 'solid-js';
|
||||
import type { Memory, Resolution } from '~bindings';
|
||||
import TextField from '~ui/components/base/TextField';
|
||||
|
|
@ -192,12 +192,17 @@ export function GameSettings(props: {
|
|||
|
||||
export function LauncherSettings(props: {
|
||||
hideOnLaunch: CreateSetting<boolean> | undefined;
|
||||
allowParallelClusters: CreateSetting<boolean> | undefined;
|
||||
}) {
|
||||
const shouldShowHeader = () => Object.values(props).some(setting => setting !== undefined);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Show when={props.hideOnLaunch !== undefined}>
|
||||
<Show when={shouldShowHeader()}>
|
||||
<BaseSettingsRow.Header>Launcher</BaseSettingsRow.Header>
|
||||
</Show>
|
||||
|
||||
<Show when={props.hideOnLaunch !== undefined}>
|
||||
<SettingsRow
|
||||
title="Hide On Launch"
|
||||
description="Hide the launcher whenever you start a game."
|
||||
|
|
@ -211,6 +216,21 @@ export function LauncherSettings(props: {
|
|||
/>
|
||||
</SettingsRow>
|
||||
</Show>
|
||||
|
||||
<Show when={props.allowParallelClusters !== undefined}>
|
||||
<SettingsRow
|
||||
title="Allow Parallel Clusters"
|
||||
description="Allow running the same cluster with the same account."
|
||||
icon={<ActivityIcon />}
|
||||
isGlobal={props.allowParallelClusters!.isGlobal}
|
||||
reset={props.allowParallelClusters!.resetToFallback}
|
||||
>
|
||||
<Toggle
|
||||
checked={props.allowParallelClusters!.get}
|
||||
onChecked={props.allowParallelClusters!.set}
|
||||
/>
|
||||
</SettingsRow>
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -335,6 +355,7 @@ function PageSettings() {
|
|||
|
||||
// Launcher
|
||||
const hideOnLaunch = createSetting(settings().hide_on_launch ?? false);
|
||||
const allowParallelClusters = createSetting(settings().allow_parallel_running_clusters ?? false);
|
||||
|
||||
// Process
|
||||
const preCommand = createSetting(settings().init_hooks.pre ?? '');
|
||||
|
|
@ -353,6 +374,7 @@ function PageSettings() {
|
|||
|
||||
// Launcher
|
||||
hide_on_launch: hideOnLaunch.get(),
|
||||
allow_parallel_running_clusters: allowParallelClusters.get(),
|
||||
|
||||
// Process
|
||||
init_hooks: {
|
||||
|
|
@ -368,10 +390,35 @@ function PageSettings() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<GameSettings {...{ fullscreen, memory, resolution }} />
|
||||
<LauncherSettings {...{ hideOnLaunch }} />
|
||||
<ProcessSettings {...{ preCommand, wrapperCommand, postCommand }} />
|
||||
<JvmSettings {...{ javaArgs, envVars }} />
|
||||
<GameSettings
|
||||
{...{
|
||||
fullscreen,
|
||||
memory,
|
||||
resolution,
|
||||
}}
|
||||
/>
|
||||
|
||||
<LauncherSettings
|
||||
{...{
|
||||
hideOnLaunch,
|
||||
allowParallelClusters,
|
||||
}}
|
||||
/>
|
||||
|
||||
<ProcessSettings
|
||||
{...{
|
||||
preCommand,
|
||||
wrapperCommand,
|
||||
postCommand,
|
||||
}}
|
||||
/>
|
||||
|
||||
<JvmSettings
|
||||
{...{
|
||||
javaArgs,
|
||||
envVars,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,98 @@
|
|||
|
||||
pub use crate::store::{Cluster, InitHooks, JavaOptions, Memory, Resolution, Settings, State};
|
||||
use crate::store::{ClusterPath, ProcessorChild};
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(specta::Type, serde::Serialize)]
|
||||
pub struct DetailedProcess {
|
||||
pub uuid: Uuid,
|
||||
pub user: Option<Uuid>,
|
||||
pub started_at: DateTime<Utc>,
|
||||
pub pid: u32,
|
||||
}
|
||||
|
||||
impl DetailedProcess {
|
||||
pub async fn from_processor_child(process: &ProcessorChild) -> Self {
|
||||
let pid = process.current_child.read().await.id().unwrap_or(0);
|
||||
Self {
|
||||
uuid: process.uuid,
|
||||
user: process.user,
|
||||
started_at: process.started_at,
|
||||
pid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// get detailed processes by a [`ClusterPath`].
|
||||
#[tracing::instrument]
|
||||
pub async fn get_process_detailed_by_id(uuid: Uuid) -> crate::Result<DetailedProcess> {
|
||||
let state = State::get().await?;
|
||||
let processor = state.processor.read().await;
|
||||
|
||||
let child = processor.get(uuid).ok_or(anyhow::anyhow!("process not found"))?;
|
||||
let child = child.read().await;
|
||||
let pid = child.current_child.read().await.id().ok_or(anyhow::anyhow!("process not found"))?;
|
||||
Ok(DetailedProcess {
|
||||
uuid,
|
||||
user: child.user,
|
||||
started_at: child.started_at,
|
||||
pid,
|
||||
})
|
||||
}
|
||||
|
||||
/// get detailed processes by a [`ClusterPath`].
|
||||
#[tracing::instrument]
|
||||
pub async fn get_processes_detailed_by_path(path: ClusterPath) -> crate::Result<Vec<DetailedProcess>> {
|
||||
let state = State::get().await?;
|
||||
let processor = state.processor.read().await;
|
||||
|
||||
let uuids = processor.running_cluster(path).await?;
|
||||
let mut processes = Vec::new();
|
||||
for uuid in uuids {
|
||||
let child = processor.get(uuid).ok_or(anyhow::anyhow!("process not found"))?;
|
||||
let child = child.read().await;
|
||||
let pid = child.current_child.read().await.id().ok_or(anyhow::anyhow!("process not found"))?;
|
||||
processes.push(DetailedProcess {
|
||||
uuid,
|
||||
user: child.user,
|
||||
started_at: child.started_at,
|
||||
pid,
|
||||
});
|
||||
}
|
||||
Ok(processes)
|
||||
}
|
||||
|
||||
/// check whether or not a process has completed by its [`Uuid`].
|
||||
#[tracing::instrument]
|
||||
pub async fn uuid_is_finished(uuid: Uuid) -> crate::Result<bool> {
|
||||
Ok(uuid_exit_status(uuid).await?.is_some())
|
||||
}
|
||||
|
||||
/// get the user from a processor by its [`Uuid`].
|
||||
#[tracing::instrument]
|
||||
pub async fn get_user_by_process(uuid: Uuid) -> crate::Result<Option<Uuid>> {
|
||||
let state = State::get().await?;
|
||||
let processor = state.processor.read().await;
|
||||
if let Some(processor) = processor.get(uuid) {
|
||||
Ok(processor.read().await.user)
|
||||
} else {
|
||||
Err(anyhow::anyhow!("process not found").into())
|
||||
}
|
||||
}
|
||||
|
||||
/// get the user from a processor by its [`Uuid`].
|
||||
#[tracing::instrument]
|
||||
pub async fn get_process_started_at(uuid: Uuid) -> crate::Result<DateTime<Utc>> {
|
||||
let state = State::get().await?;
|
||||
let processor = state.processor.read().await;
|
||||
if let Some(processor) = processor.get(uuid) {
|
||||
Ok(processor.read().await.started_at)
|
||||
} else {
|
||||
Err(anyhow::anyhow!("process not found").into())
|
||||
}
|
||||
}
|
||||
|
||||
/// check the exit status of a process by its [`Uuid`].
|
||||
#[tracing::instrument]
|
||||
pub async fn uuid_exit_status(uuid: Uuid) -> crate::Result<Option<i32>> {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use crate::api::proxy::{
|
|||
};
|
||||
use crate::proxy::OfflinePayload;
|
||||
use crate::store::{ClusterPath, IngressProcessType, IngressProcessor};
|
||||
use tauri::Emitter;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
|
|
@ -15,9 +16,6 @@ use tauri_specta::Event;
|
|||
#[cfg(feature = "tauri")]
|
||||
use crate::api::proxy::{ClusterPayload, IngressPayload, MessagePayload, ProcessPayload};
|
||||
|
||||
#[cfg(feature = "tauri")]
|
||||
use tauri::Manager;
|
||||
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn init_ingress(
|
||||
ingress_type: IngressType,
|
||||
|
|
@ -152,12 +150,6 @@ pub async fn send_ingress(
|
|||
},
|
||||
)
|
||||
.map_err(ProxyError::from)?;
|
||||
// let _ = IngressPayload {
|
||||
// fraction: display_conv,
|
||||
// message: message.unwrap_or(&ingress.message).to_string(),
|
||||
// event: ingress.ingress_type.clone(),
|
||||
// ingress_uuid: ingress.ingress_uuid,
|
||||
// }.emit(proxy_state.app.app_handle());
|
||||
|
||||
ingress.last_sent = display;
|
||||
}
|
||||
|
|
@ -169,10 +161,15 @@ pub async fn send_message(message: &str) -> crate::Result<()> {
|
|||
#[cfg(feature = "tauri")]
|
||||
{
|
||||
let proxy_state = crate::ProxyState::get().await?;
|
||||
MessagePayload {
|
||||
message: message.to_string(),
|
||||
}
|
||||
.emit(proxy_state.app.app_handle())?;
|
||||
proxy_state
|
||||
.app
|
||||
.emit(
|
||||
MessagePayload::NAME,
|
||||
MessagePayload {
|
||||
message: message.to_string(),
|
||||
},
|
||||
)
|
||||
.map_err(ProxyError::from)?;
|
||||
}
|
||||
|
||||
tracing::warn!("{}", message);
|
||||
|
|
@ -183,11 +180,15 @@ pub async fn send_offline(offline: bool) -> crate::Result<()> {
|
|||
#[cfg(feature = "tauri")]
|
||||
{
|
||||
let proxy_state = crate::ProxyState::get().await?;
|
||||
// proxy_state
|
||||
// .app
|
||||
// .emit("offline", offline)
|
||||
// .map_err(ProxyError::from)?;
|
||||
OfflinePayload { offline }.emit(proxy_state.app.app_handle())?;
|
||||
proxy_state
|
||||
.app
|
||||
.emit(
|
||||
OfflinePayload::NAME,
|
||||
OfflinePayload {
|
||||
offline,
|
||||
},
|
||||
)
|
||||
.map_err(ProxyError::from)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
@ -198,11 +199,13 @@ pub async fn send_internet(internet: InternetPayload) -> crate::Result<()> {
|
|||
#[cfg(feature = "tauri")]
|
||||
{
|
||||
let proxy_state = crate::ProxyState::get().await?;
|
||||
// proxy_state
|
||||
// .app
|
||||
// .emit("internet", internet)
|
||||
// .map_err(ProxyError::from)?;
|
||||
internet.emit(proxy_state.app.app_handle())?;
|
||||
proxy_state
|
||||
.app
|
||||
.emit(
|
||||
InternetPayload::NAME,
|
||||
internet
|
||||
)
|
||||
.map_err(ProxyError::from)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
@ -217,25 +220,18 @@ pub async fn send_process(
|
|||
#[cfg(feature = "tauri")]
|
||||
{
|
||||
let proxy_state = crate::ProxyState::get().await?;
|
||||
// proxy_state
|
||||
// .app
|
||||
// .emit(
|
||||
// "process",
|
||||
// ProcessPayload {
|
||||
// uuid,
|
||||
// pid,
|
||||
// event,
|
||||
// message: message.to_string(),
|
||||
// },
|
||||
// )
|
||||
// .map_err(ProxyError::from)?;
|
||||
ProcessPayload {
|
||||
uuid,
|
||||
pid,
|
||||
event,
|
||||
message: message.to_string(),
|
||||
}
|
||||
.emit(proxy_state.app.app_handle())?;
|
||||
proxy_state
|
||||
.app
|
||||
.emit(
|
||||
ProcessPayload::NAME,
|
||||
ProcessPayload {
|
||||
uuid,
|
||||
pid,
|
||||
event,
|
||||
message: message.to_string(),
|
||||
},
|
||||
)
|
||||
.map_err(ProxyError::from)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
@ -251,27 +247,19 @@ pub async fn send_cluster(
|
|||
{
|
||||
let path = cluster_path.full_path().await?;
|
||||
let proxy_state = crate::ProxyState::get().await?;
|
||||
// proxy_state
|
||||
// .app
|
||||
// .emit(
|
||||
// "cluster",
|
||||
// ClusterPayload {
|
||||
// uuid,
|
||||
// cluster_path: cluster_path.clone(),
|
||||
// path,
|
||||
// name: name.to_string(),
|
||||
// event,
|
||||
// },
|
||||
// )
|
||||
// .map_err(ProxyError::from)?;
|
||||
ClusterPayload {
|
||||
uuid,
|
||||
cluster_path: cluster_path.clone(),
|
||||
path,
|
||||
name: name.to_string(),
|
||||
event,
|
||||
}
|
||||
.emit(proxy_state.app.app_handle())?;
|
||||
proxy_state
|
||||
.app
|
||||
.emit(
|
||||
ClusterPayload::NAME,
|
||||
ClusterPayload {
|
||||
uuid,
|
||||
cluster_path: cluster_path.clone(),
|
||||
path,
|
||||
name: name.to_string(),
|
||||
event,
|
||||
},
|
||||
)
|
||||
.map_err(ProxyError::from)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -317,13 +317,15 @@ pub async fn launch_minecraft(
|
|||
None => Command::new(&java_version.path),
|
||||
};
|
||||
let env_args = Vec::from(env_args);
|
||||
let existing = processor::get_uuids_by_cluster_path(cluster.cluster_path()).await?;
|
||||
if let Some(uuid) = existing.first() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"cluster {} is already running ({uuid})",
|
||||
cluster.cluster_path()
|
||||
)
|
||||
.into());
|
||||
if !state.settings.read().await.allow_parallel_running_clusters {
|
||||
let existing = processor::get_uuids_by_cluster_path(cluster.cluster_path()).await?;
|
||||
if let Some(uuid) = existing.first() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"cluster {} is already running ({uuid})",
|
||||
cluster.cluster_path()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
command
|
||||
|
|
@ -462,6 +464,7 @@ pub async fn launch_minecraft(
|
|||
command,
|
||||
post_hook,
|
||||
censors,
|
||||
Some(credentials.id)
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//! Handles child processes in a unified API
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -119,6 +119,9 @@ pub struct ProcessorCache {
|
|||
pub cluster_path: ClusterPath,
|
||||
/// A post hook to be run once a process has completed.
|
||||
pub post: Option<String>,
|
||||
/// The [`Uuid`] of the Minecraft account it was started by.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl ChildType {
|
||||
|
|
@ -136,6 +139,7 @@ impl ChildType {
|
|||
uuid: Uuid,
|
||||
cluster_path: ClusterPath,
|
||||
post: Option<String>,
|
||||
user: Option<Uuid>,
|
||||
) -> crate::Result<()> {
|
||||
let pid = match self {
|
||||
ChildType::ChildProcess(child) => child.id().unwrap_or(0),
|
||||
|
|
@ -164,6 +168,7 @@ impl ChildType {
|
|||
exe,
|
||||
cluster_path,
|
||||
post,
|
||||
user
|
||||
};
|
||||
|
||||
let path = state.directories.caches_dir().await.join(PROCESSOR_FILE);
|
||||
|
|
@ -264,6 +269,10 @@ pub struct ProcessorChild {
|
|||
pub current_child: Arc<RwLock<ChildType>>,
|
||||
/// When this process was last updated in [`Utc`].
|
||||
pub last_updated: DateTime<Utc>,
|
||||
/// When this process was last updated in [`Utc`].
|
||||
pub started_at: DateTime<Utc>,
|
||||
/// What [`Uuid`] this process is running under.
|
||||
pub user: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl Processor {
|
||||
|
|
@ -422,6 +431,7 @@ impl Processor {
|
|||
mut command: Command,
|
||||
post: Option<String>,
|
||||
censors: HashMap<String, String>,
|
||||
user: Option<Uuid>,
|
||||
) -> crate::Result<Arc<RwLock<ProcessorChild>>> {
|
||||
let proc = command.spawn().map_err(IOError::from)?;
|
||||
let process = ChildType::ChildProcess(proc);
|
||||
|
|
@ -429,7 +439,7 @@ impl Processor {
|
|||
.id()
|
||||
.ok_or_else(|| anyhow::anyhow!("process failed, couldn't get pid"))?;
|
||||
process
|
||||
.cache(uuid, cluster_path.clone(), post.clone())
|
||||
.cache(uuid, cluster_path.clone(), post.clone(), user)
|
||||
.await?;
|
||||
|
||||
let current_child = Arc::new(RwLock::new(process));
|
||||
|
|
@ -449,6 +459,8 @@ impl Processor {
|
|||
current_child,
|
||||
manager,
|
||||
last_updated,
|
||||
started_at: Utc::now(),
|
||||
user,
|
||||
};
|
||||
let child = Arc::new(RwLock::new(child));
|
||||
self.0.insert(uuid, child.clone());
|
||||
|
|
@ -511,7 +523,7 @@ impl Processor {
|
|||
.id()
|
||||
.ok_or_else(|| anyhow::anyhow!("process failed, couldnt get pid"))?;
|
||||
process
|
||||
.cache(cache.uuid, cache.cluster_path.clone(), cache.post.clone())
|
||||
.cache(cache.uuid, cache.cluster_path.clone(), cache.post.clone(), cache.user)
|
||||
.await?;
|
||||
let current_child = Arc::new(RwLock::new(process));
|
||||
let manager = Some(tokio::spawn(Self::manager(
|
||||
|
|
@ -534,9 +546,11 @@ impl Processor {
|
|||
let child = ProcessorChild {
|
||||
uuid: cache.uuid,
|
||||
cluster_path: cache.cluster_path,
|
||||
started_at: Utc.timestamp_opt(cache.start_time as i64, 0).single().ok_or(anyhow::anyhow!("couldn't convert processor cache timestamp to Utc"))?, // TODO: Look into this
|
||||
current_child,
|
||||
manager,
|
||||
last_updated,
|
||||
user: cache.user,
|
||||
};
|
||||
|
||||
let child = Arc::new(RwLock::new(child));
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ pub struct Settings {
|
|||
/// A global fullscreen Minecraft state.
|
||||
#[serde(default)]
|
||||
pub force_fullscreen: bool,
|
||||
/// Whether to allow launching the same cluster under the same account.
|
||||
#[serde(default)]
|
||||
pub allow_parallel_running_clusters: bool,
|
||||
/// A global default [`Resolution`] for Minecraft.
|
||||
pub resolution: Resolution,
|
||||
/// A global [`JavaVersions`] list and default version.
|
||||
|
|
@ -95,6 +98,7 @@ impl Settings {
|
|||
hide_close_prompt: true,
|
||||
disable_animations: false,
|
||||
force_fullscreen: false,
|
||||
allow_parallel_running_clusters: false,
|
||||
resolution: Resolution::default(),
|
||||
java_versions: JavaVersions::new(),
|
||||
memory: Memory::default(),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue