chore: Do some work on reimplementing the Rust side into the frontend side
This commit is contained in:
parent
e2495c78d4
commit
576c8f0d22
8 changed files with 129 additions and 46 deletions
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
21
desktop/src/ui/hooks/useCommand.ts
Normal file
21
desktop/src/ui/hooks/useCommand.ts
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue