feat: 个性化设置页面重构与辅助功能

- 重构主题与背景页面:一选项一卡片布局,深色模式可视化预览卡片
- 新增强内容模式:设置页面自动显示 80px 模糊背景遮罩,自适应亮/暗色
- 新增辅助功能:减少动画、减少透明度、高对比度三项开关
- 新增开发者选项:即时打开/关闭启动画面、强制关闭强内容模式
- 磨砂卡片样式:降低透明度提升可读性,圆角 16px
- 启动画面显示时间调整为 4 秒
- CSS 新增 .glass-card、.content-blur-overlay、辅助功能类
- 新增 a11yStore、devStore、themeStore 状态管理
This commit is contained in:
2026-06-20 03:49:58 +08:00
parent dfcc1a6900
commit 2fa9d7041b
37 changed files with 1160 additions and 29 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ pub fn run() {
// After splash animation, close splash and show main // After splash animation, close splash and show main
let handle = app.handle().clone(); let handle = app.handle().clone();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
tokio::time::sleep(std::time::Duration::from_secs(8)).await; tokio::time::sleep(std::time::Duration::from_secs(4)).await;
// Close splash screen // Close splash screen
if let Some(splash) = handle.get_webview_window("splashscreen") { if let Some(splash) = handle.get_webview_window("splashscreen") {
+6 -4
View File
@@ -13,7 +13,7 @@
"windows": [ "windows": [
{ {
"label": "splashscreen", "label": "splashscreen",
"title": "正在启动", "title": "Koring Launcher",
"url": "/splash.html", "url": "/splash.html",
"width": 480, "width": 480,
"height": 320, "height": 320,
@@ -30,10 +30,13 @@
{ {
"label": "main", "label": "main",
"title": "Koring Launcher", "title": "Koring Launcher",
"width": 800, "width": 900,
"height": 600, "height": 600,
"minWidth": 800,
"minHeight": 600,
"decorations": false, "decorations": false,
"transparent": true, "transparent": true,
"center": true,
"visible": false "visible": false
} }
], ],
@@ -45,8 +48,7 @@
"active": true, "active": true,
"targets": "all", "targets": "all",
"icon": [ "icon": [
"icons/icon.png", "icons/icon.png"
"icons/icon.ico"
], ],
"externalBin": [ "externalBin": [
"binaries/koring-sidecar" "binaries/koring-sidecar"
@@ -1,10 +1,15 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { useBackgroundStore } from "@/stores/backgroundStore"; import { useBackgroundStore } from "@/stores/backgroundStore";
import { useRouteStore } from "@/stores/routeStore";
import { useDevStore } from "@/stores/devStore";
const DEFAULT_BG = "/background.png"; const DEFAULT_BG = "/background.png";
export function BackgroundLayer() { export function BackgroundLayer() {
const { type, image, color, blur, opacity, animationSpeed, fetchConfig } = useBackgroundStore(); const { type, image, color, blur, opacity, animationSpeed, fetchConfig } = useBackgroundStore();
const route = useRouteStore((s) => s.current);
const forceDisableContentBlur = useDevStore((s) => s.forceDisableContentBlur);
const inSetting = route === "setting" || route === "debug";
useEffect(() => { useEffect(() => {
fetchConfig(); fetchConfig();
@@ -77,6 +82,7 @@ export function BackgroundLayer() {
} }
`}</style> `}</style>
<div style={getBackgroundStyle()} /> <div style={getBackgroundStyle()} />
{inSetting && !forceDisableContentBlur && <div className="content-blur-overlay" />}
<div className="dark:block hidden fixed inset-0 z-0 pointer-events-none bg-black/35" /> <div className="dark:block hidden fixed inset-0 z-0 pointer-events-none bg-black/35" />
</> </>
); );
+1 -1
View File
@@ -7,7 +7,7 @@ export default function Splash() {
useEffect(() => { useEffect(() => {
const t1 = setTimeout(() => setPhase("visible"), 50); const t1 = setTimeout(() => setPhase("visible"), 50);
const t2 = setTimeout(() => setPhase("exit"), 7500); const t2 = setTimeout(() => setPhase("exit"), 3500);
return () => { clearTimeout(t1); clearTimeout(t2); }; return () => { clearTimeout(t1); clearTimeout(t2); };
}, []); }, []);
+52
View File
@@ -0,0 +1,52 @@
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
secondary:
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
destructive:
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
outline:
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
ghost:
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
link: "text-primary underline-offset-4 hover:underline",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant = "default",
render,
...props
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
return useRender({
defaultTagName: "span",
props: mergeProps<"span">(
{
className: cn(badgeVariants({ variant }), className),
},
props
),
render,
state: {
slot: "badge",
variant,
},
})
}
export { Badge, badgeVariants }
+103
View File
@@ -0,0 +1,103 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({
className,
size = "default",
...props
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
return (
<div
data-slot="card"
data-size={size}
className={cn(
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn(
"font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
className
)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-(--card-spacing)", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn(
"flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)",
className
)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}
+20
View File
@@ -0,0 +1,20 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
function Label({ className, ...props }: React.ComponentProps<"label">) {
return (
<label
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }
+38
View File
@@ -0,0 +1,38 @@
"use client"
import { Radio as RadioPrimitive } from "@base-ui/react/radio"
import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group"
import { cn } from "@/lib/utils"
function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) {
return (
<RadioGroupPrimitive
data-slot="radio-group"
className={cn("grid w-full gap-2", className)}
{...props}
/>
)
}
function RadioGroupItem({ className, ...props }: RadioPrimitive.Root.Props) {
return (
<RadioPrimitive.Root
data-slot="radio-group-item"
className={cn(
"group/radio-group-item peer relative flex aspect-square size-4 shrink-0 rounded-full border border-input outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary",
className
)}
{...props}
>
<RadioPrimitive.Indicator
data-slot="radio-group-indicator"
className="flex size-4 items-center justify-center"
>
<span className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary-foreground" />
</RadioPrimitive.Indicator>
</RadioPrimitive.Root>
)
}
export { RadioGroup, RadioGroupItem }
+23
View File
@@ -0,0 +1,23 @@
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
...props
}: SeparatorPrimitive.Props) {
return (
<SeparatorPrimitive
data-slot="separator"
orientation={orientation}
className={cn(
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
className
)}
{...props}
/>
)
}
export { Separator }
+52
View File
@@ -0,0 +1,52 @@
import { Slider as SliderPrimitive } from "@base-ui/react/slider"
import { cn } from "@/lib/utils"
function Slider({
className,
defaultValue,
value,
min = 0,
max = 100,
...props
}: SliderPrimitive.Root.Props) {
const _values = Array.isArray(value)
? value
: Array.isArray(defaultValue)
? defaultValue
: [min, max]
return (
<SliderPrimitive.Root
className={cn("data-horizontal:w-full data-vertical:h-full", className)}
data-slot="slider"
defaultValue={defaultValue}
value={value}
min={min}
max={max}
thumbAlignment="edge"
{...props}
>
<SliderPrimitive.Control className="relative flex w-full touch-none items-center select-none data-disabled:opacity-50 data-vertical:h-full data-vertical:min-h-40 data-vertical:w-auto data-vertical:flex-col">
<SliderPrimitive.Track
data-slot="slider-track"
className="relative grow overflow-hidden rounded-full bg-muted select-none data-horizontal:h-1 data-horizontal:w-full data-vertical:h-full data-vertical:w-1"
>
<SliderPrimitive.Indicator
data-slot="slider-range"
className="bg-primary select-none data-horizontal:h-full data-vertical:w-full"
/>
</SliderPrimitive.Track>
{Array.from({ length: _values.length }, (_, index) => (
<SliderPrimitive.Thumb
data-slot="slider-thumb"
key={index}
className="relative block size-3 shrink-0 rounded-full border border-ring bg-white ring-ring/50 transition-[color,box-shadow] select-none after:absolute after:-inset-2 hover:ring-3 focus-visible:ring-3 focus-visible:outline-hidden active:ring-3 disabled:pointer-events-none disabled:opacity-50"
/>
))}
</SliderPrimitive.Control>
</SliderPrimitive.Root>
)
}
export { Slider }
+30
View File
@@ -0,0 +1,30 @@
import { Switch as SwitchPrimitive } from "@base-ui/react/switch"
import { cn } from "@/lib/utils"
function Switch({
className,
size = "default",
...props
}: SwitchPrimitive.Root.Props & {
size?: "sm" | "default"
}) {
return (
<SwitchPrimitive.Root
data-slot="switch"
data-size={size}
className={cn(
"peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
/>
</SwitchPrimitive.Root>
)
}
export { Switch }
+12 -9
View File
@@ -1,19 +1,22 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { useThemeStore } from "@/stores/themeStore";
export function useTheme() { export function useTheme() {
const darkMode = useThemeStore((s) => s.darkMode);
useEffect(() => { useEffect(() => {
const mq = window.matchMedia("(prefers-color-scheme: dark)"); const mq = window.matchMedia("(prefers-color-scheme: dark)");
const apply = (dark: boolean) => { const apply = () => {
const mode = useThemeStore.getState().darkMode;
let dark = false;
if (mode === "auto") dark = mq.matches;
else if (mode === "dark") dark = true;
document.documentElement.classList.toggle("dark", dark); document.documentElement.classList.toggle("dark", dark);
}; };
// 初始应用 apply();
apply(mq.matches); mq.addEventListener("change", apply);
return () => mq.removeEventListener("change", apply);
// 监听系统切换 }, [darkMode]);
const handler = (e: MediaQueryListEvent) => apply(e.matches);
mq.addEventListener("change", handler);
return () => mq.removeEventListener("change", handler);
}, []);
} }
+95 -2
View File
@@ -92,7 +92,7 @@
--sidebar-ring: oklch(0.708 0 0); --sidebar-ring: oklch(0.708 0 0);
/* 标题栏 & 导航 - 亮色 */ /* 标题栏 & 导航 - 亮色 */
--titlebar-bg: rgba(255, 255, 255, 0.5); --titlebar-bg: rgba(255, 255, 255, 0.1);
--titlebar-border: rgba(0, 0, 0, 0.08); --titlebar-border: rgba(0, 0, 0, 0.08);
--indicator-bg: rgba(0, 0, 0, 0.1); --indicator-bg: rgba(0, 0, 0, 0.1);
--indicator-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); --indicator-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
@@ -132,7 +132,7 @@
--sidebar-ring: oklch(0.556 0 0); --sidebar-ring: oklch(0.556 0 0);
/* 标题栏 & 导航 - 暗色 */ /* 标题栏 & 导航 - 暗色 */
--titlebar-bg: rgba(0, 0, 0, 0.2); --titlebar-bg: rgba(0, 0, 0, 0.1);
--titlebar-border: rgba(255, 255, 255, 0.08); --titlebar-border: rgba(255, 255, 255, 0.08);
--indicator-bg: rgba(255, 255, 255, 0.15); --indicator-bg: rgba(255, 255, 255, 0.15);
--indicator-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); --indicator-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
@@ -150,6 +150,38 @@
} }
} }
/* ============================================
自定义滚动条 — 默认隐藏,滚动时显示
============================================ */
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 999px;
transition: background 0.2s;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(128, 128, 128, 0.4);
}
.dark ::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.25);
}
/* 悬停/滚动时显示滑块 */
.scroll-area:hover::-webkit-scrollbar-thumb,
.scroll-area::-webkit-scrollbar-thumb:active {
background: rgba(128, 128, 128, 0.25);
}
.dark .scroll-area:hover::-webkit-scrollbar-thumb,
.dark .scroll-area::-webkit-scrollbar-thumb:active {
background: rgba(255, 255, 255, 0.15);
}
/* ============================================ /* ============================================
View Transitions — 页面切换动效 View Transitions — 页面切换动效
============================================ */ ============================================ */
@@ -193,3 +225,64 @@
from { opacity: 0; transform: scale(0.97); } from { opacity: 0; transform: scale(0.97); }
to { opacity: 1; transform: scale(1); } to { opacity: 1; transform: scale(1); }
} }
/* ============================================
磨砂卡片 — 复用 --titlebar-bg / --titlebar-border
============================================ */
.glass-card {
background: rgba(255, 255, 255, 0.72);
border: 1px solid var(--titlebar-border);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
}
:root.dark .glass-card {
background: rgba(0, 0, 0, 0.48);
}
/* ============================================
强内容模式 — 设置页面内容区模糊遮罩
============================================ */
.content-blur-overlay {
position: fixed;
inset: 0;
backdrop-filter: blur(80px) saturate(120%);
-webkit-backdrop-filter: blur(80px) saturate(120%);
z-index: 0;
pointer-events: none;
background: rgba(255, 255, 255, 0.5);
}
:root.dark .content-blur-overlay {
background: rgba(0, 0, 0, 0.5);
}
/* ============================================
辅助功能
============================================ */
.reduce-motion *,
.reduce-motion *::before,
.reduce-motion *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
.reduce-motion ::view-transition-old(root),
.reduce-motion ::view-transition-new(root) {
animation: none !important;
}
.reduce-transparency {
--titlebar-bg: var(--background);
}
.reduce-transparency .glass-card {
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
.high-contrast {
--foreground: oklch(0 0 0);
}
:root.dark .high-contrast {
--foreground: oklch(1 0 0);
}
+12 -1
View File
@@ -1,6 +1,8 @@
import { type ReactNode } from "react"; import { type ReactNode } from "react";
import { BackgroundLayer } from "@/components/background/BackgroundLayer"; import { BackgroundLayer } from "@/components/background/BackgroundLayer";
import { SystemLayer } from "@/components/system/SystemLayer"; import { SystemLayer } from "@/components/system/SystemLayer";
import { useA11yStore } from "@/stores/a11yStore";
import clsx from "clsx";
interface RootLayoutProps { interface RootLayoutProps {
children: ReactNode; children: ReactNode;
@@ -15,8 +17,17 @@ export function RootLayout({
showMaximize = true, showMaximize = true,
showClose = true, showClose = true,
}: RootLayoutProps) { }: RootLayoutProps) {
const { reduceMotion, reduceTransparency, highContrast } = useA11yStore();
return ( return (
<div className="relative w-screen h-screen overflow-hidden"> <div
className={clsx(
"relative w-screen h-screen overflow-hidden",
reduceMotion && "reduce-motion",
reduceTransparency && "reduce-transparency",
highContrast && "high-contrast",
)}
>
{/* Layer 0: Background */} {/* Layer 0: Background */}
<BackgroundLayer /> <BackgroundLayer />
+8
View File
@@ -0,0 +1,8 @@
export function AdvancedSetting() {
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4"></h2>
<p className="text-sm text-muted-foreground"></p>
</div>
);
}
+8
View File
@@ -0,0 +1,8 @@
export function GameAccountSetting() {
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4">&</h2>
<p className="text-sm text-muted-foreground"> Minecraft </p>
</div>
);
}
+8
View File
@@ -0,0 +1,8 @@
export function GameDirSetting() {
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4"></h2>
<p className="text-sm text-muted-foreground"></p>
</div>
);
}
+8
View File
@@ -0,0 +1,8 @@
export function JavaMemSetting() {
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4">Java虚拟机与内存</h2>
<p className="text-sm text-muted-foreground"> Java </p>
</div>
);
}
+8
View File
@@ -0,0 +1,8 @@
export function AboutSetting() {
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4"></h2>
<p className="text-sm text-muted-foreground">Koring Launcher </p>
</div>
);
}
+8
View File
@@ -0,0 +1,8 @@
export function AccountSetting() {
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4">Koring Account</h2>
<p className="text-sm text-muted-foreground"> Koring </p>
</div>
);
}
+8
View File
@@ -0,0 +1,8 @@
export function CopyrightSetting() {
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4"></h2>
<p className="text-sm text-muted-foreground"></p>
</div>
);
}
+8
View File
@@ -0,0 +1,8 @@
export function HomeSetting() {
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4"></h2>
<p className="text-sm text-muted-foreground"></p>
</div>
);
}
+148 -11
View File
@@ -1,18 +1,155 @@
import { Button } from "@/components/ui/button"; import { useState, type ReactNode } from "react";
import { useRouteStore } from "@/stores/routeStore"; import {
Home,
UserCircle,
Info,
Copyright,
Gamepad2,
Cpu,
FolderOpen,
Settings,
Palette,
Monitor,
Languages,
Accessibility,
Download,
Globe,
Network,
Shield,
MessageSquareHeart,
Heart,
Code,
} from "lucide-react";
import { HomeSetting } from "./general/home";
import { AccountSetting } from "./general/account";
import { AboutSetting } from "./general/about";
import { CopyrightSetting } from "./general/copyright";
import { GameAccountSetting } from "./game/game-account";
import { JavaMemSetting } from "./game/java-mem";
import { GameDirSetting } from "./game/game-dir";
import { AdvancedSetting } from "./game/advanced";
import { ThemeBgSetting } from "./personalization/theme-bg";
import { UiSetting } from "./personalization/ui";
import { LangSetting } from "./personalization/lang";
import { A11ySetting } from "./personalization/a11y";
import { DownloadSetting } from "./network/download";
import { EtherOnlineSetting } from "./network/ether-online";
import { TawaOnlineSetting } from "./network/tawa-online";
import { SecurityIdSetting } from "./network/security-id";
import { FeedbackSetting } from "./other/feedback";
import { SponsorSetting } from "./other/sponsor";
import { DeveloperSetting } from "./other/developer";
interface MenuItem {
key: string;
label: string;
icon: ReactNode;
component: ReactNode;
}
interface MenuGroup {
title: string;
items: MenuItem[];
}
const iconCls = "w-[18px] h-[18px] shrink-0";
const menuData: MenuGroup[] = [
{
title: "通用",
items: [
{ key: "home", label: "主页", icon: <Home className={iconCls} />, component: <HomeSetting /> },
{ key: "account", label: "Koring Account", icon: <UserCircle className={iconCls} />, component: <AccountSetting /> },
{ key: "about", label: "关于", icon: <Info className={iconCls} />, component: <AboutSetting /> },
{ key: "copyright", label: "版权", icon: <Copyright className={iconCls} />, component: <CopyrightSetting /> },
],
},
{
title: "游戏",
items: [
{ key: "game-account", label: "游戏账户&档案", icon: <Gamepad2 className={iconCls} />, component: <GameAccountSetting /> },
{ key: "java-mem", label: "Java虚拟机与内存", icon: <Cpu className={iconCls} />, component: <JavaMemSetting /> },
{ key: "game-dir", label: "游戏目录", icon: <FolderOpen className={iconCls} />, component: <GameDirSetting /> },
{ key: "advanced", label: "高级设置", icon: <Settings className={iconCls} />, component: <AdvancedSetting /> },
],
},
{
title: "个性化",
items: [
{ key: "theme-bg", label: "主题与背景", icon: <Palette className={iconCls} />, component: <ThemeBgSetting /> },
{ key: "ui", label: "主界面", icon: <Monitor className={iconCls} />, component: <UiSetting /> },
{ key: "lang", label: "语言", icon: <Languages className={iconCls} />, component: <LangSetting /> },
{ key: "a11y", label: "辅助功能", icon: <Accessibility className={iconCls} />, component: <A11ySetting /> },
],
},
{
title: "网络",
items: [
{ key: "download", label: "下载", icon: <Download className={iconCls} />, component: <DownloadSetting /> },
{ key: "ether-online", label: "以太联机", icon: <Globe className={iconCls} />, component: <EtherOnlineSetting /> },
{ key: "tawa-online", label: "陶瓦联机", icon: <Network className={iconCls} />, component: <TawaOnlineSetting /> },
{ key: "security-id", label: "安全识别服务", icon: <Shield className={iconCls} />, component: <SecurityIdSetting /> },
],
},
{
title: "其他",
items: [
{ key: "feedback", label: "服务与反馈", icon: <MessageSquareHeart className={iconCls} />, component: <FeedbackSetting /> },
{ key: "sponsor", label: "赞助我们", icon: <Heart className={iconCls} />, component: <SponsorSetting /> },
{ key: "developer", label: "开发者选项", icon: <Code className={iconCls} />, component: <DeveloperSetting /> },
],
},
];
const allItems = menuData.flatMap((g) => g.items);
export function Setting() { export function Setting() {
const navigate = useRouteStore((s) => s.navigate); const [selected, setSelected] = useState("home");
const current = allItems.find((i) => i.key === selected);
return ( return (
<div className="flex items-center justify-center h-full"> <div className="flex h-full">
<div className="text-center"> {/* 侧边栏 */}
<h1 className="text-4xl font-bold mb-4 text-foreground"></h1> <aside className="scroll-area w-[240px] shrink-0 h-full overflow-y-auto py-5 pl-5 pr-2">
<p className="text-muted-foreground mb-6"></p> <nav className="space-y-5">
<div className="flex gap-3 justify-center"> {menuData.map((group) => (
<Button variant="outline" onClick={() => navigate("debug")}></Button> <div key={group.title}>
</div> <h3 className="text-[11px] font-semibold uppercase tracking-wider text-foreground/30 mb-2 px-2">
</div> {group.title}
</h3>
<div className="space-y-0.5">
{group.items.map((item) => {
const active = selected === item.key;
return (
<button
key={item.key}
onClick={() => setSelected(item.key)}
className={[
"w-full flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-[14px] transition-all duration-150",
active
? "bg-foreground/[0.08] text-foreground font-medium shadow-sm"
: "text-foreground/50 hover:text-foreground/80 hover:bg-foreground/[0.04]",
].join(" ")}
>
<span className={active ? "text-foreground/80" : "text-foreground/35"}>
{item.icon}
</span>
{item.label}
</button>
);
})}
</div>
</div>
))}
</nav>
</aside>
{/* 内容区 */}
<main className="scroll-area flex-1 h-full overflow-y-auto p-8">
{current?.component}
</main>
</div> </div>
); );
} }
+8
View File
@@ -0,0 +1,8 @@
export function DownloadSetting() {
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4"></h2>
<p className="text-sm text-muted-foreground"></p>
</div>
);
}
@@ -0,0 +1,8 @@
export function EtherOnlineSetting() {
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4"></h2>
<p className="text-sm text-muted-foreground"></p>
</div>
);
}
@@ -0,0 +1,8 @@
export function SecurityIdSetting() {
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4"></h2>
<p className="text-sm text-muted-foreground"></p>
</div>
);
}
@@ -0,0 +1,8 @@
export function TawaOnlineSetting() {
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4"></h2>
<p className="text-sm text-muted-foreground"></p>
</div>
);
}
+122
View File
@@ -0,0 +1,122 @@
import { useDevStore } from "@/stores/devStore";
import { Button } from "@/components/ui/button";
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
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>
);
}
const openSplash = async () => {
try {
const existing = await WebviewWindow.getByLabel("splashscreen");
if (existing) {
await existing.show();
await existing.setFocus();
return;
}
const splash = new WebviewWindow("splashscreen", {
url: "/splash.html",
width: 480,
height: 320,
decorations: false,
transparent: true,
center: true,
visible: true,
resizable: false,
minWidth: 480,
maxWidth: 480,
minHeight: 320,
maxHeight: 320,
} as any);
splash.once("tauri://error", (e) => {
console.error("Splash window error:", e);
});
} catch (err) {
console.error("Failed to open splash:", err);
}
};
const closeSplash = async () => {
try {
const splash = await WebviewWindow.getByLabel("splashscreen");
if (splash) {
await splash.close();
}
} catch (err) {
console.error("Failed to close splash:", err);
}
};
export function DeveloperSetting() {
const { forceDisableContentBlur, setForceDisableContentBlur } = useDevStore();
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-6"></h2>
<div className="space-y-6">
{/* ===== 启动画面 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="space-y-3">
<GlassCard>
<SettingRow
label="打开启动画面"
desc="立即创建并显示 Splash Screen 窗口(480×320"
>
<Button size="sm" onClick={openSplash}>
</Button>
</SettingRow>
</GlassCard>
<GlassCard>
<SettingRow
label="关闭启动画面"
desc="立即关闭当前显示的 Splash Screen 窗口"
>
<Button variant="destructive" size="sm" onClick={closeSplash}>
</Button>
</SettingRow>
</GlassCard>
</div>
</div>
{/* ===== 显示效果测试 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="space-y-3">
<GlassCard>
<SettingRow
label="强制关闭背景「强内容模式」"
desc="覆盖系统设置,在设置页面也禁用背景模糊遮罩,用于对比测试"
>
<Button
variant={forceDisableContentBlur ? "default" : "outline"}
size="sm"
onClick={() => setForceDisableContentBlur(!forceDisableContentBlur)}
>
{forceDisableContentBlur ? "已开启" : "已关闭"}
</Button>
</SettingRow>
</GlassCard>
</div>
</div>
</div>
</div>
);
}
+8
View File
@@ -0,0 +1,8 @@
export function FeedbackSetting() {
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4"></h2>
<p className="text-sm text-muted-foreground"></p>
</div>
);
}
+8
View File
@@ -0,0 +1,8 @@
export function SponsorSetting() {
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4"></h2>
<p className="text-sm text-muted-foreground"> Koring Launcher </p>
</div>
);
}
@@ -0,0 +1,54 @@
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>
);
}
export function A11ySetting() {
const { reduceMotion, setReduceMotion, reduceTransparency, setReduceTransparency, highContrast, setHighContrast } = useA11yStore();
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-6"></h2>
<div className="space-y-6">
{/* ===== 显示 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="space-y-3">
<GlassCard>
<SettingRow label="减少动画" desc="关闭页面切换动画和背景动效">
<Switch checked={reduceMotion} onCheckedChange={setReduceMotion} />
</SettingRow>
</GlassCard>
<GlassCard>
<SettingRow label="减少透明度" desc="将磨砂玻璃效果替换为纯色背景,提升可读性">
<Switch checked={reduceTransparency} onCheckedChange={setReduceTransparency} />
</SettingRow>
</GlassCard>
<GlassCard>
<SettingRow label="高对比度" desc="增强文字与背景的对比度,改善可读性">
<Switch checked={highContrast} onCheckedChange={setHighContrast} />
</SettingRow>
</GlassCard>
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,8 @@
export function LangSetting() {
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4"></h2>
<p className="text-sm text-muted-foreground"></p>
</div>
);
}
@@ -0,0 +1,195 @@
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 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>
);
}
/* ======== 深色模式预览卡片 ======== */
function ThemePreviewCard({ mode, selected, onClick }: { mode: DarkMode; selected: boolean; onClick: () => void }) {
const isDark = mode === "dark";
const isAuto = mode === "auto";
const label = isAuto ? "跟随系统" : isDark ? "深色模式" : "浅色模式";
return (
<button
onClick={onClick}
className={clsx(
"relative rounded-md p-1 transition-all duration-200",
"border-2",
selected
? "border-primary ring-2 ring-primary/20"
: "border-transparent hover:border-muted-foreground/20",
)}
>
<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]">
<div className="w-[3px] h-[3px] rounded-full bg-red-400" />
<div className="w-[3px] h-[3px] rounded-full bg-yellow-400" />
<div className="w-[3px] h-[3px] rounded-full bg-green-400" />
</div>
<div className="flex-1 px-2 py-1">
<div className="w-full h-[4px] rounded bg-gray-200 mb-1" />
<div className="w-3/4 h-[3px] rounded bg-gray-100" />
</div>
</div>
<div className="w-1/2 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" />
<div className="w-[3px] h-[3px] rounded-full bg-yellow-400" />
<div className="w-[3px] h-[3px] rounded-full bg-green-400" />
</div>
<div className="flex-1 px-2 py-1">
<div className="w-full h-[4px] rounded bg-[#3a3a3c] mb-1" />
<div className="w-3/4 h-[3px] rounded bg-[#2c2c2e]" />
</div>
</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" />
<div className="w-[3px] h-[3px] rounded-full bg-yellow-400" />
<div className="w-[3px] h-[3px] rounded-full bg-green-400" />
</div>
<div className="flex-1 px-2 py-1">
<div className="w-full h-[4px] rounded bg-[#3a3a3c] mb-1" />
<div className="w-3/4 h-[3px] rounded bg-[#2c2c2e] mb-1" />
<div className="w-1/2 h-[3px] rounded bg-[#2c2c2e]" />
</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" />
<div className="w-[3px] h-[3px] rounded-full bg-yellow-400" />
<div className="w-[3px] h-[3px] rounded-full bg-green-400" />
</div>
<div className="flex-1 px-2 py-1">
<div className="w-full h-[4px] rounded bg-gray-200 mb-1" />
<div className="w-3/4 h-[3px] rounded bg-gray-100 mb-1" />
<div className="w-1/2 h-[3px] rounded bg-gray-100" />
</div>
</div>
)}
</div>
<p className="text-[11px] text-center mt-1.5 text-muted-foreground">{label}</p>
</button>
);
}
/* ======== 页面 ======== */
export function ThemeBgSetting() {
const { darkMode, setDarkMode, parallax, setParallax } = useThemeStore();
const { opacity, setOpacity, blur, setBlur, reset } = useBackgroundStore();
const handlePickImage = async () => {
// TODO: 打开文件选择器
};
const handleReset = async () => {
await reset();
};
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-6"></h2>
<div className="space-y-6">
{/* ===== 深色模式 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<GlassCard>
<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>
</div>
{/* ===== 背景 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="space-y-3">
<GlassCard>
<SettingRow label="背景图片" desc="更换启动器背景图片">
<Button variant="outline" size="sm" onClick={handlePickImage}>
</Button>
</SettingRow>
</GlassCard>
<GlassCard>
<SettingRow label="背景模糊" desc={`对背景图施加高斯模糊,当前 ${blur}px`}>
<Slider
className="w-[180px]"
value={[blur]}
min={0}
max={20}
step={1}
onValueChange={(v) => setBlur(Array.isArray(v) ? v[0] : v)}
/>
</SettingRow>
</GlassCard>
<GlassCard>
<SettingRow label="背景不透明度" desc={`控制背景图的可见程度,当前 ${Math.round(opacity * 100)}%`}>
<Slider
className="w-[180px]"
value={[opacity * 100]}
min={0}
max={100}
step={1}
onValueChange={(v) => setOpacity((Array.isArray(v) ? v[0] : v) / 100)}
/>
</SettingRow>
</GlassCard>
<GlassCard>
<SettingRow
label="背景图片视差"
desc="背景图片随窗口滚动产生视差位移"
>
<Switch checked={parallax} onCheckedChange={setParallax} />
</SettingRow>
</GlassCard>
<GlassCard>
<SettingRow label="恢复默认" desc="重置所有背景设置为初始状态">
<Button variant="destructive" size="sm" onClick={handleReset}>
</Button>
</SettingRow>
</GlassCard>
</div>
</div>
</div>
</div>
);
}
+8
View File
@@ -0,0 +1,8 @@
export function UiSetting() {
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4"></h2>
<p className="text-sm text-muted-foreground"></p>
</div>
);
}
+20
View File
@@ -0,0 +1,20 @@
import { create } from "zustand";
interface A11yState {
reduceMotion: boolean;
reduceTransparency: boolean;
highContrast: boolean;
setReduceMotion: (v: boolean) => void;
setReduceTransparency: (v: boolean) => void;
setHighContrast: (v: boolean) => void;
}
export const useA11yStore = create<A11yState>((set) => ({
reduceMotion: false,
reduceTransparency: false,
highContrast: false,
setReduceMotion: (v) => set({ reduceMotion: v }),
setReduceTransparency: (v) => set({ reduceTransparency: v }),
setHighContrast: (v) => set({ highContrast: v }),
}));
+12
View File
@@ -0,0 +1,12 @@
import { create } from "zustand";
interface DevState {
forceDisableContentBlur: boolean;
setForceDisableContentBlur: (v: boolean) => void;
}
export const useDevStore = create<DevState>((set) => ({
forceDisableContentBlur: false,
setForceDisableContentBlur: (v) => set({ forceDisableContentBlur: v }),
}));
+30
View File
@@ -0,0 +1,30 @@
import { create } from "zustand";
export type DarkMode = "auto" | "light" | "dark";
interface ThemeState {
darkMode: DarkMode;
parallax: boolean;
setDarkMode: (mode: DarkMode) => void;
setParallax: (v: boolean) => void;
}
function applyDarkMode(mode: DarkMode) {
const mq = window.matchMedia("(prefers-color-scheme: dark)");
let dark = false;
if (mode === "auto") dark = mq.matches;
else if (mode === "dark") dark = true;
document.documentElement.classList.toggle("dark", dark);
}
export const useThemeStore = create<ThemeState>((set) => ({
darkMode: "auto",
parallax: true,
setDarkMode: (mode) => {
applyDarkMode(mode);
set({ darkMode: mode });
},
setParallax: (v) => set({ parallax: v }),
}));