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";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState, useCallback } from "react";
|
import React, { useEffect, useState, useCallback, useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
Form,
|
Form,
|
||||||
@ -8,9 +8,11 @@ import {
|
|||||||
InputNumber,
|
InputNumber,
|
||||||
Select,
|
Select,
|
||||||
Tabs,
|
Tabs,
|
||||||
|
Tag,
|
||||||
Button,
|
Button,
|
||||||
Space,
|
Space,
|
||||||
App,
|
App,
|
||||||
|
theme,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import { PlusOutlined, DeleteOutlined } from "@ant-design/icons";
|
import { PlusOutlined, DeleteOutlined } from "@ant-design/icons";
|
||||||
import { createConfig, updateConfig } from "@/services/config";
|
import { createConfig, updateConfig } from "@/services/config";
|
||||||
@ -40,6 +42,7 @@ import {
|
|||||||
} from "@/hooks/useFieldValidations";
|
} from "@/hooks/useFieldValidations";
|
||||||
import type { Config, DependencyType, UploadRelationType } from "@/types";
|
import type { Config, DependencyType, UploadRelationType } from "@/types";
|
||||||
import { Template } from "@/lib/utils/template";
|
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
|
// 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 {
|
interface ConfigModalFormProps {
|
||||||
mode: "add" | "edit";
|
mode: "add" | "edit";
|
||||||
initialConfig: Config | null;
|
initialConfig: Config | null;
|
||||||
@ -497,12 +504,37 @@ function ConfigModalForm({
|
|||||||
onSave,
|
onSave,
|
||||||
}: ConfigModalFormProps) {
|
}: ConfigModalFormProps) {
|
||||||
const [form] = Form.useForm<ConfigValues>();
|
const [form] = Form.useForm<ConfigValues>();
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
|
||||||
// Each Select loads its own data independently
|
// Each Select loads its own data independently
|
||||||
const mrLoaders = useModrinthLoaders();
|
const mrLoaders = useModrinthLoaders();
|
||||||
const mrEnvs = useModrinthEnvironments();
|
const mrEnvs = useModrinthEnvironments();
|
||||||
const cfMeta = useCurseforgeMeta();
|
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);
|
const { Provider, results } = useFieldValidations<Config>(form, STATIC_DEFS, LIST_DEFS);
|
||||||
|
|
||||||
@ -676,7 +708,38 @@ function ConfigModalForm({
|
|||||||
mode="multiple"
|
mode="multiple"
|
||||||
placeholder="选择加载器…"
|
placeholder="选择加载器…"
|
||||||
loading={mrLoaders.loading}
|
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={
|
notFoundContent={
|
||||||
mrLoaders.loading ? "加载中…" : "无可用选项"
|
mrLoaders.loading ? "加载中…" : "无可用选项"
|
||||||
}
|
}
|
||||||
@ -817,10 +880,41 @@ function ConfigModalForm({
|
|||||||
mode="multiple"
|
mode="multiple"
|
||||||
placeholder="选择加载器…"
|
placeholder="选择加载器…"
|
||||||
loading={cfMeta.loading}
|
loading={cfMeta.loading}
|
||||||
options={cfMeta.loaders.map((l) => ({
|
options={sortedCfOptions.map((l) => ({
|
||||||
label: l,
|
label: l,
|
||||||
value: 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={
|
notFoundContent={
|
||||||
cfMeta.loading ? "加载中…" : "无可用选项"
|
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