🔨 chore: merge
Signed-off-by: Pauline <git@ethanlibs.co>
This commit is contained in:
commit
43f16ceecf
39 changed files with 1372 additions and 1017 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> {
|
||||
|
|
|
|||
1506
apps/frontend/src/bindings.ts
generated
1506
apps/frontend/src/bindings.ts
generated
File diff suppressed because it is too large
Load diff
|
|
@ -18,7 +18,7 @@ function App(props: ParentProps) {
|
|||
|
||||
return (
|
||||
<GlobalContexts>
|
||||
<main class="h-screen max-h-screen min-h-screen w-full flex flex-col overflow-hidden bg-primary text-fg-primary">
|
||||
<main class="h-screen max-h-screen min-h-screen w-full flex flex-col overflow-hidden bg-page text-fg-primary">
|
||||
<WindowFrame />
|
||||
<div class="flex flex-col px-8">
|
||||
<Navbar />
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ function ScrollableContainer(props: ParentProps) {
|
|||
return (
|
||||
<div class="w-[calc(100%+14px)] flex flex-1 flex-col overflow-hidden">
|
||||
<OverlayScrollbarsComponent class="os-hide-horizontal-scrollbar relative flex-1">
|
||||
<div class="absolute h-full w-[calc(100%-14px)] flex flex-col gap-1">
|
||||
<div class="absolute h-full w-[calc(100%-14px)] flex flex-col gap-2">
|
||||
{props.children}
|
||||
</div>
|
||||
</OverlayScrollbarsComponent>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export type SettingsRowProps = ParentProps & {
|
|||
function SettingsRow(props: SettingsRowProps) {
|
||||
return (
|
||||
<div
|
||||
class="flex flex-row items-center gap-3.5 rounded-xl bg-component-bg p-4"
|
||||
class="bg-page-elevated flex flex-row items-center gap-3.5 rounded-xl p-4"
|
||||
classList={{
|
||||
'bg-component-bg-disabled': props.disabled,
|
||||
'text-fg-primary-disabled': props.disabled,
|
||||
|
|
|
|||
|
|
@ -26,12 +26,14 @@ 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 (
|
||||
<div class="w-52 flex flex-col">
|
||||
<div class="w-56 flex flex-col pr-2">
|
||||
<For each={Object.keys(props.links)}>
|
||||
{section => (
|
||||
<div class="flex flex-col gap-y-2">
|
||||
|
|
@ -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 border hover:bg-component-bg-hover active:bg-component-bg-pressed ${isActive(link[2], link[3]) ? 'bg-component-bg border-gray-05' : 'border-transparent'}`
|
||||
}
|
||||
>
|
||||
<span class="flex flex-row items-center gap-x-3 *:w-5">
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ interface TitlebarButtonProps {
|
|||
function TitlebarButton(props: TitlebarButtonProps) {
|
||||
return (
|
||||
<button class="group h-8 w-8 flex items-center justify-center" onClick={e => props.onClick(e)}>
|
||||
<div class="rounded-lg p-1 group-hover:bg-primary">
|
||||
<div class="rounded-lg p-1 group-hover:bg-page">
|
||||
<props.icon class={`w-[18px] h-[18px] stroke-slate ${props.danger ? 'group-hover:stroke-danger' : 'group-hover:stroke-white'}`} />
|
||||
</div>
|
||||
</button>
|
||||
|
|
@ -51,7 +51,7 @@ function WindowFrame() {
|
|||
});
|
||||
|
||||
return (
|
||||
<div data-tauri-drag-region class="z-[99999] h-8 w-screen flex flex-row items-center justify-between gap-0.5 bg-secondary pr-0.5">
|
||||
<div data-tauri-drag-region class="z-[99999] h-8 w-screen flex flex-row items-center justify-between gap-0.5 bg-page-elevated pr-0.5">
|
||||
<div class="flex flex-row items-center">
|
||||
<TitlebarButton icon={ChevronLeftIcon} onClick={() => history.back()} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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) => {
|
|||
<Button
|
||||
onClick={(e) => {
|
||||
toggle();
|
||||
// @ts-expect-error -- type error which i cba to resolve
|
||||
// @ts-expect-error -- JSX doesn't seem to use the same type
|
||||
split.onClick?.(e);
|
||||
}}
|
||||
aria-checked={checked()}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -60,9 +60,8 @@ SelectList.Row = (props: SelectListRowProps) => {
|
|||
if (props.index !== undefined)
|
||||
setSelected(props.index);
|
||||
|
||||
if (split.onClick)
|
||||
// @ts-expect-error -- la
|
||||
split.onClick(e);
|
||||
// @ts-expect-error -- JSX doesn't seem to use the same type
|
||||
split.onClick?.(e);
|
||||
}}
|
||||
class={`${styles.row} ${props.index !== undefined && selected() === props.index ? styles.selected : ''} ${split.class || ''}`}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@
|
|||
@apply pointer-events-none;
|
||||
@apply opacity-0 transition-opacity;
|
||||
@apply absolute left-1/2 -translate-x-1/2 top-0 -translate-y-6/5;
|
||||
@apply min-w-44 w-max max-w-52;
|
||||
@apply w-max max-w-52;
|
||||
@apply backdrop-blur-lg rounded-md bg-black/50 p-2 text-center text-sm text-wrap text-white shadow shadow-black/30;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,79 @@
|
|||
import { For, createMemo } from 'solid-js';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-solid';
|
||||
import { Index, Show, createMemo, createSignal, untrack } from 'solid-js';
|
||||
import { OverlayScrollbarsComponent, type OverlayScrollbarsComponentRef } from 'overlayscrollbars-solid';
|
||||
import type { Ref } from '@solid-primitives/refs';
|
||||
import { AlignBottom01Icon } from '@untitled-theme/icons-solid';
|
||||
import type { OnUpdatedEventListenerArgs, OverlayScrollbars } from 'overlayscrollbars';
|
||||
import Button from '../base/Button';
|
||||
import styles from './FormattedLog.module.scss';
|
||||
|
||||
interface FormattedLogProps {
|
||||
log: string;
|
||||
log?: string | undefined;
|
||||
codeRef?: Ref<HTMLElement>;
|
||||
enableAutoScroll?: boolean;
|
||||
};
|
||||
|
||||
function FormattedLog(props: FormattedLogProps) {
|
||||
const lines = createMemo(() => props.log.split('\n'));
|
||||
// TODO(perf): Do a "infinite scroll" method of rendering the log. Adding 15918590815 dom elements at once lags the render thread for a bit
|
||||
const lines = createMemo(() => (props.log?.trim().split('\n') || []));
|
||||
// eslint-disable-next-line solid/reactivity -- Shouldn't really be tracked
|
||||
const [shouldScroll, setShouldScroll] = createSignal(props.enableAutoScroll === true);
|
||||
|
||||
let overlayScrollbars!: OverlayScrollbarsComponentRef;
|
||||
|
||||
function autoScrollUpdated(instance: OverlayScrollbars, _args: OnUpdatedEventListenerArgs) {
|
||||
if (untrack(shouldScroll) === true)
|
||||
instance.elements().scrollOffsetElement.scrollTop = instance.elements().scrollOffsetElement.scrollHeight;
|
||||
}
|
||||
|
||||
function autoScrollInitialized(instance: OverlayScrollbars) {
|
||||
instance.elements().scrollEventElement.addEventListener('wheel', () => {
|
||||
toggleAutoScroll(false);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleAutoScroll(value: boolean) {
|
||||
setShouldScroll(value);
|
||||
if (value === true)
|
||||
overlayScrollbars.osInstance()?.update(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="mt-2 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()}>
|
||||
{line => <Line line={line} />}
|
||||
</For>
|
||||
</code>
|
||||
</OverlayScrollbarsComponent>
|
||||
<div class="relative h-full flex flex-1 flex-col rounded-md bg-component-bg p-2">
|
||||
<Show when={props.enableAutoScroll === true}>
|
||||
<div class="absolute right-0 top-0 z-1 h-6 w-full flex flex-row justify-end border border-gray-05 rounded-t-md bg-page p-px">
|
||||
<Button.Toggle
|
||||
buttonStyle="icon"
|
||||
children={<AlignBottom01Icon />}
|
||||
checked={shouldScroll}
|
||||
onChecked={toggleAutoScroll}
|
||||
class="h-full! rounded-md! py-1!"
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div
|
||||
class={`${props.enableAutoScroll === true ? 'mt-6' : ''} h-full flex flex-1 overflow-auto font-medium font-mono`}
|
||||
>
|
||||
<OverlayScrollbarsComponent
|
||||
ref={overlayScrollbars}
|
||||
events={{
|
||||
updated: autoScrollUpdated,
|
||||
initialized: autoScrollInitialized,
|
||||
}}
|
||||
class="relative h-full flex-1"
|
||||
>
|
||||
<code ref={props.codeRef} class={styles.log}>
|
||||
<Index each={lines()}>
|
||||
{(line, index) => {
|
||||
if (index === lines().length - 1)
|
||||
return <></>;
|
||||
|
||||
return <Line line={line()} />;
|
||||
}}
|
||||
</Index>
|
||||
</code>
|
||||
</OverlayScrollbarsComponent>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -41,10 +97,11 @@ interface LineProps {
|
|||
* It only matches `CHAT` if it is present.
|
||||
* This is an **optional** capture group.
|
||||
*/
|
||||
const REGEX_PATTERN = /\[(\d+:\d+:\d+)\] \[(.[^(\n\r/\u2028\u2029]*)\/(\w+)\]: (?:\[(CHAT)\])?/;
|
||||
const REGEX_PATTERN = /\[(\d+:\d+:\d+)\] \[(.[^(\n\r/\u2028\u2029]*)\/(\w+)\]:? (?:\[(CHAT)\])?/;
|
||||
|
||||
function Line(props: LineProps) {
|
||||
export function Line(props: LineProps) {
|
||||
const format = (line: string) => {
|
||||
line = line.replace(/§./g, '');
|
||||
const prefix = line.match(REGEX_PATTERN);
|
||||
|
||||
const isEmpty = line.trim() === '';
|
||||
|
|
|
|||
|
|
@ -51,9 +51,8 @@ function ClusterCover(props: ClusterCoverProps) {
|
|||
src={image()}
|
||||
onError={(e) => {
|
||||
e.currentTarget.src = split.override ? convertFileSrc(split.override) : defaultCover;
|
||||
if (split.onError)
|
||||
// @ts-expect-error -- jsx stuff
|
||||
split.onError!(e);
|
||||
// @ts-expect-error -- JSX doesn't seem to use the same type
|
||||
split.onError?.(e);
|
||||
}}
|
||||
/>
|
||||
</Wrapper>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ function ContextMenu(props: ContextMenuProps) {
|
|||
visible={props.visible}
|
||||
setVisible={props.setVisible}
|
||||
>
|
||||
<div ref={setRef} class="border border-gray-05 rounded-xl bg-secondary p-1 shadow-black/30 shadow-md">
|
||||
<div ref={setRef} class="border border-gray-05 rounded-xl bg-page-elevated p-1 shadow-black/30 shadow-md">
|
||||
<div class="flex flex-col gap-y-1 text-fg-primary">
|
||||
{props.children}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ export type ModalProps = {
|
|||
|
||||
function Modal(props: ModalProps) {
|
||||
return (
|
||||
<div ref={mergeRefs(props.ref)} class="min-w-xs flex flex-col gap-y-2 border border-white/5 rounded-lg bg-primary p-4 text-center">
|
||||
<div ref={mergeRefs(props.ref)} class="min-w-xs flex flex-col gap-y-2 border border-white/5 rounded-lg bg-page p-4 text-center">
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -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}
|
||||
/>,
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ function AccountPopup(props: Popup.PopupProps) {
|
|||
|
||||
return (
|
||||
<Popup {...props}>
|
||||
<div class="w-72 border border-gray-10 rounded-xl bg-secondary p-2 shadow-black/30 shadow-md">
|
||||
<div class="w-72 border border-gray-10 rounded-xl bg-page-elevated p-2 shadow-black/30 shadow-md">
|
||||
<div class="flex flex-col gap-y-2 text-fg-primary">
|
||||
<Show when={controller.defaultAccount() !== null || controller.defaultAccount() !== undefined}>
|
||||
<AccountComponent
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ export function createClusterStep(props: CreateClusterStepType): () => JSX.Eleme
|
|||
}
|
||||
|
||||
return (
|
||||
<div class="min-w-sm flex flex-col rounded-lg bg-primary text-center">
|
||||
<div class="min-w-sm flex flex-col rounded-lg bg-page text-center">
|
||||
<div class="relative h-25 flex">
|
||||
<div class="absolute left-0 top-0 h-full w-full">
|
||||
<img class="h-full w-full rounded-t-lg" src={HeaderImage} alt="Header Image" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ function NotificationPopup(props: Popup.PopupProps) {
|
|||
|
||||
return (
|
||||
<Popup {...props}>
|
||||
<div class="w-96 border border-gray-10 rounded-xl bg-secondary p-2 shadow-black/30 shadow-md">
|
||||
<div class="w-96 border border-gray-10 rounded-xl bg-page-elevated p-2 shadow-black/30 shadow-md">
|
||||
<div class="overflow-hidden transition-height" ref={parent}>
|
||||
<div class="flex flex-col items-stretch justify-start gap-2 text-start" ref={inner}>
|
||||
<p class="px-2 pt-1 text-2lg">Notifications</p>
|
||||
|
|
|
|||
|
|
@ -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!" />
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ function BrowserMain() {
|
|||
|
||||
return (
|
||||
<div class="relative h-full flex flex-1 flex-col gap-2">
|
||||
<div class="sticky top-0 z-10 flex flex-row justify-between bg-primary">
|
||||
<div class="sticky top-0 z-10 flex flex-row justify-between bg-page">
|
||||
<div class="h-8 flex flex-row gap-2">
|
||||
|
||||
<TextField
|
||||
|
|
|
|||
|
|
@ -1,46 +1,44 @@
|
|||
import { type Params, useNavigate, useSearchParams } from '@solidjs/router';
|
||||
import { createEffect, createSignal, onCleanup, onMount } from 'solid-js';
|
||||
import { type Params, useSearchParams } from '@solidjs/router';
|
||||
import { createSignal, onCleanup, onMount } from 'solid-js';
|
||||
import type { UnlistenFn } from '@tauri-apps/api/event';
|
||||
import ClusterRoot from './ClusterRoot';
|
||||
import { SlashOctagonIcon } from '@untitled-theme/icons-solid';
|
||||
import { render } from 'solid-js/web';
|
||||
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 FormattedLog, { Line } 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';
|
||||
import useCommand from '~ui/hooks/useCommand';
|
||||
import FormattedLog from '~ui/components/content/FormattedLog';
|
||||
|
||||
interface ClusterGameParams extends Params {
|
||||
process_uuid: string;
|
||||
started_at: string;
|
||||
pid: string;
|
||||
user: string;
|
||||
};
|
||||
|
||||
function ClusterGame() {
|
||||
const [cluster] = useClusterContext();
|
||||
const [params] = useSearchParams<ClusterGameParams>();
|
||||
const navigate = useNavigate();
|
||||
const [log] = useCommand(bridge.commands.getClusterLog, cluster()!.uuid, 'latest.log');
|
||||
const [isRunning, setIsRunning] = createSignal(true);
|
||||
|
||||
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());
|
||||
});
|
||||
let codeRef!: HTMLElement;
|
||||
|
||||
onMount(async () => {
|
||||
const unlisten = await bridge.events.processPayload.listen(({ payload }) => {
|
||||
if (payload.event === 'finished' && payload.uuid === params.process_uuid && cluster() !== undefined)
|
||||
ClusterRoot.open(navigate, cluster()!.uuid);
|
||||
const unlisten = await bridge.events.processPayload.listen((event) => {
|
||||
if (event.payload.uuid !== params.process_uuid)
|
||||
return;
|
||||
|
||||
if (event.payload.event === 'logging')
|
||||
render(() => <Line line={event.payload.message} />, codeRef);
|
||||
else if (event.payload.event === 'finished')
|
||||
setIsRunning(false);
|
||||
});
|
||||
|
||||
setUnlisten(() => unlisten);
|
||||
|
|
@ -50,39 +48,82 @@ function ClusterGame() {
|
|||
unlisten()?.();
|
||||
});
|
||||
|
||||
const startedAtFormatted = (startedAt: number) => {
|
||||
const date = new Date(startedAt);
|
||||
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"
|
||||
timeLeft={1}
|
||||
/>
|
||||
));
|
||||
|
||||
function killProcess(force: boolean = false) {
|
||||
if (params.process_uuid !== undefined) {
|
||||
if (force === true) {
|
||||
bridge.commands.killProcess(params.process_uuid);
|
||||
return;
|
||||
}
|
||||
|
||||
killModal.show();
|
||||
}
|
||||
}
|
||||
|
||||
const date = new Date(params.started_at!);
|
||||
|
||||
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:
|
||||
<h1>{isRunning() ? 'Game Running' : 'Game Stopped'}</h1>
|
||||
<div class="flex flex-1 flex-col gap-y-4">
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<div class="flex flex-row">
|
||||
<div>
|
||||
<Tooltip text={date.toLocaleString()}>
|
||||
Started:
|
||||
{' '}
|
||||
<strong>
|
||||
<TimeAgo timestamp={date.getTime()} />
|
||||
</strong>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</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
|
||||
codeRef={el => codeRef = el}
|
||||
log={log()?.trim()}
|
||||
enableAutoScroll={true}
|
||||
/>
|
||||
|
||||
<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)}
|
||||
disabled={!isRunning()}
|
||||
/>
|
||||
</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({
|
||||
|
|
@ -263,7 +258,7 @@ function Banner(props: BannerProps) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div class="h-37 flex flex-row gap-x-2.5 rounded-xl bg-component-bg p-2.5">
|
||||
<div class="bg-page-elevated h-37 flex flex-row gap-x-2.5 rounded-xl p-2.5">
|
||||
<div class="relative aspect-ratio-video h-full min-w-57 w-57 overflow-hidden border border-gray-10 rounded-lg">
|
||||
<Show when={props.editMode()}>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -83,8 +88,9 @@ function ClusterSidebar() {
|
|||
});
|
||||
|
||||
onMount(async () => {
|
||||
const unlisten = await bridge.events.processPayload.listen(() => {
|
||||
refetchProcesses();
|
||||
const unlisten = await bridge.events.processPayload.listen(({ payload }) => {
|
||||
if (payload.event === 'started' || payload.event === 'finished')
|
||||
refetchProcesses();
|
||||
});
|
||||
|
||||
setListener(() => unlisten);
|
||||
|
|
@ -106,13 +112,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';
|
||||
|
|
@ -48,12 +48,10 @@ export function createSetting<T>(initial: T | undefined | null, fallback?: T): C
|
|||
checkGlobal();
|
||||
});
|
||||
|
||||
// @ts-expect-error -- aaa
|
||||
// @ts-expect-error -- Can't be bothered to fix this type issue
|
||||
const set: Setter<T> = (value) => {
|
||||
// @ts-expect-error -- aaa
|
||||
setRaw(value);
|
||||
// @ts-expect-error -- aaa
|
||||
setValue(value);
|
||||
setRaw(value!);
|
||||
setValue(value!);
|
||||
|
||||
checkGlobal();
|
||||
};
|
||||
|
|
@ -64,8 +62,7 @@ export function createSetting<T>(initial: T | undefined | null, fallback?: T): C
|
|||
|
||||
setValue(() => fallback);
|
||||
|
||||
// @ts-expect-error -- aaa
|
||||
setRaw(() => (raw || null));
|
||||
setRaw(() => (raw || null)!);
|
||||
|
||||
checkGlobal();
|
||||
};
|
||||
|
|
@ -192,12 +189,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 +213,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 +352,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 +371,7 @@ function PageSettings() {
|
|||
|
||||
// Launcher
|
||||
hide_on_launch: hideOnLaunch.get(),
|
||||
allow_parallel_running_clusters: allowParallelClusters.get(),
|
||||
|
||||
// Process
|
||||
init_hooks: {
|
||||
|
|
@ -368,10 +387,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,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ export default defineConfig({
|
|||
sans: 'Poppins',
|
||||
},
|
||||
|
||||
borderRadius: {
|
||||
md: '0.438rem', // 7px
|
||||
},
|
||||
|
||||
fontSize: {
|
||||
// Based off 16px
|
||||
'xs': '0.688rem',
|
||||
|
|
@ -107,7 +111,12 @@ export default defineConfig({
|
|||
trace: '#FDFDFD',
|
||||
},
|
||||
|
||||
primary: '#11171C',
|
||||
page: {
|
||||
DEFAULT: '#11171c',
|
||||
elevated: '#151c22',
|
||||
pressed: '#0e1317',
|
||||
},
|
||||
|
||||
secondary: '#192026',
|
||||
},
|
||||
extend: {
|
||||
|
|
@ -123,10 +132,6 @@ export default defineConfig({
|
|||
transparent: 'transparent',
|
||||
},
|
||||
|
||||
borderRadius: {
|
||||
mdlg: '0.438rem',
|
||||
},
|
||||
|
||||
// animation: {
|
||||
// 'fade-in': 'fadeIn 150ms easeOutExpo forwards',
|
||||
// 'fade-out': 'fadeOut 150ms easeOutExpo forwards',
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import solid from 'vite-plugin-solid';
|
|||
import solidSvg from 'vite-plugin-solid-svg';
|
||||
import paths from 'vite-tsconfig-paths';
|
||||
import unocss from 'unocss/vite';
|
||||
import sentry from '@sentry/vite-plugin';
|
||||
import * as sentry from '@sentry/vite-plugin';
|
||||
|
||||
export default defineConfig(async ({ mode }) => {
|
||||
process.env = { ...process.env, ...loadEnv(mode, process.cwd(), '') };
|
||||
|
|
|
|||
|
|
@ -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>> {
|
||||
|
|
|
|||
|
|
@ -231,6 +231,7 @@ pub enum ProcessPayloadType {
|
|||
Started,
|
||||
Modified,
|
||||
Finished,
|
||||
Logging,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tauri", derive(specta::Type, tauri_specta::Event))]
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ pub const DUMMY_REPLACE_NEWLINE: &str = "\n";
|
|||
|
||||
// =========== Authentication ===========
|
||||
/// Our Discord RPC client ID.
|
||||
pub const DISCORD_RPC_CLIENT_ID: &str = "1234567890000000"; // todo
|
||||
pub const DISCORD_RPC_CLIENT_ID: &str = "1274084193193955408";
|
||||
/// Our Microsoft client ID.
|
||||
pub const MICROSOFT_CLIENT_ID: &str = "9eac3a4e-8cdd-43ef-863e-49cd601b1f03";
|
||||
/// Mojang/Microsoft client ID.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use discord_rich_presence::activity::{Activity, Assets};
|
||||
use discord_rich_presence::activity::{Activity, Assets, Button};
|
||||
use discord_rich_presence::{DiscordIpc, DiscordIpcClient};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
|
|
@ -59,11 +59,16 @@ impl DiscordRPC {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
let activity = Activity::new().state(msg).assets(
|
||||
Assets::new()
|
||||
.large_image("polyfrost")
|
||||
.large_text("Polyfrost Logo"),
|
||||
);
|
||||
let activity = Activity::new()
|
||||
.state(msg)
|
||||
.buttons(vec![
|
||||
Button::new("Download", "https://polyfrost.org/"),
|
||||
])
|
||||
.assets(
|
||||
Assets::new()
|
||||
.large_image("polyfrost_logo_512")
|
||||
.large_text("Polyfrost Logo"),
|
||||
);
|
||||
|
||||
let mut client: tokio::sync::RwLockWriteGuard<'_, DiscordIpcClient> =
|
||||
self.client.write().await;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
//! Handles child processes in a unified API
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use std::collections::HashMap;
|
||||
use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
use tokio::process::{Child, Command};
|
||||
use tokio::sync::RwLock;
|
||||
|
|
@ -119,6 +121,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 +141,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 +170,7 @@ impl ChildType {
|
|||
exe,
|
||||
cluster_path,
|
||||
post,
|
||||
user
|
||||
};
|
||||
|
||||
let path = state.directories.caches_dir().await.join(PROCESSOR_FILE);
|
||||
|
|
@ -264,6 +271,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,14 +433,47 @@ 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);
|
||||
let pid = process
|
||||
command
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.stdin(Stdio::null());
|
||||
|
||||
let mut proc = command.spawn().map_err(IOError::from)?;
|
||||
|
||||
let pid = proc
|
||||
.id()
|
||||
.ok_or_else(|| anyhow::anyhow!("process failed, couldn't get pid"))?;
|
||||
|
||||
let stdout = proc.stdout.take().unwrap();
|
||||
let stderr = proc.stderr.take().unwrap();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut stdout = BufReader::new(stdout).lines();
|
||||
let mut stderr = BufReader::new(stderr).lines();
|
||||
|
||||
while let Ok(line) = tokio::select! {
|
||||
line = stdout.next_line() => line,
|
||||
line = stderr.next_line() => line,
|
||||
} {
|
||||
if let Some(line) = line {
|
||||
let mut censored = line.clone();
|
||||
for (key, value) in censors.iter() {
|
||||
censored = censored.replace(key, value);
|
||||
}
|
||||
|
||||
if let Err(err) = send_process(uuid, pid, ProcessPayloadType::Logging, &censored).await {
|
||||
tracing::warn!("failed to send process log: {}", err);
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let process = ChildType::ChildProcess(proc);
|
||||
|
||||
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 +493,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 +557,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 +580,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