fix: Minecraft manifest deserializing
This commit is contained in:
parent
b0b1aa48e9
commit
2af86b5fda
3 changed files with 107 additions and 161 deletions
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue