diff --git a/AGENTS.md b/AGENTS.md index d19a473..b1a8604 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,8 +27,8 @@ pnpm dist:run # production icon + Windows installer - **Preload script**: `electron/preload.ts` exposes `window.electronAPI` via context bridge. - **IPC handlers**: All handlers are in `electron/handlers/` directory. - **Mutable win ref**: `electron/main.ts` uses a mutable `win` object — handlers read `win.mainWindow` at runtime, not at registration time. -- **Config**: YAML format (`Koring.yml`) stored next to executable. Sparse save (only non-default values). -- **Auth**: JSON file (`koring-auth.json`) stored next to executable. +- **Config**: YAML format (`Koring.yml`) stored in userData (packaged) / project root (dev). Sparse save (only non-default values). ⚠️ 不可放安装目录:NSIS 重装/升级会经旧卸载器删除整个安装目录。 +- **Auth**: JSON file (`koring-auth.json`) stored in userData (packaged) / project root (dev). - **Path alias**: `@/` maps to `src/` (configured in `vite.config.ts` and `tsconfig.json`). - **Dev mode**: Vite runs on port 1420, Electron loads from localhost. - **Asset paths**: Use `import.meta.env.BASE_URL` prefix for public assets. Absolute paths break in packaged app. @@ -66,7 +66,8 @@ build/ ← generated by switch-icon.js (gitignored) - `electron/config.ts`: YAML config management (sparse save) - `electron/auth.ts`: Auth data persistence (JSON file) - `electron/core/`: @xmcl/* integrations (auth, installer, launcher, modrinth, instance) -- `electron/core/background-image.ts`: 背景图处理服务(自选背景降采样/重编码,程序本体资源管理) +- `electron/core/background-image.ts`: 背景图处理服务 —— 自选壁纸复制到 userData 并按屏幕尺寸降采样/重编码**落盘**,配置文件只存**文件路径**(不使用 BASE64) +- `electron/resource-protocol.ts`: `koring-res://` 特权自定义协议 —— 渲染进程以「资源引用」流式读取本地壁纸;仅服务 userData 内 `background-custom*` 白名单文件(realpath 二次校验,防目录穿越) - `electron/handlers/`: IPC handlers (config, auth, install, launch, mods, instance, background, task, system, window) ## Frontend notes @@ -78,7 +79,7 @@ build/ ← generated by switch-icon.js (gitignored) - `registry.ts` 资源注册表服务(acquire/release、引用计数、预算 + LRU 逐出、onRelease 释放回调) - `store.ts` 注册表 → zustand 镜像(调试面板消费) - `image.ts` 图片解码管线(按显示尺寸降采样)、`hooks.ts`/`ManagedImage.tsx` 复用组件(供列表缩略图) - - 当前接线点:`BackgroundLayer` 把当前背景登记为 `background` 类资源;主进程 `background:prepare` 在进渲染端前压小大图 + - 当前接线点:`BackgroundLayer` 把当前背景(dataURL 或 koring-res 引用)登记为 `background` 类资源;自选壁纸经 `background:import` 落盘、配置存文件路径、渲染端经 `background:resolve` 拿 `koring-res://` 引用(全程无 base64) - 监控入口:debug 页「资源与内存」(`debug-resource`) - `src/hooks/useTheme.ts`: Dark mode sync with Electron theme - `src/components/system/WindowControls.tsx`: Custom window controls (min/max/close), uses ` + ); + })} + + + ))} + + {grouped.length === 0 && ( +

+ 没有匹配「{query.trim()}」的页面 +

+ )} + + +

+ 跳转到 OOBE / 更新引导等流程页面后,标题栏会切换为对应模式 +

+ + ); +} diff --git a/src/pages/oobe/step-finish.tsx b/src/pages/oobe/step-finish.tsx index 849556a..bcc9661 100644 --- a/src/pages/oobe/step-finish.tsx +++ b/src/pages/oobe/step-finish.tsx @@ -1,3 +1,4 @@ +import { useEffect, useState } from "react"; import { useRouteStore } from "@/stores/routeStore"; import { useConfigStore } from "@/stores/configStore"; import { OobeLayout } from "./layout"; @@ -7,6 +8,13 @@ export function OobeFinish() { const navigate = useRouteStore((s) => s.navigate); const setOobe = useConfigStore((s) => s.setOobe); + // 「前往首页」按钮先隐藏,hello 动画播完后(4 秒)渐入浮现 + const [btnVisible, setBtnVisible] = useState(false); + useEffect(() => { + const t = setTimeout(() => setBtnVisible(true), 4000); + return () => clearTimeout(t); + }, []); + const handleFinish = () => { setOobe(false); navigate("home"); @@ -21,7 +29,8 @@ export function OobeFinish() {
diff --git a/src/pages/setting/personalization/theme-bg.tsx b/src/pages/setting/personalization/theme-bg.tsx index ff14c91..56d5c03 100644 --- a/src/pages/setting/personalization/theme-bg.tsx +++ b/src/pages/setting/personalization/theme-bg.tsx @@ -1,3 +1,4 @@ +import { useEffect, useState } from "react"; import { useThemeStore, type DarkMode } from "@/stores/themeStore"; import { useBackgroundStore } from "@/stores/backgroundStore"; import { Switch, Button, Slider } from "@heroui/react"; @@ -5,6 +6,9 @@ import { DEFAULT_BG } from "@/lib/mode"; import clsx from "clsx"; import { SettingCard, SettingRow, PageHeader, SectionTitle } from "@/components/setting"; +/** 可直接用于 CSS/`` 的源(data:/http(s):/file:/相对 URL) */ +const isCssSource = (v: string) => /^(data:|https?:|file:|\/|\.\/|\.\.\/)/i.test(v); + function ThemePreviewCard({ mode, selected, onClick }: { mode: DarkMode; selected: boolean; onClick: () => void }) { const isDark = mode === "dark"; const isAuto = mode === "auto"; @@ -85,11 +89,30 @@ function ThemePreviewCard({ mode, selected, onClick }: { mode: DarkMode; selecte export function ThemeBgSetting() { const { darkMode, setDarkMode, parallax, setParallax } = useThemeStore(); const { image, opacity, setOpacity, blur, setBlur, setImage, reset } = useBackgroundStore(); + // 预览图:配置文件里存的是文件路径 → 主进程解析为 koring-res:// 引用再显示 + const [previewUrl, setPreviewUrl] = useState(null); + + useEffect(() => { + let cancelled = false; + if (!image || image === DEFAULT_BG || isCssSource(image)) { + setPreviewUrl(null); + return () => { + cancelled = true; + }; + } + window.electronAPI?.resolveBackgroundResource?.(image).then((res) => { + if (!cancelled) setPreviewUrl(res?.url ?? null); + }); + return () => { + cancelled = true; + }; + }, [image]); const handlePickImage = async () => { - const dataUrl = await (window as any).electronAPI?.pickBackgroundImage(); - if (dataUrl) { - setImage(dataUrl); + // 返回的是 userData 内的文件路径(配置/Store 以路径保存,不使用 BASE64) + const filePath = await window.electronAPI?.pickBackgroundImage?.(); + if (filePath) { + setImage(filePath); } }; @@ -124,11 +147,15 @@ export function ThemeBgSetting() { {image && image !== DEFAULT_BG && (
- 背景预览 + {previewUrl ? ( + 背景预览 + ) : isCssSource(image) ? ( + 背景预览 + ) : ( +
+ 预览加载中… +
+ )}
)} diff --git a/src/pages/upvp/step-finish.tsx b/src/pages/upvp/step-finish.tsx index 9b38887..96d8b2c 100644 --- a/src/pages/upvp/step-finish.tsx +++ b/src/pages/upvp/step-finish.tsx @@ -20,6 +20,13 @@ export function UpvpFinish() { const [ready, setReady] = useState(false); const [shouldWrite, setShouldWrite] = useState(false); + // 「前往首页」按钮先隐藏,hello 动画播完后(4 秒)渐入浮现 + const [btnVisible, setBtnVisible] = useState(false); + useEffect(() => { + const t = setTimeout(() => setBtnVisible(true), 4000); + return () => clearTimeout(t); + }, []); + useEffect(() => { let cancelled = false; compareVersions(VERSION, appVersion) @@ -58,7 +65,8 @@ export function UpvpFinish() { diff --git a/src/stores/routeStore.ts b/src/stores/routeStore.ts index 08d1939..996cb97 100644 --- a/src/stores/routeStore.ts +++ b/src/stores/routeStore.ts @@ -33,7 +33,8 @@ export type RouteKey = | "debug-update" | "debug-task" | "debug-resource" - | "debug-crash"; + | "debug-crash" + | "debug-routes"; export type TitleBarMode = "default" | "sub" | "window" | "oobe"; export type TransitionDirection = "forward" | "backward"; @@ -83,6 +84,8 @@ export const allRoutes: RouteItem[] = [ { key: "debug-update", label: "更新功能测试", path: "/debug/update", hidden: true }, { key: "debug-task", label: "任务队列调试", path: "/debug/task", hidden: true }, { key: "debug-resource", label: "资源与内存", path: "/debug/resource", hidden: true }, + { key: "debug-crash", label: "崩溃测试", path: "/debug/crash", hidden: true }, + { key: "debug-routes", label: "页面跳转", path: "/debug/routes", hidden: true }, ]; const topLevelKeys = new Set(routes.map((r) => r.key)); diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 1a8015a..d3c70a7 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -25,6 +25,10 @@ interface ElectronAPI { // Config reset resetConfig: () => Promise; + // 壁纸(文件路径存储 → koring-res:// 资源引用,不使用 BASE64) + pickBackgroundImage: () => Promise; + resolveBackgroundResource: (value: string) => Promise<{ url: string | null; bytes: number }>; + // Auto-update checkForUpdates: (manual?: boolean) => Promise; downloadUpdate: () => Promise;