From 08528ec7c1762931a505a7d21a8004c078eeab32 Mon Sep 17 00:00:00 2001 From: LynithDev <61880709+LynithDev@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:03:04 +0200 Subject: [PATCH 1/4] feat: Better ClusterGame page + Added multi-cluster process support --- apps/desktop/src/api/commands/cluster.rs | 16 +-- apps/desktop/src/api/commands/mod.rs | 4 + apps/desktop/src/api/commands/processor.rs | 27 ++++- apps/frontend/src/ui/components/Sidebar.tsx | 8 +- .../ui/components/content/FormattedLog.tsx | 2 +- .../src/ui/components/overlay/Modal.tsx | 38 +++++- .../overlay/cluster/ClusterGameSetup.tsx | 20 ++- apps/frontend/src/ui/hooks/useCluster.tsx | 35 +++++- apps/frontend/src/ui/pages/Home.tsx | 8 +- .../src/ui/pages/cluster/ClusterGame.tsx | 102 ++++++++++------ .../src/ui/pages/cluster/ClusterOverview.tsx | 9 +- .../src/ui/pages/cluster/ClusterRoot.tsx | 30 +++-- .../src/ui/pages/cluster/ClusterSettings.tsx | 33 ++++- .../pages/settings/game/SettingsMinecraft.tsx | 59 ++++++++- packages/core/src/api/processor.rs | 84 +++++++++++++ packages/core/src/api/proxy/send.rs | 114 ++++++++---------- packages/core/src/game/mod.rs | 17 +-- packages/core/src/store/processor.rs | 20 ++- packages/core/src/store/settings.rs | 4 + 19 files changed, 455 insertions(+), 175 deletions(-) diff --git a/apps/desktop/src/api/commands/cluster.rs b/apps/desktop/src/api/commands/cluster.rs index 446b2e85..e2cb9bba 100644 --- a/apps/desktop/src/api/commands/cluster.rs +++ b/apps/desktop/src/api/commands/cluster.rs @@ -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 { 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] diff --git a/apps/desktop/src/api/commands/mod.rs b/apps/desktop/src/api/commands/mod.rs index 692b6e82..04738b46 100644 --- a/apps/desktop/src/api/commands/mod.rs +++ b/apps/desktop/src/api/commands/mod.rs @@ -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, diff --git a/apps/desktop/src/api/commands/processor.rs b/apps/desktop/src/api/commands/processor.rs index f7816308..e1017118 100644 --- a/apps/desktop/src/api/commands/processor.rs +++ b/apps/desktop/src/api/commands/processor.rs @@ -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, 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, 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 { + Ok(processor::get_process_detailed_by_id(uuid).await?) +} + +#[specta::specta] +#[tauri::command] +pub async fn get_user_by_process(uuid: Uuid) -> Result, String> { + Ok(processor::get_user_by_process(uuid).await?) +} + +#[specta::specta] +#[tauri::command] +pub async fn get_process_started_at(uuid: Uuid) -> Result, String> { + Ok(processor::get_process_started_at(uuid).await?) +} + #[specta::specta] #[tauri::command] pub async fn get_pid_by_uuid(uuid: Uuid) -> Result { diff --git a/apps/frontend/src/ui/components/Sidebar.tsx b/apps/frontend/src/ui/components/Sidebar.tsx index 44ed8189..7c986a69 100644 --- a/apps/frontend/src/ui/components/Sidebar.tsx +++ b/apps/frontend/src/ui/components/Sidebar.tsx @@ -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) { 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' : ''}` } > diff --git a/apps/frontend/src/ui/components/content/FormattedLog.tsx b/apps/frontend/src/ui/components/content/FormattedLog.tsx index 11ae06f5..13493be4 100644 --- a/apps/frontend/src/ui/components/content/FormattedLog.tsx +++ b/apps/frontend/src/ui/components/content/FormattedLog.tsx @@ -10,7 +10,7 @@ function FormattedLog(props: FormattedLogProps) { const lines = createMemo(() => props.log.split('\n')); return ( -
+
diff --git a/apps/frontend/src/ui/components/overlay/Modal.tsx b/apps/frontend/src/ui/components/overlay/Modal.tsx index a3cc16a0..48a47e62 100644 --- a/apps/frontend/src/ui/components/overlay/Modal.tsx +++ b/apps/frontend/src/ui/components/overlay/Modal.tsx @@ -164,14 +164,48 @@ Modal.Simple = function (props: ModalSimpleProps) { ); }; +type ModalErrorProps = MakeOptional & { + message?: string | undefined; +}; + +Modal.Error = function (props: ModalErrorProps) { + const [split, rest] = splitProps(props, ['message', 'title', 'buttons', 'children']); + + return ( + , + ]} + > +
+ {split.children || ( + Something went wrong. + )} + {split.message && ( + + {split.message} + + )} +
+
+ ); +}; + type ModalDeleteProps = MakeOptional & { 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(undefined); @@ -235,7 +269,7 @@ Modal.Delete = function (props: ModalDeleteProps) { />,
)} diff --git a/apps/frontend/src/ui/hooks/useCluster.tsx b/apps/frontend/src/ui/hooks/useCluster.tsx index a0e554d5..08717707 100644 --- a/apps/frontend/src/ui/hooks/useCluster.tsx +++ b/apps/frontend/src/ui/hooks/useCluster.tsx @@ -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>; @@ -35,4 +38,32 @@ export function useClusterContext() { return context; } +export function useLaunchCluster(uuid: string | Accessor | (() => string | undefined) | undefined) { + const navigate = useNavigate(); + const [error, setError] = createSignal(undefined); + + const getUuid = () => typeof uuid === 'function' ? uuid() : uuid; + + const modal = createModal(props => ( + + )); + + 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; diff --git a/apps/frontend/src/ui/pages/Home.tsx b/apps/frontend/src/ui/pages/Home.tsx index 1f666ffb..c012ecde 100644 --- a/apps/frontend/src/ui/pages/Home.tsx +++ b/apps/frontend/src/ui/pages/Home.tsx @@ -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; @@ -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(); + const launch = useLaunchCluster(() => cluster()?.uuid); const navigate = useNavigate(); createEffect(() => { @@ -259,7 +261,7 @@ function Banner(props: BannerProps) { buttonStyle="primary" iconLeft={} children={`Launch ${cluster()!.meta.mc_version}`} - onClick={() => ClusterRoot.launch(navigate, cluster()!.uuid)} + onClick={launch} />
+ ); } -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; diff --git a/apps/frontend/src/ui/pages/cluster/ClusterOverview.tsx b/apps/frontend/src/ui/pages/cluster/ClusterOverview.tsx index da62c274..97a8de1d 100644 --- a/apps/frontend/src/ui/pages/cluster/ClusterOverview.tsx +++ b/apps/frontend/src/ui/pages/cluster/ClusterOverview.tsx @@ -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({ diff --git a/apps/frontend/src/ui/pages/cluster/ClusterRoot.tsx b/apps/frontend/src/ui/pages/cluster/ClusterRoot.tsx index 1eff52b5..ef342291 100644 --- a/apps/frontend/src/ui/pages/cluster/ClusterRoot.tsx +++ b/apps/frontend/src/ui/pages/cluster/ClusterRoot.tsx @@ -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() { - + { + const [searchParams] = useSearchParams(); + + return ( + + + + ); + }} + /> ); } @@ -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(); @@ -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() { [, 'Logs', '/logs'], [, '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 = ; // : ; + Running: runningProcesses()!.map((details, index) => { + const icon = ; ; - return [icon, `Process #${index + 1}`, '/game', ClusterGame.buildUrl(cluster()!.uuid, process_uuid)]; + return [icon, `Process #${index + 1}`, '/game', ClusterGame.buildUrl(cluster()!.uuid, details)]; }), } : {}), diff --git a/apps/frontend/src/ui/pages/cluster/ClusterSettings.tsx b/apps/frontend/src/ui/pages/cluster/ClusterSettings.tsx index 7a12f8f5..e2259bd4 100644 --- a/apps/frontend/src/ui/pages/cluster/ClusterSettings.tsx +++ b/apps/frontend/src/ui/pages/cluster/ClusterSettings.tsx @@ -69,10 +69,35 @@ function PageSettings(cluster: Accessor) { return ( <> - - - - + + + + + + + ); } diff --git a/apps/frontend/src/ui/pages/settings/game/SettingsMinecraft.tsx b/apps/frontend/src/ui/pages/settings/game/SettingsMinecraft.tsx index 371a8cb8..dc1c7273 100644 --- a/apps/frontend/src/ui/pages/settings/game/SettingsMinecraft.tsx +++ b/apps/frontend/src/ui/pages/settings/game/SettingsMinecraft.tsx @@ -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 | undefined; + allowParallelClusters: CreateSetting | undefined; }) { + const shouldShowHeader = () => Object.values(props).some(setting => setting !== undefined); + return ( <> - + Launcher + + + + + } + isGlobal={props.allowParallelClusters!.isGlobal} + reset={props.allowParallelClusters!.resetToFallback} + > + + + ); } @@ -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 ( <> - - - - + + + + + + + ); } diff --git a/packages/core/src/api/processor.rs b/packages/core/src/api/processor.rs index c1380e34..3f731492 100644 --- a/packages/core/src/api/processor.rs +++ b/packages/core/src/api/processor.rs @@ -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, + pub started_at: DateTime, + 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 { + 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> { + 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 { 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> { + 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> { + 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> { diff --git a/packages/core/src/api/proxy/send.rs b/packages/core/src/api/proxy/send.rs index f704b916..db463901 100644 --- a/packages/core/src/api/proxy/send.rs +++ b/packages/core/src/api/proxy/send.rs @@ -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(()) diff --git a/packages/core/src/game/mod.rs b/packages/core/src/game/mod.rs index 07bb15a4..65d8e3cd 100644 --- a/packages/core/src/game/mod.rs +++ b/packages/core/src/game/mod.rs @@ -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 } diff --git a/packages/core/src/store/processor.rs b/packages/core/src/store/processor.rs index 2aaaa43e..a3c0bb98 100644 --- a/packages/core/src/store/processor.rs +++ b/packages/core/src/store/processor.rs @@ -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, + /// The [`Uuid`] of the Minecraft account it was started by. + #[serde(skip_serializing_if = "Option::is_none")] + pub user: Option, } impl ChildType { @@ -136,6 +139,7 @@ impl ChildType { uuid: Uuid, cluster_path: ClusterPath, post: Option, + user: Option, ) -> 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>, /// When this process was last updated in [`Utc`]. pub last_updated: DateTime, + /// When this process was last updated in [`Utc`]. + pub started_at: DateTime, + /// What [`Uuid`] this process is running under. + pub user: Option, } impl Processor { @@ -422,6 +431,7 @@ impl Processor { mut command: Command, post: Option, censors: HashMap, + user: Option, ) -> crate::Result>> { 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)); diff --git a/packages/core/src/store/settings.rs b/packages/core/src/store/settings.rs index 0c72c6da..5bba16f7 100644 --- a/packages/core/src/store/settings.rs +++ b/packages/core/src/store/settings.rs @@ -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(), From 6dcf123810c176cd8d4bc9fcc5d1782407545639 Mon Sep 17 00:00:00 2001 From: LynithDev <61880709+LynithDev@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:24:17 +0200 Subject: [PATCH 2/4] feat: ClusterGame supports stdout + stderr feat: FormattedLog auto scrolling refactor: Refactored some @ts-expect-error comments --- apps/frontend/src/bindings.ts | 1506 ++++++++--------- .../src/ui/components/base/Button.tsx | 4 +- .../src/ui/components/base/SelectList.tsx | 5 +- .../ui/components/base/Tooltip.module.scss | 4 +- .../ui/components/content/FormattedLog.tsx | 85 +- .../src/ui/components/game/ClusterCover.tsx | 5 +- .../src/ui/pages/cluster/ClusterGame.tsx | 53 +- .../src/ui/pages/cluster/ClusterRoot.tsx | 5 +- .../pages/settings/game/SettingsMinecraft.tsx | 11 +- apps/frontend/vite.config.ts | 2 +- packages/core/src/api/proxy/mod.rs | 1 + packages/core/src/store/processor.rs | 40 +- 12 files changed, 892 insertions(+), 829 deletions(-) diff --git a/apps/frontend/src/bindings.ts b/apps/frontend/src/bindings.ts index 6ed78739..8bb62592 100644 --- a/apps/frontend/src/bindings.ts +++ b/apps/frontend/src/bindings.ts @@ -1,875 +1,839 @@ + // This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. -/** user-defined commands */ +/** user-defined commands **/ -/** tauri-specta globals */ - -import { - invoke as TAURI_INVOKE, -} from '@tauri-apps/api/core'; -import * as TAURI_API_EVENT from '@tauri-apps/api/event'; -import type { WebviewWindow as __WebviewWindow__ } from '@tauri-apps/api/webviewWindow'; export const commands = { - async authLogin(): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('auth_login') }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async getUsers(): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('get_users') }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async getUser(uuid: string): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('get_user', { uuid }) }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async getDefaultUser(fallback: boolean | null): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('get_default_user', { fallback }) }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async setDefaultUser(uuid: string | null): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('set_default_user', { uuid }) }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async removeUser(uuid: string): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('remove_user', { uuid }) }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async createCluster(props: CreateCluster): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('create_cluster', { props }) }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - /** - * Updates the cluster with the given UUID. The cluster only updates game setting fields - */ - async editGameSettings(uuid: string, newCluster: Cluster): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('edit_game_settings', { uuid, newCluster }) }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async editClusterMeta(uuid: string, name: string | null, iconPath: string | null): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('edit_cluster_meta', { uuid, name, iconPath }) }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async removeCluster(uuid: string): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('remove_cluster', { uuid }) }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async getCluster(uuid: string): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('get_cluster', { uuid }) }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async getClusters(): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('get_clusters') }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async getClustersGrouped(): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('get_clusters_grouped') }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async runCluster(uuid: string): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('run_cluster', { uuid }) }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async getClusterLogs(uuid: string): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('get_cluster_logs', { uuid }) }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async getClusterLog(uuid: string, logName: string): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('get_cluster_log', { uuid, logName }) }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async uploadLog(uuid: string, logName: string): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('upload_log', { uuid, logName }) }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async getScreenshots(uuid: string): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('get_screenshots', { uuid }) }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async getWorlds(uuid: string): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('get_worlds', { uuid }) }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async getRunningClusters(): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('get_running_clusters') }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async getProcessesByPath(path: ClusterPath): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('get_processes_by_path', { path }) }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async killProcess(uuid: string): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('kill_process', { uuid }) }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async isClusterRunning(uuid: string): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('is_cluster_running', { uuid }) }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async getPidByUuid(uuid: string): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('get_pid_by_uuid', { uuid }) }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async getSettings(): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('get_settings') }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async setSettings(settings: Settings): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('set_settings', { settings }) }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async getMinecraftVersions(): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('get_minecraft_versions') }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async randomMods(): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('random_mods') }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async getMod(projectId: string): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('get_mod', { projectId }) }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async downloadMod(clusterId: string, versionId: string): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('download_mod', { clusterId, versionId }) }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async checkForUpdate(): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('check_for_update') }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, - async installUpdate(): Promise> { - try { - return { status: 'ok', data: await TAURI_INVOKE('install_update') }; - } - catch (e) { - if (e instanceof Error) - throw e; - else return { status: 'error', error: e as any }; - } - }, -}; +async authLogin() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("auth_login") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getUsers() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_users") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getUser(uuid: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_user", { uuid }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getDefaultUser(fallback: boolean | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_default_user", { fallback }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async setDefaultUser(uuid: string | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("set_default_user", { uuid }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async removeUser(uuid: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("remove_user", { uuid }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async createCluster(props: CreateCluster) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("create_cluster", { props }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Updates the cluster with the given UUID. The cluster only updates game setting fields + */ +async editGameSettings(uuid: string, newCluster: Cluster) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("edit_game_settings", { uuid, newCluster }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async editClusterMeta(uuid: string, name: string | null, iconPath: string | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("edit_cluster_meta", { uuid, name, iconPath }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async removeCluster(uuid: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("remove_cluster", { uuid }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getCluster(uuid: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_cluster", { uuid }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getClusters() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_clusters") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getClustersGrouped() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_clusters_grouped") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async runCluster(uuid: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("run_cluster", { uuid }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getClusterLogs(uuid: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_cluster_logs", { uuid }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getClusterLog(uuid: string, logName: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_cluster_log", { uuid, logName }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async uploadLog(uuid: string, logName: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("upload_log", { uuid, logName }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getScreenshots(uuid: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_screenshots", { uuid }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getWorlds(uuid: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_worlds", { uuid }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getRunningClusters() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_running_clusters") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getProcessesByPath(path: ClusterPath) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_processes_by_path", { path }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async killProcess(uuid: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("kill_process", { uuid }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async isClusterRunning(uuid: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("is_cluster_running", { uuid }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getPidByUuid(uuid: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_pid_by_uuid", { uuid }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getUserByProcess(uuid: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_user_by_process", { uuid }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getProcessStartedAt(uuid: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_process_started_at", { uuid }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getProcessesDetailedByPath(path: ClusterPath) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_processes_detailed_by_path", { path }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getProcessDetailedById(uuid: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_process_detailed_by_id", { uuid }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getSettings() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_settings") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async setSettings(settings: Settings) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("set_settings", { settings }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getMinecraftVersions() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_minecraft_versions") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async randomMods() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("random_mods") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getMod(projectId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_mod", { projectId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async downloadMod(clusterId: string, versionId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("download_mod", { clusterId, versionId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async checkForUpdate() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("check_for_update") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async installUpdate() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("install_update") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +} +} + +/** user-defined events **/ -/** user-defined events */ export const events = __makeEvents__<{ - clusterPayload: ClusterPayload; - ingressPayload: IngressPayload; - internetPayload: InternetPayload; - messagePayload: MessagePayload; - offlinePayload: OfflinePayload; - processPayload: ProcessPayload; +clusterPayload: ClusterPayload, +ingressPayload: IngressPayload, +internetPayload: InternetPayload, +messagePayload: MessagePayload, +offlinePayload: OfflinePayload, +processPayload: ProcessPayload }>({ - clusterPayload: 'cluster-payload', - ingressPayload: 'ingress-payload', - internetPayload: 'internet-payload', - messagePayload: 'message-payload', - offlinePayload: 'offline-payload', - processPayload: 'process-payload', -}); +clusterPayload: "cluster-payload", +ingressPayload: "ingress-payload", +internetPayload: "internet-payload", +messagePayload: "message-payload", +offlinePayload: "offline-payload", +processPayload: "process-payload" +}) -/** user-defined constants */ +/** user-defined constants **/ -export const PROGRAM_INFO = { arch: '64', dev_build: true, launcher_version: '1.0.0-alpha.1', platform: 'osx', tauri_version: '2.0.0-rc.0', webview_version: '19618.3.11.11.5' } as const; +export const PROGRAM_INFO = {"arch":"64","dev_build":true,"launcher_version":"1.0.0-alpha.1","platform":"linux","tauri_version":"2.0.0-rc.0","webview_version":"2.44.2"} as const; -/** user-defined types */ +/** user-defined types **/ /** * Represents a single Instance and installation of Minecraft * Contains settings and identifiers on a per-Cluster basis, falling back to default settings for Options<> */ -export interface Cluster { - /** - * The Uuid of a specific cluster. - */ - uuid: string; - /** - * The download stage that of a cluster. - */ - stage?: ClusterStage; - /** - * The core path of the cluster. - */ - path?: string; - /** - * The associated cluster metadata. - */ - meta: ClusterMeta; - /** - * The per-cluster JVM memory allocation options. - */ - memory?: Memory | null; - /** - * The per-cluster JVM options. - */ - java?: JavaOptions | null; - /** - * The per-cluster Minecraft window default resolution. - */ - resolution?: Resolution | null; - /** - * The per-cluster Minecraft window fullscreen status. - */ - force_fullscreen: boolean | null; - init_hooks?: InitHooks | null; - packages: { [key in PackagePath]: Package }; - update?: string | null; -}; +export type Cluster = { +/** + * The Uuid of a specific cluster. + */ +uuid: string; +/** + * The download stage that of a cluster. + */ +stage?: ClusterStage; +/** + * The core path of the cluster. + */ +path?: string; +/** + * The associated cluster metadata. + */ +meta: ClusterMeta; +/** + * The per-cluster JVM memory allocation options. + */ +memory?: Memory | null; +/** + * The per-cluster JVM options. + */ +java?: JavaOptions | null; +/** + * The per-cluster Minecraft window default resolution. + */ +resolution?: Resolution | null; +/** + * The per-cluster Minecraft window fullscreen status. + */ +force_fullscreen: boolean | null; init_hooks?: InitHooks | null; packages: { [key in PackagePath]: Package }; update?: string | null } /** * Represents core Cluster metadata ([`Cluster#meta`]). */ -export interface ClusterMeta { - /** - * The user-facing name of the cluster stored as a string. - */ - name: string; - /** - * The user-facing group of the cluster stored as a string. - */ - group?: string | null; - /** - * The associated Minecraft version of the cluster as last updated. - */ - mc_version: string; - /** - * The associated mod [`Loader`] as specified in the cluster. - */ - loader?: Loader; - /** - * The associated mod [`LoaderVersion`] if available. - */ - loader_version?: LoaderVersion | null; - /** - * The time that the cluster was created in [`DateTime`]. - */ - created_at?: string; - /** - * The time the cluster was last modified in [`DateTime`]. - */ - modified_at?: string; - /** - * The last time the cluster was played in [`DateTime`]. - * (Defaults to None if the cluster has never been played) - */ - played_at?: string | null; - /** - * The overall time played in seconds stored as a [`u64`]. - */ - overall_played?: bigint; - /** - * The recent time played stored as a [`u64`]. - */ - recently_played?: bigint; - /** - * The associated [`PackageData`] and modpack data for the cluster. - */ - package_data?: PackageData | null; - /** - * The user-facing cluster icon as a [`PathBuf`]. - */ - icon?: string | null; - /** - * The user-facing cluster icon as a URL string. - */ - icon_url?: string | null; -}; +export type ClusterMeta = { +/** + * The user-facing name of the cluster stored as a string. + */ +name: string; +/** + * The user-facing group of the cluster stored as a string. + */ +group?: string | null; +/** + * The associated Minecraft version of the cluster as last updated. + */ +mc_version: string; +/** + * The associated mod [`Loader`] as specified in the cluster. + */ +loader?: Loader; +/** + * The associated mod [`LoaderVersion`] if available. + */ +loader_version?: LoaderVersion | null; +/** + * The time that the cluster was created in [`DateTime`]. + */ +created_at?: string; +/** + * The time the cluster was last modified in [`DateTime`]. + */ +modified_at?: string; +/** + * The last time the cluster was played in [`DateTime`]. + * (Defaults to None if the cluster has never been played) + */ +played_at?: string | null; +/** + * The overall time played in seconds stored as a [`u64`]. + */ +overall_played?: bigint; +/** + * The recent time played stored as a [`u64`]. + */ +recently_played?: bigint; +/** + * The associated [`PackageData`] and modpack data for the cluster. + */ +package_data?: PackageData | null; +/** + * The user-facing cluster icon as a [`PathBuf`]. + */ +icon?: string | null; +/** + * The user-facing cluster icon as a URL string. + */ +icon_url?: string | null } /** * Relative Path wrapper to be used as an identifer for a cluster path. */ -export type ClusterPath = string; -export interface ClusterPayload { uuid: string; cluster_path: ClusterPath; path: string; name: string; event: ClusterPayloadType }; -export type ClusterPayloadType = 'created' | 'inserted' | 'synced' | 'edited' | 'deleted'; +export type ClusterPath = string +export type ClusterPayload = { uuid: string; cluster_path: ClusterPath; path: string; name: string; event: ClusterPayloadType } +export type ClusterPayloadType = "created" | "inserted" | "synced" | "edited" | "deleted" /** * Core Cluster stages used in package logic. */ -export type ClusterStage = - /** - * Installed with no downloads left. - */ - 'installed' | - /** - * Downloading any sort of core metadata or mod file. - */ - 'downloading' | - /** - * Downloading a full pack file, which is granted lower priority due to the high network demand. - */ - 'pack_downloading' | - /** - * Not installed at all. - */ - 'not_installed'; -export interface CreateCluster { name: string; mc_version: string; mod_loader: Loader; loader_version: string | null; icon: string | null; icon_url: string | null; package_data: PackageData | null; skip: boolean | null; skip_watch: boolean | null }; -export interface IngressPayload { event: IngressType; ingress_uuid: string; fraction: number | null; message: string }; -export type IngressType = { type: 'initialize' } | { type: 'download_java'; version: number } | { type: 'download_loader'; cluster_path: string; cluster_name: string } | { type: 'sync_cluster'; cluster_path: string; cluster_name: string } | { type: 'copy_cluster'; import: string; cluster_name: string } | { type: 'sync_config'; new_path: string } | { type: 'archival'; cluster_path: string; cluster_name: string } | { type: 'download_package'; cluster_path: string; package_name: string; icon: string | null; package_id: string | null; package_version: string | null } | { type: 'download_pack'; cluster_path: string; package_name: string; icon: string | null; package_version: string }; +export type ClusterStage = +/** + * Installed with no downloads left. + */ +"installed" | +/** + * Downloading any sort of core metadata or mod file. + */ +"downloading" | +/** + * Downloading a full pack file, which is granted lower priority due to the high network demand. + */ +"pack_downloading" | +/** + * Not installed at all. + */ +"not_installed" +export type CreateCluster = { name: string; mc_version: string; mod_loader: Loader; loader_version: string | null; icon: string | null; icon_url: string | null; package_data: PackageData | null; skip: boolean | null; skip_watch: boolean | null } +export type DetailedProcess = { uuid: string; user: string | null; started_at: string; pid: number } +export type IngressPayload = { event: IngressType; ingress_uuid: string; fraction: number | null; message: string } +export type IngressType = { type: "initialize" } | { type: "download_java"; version: number } | { type: "download_loader"; cluster_path: string; cluster_name: string } | { type: "sync_cluster"; cluster_path: string; cluster_name: string } | { type: "copy_cluster"; import: string; cluster_name: string } | { type: "sync_config"; new_path: string } | { type: "archival"; cluster_path: string; cluster_name: string } | { type: "download_package"; cluster_path: string; package_name: string; icon: string | null; package_id: string | null; package_version: string | null } | { type: "download_pack"; cluster_path: string; package_name: string; icon: string | null; package_version: string } /** * Global initialization hooks for all Minecraft clusters. */ -export interface InitHooks { - /** - * Pre-launch hook. - */ - pre?: string | null; - /** - * Wrapper hook for the runtime of the game instance. - */ - wrapper?: string | null; - /** - * Post-launch hook. - */ - post?: string | null; -}; -export type InternetPayload = { event: 'InstallPackage'; id: string } | { event: 'InstallPack'; id: string } | { event: 'InstallPath'; path: string }; +export type InitHooks = { +/** + * Pre-launch hook. + */ +pre?: string | null; +/** + * Wrapper hook for the runtime of the game instance. + */ +wrapper?: string | null; +/** + * Post-launch hook. + */ +post?: string | null } +export type InternetPayload = { event: "InstallPackage"; id: string } | { event: "InstallPack"; id: string } | { event: "InstallPath"; path: string } /** * Custom Java arguments on a per-cluster basis. */ -export interface JavaOptions { - /** - * A custom java version from the global java store, if specified. - */ - custom_version?: JavaVersion | null; - /** - * Custom runtime arguments when running the cluster. - */ - custom_arguments?: string[] | null; - /** - * Custom environment variables when running the cluster. - */ - custom_env_arguments?: ([string, string])[] | null; -}; +export type JavaOptions = { +/** + * A custom java version from the global java store, if specified. + */ +custom_version?: JavaVersion | null; +/** + * Custom runtime arguments when running the cluster. + */ +custom_arguments?: string[] | null; +/** + * Custom environment variables when running the cluster. + */ +custom_env_arguments?: ([string, string])[] | null } /** * A struct representing a single version of the Java Runtime Environment. * Use [`locate_java`] to get an array of all located java instances on the machine. - * + * * common java paths from https://github.com/PrismLauncher/PrismLauncher/blob/develop/launcher/java/JavaUtils.cpp under GPL 3.0 - * + * * java check functionality from https://github.com/modrinth/theseus/blob/master/theseus/src/util/jre.rs under MIT */ -export interface JavaVersion { version: string; arch: string; path: string }; +export type JavaVersion = { version: string; arch: string; path: string } /** * A HashMap of all located and installed available Java versions */ -export type JavaVersions = { [key in string]: JavaVersion }; +export type JavaVersions = { [key in string]: JavaVersion } /** * Available mod loaders to be used for a cluster. */ -export type Loader = - /** - * The default vanilla loader, no modding supported. - */ - 'vanilla' | - /** - * The MinecraftForge Minecraft mod loader. - */ - 'forge' | - /** - * The FabircMC Minecraft mod loader. - */ - 'fabric' | - /** - * The NeoForge Minecraft mod loader. - */ - 'neoforge' | - /** - * The Quilt Minecraft mod loader. - */ - 'quilt' | - /** - * The Legacy Fabric port mod loader. - */ - 'legacyfabric'; +export type Loader = +/** + * The default vanilla loader, no modding supported. + */ +"vanilla" | +/** + * The MinecraftForge Minecraft mod loader. + */ +"forge" | +/** + * The FabircMC Minecraft mod loader. + */ +"fabric" | +/** + * The NeoForge Minecraft mod loader. + */ +"neoforge" | +/** + * The Quilt Minecraft mod loader. + */ +"quilt" | +/** + * The Legacy Fabric port mod loader. + */ +"legacyfabric" /** * A version of a Minecraft mod loader */ -export interface LoaderVersion { - /** - * The version ID of the loader - */ - id: string; - /** - * The URL of the version's manifest - */ - url: string; - /** - * Whether the loader is stable or not - */ - stable: boolean; -}; +export type LoaderVersion = { +/** + * The version ID of the loader + */ +id: string; +/** + * The URL of the version's manifest + */ +url: string; +/** + * Whether the loader is stable or not + */ +stable: boolean } /** * Universal interface for managed package dependencies. */ -export interface ManagedDependency { version_id: string | null; package_id: string | null; file_name: string | null; dependency_type: PackageDependency }; +export type ManagedDependency = { version_id: string | null; package_id: string | null; file_name: string | null; dependency_type: PackageDependency } /** * Universal metadata for any managed package from a Mod distribution platform. */ -export interface ManagedPackage { id: string; uid: string | null; package_type: PackageType; title: string; description: string; main: string; versions: string[]; game_versions: string[]; loaders: string[]; icon_url: string | null; created: string; updated: string; client: PackageSide; server: PackageSide; downloads: number; followers: number; categories: string[]; optional_categories: string[] | null }; +export type ManagedPackage = { id: string; uid: string | null; package_type: PackageType; title: string; description: string; main: string; versions: string[]; game_versions: string[]; loaders: string[]; icon_url: string | null; created: string; updated: string; client: PackageSide; server: PackageSide; downloads: number; followers: number; categories: string[]; optional_categories: string[] | null } /** * Universal interface for managed package authors and users. */ -export interface ManagedUser { id: string; role: string; username: string; name: string | null; avatar: string | null; description: string | null; created: string }; +export type ManagedUser = { id: string; role: string; username: string; name: string | null; avatar: string | null; description: string | null; created: string } /** * Universal managed package version of a package. */ -export interface ManagedVersion { id: string; package_id: string; author: string; name: string; featured: boolean; version_id: string; changelog: string; changelog_url: string | null; published: string; downloads: number; version_type: string; files: ManagedVersionFile[]; deps: ManagedDependency[]; game_versions: string[]; loaders: string[] }; +export type ManagedVersion = { id: string; package_id: string; author: string; name: string; featured: boolean; version_id: string; changelog: string; changelog_url: string | null; published: string; downloads: number; version_type: string; files: ManagedVersionFile[]; deps: ManagedDependency[]; game_versions: string[]; loaders: string[] } /** * Universal interface for managed package files. */ -export interface ManagedVersionFile { url: string; file_name: string; primary: boolean; size: number; file_type: PackageFile | null; hashes: { [key in string]: string } }; +export type ManagedVersionFile = { url: string; file_name: string; primary: boolean; size: number; file_type: PackageFile | null; hashes: { [key in string]: string } } /** * Global memory settings across all clusters. */ -export interface Memory { - /** - * Maximum amount of Java memory available globally. - */ - maximum: number; - /** - * Minimum amount of Java memory available globally. - */ - minimum: number; -}; -export interface MessagePayload { message: string }; +export type Memory = { +/** + * Maximum amount of Java memory available globally. + */ +maximum: number; +/** + * Minimum amount of Java memory available globally. + */ +minimum: number } +export type MessagePayload = { message: string } /** * A structure of all needed Minecraft credentials for logging in and account management. */ -export interface MinecraftCredentials { - /** - * The uuid of the credentials. - */ - id: string; - /** - * The username of the Minecraft account. - */ - username: string; - /** - * The access token as a String. - */ - access_token: string; - /** - * The refresh token as a string for [`MinecraftState#refresh`]. - */ - refresh_token: string; - /** - * The time that the access token expires as a [`DateTime`]. - */ - expires: string; -}; -export interface OfflinePayload { offline: boolean }; +export type MinecraftCredentials = { +/** + * The uuid of the credentials. + */ +id: string; +/** + * The username of the Minecraft account. + */ +username: string; +/** + * The access token as a String. + */ +access_token: string; +/** + * The refresh token as a string for [`MinecraftState#refresh`]. + */ +refresh_token: string; +/** + * The time that the access token expires as a [`DateTime`]. + */ +expires: string } +export type OfflinePayload = { offline: boolean } /** * A struct that represents a Package. */ -export interface Package { sha512: string; meta: PackageMetadata; file_name: string; disabled: boolean }; +export type Package = { sha512: string; meta: PackageMetadata; file_name: string; disabled: boolean } /** * Optional data used to link a specific cluster to a package project. */ -export interface PackageData { - /** - * The package ID as a String. - */ - package_id: string | null; - /** - * The version of the package as a String. - */ - version_id: string | null; - /** - * Whether or not the current package is locked (for legacy modpack support). - */ - locked?: boolean | null; -}; +export type PackageData = { +/** + * The package ID as a String. + */ +package_id: string | null; +/** + * The version of the package as a String. + */ +version_id: string | null; +/** + * Whether or not the current package is locked (for legacy modpack support). + */ +locked?: boolean | null } /** * The type of a [`ManagedDependency`]. */ -export type PackageDependency = 'Required' | 'Optional' | 'Incompatible' | 'Embedded'; +export type PackageDependency = "Required" | "Optional" | "Incompatible" | "Embedded" /** * The file type of a [`Package`]. */ -export type PackageFile = 'required_pack' | 'optional_pack' | 'unknown'; +export type PackageFile = "required_pack" | "optional_pack" | "unknown" /** * Metadata that represents a [`Package`]. */ -export type PackageMetadata = { type: 'managed'; package: ManagedPackage; version: ManagedVersion; users: ManagedUser; update: ManagedVersion | null; incompatible: boolean } | { type: 'mapped'; title: string | null; description: string | null; authors: string[]; version: string | null; icon: string | null; package_type: string | null } | { type: 'unknown' }; +export type PackageMetadata = { type: "managed"; package: ManagedPackage; version: ManagedVersion; users: ManagedUser; update: ManagedVersion | null; incompatible: boolean } | { type: "mapped"; title: string | null; description: string | null; authors: string[]; version: string | null; icon: string | null; package_type: string | null } | { type: "unknown" } /** * Relative [`PathBuf`] for a specific [`Package`] of a [`Cluster`]. */ -export type PackagePath = string; +export type PackagePath = string /** * The Client/Server side type of a [`Package`]. */ -export type PackageSide = 'required' | 'optional' | 'unsupported' | 'unknown'; +export type PackageSide = "required" | "optional" | "unsupported" | "unknown" /** * Represents types of packages handled by the launcher. */ -export type PackageType = - /** - * represents a mod jarfile - */ - 'mod' | - /** - * represents a datapack file - */ - 'datapack' | - /** - * represents a resourcepack file - */ - 'resource' | - /** - * represents a shaderpack file - */ - 'shader'; -export interface ProcessPayload { uuid: string; pid: number; event: ProcessPayloadType; message: string }; -export type ProcessPayloadType = 'started' | 'modified' | 'finished'; +export type PackageType = +/** + * represents a mod jarfile + */ +"mod" | +/** + * represents a datapack file + */ +"datapack" | +/** + * represents a resourcepack file + */ +"resource" | +/** + * represents a shaderpack file + */ +"shader" +export type ProcessPayload = { uuid: string; pid: number; event: ProcessPayloadType; message: string } +export type ProcessPayloadType = "started" | "modified" | "finished" | "logging" /** * Global Minecraft resolution. */ -export type Resolution = [number, number]; +export type Resolution = [number, number] /** * A global settings state for the launcher. */ -export interface Settings { - /** - * A OneLauncher [`Theme`] managed by the core GUI. - */ - theme: Theme; - /** - * Does not ask for confirmation when closing the OneLauncher GUI - */ - hide_close_prompt?: boolean; - /** - * Disables animations in the OneLauncher GUI - */ - disable_animations?: boolean; - /** - * A global fullscreen Minecraft state. - */ - force_fullscreen?: boolean; - /** - * A global default [`Resolution`] for Minecraft. - */ - resolution: Resolution; - /** - * A global [`JavaVersions`] list and default version. - */ - java_versions: JavaVersions; - /** - * A global [`Memory`] settings store for Java memory settings. - */ - memory: Memory; - /** - * Global and default initialization hooks . - */ - init_hooks: InitHooks; - /** - * Global and default custom Java arguments. - */ - custom_java_args: string[]; - /** - * Global and default custom environment variables. - */ - custom_env_args: ([string, string])[]; - /** - * Global and default maximum [`Semaphore`] I/O operations. - */ - max_async_io_operations: bigint; - /** - * Global and default maximum [`Semaphore`] HTTP operations. - */ - max_async_fetches: bigint; - /** - * The [`CURRENT_FORMAT_VERSION`] of this settings file. - */ - version: number; - /** - * Whether or not to disable Discord IPC. - */ - disable_discord?: boolean; - /** - * Whether or not to enable a debug mode in the launcher. - */ - debug_mode?: boolean; - /** - * Whether or not to disable Plausible and Sentry analytics. - */ - disable_analytics?: boolean; - /** - * The core global config directory stored as a [`PathBuf`]. - */ - config_dir?: string | null; - /** - * Whether or not to minimize the launcher upon a game launch. - */ - hide_on_launch?: boolean; - /** - * Enable/disable advanced rendering and window decorations. - */ - rendering?: boolean; -}; +export type Settings = { +/** + * A OneLauncher [`Theme`] managed by the core GUI. + */ +theme: Theme; +/** + * Does not ask for confirmation when closing the OneLauncher GUI + */ +hide_close_prompt?: boolean; +/** + * Disables animations in the OneLauncher GUI + */ +disable_animations?: boolean; +/** + * A global fullscreen Minecraft state. + */ +force_fullscreen?: boolean; +/** + * Whether to allow launching the same cluster under the same account. + */ +allow_parallel_running_clusters?: boolean; +/** + * A global default [`Resolution`] for Minecraft. + */ +resolution: Resolution; +/** + * A global [`JavaVersions`] list and default version. + */ +java_versions: JavaVersions; +/** + * A global [`Memory`] settings store for Java memory settings. + */ +memory: Memory; +/** + * Global and default initialization hooks . + */ +init_hooks: InitHooks; +/** + * Global and default custom Java arguments. + */ +custom_java_args: string[]; +/** + * Global and default custom environment variables. + */ +custom_env_args: ([string, string])[]; +/** + * Global and default maximum [`Semaphore`] I/O operations. + */ +max_async_io_operations: bigint; +/** + * Global and default maximum [`Semaphore`] HTTP operations. + */ +max_async_fetches: bigint; +/** + * The [`CURRENT_FORMAT_VERSION`] of this settings file. + */ +version: number; +/** + * Whether or not to disable Discord IPC. + */ +disable_discord?: boolean; +/** + * Whether or not to enable a debug mode in the launcher. + */ +debug_mode?: boolean; +/** + * Whether or not to disable Plausible and Sentry analytics. + */ +disable_analytics?: boolean; +/** + * The core global config directory stored as a [`PathBuf`]. + */ +config_dir?: string | null; +/** + * Whether or not to minimize the launcher upon a game launch. + */ +hide_on_launch?: boolean; +/** + * Enable/disable advanced rendering and window decorations. + */ +rendering?: boolean } /** * A OneLauncher theme managed by the GUI. */ -export type Theme = - /** - * A default Dark theme. - */ - 'dark' | - /** - * A default Light theme. - */ - 'light' | - /** - * OLED Dark Theme. - */ - 'contrast' | - /** - * Cute and colorful theme. - */ - 'cat'; -export interface Update { version: string }; +export type Theme = +/** + * A default Dark theme. + */ +"dark" | +/** + * A default Light theme. + */ +"light" | +/** + * OLED Dark Theme. + */ +"contrast" | +/** + * Cute and colorful theme. + */ +"cat" +export type Update = { version: string } /** * A game version of Minecraft */ -export interface Version { - /** - * A unique identifier of the version - */ - id: string; - /** - * The release type of the version - */ - type: VersionType; - /** - * A link to additional information about the version - */ - url: string; - /** - * The latest time a file in this version was updated - */ - time: string; - /** - * The time this version was released - */ - releaseTime: string; - /** - * The SHA1 hash of the additional information about the version - */ - sha1: string; - /** - * Whether the version supports the latest player safety features - */ - complianceLevel: number; - /** - * The SHA1 hash of the original unmodified Minecraft version's JSON - * This is only available when using the Pulseflow mirror - */ - originalSha1?: string | null; -}; +export type Version = { +/** + * A unique identifier of the version + */ +id: string; +/** + * The release type of the version + */ +type: VersionType; +/** + * A link to additional information about the version + */ +url: string; +/** + * The latest time a file in this version was updated + */ +time: string; +/** + * The time this version was released + */ +releaseTime: string; +/** + * The SHA1 hash of the additional information about the version + */ +sha1: string; +/** + * Whether the version supports the latest player safety features + */ +complianceLevel: number; +/** + * The SHA1 hash of the original unmodified Minecraft version's JSON + * This is only available when using the Pulseflow mirror + */ +originalSha1?: string | null } /** * The version type */ -export type VersionType = - /** - * A major version, which is stable for all players to use - */ - 'release' | - /** - * An experimental version, which is unstable and used for feature previews and beta testing - */ - 'snapshot' | - /** - * The oldest versions before the game was released - */ - 'old_alpha' | - /** - * Early versions of the game - */ - 'old_beta'; +export type VersionType = +/** + * A major version, which is stable for all players to use + */ +"release" | +/** + * An experimental version, which is unstable and used for feature previews and beta testing + */ +"snapshot" | +/** + * The oldest versions before the game was released + */ +"old_alpha" | +/** + * Early versions of the game + */ +"old_beta" -interface __EventObj__ { +/** tauri-specta globals **/ + +import { + invoke as TAURI_INVOKE, + Channel as TAURI_CHANNEL, +} from "@tauri-apps/api/core"; +import * as TAURI_API_EVENT from "@tauri-apps/api/event"; +import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; + +type __EventObj__ = { listen: ( cb: TAURI_API_EVENT.EventCallback, ) => ReturnType>; @@ -879,11 +843,11 @@ interface __EventObj__ { emit: T extends null ? (payload?: T) => ReturnType : (payload: T) => ReturnType; -} +}; export type Result = - | { status: 'ok'; data: T } - | { status: 'error'; error: E }; + | { status: "ok"; data: T } + | { status: "error"; error: E }; function __makeEvents__>( mappings: Record, @@ -898,7 +862,7 @@ function __makeEvents__>( get: (_, event) => { const name = mappings[event as keyof T]; - return new Proxy((() => { }) as any, { + return new Proxy((() => {}) as any, { apply: (_, __, [window]: [__WebviewWindow__]) => ({ listen: (arg: any) => window.listen(name, arg), once: (arg: any) => window.once(name, arg), @@ -906,11 +870,11 @@ function __makeEvents__>( }), get: (_, command: keyof __EventObj__) => { switch (command) { - case 'listen': + case "listen": return (arg: any) => TAURI_API_EVENT.listen(name, arg); - case 'once': + case "once": return (arg: any) => TAURI_API_EVENT.once(name, arg); - case 'emit': + case "emit": return (arg: any) => TAURI_API_EVENT.emit(name, arg); } }, diff --git a/apps/frontend/src/ui/components/base/Button.tsx b/apps/frontend/src/ui/components/base/Button.tsx index ff3be616..0f794ad9 100644 --- a/apps/frontend/src/ui/components/base/Button.tsx +++ b/apps/frontend/src/ui/components/base/Button.tsx @@ -28,7 +28,7 @@ type ButtonToggleProps = ButtonProps & { }; Button.Toggle = (props: ButtonToggleProps) => { - const [split, rest] = splitProps(props, ['checked', 'onChecked', 'onClick', 'class']); + const [split, rest] = splitProps(props, ['checked', 'onChecked', 'onClick']); const [checked, setChecked] = createSignal(untrack(() => split.checked?.()) || false); createEffect(on(() => split.checked?.(), newValue => setChecked(newValue || false))); @@ -43,7 +43,7 @@ Button.Toggle = (props: ButtonToggleProps) => { @@ -51,7 +51,7 @@ function WindowFrame() { }); return ( -
+
history.back()} />
diff --git a/apps/frontend/src/ui/components/base/Dropdown.module.scss b/apps/frontend/src/ui/components/base/Dropdown.module.scss index c40e8028..7a936649 100644 --- a/apps/frontend/src/ui/components/base/Dropdown.module.scss +++ b/apps/frontend/src/ui/components/base/Dropdown.module.scss @@ -10,7 +10,7 @@ } .dropdown_element { - @apply bg-secondary rounded-lg border border-gray-05 p-1 shadow-md shadow-black/30 h-full; + @apply bg-page-elevated rounded-lg border border-gray-05 p-1 shadow-md shadow-black/30 h-full; } .list { diff --git a/apps/frontend/src/ui/components/content/FormattedLog.tsx b/apps/frontend/src/ui/components/content/FormattedLog.tsx index 545ab527..36cb4587 100644 --- a/apps/frontend/src/ui/components/content/FormattedLog.tsx +++ b/apps/frontend/src/ui/components/content/FormattedLog.tsx @@ -40,7 +40,7 @@ function FormattedLog(props: FormattedLogProps) { return (
-
+
} diff --git a/apps/frontend/src/ui/components/overlay/ContextMenu.tsx b/apps/frontend/src/ui/components/overlay/ContextMenu.tsx index ef8fad67..c4dcd824 100644 --- a/apps/frontend/src/ui/components/overlay/ContextMenu.tsx +++ b/apps/frontend/src/ui/components/overlay/ContextMenu.tsx @@ -36,7 +36,7 @@ function ContextMenu(props: ContextMenuProps) { visible={props.visible} setVisible={props.setVisible} > -
+
{props.children}
diff --git a/apps/frontend/src/ui/components/overlay/Modal.tsx b/apps/frontend/src/ui/components/overlay/Modal.tsx index 48a47e62..304afd16 100644 --- a/apps/frontend/src/ui/components/overlay/Modal.tsx +++ b/apps/frontend/src/ui/components/overlay/Modal.tsx @@ -127,7 +127,7 @@ export type ModalProps = { function Modal(props: ModalProps) { return ( -
+
{props.children}
); diff --git a/apps/frontend/src/ui/components/overlay/account/AccountsPopup.tsx b/apps/frontend/src/ui/components/overlay/account/AccountsPopup.tsx index ff5bb8bb..4ee10237 100644 --- a/apps/frontend/src/ui/components/overlay/account/AccountsPopup.tsx +++ b/apps/frontend/src/ui/components/overlay/account/AccountsPopup.tsx @@ -60,7 +60,7 @@ function AccountPopup(props: Popup.PopupProps) { return ( -
+
JSX.Eleme } return ( -
+
Header Image diff --git a/apps/frontend/src/ui/components/overlay/notifications/NotificationPopup.tsx b/apps/frontend/src/ui/components/overlay/notifications/NotificationPopup.tsx index ea427471..b8479579 100644 --- a/apps/frontend/src/ui/components/overlay/notifications/NotificationPopup.tsx +++ b/apps/frontend/src/ui/components/overlay/notifications/NotificationPopup.tsx @@ -35,7 +35,7 @@ function NotificationPopup(props: Popup.PopupProps) { return ( -
+

Notifications

diff --git a/apps/frontend/src/ui/pages/browser/BrowserMain.tsx b/apps/frontend/src/ui/pages/browser/BrowserMain.tsx index 18de59c5..adbfd690 100644 --- a/apps/frontend/src/ui/pages/browser/BrowserMain.tsx +++ b/apps/frontend/src/ui/pages/browser/BrowserMain.tsx @@ -66,7 +66,7 @@ function BrowserMain() { return (
-
+
{ const unlisten = await bridge.events.processPayload.listen((event) => { - if (event.payload.event === 'logging' && event.payload.uuid === params.process_uuid) - render(() => , codeRef); + if (event.payload.uuid !== params.process_uuid) + return; - if (event.payload.event === 'finished' && event.payload.uuid === params.process_uuid && cluster() !== undefined) + if (event.payload.event === 'logging') + render(() => , codeRef); + else if (event.payload.event === 'finished') setIsRunning(false); }); diff --git a/apps/frontend/src/ui/pages/cluster/ClusterOverview.tsx b/apps/frontend/src/ui/pages/cluster/ClusterOverview.tsx index 97a8de1d..46d0caf0 100644 --- a/apps/frontend/src/ui/pages/cluster/ClusterOverview.tsx +++ b/apps/frontend/src/ui/pages/cluster/ClusterOverview.tsx @@ -258,7 +258,7 @@ function Banner(props: BannerProps) { } return ( -
+
= self.client.write().await;