mirror of
https://github.com/dream-pep/koring-launcher.git
synced 2026-09-12 05:45:18 +08:00
Add Koring auth flow and settings UI refactor
Implements Koring OIDC device-auth flow and integrates it into the app: core auth (TS/JS), IPC handlers, renderer API, KoringLogin React component, and a zustand koringAuth store. Handlers support device-code request, polling, refresh, get-user and logout and persist user info to existing auth/config. Also refactors many settings pages to use shared Setting components (SettingCard, SettingRow, SectionTitle, PageHeader) and replaces custom controls with @heroui/react primitives (Avatar, Button, Switch, RadioGroup, Slider, Input, TextArea, EmptyState). Adds OOBE login/welcome pages, a setting login route, installer header generator script, and a sample koring-auth.json for development.
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { ipcInvoke } from './ipc';
|
||||
|
||||
export interface DeviceAuthResponse {
|
||||
device_code: string;
|
||||
user_code: string;
|
||||
verification_uri: string;
|
||||
verification_uri_complete: string;
|
||||
expires_in: number;
|
||||
}
|
||||
|
||||
export interface KoringUser {
|
||||
sub: string;
|
||||
name: string;
|
||||
username: string;
|
||||
email: string;
|
||||
picture: string;
|
||||
}
|
||||
|
||||
export interface KoringAuthData {
|
||||
user: KoringUser;
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
id_token: string;
|
||||
expires_at: number;
|
||||
}
|
||||
|
||||
export async function requestDeviceCode(): Promise<DeviceAuthResponse> {
|
||||
return ipcInvoke<DeviceAuthResponse>('koring-auth:request-device-code');
|
||||
}
|
||||
|
||||
export async function pollForToken(
|
||||
device_code: string
|
||||
): Promise<{ user: KoringUser }> {
|
||||
return ipcInvoke<{ user: KoringUser }>('koring-auth:poll-token', device_code);
|
||||
}
|
||||
|
||||
export async function refreshKoringToken(): Promise<{ user: KoringUser }> {
|
||||
return ipcInvoke<{ user: KoringUser }>('koring-auth:refresh');
|
||||
}
|
||||
|
||||
export async function getKoringUser(): Promise<KoringAuthData | null> {
|
||||
return ipcInvoke<KoringAuthData | null>('koring-auth:get-user');
|
||||
}
|
||||
|
||||
export async function logoutKoring(): Promise<void> {
|
||||
return ipcInvoke<void>('koring-auth:logout');
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { QRCodeSVG } from "qrcode.react";
|
||||
import {
|
||||
requestDeviceCode,
|
||||
pollForToken,
|
||||
type KoringUser,
|
||||
} from "@/api/koring-auth";
|
||||
import { useKoringAuthStore } from "@/stores/koringAuthStore";
|
||||
import { Loader2, CheckCircle2, Copy, ExternalLink } from "lucide-react";
|
||||
|
||||
type Step = "loading" | "scan" | "polling" | "success" | "error";
|
||||
|
||||
interface KoringLoginProps {
|
||||
onLoginSuccess?: (user: KoringUser) => void;
|
||||
}
|
||||
|
||||
export function KoringLogin({ onLoginSuccess }: KoringLoginProps) {
|
||||
const [step, setStep] = useState<Step>("loading");
|
||||
const [userCode, setUserCode] = useState("");
|
||||
const [verifyUri, setVerifyUri] = useState("");
|
||||
const [verifyUriComplete, setVerifyUriComplete] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [copied, setCopied] = useState(false);
|
||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const setUser = useKoringAuthStore((s) => s.setUser);
|
||||
|
||||
const cleanup = useCallback(() => {
|
||||
if (pollRef.current) {
|
||||
clearInterval(pollRef.current);
|
||||
pollRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => cleanup, [cleanup]);
|
||||
|
||||
// Auto-start on mount
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const res = await requestDeviceCode();
|
||||
if (cancelled) return;
|
||||
setUserCode(res.user_code);
|
||||
setVerifyUri(res.verification_uri);
|
||||
setVerifyUriComplete(res.verification_uri_complete);
|
||||
setStep("scan");
|
||||
startPolling(res.device_code, res.expires_in, cancelled);
|
||||
} catch (e: any) {
|
||||
if (!cancelled) {
|
||||
setError(e.message || "请求设备码失败");
|
||||
setStep("error");
|
||||
}
|
||||
}
|
||||
})();
|
||||
return () => { cancelled = true; cleanup(); };
|
||||
}, []);
|
||||
|
||||
const startPolling = (deviceCode: string, expires: number, cancelled: boolean) => {
|
||||
setStep("polling");
|
||||
const startTime = Date.now();
|
||||
pollRef.current = setInterval(async () => {
|
||||
if (cancelled) { cleanup(); return; }
|
||||
if (Date.now() - startTime > expires * 1000) {
|
||||
cleanup();
|
||||
setError("二维码已过期,请重新获取");
|
||||
setStep("error");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await pollForToken(deviceCode);
|
||||
if (cancelled) return;
|
||||
cleanup();
|
||||
setUser(result.user);
|
||||
setStep("success");
|
||||
onLoginSuccess?.(result.user);
|
||||
} catch (e: any) {
|
||||
if (e.message !== "authorization_pending" && e.message !== "slow_down") {
|
||||
if (!cancelled) {
|
||||
cleanup();
|
||||
setError(e.message || "验证失败");
|
||||
setStep("error");
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
const retry = async () => {
|
||||
setStep("loading");
|
||||
setError("");
|
||||
try {
|
||||
const res = await requestDeviceCode();
|
||||
setUserCode(res.user_code);
|
||||
setVerifyUri(res.verification_uri);
|
||||
setVerifyUriComplete(res.verification_uri_complete);
|
||||
setStep("scan");
|
||||
startPolling(res.device_code, res.expires_in, false);
|
||||
} catch (e: any) {
|
||||
setError(e.message || "请求设备码失败");
|
||||
setStep("error");
|
||||
}
|
||||
};
|
||||
|
||||
const copyCode = () => {
|
||||
navigator.clipboard.writeText(userCode);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
const openVerify = () => {
|
||||
window.electronAPI?.openExternal(verifyUri);
|
||||
};
|
||||
|
||||
if (step === "loading") {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-3 py-6">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-foreground/40" />
|
||||
<span className="text-sm text-muted-foreground">正在获取设备码...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === "error") {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-3 py-4">
|
||||
<p className="text-sm text-red-500">{error}</p>
|
||||
<button
|
||||
onClick={retry}
|
||||
className="px-4 py-1.5 rounded-md text-[13px] font-medium bg-foreground/[0.06] hover:bg-foreground/[0.12] text-foreground/60 hover:text-foreground transition-colors"
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
{/* QR Code */}
|
||||
<div className="p-3 bg-white rounded-xl">
|
||||
<QRCodeSVG value={verifyUriComplete} size={160} level="M" />
|
||||
</div>
|
||||
|
||||
{/* 提示文字 */}
|
||||
<p className="text-sm text-muted-foreground text-center leading-relaxed">
|
||||
请使用任意二维码扫描器打开
|
||||
<br />
|
||||
或访问以下链接并输入验证码
|
||||
</p>
|
||||
|
||||
{/* 验证链接 */}
|
||||
<button
|
||||
onClick={openVerify}
|
||||
className="inline-flex items-center gap-1.5 text-[13px] text-primary hover:underline"
|
||||
>
|
||||
{verifyUri}
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</button>
|
||||
|
||||
{/* 用户码 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl font-mono font-bold tracking-[0.3em] text-foreground">
|
||||
{userCode}
|
||||
</span>
|
||||
<button
|
||||
onClick={copyCode}
|
||||
className="p-1.5 rounded-md hover:bg-foreground/[0.06] transition-colors"
|
||||
title="复制验证码"
|
||||
>
|
||||
{copied ? (
|
||||
<CheckCircle2 className="w-4 h-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4 text-foreground/40" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 轮询状态 */}
|
||||
{step === "polling" && (
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
等待验证中...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === "success" && (
|
||||
<div className="flex items-center gap-2 text-sm text-green-600 dark:text-green-400">
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
登录成功
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export function PageHeader({ title, desc }: { title: string; desc: string }) {
|
||||
return (
|
||||
<>
|
||||
<h2 className="text-xl font-bold text-foreground mb-1">{title}</h2>
|
||||
<p className="text-sm text-muted-foreground mb-6">{desc}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function SectionTitle({ children }: { children: React.ReactNode }) {
|
||||
return <h3 className="text-lg font-bold text-foreground mb-3">{children}</h3>;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Card } from "@heroui/react";
|
||||
|
||||
export function SettingCard({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||
return (
|
||||
<Card variant="transparent" className={`glass-card px-5 py-4 ${className ?? ""}`}>
|
||||
{children}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export function SettingRow({ label, desc, children }: { label: string; desc?: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-foreground">{label}</p>
|
||||
{desc && <p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>}
|
||||
</div>
|
||||
<div className="shrink-0">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { SettingCard } from "./SettingCard";
|
||||
export { SettingRow } from "./SettingRow";
|
||||
export { PageHeader, SectionTitle } from "./SectionTitle";
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useRouteStore } from "@/stores/routeStore";
|
||||
import { useKoringAuthStore } from "@/stores/koringAuthStore";
|
||||
import { OobeLayout } from "./layout";
|
||||
import { NextButton } from "./next-button";
|
||||
import { KoringLogin } from "@/components/KoringLogin";
|
||||
|
||||
export function OobeLogin() {
|
||||
const navigate = useRouteStore((s) => s.navigate);
|
||||
const user = useKoringAuthStore((s) => s.user);
|
||||
const [showSkip, setShowSkip] = useState(false);
|
||||
const [justLoggedIn, setJustLoggedIn] = useState(false);
|
||||
|
||||
// 10秒后显示"我暂时不需要"
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => setShowSkip(true), 10000);
|
||||
return () => clearTimeout(t);
|
||||
}, []);
|
||||
|
||||
const handleSuccess = useCallback(() => {
|
||||
setJustLoggedIn(true);
|
||||
setTimeout(() => navigate("oobe/agreement"), 1500);
|
||||
}, [navigate]);
|
||||
|
||||
return (
|
||||
<OobeLayout>
|
||||
<div className="w-full max-w-md flex flex-col items-center gap-6 px-6">
|
||||
{/* 标题 */}
|
||||
<div className="text-center space-y-1">
|
||||
<h2 className="text-lg font-bold text-foreground">登录 Koring 账户</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
扫码登录以同步数据、皮肤与个人配置
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 登录组件 — 自动显示 QR */}
|
||||
<KoringLogin onLoginSuccess={handleSuccess} />
|
||||
|
||||
{/* 已登录提示 */}
|
||||
{justLoggedIn && (
|
||||
<p className="text-xs text-green-600 dark:text-green-400">
|
||||
登录成功,正在跳转...
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 下一步按钮 — 需要登录才能点击 */}
|
||||
<NextButton onClick={() => navigate("oobe/agreement")} disabled={!user && !justLoggedIn} />
|
||||
|
||||
{/* 跳过按钮 — 10秒后显示 */}
|
||||
{showSkip && !user && !justLoggedIn && (
|
||||
<div className="absolute bottom-6">
|
||||
<button
|
||||
onClick={() => navigate("oobe/agreement")}
|
||||
className="text-[12px] text-foreground/30 hover:text-foreground/50 transition-colors"
|
||||
>
|
||||
我暂时不需要
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</OobeLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouteStore } from "@/stores/routeStore";
|
||||
import { useKoringAuthStore } from "@/stores/koringAuthStore";
|
||||
import { OobeLayout } from "./layout";
|
||||
import { NextButton } from "./next-button";
|
||||
|
||||
export function OobeWelcome() {
|
||||
const navigate = useRouteStore((s) => s.navigate);
|
||||
const user = useKoringAuthStore((s) => s.user);
|
||||
const [showBtn, setShowBtn] = useState(false);
|
||||
const [animClass, setAnimClass] = useState("scale-90 opacity-0");
|
||||
|
||||
useEffect(() => {
|
||||
requestAnimationFrame(() => {
|
||||
setAnimClass("scale-100 opacity-100");
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => setShowBtn(true), 1500);
|
||||
return () => clearTimeout(t);
|
||||
}, []);
|
||||
|
||||
const displayName = user?.name || user?.username || "用户";
|
||||
|
||||
return (
|
||||
<OobeLayout>
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<h2
|
||||
className={`text-4xl font-bold text-foreground transition-all duration-700 ease-out ${animClass}`}
|
||||
>
|
||||
欢迎回来,{displayName}。
|
||||
</h2>
|
||||
{user?.sub && (
|
||||
<p className="text-xs text-muted-foreground font-mono">
|
||||
UUID: {user.sub}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showBtn && <NextButton onClick={() => navigate("oobe/version")} />}
|
||||
</OobeLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +1,6 @@
|
||||
import { useConfigStore } from "@/stores/configStore";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
function GlassCard({ children }: { children: React.ReactNode }) {
|
||||
return <div className="glass-card px-5 py-4">{children}</div>;
|
||||
}
|
||||
|
||||
function SettingRow({ label, desc, children }: { label: string; desc?: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-foreground">{label}</p>
|
||||
{desc && <p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>}
|
||||
</div>
|
||||
<div className="shrink-0">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { Switch, RadioGroup, Radio, Input, TextArea } from "@heroui/react";
|
||||
import { SettingCard, SettingRow, PageHeader, SectionTitle } from "@/components/setting";
|
||||
|
||||
const launcherBehavior = [
|
||||
{ value: "close", label: "关闭启动器" },
|
||||
@@ -35,125 +20,127 @@ export function AdvancedSetting() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-foreground mb-1">高级设置</h2>
|
||||
<p className="text-sm text-muted-foreground mb-6">游戏高级启动参数、调试选项与实验性功能</p>
|
||||
<PageHeader title="高级设置" desc="游戏高级启动参数、调试选项与实验性功能" />
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* ===== 启动行为 ===== */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">启动行为</h3>
|
||||
<SectionTitle>启动行为</SectionTitle>
|
||||
<div className="space-y-3">
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-foreground">启动后启动器行为</p>
|
||||
<p className="text-[13px] text-muted-foreground">游戏启动后启动器的处理方式</p>
|
||||
<div className="space-y-2 mt-2">
|
||||
<RadioGroup
|
||||
value={adv.afterLaunch}
|
||||
onValueChange={(v) => setAdvanced({ afterLaunch: v })}
|
||||
className="mt-2 space-y-2"
|
||||
>
|
||||
{launcherBehavior.map((opt) => (
|
||||
<label key={opt.value} className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="afterLaunch" checked={adv.afterLaunch === opt.value} onChange={() => setAdvanced({ afterLaunch: opt.value })} className="accent-primary" />
|
||||
<span className="text-sm text-foreground">{opt.label}</span>
|
||||
</label>
|
||||
<Radio key={opt.value} value={opt.value}>
|
||||
<Radio.Content>{opt.label}</Radio.Content>
|
||||
</Radio>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ===== 窗口设置 ===== */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">窗口设置</h3>
|
||||
<SectionTitle>窗口设置</SectionTitle>
|
||||
<div className="space-y-3">
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-foreground">窗口大小</p>
|
||||
<div className="space-y-2">
|
||||
<RadioGroup
|
||||
value={adv.winMode}
|
||||
onValueChange={(v) => setAdvanced({ winMode: v })}
|
||||
className="space-y-2"
|
||||
>
|
||||
{windowSize.map((opt) => (
|
||||
<label key={opt.value} className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="winMode" checked={adv.winMode === opt.value} onChange={() => setAdvanced({ winMode: opt.value })} className="accent-primary" />
|
||||
<span className="text-sm text-foreground">{opt.label}</span>
|
||||
</label>
|
||||
<Radio key={opt.value} value={opt.value}>
|
||||
<Radio.Content>{opt.label}</Radio.Content>
|
||||
</Radio>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
{adv.winMode === "custom" && (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[13px] text-muted-foreground">宽</span>
|
||||
<input
|
||||
<Input
|
||||
type="number"
|
||||
value={adv.customWidth}
|
||||
value={String(adv.customWidth)}
|
||||
onChange={(e) => setAdvanced({ customWidth: Number(e.target.value) })}
|
||||
className="w-20 h-8 px-2 rounded-md border border-input bg-background text-sm text-center font-mono focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
className="w-20"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-muted-foreground">×</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[13px] text-muted-foreground">高</span>
|
||||
<input
|
||||
<Input
|
||||
type="number"
|
||||
value={adv.customHeight}
|
||||
value={String(adv.customHeight)}
|
||||
onChange={(e) => setAdvanced({ customHeight: Number(e.target.value) })}
|
||||
className="w-20 h-8 px-2 rounded-md border border-input bg-background text-sm text-center font-mono focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
className="w-20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ===== 游戏参数 ===== */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">游戏参数</h3>
|
||||
<SectionTitle>游戏参数</SectionTitle>
|
||||
<div className="space-y-3">
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-foreground">额外游戏启动参数</p>
|
||||
<p className="text-[13px] text-muted-foreground">附加到游戏启动命令末尾的参数</p>
|
||||
<input
|
||||
type="text"
|
||||
<Input
|
||||
value={adv.gameArgs}
|
||||
onChange={(e) => setAdvanced({ gameArgs: e.target.value })}
|
||||
placeholder="可选,例如 --demo"
|
||||
className="w-full h-8 px-3 rounded-md border border-input bg-background text-sm font-mono placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ===== 启动命令 ===== */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">启动命令</h3>
|
||||
<SectionTitle>启动命令</SectionTitle>
|
||||
<div className="space-y-3">
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-foreground">启动前执行命令</p>
|
||||
<p className="text-[13px] text-muted-foreground">游戏启动前自动执行的命令或程序路径</p>
|
||||
<input
|
||||
type="text"
|
||||
<Input
|
||||
value={adv.preLaunchCmd}
|
||||
onChange={(e) => setAdvanced({ preLaunchCmd: e.target.value })}
|
||||
placeholder="可选,例如 D:\scripts\pre-launch.bat"
|
||||
className="w-full h-8 px-3 rounded-md border border-input bg-background text-sm font-mono placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ===== 调试 ===== */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">调试</h3>
|
||||
<SectionTitle>调试</SectionTitle>
|
||||
<div className="space-y-3">
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<SettingRow label="调试模式" desc="启用后将在控制台输出详细日志,可能影响性能">
|
||||
<Switch checked={adv.debugMode} onCheckedChange={(v) => setAdvanced({ debugMode: v })} />
|
||||
<Switch isSelected={adv.debugMode} onValueChange={(v) => setAdvanced({ debugMode: v })}>
|
||||
<Switch.Control>
|
||||
<Switch.Thumb />
|
||||
</Switch.Control>
|
||||
</Switch>
|
||||
</SettingRow>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { EmptyState } from "@heroui/react";
|
||||
import { Gamepad2 } from "lucide-react";
|
||||
import { PageHeader } from "@/components/setting";
|
||||
|
||||
export function GameAccountSetting() {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-foreground mb-1">游戏账户&档案</h2>
|
||||
<p className="text-sm text-muted-foreground mb-6">管理 Minecraft 游戏内账户、正版验证与游戏档案配置</p>
|
||||
<PageHeader title="游戏账户&档案" desc="管理 Minecraft 游戏内账户、正版验证与游戏档案配置" />
|
||||
<EmptyState className="py-16">
|
||||
<Gamepad2 className="w-10 h-10 text-muted-foreground/30" />
|
||||
<p className="text-sm text-muted-foreground mt-3">该功能正在开发中</p>
|
||||
</EmptyState>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,30 +1,26 @@
|
||||
import { useConfigStore } from "@/stores/configStore";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Button, Input } from "@heroui/react";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
|
||||
function GlassCard({ children }: { children: React.ReactNode }) {
|
||||
return <div className="glass-card px-5 py-4">{children}</div>;
|
||||
}
|
||||
import { SettingCard, PageHeader, SectionTitle } from "@/components/setting";
|
||||
|
||||
function PathInput({ label, desc, value, onChange }: { label: string; desc: string; value: string; onChange: (v: string) => void }) {
|
||||
return (
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">{label}</p>
|
||||
<p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="flex-1 h-8 px-3 rounded-md border border-input bg-background text-sm font-mono placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
fullWidth
|
||||
/>
|
||||
<Button size="sm" variant="outline">浏览</Button>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,13 +30,11 @@ export function GameDirSetting() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-foreground mb-1">游戏目录</h2>
|
||||
<p className="text-sm text-muted-foreground mb-6">设置 Minecraft 游戏安装路径、存档位置与资源包目录</p>
|
||||
<PageHeader title="游戏目录" desc="设置 Minecraft 游戏安装路径、存档位置与资源包目录" />
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* ===== 游戏路径 ===== */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">游戏路径</h3>
|
||||
<SectionTitle>游戏路径</SectionTitle>
|
||||
<div className="space-y-3">
|
||||
<PathInput
|
||||
label="游戏安装根目录"
|
||||
@@ -48,7 +42,7 @@ export function GameDirSetting() {
|
||||
value={game.gameDir}
|
||||
onChange={(v) => setGame({ gameDir: v })}
|
||||
/>
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">已安装版本</p>
|
||||
@@ -59,13 +53,12 @@ export function GameDirSetting() {
|
||||
扫描
|
||||
</Button>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ===== 资源包 ===== */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">资源包目录</h3>
|
||||
<SectionTitle>资源包目录</SectionTitle>
|
||||
<div className="space-y-3">
|
||||
<PathInput
|
||||
label="资源包路径"
|
||||
@@ -76,9 +69,8 @@ export function GameDirSetting() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ===== 存档 ===== */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">存档目录</h3>
|
||||
<SectionTitle>存档目录</SectionTitle>
|
||||
<div className="space-y-3">
|
||||
<PathInput
|
||||
label="存档路径"
|
||||
|
||||
@@ -1,23 +1,7 @@
|
||||
import { useConfigStore } from "@/stores/configStore";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { Button, Slider, RadioGroup, Radio, Input, TextArea } from "@heroui/react";
|
||||
import { Cpu, FolderSearch } from "lucide-react";
|
||||
|
||||
function GlassCard({ children }: { children: React.ReactNode }) {
|
||||
return <div className="glass-card px-5 py-4">{children}</div>;
|
||||
}
|
||||
|
||||
function SettingRow({ label, desc, children }: { label: string; desc?: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-foreground">{label}</p>
|
||||
{desc && <p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>}
|
||||
</div>
|
||||
<div className="shrink-0">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { SettingCard, SettingRow, PageHeader, SectionTitle } from "@/components/setting";
|
||||
|
||||
interface JavaInfo {
|
||||
path: string;
|
||||
@@ -42,25 +26,23 @@ export function JavaMemSetting() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-foreground mb-1">Java 虚拟机与内存</h2>
|
||||
<p className="text-sm text-muted-foreground mb-6">配置 Java 运行环境路径、JVM 参数与游戏内存分配</p>
|
||||
<PageHeader title="Java 虚拟机与内存" desc="配置 Java 运行环境路径、JVM 参数与游戏内存分配" />
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* ===== Java 环境 ===== */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">Java 环境</h3>
|
||||
<SectionTitle>Java 环境</SectionTitle>
|
||||
<div className="space-y-3">
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<SettingRow label="自动检测" desc="扫描系统中已安装的 Java 版本">
|
||||
<Button size="sm" variant="outline">
|
||||
<FolderSearch className="w-3.5 h-3.5 mr-1.5" />
|
||||
检测
|
||||
</Button>
|
||||
</SettingRow>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
|
||||
{mockJavaList.map((j) => (
|
||||
<GlassCard key={j.path}>
|
||||
<SettingCard key={j.path}>
|
||||
<div className="flex items-center gap-3">
|
||||
<Cpu className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -71,47 +53,47 @@ export function JavaMemSetting() {
|
||||
{j.path}
|
||||
</p>
|
||||
</div>
|
||||
<Button size="sm" variant="outline" className="shrink-0" onClick={() => setJava({ javaPath: j.path })}>
|
||||
<Button size="sm" variant="outline" className="shrink-0" onPress={() => setJava({ javaPath: j.path })}>
|
||||
使用
|
||||
</Button>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
))}
|
||||
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-foreground">手动指定路径</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
<Input
|
||||
value={java.javaPath}
|
||||
onChange={(e) => setJava({ javaPath: e.target.value })}
|
||||
placeholder="输入 javaw.exe 完整路径"
|
||||
className="flex-1 h-8 px-3 rounded-md border border-input bg-background text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
fullWidth
|
||||
/>
|
||||
<Button size="sm" variant="outline">浏览</Button>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ===== 内存分配 ===== */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">内存分配</h3>
|
||||
<SectionTitle>内存分配</SectionTitle>
|
||||
<div className="space-y-3">
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="mem" checked={java.memMode === "auto"} onChange={() => setJava({ memMode: "auto" })} className="accent-primary" />
|
||||
<span className="text-sm text-foreground">自动配置</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="mem" checked={java.memMode === "custom"} onChange={() => setJava({ memMode: "custom" })} className="accent-primary" />
|
||||
<span className="text-sm text-foreground">自定义</span>
|
||||
</label>
|
||||
</div>
|
||||
<RadioGroup
|
||||
value={java.memMode}
|
||||
onValueChange={(v) => setJava({ memMode: v })}
|
||||
className="flex items-center gap-4"
|
||||
>
|
||||
<Radio value="auto">
|
||||
<Radio.Content>自动配置</Radio.Content>
|
||||
</Radio>
|
||||
<Radio value="custom">
|
||||
<Radio.Content>自定义</Radio.Content>
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
{java.memMode === "custom" && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
@@ -119,12 +101,17 @@ export function JavaMemSetting() {
|
||||
<span className="text-[13px] text-muted-foreground tabular-nums">{java.memGB} GB</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[java.memGB]}
|
||||
onValueChange={(v) => setJava({ memGB: Array.isArray(v) ? v[0] : v })}
|
||||
min={1}
|
||||
max={16}
|
||||
value={java.memGB}
|
||||
onChange={(v) => setJava({ memGB: typeof v === "number" ? v : v[0] })}
|
||||
minValue={1}
|
||||
maxValue={16}
|
||||
step={1}
|
||||
/>
|
||||
>
|
||||
<Slider.Track>
|
||||
<Slider.Fill />
|
||||
<Slider.Thumb />
|
||||
</Slider.Track>
|
||||
</Slider>
|
||||
<div className="flex justify-between mt-1">
|
||||
<span className="text-[11px] text-muted-foreground/50">1 GB</span>
|
||||
<span className="text-[11px] text-muted-foreground/50">16 GB</span>
|
||||
@@ -132,47 +119,48 @@ export function JavaMemSetting() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ===== JVM 参数 ===== */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">JVM 参数</h3>
|
||||
<SectionTitle>JVM 参数</SectionTitle>
|
||||
<div className="space-y-3">
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-foreground">额外 JVM 启动参数</p>
|
||||
<p className="text-[13px] text-muted-foreground">每行一个参数,例如 -XX:+UseZGC</p>
|
||||
<textarea
|
||||
<TextArea
|
||||
value={java.jvmArgs}
|
||||
onChange={(e) => setJava({ jvmArgs: e.target.value })}
|
||||
placeholder="可选,留空使用默认参数"
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 rounded-md border border-input bg-background text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring resize-none font-mono"
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ===== 垃圾回收 ===== */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">垃圾回收</h3>
|
||||
<SectionTitle>垃圾回收</SectionTitle>
|
||||
<div className="space-y-3">
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-foreground">GC 算法</p>
|
||||
<div className="space-y-2">
|
||||
<RadioGroup
|
||||
value={java.gc}
|
||||
onValueChange={(v) => setJava({ gc: v })}
|
||||
className="space-y-2"
|
||||
>
|
||||
{gcOptions.map((opt) => (
|
||||
<label key={opt.value} className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="gc" checked={java.gc === opt.value} onChange={() => setJava({ gc: opt.value })} className="accent-primary" />
|
||||
<span className="text-sm text-foreground">{opt.label}</span>
|
||||
</label>
|
||||
<Radio key={opt.value} value={opt.value}>
|
||||
<Radio.Content>{opt.label}</Radio.Content>
|
||||
</Radio>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,22 +2,8 @@ import { VersionCard } from "@/components/VersionCard";
|
||||
import { BUILD_MODE } from "@/lib/mode";
|
||||
import { ExternalLink, GitFork, RotateCcw } from "lucide-react";
|
||||
import { useConfirmDialogStore } from "@/stores/confirmDialogStore";
|
||||
|
||||
function GlassCard({ children }: { children: React.ReactNode }) {
|
||||
return <div className="glass-card px-5 py-4">{children}</div>;
|
||||
}
|
||||
|
||||
function SettingRow({ label, desc, children }: { label: string; desc?: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-foreground">{label}</p>
|
||||
{desc && <p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>}
|
||||
</div>
|
||||
<div className="shrink-0">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { Button, Link } from "@heroui/react";
|
||||
import { SettingCard, SettingRow, PageHeader, SectionTitle } from "@/components/setting";
|
||||
|
||||
const modeLabels: Record<string, string> = {
|
||||
dev: "开发版",
|
||||
@@ -47,79 +33,65 @@ export function AboutSetting() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-foreground mb-1">关于</h2>
|
||||
<p className="text-sm text-muted-foreground mb-6">查看版本信息、更新状态与项目相关链接</p>
|
||||
<PageHeader title="关于" desc="查看版本信息、更新状态与项目相关链接" />
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 版本卡片 */}
|
||||
<VersionCard />
|
||||
|
||||
{/* 项目信息 */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">项目信息</h3>
|
||||
<SectionTitle>项目信息</SectionTitle>
|
||||
<div className="space-y-3">
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<SettingRow label="应用名称" desc="Koring Launcher">
|
||||
<span className="text-[13px] text-muted-foreground">Lingke Network 提供技术支持</span>
|
||||
</SettingRow>
|
||||
</GlassCard>
|
||||
<GlassCard>
|
||||
</SettingCard>
|
||||
<SettingCard>
|
||||
<SettingRow label="构建模式">
|
||||
<span className="text-[13px] text-muted-foreground">{modeLabels[BUILD_MODE] ?? BUILD_MODE}</span>
|
||||
</SettingRow>
|
||||
</GlassCard>
|
||||
<GlassCard>
|
||||
</SettingCard>
|
||||
<SettingCard>
|
||||
<SettingRow label="技术栈" desc="Electron + React 19 + TypeScript + @xmcl">
|
||||
<span className="text-[13px] text-muted-foreground">Node.js</span>
|
||||
</SettingRow>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 链接 */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">相关链接</h3>
|
||||
<SectionTitle>相关链接</SectionTitle>
|
||||
<div className="space-y-3">
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<SettingRow label="GitHub 仓库" desc="查看源代码、提交 Issue">
|
||||
<button
|
||||
onClick={() => openLink(GITHUB_URL)}
|
||||
className="inline-flex items-center gap-1.5 text-[13px] text-primary hover:underline"
|
||||
>
|
||||
<Link onPress={() => openLink(GITHUB_URL)}>
|
||||
<GitFork className="w-4 h-4" />
|
||||
打开
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</button>
|
||||
</Link>
|
||||
</SettingRow>
|
||||
</GlassCard>
|
||||
<GlassCard>
|
||||
</SettingCard>
|
||||
<SettingCard>
|
||||
<SettingRow label="官方网站" desc="了解更多功能与文档">
|
||||
<button
|
||||
onClick={() => openLink(OFFICIAL_URL)}
|
||||
className="inline-flex items-center gap-1.5 text-[13px] text-primary hover:underline"
|
||||
>
|
||||
<Link onPress={() => openLink(OFFICIAL_URL)}>
|
||||
访问
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</button>
|
||||
</Link>
|
||||
</SettingRow>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 危险操作 */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">危险操作</h3>
|
||||
<GlassCard>
|
||||
<SectionTitle>危险操作</SectionTitle>
|
||||
<SettingCard>
|
||||
<SettingRow label="还原所有设置" desc="删除所有配置文件并重启应用">
|
||||
<button
|
||||
onClick={handleResetClick}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md text-[13px] font-medium bg-red-500/10 text-red-600 dark:text-red-400 hover:bg-red-500/20 transition-colors"
|
||||
>
|
||||
<Button variant="danger" size="sm" onPress={handleResetClick}>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
还原
|
||||
</button>
|
||||
</Button>
|
||||
</SettingRow>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,10 +2,8 @@ import { useEffect } from "react";
|
||||
import { useKoringAuthStore } from "@/stores/koringAuthStore";
|
||||
import { useRouteStore } from "@/stores/routeStore";
|
||||
import { UserCircle, LogOut, Loader2, ChevronRight } from "lucide-react";
|
||||
|
||||
function GlassCard({ children }: { children: React.ReactNode }) {
|
||||
return <div className="glass-card px-5 py-4">{children}</div>;
|
||||
}
|
||||
import { Button, Avatar } from "@heroui/react";
|
||||
import { SettingCard, PageHeader, SectionTitle } from "@/components/setting";
|
||||
|
||||
export function AccountSetting() {
|
||||
const { user, loading, initFromDisk, logout } = useKoringAuthStore();
|
||||
@@ -17,81 +15,85 @@ export function AccountSetting() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-foreground mb-1">Koring 账户</h2>
|
||||
<p className="text-sm text-muted-foreground mb-6">管理 Koring 工作室账户,用于同步数据、皮肤与个人配置</p>
|
||||
<PageHeader title="Koring 账户" desc="管理 Koring 工作室账户,用于同步数据、皮肤与个人配置" />
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 账户信息 */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">账户信息</h3>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!user && !loading) navigate("setting/login");
|
||||
}}
|
||||
className={[
|
||||
"glass-card px-5 py-4 w-full text-left transition-all duration-200",
|
||||
!user && !loading ? "cursor-pointer hover:bg-foreground/[0.04]" : "cursor-default",
|
||||
].join(" ")}
|
||||
<SectionTitle>账户信息</SectionTitle>
|
||||
<SettingCard
|
||||
className={
|
||||
!user && !loading
|
||||
? "cursor-pointer hover:bg-foreground/[0.04] transition-all duration-200"
|
||||
: "cursor-default"
|
||||
}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="flex items-center gap-2 py-2">
|
||||
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">加载中...</span>
|
||||
</div>
|
||||
) : user ? (
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-14 h-14 rounded-full bg-foreground/[0.06] flex items-center justify-center shrink-0 overflow-hidden">
|
||||
{user.picture ? (
|
||||
<img src={user.picture} alt="" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<UserCircle className="w-8 h-8 text-foreground/40" />
|
||||
)}
|
||||
<div
|
||||
onClick={() => {
|
||||
if (!user && !loading) navigate("setting/login");
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="flex items-center gap-2 py-2">
|
||||
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">加载中...</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-base font-medium text-foreground truncate">
|
||||
{user.name || user.username}
|
||||
</p>
|
||||
<p className="text-[13px] text-muted-foreground mt-0.5">Koring 账户</p>
|
||||
{user.email && (
|
||||
<p className="text-[11px] text-muted-foreground/60 mt-0.5">{user.email}</p>
|
||||
)}
|
||||
) : user ? (
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar size="lg">
|
||||
{user.picture ? (
|
||||
<Avatar.Image src={user.picture} alt="" />
|
||||
) : (
|
||||
<Avatar.Fallback>
|
||||
<UserCircle className="w-8 h-8 text-foreground/40" />
|
||||
</Avatar.Fallback>
|
||||
)}
|
||||
</Avatar>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-base font-medium text-foreground truncate">
|
||||
{user.name || user.username}
|
||||
</p>
|
||||
<p className="text-[13px] text-muted-foreground mt-0.5">Koring 账户</p>
|
||||
{user.email && (
|
||||
<p className="text-[11px] text-muted-foreground/60 mt-0.5">{user.email}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-14 h-14 rounded-full bg-foreground/[0.06] flex items-center justify-center shrink-0">
|
||||
<UserCircle className="w-8 h-8 text-foreground/30" />
|
||||
) : (
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-14 h-14 rounded-full bg-foreground/[0.06] flex items-center justify-center shrink-0">
|
||||
<UserCircle className="w-8 h-8 text-foreground/30" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-muted-foreground">尚未登录</p>
|
||||
<p className="text-[12px] text-muted-foreground/60 mt-0.5">点击登录 Koring 账户</p>
|
||||
</div>
|
||||
<ChevronRight className="w-4 h-4 text-foreground/20 shrink-0" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-muted-foreground">尚未登录</p>
|
||||
<p className="text-[12px] text-muted-foreground/60 mt-0.5">点击登录 Koring 账户</p>
|
||||
</div>
|
||||
<ChevronRight className="w-4 h-4 text-foreground/20 shrink-0" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</SettingCard>
|
||||
</div>
|
||||
|
||||
{/* 退出登录 */}
|
||||
{user && (
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">账户操作</h3>
|
||||
<GlassCard>
|
||||
<SectionTitle>账户操作</SectionTitle>
|
||||
<SettingCard>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">退出登录</p>
|
||||
<p className="text-[13px] text-muted-foreground mt-0.5">退出当前账户,数据将保留在本地</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={logout}
|
||||
disabled={loading}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md text-[13px] font-medium bg-red-500/10 text-red-600 dark:text-red-400 hover:bg-red-500/20 transition-colors disabled:opacity-40"
|
||||
<Button
|
||||
variant="danger-soft"
|
||||
size="sm"
|
||||
isDisabled={loading}
|
||||
onPress={logout}
|
||||
>
|
||||
<LogOut className="w-3.5 h-3.5" />
|
||||
退出
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,6 @@
|
||||
import { ExternalLink } from "lucide-react";
|
||||
|
||||
function GlassCard({ children }: { children: React.ReactNode }) {
|
||||
return <div className="glass-card px-5 py-4">{children}</div>;
|
||||
}
|
||||
|
||||
function SettingRow({ label, desc, children }: { label: string; desc?: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-foreground">{label}</p>
|
||||
{desc && <p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>}
|
||||
</div>
|
||||
<div className="shrink-0">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { Link } from "@heroui/react";
|
||||
import { SettingCard, SettingRow, PageHeader, SectionTitle } from "@/components/setting";
|
||||
|
||||
const openLink = (url: string) => {
|
||||
window.electronAPI?.openExternal(url);
|
||||
@@ -28,7 +14,7 @@ const licenses = [
|
||||
{ name: "@xmcl/user", license: "MIT", url: "https://github.com/VoxelCogs/xmcl" },
|
||||
{ name: "Zustand", license: "MIT", url: "https://github.com/pmndrs/zustand" },
|
||||
{ name: "Tailwind CSS", license: "MIT", url: "https://github.com/tailwindlabs/tailwindcss" },
|
||||
{ name: "shadcn/ui", license: "MIT", url: "https://github.com/shadcn-ui/ui" },
|
||||
{ name: "HeroUI", license: "MIT", url: "https://github.com/heroui-inc/heroui" },
|
||||
{ name: "Vite", license: "MIT", url: "https://github.com/vitejs/vite" },
|
||||
{ name: "Lucide React", license: "ISC", url: "https://github.com/lucide-icons/lucide" },
|
||||
];
|
||||
@@ -42,55 +28,45 @@ const fonts = [
|
||||
export function CopyrightSetting() {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-foreground mb-1">版权</h2>
|
||||
<p className="text-sm text-muted-foreground mb-6">开源协议、依赖项目授权与字体版权信息</p>
|
||||
<PageHeader title="版权" desc="开源协议、依赖项目授权与字体版权信息" />
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 项目协议 */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">项目协议</h3>
|
||||
<GlassCard>
|
||||
<SectionTitle>项目协议</SectionTitle>
|
||||
<SettingCard>
|
||||
<SettingRow label="LL-1.0 (LingkeLice 1.0)" desc="Copyright © Shenzhen Lingke Network Technology Co., Ltd.">
|
||||
<button
|
||||
onClick={() => openLink("https://support.lingke.ink/LL-1.0")}
|
||||
className="inline-flex items-center gap-1.5 text-[13px] text-primary hover:underline"
|
||||
>
|
||||
<Link onPress={() => openLink("https://support.lingke.ink/LL-1.0")}>
|
||||
查看
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</button>
|
||||
</Link>
|
||||
</SettingRow>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
</div>
|
||||
|
||||
{/* 开源依赖 */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">开源依赖</h3>
|
||||
<SectionTitle>开源依赖</SectionTitle>
|
||||
<div className="space-y-2">
|
||||
{licenses.map((dep) => (
|
||||
<GlassCard key={dep.name}>
|
||||
<SettingCard key={dep.name}>
|
||||
<SettingRow label={dep.name} desc={dep.license}>
|
||||
<button
|
||||
onClick={() => openLink(dep.url)}
|
||||
className="inline-flex items-center gap-1 text-[13px] text-primary hover:underline"
|
||||
>
|
||||
<Link onPress={() => openLink(dep.url)}>
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</button>
|
||||
</Link>
|
||||
</SettingRow>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 字体 */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">字体授权</h3>
|
||||
<SectionTitle>字体授权</SectionTitle>
|
||||
<div className="space-y-2">
|
||||
{fonts.map((font) => (
|
||||
<GlassCard key={font.name}>
|
||||
<SettingCard key={font.name}>
|
||||
<SettingRow label={font.name} desc={font.license}>
|
||||
<span className="text-[12px] text-muted-foreground">{font.note}</span>
|
||||
</SettingRow>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useKoringAuthStore } from "@/stores/koringAuthStore";
|
||||
import { useRouteStore } from "@/stores/routeStore";
|
||||
import { BUILD_MODE } from "@/lib/mode";
|
||||
import { useUpdateStore } from "@/stores/updateStore";
|
||||
import {
|
||||
@@ -11,11 +10,10 @@ import {
|
||||
Cpu,
|
||||
Info,
|
||||
ChevronRight,
|
||||
Search,
|
||||
} from "lucide-react";
|
||||
|
||||
function GlassCard({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||
return <div className={`glass-card px-5 py-4 ${className ?? ""}`}>{children}</div>;
|
||||
}
|
||||
import { Avatar } from "@heroui/react";
|
||||
import { SettingCard, PageHeader, SectionTitle } from "@/components/setting";
|
||||
|
||||
interface ShortcutItem {
|
||||
icon: React.ReactNode;
|
||||
@@ -68,31 +66,27 @@ export function HomeSetting({ onNavigate }: HomeSettingProps) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-foreground mb-1">主页</h2>
|
||||
<p className="text-sm text-muted-foreground mb-6">自定义启动器主页的显示内容与常用设置的快捷入口</p>
|
||||
<PageHeader title="主页" desc="自定义启动器主页的显示内容与常用设置的快捷入口" />
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* ===== 搜索栏占位 ===== */}
|
||||
<div className="glass-card flex items-center gap-3 px-4 py-3">
|
||||
<svg className="w-4 h-4 text-muted-foreground shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21-4.3-4.3" />
|
||||
</svg>
|
||||
<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>
|
||||
</div>
|
||||
</SettingCard>
|
||||
|
||||
{/* ===== 快捷状态 ===== */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">快速概览</h3>
|
||||
<GlassCard>
|
||||
<SectionTitle>快速概览</SectionTitle>
|
||||
<SettingCard>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-full bg-foreground/[0.06] flex items-center justify-center shrink-0 overflow-hidden">
|
||||
<Avatar size="lg">
|
||||
{user?.picture ? (
|
||||
<img src={user.picture} alt="" className="w-full h-full object-cover" />
|
||||
<Avatar.Image src={user.picture} alt="" />
|
||||
) : (
|
||||
<UserCircle className="w-7 h-7 text-foreground/40" />
|
||||
<Avatar.Fallback>
|
||||
<UserCircle className="w-7 h-7 text-foreground/40" />
|
||||
</Avatar.Fallback>
|
||||
)}
|
||||
</div>
|
||||
</Avatar>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-foreground truncate">
|
||||
{user ? (user.name || user.username) : "未登录"}
|
||||
@@ -106,12 +100,11 @@ export function HomeSetting({ onNavigate }: HomeSettingProps) {
|
||||
{update ? " · 有更新" : ""}
|
||||
</span>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
</div>
|
||||
|
||||
{/* ===== 快捷入口 ===== */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">常用设置</h3>
|
||||
<SectionTitle>常用设置</SectionTitle>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{shortcuts.map((s) => (
|
||||
<ShortcutTile key={s.navKey} {...s} onClick={onNavigate} />
|
||||
|
||||
@@ -133,12 +133,13 @@ export function Setting() {
|
||||
|
||||
return (
|
||||
<div className="flex h-full">
|
||||
{/* 侧边栏 — 无动画 */}
|
||||
<aside className="scroll-area w-[240px] shrink-0 h-full overflow-y-auto py-5 pl-5 pr-2">
|
||||
<nav className="space-y-5">
|
||||
{menuData.map((group) => (
|
||||
{/* 侧边栏 */}
|
||||
<aside className="scroll-area w-[220px] shrink-0 h-full overflow-y-auto py-5 pl-4 pr-2">
|
||||
<nav className="space-y-6">
|
||||
{menuData.map((group, gi) => (
|
||||
<div key={group.title}>
|
||||
<h3 className="text-[11px] font-semibold uppercase tracking-wider text-foreground/30 mb-2 px-2">
|
||||
{gi > 0 && <div className="mx-3 mb-3 border-t border-border/40" />}
|
||||
<h3 className="text-[11px] font-semibold uppercase tracking-wider text-foreground/40 mb-2 px-3">
|
||||
{group.title}
|
||||
</h3>
|
||||
<div className="space-y-0.5">
|
||||
@@ -149,12 +150,15 @@ export function Setting() {
|
||||
key={item.key}
|
||||
onClick={() => handleItemClick(item)}
|
||||
className={[
|
||||
"w-full flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-[14px] transition-all duration-150",
|
||||
"relative w-full flex items-center gap-2.5 px-3 py-2 rounded-md text-[13.5px] transition-all duration-150",
|
||||
active
|
||||
? "bg-foreground/[0.08] text-foreground font-medium shadow-sm"
|
||||
? "text-foreground font-medium"
|
||||
: "text-foreground/50 hover:text-foreground/80 hover:bg-foreground/[0.04]",
|
||||
].join(" ")}
|
||||
>
|
||||
{active && (
|
||||
<span className="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-[16px] rounded-full bg-primary" />
|
||||
)}
|
||||
<span className={active ? "text-foreground/80" : "text-foreground/35"}>
|
||||
{item.icon}
|
||||
</span>
|
||||
@@ -168,7 +172,7 @@ export function Setting() {
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
{/* 内容区 — 仅内容有 fade 动画 */}
|
||||
{/* 内容区 */}
|
||||
<main className="scroll-area flex-1 h-full overflow-y-auto p-8">
|
||||
<div key={animKey} className="setting-page-enter">
|
||||
{current?.component}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useCallback } from "react";
|
||||
import { useRouteStore } from "@/stores/routeStore";
|
||||
import { KoringLogin } from "@/components/KoringLogin";
|
||||
|
||||
export function SettingLogin() {
|
||||
const goBack = useRouteStore((s) => s.goBack);
|
||||
|
||||
const handleSuccess = useCallback(() => {
|
||||
setTimeout(() => goBack(), 1500);
|
||||
}, [goBack]);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col items-center justify-center p-8">
|
||||
<div className="w-full max-w-sm flex flex-col items-center gap-6">
|
||||
<div className="text-center space-y-1">
|
||||
<h2 className="text-lg font-bold text-foreground">扫码登录</h2>
|
||||
<p className="text-xs text-muted-foreground">使用任意二维码扫描器扫描下方二维码</p>
|
||||
</div>
|
||||
<KoringLogin onLoginSuccess={handleSuccess} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
import { useConfigStore } from "@/stores/configStore";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
|
||||
function GlassCard({ children }: { children: React.ReactNode }) {
|
||||
return <div className="glass-card px-5 py-4">{children}</div>;
|
||||
}
|
||||
import { Slider, RadioGroup, Radio, Input } from "@heroui/react";
|
||||
import { SettingCard, PageHeader, SectionTitle } from "@/components/setting";
|
||||
|
||||
const downloadSources = [
|
||||
{ value: "mirror", label: "尽量使用镜像源(推荐国内用户)" },
|
||||
@@ -17,51 +14,54 @@ export function DownloadSetting() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-foreground mb-1">下载</h2>
|
||||
<p className="text-sm text-muted-foreground mb-6">配置游戏资源下载源、并发数与存储路径</p>
|
||||
<PageHeader title="下载" desc="配置游戏资源下载源、并发数与存储路径" />
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* ===== 下载源 ===== */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">下载源</h3>
|
||||
<SectionTitle>下载源</SectionTitle>
|
||||
<div className="space-y-3">
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-foreground">文件下载源</p>
|
||||
<p className="text-[13px] text-muted-foreground">游戏文件(jar、lib)的下载来源</p>
|
||||
<div className="space-y-2 mt-2">
|
||||
<RadioGroup
|
||||
value={dl.fileSource}
|
||||
onValueChange={(v) => setDownload({ fileSource: v })}
|
||||
className="mt-2 space-y-2"
|
||||
>
|
||||
{downloadSources.map((opt) => (
|
||||
<label key={opt.value} className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="fileSource" checked={dl.fileSource === opt.value} onChange={() => setDownload({ fileSource: opt.value })} className="accent-primary" />
|
||||
<span className="text-sm text-foreground">{opt.label}</span>
|
||||
</label>
|
||||
<Radio key={opt.value} value={opt.value}>
|
||||
<Radio.Content>{opt.label}</Radio.Content>
|
||||
</Radio>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-foreground">版本列表源</p>
|
||||
<p className="text-[13px] text-muted-foreground">获取可用游戏版本列表的来源</p>
|
||||
<div className="space-y-2 mt-2">
|
||||
<RadioGroup
|
||||
value={dl.versionSource}
|
||||
onValueChange={(v) => setDownload({ versionSource: v })}
|
||||
className="mt-2 space-y-2"
|
||||
>
|
||||
{downloadSources.map((opt) => (
|
||||
<label key={opt.value} className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="versionSource" checked={dl.versionSource === opt.value} onChange={() => setDownload({ versionSource: opt.value })} className="accent-primary" />
|
||||
<span className="text-sm text-foreground">{opt.label}</span>
|
||||
</label>
|
||||
<Radio key={opt.value} value={opt.value}>
|
||||
<Radio.Content>{opt.label}</Radio.Content>
|
||||
</Radio>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ===== 并发控制 ===== */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">并发控制</h3>
|
||||
<SectionTitle>并发控制</SectionTitle>
|
||||
<div className="space-y-3">
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -71,41 +71,44 @@ export function DownloadSetting() {
|
||||
<span className="text-[13px] text-muted-foreground tabular-nums shrink-0 ml-4">{dl.threads}</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[dl.threads]}
|
||||
onValueChange={(v) => setDownload({ threads: Array.isArray(v) ? v[0] : v })}
|
||||
min={1}
|
||||
max={64}
|
||||
value={dl.threads}
|
||||
onChange={(v) => setDownload({ threads: typeof v === "number" ? v : v[0] })}
|
||||
minValue={1}
|
||||
maxValue={64}
|
||||
step={1}
|
||||
/>
|
||||
>
|
||||
<Slider.Track>
|
||||
<Slider.Fill />
|
||||
<Slider.Thumb />
|
||||
</Slider.Track>
|
||||
</Slider>
|
||||
<div className="flex justify-between mt-1">
|
||||
<span className="text-[11px] text-muted-foreground/50">1</span>
|
||||
<span className="text-[11px] text-muted-foreground/50">64</span>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ===== 速度限制 ===== */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">速度限制</h3>
|
||||
<SectionTitle>速度限制</SectionTitle>
|
||||
<div className="space-y-3">
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-foreground">下载速度上限</p>
|
||||
<p className="text-[13px] text-muted-foreground">单位 KB/s,0 表示不限速</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
<Input
|
||||
type="number"
|
||||
value={dl.speedLimit}
|
||||
value={String(dl.speedLimit)}
|
||||
onChange={(e) => setDownload({ speedLimit: Number(e.target.value) })}
|
||||
min={0}
|
||||
className="w-28 h-8 px-3 rounded-md border border-input bg-background text-sm font-mono placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
className="w-28"
|
||||
/>
|
||||
<span className="text-[13px] text-muted-foreground">KB/s</span>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { EmptyState } from "@heroui/react";
|
||||
import { Globe } from "lucide-react";
|
||||
import { PageHeader } from "@/components/setting";
|
||||
|
||||
export function EtherOnlineSetting() {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-foreground mb-1">以太联机</h2>
|
||||
<p className="text-sm text-muted-foreground mb-6">配置以太联机服务,实现多人局域网或远程联机</p>
|
||||
<PageHeader title="以太联机" desc="配置以太联机服务,实现多人局域网或远程联机" />
|
||||
<EmptyState className="py-16">
|
||||
<Globe className="w-10 h-10 text-muted-foreground/30" />
|
||||
<p className="text-sm text-muted-foreground mt-3">该功能正在开发中</p>
|
||||
</EmptyState>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,8 @@
|
||||
import { useCallback } from "react";
|
||||
import { useConfigStore } from "@/stores/configStore";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Switch, Input } from "@heroui/react";
|
||||
import { ShieldCheck } from "lucide-react";
|
||||
|
||||
function GlassCard({ children }: { children: React.ReactNode }) {
|
||||
return <div className="glass-card px-5 py-4">{children}</div>;
|
||||
}
|
||||
|
||||
function SettingRow({ label, desc, children }: { label: string; desc?: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-foreground">{label}</p>
|
||||
{desc && <p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>}
|
||||
</div>
|
||||
<div className="shrink-0">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { SettingCard, SettingRow, PageHeader, SectionTitle } from "@/components/setting";
|
||||
|
||||
export function SecurityIdSetting() {
|
||||
const enabled = useConfigStore((s) => s.config.network.securityId.enabled);
|
||||
@@ -34,39 +19,41 @@ export function SecurityIdSetting() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-foreground mb-1">安全识别服务</h2>
|
||||
<p className="text-sm text-muted-foreground mb-6">管理账户安全验证、设备识别与登录保护</p>
|
||||
<PageHeader title="安全识别服务" desc="管理账户安全验证、设备识别与登录保护" />
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">认证服务</h3>
|
||||
<SectionTitle>认证服务</SectionTitle>
|
||||
<div className="space-y-3">
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<SettingRow
|
||||
label="启用第三方认证"
|
||||
desc="使用自定义认证服务器替代 Microsoft 认证(适用于离线服务器)"
|
||||
>
|
||||
<Switch checked={enabled} onCheckedChange={handleToggle} />
|
||||
<Switch isSelected={enabled} onValueChange={handleToggle}>
|
||||
<Switch.Control>
|
||||
<Switch.Thumb />
|
||||
</Switch.Control>
|
||||
</Switch>
|
||||
</SettingRow>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
|
||||
{enabled && (
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-foreground">认证服务器 URL</p>
|
||||
<p className="text-[13px] text-muted-foreground">输入第三方认证服务的地址</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldCheck className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
<input
|
||||
type="text"
|
||||
<Input
|
||||
value={authUrl}
|
||||
onChange={handleUrlChange}
|
||||
placeholder="https://auth.example.com"
|
||||
className="flex-1 h-8 px-3 rounded-md border border-input bg-background text-sm font-mono placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { EmptyState } from "@heroui/react";
|
||||
import { Network } from "lucide-react";
|
||||
import { PageHeader } from "@/components/setting";
|
||||
|
||||
export function TawaOnlineSetting() {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-foreground mb-1">陶瓦联机</h2>
|
||||
<p className="text-sm text-muted-foreground mb-6">配置陶瓦联机服务,通过中继服务器进行多人游戏</p>
|
||||
<PageHeader title="陶瓦联机" desc="配置陶瓦联机服务,通过中继服务器进行多人游戏" />
|
||||
<EmptyState className="py-16">
|
||||
<Network className="w-10 h-10 text-muted-foreground/30" />
|
||||
<p className="text-sm text-muted-foreground mt-3">该功能正在开发中</p>
|
||||
</EmptyState>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { EmptyState } from "@heroui/react";
|
||||
import { MessageSquareHeart } from "lucide-react";
|
||||
import { PageHeader } from "@/components/setting";
|
||||
|
||||
export function FeedbackSetting() {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-foreground mb-1">服务与反馈</h2>
|
||||
<p className="text-sm text-muted-foreground mb-6">提交问题反馈、功能建议与联系开发团队</p>
|
||||
<PageHeader title="服务与反馈" desc="提交问题反馈、功能建议与联系开发团队" />
|
||||
<EmptyState className="py-16">
|
||||
<MessageSquareHeart className="w-10 h-10 text-muted-foreground/30" />
|
||||
<p className="text-sm text-muted-foreground mt-3">该功能正在开发中</p>
|
||||
</EmptyState>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { EmptyState } from "@heroui/react";
|
||||
import { Heart } from "lucide-react";
|
||||
import { PageHeader } from "@/components/setting";
|
||||
|
||||
export function SponsorSetting() {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-foreground mb-1">赞助我们</h2>
|
||||
<p className="text-sm text-muted-foreground mb-6">如果 Koring Launcher 对你有帮助,欢迎赞助支持开发</p>
|
||||
<PageHeader title="赞助我们" desc="如果 Koring Launcher 对你有帮助,欢迎赞助支持开发" />
|
||||
<EmptyState className="py-16">
|
||||
<Heart className="w-10 h-10 text-muted-foreground/30" />
|
||||
<p className="text-sm text-muted-foreground mt-3">该功能正在开发中</p>
|
||||
</EmptyState>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,51 +1,47 @@
|
||||
import { useA11yStore } from "@/stores/a11yStore";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
function GlassCard({ children }: { children: React.ReactNode }) {
|
||||
return <div className="glass-card px-5 py-4">{children}</div>;
|
||||
}
|
||||
|
||||
function SettingRow({ label, desc, children }: { label: string; desc: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-foreground">{label}</p>
|
||||
<p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>
|
||||
</div>
|
||||
<div className="shrink-0">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { Switch } from "@heroui/react";
|
||||
import { SettingCard, SettingRow, PageHeader, SectionTitle } from "@/components/setting";
|
||||
|
||||
export function A11ySetting() {
|
||||
const { reduceMotion, setReduceMotion, reduceTransparency, setReduceTransparency, highContrast, setHighContrast } = useA11yStore();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-foreground mb-1">辅助功能</h2>
|
||||
<p className="text-sm text-muted-foreground mb-6">调整动画、透明度与对比度以改善使用体验</p>
|
||||
<PageHeader title="辅助功能" desc="调整动画、透明度与对比度以改善使用体验" />
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">显示</h3>
|
||||
<SectionTitle>显示</SectionTitle>
|
||||
<div className="space-y-3">
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<SettingRow label="减少动画" desc="关闭页面切换动画和背景动效">
|
||||
<Switch checked={reduceMotion} onCheckedChange={setReduceMotion} />
|
||||
<Switch isSelected={reduceMotion} onValueChange={setReduceMotion}>
|
||||
<Switch.Control>
|
||||
<Switch.Thumb />
|
||||
</Switch.Control>
|
||||
</Switch>
|
||||
</SettingRow>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<SettingRow label="减少透明度" desc="将磨砂玻璃效果替换为纯色背景,提升可读性">
|
||||
<Switch checked={reduceTransparency} onCheckedChange={setReduceTransparency} />
|
||||
<Switch isSelected={reduceTransparency} onValueChange={setReduceTransparency}>
|
||||
<Switch.Control>
|
||||
<Switch.Thumb />
|
||||
</Switch.Control>
|
||||
</Switch>
|
||||
</SettingRow>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<SettingRow label="高对比度" desc="增强文字与背景的对比度,改善可读性">
|
||||
<Switch checked={highContrast} onCheckedChange={setHighContrast} />
|
||||
<Switch isSelected={highContrast} onValueChange={setHighContrast}>
|
||||
<Switch.Control>
|
||||
<Switch.Thumb />
|
||||
</Switch.Control>
|
||||
</Switch>
|
||||
</SettingRow>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { EmptyState } from "@heroui/react";
|
||||
import { Languages } from "lucide-react";
|
||||
import { PageHeader } from "@/components/setting";
|
||||
|
||||
export function LangSetting() {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-foreground mb-1">语言</h2>
|
||||
<p className="text-sm text-muted-foreground mb-6">选择启动器界面的显示语言与地区偏好</p>
|
||||
<PageHeader title="语言" desc="选择启动器界面的显示语言与地区偏好" />
|
||||
<EmptyState className="py-16">
|
||||
<Languages className="w-10 h-10 text-muted-foreground/30" />
|
||||
<p className="text-sm text-muted-foreground mt-3">该功能正在开发中</p>
|
||||
</EmptyState>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,9 @@
|
||||
import { useThemeStore, type DarkMode } from "@/stores/themeStore";
|
||||
import { useBackgroundStore } from "@/stores/backgroundStore";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Switch, Button, Slider } from "@heroui/react";
|
||||
import { DEFAULT_BG } from "@/lib/mode";
|
||||
import clsx from "clsx";
|
||||
|
||||
function GlassCard({ children }: { children: React.ReactNode }) {
|
||||
return <div className="glass-card px-5 py-4">{children}</div>;
|
||||
}
|
||||
|
||||
function SettingRow({ label, desc, children }: { label: string; desc?: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-foreground">{label}</p>
|
||||
{desc && <p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>}
|
||||
</div>
|
||||
<div className="shrink-0">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ======== 深色模式预览卡片 ======== */
|
||||
import { SettingCard, SettingRow, PageHeader, SectionTitle } from "@/components/setting";
|
||||
|
||||
function ThemePreviewCard({ mode, selected, onClick }: { mode: DarkMode; selected: boolean; onClick: () => void }) {
|
||||
const isDark = mode === "dark";
|
||||
@@ -43,7 +24,6 @@ function ThemePreviewCard({ mode, selected, onClick }: { mode: DarkMode; selecte
|
||||
>
|
||||
<div className="relative w-[140px] h-[96px] rounded overflow-hidden">
|
||||
{isAuto ? (
|
||||
/* 跟随系统 — 左右分屏 */
|
||||
<div className="flex w-full h-full">
|
||||
<div className="w-1/2 h-full bg-white flex flex-col">
|
||||
<div className="h-[6px] bg-gray-200 flex items-center px-1 gap-[2px]">
|
||||
@@ -69,7 +49,6 @@ function ThemePreviewCard({ mode, selected, onClick }: { mode: DarkMode; selecte
|
||||
</div>
|
||||
</div>
|
||||
) : isDark ? (
|
||||
/* 深色模式 */
|
||||
<div className="w-full h-full bg-[#1c1c1e] flex flex-col">
|
||||
<div className="h-[6px] bg-[#2c2c2e] flex items-center px-1 gap-[2px]">
|
||||
<div className="w-[3px] h-[3px] rounded-full bg-red-400" />
|
||||
@@ -83,7 +62,6 @@ function ThemePreviewCard({ mode, selected, onClick }: { mode: DarkMode; selecte
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* 浅色模式 */
|
||||
<div className="w-full h-full bg-white flex flex-col">
|
||||
<div className="h-[6px] bg-gray-200 flex items-center px-1 gap-[2px]">
|
||||
<div className="w-[3px] h-[3px] rounded-full bg-red-400" />
|
||||
@@ -103,8 +81,6 @@ function ThemePreviewCard({ mode, selected, onClick }: { mode: DarkMode; selecte
|
||||
);
|
||||
}
|
||||
|
||||
/* ======== 页面 ======== */
|
||||
|
||||
export function ThemeBgSetting() {
|
||||
const { darkMode, setDarkMode, parallax, setParallax } = useThemeStore();
|
||||
const { image, opacity, setOpacity, blur, setBlur, setImage, reset } = useBackgroundStore();
|
||||
@@ -122,29 +98,26 @@ export function ThemeBgSetting() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-foreground mb-1">主题与背景</h2>
|
||||
<p className="text-sm text-muted-foreground mb-6">切换深色模式、更换背景图片与调整视觉效果</p>
|
||||
<PageHeader title="主题与背景" desc="切换深色模式、更换背景图片与调整视觉效果" />
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* ===== 深色模式 ===== */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">深色模式</h3>
|
||||
<GlassCard>
|
||||
<SectionTitle>深色模式</SectionTitle>
|
||||
<SettingCard>
|
||||
<div className="flex items-center justify-center gap-4 py-2">
|
||||
<ThemePreviewCard mode="light" selected={darkMode === "light"} onClick={() => setDarkMode("light")} />
|
||||
<ThemePreviewCard mode="auto" selected={darkMode === "auto"} onClick={() => setDarkMode("auto")} />
|
||||
<ThemePreviewCard mode="dark" selected={darkMode === "dark"} onClick={() => setDarkMode("dark")} />
|
||||
</div>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
</div>
|
||||
|
||||
{/* ===== 背景 ===== */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-3">背景</h3>
|
||||
<SectionTitle>背景</SectionTitle>
|
||||
<div className="space-y-3">
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<SettingRow label="背景图片" desc="更换启动器背景图片">
|
||||
<Button variant="outline" size="sm" onClick={handlePickImage}>
|
||||
<Button variant="outline" size="sm" onPress={handlePickImage}>
|
||||
选择图片
|
||||
</Button>
|
||||
</SettingRow>
|
||||
@@ -157,50 +130,61 @@ export function ThemeBgSetting() {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<SettingRow label="背景模糊" desc={`对背景图施加高斯模糊,当前 ${blur}px`}>
|
||||
<Slider
|
||||
className="w-[180px]"
|
||||
value={[blur]}
|
||||
min={0}
|
||||
max={20}
|
||||
value={blur}
|
||||
minValue={0}
|
||||
maxValue={20}
|
||||
step={1}
|
||||
onValueChange={(v) => setBlur(Array.isArray(v) ? v[0] : v)}
|
||||
/>
|
||||
onChange={(v) => setBlur(typeof v === "number" ? v : v[0])}
|
||||
>
|
||||
<Slider.Track>
|
||||
<Slider.Fill />
|
||||
<Slider.Thumb />
|
||||
</Slider.Track>
|
||||
</Slider>
|
||||
</SettingRow>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<SettingRow label="背景不透明度" desc={`控制背景图的可见程度,当前 ${Math.round(opacity * 100)}%`}>
|
||||
<Slider
|
||||
className="w-[180px]"
|
||||
value={[opacity * 100]}
|
||||
min={0}
|
||||
max={100}
|
||||
value={opacity * 100}
|
||||
minValue={0}
|
||||
maxValue={100}
|
||||
step={1}
|
||||
onValueChange={(v) => setOpacity((Array.isArray(v) ? v[0] : v) / 100)}
|
||||
/>
|
||||
onChange={(v) => setOpacity((typeof v === "number" ? v : v[0]) / 100)}
|
||||
>
|
||||
<Slider.Track>
|
||||
<Slider.Fill />
|
||||
<Slider.Thumb />
|
||||
</Slider.Track>
|
||||
</Slider>
|
||||
</SettingRow>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
|
||||
<GlassCard>
|
||||
<SettingRow
|
||||
label="背景图片视差"
|
||||
desc="背景图片随窗口滚动产生视差位移"
|
||||
>
|
||||
<Switch checked={parallax} onCheckedChange={setParallax} />
|
||||
<SettingCard>
|
||||
<SettingRow label="背景图片视差" desc="背景图片随窗口滚动产生视差位移">
|
||||
<Switch isSelected={parallax} onValueChange={setParallax}>
|
||||
<Switch.Control>
|
||||
<Switch.Thumb />
|
||||
</Switch.Control>
|
||||
</Switch>
|
||||
</SettingRow>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
|
||||
<GlassCard>
|
||||
<SettingCard>
|
||||
<SettingRow label="恢复默认" desc="重置所有背景设置为初始状态">
|
||||
<Button variant="destructive" size="sm" onClick={handleReset}>
|
||||
<Button variant="danger" size="sm" onPress={handleReset}>
|
||||
恢复默认
|
||||
</Button>
|
||||
</SettingRow>
|
||||
</GlassCard>
|
||||
</SettingCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { EmptyState } from "@heroui/react";
|
||||
import { Monitor } from "lucide-react";
|
||||
import { PageHeader } from "@/components/setting";
|
||||
|
||||
export function UiSetting() {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-foreground mb-1">主界面</h2>
|
||||
<p className="text-sm text-muted-foreground mb-6">自定义启动器主界面的布局、模块显示与交互方式</p>
|
||||
<PageHeader title="主界面" desc="自定义启动器主界面的布局、模块显示与交互方式" />
|
||||
<EmptyState className="py-16">
|
||||
<Monitor className="w-10 h-10 text-muted-foreground/30" />
|
||||
<p className="text-sm text-muted-foreground mt-3">该功能正在开发中</p>
|
||||
</EmptyState>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { create } from "zustand";
|
||||
import {
|
||||
getKoringUser,
|
||||
logoutKoring,
|
||||
type KoringUser,
|
||||
type KoringAuthData,
|
||||
} from "../api/koring-auth";
|
||||
|
||||
interface KoringAuthState {
|
||||
user: KoringUser | null;
|
||||
authData: KoringAuthData | null;
|
||||
loading: boolean;
|
||||
|
||||
initFromDisk: () => Promise<void>;
|
||||
setUser: (user: KoringUser) => void;
|
||||
logout: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useKoringAuthStore = create<KoringAuthState>((set) => ({
|
||||
user: null,
|
||||
authData: null,
|
||||
loading: false,
|
||||
|
||||
initFromDisk: async () => {
|
||||
// 如果已经有用户数据,跳过重复读取
|
||||
if (get().user) return;
|
||||
set({ loading: true });
|
||||
try {
|
||||
const data = await getKoringUser();
|
||||
if (data?.user?.sub) {
|
||||
set({ user: data.user, authData: data, loading: false });
|
||||
} else {
|
||||
set({ loading: false });
|
||||
}
|
||||
} catch {
|
||||
set({ loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
setUser: (user) => set({ user }),
|
||||
|
||||
logout: async () => {
|
||||
set({ user: null, authData: null });
|
||||
await logoutKoring().catch(() => {});
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user