feat: ClusterLogs + ClusterOverview
This commit is contained in:
parent
f823d610d0
commit
7c8e5139aa
18 changed files with 438 additions and 117 deletions
BIN
desktop/src/assets/logos/fabric.png
Normal file
BIN
desktop/src/assets/logos/fabric.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
BIN
desktop/src/assets/logos/forge.png
Normal file
BIN
desktop/src/assets/logos/forge.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
BIN
desktop/src/assets/logos/quilt.png
Normal file
BIN
desktop/src/assets/logos/quilt.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
desktop/src/assets/logos/vanilla.png
Normal file
BIN
desktop/src/assets/logos/vanilla.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
|
|
@ -1,5 +1,5 @@
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { listen } from '@tauri-apps/api/event';
|
import { type UnlistenFn, listen } from '@tauri-apps/api/event';
|
||||||
|
|
||||||
const _test_cluster = {
|
const _test_cluster = {
|
||||||
cluster: {
|
cluster: {
|
||||||
|
|
@ -32,6 +32,28 @@ export async function getCluster(uuid: string): Promise<Core.ClusterWithManifest
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getClusterLogs(uuid: string): Promise<string[]> {
|
||||||
|
return ['test log'];
|
||||||
|
try {
|
||||||
|
return await invoke<string[]>(
|
||||||
|
'plugin:onelauncher|get_cluster_logs',
|
||||||
|
{ uuid },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getClusterLog(uuid: string, log: string): Promise<string> {
|
||||||
|
return 'test log';
|
||||||
|
return await invoke<string>(
|
||||||
|
'plugin:onelauncher|get_cluster_log',
|
||||||
|
{ uuid, log },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getClustersGrouped(): Promise<Map<string, WithIndex<Core.ClusterWithManifest>[]>> {
|
export async function getClustersGrouped(): Promise<Map<string, WithIndex<Core.ClusterWithManifest>[]>> {
|
||||||
const clusters = await getClusters();
|
const clusters = await getClusters();
|
||||||
const map = new Map<string, WithIndex<Core.ClusterWithManifest>[]>();
|
const map = new Map<string, WithIndex<Core.ClusterWithManifest>[]>();
|
||||||
|
|
@ -80,19 +102,55 @@ interface LaunchCallbacks {
|
||||||
on_stderr: (line: string) => any;
|
on_stderr: (line: string) => any;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function launchCluster(uuid: string, callbacks: LaunchCallbacks): Promise<number> {
|
export function launchCluster(uuid: string, callbacks?: LaunchCallbacks): Promise<number> {
|
||||||
const unlisten_launch = await listen<number>('game:launch', e => callbacks.on_launch(e.payload));
|
// eslint-disable-next-line no-async-promise-executor
|
||||||
const unlisten_stdout = await listen<string>('game:stdout', e => callbacks.on_stdout(e.payload));
|
return new Promise(async (resolve) => {
|
||||||
const unlisten_stderr = await listen<string>('game:stderr', e => callbacks.on_stderr(e.payload));
|
let unlisten_stdout: UnlistenFn | undefined;
|
||||||
|
let unlisten_stderr: UnlistenFn | undefined;
|
||||||
|
let pid: number = -1;
|
||||||
|
|
||||||
const exit_code = await invoke<number>(
|
function guard(passed_pid: number, fn: () => any | undefined) {
|
||||||
'plugin:onelauncher|launch_cluster',
|
if (passed_pid !== pid)
|
||||||
{ uuid },
|
return;
|
||||||
);
|
|
||||||
|
|
||||||
unlisten_launch();
|
if (fn)
|
||||||
unlisten_stdout();
|
fn();
|
||||||
unlisten_stderr();
|
}
|
||||||
|
|
||||||
return exit_code;
|
const unlisten_launch = await listen<number>('game:launch', async (e) => {
|
||||||
|
pid = e.payload;
|
||||||
|
|
||||||
|
if (callbacks)
|
||||||
|
callbacks.on_launch(pid);
|
||||||
|
|
||||||
|
unlisten_launch!();
|
||||||
|
});
|
||||||
|
|
||||||
|
const unlisten_exit = await listen<[number, number]>(`game:exit`, e => guard(e.payload[0], () => {
|
||||||
|
if (callbacks) {
|
||||||
|
unlisten_stdout!();
|
||||||
|
unlisten_stderr!();
|
||||||
|
}
|
||||||
|
|
||||||
|
unlisten_exit!();
|
||||||
|
resolve(e.payload[1]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (callbacks) {
|
||||||
|
unlisten_stdout = await listen<[number, string]>(`game:stdout`, e => guard(
|
||||||
|
e.payload[0],
|
||||||
|
callbacks.on_stdout(e.payload[1]),
|
||||||
|
));
|
||||||
|
|
||||||
|
unlisten_stderr = await listen<[number, string]>(`game:stderr`, e => guard(
|
||||||
|
e.payload[0],
|
||||||
|
callbacks.on_stderr(e.payload[1]),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
await invoke<number>(
|
||||||
|
'plugin:onelauncher|launch_cluster',
|
||||||
|
{ uuid },
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ img {
|
||||||
}
|
}
|
||||||
/* end lynith's native experience (tm) */
|
/* end lynith's native experience (tm) */
|
||||||
|
|
||||||
h1 { @apply text-2xl font-semibold; }
|
h1 { @apply text-2xl font-semibold; line-height: 1.5 !important; }
|
||||||
h2 { @apply text-xxl font-semibold; }
|
h2 { @apply text-xxl font-semibold; line-height: 1.25 !important; }
|
||||||
h3 { @apply text-xl font-medium; }
|
h3 { @apply text-xl font-medium; }
|
||||||
h4 { @apply text-lg font-medium; }
|
h4 { @apply text-lg font-medium; }
|
||||||
|
|
||||||
|
|
@ -31,3 +31,7 @@ h4 { @apply text-lg font-medium; }
|
||||||
--os-handle-bg-hover: theme(colors.gray[0.10]); */
|
--os-handle-bg-hover: theme(colors.gray[0.10]); */
|
||||||
/* --os-handle-bg-active: theme(colors.gray[0.05]); */
|
/* --os-handle-bg-active: theme(colors.gray[0.05]); */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.os-hide-horizontal-scrollbar > .os-scrollbar-horizontal {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,19 @@ import App from './ui/App';
|
||||||
import HomePage from './ui/pages/Home';
|
import HomePage from './ui/pages/Home';
|
||||||
import BrowserPage from './ui/pages/Browser';
|
import BrowserPage from './ui/pages/Browser';
|
||||||
import UpdatesPage from './ui/pages/Updates';
|
import UpdatesPage from './ui/pages/Updates';
|
||||||
import ClusterPage from './ui/pages/cluster';
|
|
||||||
import ClusterOverview from './ui/pages/cluster/ClusterOverview';
|
import ClusterOverview from './ui/pages/cluster/ClusterOverview';
|
||||||
import ClusterMods from './ui/pages/cluster/ClusterMods';
|
import ClusterMods from './ui/pages/cluster/ClusterMods';
|
||||||
|
import ClusterRoot from '~ui/pages/cluster/ClusterRoot';
|
||||||
|
import ClusterLogs from '~ui/pages/cluster/ClusterLogs';
|
||||||
|
|
||||||
render(() => (
|
render(() => (
|
||||||
<Router root={App}>
|
<Router root={App}>
|
||||||
<Route path="/" component={HomePage} />
|
<Route path="/" component={HomePage} />
|
||||||
<Route path="/browser" component={BrowserPage} />
|
<Route path="/browser" component={BrowserPage} />
|
||||||
<Route path="/updates" component={UpdatesPage} />
|
<Route path="/updates" component={UpdatesPage} />
|
||||||
<Route path="/clusters" component={ClusterPage}>
|
<Route path="/clusters" component={ClusterRoot}>
|
||||||
<Route path="/" component={ClusterOverview} />
|
<Route path="/" component={ClusterOverview} />
|
||||||
|
<Route path="/logs" component={ClusterLogs} />
|
||||||
<Route path="/mods" component={ClusterMods} />
|
<Route path="/mods" component={ClusterMods} />
|
||||||
</Route>
|
</Route>
|
||||||
</Router>
|
</Router>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import type { ParentProps } from 'solid-js';
|
import type { ParentProps } from 'solid-js';
|
||||||
import { Transition } from 'solid-transition-group';
|
|
||||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-solid';
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-solid';
|
||||||
import environment from '../utils/environment';
|
import environment from '../utils/environment';
|
||||||
import WindowFrame from './components/WindowFrame';
|
import WindowFrame from './components/WindowFrame';
|
||||||
import Navbar from './components/Navbar';
|
import Navbar from './components/Navbar';
|
||||||
import ErrorBoundary from './components/ErrorBoundary';
|
import ErrorBoundary from './components/ErrorBoundary';
|
||||||
|
import AnimatedRoutes from './components/AnimatedRoutes';
|
||||||
|
|
||||||
function App(props: ParentProps) {
|
function App(props: ParentProps) {
|
||||||
if (!environment.isDev()) {
|
if (!environment.isDev()) {
|
||||||
|
|
@ -14,8 +14,8 @@ function App(props: ParentProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main class="flex flex-col bg-primary w-full min-h-screen overflow-hidden h-screen max-h-screen text-white">
|
<main class="flex flex-col bg-primary w-full min-h-screen overflow-hidden h-screen max-h-screen text-fg-primary">
|
||||||
<WindowFrame />
|
<ErrorBoundary><WindowFrame /></ErrorBoundary>
|
||||||
<div class="flex flex-col px-8">
|
<div class="flex flex-col px-8">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -23,71 +23,16 @@ function App(props: ParentProps) {
|
||||||
<div class="relative h-full w-full overflow-hidden">
|
<div class="relative h-full w-full overflow-hidden">
|
||||||
<div class="absolute top-0 left-0 flex flex-col h-full w-full overflow-x-hidden">
|
<div class="absolute top-0 left-0 flex flex-col h-full w-full overflow-x-hidden">
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<OverlayScrollbarsComponent class="absolute top-0 left-0 flex flex-col h-full w-full overflow-x-hidden overflow-y-auto px-8">
|
<OverlayScrollbarsComponent class="os-hide-horizontal-scrollbar absolute top-0 left-0 flex flex-col h-full w-full overflow-x-hidden overflow-y-auto px-8 pb-8">
|
||||||
<AnimatedRoutes>
|
<AnimatedRoutes>
|
||||||
{props.children}
|
{props.children}
|
||||||
</AnimatedRoutes>
|
</AnimatedRoutes>
|
||||||
<div class="pb-8" />
|
|
||||||
</OverlayScrollbarsComponent>
|
</OverlayScrollbarsComponent>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
||||||
function AnimatedRoutes(props: ParentProps) {
|
|
||||||
const keyframesEnter = [
|
|
||||||
{
|
|
||||||
opacity: 0,
|
|
||||||
transform: 'translateX(-100px)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
opacity: 1,
|
|
||||||
transform: 'translateX(0px)',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const keyframesExit = [
|
|
||||||
{
|
|
||||||
opacity: 1,
|
|
||||||
transform: 'translateX(0px)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
opacity: 0,
|
|
||||||
transform: 'translateX(100px)',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const properties: KeyframeAnimationOptions = {
|
|
||||||
duration: 100,
|
|
||||||
easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition
|
|
||||||
mode="outin"
|
|
||||||
onEnter={(element, done) => {
|
|
||||||
const animation = element.animate(
|
|
||||||
keyframesEnter,
|
|
||||||
properties,
|
|
||||||
);
|
|
||||||
|
|
||||||
animation.onfinish = done;
|
|
||||||
}}
|
|
||||||
onExit={(element, done) => {
|
|
||||||
const animation = element.animate(
|
|
||||||
keyframesExit,
|
|
||||||
properties,
|
|
||||||
);
|
|
||||||
|
|
||||||
animation.onfinish = done;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</Transition>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
57
desktop/src/ui/components/AnimatedRoutes.tsx
Normal file
57
desktop/src/ui/components/AnimatedRoutes.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import type { ParentProps } from 'solid-js';
|
||||||
|
import { Transition } from 'solid-transition-group';
|
||||||
|
|
||||||
|
function AnimatedRoutes(props: ParentProps) {
|
||||||
|
const keyframesEnter = [
|
||||||
|
{
|
||||||
|
opacity: 0,
|
||||||
|
transform: 'translateX(-100px)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
opacity: 1,
|
||||||
|
transform: 'translateX(0px)',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const keyframesExit = [
|
||||||
|
{
|
||||||
|
opacity: 1,
|
||||||
|
transform: 'translateX(0px)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
opacity: 0,
|
||||||
|
transform: 'translateX(100px)',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const properties: KeyframeAnimationOptions = {
|
||||||
|
duration: 100,
|
||||||
|
easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition
|
||||||
|
mode="outin"
|
||||||
|
onEnter={(element, done) => {
|
||||||
|
const animation = element.animate(
|
||||||
|
keyframesEnter,
|
||||||
|
properties,
|
||||||
|
);
|
||||||
|
|
||||||
|
animation.onfinish = done;
|
||||||
|
}}
|
||||||
|
onExit={(element, done) => {
|
||||||
|
const animation = element.animate(
|
||||||
|
keyframesExit,
|
||||||
|
properties,
|
||||||
|
);
|
||||||
|
|
||||||
|
animation.onfinish = done;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Transition>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AnimatedRoutes;
|
||||||
70
desktop/src/ui/components/Sidebar.tsx
Normal file
70
desktop/src/ui/components/Sidebar.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { useLocation, useNavigate } from '@solidjs/router';
|
||||||
|
import type { JSX, ParentProps } from 'solid-js';
|
||||||
|
import { For, createEffect } from 'solid-js';
|
||||||
|
|
||||||
|
type SidebarProps = ParentProps & {
|
||||||
|
state: {
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
base: string;
|
||||||
|
links: {
|
||||||
|
[key: string]: [JSX.Element, string, string][];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function Sidebar(props: SidebarProps) {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (props.base.endsWith('/'))
|
||||||
|
throw new Error('Base should not end with a slash');
|
||||||
|
|
||||||
|
Object.keys(props.state).forEach((key) => {
|
||||||
|
searchParams.set(key, props.state[key]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function goto(href: string) {
|
||||||
|
const url = `${props.base}${href}?${searchParams.toString()}`;
|
||||||
|
navigate(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isActive(link: string) {
|
||||||
|
return location.pathname === `${props.base}${link}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="flex flex-col w-52">
|
||||||
|
<For each={Object.keys(props.links)}>
|
||||||
|
{section => (
|
||||||
|
<div class="flex flex-col gap-y-2">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-fg-secondary text-xs font-medium ml-1.5 m-1">{section.toUpperCase()}</h3>
|
||||||
|
<div class="flex flex-col gap-y-1 fill-fg-primary text-fg-primary font-medium">
|
||||||
|
<For each={props.links[section]}>
|
||||||
|
{link => (
|
||||||
|
<a
|
||||||
|
onClick={() => goto(link[2])}
|
||||||
|
class={
|
||||||
|
`px-3 py-1 rounded-md text-md hover:bg-component-bg-hover active:bg-component-bg-pressed ${isActive(link[2]) ? 'bg-gray-0.05' : ''}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span class="flex flex-row items-center gap-x-3 *:w-5">
|
||||||
|
{link[0]}
|
||||||
|
{link[1]}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Sidebar;
|
||||||
15
desktop/src/ui/components/game/ClusterCover.tsx
Normal file
15
desktop/src/ui/components/game/ClusterCover.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { type JSX, splitProps } from 'solid-js';
|
||||||
|
import defaultCover from '~assets/images/default_instance_cover.jpg';
|
||||||
|
|
||||||
|
type ClusterCoverProps = JSX.HTMLAttributes<HTMLImageElement> & {
|
||||||
|
cluster: Core.Cluster;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ClusterCover(props: ClusterCoverProps) {
|
||||||
|
const [split, rest] = splitProps(props, ['cluster']);
|
||||||
|
return (
|
||||||
|
<img {...rest} src={split.cluster.cover || defaultCover} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ClusterCover;
|
||||||
31
desktop/src/ui/components/game/LoaderIcon.tsx
Normal file
31
desktop/src/ui/components/game/LoaderIcon.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { type JSX, splitProps } from 'solid-js';
|
||||||
|
import fabric from '~assets/logos/fabric.png';
|
||||||
|
import forge from '~assets/logos/forge.png';
|
||||||
|
import quilt from '~assets/logos/quilt.png';
|
||||||
|
import vanilla from '~assets/logos/vanilla.png';
|
||||||
|
|
||||||
|
export function getLoaderIcon(loader: Core.Loader) {
|
||||||
|
switch (loader) {
|
||||||
|
case 'Vanilla':
|
||||||
|
return vanilla;
|
||||||
|
case 'Forge':
|
||||||
|
return forge;
|
||||||
|
case 'Fabric':
|
||||||
|
return fabric;
|
||||||
|
case 'Quilt':
|
||||||
|
return quilt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoaderIconProp = JSX.HTMLAttributes<HTMLImageElement> & {
|
||||||
|
loader: Core.Loader;
|
||||||
|
};
|
||||||
|
|
||||||
|
function LoaderIcon(props: LoaderIconProp) {
|
||||||
|
const [split, rest] = splitProps(props, ['loader']);
|
||||||
|
return (
|
||||||
|
<img {...rest} src={getLoaderIcon(split.loader)} alt={split.loader} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoaderIcon;
|
||||||
13
desktop/src/ui/hooks/useCluster.ts
Normal file
13
desktop/src/ui/hooks/useCluster.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import type { Resource } from 'solid-js';
|
||||||
|
import { createResource } from 'solid-js';
|
||||||
|
import { getCluster } from '../../bridge/game';
|
||||||
|
|
||||||
|
function useCluster(uuid: string | undefined | null): Resource<Core.ClusterWithManifest> | null {
|
||||||
|
if (typeof uuid !== 'string' || uuid.length === 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const [resource] = createResource(uuid, getCluster);
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useCluster;
|
||||||
|
|
@ -10,12 +10,12 @@ import type { Accessor, Setter } from 'solid-js';
|
||||||
import { For, Show, createResource, createSignal } from 'solid-js';
|
import { For, Show, createResource, createSignal } from 'solid-js';
|
||||||
import { useNavigate } from '@solidjs/router';
|
import { useNavigate } from '@solidjs/router';
|
||||||
import image from '../../assets/images/header.png';
|
import image from '../../assets/images/header.png';
|
||||||
import defaultCover from '../../assets/images/default_instance_cover.jpg';
|
|
||||||
import ContextMenu from '../components/overlay/ContextMenu';
|
import ContextMenu from '../components/overlay/ContextMenu';
|
||||||
import { createCluster, getClustersGrouped, launchCluster } from '../../bridge/game';
|
import { createCluster, getClustersGrouped, launchCluster } from '../../bridge/game';
|
||||||
import Button from '../components/base/Button';
|
import Button from '../components/base/Button';
|
||||||
import Tag from '../components/base/Tag';
|
import Tag from '../components/base/Tag';
|
||||||
import TextField from '../components/base/TextField';
|
import TextField from '../components/base/TextField';
|
||||||
|
import ClusterCover from '~ui/components/game/ClusterCover';
|
||||||
|
|
||||||
// TODO: Replace this into it's own component
|
// TODO: Replace this into it's own component
|
||||||
// possibly with the core ui lirbary with autogenerated and optimized svgs pls
|
// possibly with the core ui lirbary with autogenerated and optimized svgs pls
|
||||||
|
|
@ -142,9 +142,9 @@ function ClusterCard(props: Core.ClusterWithManifest) {
|
||||||
class="absolute h-full w-full group-hover:!scale-110 transition-transform"
|
class="absolute h-full w-full group-hover:!scale-110 transition-transform"
|
||||||
style={{ '-webkit-transform': 'translateZ(0)' }}
|
style={{ '-webkit-transform': 'translateZ(0)' }}
|
||||||
>
|
>
|
||||||
<img
|
<ClusterCover
|
||||||
class={`object-cover h-full w-full ${props.cluster.cover ? '' : ' filter grayscale'}`}
|
cluster={props.cluster}
|
||||||
src={props.cluster.cover || defaultCover}
|
class="object-cover h-full w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
61
desktop/src/ui/pages/cluster/ClusterLogs.tsx
Normal file
61
desktop/src/ui/pages/cluster/ClusterLogs.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { useSearchParams } from '@solidjs/router';
|
||||||
|
import { For, Show, createEffect, createResource, createSignal } from 'solid-js';
|
||||||
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-solid';
|
||||||
|
import { getClusterLog, getClusterLogs } from '../../../bridge/game';
|
||||||
|
|
||||||
|
function ClusterLogs() {
|
||||||
|
const [params] = useSearchParams();
|
||||||
|
const [logs] = createResource(params.id, getClusterLogs);
|
||||||
|
|
||||||
|
const [activeLogFile, setActiveLogFile] = createSignal<string | null>(null);
|
||||||
|
const [log, setLog] = createSignal<string | null>(null);
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const log = logs()?.[0];
|
||||||
|
|
||||||
|
// Set default log to the first file
|
||||||
|
if (log && activeLogFile() === null)
|
||||||
|
setActiveLogFile(log);
|
||||||
|
|
||||||
|
// Fetch log content
|
||||||
|
if (activeLogFile() !== null) {
|
||||||
|
getClusterLog(params.id!, activeLogFile()!).then((log) => {
|
||||||
|
setLog(log);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="flex flex-col flex-1">
|
||||||
|
<h1>Logs</h1>
|
||||||
|
|
||||||
|
<div class="flex flex-col flex-1">
|
||||||
|
<div class="flex flex-row gap-x-1 overflow-auto">
|
||||||
|
<For each={logs() || []}>
|
||||||
|
{(log) => {
|
||||||
|
const pretty = log.split('/').pop();
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
class={`border border-gray-0.10 rounded-md px-2 py-1 ${activeLogFile() === log ? 'bg-gray-0.10' : ''}`}
|
||||||
|
onClick={() => setActiveLogFile(log)}
|
||||||
|
children={pretty}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* TODO: Try fix this when using OverlayScrollbarComponent https://github.com/KingSora/OverlayScrollbars/issues/627 */}
|
||||||
|
<Show when={log() !== null} fallback={<span>No logs found</span>}>
|
||||||
|
<div class="bg-component-bg flex flex-1 h-full font-mono font-medium overflow-auto p-2 rounded-md mt-2">
|
||||||
|
<OverlayScrollbarsComponent class="flex-1 h-full relative">
|
||||||
|
<code class="whitespace-pre select-text absolute">{log()}</code>
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ClusterLogs;
|
||||||
|
|
@ -1,7 +1,68 @@
|
||||||
|
import { useSearchParams } from '@solidjs/router';
|
||||||
|
import { Show } from 'solid-js';
|
||||||
|
import { PlayIcon, Share07Icon } from '@untitled-theme/icons-solid';
|
||||||
|
import useCluster from '../../hooks/useCluster';
|
||||||
|
import ClusterCover from '../../components/game/ClusterCover';
|
||||||
|
import LoaderIcon from '../../components/game/LoaderIcon';
|
||||||
|
import Button from '../../components/base/Button';
|
||||||
|
import { launchCluster } from '../../../bridge/game';
|
||||||
|
|
||||||
function ClusterOverview() {
|
function ClusterOverview() {
|
||||||
|
const [params] = useSearchParams();
|
||||||
|
const cluster = useCluster(params.id);
|
||||||
|
if (cluster === null)
|
||||||
|
throw new Error('Cluster doesn\'t exist');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div class="flex flex-col flex-1">
|
||||||
<h1>This is the overview</h1>
|
<h1>Overview</h1>
|
||||||
|
|
||||||
|
<Show when={!cluster.loading}>
|
||||||
|
<Banner {...cluster()!} />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line solid/no-destructure
|
||||||
|
function Banner({ cluster, manifest }: Core.ClusterWithManifest) {
|
||||||
|
return (
|
||||||
|
<div class="flex flex-row bg-component-bg rounded-xl p-2.5 gap-x-2.5">
|
||||||
|
<div class="w-48 rounded-lg overflow-hidden border border-gray-0.10">
|
||||||
|
<ClusterCover cluster={cluster} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col flex-1 text-fg-primary">
|
||||||
|
<div class="flex-1">
|
||||||
|
<h2 class="text-2xl">{cluster.name}</h2>
|
||||||
|
<span class="flex flex-row items-center gap-x-1">
|
||||||
|
<LoaderIcon loader={cluster.client.type} class="w-5" />
|
||||||
|
<span>{cluster.client.type}</span>
|
||||||
|
<span>{manifest.manifest.id}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-fg-secondary">
|
||||||
|
Played for
|
||||||
|
{' '}
|
||||||
|
<b>3.9</b>
|
||||||
|
{' '}
|
||||||
|
hours
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row items-end gap-x-2.5 *:h-8 *:w-8">
|
||||||
|
<Button
|
||||||
|
buttonStyle="secondary"
|
||||||
|
iconLeft={<Share07Icon />}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
buttonStyle="primary"
|
||||||
|
iconLeft={<PlayIcon />}
|
||||||
|
children="Launch"
|
||||||
|
class="!w-auto"
|
||||||
|
onClick={() => launchCluster(cluster.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
38
desktop/src/ui/pages/cluster/ClusterRoot.tsx
Normal file
38
desktop/src/ui/pages/cluster/ClusterRoot.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { useSearchParams } from '@solidjs/router';
|
||||||
|
import type { ParentProps } from 'solid-js';
|
||||||
|
import { EyeIcon, File06Icon, PackagePlusIcon } from '@untitled-theme/icons-solid';
|
||||||
|
import Sidebar from '../../components/Sidebar';
|
||||||
|
import AnimatedRoutes from '../../components/AnimatedRoutes';
|
||||||
|
import ErrorBoundary from '../../components/ErrorBoundary';
|
||||||
|
|
||||||
|
function ClusterRoot(props: ParentProps) {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="flex flex-row flex-1 h-full gap-x-7">
|
||||||
|
<div class="mt-8">
|
||||||
|
<Sidebar
|
||||||
|
base="/clusters"
|
||||||
|
state={{ id: searchParams.id }}
|
||||||
|
links={{
|
||||||
|
Cluster: [
|
||||||
|
[<EyeIcon />, 'Overview', '/'],
|
||||||
|
[<PackagePlusIcon />, 'Mods', '/mods'],
|
||||||
|
[<File06Icon />, 'Logs', '/logs'],
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col w-full h-full">
|
||||||
|
<AnimatedRoutes>
|
||||||
|
<ErrorBoundary>
|
||||||
|
{props.children}
|
||||||
|
</ErrorBoundary>
|
||||||
|
</AnimatedRoutes>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ClusterRoot;
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
import { A, type AnchorProps, useNavigate, useSearchParams } from '@solidjs/router';
|
|
||||||
import { type ParentProps, createEffect, splitProps } from 'solid-js';
|
|
||||||
|
|
||||||
function Link(props: AnchorProps) {
|
|
||||||
const [split, rest] = splitProps(props, ['href']);
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<A {...rest} href={`${split.href}?id=${searchParams.id}`} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ClusterPage(props: ParentProps) {
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (typeof searchParams.id !== 'string')
|
|
||||||
navigate('/');
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<Link href="/clusters/">Overview</Link>
|
|
||||||
<Link href="/clusters/mods">mods</Link>
|
|
||||||
</div>
|
|
||||||
{props.children}
|
|
||||||
{searchParams.id}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ClusterPage;
|
|
||||||
Loading…
Add table
Reference in a new issue