diff --git a/desktop/src/assets/logos/fabric.png b/desktop/src/assets/logos/fabric.png new file mode 100644 index 00000000..4ab8370f Binary files /dev/null and b/desktop/src/assets/logos/fabric.png differ diff --git a/desktop/src/assets/logos/forge.png b/desktop/src/assets/logos/forge.png new file mode 100644 index 00000000..5b8e835c Binary files /dev/null and b/desktop/src/assets/logos/forge.png differ diff --git a/desktop/src/assets/logos/quilt.png b/desktop/src/assets/logos/quilt.png new file mode 100644 index 00000000..d67ff6e9 Binary files /dev/null and b/desktop/src/assets/logos/quilt.png differ diff --git a/desktop/src/assets/logos/vanilla.png b/desktop/src/assets/logos/vanilla.png new file mode 100644 index 00000000..098bcb58 Binary files /dev/null and b/desktop/src/assets/logos/vanilla.png differ diff --git a/desktop/src/bridge/game.ts b/desktop/src/bridge/game.ts index 24b06a94..048e5443 100644 --- a/desktop/src/bridge/game.ts +++ b/desktop/src/bridge/game.ts @@ -1,5 +1,5 @@ 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 = { cluster: { @@ -32,6 +32,28 @@ export async function getCluster(uuid: string): Promise { + return ['test log']; + try { + return await invoke( + 'plugin:onelauncher|get_cluster_logs', + { uuid }, + ); + } + catch (e) { + console.error(e); + return []; + } +} + +export async function getClusterLog(uuid: string, log: string): Promise { + return 'test log'; + return await invoke( + 'plugin:onelauncher|get_cluster_log', + { uuid, log }, + ); +} + export async function getClustersGrouped(): Promise[]>> { const clusters = await getClusters(); const map = new Map[]>(); @@ -80,19 +102,55 @@ interface LaunchCallbacks { on_stderr: (line: string) => any; }; -export async function launchCluster(uuid: string, callbacks: LaunchCallbacks): Promise { - const unlisten_launch = await listen('game:launch', e => callbacks.on_launch(e.payload)); - const unlisten_stdout = await listen('game:stdout', e => callbacks.on_stdout(e.payload)); - const unlisten_stderr = await listen('game:stderr', e => callbacks.on_stderr(e.payload)); +export function launchCluster(uuid: string, callbacks?: LaunchCallbacks): Promise { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve) => { + let unlisten_stdout: UnlistenFn | undefined; + let unlisten_stderr: UnlistenFn | undefined; + let pid: number = -1; - const exit_code = await invoke( - 'plugin:onelauncher|launch_cluster', - { uuid }, - ); + function guard(passed_pid: number, fn: () => any | undefined) { + if (passed_pid !== pid) + return; - unlisten_launch(); - unlisten_stdout(); - unlisten_stderr(); + if (fn) + fn(); + } - return exit_code; + const unlisten_launch = await listen('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( + 'plugin:onelauncher|launch_cluster', + { uuid }, + ); + }); } diff --git a/desktop/src/globals.css b/desktop/src/globals.css index d7738ecf..eaf54c27 100644 --- a/desktop/src/globals.css +++ b/desktop/src/globals.css @@ -19,8 +19,8 @@ img { } /* end lynith's native experience (tm) */ -h1 { @apply text-2xl font-semibold; } -h2 { @apply text-xxl font-semibold; } +h1 { @apply text-2xl font-semibold; line-height: 1.5 !important; } +h2 { @apply text-xxl font-semibold; line-height: 1.25 !important; } h3 { @apply text-xl 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-active: theme(colors.gray[0.05]); */ } + +.os-hide-horizontal-scrollbar > .os-scrollbar-horizontal { + display: none; +} diff --git a/desktop/src/index.tsx b/desktop/src/index.tsx index 160d74a0..10b26960 100644 --- a/desktop/src/index.tsx +++ b/desktop/src/index.tsx @@ -8,17 +8,19 @@ import App from './ui/App'; import HomePage from './ui/pages/Home'; import BrowserPage from './ui/pages/Browser'; import UpdatesPage from './ui/pages/Updates'; -import ClusterPage from './ui/pages/cluster'; import ClusterOverview from './ui/pages/cluster/ClusterOverview'; import ClusterMods from './ui/pages/cluster/ClusterMods'; +import ClusterRoot from '~ui/pages/cluster/ClusterRoot'; +import ClusterLogs from '~ui/pages/cluster/ClusterLogs'; render(() => ( - + + diff --git a/desktop/src/ui/App.tsx b/desktop/src/ui/App.tsx index 3fe479f8..83e5f422 100644 --- a/desktop/src/ui/App.tsx +++ b/desktop/src/ui/App.tsx @@ -1,10 +1,10 @@ import type { ParentProps } from 'solid-js'; -import { Transition } from 'solid-transition-group'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-solid'; import environment from '../utils/environment'; import WindowFrame from './components/WindowFrame'; import Navbar from './components/Navbar'; import ErrorBoundary from './components/ErrorBoundary'; +import AnimatedRoutes from './components/AnimatedRoutes'; function App(props: ParentProps) { if (!environment.isDev()) { @@ -14,8 +14,8 @@ function App(props: ParentProps) { } return ( -
- +
+
@@ -23,71 +23,16 @@ function App(props: ParentProps) {
- + {props.children} -
-
); } -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 ( - { - const animation = element.animate( - keyframesEnter, - properties, - ); - - animation.onfinish = done; - }} - onExit={(element, done) => { - const animation = element.animate( - keyframesExit, - properties, - ); - - animation.onfinish = done; - }} - > - {props.children} - - ); -} +export default App; \ No newline at end of file diff --git a/desktop/src/ui/components/AnimatedRoutes.tsx b/desktop/src/ui/components/AnimatedRoutes.tsx new file mode 100644 index 00000000..a9929feb --- /dev/null +++ b/desktop/src/ui/components/AnimatedRoutes.tsx @@ -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 ( + { + const animation = element.animate( + keyframesEnter, + properties, + ); + + animation.onfinish = done; + }} + onExit={(element, done) => { + const animation = element.animate( + keyframesExit, + properties, + ); + + animation.onfinish = done; + }} + > + {props.children} + + ); +} + +export default AnimatedRoutes; diff --git a/desktop/src/ui/components/Sidebar.tsx b/desktop/src/ui/components/Sidebar.tsx new file mode 100644 index 00000000..d0550e6b --- /dev/null +++ b/desktop/src/ui/components/Sidebar.tsx @@ -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 ( + + ); +} + +export default Sidebar; diff --git a/desktop/src/ui/components/game/ClusterCover.tsx b/desktop/src/ui/components/game/ClusterCover.tsx new file mode 100644 index 00000000..e8b22c69 --- /dev/null +++ b/desktop/src/ui/components/game/ClusterCover.tsx @@ -0,0 +1,15 @@ +import { type JSX, splitProps } from 'solid-js'; +import defaultCover from '~assets/images/default_instance_cover.jpg'; + +type ClusterCoverProps = JSX.HTMLAttributes & { + cluster: Core.Cluster; +}; + +function ClusterCover(props: ClusterCoverProps) { + const [split, rest] = splitProps(props, ['cluster']); + return ( + + ); +} + +export default ClusterCover; diff --git a/desktop/src/ui/components/game/LoaderIcon.tsx b/desktop/src/ui/components/game/LoaderIcon.tsx new file mode 100644 index 00000000..0b3c7860 --- /dev/null +++ b/desktop/src/ui/components/game/LoaderIcon.tsx @@ -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 & { + loader: Core.Loader; +}; + +function LoaderIcon(props: LoaderIconProp) { + const [split, rest] = splitProps(props, ['loader']); + return ( + {split.loader} + ); +} + +export default LoaderIcon; diff --git a/desktop/src/ui/hooks/useCluster.ts b/desktop/src/ui/hooks/useCluster.ts new file mode 100644 index 00000000..9d0276c8 --- /dev/null +++ b/desktop/src/ui/hooks/useCluster.ts @@ -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 | null { + if (typeof uuid !== 'string' || uuid.length === 0) + return null; + + const [resource] = createResource(uuid, getCluster); + return resource; +} + +export default useCluster; diff --git a/desktop/src/ui/pages/Home.tsx b/desktop/src/ui/pages/Home.tsx index 55bc3334..51d2ae44 100644 --- a/desktop/src/ui/pages/Home.tsx +++ b/desktop/src/ui/pages/Home.tsx @@ -10,12 +10,12 @@ import type { Accessor, Setter } from 'solid-js'; import { For, Show, createResource, createSignal } from 'solid-js'; import { useNavigate } from '@solidjs/router'; import image from '../../assets/images/header.png'; -import defaultCover from '../../assets/images/default_instance_cover.jpg'; import ContextMenu from '../components/overlay/ContextMenu'; import { createCluster, getClustersGrouped, launchCluster } from '../../bridge/game'; import Button from '../components/base/Button'; import Tag from '../components/base/Tag'; import TextField from '../components/base/TextField'; +import ClusterCover from '~ui/components/game/ClusterCover'; // TODO: Replace this into it's own component // 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" style={{ '-webkit-transform': 'translateZ(0)' }} > - diff --git a/desktop/src/ui/pages/cluster/ClusterLogs.tsx b/desktop/src/ui/pages/cluster/ClusterLogs.tsx new file mode 100644 index 00000000..046ae7c2 --- /dev/null +++ b/desktop/src/ui/pages/cluster/ClusterLogs.tsx @@ -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(null); + const [log, setLog] = createSignal(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 ( +
+

Logs

+ +
+
+ + {(log) => { + const pretty = log.split('/').pop(); + return ( +
+ + {/* TODO: Try fix this when using OverlayScrollbarComponent https://github.com/KingSora/OverlayScrollbars/issues/627 */} + No logs found}> +
+ + {log()} + +
+
+
+
+ ); +} + +export default ClusterLogs; diff --git a/desktop/src/ui/pages/cluster/ClusterOverview.tsx b/desktop/src/ui/pages/cluster/ClusterOverview.tsx index 9ac16c57..88b243bf 100644 --- a/desktop/src/ui/pages/cluster/ClusterOverview.tsx +++ b/desktop/src/ui/pages/cluster/ClusterOverview.tsx @@ -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() { + const [params] = useSearchParams(); + const cluster = useCluster(params.id); + if (cluster === null) + throw new Error('Cluster doesn\'t exist'); + return ( -
-

This is the overview

+
+

Overview

+ + + + +
+ ); +} + +// eslint-disable-next-line solid/no-destructure +function Banner({ cluster, manifest }: Core.ClusterWithManifest) { + return ( +
+
+ +
+ +
+
+

{cluster.name}

+ + + {cluster.client.type} + {manifest.manifest.id} + +
+ + Played for + {' '} + 3.9 + {' '} + hours + +
+ +
+
); } diff --git a/desktop/src/ui/pages/cluster/ClusterRoot.tsx b/desktop/src/ui/pages/cluster/ClusterRoot.tsx new file mode 100644 index 00000000..41254d84 --- /dev/null +++ b/desktop/src/ui/pages/cluster/ClusterRoot.tsx @@ -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 ( +
+
+ , 'Overview', '/'], + [, 'Mods', '/mods'], + [, 'Logs', '/logs'], + ], + }} + /> +
+ +
+ + + {props.children} + + +
+
+ ); +} + +export default ClusterRoot; diff --git a/desktop/src/ui/pages/cluster/index.tsx b/desktop/src/ui/pages/cluster/index.tsx deleted file mode 100644 index 1205de77..00000000 --- a/desktop/src/ui/pages/cluster/index.tsx +++ /dev/null @@ -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 ( - - ); -} - -function ClusterPage(props: ParentProps) { - const [searchParams] = useSearchParams(); - const navigate = useNavigate(); - - createEffect(() => { - if (typeof searchParams.id !== 'string') - navigate('/'); - }); - - return ( -
-
- Overview - mods -
- {props.children} - {searchParams.id} -
- ); -} - -export default ClusterPage;