Add loader icons, colors, and sort order to ConfigModal selects
This commit is contained in:
parent
80d664936a
commit
269e7024a2
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import React, { useEffect, useState, useCallback, useMemo } from "react";
|
||||
import {
|
||||
Modal,
|
||||
Form,
|
||||
@ -8,9 +8,11 @@ import {
|
||||
InputNumber,
|
||||
Select,
|
||||
Tabs,
|
||||
Tag,
|
||||
Button,
|
||||
Space,
|
||||
App,
|
||||
theme,
|
||||
} from "antd";
|
||||
import { PlusOutlined, DeleteOutlined } from "@ant-design/icons";
|
||||
import { createConfig, updateConfig } from "@/services/config";
|
||||
@ -40,6 +42,7 @@ import {
|
||||
} from "@/hooks/useFieldValidations";
|
||||
import type { Config, DependencyType, UploadRelationType } from "@/types";
|
||||
import { Template } from "@/lib/utils/template";
|
||||
import { getModLoaderMetadata, MOD_LOADERS_METADATA, type ModLoaderMetadata } from "./loaders";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 工具
|
||||
@ -485,6 +488,10 @@ export function ConfigModal({
|
||||
// Inner component — Form with independently-loading Selects
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function LoaderIcon({ meta, size }: { meta: ModLoaderMetadata; size: number }) {
|
||||
return <meta.iconComponent style={{ color: meta.color, fontSize: size }} />;
|
||||
}
|
||||
|
||||
interface ConfigModalFormProps {
|
||||
mode: "add" | "edit";
|
||||
initialConfig: Config | null;
|
||||
@ -497,12 +504,37 @@ function ConfigModalForm({
|
||||
onSave,
|
||||
}: ConfigModalFormProps) {
|
||||
const [form] = Form.useForm<ConfigValues>();
|
||||
const { token } = theme.useToken();
|
||||
|
||||
// Each Select loads its own data independently
|
||||
const mrLoaders = useModrinthLoaders();
|
||||
const mrEnvs = useModrinthEnvironments();
|
||||
const cfMeta = useCurseforgeMeta();
|
||||
|
||||
const sortedMrOptions = useMemo(() => {
|
||||
const orderMap = new Map(
|
||||
MOD_LOADERS_METADATA.map((m, i) => [m.name.toLowerCase(), i]),
|
||||
);
|
||||
const sortFn = (a: { label: string }, b: { label: string }) => {
|
||||
const ai = orderMap.get(a.label.toLowerCase()) ?? Infinity;
|
||||
const bi = orderMap.get(b.label.toLowerCase()) ?? Infinity;
|
||||
return ai - bi;
|
||||
};
|
||||
return [...mrLoaders.options].sort(sortFn);
|
||||
}, [mrLoaders.options]);
|
||||
|
||||
const sortedCfOptions = useMemo(() => {
|
||||
const orderMap = new Map(
|
||||
MOD_LOADERS_METADATA.map((m, i) => [m.name.toLowerCase(), i]),
|
||||
);
|
||||
const sortFn = (a: string, b: string) => {
|
||||
const ai = orderMap.get(a.toLowerCase()) ?? Infinity;
|
||||
const bi = orderMap.get(b.toLowerCase()) ?? Infinity;
|
||||
return ai - bi;
|
||||
};
|
||||
return [...cfMeta.loaders].sort(sortFn);
|
||||
}, [cfMeta.loaders]);
|
||||
|
||||
// 表单项实时校验
|
||||
const { Provider, results } = useFieldValidations<Config>(form, STATIC_DEFS, LIST_DEFS);
|
||||
|
||||
@ -676,7 +708,38 @@ function ConfigModalForm({
|
||||
mode="multiple"
|
||||
placeholder="选择加载器…"
|
||||
loading={mrLoaders.loading}
|
||||
options={mrLoaders.options}
|
||||
options={sortedMrOptions}
|
||||
optionRender={(option) => {
|
||||
const meta = getModLoaderMetadata(option.label as string);
|
||||
return (
|
||||
<span style={{ display: "flex", alignItems: "center", gap: token.marginXS }}>
|
||||
{meta && <LoaderIcon meta={meta} size={token.fontSize} />}
|
||||
<span style={{ color: meta?.color }}>
|
||||
{meta?.name ?? option.label}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}}
|
||||
tagRender={(props) => {
|
||||
const { label, closable, onClose } = props;
|
||||
const meta = getModLoaderMetadata(label as string);
|
||||
const onPreventMouseDown = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
return (
|
||||
<Tag
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
onMouseDown={onPreventMouseDown}
|
||||
color={meta?.color}
|
||||
icon={meta ? <LoaderIcon meta={meta} size={token.fontSizeSM} /> : undefined}
|
||||
style={{ marginInlineEnd: token.marginXXS, fontWeight: 500 }}
|
||||
>
|
||||
{meta?.name ?? label}
|
||||
</Tag>
|
||||
);
|
||||
}}
|
||||
notFoundContent={
|
||||
mrLoaders.loading ? "加载中…" : "无可用选项"
|
||||
}
|
||||
@ -817,10 +880,41 @@ function ConfigModalForm({
|
||||
mode="multiple"
|
||||
placeholder="选择加载器…"
|
||||
loading={cfMeta.loading}
|
||||
options={cfMeta.loaders.map((l) => ({
|
||||
options={sortedCfOptions.map((l) => ({
|
||||
label: l,
|
||||
value: l,
|
||||
}))}
|
||||
optionRender={(option) => {
|
||||
const meta = getModLoaderMetadata(option.label as string);
|
||||
return (
|
||||
<span style={{ display: "flex", alignItems: "center", gap: token.marginXS }}>
|
||||
{meta && <LoaderIcon meta={meta} size={token.fontSize} />}
|
||||
<span style={{ color: meta?.color }}>
|
||||
{meta?.name ?? option.label}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}}
|
||||
tagRender={(props) => {
|
||||
const { label, closable, onClose } = props;
|
||||
const meta = getModLoaderMetadata(label as string);
|
||||
const onPreventMouseDown = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
return (
|
||||
<Tag
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
onMouseDown={onPreventMouseDown}
|
||||
color={meta?.color}
|
||||
icon={meta ? <LoaderIcon meta={meta} size={token.fontSizeSM} /> : undefined}
|
||||
style={{ marginInlineEnd: token.marginXXS, fontWeight: 500 }}
|
||||
>
|
||||
{meta?.name ?? label}
|
||||
</Tag>
|
||||
);
|
||||
}}
|
||||
notFoundContent={
|
||||
cfMeta.loading ? "加载中…" : "无可用选项"
|
||||
}
|
||||
|
||||
94
src/components/loaders.tsx
Normal file
94
src/components/loaders.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import Icon from "@ant-design/icons";
|
||||
import { GetProps } from "antd";
|
||||
import React from "react";
|
||||
|
||||
export type ModLoaderMetadataPart = {
|
||||
icon: React.ReactElement<React.SVGProps<SVGSVGElement>>,
|
||||
name: string,
|
||||
/** 加载器别名 */
|
||||
alias?: string[],
|
||||
color: string,
|
||||
};
|
||||
|
||||
export type ModLoaderMetadata = ModLoaderMetadataPart & {
|
||||
iconComponent: React.FC<Partial<GetProps<typeof Icon>>>,
|
||||
};
|
||||
|
||||
function preprocessMetadata(metadata: ModLoaderMetadataPart[]): ModLoaderMetadata[] {
|
||||
return metadata.map(meta => ({
|
||||
...meta,
|
||||
iconComponent: (props) => (
|
||||
<Icon component={
|
||||
(svgProps: React.SVGProps<SVGSVGElement>) => {
|
||||
const { children, ...rest } = svgProps;
|
||||
return React.cloneElement(meta.icon, rest);
|
||||
}
|
||||
} {...props} />
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
export const MOD_LOADERS_METADATA: ModLoaderMetadata[] = preprocessMetadata([
|
||||
{
|
||||
icon: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" fillRule="evenodd" strokeLinecap="round" strokeLinejoin="round" clipRule="evenodd" viewBox="0 0 24 24">
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path fill="none" stroke="currentColor" strokeWidth="23" d="m820 761-85.6-87.6c-4.6-4.7-10.4-9.6-25.9 1-19.9 13.6-8.4 21.9-5.2 25.4 8.2 9 84.1 89 97.2 104 2.5 2.8-20.3-22.5-6.5-39.7 5.4-7 18-12 26-3 6.5 7.3 10.7 18-3.4 29.7-24.7 20.4-102 82.4-127 103-12.5 10.3-28.5 2.3-35.8-6-7.5-8.9-30.6-34.6-51.3-58.2-5.5-6.3-4.1-19.6 2.3-25 35-30.3 91.9-73.8 111.9-90.8" transform="matrix(.08671 0 0 .0867 -49.8 -56)"/>
|
||||
</svg>
|
||||
),
|
||||
name: "Fabric",
|
||||
color: "#8a7b71"
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg xmlSpace="preserve" fillRule="evenodd" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="1.5" clipRule="evenodd" viewBox="0 0 24 24">
|
||||
<path fill="none" d="M0 0h24v24H0z"></path>
|
||||
<path fill="none" stroke="currentColor" strokeWidth="2" d="M2 7.5h8v-2h12v2s-7 3.4-7 6 3.1 3.1 3.1 3.1l.9 3.9H5l1-4.1s3.8.1 4-2.9c.2-2.7-6.5-.7-8-6Z"></path>
|
||||
</svg>
|
||||
),
|
||||
name: "Forge",
|
||||
color: "#5b6197"
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg enableBackground="new 0 0 24 24" version="1.1" viewBox="0 0 24 24" xmlSpace="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||
<path d="m12 19.2v2m0-2v2"/>
|
||||
<path d="m8.4 1.3c0.5 1.5 0.7 3 0.1 4.6-0.2 0.5-0.9 1.5-1.6 1.5m8.7-6.1c-0.5 1.5-0.7 3-0.1 4.6 0.2 0.6 0.9 1.5 1.6 1.5"/>
|
||||
<path d="m3.6 15.8h-1.7m18.5 0h1.7"/>
|
||||
<path d="m3.2 12.1h-1.7m19.3 0h1.8"/>
|
||||
<path d="m8.1 12.7v1.6m7.8-1.6v1.6"/>
|
||||
<path d="m10.8 18h1.2m0 1.2-1.2-1.2m2.4 0h-1.2m0 1.2 1.2-1.2"/>
|
||||
<path d="m4 9.7c-0.5 1.2-0.8 2.4-0.8 3.7 0 3.1 2.9 6.3 5.3 8.2 0.9 0.7 2.2 1.1 3.4 1.1m0.1-17.8c-1.1 0-2.1 0.2-3.2 0.7m11.2 4.1c0.5 1.2 0.8 2.4 0.8 3.7 0 3.1-2.9 6.3-5.3 8.2-0.9 0.7-2.2 1.1-3.4 1.1m-0.1-17.8c1.1 0 2.1 0.2 3.2 0.7"/>
|
||||
<path d="m4 9.7c-0.2-1.8-0.3-3.7 0.5-5.5s2.2-2.6 3.9-3m11.6 8.5c0.2-1.9 0.3-3.7-0.5-5.5s-2.2-2.6-3.9-3"/>
|
||||
<path d="m12 21.2-2.4 0.4m2.4-0.4 2.4 0.4"/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
name: "NeoForge",
|
||||
color: "#dc895c"
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg xmlnsXlink="http://www.w3.org/1999/xlink" xmlSpace="preserve" fillRule="evenodd" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="2" clipRule="evenodd" viewBox="0 0 24 24">
|
||||
<defs>
|
||||
<path id="quilt" fill="none" stroke="currentColor" strokeWidth="65.6" d="M442.5 233.9c0-6.4-5.2-11.6-11.6-11.6h-197c-6.4 0-11.6 5.2-11.6 11.6v197c0 6.4 5.2 11.6 11.6 11.6h197c6.4 0 11.6-5.2 11.6-11.7v-197Z"></path>
|
||||
</defs>
|
||||
<path fill="none" d="M0 0h24v24H0z"></path>
|
||||
<use xlinkHref="#quilt" strokeWidth="65.6" transform="matrix(.03053 0 0 .03046 -3.2 -3.2)"></use>
|
||||
<use xlinkHref="#quilt" strokeWidth="65.6" transform="matrix(.03053 0 0 .03046 -3.2 7)"></use>
|
||||
<use xlinkHref="#quilt" strokeWidth="65.6" transform="matrix(.03053 0 0 .03046 6.9 -3.2)"></use>
|
||||
<path fill="none" stroke="currentColor" strokeWidth="70.4" d="M442.5 234.8c0-7-5.6-12.5-12.5-12.5H234.7c-6.8 0-12.4 5.6-12.4 12.5V430c0 6.9 5.6 12.5 12.4 12.5H430c6.9 0 12.5-5.6 12.5-12.5V234.8Z" transform="rotate(45 3.5 24) scale(.02843 .02835)"></path>
|
||||
</svg>
|
||||
),
|
||||
name: "Quilt",
|
||||
color: "#8b61b4"
|
||||
},
|
||||
]);
|
||||
|
||||
export function getModLoaderMetadata(name: string): ModLoaderMetadata | undefined {
|
||||
return MOD_LOADERS_METADATA.find(loader =>
|
||||
loader.name.toLowerCase() === name.toLowerCase() ||
|
||||
(loader.alias && loader.alias.some(alias => alias.toLowerCase() === name.toLowerCase()))
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user