chore: Do some work on reimplementing the Rust side into the frontend side

This commit is contained in:
LynithDev 2024-06-13 19:25:37 +02:00
parent e2495c78d4
commit 576c8f0d22
8 changed files with 129 additions and 46 deletions

View file

@ -42,6 +42,7 @@ pub enum ClusterStage {
} }
/// Relative Path wrapper to be used as an identifer for a cluster path. /// Relative Path wrapper to be used as an identifer for a cluster path.
#[cfg_attr(feature = "tauri", derive(specta::Type))]
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)] #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)]
#[serde(transparent)] #[serde(transparent)]
pub struct ClusterPath(pub PathBuf); pub struct ClusterPath(pub PathBuf);

View file

@ -1,6 +1,7 @@
use std::path::PathBuf; use std::{collections::HashMap, path::PathBuf, str::FromStr};
use onelauncher::{cluster, data::{Loader, PackageData}}; use chrono::DateTime;
use onelauncher::{cluster, data::{ClusterMeta, Loader, PackageData}, store::{Cluster, ClusterPath}, State};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specta::Type; use specta::Type;
use uuid::Uuid; use uuid::Uuid;
@ -11,9 +12,12 @@ macro_rules! collect_commands {
{ {
use $crate::api::commands::*; use $crate::api::commands::*;
tauri_specta::ts::builder() tauri_specta::ts::builder()
.config(specta::ts::ExportConfig::default().bigint(specta::ts::BigIntExportBehavior::BigInt))
.commands(tauri_specta::collect_commands![ .commands(tauri_specta::collect_commands![
is_dev, is_dev,
create_cluster create_cluster,
get_cluster,
get_clusters
]) ])
} }
}; };
@ -59,3 +63,57 @@ pub async fn create_cluster(props: CreateCluster) -> Result<Uuid, String> {
Err("Cluster does not exist".to_string()) Err("Cluster does not exist".to_string())
} }
} }
fn placeholder_cluster() -> Cluster {
let path = ClusterPath("test".into());
Cluster {
uuid: Uuid::from_str("56d1cbcf-1961-4477-b263-80e3b1c7a9d1").unwrap(),
stage: onelauncher::store::ClusterStage::Installed,
path: path.0,
meta: ClusterMeta {
created_at: DateTime::from_timestamp_millis(1718297861712).unwrap(),
modified_at: DateTime::from_timestamp_millis(1718297861712).unwrap(),
group: vec![],
icon: None,
icon_url: None,
loader: Loader::Vanilla,
loader_version: None,
mc_version: "1.8.9".into(),
name: "Test Cluster".into(),
overall_played: 58195,
recently_played: 0,
package_data: None,
played_at: None
},
memory: None,
java: None,
resolution: None,
force_fullscreen: None,
init_hooks: None,
packages: HashMap::new(),
update: None
}
}
#[specta::specta]
#[tauri::command]
pub async fn get_cluster(uuid: Uuid) -> Result<Cluster, String> {
Ok(placeholder_cluster())
// match cluster::get_by_uuid(uuid, None).await? {
// Some(cluster) => Ok(cluster),
// None => Err("Cluster does not exist".into())
// }
}
#[specta::specta]
#[tauri::command]
pub async fn get_clusters() -> Result<HashMap<ClusterPath, Cluster>, String> {
let mut map = HashMap::<ClusterPath, Cluster>::new();
let cluster = placeholder_cluster();
map.insert(cluster.cluster_path(), cluster);
Ok(map)
// cluster::list(None).await.map_err(|op| op.into())
}

View file

@ -1,14 +1,15 @@
import { type JSX, splitProps } from 'solid-js'; import { type JSX, splitProps } from 'solid-js';
import defaultCover from '~assets/images/default_instance_cover.jpg'; import defaultCover from '~assets/images/default_instance_cover.jpg';
import type { Cluster } from '~bindings';
type ClusterCoverProps = JSX.HTMLAttributes<HTMLImageElement> & { type ClusterCoverProps = JSX.HTMLAttributes<HTMLImageElement> & {
cluster: Core.Cluster; cluster: Cluster;
}; };
function ClusterCover(props: ClusterCoverProps) { function ClusterCover(props: ClusterCoverProps) {
const [split, rest] = splitProps(props, ['cluster']); const [split, rest] = splitProps(props, ['cluster']);
return ( return (
<img {...rest} src={split.cluster.cover || defaultCover} /> <img {...rest} src={split.cluster.meta.icon_url || defaultCover} />
); );
} }

View file

@ -3,28 +3,30 @@ import fabric from '~assets/logos/fabric.png';
import forge from '~assets/logos/forge.png'; import forge from '~assets/logos/forge.png';
import quilt from '~assets/logos/quilt.png'; import quilt from '~assets/logos/quilt.png';
import vanilla from '~assets/logos/vanilla.png'; import vanilla from '~assets/logos/vanilla.png';
import type { Loader } from '~bindings';
export function getLoaderIcon(loader: Core.Loader) { export function getLoaderIcon(loader: Loader): string {
switch (loader) { switch (loader) {
case 'Vanilla': case 'forge':
return vanilla;
case 'Forge':
return forge; return forge;
case 'Fabric': case 'fabric':
return fabric; return fabric;
case 'Quilt': case 'quilt':
return quilt; return quilt;
case 'vanilla':
default:
return vanilla;
} }
} }
type LoaderIconProp = JSX.HTMLAttributes<HTMLImageElement> & { type LoaderIconProp = JSX.HTMLAttributes<HTMLImageElement> & {
loader: Core.Loader; loader: Loader | undefined;
}; };
function LoaderIcon(props: LoaderIconProp) { function LoaderIcon(props: LoaderIconProp) {
const [split, rest] = splitProps(props, ['loader']); const [{ loader = 'vanilla' }, rest] = splitProps(props, ['loader']);
return ( return (
<img {...rest} src={getLoaderIcon(split.loader)} alt={split.loader} /> <img {...rest} src={getLoaderIcon(loader)} alt={loader} />
); );
} }

View file

@ -1,15 +1,13 @@
import type { Resource } from 'solid-js'; import type { Resource } from 'solid-js';
import { createResource } from 'solid-js'; import useCommand from './useCommand';
import type { Cluster } from '~bindings';
import { bridge } from '~index';
function getCluster(uuid: string): Core.ClusterWithManifest { function useCluster(uuid: string | undefined | null): Resource<Cluster> | null {
throw new Error('get cluster');
}
function useCluster(uuid: string | undefined | null): Resource<Core.ClusterWithManifest> | null {
if (typeof uuid !== 'string' || uuid.length === 0) if (typeof uuid !== 'string' || uuid.length === 0)
return null; return null;
const [resource] = createResource(uuid, getCluster); const [resource] = useCommand(bridge.commands.getCluster, uuid);
return resource; return resource;
} }

View file

@ -0,0 +1,21 @@
import { type Resource, type ResourceReturn, createResource } from 'solid-js';
import type { Result } from '~bindings';
/**
* A helper function which handles a Rust Result properly
* @param cmd The command to call
* @throws
*/
export default function useCommand<R, E, Args extends unknown[]>(
cmd: (...args: Args) => Promise<Result<R, E>>,
...args: Args
): ResourceReturn<R> {
return createResource(async () => {
const value = await cmd(...args);
if (value.status === 'ok')
return value.data;
throw value.error;
});
}

View file

@ -7,7 +7,7 @@ import {
SearchMdIcon, SearchMdIcon,
} from '@untitled-theme/icons-solid'; } from '@untitled-theme/icons-solid';
import type { Accessor, Setter } from 'solid-js'; import type { Accessor, Setter } from 'solid-js';
import { For, createSignal } from 'solid-js'; import { For, Show, 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 ContextMenu from '../components/overlay/ContextMenu'; import ContextMenu from '../components/overlay/ContextMenu';
@ -15,6 +15,9 @@ 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'; import ClusterCover from '~ui/components/game/ClusterCover';
import type { Cluster } from '~bindings';
import useCommand from '~ui/hooks/useCommand';
import { bridge } from '~index';
// 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
@ -97,14 +100,14 @@ function ClusterCardContextMenu(props: ClusterCardContextMenuProps) {
); );
} }
function ClusterCard(props: Core.ClusterWithManifest) { function ClusterCard(props: Cluster) {
const navigate = useNavigate(); const navigate = useNavigate();
const [pos, setPos] = createSignal({ x: 0, y: 0 }); const [pos, setPos] = createSignal({ x: 0, y: 0 });
const [contextMenuVisible, setContextMenuVisible] = createSignal(false); const [contextMenuVisible, setContextMenuVisible] = createSignal(false);
let ref!: HTMLDivElement; let ref!: HTMLDivElement;
function openClusterPage(_e: MouseEvent) { function openClusterPage(_e: MouseEvent) {
navigate(`/clusters/?id=${props.cluster.id}`); navigate(`/clusters/?id=${props.uuid}`);
} }
function openContextMenu(e: MouseEvent) { function openContextMenu(e: MouseEvent) {
@ -132,18 +135,18 @@ function ClusterCard(props: Core.ClusterWithManifest) {
style={{ '-webkit-transform': 'translateZ(0)' }} style={{ '-webkit-transform': 'translateZ(0)' }}
> >
<ClusterCover <ClusterCover
cluster={props.cluster} cluster={props}
class="object-cover h-full w-full" class="object-cover h-full w-full"
/> />
</div> </div>
</div> </div>
<div class="z-10 p-3 flex flex-row items-center justify-between"> <div class="z-10 p-3 flex flex-row items-center justify-between">
<div class="flex flex-col gap-1.5"> <div class="flex flex-col gap-1.5">
<p class="h-4 font-semibold">{props.cluster.name}</p> <p class="h-4 font-semibold">{props.meta.name}</p>
<p class="h-4 text-xs"> <p class="h-4 text-xs">
{props.cluster.client.type} {props.meta.loader}
{' '} {' '}
{props.manifest.manifest.id} {props.meta.mc_version}
{/* {' '} {/* {' '}
{props.instance.mods && `• ${props.mods} mods`} */} {props.instance.mods && `• ${props.mods} mods`} */}
</p> </p>
@ -158,7 +161,7 @@ function ClusterCard(props: Core.ClusterWithManifest) {
contextMenuVisible={contextMenuVisible} contextMenuVisible={contextMenuVisible}
setContextMenuVisible={setContextMenuVisible} setContextMenuVisible={setContextMenuVisible}
pos={pos} pos={pos}
id={props.cluster.id} id={props.uuid}
/> />
</> </>
); );
@ -166,7 +169,7 @@ function ClusterCard(props: Core.ClusterWithManifest) {
interface ClusterGroupProps { interface ClusterGroupProps {
title: string; title: string;
clusters: Core.ClusterWithManifest[]; clusters: Cluster[];
} }
function ClusterGroup(props: ClusterGroupProps) { function ClusterGroup(props: ClusterGroupProps) {
@ -181,7 +184,7 @@ function ClusterGroup(props: ClusterGroupProps) {
} }
function HomePage() { function HomePage() {
// const [clusters, { refetch }] = createResource(getClustersGrouped); const [clusters, { refetch }] = useCommand(bridge.commands.getClusters);
async function newInstance() { async function newInstance() {
// bridge.commands.createCluster({ // bridge.commands.createCluster({
@ -225,13 +228,9 @@ function HomePage() {
</div> </div>
</div> </div>
{/* <Show when={clusters() !== undefined}> <Show when={clusters() !== undefined}>
<For each={Array.from(clusters()!.entries())}> <ClusterGroup title="Placeholder" clusters={Object.values(clusters()!)} />
{([title, clusters]) => ( </Show>
<ClusterGroup title={title} clusters={clusters} />
)}
</For>
</Show> */}
</div> </div>
); );
} }

View file

@ -7,6 +7,8 @@ import LoaderIcon from '../../../components/game/LoaderIcon';
import Button from '../../../components/base/Button'; import Button from '../../../components/base/Button';
import ScrollableContainer from '~ui/components/ScrollableContainer'; import ScrollableContainer from '~ui/components/ScrollableContainer';
import Sidebar from '~ui/components/Sidebar'; import Sidebar from '~ui/components/Sidebar';
import type { Cluster } from '~bindings';
import { upperFirst } from '~utils/string';
function ClusterOverview() { function ClusterOverview() {
const [params] = useSearchParams(); const [params] = useSearchParams();
@ -26,29 +28,30 @@ function ClusterOverview() {
); );
} }
// eslint-disable-next-line solid/no-destructure function Banner(props: Cluster) {
function Banner({ cluster, manifest }: Core.ClusterWithManifest) {
return ( return (
<div class="flex flex-row bg-component-bg rounded-xl p-2.5 gap-x-2.5"> <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-10"> <div class="w-48 rounded-lg overflow-hidden border border-gray-10">
<ClusterCover cluster={cluster} /> <ClusterCover cluster={props} />
</div> </div>
<div class="flex flex-col flex-1 text-fg-primary"> <div class="flex flex-col flex-1 text-fg-primary">
<div class="flex-1"> <div class="flex-1">
<h2 class="text-2xl">{cluster.name}</h2> <h2 class="text-2xl">{props.meta.name}</h2>
<span class="flex flex-row items-center gap-x-1"> <span class="flex flex-row items-center gap-x-1">
<LoaderIcon loader={cluster.client.type} class="w-5" /> <LoaderIcon loader={props.meta.loader} class="w-5" />
<span>{cluster.client.type}</span> <span>{upperFirst(props.meta.loader || 'unknown')}</span>
<span>{manifest.manifest.id}</span> {props.meta.loader_version && <span>{props.meta.loader_version.id}</span>}
<span>{props.meta.mc_version}</span>
</span> </span>
</div> </div>
<span class="text-xs text-fg-secondary"> <span class="text-xs text-fg-secondary">
Played for Played for
{' '} {' '}
<b>3.9</b> <b>{((props.meta.overall_played || 0n)).toString()}</b>
{' '} {' '}
hours hours
{/* TODO: Ask Pauline if this is measured in seconds or milliseconds */}
</span> </span>
</div> </div>