fix: Minecraft manifest deserializing

This commit is contained in:
LynithDev 2024-03-26 20:01:46 +01:00
parent b0b1aa48e9
commit 2af86b5fda
3 changed files with 107 additions and 161 deletions

View file

@ -1,4 +1,7 @@
use serde::{Serialize, Deserialize};
use core::fmt;
use std::{collections::HashMap, marker::PhantomData};
use serde::{de::{self, SeqAccess, Visitor}, Deserialize, Deserializer, Serialize};
use serde_json::Value;
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
@ -6,32 +9,30 @@ use serde_json::Value;
pub struct MinecraftManifest {
pub asset_index: AssetIndex,
pub downloads: Downloads,
#[serde(rename = "id")]
pub version: String,
pub java_version: JavaVersion,
pub libraries: Vec<Library>,
pub logging: Logging,
pub main_class: String,
pub arguments: Arguments,
pub release_time: String,
#[serde(rename = "id")]
pub version: String,
#[serde(rename = "type")]
pub release_type: ReleaseType,
#[serde(skip_serializing_if = "Option::is_none")]
pub minecraft_arguments: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<ModernArguments>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(untagged)]
pub enum Arguments {
// TODO: https://serde.rs/string-or-struct.html
MinecraftArguments(String),
Arguments {
game: Vec<ModernArgumentsItem>,
jvm: Vec<ModernArgumentsItem>,
}
}
impl Default for Arguments {
fn default() -> Self { Arguments::MinecraftArguments(String::new()) }
pub struct ModernArguments {
game: Vec<ModernArgumentsItem>,
jvm: Vec<ModernArgumentsItem>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@ -39,18 +40,18 @@ impl Default for Arguments {
#[serde(untagged)]
pub enum ModernArgumentsItem {
Simple(String),
Rule {
rules: Vec<Rule>,
value: ArgumentRuleValue,
},
Rule(ArgumentRule),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(untagged)]
pub enum ArgumentRuleValue {
String(String),
List(Vec<String>),
pub struct ArgumentRule {
#[serde(skip_serializing_if = "Vec::is_empty")]
rules: Vec<Rule>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(deserialize_with = "string_or_seq")]
value: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@ -96,17 +97,17 @@ pub struct JavaVersion {
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Library {
pub downloads: Downloads2,
pub downloads: LibraryDownload,
pub name: String,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub rules: Vec<Rule>,
#[serde(skip_serializing_if = "Option::is_none")]
pub natives: Option<Natives>,
pub natives: Option<HashMap<String, String>>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Downloads2 {
pub struct LibraryDownload {
#[serde(skip_serializing_if = "Option::is_none")]
pub artifact: Option<Artifact>,
#[serde(skip_serializing_if = "Option::is_none")]
@ -126,23 +127,15 @@ pub struct Artifact {
pub struct Classifiers {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "natives-osx")]
pub natives_osx: Option<Native>,
pub natives_osx: Option<Artifact>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "natives-linux")]
pub natives_linux: Option<Native>,
pub natives_linux: Option<Artifact>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "natives-windows")]
pub natives_windows: Option<Native>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Native {
pub path: String,
pub sha1: String,
pub url: String,
pub natives_windows: Option<Artifact>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@ -154,33 +147,28 @@ pub struct Rule {
pub os: Option<Os>,
#[serde(skip_serializing_if = "Option::is_none")]
pub features: Option<Value>,
pub features: Option<HashMap<String, bool>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(untagged)]
pub enum RuleAction {
#[serde(rename = "allow")]
Allow,
#[serde(rename = "disallow")]
Disallow,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Os {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arch: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Natives {
pub windows: Option<String>,
pub linux: Option<String>,
pub osx: Option<String>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Logging {
@ -203,3 +191,34 @@ pub struct File {
pub sha1: String,
pub url: String,
}
fn string_or_seq<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
struct StringOrVec(PhantomData<Vec<String>>);
impl<'de> Visitor<'de> for StringOrVec {
type Value = Vec<String>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("string or list of strings")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(vec![value.into()])
}
fn visit_seq<S>(self, seq: S) -> Result<Self::Value, S::Error>
where
S: SeqAccess<'de>,
{
Deserialize::deserialize(de::value::SeqAccessDeserializer::new(seq))
}
}
deserializer.deserialize_any(StringOrVec(PhantomData))
}

View file

@ -21,6 +21,23 @@ export async function getInstancesWithManifests(): Promise<Core.InstanceWithMani
})));
}
export async function getGroupedInstances(): Promise<Map<string, Core.InstanceWithManifest[]>> {
const instances = await getInstancesWithManifests();
const map = new Map<string, Core.InstanceWithManifest[]>();
instances.forEach((instance, index) => {
const groupName = instance.instance.group || 'Unnamed';
const value = { ...instance, index };
if (map.has(groupName))
map.get(groupName)!.push(value);
else
map.set(groupName, [value]);
});
return map;
}
export async function getInstances(): Promise<Core.Instance[]> {
return await invoke<Core.Instance[]>('plugin:launcher-core|get_instances');
}

View file

@ -5,13 +5,10 @@ import {
EyeIcon,
PlayIcon,
PlusIcon,
RefreshCw01Icon,
SearchMdIcon,
} from '@untitled-theme/icons-solid';
import type { Accessor, Setter } from 'solid-js';
import { For, Suspense, createSignal, lazy } from 'solid-js';
import { DragDropProvider, DragDropSensors, SortableProvider, closestCenter, createSortable } from '@thisbeyond/solid-dnd';
import type { DragEventHandler, Id } from '@thisbeyond/solid-dnd';
import { For, Show, createResource, createSignal } from 'solid-js';
import { useNavigate } from '@solidjs/router';
import image from '../../assets/images/header.png';
import Button from '../components/base/Button';
@ -19,7 +16,8 @@ import Tag from '../components/base/Tag';
import TextField from '../components/base/TextField';
import defaultCover from '../../assets/images/default_instance_cover.jpg';
import ContextMenu from '../components/overlay/ContextMenu';
import { createInstance, getInstancesWithManifests, refreshClientManager } from '../../bridge/game';
import { createInstance, getGroupedInstances, getInstancesWithManifests } from '../../bridge/game';
import Modal from '../components/overlay/Modal';
// TODO: Replace this into it's own component
// possibly with the core ui lirbary with autogenerated and optimized svgs pls
@ -99,12 +97,6 @@ function InstanceCardContextMenu(props: InstanceCardContextMenuProps) {
);
}
interface InstanceCardProps {
id: number;
instance: Core.Instance;
manifest: Core.Manifest;
}
declare module 'solid-js' {
// eslint-disable-next-line ts/no-namespace
namespace JSX {
@ -114,39 +106,19 @@ declare module 'solid-js' {
}
}
function InstanceCard(props: InstanceCardProps) {
function InstanceCard(props: Core.InstanceWithManifest) {
const navigate = useNavigate();
const [pos, setPos] = createSignal({ x: 0, y: 0 });
const [dragged, setDragged] = createSignal(false);
const [contextMenuVisible, setContextMenuVisible] = createSignal(false);
const id = () => props.id as Id;
let ref!: HTMLDivElement;
const sortable = createSortable(id());
function didDrag() {
const value = dragged();
if (value) {
setDragged(false);
return true;
}
return false;
}
function openInstancePage(_e: MouseEvent) {
if (didDrag())
return;
navigate(`/instances/?id=${props.instance.id}`);
}
function openContextMenu(e: MouseEvent) {
e.preventDefault();
e.stopPropagation();
if (didDrag())
return;
setContextMenuVisible(!contextMenuVisible());
}
@ -154,21 +126,11 @@ function InstanceCard(props: InstanceCardProps) {
setPos({ x: e.clientX, y: e.clientY });
}
function onMouseUp(e: MouseEvent) {
const xMoved = Math.abs(e.clientX - pos().x) > 3;
const yMoved = Math.abs(e.clientY - pos().y) > 3;
if (xMoved || yMoved)
setDragged(true);
}
return (
<>
<div
use:sortable={sortable}
onClick={e => openInstancePage(e)}
onMouseDown={e => onMouseDown(e)}
onMouseUp={e => onMouseUp(e)}
onContextMenu={e => openContextMenu(e)}
ref={ref}
class="relative h-[152px] group flex flex-col rounded-xl bg-component-bg hover:bg-component-bg-hover active:bg-component-bg-pressed border border-gray-0.05"
@ -216,81 +178,31 @@ interface InstanceGroupProps {
}
function InstanceGroup(props: InstanceGroupProps) {
// eslint-disable-next-line solid/reactivity
const [instances, setInstances] = createSignal(props.instances.map((instance, index) => ({ ...instance, id: index })));
const ids = () => instances().map(instance => instance.id);
const onDragStart: DragEventHandler = ({ draggable }) => {
draggable.node.setAttribute('data-dragging', 'true');
draggable.node.style.zIndex = '1000';
};
const onDragEnd: DragEventHandler = ({ draggable, droppable }) => {
draggable.node.setAttribute('data-dragging', 'false');
draggable.node.style.zIndex = '';
if (draggable && droppable) {
const currentItems = ids();
const fromIndex = currentItems.indexOf(draggable.id as number);
const toIndex = currentItems.indexOf(droppable.id as number);
if (fromIndex !== toIndex) {
const updatedItems = instances().slice();
updatedItems.splice(toIndex, 0, ...updatedItems.splice(fromIndex, 1));
setInstances(updatedItems);
}
}
};
return (
<div class="flex flex-col gap-y-4">
<h4>{props.title}</h4>
<DragDropProvider
onDragStart={onDragStart}
onDragEnd={onDragEnd}
collisionDetector={closestCenter}
>
<DragDropSensors />
<div class="grid grid-cols-4 2xl:grid-cols-6 gap-4">
<SortableProvider ids={ids()}>
<For each={instances()}>{item => <InstanceCard {...item} />}</For>
</SortableProvider>
</div>
</DragDropProvider>
<div class="grid grid-cols-4 2xl:grid-cols-6 gap-4">
<For each={props.instances}>{item => <InstanceCard {...item} />}</For>
</div>
</div>
);
}
const Instances = lazy(async () => {
const instances = await getInstancesWithManifests();
const map = new Map<string, Core.InstanceWithManifest[]>();
instances.forEach((instance) => {
const groupName = instance.instance.group || 'Unnamed';
if (map.has(groupName))
map.get(groupName)!.push(instance);
else
map.set(groupName, [instance]);
});
return {
default: () => Array.from(map.entries()).map(([title, instances]) => (
<InstanceGroup title={title} instances={instances} />
)),
};
});
function HomePage() {
function newInstance() {
createInstance({
const [instances, { refetch }] = createResource(getGroupedInstances);
async function newInstance() {
await createInstance({
name: 'New Instance',
version: '1.19.2',
version: '1.8.9',
client: {
type: 'Vanilla',
},
cover: null,
group: null,
});
await refetch();
}
return (
@ -314,18 +226,16 @@ function HomePage() {
iconLeft={<Download01Icon />}
children="From URL"
/>
<Button
styleType="secondary"
iconLeft={<RefreshCw01Icon />}
onClick={() => refreshClientManager()}
/>
</div>
</div>
<Suspense>
<Instances />
</Suspense>
<Show when={instances() !== undefined}>
<For each={Array.from(instances()!.entries())}>
{([title, instances]) => (
<InstanceGroup title={title} instances={instances} />
)}
</For>
</Show>
</div>
);
}