mirror of
https://github.com/dream-pep/koring-launcher.git
synced 2026-09-12 05:45:18 +08:00
feat: 个性化设置页面重构与辅助功能
- 重构主题与背景页面:一选项一卡片布局,深色模式可视化预览卡片 - 新增强内容模式:设置页面自动显示 80px 模糊背景遮罩,自适应亮/暗色 - 新增辅助功能:减少动画、减少透明度、高对比度三项开关 - 新增开发者选项:即时打开/关闭启动画面、强制关闭强内容模式 - 磨砂卡片样式:降低透明度提升可读性,圆角 16px - 启动画面显示时间调整为 4 秒 - CSS 新增 .glass-card、.content-blur-overlay、辅助功能类 - 新增 a11yStore、devStore、themeStore 状态管理
This commit is contained in:
@@ -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") {
|
||||||
|
|||||||
@@ -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" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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); };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -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 }
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
@@ -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 }
|
||||||
@@ -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 }
|
||||||
@@ -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 }
|
||||||
@@ -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
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 }),
|
||||||
|
}));
|
||||||
@@ -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 }),
|
||||||
|
}));
|
||||||
@@ -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 }),
|
||||||
|
}));
|
||||||
Reference in New Issue
Block a user