mirror of
https://github.com/dream-pep/koring-launcher.git
synced 2026-09-12 05:45:18 +08:00
feat: add auth flow and settings UI refactor
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "123",
|
||||
"author": "",
|
||||
"description": "123",
|
||||
"runtime": {
|
||||
"minecraft": "1.21.4"
|
||||
},
|
||||
"creationDate": 1785342079436,
|
||||
"lastAccessDate": 1785342079436,
|
||||
"lastPlayedDate": 0,
|
||||
"playtime": 0
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
oobe: false
|
||||
|
||||
@@ -70,6 +70,18 @@ export interface SecurityIdConfig {
|
||||
authUrl: string;
|
||||
}
|
||||
|
||||
export interface InstanceMeta {
|
||||
name: string;
|
||||
displayName: string;
|
||||
icon: string;
|
||||
gameVersion: string;
|
||||
loader: string;
|
||||
loaderVersion: string;
|
||||
createdAt: number;
|
||||
lastPlayed: number;
|
||||
playtime: number;
|
||||
}
|
||||
|
||||
export interface NetworkConfig {
|
||||
securityId: SecurityIdConfig;
|
||||
}
|
||||
@@ -85,6 +97,7 @@ export interface AppConfig {
|
||||
advanced: AdvancedConfig;
|
||||
download: DownloadConfig;
|
||||
network: NetworkConfig;
|
||||
instances: InstanceMeta[];
|
||||
}
|
||||
|
||||
const DEFAULTS: AppConfig = {
|
||||
@@ -98,6 +111,7 @@ const DEFAULTS: AppConfig = {
|
||||
advanced: { afterLaunch: 'close', winMode: 'default', customWidth: 854, customHeight: 480, gameArgs: '', preLaunchCmd: '', debugMode: false },
|
||||
download: { fileSource: 'mirror', versionSource: 'mirror', threads: 16, speedLimit: 0 },
|
||||
network: { securityId: { enabled: false, authUrl: '' } },
|
||||
instances: [],
|
||||
};
|
||||
|
||||
function migrate(config: AppConfig): AppConfig {
|
||||
|
||||
@@ -60,6 +60,18 @@ export interface NetworkConfig {
|
||||
securityId: SecurityIdConfig;
|
||||
}
|
||||
|
||||
export interface InstanceMeta {
|
||||
name: string;
|
||||
displayName: string;
|
||||
icon: string;
|
||||
gameVersion: string;
|
||||
loader: string;
|
||||
loaderVersion: string;
|
||||
createdAt: number;
|
||||
lastPlayed: number;
|
||||
playtime: number;
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
version: number;
|
||||
oobe: boolean;
|
||||
@@ -71,6 +83,7 @@ export interface AppConfig {
|
||||
advanced: AdvancedConfig;
|
||||
download: DownloadConfig;
|
||||
network: NetworkConfig;
|
||||
instances: InstanceMeta[];
|
||||
}
|
||||
|
||||
interface CommandResult {
|
||||
|
||||
@@ -3,8 +3,9 @@ import { VERSION } from "@/lib/version";
|
||||
import { useUpdateStore } from "@/stores/updateStore";
|
||||
import { relaunchApp } from "@/api/update";
|
||||
import Silk from "@/components/silk/Silk";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Button } from "@heroui/react";
|
||||
import clsx from "clsx";
|
||||
import { Suspense } from "react";
|
||||
|
||||
const modeColors: Record<string, string> = {
|
||||
dev: "#F59E0B",
|
||||
@@ -12,6 +13,12 @@ const modeColors: Record<string, string> = {
|
||||
run: "#3B82F6",
|
||||
};
|
||||
|
||||
const modeGradients: Record<string, string> = {
|
||||
dev: "linear-gradient(135deg, #F59E0B, #D97706)",
|
||||
beta: "linear-gradient(135deg, #10B981, #059669)",
|
||||
run: "linear-gradient(135deg, #3B82F6, #2563EB)",
|
||||
};
|
||||
|
||||
const modeLabels: Record<string, string> = {
|
||||
dev: "开发版",
|
||||
beta: "测试版",
|
||||
@@ -22,124 +29,88 @@ type UpdateState = "latest" | "hasUpdate" | "installed";
|
||||
|
||||
interface VersionCardProps {
|
||||
className?: string;
|
||||
/** 开发者模式:覆盖颜色 */
|
||||
overrideMode?: string | null;
|
||||
/** 开发者模式:覆盖更新状态 */
|
||||
overrideState?: UpdateState | null;
|
||||
/** 开发者模式:遮罩透明度 (0-100) */
|
||||
overlayOpacity?: number;
|
||||
/** 开发者模式:模糊强度 (px) */
|
||||
blurAmount?: number;
|
||||
simple?: boolean;
|
||||
}
|
||||
|
||||
export function VersionCard({
|
||||
className,
|
||||
overrideMode,
|
||||
overrideState,
|
||||
overlayOpacity = 30,
|
||||
blurAmount = 12,
|
||||
simple = false,
|
||||
}: VersionCardProps) {
|
||||
const color = modeColors[overrideMode ?? BUILD_MODE] ?? modeColors.run;
|
||||
const gradient = modeGradients[overrideMode ?? BUILD_MODE] ?? modeGradients.run;
|
||||
const label = modeLabels[overrideMode ?? BUILD_MODE] ?? modeLabels.run;
|
||||
|
||||
const { checking, downloading, installed, update, check, install } = useUpdateStore();
|
||||
|
||||
const effectiveState: UpdateState = overrideState ?? (installed ? "installed" : update ? "hasUpdate" : "latest");
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"relative overflow-hidden rounded-xl border border-white/10 min-h-[200px]",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{/* Silk 背景 */}
|
||||
<div className="absolute inset-0 z-0" style={{ background: color }}>
|
||||
<Silk speed={3} scale={1.2} color={color} noiseIntensity={1.2} rotation={0.3} />
|
||||
</div>
|
||||
const Btn = (props: React.ComponentProps<typeof Button>) => (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="text-white/90 hover:text-white hover:bg-white/10 border border-white/20"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
{/* 磨砂层 */}
|
||||
return (
|
||||
<div className={clsx("relative overflow-hidden rounded-xl border border-white/10 min-h-[200px]", className)}>
|
||||
{/* 背景层 */}
|
||||
<div className="absolute inset-0" style={{ background: gradient }} />
|
||||
|
||||
{/* Silk 动画层 (非 simple 模式) */}
|
||||
{!simple && (
|
||||
<Suspense fallback={null}>
|
||||
<div className="absolute inset-0 opacity-60 mix-blend-soft-light">
|
||||
<Silk speed={3} scale={1.2} color={color} noiseIntensity={1.2} rotation={0.3} />
|
||||
</div>
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
{/* 磨砂遮罩 */}
|
||||
<div
|
||||
className="absolute inset-0 z-[1]"
|
||||
style={{
|
||||
background: `rgba(0,0,0,${overlayOpacity / 100})`,
|
||||
backdropFilter: `blur(${blurAmount}px)`,
|
||||
}}
|
||||
className="absolute inset-0"
|
||||
style={{ background: "rgba(0,0,0,0.25)", backdropFilter: "blur(12px)" }}
|
||||
/>
|
||||
|
||||
{/* 内容层 */}
|
||||
{/* 内容 */}
|
||||
<div className="relative z-10 flex flex-col items-center justify-center px-6 py-10 gap-4">
|
||||
{/* 版本类型 */}
|
||||
<span className="text-[11px] font-bold tracking-wider text-white/70">
|
||||
{label}
|
||||
</span>
|
||||
|
||||
{/* Logo */}
|
||||
<img src={LOGO_SVG} alt="Koring" className="w-50 h-20 drop-shadow-lg" style={{ filter: "brightness(0) invert(1)" }} />
|
||||
|
||||
{/* 版本号 */}
|
||||
<span className="text-[11px] font-bold tracking-wider text-white/70">{label}</span>
|
||||
<img
|
||||
src={LOGO_SVG}
|
||||
alt="Koring"
|
||||
className="w-40 h-auto drop-shadow-lg"
|
||||
style={{ filter: "brightness(0) invert(1)" }}
|
||||
/>
|
||||
<p className="text-sm text-white/70 font-medium">v{VERSION}</p>
|
||||
|
||||
{/* 按钮组 */}
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
{effectiveState === "latest" && (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="bg-white/10 border-white/20 text-white hover:bg-white/20 hover:text-white"
|
||||
onClick={check}
|
||||
disabled={checking}
|
||||
>
|
||||
<Btn onPress={check} isDisabled={checking}>
|
||||
{checking ? "检查中..." : "检查更新"}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="bg-white/10 border-white/20 text-white hover:bg-white/20 hover:text-white"
|
||||
>
|
||||
查看亮点
|
||||
</Button>
|
||||
</Btn>
|
||||
<Btn>查看亮点</Btn>
|
||||
</>
|
||||
)}
|
||||
|
||||
{effectiveState === "hasUpdate" && (
|
||||
<>
|
||||
<span className="text-[12px] text-white/80 mr-1">
|
||||
有新的版本可用
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="bg-white/10 border-white/20 text-white hover:bg-white/20 hover:text-white"
|
||||
onClick={install}
|
||||
disabled={downloading}
|
||||
>
|
||||
<span className="text-[12px] text-white/80 mr-1">有新的版本可用</span>
|
||||
<Btn onPress={install} isDisabled={downloading}>
|
||||
{downloading ? "下载中..." : "下载更新"}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="bg-white/10 border-white/20 text-white hover:bg-white/20 hover:text-white"
|
||||
>
|
||||
查看亮点
|
||||
</Button>
|
||||
</Btn>
|
||||
</>
|
||||
)}
|
||||
|
||||
{effectiveState === "installed" && (
|
||||
<>
|
||||
<span className="text-[12px] text-white/80 mr-1">
|
||||
更新已下载
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="bg-white/10 border-white/20 text-white hover:bg-white/20 hover:text-white"
|
||||
onClick={relaunchApp}
|
||||
>
|
||||
立即更新
|
||||
</Button>
|
||||
<span className="text-[12px] text-white/80 mr-1">更新已下载</span>
|
||||
<Btn onPress={relaunchApp}>立即更新</Btn>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Component, type ReactNode } from "react";
|
||||
import { Button } from "@heroui/react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
|
||||
interface Props { children: ReactNode; }
|
||||
interface State { hasError: boolean; errorMsg: string; }
|
||||
|
||||
export class ErrorBoundary extends Component<Props, State> {
|
||||
state: State = { hasError: false, errorMsg: "" };
|
||||
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
return { hasError: true, errorMsg: error.message };
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full gap-4 p-8">
|
||||
<AlertTriangle className="w-12 h-12 text-destructive/50" />
|
||||
<p className="text-sm text-muted-foreground text-center max-w-[320px]">
|
||||
页面渲染出错,请尝试刷新
|
||||
</p>
|
||||
<p className="text-[11px] text-muted-foreground/50 font-mono text-center max-w-[400px] break-all">
|
||||
{this.state.errorMsg}
|
||||
</p>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
onPress={() => this.setState({ hasError: false, errorMsg: "" })}
|
||||
>
|
||||
重试
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface SurfaceProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
variant?: "raised" | "flat" | "bordered"
|
||||
frost?: "none" | "sm" | "md" | "lg"
|
||||
padding?: "none" | "sm" | "md" | "lg"
|
||||
}
|
||||
|
||||
const frostMap = {
|
||||
none: "",
|
||||
sm: "backdrop-blur-[6px]",
|
||||
md: "backdrop-blur-[12px]",
|
||||
lg: "backdrop-blur-[20px]",
|
||||
}
|
||||
|
||||
const paddingMap = {
|
||||
none: "",
|
||||
sm: "px-3 py-2.5",
|
||||
md: "px-5 py-4",
|
||||
lg: "px-6 py-5",
|
||||
}
|
||||
|
||||
function Surface({
|
||||
variant = "raised",
|
||||
frost = "none",
|
||||
padding = "md",
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: SurfaceProps) {
|
||||
return (
|
||||
<div
|
||||
data-slot="surface"
|
||||
className={cn(
|
||||
"rounded-xl transition-colors duration-200",
|
||||
"bg-white/85 dark:bg-black/45",
|
||||
"border border-black/[0.06] dark:border-white/[0.07]",
|
||||
frostMap[frost],
|
||||
paddingMap[padding],
|
||||
variant === "raised" && "shadow-sm",
|
||||
variant === "bordered" && "shadow-none",
|
||||
variant === "flat" && "shadow-none bg-transparent border-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SurfaceHeader({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="surface-header"
|
||||
className={cn(
|
||||
"flex items-center gap-2 mb-3 pb-3 border-b border-black/[0.06] dark:border-white/[0.06]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SurfaceContent({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div data-slot="surface-content" className={cn("space-y-3", className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { Surface, SurfaceHeader, SurfaceContent }
|
||||
@@ -1,3 +1,5 @@
|
||||
export { SettingCard } from "./SettingCard";
|
||||
export { SettingRow } from "./SettingRow";
|
||||
export { PageHeader, SectionTitle } from "./SectionTitle";
|
||||
export { Surface, SurfaceHeader, SurfaceContent } from "./Surface";
|
||||
export { Skeleton } from "@/components/ui/skeleton";
|
||||
@@ -126,8 +126,8 @@ const Silk = ({ speed = 5, scale = 1, color = "#7B7481", noiseIntensity = 1.5, r
|
||||
<Canvas
|
||||
dpr={[1, 1.5]}
|
||||
frameloop="demand"
|
||||
style={{ position: "absolute", inset: 0, width: "100%", height: "100%", background: "black" }}
|
||||
gl={{ antialias: false, alpha: false }}
|
||||
style={{ position: "absolute", inset: 0, width: "100%", height: "100%" }}
|
||||
gl={{ antialias: false, alpha: true, premultipliedAlpha: true }}
|
||||
>
|
||||
<SilkPlane ref={meshRef} uniforms={uniforms} />
|
||||
</Canvas>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="skeleton"
|
||||
className={cn(
|
||||
"animate-pulse rounded-md bg-muted",
|
||||
"dark:bg-muted/50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
@@ -0,0 +1,189 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button, Input, RadioGroup, Radio, Slider } from "@heroui/react";
|
||||
import { X, Loader2, Plus } from "lucide-react";
|
||||
import { getMinecraftVersionList, getFabricVersionList, getForgeVersionList, getQuiltVersionList, type InstanceRuntime } from "@/api/instance";
|
||||
|
||||
interface CreateDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onCreate: (name: string, runtime: InstanceRuntime, displayName?: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const loaderOptions = [
|
||||
{ value: "vanilla", label: "原版 (Vanilla)", desc: "纯净 Minecraft,无模组加载器" },
|
||||
{ value: "forge", label: "Forge", desc: "最经典的模组加载器,模组生态最丰富" },
|
||||
{ value: "fabric", label: "Fabric", desc: "轻量快速,适合性能优化与辅助模组" },
|
||||
{ value: "quilt", label: "Quilt", desc: "Fabric 的分支,实验性功能更多" },
|
||||
{ value: "neoforged", label: "NeoForge", desc: "Forge 的现代化分支,1.20+ 推荐" },
|
||||
];
|
||||
|
||||
export function CreateDialog({ isOpen, onClose, onCreate }: CreateDialogProps) {
|
||||
const [step, setStep] = useState(0);
|
||||
const [name, setName] = useState("");
|
||||
const [displayName, setDisplayName] = useState("");
|
||||
const [loader, setLoader] = useState("vanilla");
|
||||
const [mcVersion, setMcVersion] = useState("");
|
||||
const [memory, setMemory] = useState(4);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [mcVersions, setMcVersions] = useState<string[]>([]);
|
||||
const [loadingVersions, setLoadingVersions] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) { loadMcVersions(); } else { resetForm(); }
|
||||
}, [isOpen]);
|
||||
|
||||
const resetForm = () => {
|
||||
setStep(0); setName(""); setDisplayName("");
|
||||
setLoader("vanilla"); setMcVersion(""); setMemory(4);
|
||||
setError(""); setCreating(false);
|
||||
};
|
||||
|
||||
const loadMcVersions = async () => {
|
||||
setLoadingVersions(true);
|
||||
try {
|
||||
const list = await getMinecraftVersionList();
|
||||
const releases = list.filter((v: string) => /^1\.\d+/.test(v)).slice(0, 24);
|
||||
setMcVersions(releases.length ? releases : ["1.21.4","1.21","1.20.6","1.20.4","1.20.1","1.19.4","1.19.2","1.18.2","1.17.1","1.16.5"]);
|
||||
if (!mcVersion && releases.length) setMcVersion(releases[0]);
|
||||
} catch { setMcVersions(["1.21.4","1.21","1.20.1","1.19.2","1.18.2","1.16.5"]); if (!mcVersion) setMcVersion("1.21.4"); }
|
||||
setLoadingVersions(false);
|
||||
};
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!name.trim()) { setError("请输入实例名称"); return; }
|
||||
setCreating(true);
|
||||
try {
|
||||
const runtime: any = { minecraft: mcVersion };
|
||||
if (loader === "forge") runtime.forge = "latest";
|
||||
else if (loader === "fabric") runtime.fabricLoader = "latest";
|
||||
else if (loader === "quilt") runtime.quiltLoader = "latest";
|
||||
else if (loader === "neoforged") runtime.neoForged = "latest";
|
||||
await onCreate(name.trim(), runtime, displayName.trim() || undefined);
|
||||
onClose();
|
||||
} catch (e: any) { setError(e.message || "创建失败"); }
|
||||
setCreating(false);
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" onClick={onClose} />
|
||||
<div className="relative z-10 w-full max-w-lg mx-4 bg-background border border-border/50 dark:border-white/[0.08] rounded-2xl shadow-2xl overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-border/30 dark:border-white/[0.04]">
|
||||
<div className="flex items-center gap-2">
|
||||
<Plus className="w-5 h-5 text-primary" />
|
||||
<h2 className="text-base font-semibold text-foreground">新建实例</h2>
|
||||
</div>
|
||||
<button onClick={onClose} className="p-1 rounded-md hover:bg-foreground/[0.06] transition-colors">
|
||||
<X className="w-4 h-4 text-muted-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="px-6 py-5 space-y-5 max-h-[60vh] overflow-y-auto scroll-area">
|
||||
{/* 步骤指示器 */}
|
||||
<div className="flex items-center gap-2">
|
||||
{["基本信息", "版本选择", "内存设置"].map((label, i) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<div className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-medium shrink-0 transition-colors ${
|
||||
i === step ? "bg-primary text-primary-foreground" :
|
||||
i < step ? "bg-primary/20 text-primary" :
|
||||
"bg-foreground/[0.06] dark:bg-white/[0.06] text-muted-foreground"
|
||||
}`}>{i < step ? "✓" : i + 1}</div>
|
||||
<span className={`text-[12px] ${i === step ? "text-foreground font-medium" : "text-muted-foreground"}`}>{label}</span>
|
||||
{i < 2 && <div className="w-5 h-px bg-border/40" />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{step === 0 && (
|
||||
<div className="space-y-4">
|
||||
<Input label="实例名称 (英文ID)" placeholder="例如: survival-world"
|
||||
value={name} onChange={(e) => setName(e.target.value.replace(/\s/g, "-").toLowerCase())} autoFocus />
|
||||
<Input label="显示名称 (可选)" placeholder="例如: 生存世界"
|
||||
value={displayName} onChange={(e) => setDisplayName(e.target.value)} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 1 && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground mb-2">Minecraft 版本</p>
|
||||
{loadingVersions ? (
|
||||
<div className="flex items-center gap-2 py-4"><Loader2 className="w-4 h-4 animate-spin text-muted-foreground" /><span className="text-sm text-muted-foreground">加载中...</span></div>
|
||||
) : (
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{mcVersions.map((v) => (
|
||||
<button key={v} onClick={() => setMcVersion(v)}
|
||||
className={`px-3 py-2 rounded-lg text-[13px] border transition-all ${
|
||||
mcVersion === v
|
||||
? "border-primary bg-primary/10 text-primary font-medium"
|
||||
: "border-border/40 dark:border-white/[0.06] text-muted-foreground hover:border-primary/30 hover:text-foreground"
|
||||
}`}>{v}</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground mb-2">模组加载器</p>
|
||||
<RadioGroup value={loader} onValueChange={setLoader}>
|
||||
{loaderOptions.map((opt) => (
|
||||
<Radio key={opt.value} value={opt.value} className="py-1.5">
|
||||
<Radio.Content>
|
||||
<div><p className="text-sm font-medium">{opt.label}</p><p className="text-[11px] text-muted-foreground">{opt.desc}</p></div>
|
||||
</Radio.Content>
|
||||
</Radio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="text-sm font-medium text-foreground">内存分配</p>
|
||||
<span className="text-sm font-semibold text-foreground">{memory} GB</span>
|
||||
</div>
|
||||
<Slider value={memory} onChange={(v) => setMemory(typeof v === "number" ? v : v[0])} minValue={1} maxValue={16} step={1}>
|
||||
<Slider.Track><Slider.Fill /><Slider.Thumb /></Slider.Track>
|
||||
</Slider>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-foreground/[0.03] dark:bg-white/[0.03] border border-border/30 dark:border-white/[0.04] space-y-1">
|
||||
<p className="text-[12px] font-medium text-foreground">创建摘要</p>
|
||||
<p className="text-[13px] text-muted-foreground">名称: {displayName || name}</p>
|
||||
<p className="text-[13px] text-muted-foreground">版本: Minecraft {mcVersion} ({loaderOptions.find(o=>o.value===loader)?.label})</p>
|
||||
<p className="text-[13px] text-muted-foreground">内存: {memory} GB</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && <p className="text-[12px] text-destructive">{error}</p>}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-t border-border/30 dark:border-white/[0.04]">
|
||||
<div>
|
||||
{step > 0 && <Button variant="light" onPress={() => setStep(step - 1)}>上一步</Button>}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="light" onPress={onClose}>取消</Button>
|
||||
{step < 2 ? (
|
||||
<Button onPress={() => setStep(step + 1)} isDisabled={step === 0 ? !name.trim() : !mcVersion}>下一步</Button>
|
||||
) : (
|
||||
<Button onPress={handleCreate} isDisabled={creating || !name.trim()}
|
||||
startContent={creating ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : undefined}>
|
||||
{creating ? "创建中..." : "创建实例"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { Button } from "@heroui/react";
|
||||
import {
|
||||
Play,
|
||||
Settings,
|
||||
Clock,
|
||||
Gamepad2,
|
||||
} from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
import type { InstanceInfo } from "@/api/instance";
|
||||
|
||||
function getLoader(runtime: InstanceInfo["config"]["runtime"]): string {
|
||||
if (runtime.forge) return "forge";
|
||||
if (runtime.fabricLoader) return "fabric";
|
||||
if (runtime.quiltLoader) return "quilt";
|
||||
if (runtime.neoForged) return "neoforged";
|
||||
return "vanilla";
|
||||
}
|
||||
|
||||
function getLoaderVersion(runtime: InstanceInfo["config"]["runtime"]): string {
|
||||
return runtime.forge || runtime.fabricLoader || runtime.quiltLoader || runtime.neoForged || "";
|
||||
}
|
||||
|
||||
const loaderBadgeColors: Record<string, string> = {
|
||||
vanilla: "bg-green-500/10 text-green-600 dark:bg-green-500/15 dark:text-green-400 border-green-500/20",
|
||||
forge: "bg-orange-500/10 text-orange-600 dark:bg-orange-500/15 dark:text-orange-400 border-orange-500/20",
|
||||
fabric: "bg-sky-500/10 text-sky-600 dark:bg-sky-500/15 dark:text-sky-400 border-sky-500/20",
|
||||
quilt: "bg-indigo-500/10 text-indigo-600 dark:bg-indigo-500/15 dark:text-indigo-400 border-indigo-500/20",
|
||||
neoforged: "bg-red-500/10 text-red-600 dark:bg-red-500/15 dark:text-red-400 border-red-500/20",
|
||||
};
|
||||
|
||||
const loaderLabels: Record<string, string> = {
|
||||
vanilla: "原版",
|
||||
forge: "Forge",
|
||||
fabric: "Fabric",
|
||||
quilt: "Quilt",
|
||||
neoforged: "NeoForge",
|
||||
};
|
||||
|
||||
const loaderColors: Record<string, string> = {
|
||||
vanilla: "bg-green-400",
|
||||
forge: "bg-orange-400",
|
||||
fabric: "bg-sky-400",
|
||||
quilt: "bg-indigo-400",
|
||||
neoforged: "bg-red-400",
|
||||
};
|
||||
|
||||
function formatPlaytime(ms: number): string {
|
||||
if (!ms || ms < 60000) return "";
|
||||
const hours = Math.floor(ms / 3600000);
|
||||
const mins = Math.floor((ms % 3600000) / 60000);
|
||||
if (hours > 0) return `${hours}h ${mins}m`;
|
||||
return `${mins}m`;
|
||||
}
|
||||
|
||||
function formatLastPlayed(ts: number): string {
|
||||
if (!ts) return "";
|
||||
const diff = Date.now() - ts;
|
||||
if (diff < 3600000) return "刚刚";
|
||||
if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前`;
|
||||
if (diff < 604800000) return `${Math.floor(diff / 86400000)}天前`;
|
||||
return new Date(ts).toLocaleDateString("zh-CN");
|
||||
}
|
||||
|
||||
interface InstanceCardProps {
|
||||
instance: InstanceInfo;
|
||||
displayName?: string;
|
||||
onPlay: (name: string) => void;
|
||||
onSettings: (name: string) => void;
|
||||
isLaunching: boolean;
|
||||
}
|
||||
|
||||
export function InstanceCard({ instance, displayName, onPlay, onSettings, isLaunching }: InstanceCardProps) {
|
||||
const { name, config, modCount, healthy } = instance;
|
||||
const loader = getLoader(config.runtime);
|
||||
const version = config.runtime.minecraft || "未知";
|
||||
const loaderLabel = loaderLabels[loader] ?? loader;
|
||||
const badgeCls = loaderBadgeColors[loader] ?? loaderBadgeColors.vanilla;
|
||||
const topColor = loaderColors[loader] ?? loaderColors.vanilla;
|
||||
const title = displayName || config.name || name;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"relative group rounded-xl transition-all duration-200",
|
||||
"bg-white/85 dark:bg-black/45",
|
||||
"border border-black/[0.06] dark:border-white/[0.07]",
|
||||
"hover:shadow-md hover:border-primary/20 dark:hover:border-primary/20",
|
||||
"overflow-hidden",
|
||||
)}
|
||||
>
|
||||
<div className={clsx("h-1.5 w-full", topColor)} />
|
||||
|
||||
<div className="px-5 py-4 space-y-3">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-semibold text-foreground truncate">{title}</p>
|
||||
<p className="text-[10.5px] text-muted-foreground/60 mt-0.5 truncate">{name}</p>
|
||||
</div>
|
||||
{healthy === false && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-destructive/10 text-destructive font-medium shrink-0">
|
||||
异常
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="light"
|
||||
className="min-w-0 w-7 h-7 p-0 opacity-0 group-hover:opacity-100 transition-opacity shrink-0"
|
||||
onPress={() => onSettings(name)}
|
||||
>
|
||||
<Settings className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<span className={clsx("text-[10.5px] px-2 py-0.5 rounded-md border font-medium", badgeCls)}>
|
||||
{loaderLabel}
|
||||
</span>
|
||||
<span className="text-[10.5px] px-2 py-0.5 rounded-md bg-foreground/[0.05] dark:bg-white/[0.05] text-muted-foreground border border-border/30 dark:border-white/[0.05]">
|
||||
{version}
|
||||
</span>
|
||||
{modCount > 0 && (
|
||||
<span className="text-[10.5px] px-2 py-0.5 rounded-md bg-purple-500/10 text-purple-600 dark:bg-purple-500/15 dark:text-purple-400 border border-purple-500/20">
|
||||
{modCount} 模组
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 text-[11px] text-muted-foreground/60">
|
||||
{config.lastPlayedDate ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />{formatLastPlayed(config.lastPlayedDate)}
|
||||
</span>
|
||||
) : null}
|
||||
{config.playtime > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Gamepad2 className="w-3 h-3" />{formatPlaytime(config.playtime)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
className="w-full"
|
||||
variant="flat"
|
||||
startContent={<Play className="w-3.5 h-3.5" />}
|
||||
onPress={() => onPlay(name)}
|
||||
isDisabled={isLaunching}
|
||||
>
|
||||
启动游戏
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+162
-3
@@ -1,8 +1,167 @@
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { Button } from "@heroui/react";
|
||||
import { Plus, Gamepad2, RefreshCw } from "lucide-react";
|
||||
import { useInstanceStore } from "@/stores/instanceStore";
|
||||
import { useConfigStore } from "@/stores/configStore";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Surface } from "@/components/setting/Surface";
|
||||
import { InstanceCard } from "./InstanceCard";
|
||||
import { CreateDialog } from "./CreateDialog";
|
||||
import type { InstanceRuntime } from "@/api/instance";
|
||||
|
||||
export function Gallery() {
|
||||
const { instances, loading, fetchInstances, create, launch } = useInstanceStore();
|
||||
const gameConfig = useConfigStore((s) => s.config.game);
|
||||
const configInstances = useConfigStore((s) => s.config.instances);
|
||||
const setInstances = useConfigStore((s) => s.setInstances);
|
||||
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [launching, setLaunching] = useState<string | null>(null);
|
||||
|
||||
const loadInstances = useCallback(async () => {
|
||||
await fetchInstances(gameConfig.gameDir);
|
||||
}, [fetchInstances, gameConfig.gameDir]);
|
||||
|
||||
useEffect(() => {
|
||||
loadInstances();
|
||||
}, [loadInstances]);
|
||||
|
||||
const handleCreate = async (name: string, runtime: InstanceRuntime, displayName?: string) => {
|
||||
await create(name, gameConfig.gameDir, runtime, {
|
||||
description: displayName,
|
||||
});
|
||||
|
||||
// Sync to Koring.yml
|
||||
const loader = runtime.forge ? "forge"
|
||||
: runtime.fabricLoader ? "fabric"
|
||||
: runtime.quiltLoader ? "quilt"
|
||||
: runtime.neoForged ? "neoforged"
|
||||
: "vanilla";
|
||||
|
||||
const loaderVersion = runtime.forge || runtime.fabricLoader || runtime.quiltLoader || runtime.neoForged || "";
|
||||
|
||||
const updated = configInstances.filter((m) => m.name !== name);
|
||||
updated.push({
|
||||
name,
|
||||
displayName: displayName || name,
|
||||
icon: "",
|
||||
gameVersion: runtime.minecraft,
|
||||
loader,
|
||||
loaderVersion,
|
||||
createdAt: Date.now(),
|
||||
lastPlayed: 0,
|
||||
playtime: 0,
|
||||
});
|
||||
setInstances(updated);
|
||||
setCreateOpen(false);
|
||||
};
|
||||
|
||||
const handlePlay = async (name: string) => {
|
||||
setLaunching(name);
|
||||
try {
|
||||
await launch(name, gameConfig.gameDir);
|
||||
} catch {
|
||||
// Launch events handled via IPC listeners
|
||||
}
|
||||
// Update lastPlayed in config
|
||||
const idx = configInstances.findIndex((m) => m.name === name);
|
||||
if (idx >= 0) {
|
||||
const updated = [...configInstances];
|
||||
updated[idx] = { ...updated[idx], lastPlayed: Date.now() };
|
||||
setInstances(updated);
|
||||
}
|
||||
setLaunching(null);
|
||||
};
|
||||
|
||||
const handleSettings = (name: string) => {
|
||||
// TODO: Navigate to instance settings
|
||||
console.log("Settings for:", name);
|
||||
};
|
||||
|
||||
const getDisplayName = (name: string) => {
|
||||
return configInstances.find((m) => m.name === name)?.displayName;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h1 className="text-xl font-bold text-foreground mb-2">实例管理</h1>
|
||||
<p className="text-sm text-muted-foreground">管理与选择我的世界实例</p>
|
||||
<div className="p-6 h-full overflow-y-auto scroll-area">
|
||||
{/* 头部 */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground">实例管理</h1>
|
||||
<p className="text-[13px] text-muted-foreground mt-1">
|
||||
创建、管理和启动 Minecraft 实例
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm" variant="light" onPress={loadInstances} isDisabled={loading}>
|
||||
<RefreshCw className={`w-4 h-4 ${loading ? "animate-spin" : ""}`} />
|
||||
</Button>
|
||||
<Button size="sm" startContent={<Plus className="w-4 h-4" />} onPress={() => setCreateOpen(true)}>
|
||||
新建实例
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 加载骨架 */}
|
||||
{loading && instances.length === 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<Surface key={i} padding="md">
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-3 w-1/2" />
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-5 w-14 rounded-md" />
|
||||
<Skeleton className="h-5 w-20 rounded-md" />
|
||||
</div>
|
||||
<Skeleton className="h-8 w-full rounded-lg" />
|
||||
</div>
|
||||
</Surface>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 空状态 */}
|
||||
{!loading && instances.length === 0 && (
|
||||
<Surface variant="flat" padding="lg">
|
||||
<div className="flex flex-col items-center gap-4 py-16">
|
||||
<div className="w-16 h-16 rounded-full bg-foreground/[0.04] dark:bg-white/[0.04] flex items-center justify-center">
|
||||
<Gamepad2 className="w-8 h-8 text-muted-foreground/30" />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-muted-foreground">还没有任何实例</p>
|
||||
<p className="text-[12px] text-muted-foreground/60 mt-1 max-w-[300px]">
|
||||
点击"新建实例"创建你的第一个 Minecraft 游戏实例
|
||||
</p>
|
||||
</div>
|
||||
<Button size="sm" startContent={<Plus className="w-4 h-4" />} onPress={() => setCreateOpen(true)}>
|
||||
新建实例
|
||||
</Button>
|
||||
</div>
|
||||
</Surface>
|
||||
)}
|
||||
|
||||
{/* 实例网格 */}
|
||||
{instances.length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{instances.map((inst) => (
|
||||
<InstanceCard
|
||||
key={inst.name}
|
||||
instance={inst}
|
||||
displayName={getDisplayName(inst.name)}
|
||||
onPlay={handlePlay}
|
||||
onSettings={handleSettings}
|
||||
isLaunching={launching === inst.name}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CreateDialog
|
||||
isOpen={createOpen}
|
||||
onClose={() => setCreateOpen(false)}
|
||||
onCreate={handleCreate}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { useKoringAuthStore } from "@/stores/koringAuthStore";
|
||||
import { BUILD_MODE } from "@/lib/mode";
|
||||
import { useUpdateStore } from "@/stores/updateStore";
|
||||
import {
|
||||
UserCircle,
|
||||
Gamepad2,
|
||||
@@ -10,10 +7,9 @@ import {
|
||||
Cpu,
|
||||
Info,
|
||||
ChevronRight,
|
||||
Search,
|
||||
} from "lucide-react";
|
||||
import { Avatar } from "@heroui/react";
|
||||
import { SettingCard, PageHeader, SectionTitle } from "@/components/setting";
|
||||
import { VersionCard } from "@/components/VersionCard";
|
||||
import { Surface, PageHeader, SectionTitle } from "@/components/setting";
|
||||
|
||||
interface ShortcutItem {
|
||||
icon: React.ReactNode;
|
||||
@@ -26,34 +22,28 @@ function ShortcutTile({ icon, label, desc, navKey, onClick }: ShortcutItem & { o
|
||||
return (
|
||||
<button
|
||||
onClick={() => onClick(navKey)}
|
||||
className="glass-card flex items-center gap-3.5 px-4 py-3.5 text-left transition-all duration-150 hover:bg-foreground/[0.04] group"
|
||||
className="w-full flex items-center gap-3 px-3.5 py-3 text-left rounded-lg transition-colors duration-150 hover:bg-foreground/5 dark:hover:bg-white/5 group"
|
||||
>
|
||||
<span className="flex items-center justify-center w-9 h-9 rounded-lg bg-foreground/[0.06] text-foreground/60 group-hover:text-foreground/80 shrink-0">
|
||||
<span className="flex items-center justify-center w-8 h-8 rounded-md bg-foreground/[0.06] dark:bg-white/[0.08] text-foreground/55 group-hover:text-foreground/80 shrink-0 transition-colors">
|
||||
{icon}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-foreground truncate">{label}</p>
|
||||
<p className="text-[12px] text-muted-foreground truncate">{desc}</p>
|
||||
<p className="text-[13px] font-medium text-foreground truncate">{label}</p>
|
||||
<p className="text-[11.5px] text-muted-foreground truncate mt-0.5">{desc}</p>
|
||||
</div>
|
||||
<ChevronRight className="w-4 h-4 text-foreground/25 shrink-0" />
|
||||
<ChevronRight className="w-4 h-4 text-foreground/20 shrink-0" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const modeLabels: Record<string, string> = {
|
||||
dev: "开发版",
|
||||
beta: "测试版",
|
||||
run: "正式版",
|
||||
};
|
||||
|
||||
const shortcuts: ShortcutItem[] = [
|
||||
{ icon: <UserCircle className="w-[18px] h-[18px]" />, label: "Koring 账户", desc: "同步数据、皮肤与个人配置", navKey: "account" },
|
||||
{ icon: <Gamepad2 className="w-[18px] h-[18px]" />, label: "游戏账户与档案", desc: "管理游戏内账户和档案配置", navKey: "game-account" },
|
||||
{ icon: <Palette className="w-[18px] h-[18px]" />, label: "主题与背景", desc: "深色模式、背景图片与视差", navKey: "theme-bg" },
|
||||
{ icon: <Download className="w-[18px] h-[18px]" />, label: "下载设置", desc: "下载线程数与存储路径", navKey: "download" },
|
||||
{ icon: <Globe className="w-[18px] h-[18px]" />, label: "联机功能", desc: "以太联机与陶瓦联机", navKey: "ether-online" },
|
||||
{ icon: <Cpu className="w-[18px] h-[18px]" />, label: "Java 虚拟机与内存", desc: "Java 路径与内存分配", navKey: "java-mem" },
|
||||
{ icon: <Info className="w-[18px] h-[18px]" />, label: "关于 Koring Launcher", desc: "版本信息与更新", navKey: "about" },
|
||||
{ icon: <UserCircle className="w-4 h-4" />, label: "Koring 账户", desc: "同步数据、皮肤与个人配置", navKey: "account" },
|
||||
{ icon: <Gamepad2 className="w-4 h-4" />, label: "游戏账户与档案", desc: "管理游戏内账户和档案配置", navKey: "game-account" },
|
||||
{ icon: <Palette className="w-4 h-4" />, label: "主题与背景", desc: "深色模式、背景图片与视差", navKey: "theme-bg" },
|
||||
{ icon: <Download className="w-4 h-4" />, label: "下载设置", desc: "下载线程数与存储路径", navKey: "download" },
|
||||
{ icon: <Globe className="w-4 h-4" />, label: "联机功能", desc: "以太联机与陶瓦联机", navKey: "ether-online" },
|
||||
{ icon: <Cpu className="w-4 h-4" />, label: "Java 虚拟机与内存", desc: "Java 路径与内存分配", navKey: "java-mem" },
|
||||
{ icon: <Info className="w-4 h-4" />, label: "关于 Koring Launcher", desc: "版本信息与更新", navKey: "about" },
|
||||
];
|
||||
|
||||
interface HomeSettingProps {
|
||||
@@ -61,55 +51,22 @@ interface HomeSettingProps {
|
||||
}
|
||||
|
||||
export function HomeSetting({ onNavigate }: HomeSettingProps) {
|
||||
const user = useKoringAuthStore((s) => s.user);
|
||||
const { update } = useUpdateStore();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader title="主页" desc="自定义启动器主页的显示内容与常用设置的快捷入口" />
|
||||
<PageHeader title="主页" desc="版本信息与常用设置的快捷入口" />
|
||||
|
||||
<div className="space-y-6">
|
||||
<SettingCard className="flex items-center gap-3 px-4 py-3">
|
||||
<Search className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
<span className="text-sm text-muted-foreground">查找设置</span>
|
||||
</SettingCard>
|
||||
|
||||
<div>
|
||||
<SectionTitle>快速概览</SectionTitle>
|
||||
<SettingCard>
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar size="lg">
|
||||
{user?.picture ? (
|
||||
<Avatar.Image src={user.picture} alt="" />
|
||||
) : (
|
||||
<Avatar.Fallback>
|
||||
<UserCircle className="w-7 h-7 text-foreground/40" />
|
||||
</Avatar.Fallback>
|
||||
)}
|
||||
</Avatar>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-foreground truncate">
|
||||
{user ? (user.name || user.username) : "未登录"}
|
||||
</p>
|
||||
<p className="text-[12px] text-muted-foreground mt-0.5">
|
||||
{user ? "Koring 账户" : "登录以同步数据和皮肤"}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-[11px] text-muted-foreground shrink-0">
|
||||
{modeLabels[BUILD_MODE] ?? BUILD_MODE}
|
||||
{update ? " · 有更新" : ""}
|
||||
</span>
|
||||
</div>
|
||||
</SettingCard>
|
||||
</div>
|
||||
<VersionCard simple />
|
||||
|
||||
<div>
|
||||
<SectionTitle>常用设置</SectionTitle>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{shortcuts.map((s) => (
|
||||
<ShortcutTile key={s.navKey} {...s} onClick={onNavigate} />
|
||||
))}
|
||||
</div>
|
||||
<Surface padding="sm">
|
||||
<div className="divide-y divide-border/30 dark:divide-white/[0.04]">
|
||||
{shortcuts.map((s) => (
|
||||
<ShortcutTile key={s.navKey} {...s} onClick={onNavigate} />
|
||||
))}
|
||||
</div>
|
||||
</Surface>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
type AdvancedConfig,
|
||||
type DownloadConfig,
|
||||
type NetworkConfig,
|
||||
type InstanceMeta,
|
||||
} from "@/api/config";
|
||||
import { DEFAULT_BG } from "@/lib/mode";
|
||||
|
||||
@@ -39,6 +40,7 @@ interface ConfigState {
|
||||
setAdvanced: (patch: Partial<AdvancedConfig>) => void;
|
||||
setDownload: (patch: Partial<DownloadConfig>) => void;
|
||||
setNetwork: (patch: Partial<NetworkConfig>) => void;
|
||||
setInstances: (instances: InstanceMeta[]) => void;
|
||||
setOobe: (value: boolean) => void;
|
||||
}
|
||||
|
||||
@@ -53,6 +55,7 @@ const DEFAULT_CONFIG: AppConfig = {
|
||||
advanced: { afterLaunch: "close", winMode: "default", customWidth: 854, customHeight: 480, gameArgs: "", preLaunchCmd: "", debugMode: false },
|
||||
download: { fileSource: "mirror", versionSource: "mirror", threads: 16, speedLimit: 0 },
|
||||
network: { securityId: { enabled: false, authUrl: "" } },
|
||||
instances: [],
|
||||
};
|
||||
|
||||
export const useConfigStore = create<ConfigState>((set, get) => ({
|
||||
@@ -132,6 +135,13 @@ export const useConfigStore = create<ConfigState>((set, get) => ({
|
||||
debouncedSave(next);
|
||||
},
|
||||
|
||||
setInstances: (instances) => {
|
||||
const { config } = get();
|
||||
const next = { ...config, instances };
|
||||
set({ config: next });
|
||||
debouncedSave(next);
|
||||
},
|
||||
|
||||
setOobe: (value) => {
|
||||
const { config } = get();
|
||||
const next = { ...config, oobe: value };
|
||||
|
||||
@@ -38,5 +38,8 @@ export default defineConfig(async () => ({
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
watch: {
|
||||
ignored: ["**/Koring.yml", "**/koring-auth.json"],
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user