[feat] more hw info and update feature

This commit is contained in:
2025-11-08 18:09:35 +08:00
parent 41105d3bab
commit c8d8339f30
13 changed files with 813 additions and 500 deletions

View File

@@ -1,29 +1,76 @@
"use client"
import { useEffect } from "react"
import { useAppStore } from "@/store/app"
import { Switch } from "@heroui/react"
import { Switch, Chip } from "@heroui/react"
import { UpdateChecker } from "@/components/cstb/UpdateChecker"
import { getVersion } from "@tauri-apps/api/app"
export default function Page() {
const app = useAppStore()
// 初始化版本号(如果还没有设置)
useEffect(() => {
if (typeof window !== "undefined" && (!app.state.version || app.state.version === "0.0.1")) {
void getVersion().then((version) => {
app.setVersion(version)
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// 从环境变量或配置中获取更新服务器地址
// 这里可以改为从 store 或配置文件中读取
const customEndpoint = process.env.NEXT_PUBLIC_UPDATE_ENDPOINT || ""
const githubRepo = process.env.NEXT_PUBLIC_GITHUB_REPO || ""
return (
<section className="flex flex-col gap-4 overflow-hidden">
<div className="flex flex-col items-start gap-4 pt-2 pb-1">
<div className="space-y-2">
<p className="text-sm">{app.state.version}</p>
<p className="text-sm">{app.state.hasUpdate ? "有" : "无"}</p>
<p className="text-sm">使{app.state.useMirror ? "是" : "否"}</p>
<div className="flex items-center gap-2">
<p className="text-sm">{app.state.version}</p>
{app.state.hasUpdate && app.state.latestVersion && (
<Chip size="sm" color="success" variant="flat">
{app.state.latestVersion}
</Chip>
)}
</div>
{/* <p className="text-sm">是否有更新:{app.state.hasUpdate ? "有" : "无"}</p> */}
{/* <p className="text-sm">是否使用镜像源:{app.state.useMirror ? "是" : "否"}</p> */}
</div>
{/* <div className="w-full pt-4 border-t border-zinc-200 dark:border-zinc-800">
<div className="w-full pt-4 border-t border-zinc-200 dark:border-zinc-800">
<h3 className="mb-3 text-sm font-semibold"></h3>
<UpdateChecker customEndpoint={customEndpoint} githubRepo={githubRepo} />
</div> */}
<div className="mb-3 space-y-3">
{/* <Switch
isSelected={app.state.useMirror}
size="sm"
onChange={(e) => app.setUseMirror(e.target.checked)}
>
使用镜像源
</Switch> */}
{/* <p className="text-xs text-zinc-500">
{app.state.useMirror
? "使用自建更新服务检查更新"
: "使用 GitHub Release 检查更新"}
</p> */}
<Switch
isSelected={app.state.includePrerelease}
size="sm"
onChange={(e) => app.setIncludePrerelease(e.target.checked)}
>
</Switch>
<p className="text-xs text-zinc-500">
{app.state.includePrerelease
? "检查更新时会包含预发布版本beta、alpha等"
: "仅检查正式版本"}
</p>
</div>
<UpdateChecker
useMirror={app.state.useMirror}
customEndpoint={customEndpoint || undefined}
includePrerelease={app.state.includePrerelease}
/>
</div>
<div className="flex flex-col w-full pt-4 space-y-3 border-t border-zinc-200 dark:border-zinc-800">
<h3 className="mb-3 text-sm font-semibold"></h3>

View File

@@ -19,22 +19,21 @@ import {
Input,
} from "@heroui/react"
import { useState, useEffect, useRef, useCallback } from "react"
import {
TestTube,
Power,
Play,
Check,
Close,
Square,
DownloadOne,
List,
} from "@icon-park/react"
import { TestTube, Power, Play, Check, Close, Square, DownloadOne, List } from "@icon-park/react"
// 导入提取的模块
import { BENCHMARK_MAPS, TEST_TIMEOUT, PRESET_RESOLUTIONS } from "./FpsTest/constants"
import { parseVProfReport } from "./FpsTest/utils/vprof-parser"
import { compareTimestamps, formatCurrentTimestamp, timestampToISO } from "./FpsTest/utils/timestamp"
import {
compareTimestamps,
formatCurrentTimestamp,
timestampToISO,
} from "./FpsTest/utils/timestamp"
import { extractFpsMetrics } from "./FpsTest/utils/fps-metrics"
import { handleExportCSV, handleExportAverageCSV, formatVideoSettingSummary } from "./FpsTest/utils/csv-export"
import {
handleExportCSV,
handleExportAverageCSV,
formatVideoSettingSummary,
} from "./FpsTest/utils/csv-export"
import { useGameMonitor } from "./FpsTest/hooks/useGameMonitor"
import { useHardwareInfo } from "./FpsTest/hooks/useHardwareInfo"
import { NoteCell } from "./FpsTest/components/NoteCell"
@@ -94,9 +93,17 @@ export function FpsTest() {
// 记录测试开始时的视频设置
const testStartVideoSettingRef = useRef<typeof tool.state.videoSetting | null>(null)
// 记录当前测试的分辨率信息(用于备注)
const currentTestResolutionRef = useRef<{ width: string; height: string; label: string } | null>(null)
const currentTestResolutionRef = useRef<{ width: string; height: string; label: string } | null>(
null
)
// 记录当前分辨率在分辨率组中的索引和总测试次数(用于批量测试备注)
const currentResolutionGroupInfoRef = useRef<{ resIndex: number; totalResolutions: number; totalTestCount: number; currentBatchIndex: number; batchCount: number } | null>(null)
const currentResolutionGroupInfoRef = useRef<{
resIndex: number
totalResolutions: number
totalTestCount: number
currentBatchIndex: number
batchCount: number
} | null>(null)
// 记录最后一次测试的时间戳(用于平均值记录)
const lastTestTimestampRef = useRef<string | null>(null)
@@ -176,27 +183,31 @@ export function FpsTest() {
memory: hardwareInfo.systemInfo.total_memory
? Math.round(hardwareInfo.systemInfo.total_memory / 1024 / 1024 / 1024)
: null,
memoryManufacturer: hardwareInfo.memoryInfo && hardwareInfo.memoryInfo.length > 0
? hardwareInfo.memoryInfo[0].manufacturer || null
: null,
memorySpeed: hardwareInfo.memoryInfo && hardwareInfo.memoryInfo.length > 0
? hardwareInfo.memoryInfo[0].speed || null
: null,
memoryDefaultSpeed: hardwareInfo.memoryInfo && hardwareInfo.memoryInfo.length > 0
? hardwareInfo.memoryInfo[0].default_speed || null
: null,
gpu: hardwareInfo.gpuInfo
? hardwareInfo.gpuInfo.model
: null,
monitor: hardwareInfo.monitorInfo && hardwareInfo.monitorInfo.length > 0
? hardwareInfo.monitorInfo[0].name || null
: null,
monitorManufacturer: hardwareInfo.monitorInfo && hardwareInfo.monitorInfo.length > 0
? hardwareInfo.monitorInfo[0].manufacturer || null
: null,
monitorModel: hardwareInfo.monitorInfo && hardwareInfo.monitorInfo.length > 0
? hardwareInfo.monitorInfo[0].model || null
: null,
memoryManufacturer:
hardwareInfo.memoryInfo && hardwareInfo.memoryInfo.length > 0
? hardwareInfo.memoryInfo[0].manufacturer || null
: null,
memorySpeed:
hardwareInfo.memoryInfo && hardwareInfo.memoryInfo.length > 0
? hardwareInfo.memoryInfo[0].speed || null
: null,
memoryDefaultSpeed:
hardwareInfo.memoryInfo && hardwareInfo.memoryInfo.length > 0
? hardwareInfo.memoryInfo[0].default_speed || null
: null,
gpu: hardwareInfo.gpuInfo ? hardwareInfo.gpuInfo.model : null,
monitor:
hardwareInfo.monitorInfo && hardwareInfo.monitorInfo.length > 0
? hardwareInfo.monitorInfo[0].name || null
: null,
monitorManufacturer:
hardwareInfo.monitorInfo && hardwareInfo.monitorInfo.length > 0
? hardwareInfo.monitorInfo[0].manufacturer || null
: null,
monitorModel:
hardwareInfo.monitorInfo && hardwareInfo.monitorInfo.length > 0
? hardwareInfo.monitorInfo[0].model || null
: null,
motherboardModel: hardwareInfo.motherboardInfo?.model || null,
motherboardVersion: hardwareInfo.motherboardInfo?.version || null,
biosVersion: hardwareInfo.computerInfo?.BiosSMBIOSBIOSVersion || null,
@@ -223,9 +234,9 @@ export function FpsTest() {
} catch (error) {
console.error("获取控制台日志路径失败:", error)
if (!silent) {
addToast({
title: "获取控制台日志路径失败",
color: "warning"
addToast({
title: "获取控制台日志路径失败",
color: "warning",
})
}
return false
@@ -240,9 +251,9 @@ export function FpsTest() {
} catch (error) {
console.error("读取性能报告失败:", error)
if (!silent) {
addToast({
title: "读取性能报告失败",
color: "warning"
addToast({
title: "读取性能报告失败",
color: "warning",
})
}
return false
@@ -281,11 +292,6 @@ export function FpsTest() {
timeoutRef.current = null
}
// 测试结束后读取视频设置(检测分辨率)
if (steam.state.steamDirValid && steam.currentUser()) {
await tool.getVideoConfig(steam.state.steamDir, steam.currentUser()?.steam_id32 || 0)
}
// 提取 avg 和 p1 值
const { avg, p1 } = extractFpsMetrics(parsed.data)
@@ -294,8 +300,13 @@ export function FpsTest() {
const testDate = now.toISOString()
const mapConfig = BENCHMARK_MAPS[selectedMapIndex]
// 使用读取到的视频设置(测试结束后读取的
const currentVideoSetting = tool.store.state.videoSetting
// 测试结束时读取视频设置(检测分辨率
// 无论是自动监听还是手动读取,都在测试结束时读取当前的视频设置
let currentVideoSetting: typeof tool.state.videoSetting | null = null
if (steam.state.steamDirValid && steam.currentUser()) {
await tool.getVideoConfig(steam.state.steamDir, steam.currentUser()?.steam_id32 || 0)
currentVideoSetting = tool.store.state.videoSetting
}
// 如果是批量测试,保存结果到批量结果数组,否则直接保存
const currentBatchProgress = batchTestProgress
@@ -314,7 +325,7 @@ export function FpsTest() {
if (testNote) {
batchNote = batchNote ? `${testNote} ${batchNote}` : testNote
}
// 如果启用了分辨率组,使用新的备注格式:[分辨率] [批量当前测试/该分辨率批量总数]
if (resolutionGroupInfo && isResolutionGroupEnabled) {
const { currentBatchIndex, batchCount } = resolutionGroupInfo
@@ -504,18 +515,18 @@ export function FpsTest() {
): Promise<boolean> => {
// 验证路径是否存在且有效
if (!steam.state.steamDir || !steam.state.cs2Dir) {
addToast({
title: "Steam 或 CS2 路径未设置,请先配置路径",
color: "warning"
addToast({
title: "Steam 或 CS2 路径未设置,请先配置路径",
color: "warning",
})
return false
}
// 验证 Steam 路径是否有效
if (!steam.state.steamDirValid) {
addToast({
title: "Steam 路径无效,请检查路径设置",
color: "warning"
addToast({
title: "Steam 路径无效,请检查路径设置",
color: "warning",
})
return false
}
@@ -553,6 +564,12 @@ export function FpsTest() {
testStartTimestampRef.current = `${month}/${day} ${hour}:${minute}:${second}`
testStartTimeRef.current = now.getTime()
// 在测试开始时读取并记录画面设置
if (steam.state.steamDirValid && steam.currentUser()) {
await tool.getVideoConfig(steam.state.steamDir, steam.currentUser()?.steam_id32 || 0)
testStartVideoSettingRef.current = tool.store.state.videoSetting
}
try {
// 构建启动参数:基础参数 + 分辨率和全屏设置 + 自定义启动项(如果有)
let baseLaunchOption = `-allow_third_party_software -condebug -conclearlog +map_workshop ${mapConfig.workshopId} ${mapConfig.map}`
@@ -593,9 +610,9 @@ export function FpsTest() {
})
} catch (error) {
console.error("启动游戏失败:", error)
addToast({
title: `启动游戏失败: ${error instanceof Error ? error.message : String(error)}`,
color: "danger"
addToast({
title: `启动游戏失败: ${error instanceof Error ? error.message : String(error)}`,
color: "danger",
})
return false
}
@@ -758,7 +775,14 @@ export function FpsTest() {
if (validResults.length > 0) {
const avgAvg =
validResults.reduce((sum, r) => sum + (r.avg || 0), 0) / validResults.length
const avgP1 = validResults.reduce((sum, r) => sum + (r.p1 || 0), 0) / validResults.length
const avgP1 =
validResults.reduce((sum, r) => sum + (r.p1 || 0), 0) / validResults.length
// 测试结束后读取视频设置(检测分辨率)
if (steam.state.steamDirValid && steam.currentUser()) {
await tool.getVideoConfig(steam.state.steamDir, steam.currentUser()?.steam_id32 || 0)
}
const currentVideoSetting = tool.store.state.videoSetting
// 使用最后一次测试的时间戳作为平均值记录的时间戳(更准确)
// 如果最后一次测试时间戳存在,使用它;否则使用当前时间
@@ -799,9 +823,7 @@ export function FpsTest() {
averageNote = `${averageNote} [批量${totalTests}次平均]`
// 生成唯一ID
const idTime = lastTestTimestampRef.current
? new Date(testDate).getTime()
: Date.now()
const idTime = lastTestTimestampRef.current ? new Date(testDate).getTime() : Date.now()
fpsTest.addResult({
id: `${idTime}-${Math.random().toString(36).slice(2, 11)}`,
testTime,
@@ -810,10 +832,12 @@ export function FpsTest() {
mapLabel: mapConfig?.label || "未知地图",
avg: avgAvg,
p1: avgP1,
rawResult: `分辨率${currentResolution.label}批量测试${totalTests}次平均值\n平均帧: ${avgAvg.toFixed(
rawResult: `分辨率${
currentResolution.label
}批量测试${totalTests}次平均值\n平均帧: ${avgAvg.toFixed(1)}\nP1低帧: ${avgP1.toFixed(
1
)}\nP1低帧: ${avgP1.toFixed(1)}`,
videoSetting: tool.store.state.videoSetting,
)}`,
videoSetting: currentVideoSetting,
hardwareInfo: getHardwareInfoObject(),
note: averageNote,
})
@@ -874,6 +898,12 @@ export function FpsTest() {
validResults.reduce((sum, r) => sum + (r.avg || 0), 0) / validResults.length
const avgP1 = validResults.reduce((sum, r) => sum + (r.p1 || 0), 0) / validResults.length
// 测试结束后读取视频设置(检测分辨率)
if (steam.state.steamDirValid && steam.currentUser()) {
await tool.getVideoConfig(steam.state.steamDir, steam.currentUser()?.steam_id32 || 0)
}
const currentVideoSetting = tool.store.state.videoSetting
// 使用最后一次测试的时间戳作为平均值记录的时间戳(更准确)
let testTime: string
let testDate: string
@@ -909,9 +939,7 @@ export function FpsTest() {
: `[批量${totalTests}次平均]`
// 生成唯一ID
const idTime = lastTestTimestampRef.current
? new Date(testDate).getTime()
: Date.now()
const idTime = lastTestTimestampRef.current ? new Date(testDate).getTime() : Date.now()
fpsTest.addResult({
id: `${idTime}-${Math.random().toString(36).slice(2, 11)}`,
testTime,
@@ -923,7 +951,7 @@ export function FpsTest() {
rawResult: `批量测试${totalTests}次平均值\n平均帧: ${avgAvg.toFixed(
1
)}\nP1低帧: ${avgP1.toFixed(1)}`,
videoSetting: tool.store.state.videoSetting,
videoSetting: currentVideoSetting,
hardwareInfo: getHardwareInfoObject(),
note: averageNote,
})
@@ -999,11 +1027,21 @@ export function FpsTest() {
<BatchTestProgress progress={batchTestProgress} />
{showResultsTable && (
<>
<Button size="sm" variant="flat" onPress={handleExportAverageCSVWrapper} className="font-medium">
<Button
size="sm"
variant="flat"
onPress={handleExportAverageCSVWrapper}
className="font-medium"
>
<DownloadOne size={14} />
</Button>
<Button size="sm" variant="flat" onPress={handleExportCSVWrapper} className="font-medium">
<Button
size="sm"
variant="flat"
onPress={handleExportCSVWrapper}
className="font-medium"
>
<DownloadOne size={14} />
CSV
</Button>
@@ -1221,4 +1259,3 @@ export function FpsTest() {
</Card>
)
}

View File

@@ -145,10 +145,10 @@ export function ResolutionConfig({
</div>
</div>
{/* 主体:宽高输入框 + 全屏按钮(始终显示) */}
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 ">
<div className="flex items-center gap-1">
<Input
size="sm"
size="md"
type="number"
placeholder="宽"
value={resolutionWidth}
@@ -162,7 +162,7 @@ export function ResolutionConfig({
/>
<span className="text-xs text-default-400">x</span>
<Input
size="sm"
size="md"
type="number"
placeholder="高"
value={resolutionHeight}
@@ -176,7 +176,7 @@ export function ResolutionConfig({
/>
</div>
<Button
size="sm"
size="md"
variant={isFullscreen ? "solid" : "flat"}
color={isFullscreen ? "primary" : "default"}
onPress={() => fpsTest.setIsFullscreen(!isFullscreen)}

View File

@@ -88,24 +88,6 @@ export function TestConfigPanel({
</div>
</div>
</div>
{/* 启动项占满一行,右侧放置分辨率和全屏切换 */}
<div className="flex items-start gap-4">
{/* 自定义启动项 */}
<div className="flex flex-col flex-1 gap-1.5">
<label className="h-5 text-xs text-default-500"></label>
<div className="flex items-center gap-2">
<Input
size="md"
placeholder="输入自定义启动参数(可选)"
value={customLaunchOption}
onValueChange={onCustomLaunchOptionChange}
isDisabled={isMonitoring}
className="flex-1"
/>
</div>
</div>
</div>
</>
)
}

View File

@@ -11,7 +11,6 @@ import {
import { Delete } from "@icon-park/react"
import { addToast } from "@heroui/react"
import { NoteCell } from "./NoteCell"
import { formatVideoSettingSummary } from "../utils/csv-export"
import type { FpsTestResult } from "@/store/fps_test"
import type { useFpsTestStore } from "@/store/fps_test"
@@ -42,17 +41,17 @@ export function TestResultsTable({
<TableHeader>
<TableColumn minWidth={140}></TableColumn>
<TableColumn width={40}></TableColumn>
<TableColumn width={100}></TableColumn>
<TableColumn width={60}></TableColumn>
<TableColumn width={60}>P1低帧</TableColumn>
<TableColumn width={100}>CPU</TableColumn>
<TableColumn minWidth={80}></TableColumn>
<TableColumn minWidth={100}>GPU</TableColumn>
<TableColumn width={80}></TableColumn>
<TableColumn width={80}></TableColumn>
<TableColumn minWidth={80}></TableColumn>
<TableColumn width={100}></TableColumn>
<TableColumn minWidth={80}></TableColumn>
<TableColumn minWidth={80}>BIOS版本</TableColumn>
<TableColumn width={120}></TableColumn>
<TableColumn minWidth={40}></TableColumn>
<TableColumn width={60} align="center">
@@ -67,6 +66,11 @@ export function TestResultsTable({
<TableCell className="text-xs">
<div className="truncate">{result.mapLabel}</div>
</TableCell>
<TableCell className="text-xs whitespace-nowrap">
{result.videoSetting
? `${result.videoSetting.defaultres}x${result.videoSetting.defaultresheight}`
: "N/A"}
</TableCell>
<TableCell className="text-xs">
{result.avg !== null ? `${result.avg.toFixed(1)}` : "N/A"}
</TableCell>
@@ -84,9 +88,6 @@ export function TestResultsTable({
</div>
</Tooltip>
</TableCell>
<TableCell className="text-xs">
<div className="truncate">{result.hardwareInfo?.os || "N/A"}</div>
</TableCell>
<TableCell className="text-xs">
<div className="truncate">{result.hardwareInfo?.gpu || "N/A"}</div>
</TableCell>
@@ -100,6 +101,9 @@ export function TestResultsTable({
? `${result.hardwareInfo.memorySpeed}MHz`
: "N/A"}
</TableCell>
<TableCell className="text-xs">
<div className="truncate">{result.hardwareInfo?.os || "N/A"}</div>
</TableCell>
<TableCell className="text-xs">
<Tooltip
content={result.hardwareInfo?.motherboardModel || "N/A"}
@@ -117,15 +121,6 @@ export function TestResultsTable({
<TableCell className="text-xs">
<div className="truncate">{result.hardwareInfo?.biosVersion || "N/A"}</div>
</TableCell>
<TableCell className="text-xs">
<Tooltip content={formatVideoSettingSummary(result.videoSetting)}>
<span className="truncate cursor-help">
{result.videoSetting
? `${result.videoSetting.defaultres}x${result.videoSetting.defaultresheight}`
: "N/A"}
</span>
</Tooltip>
</TableCell>
<TableCell className="text-xs min-w-fit">
<NoteCell
note={result.note || ""}

View File

@@ -173,8 +173,16 @@ export async function readResult(
memory: hardwareInfo.total_memory
? Math.round(hardwareInfo.total_memory / 1024 / 1024 / 1024)
: null,
memoryManufacturer: null,
memorySpeed: null,
memoryDefaultSpeed: null,
gpu: null,
monitor: null,
monitorManufacturer: null,
monitorModel: null,
motherboardModel: null,
motherboardVersion: null,
biosVersion: null,
}
: null,
note: batchNote,
@@ -208,8 +216,16 @@ export async function readResult(
memory: hardwareInfo.total_memory
? Math.round(hardwareInfo.total_memory / 1024 / 1024 / 1024)
: null,
memoryManufacturer: null,
memorySpeed: null,
memoryDefaultSpeed: null,
gpu: null,
monitor: null,
monitorManufacturer: null,
monitorModel: null,
motherboardModel: null,
motherboardVersion: null,
biosVersion: null,
}
: null,
note: singleNote, // 保存备注(包含分辨率信息)

View File

@@ -45,6 +45,17 @@ export async function handleExportCSV(
"分辨率",
"视频设置",
"备注",
"光影质量",
"纹理过滤质量",
"多重采样抗锯齿",
"CMAA抗锯齿",
"阴影质量",
"动态阴影",
"纹理细节",
"粒子细节",
"环境光遮蔽",
"HDR细节",
"FSR细节",
]
const csvRows = [headers.join(",")]
@@ -68,6 +79,17 @@ export async function handleExportCSV(
: "N/A",
`"${formatVideoSettingSummary(result.videoSetting)}"`,
`"${result.note || ""}"`,
result.videoSetting?.shaderquality || "N/A",
result.videoSetting?.r_texturefilteringquality || "N/A",
result.videoSetting?.msaa_samples || "N/A",
result.videoSetting?.r_csgo_cmaa_enable || "N/A",
result.videoSetting?.videocfg_shadow_quality || "N/A",
result.videoSetting?.videocfg_dynamic_shadows || "N/A",
result.videoSetting?.videocfg_texture_detail || "N/A",
result.videoSetting?.videocfg_particle_detail || "N/A",
result.videoSetting?.videocfg_ao_detail || "N/A",
result.videoSetting?.videocfg_hdr_detail || "N/A",
result.videoSetting?.videocfg_fsr_detail || "N/A",
]
csvRows.push(row.join(","))
}
@@ -133,6 +155,17 @@ export async function handleExportAverageCSV(
"分辨率",
"视频设置",
"备注",
"光影质量",
"纹理过滤质量",
"多重采样抗锯齿",
"CMAA抗锯齿",
"阴影质量",
"动态阴影",
"纹理细节",
"粒子细节",
"环境光遮蔽",
"HDR细节",
"FSR细节",
]
const csvRows = [headers.join(",")]
@@ -156,6 +189,17 @@ export async function handleExportAverageCSV(
: "N/A",
`"${formatVideoSettingSummary(result.videoSetting)}"`,
`"${result.note || ""}"`,
result.videoSetting?.shaderquality || "N/A",
result.videoSetting?.r_texturefilteringquality || "N/A",
result.videoSetting?.msaa_samples || "N/A",
result.videoSetting?.r_csgo_cmaa_enable || "N/A",
result.videoSetting?.videocfg_shadow_quality || "N/A",
result.videoSetting?.videocfg_dynamic_shadows || "N/A",
result.videoSetting?.videocfg_texture_detail || "N/A",
result.videoSetting?.videocfg_particle_detail || "N/A",
result.videoSetting?.videocfg_ao_detail || "N/A",
result.videoSetting?.videocfg_hdr_detail || "N/A",
result.videoSetting?.videocfg_fsr_detail || "N/A",
]
csvRows.push(row.join(","))
}

View File

@@ -1,34 +1,54 @@
"use client"
import { useState } from "react"
import { Button, Progress, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/react"
import { Download, Refresh, CheckCorrect } from "@icon-park/react"
import { useState, useEffect } from "react"
import { Button, CircularProgress, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/react"
import { Download, Refresh, FileText, Close, Check } from "@icon-park/react"
import { invoke } from "@tauri-apps/api/core"
import { listen } from "@tauri-apps/api/event"
import { relaunch } from "@tauri-apps/plugin-process"
import { addToast } from "@heroui/react"
import { useAppStore } from "@/store/app"
import { MarkdownRender } from "@/components/markdown"
interface UpdateInfo {
version: string
notes?: string
pub_date?: string
download_url: string
signature?: string
}
interface UpdateCheckerProps {
useMirror?: boolean
customEndpoint?: string
githubRepo?: string
includePrerelease?: boolean
}
export function UpdateChecker({ customEndpoint, githubRepo }: UpdateCheckerProps) {
export function UpdateChecker({ useMirror = true, customEndpoint, includePrerelease = false }: UpdateCheckerProps) {
const app = useAppStore()
const [checking, setChecking] = useState(false)
const [downloading, setDownloading] = useState(false)
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null)
const [downloadProgress, setDownloadProgress] = useState(0)
const [installerPath, setInstallerPath] = useState<string | null>(null)
const { isOpen, onOpen, onOpenChange } = useDisclosure()
const [downloadCompleted, setDownloadCompleted] = useState(false)
const { isOpen: isChangelogOpen, onOpen: onChangelogOpen, onOpenChange: onChangelogOpenChange } = useDisclosure()
// 监听下载进度事件
useEffect(() => {
const unlisten = listen<number>("update-download-progress", (event) => {
const progress = event.payload
setDownloadProgress(progress)
// 如果进度达到 100%,标记下载完成
if (progress === 100) {
setDownloading(false)
setDownloadCompleted(true)
}
})
return () => {
unlisten.then(fn => fn())
}
}, [])
// 检查更新
const handleCheckUpdate = async () => {
@@ -36,24 +56,32 @@ export function UpdateChecker({ customEndpoint, githubRepo }: UpdateCheckerProps
setUpdateInfo(null)
setDownloadProgress(0)
setInstallerPath(null)
setDownloading(false)
setDownloadCompleted(false)
try {
// 如果有自定义端点使用自定义端点否则使用默认端点GitHub Release 或镜像源)
const endpoint = customEndpoint || null
const result = await invoke<UpdateInfo | null>("check_app_update", {
customEndpoint: customEndpoint || null,
githubRepo: githubRepo || null,
endpoint: endpoint,
useMirror: useMirror,
includePrerelease: includePrerelease,
})
if (result) {
setUpdateInfo(result)
// 更新 store 中的更新状态和最新版本号
app.setHasUpdate(true)
onOpen()
app.setLatestVersion(result.version)
addToast({
title: "发现新版本",
description: `版本 ${result.version} 可用`,
color: "success",
})
} else {
// 没有更新,更新 store 状态
app.setHasUpdate(false)
app.setLatestVersion("")
addToast({
title: "已是最新版本",
color: "default",
@@ -77,30 +105,66 @@ export function UpdateChecker({ customEndpoint, githubRepo }: UpdateCheckerProps
setDownloading(true)
setDownloadProgress(0)
setDownloadCompleted(false)
try {
// 注意:这里没有实现进度回调,实际项目中可以使用事件监听
// 如果使用镜像源,给下载链接前面套一个 CDN 加速
let downloadUrl = updateInfo.download_url
if (useMirror) {
downloadUrl = `https://cdn.upup.cool/${downloadUrl}`
}
// 打印最终下载链接
console.log("[下载更新] 最终下载链接:", downloadUrl)
const path = await invoke<string>("download_app_update", {
downloadUrl: updateInfo.download_url,
downloadUrl: downloadUrl,
})
setInstallerPath(path)
setDownloadProgress(100)
setDownloading(false)
setDownloadCompleted(true)
addToast({
title: "下载完成",
description: "准备安装更新",
description: "可以点击安装按钮进行安装",
color: "success",
})
} catch (error) {
console.error("下载更新失败:", error)
addToast({
title: "下载失败",
description: String(error),
color: "danger",
})
const errorMsg = String(error)
if (errorMsg.includes("取消")) {
addToast({
title: "下载已取消",
color: "default",
})
} else {
addToast({
title: "下载失败",
description: errorMsg,
color: "danger",
})
}
setDownloadProgress(0)
} finally {
setDownloading(false)
setDownloadCompleted(false)
}
}
// 取消下载
const handleCancelDownload = async () => {
try {
await invoke("cancel_download_update")
setDownloading(false)
setDownloadProgress(0)
setDownloadCompleted(false)
addToast({
title: "已取消下载",
color: "default",
})
} catch (error) {
console.error("取消下载失败:", error)
}
}
@@ -119,7 +183,6 @@ export function UpdateChecker({ customEndpoint, githubRepo }: UpdateCheckerProps
color: "success",
})
// 等待一小段时间后重启
setTimeout(async () => {
await relaunch()
}, 1000)
@@ -133,18 +196,6 @@ export function UpdateChecker({ customEndpoint, githubRepo }: UpdateCheckerProps
}
}
// 格式化更新说明Markdown 转 HTML
const formatNotes = (notes?: string) => {
if (!notes) return "无更新说明"
// 简单的 Markdown 处理:换行
return notes.split("\n").map((line, i) => (
<span key={i}>
{line}
<br />
</span>
))
}
return (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
@@ -155,79 +206,112 @@ export function UpdateChecker({ customEndpoint, githubRepo }: UpdateCheckerProps
startContent={checking ? undefined : <Refresh />}
isLoading={checking}
onPress={handleCheckUpdate}
className="w-fit"
>
{checking ? "检查中..." : "检查更新"}
</Button>
{app.state.hasUpdate && (
<span className="flex items-center gap-1 text-sm text-green-600 dark:text-green-400">
<CheckCorrect size={16} />
</span>
{updateInfo && (
<>
{!downloading && !installerPath && (
<Button
size="sm"
color="primary"
variant="flat"
startContent={<Download />}
onPress={handleDownloadUpdate}
className="w-fit"
>
</Button>
)}
{downloading && (
<Button
size="sm"
color="danger"
variant="flat"
startContent={<Close />}
onPress={handleCancelDownload}
className="w-fit"
>
</Button>
)}
{installerPath && (
<Button
size="sm"
color="primary"
variant="flat"
onPress={handleInstallUpdate}
className="w-fit"
>
</Button>
)}
<Button
size="sm"
color="default"
variant="flat"
startContent={<FileText />}
onPress={onChangelogOpen}
className="w-fit"
>
</Button>
{(downloading || downloadProgress > 0 || downloadCompleted) && (
<div className="flex items-center gap-1">
{downloadCompleted ? (
<>
<Check className="text-green-500 dark:text-green-400" size={14} />
<span className="text-xs text-green-500 dark:text-green-400">
</span>
</>
) : (
<>
<CircularProgress
aria-label="下载进度"
value={downloadProgress}
color="primary"
size="sm"
showValueLabel={false}
/>
<span className="text-xs text-zinc-500 dark:text-zinc-400">
{downloadProgress}%
</span>
</>
)}
</div>
)}
</>
)}
</div>
{downloading && (
<div className="flex flex-col gap-2">
<Progress
aria-label="下载进度"
value={downloadProgress}
color="primary"
showValueLabel
className="max-w-full"
/>
<p className="text-xs text-zinc-500">...</p>
</div>
)}
<Modal isOpen={isOpen} onOpenChange={onOpenChange} size="lg">
{/* 更新日志对话框 */}
<Modal isOpen={isChangelogOpen} onOpenChange={onChangelogOpenChange} size="lg">
<ModalContent>
{(onClose) => (
<>
<ModalHeader className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<span></span>
<span className="text-sm font-normal text-zinc-500">v{updateInfo?.version}</span>
</div>
<span> v{updateInfo?.version}</span>
</ModalHeader>
<ModalBody>
<div className="space-y-3">
<div>
<p className="mb-1 text-sm font-semibold"></p>
<div className="text-sm whitespace-pre-wrap text-zinc-600 dark:text-zinc-400">
{formatNotes(updateInfo?.notes)}
</div>
{updateInfo?.notes ? (
<div className="text-sm text-zinc-600 dark:text-zinc-400">
<MarkdownRender>{updateInfo.notes}</MarkdownRender>
</div>
{updateInfo?.pub_date && (
<p className="text-xs text-zinc-500">{new Date(updateInfo.pub_date).toLocaleString("zh-CN")}</p>
)}
</div>
) : (
<p className="text-sm text-zinc-500"></p>
)}
</ModalBody>
<ModalFooter>
<Button color="danger" variant="light" onPress={onClose}>
<Button color="primary" variant="flat" onPress={onClose}>
</Button>
{!downloading && !installerPath && (
<Button
color="primary"
startContent={<Download />}
onPress={async () => {
await handleDownloadUpdate()
}}
>
</Button>
)}
{installerPath && (
<Button
color="primary"
onPress={async () => {
await handleInstallUpdate()
onClose()
}}
>
</Button>
)}
</ModalFooter>
</>
)}

View File

@@ -7,9 +7,11 @@ import { LazyStore } from '@tauri-apps/plugin-store';
const defaultValue = {
version: "0.0.1",
hasUpdate: false,
latestVersion: "", // 最新版本号
inited: false,
notice: "",
useMirror: true,
useMirror: true, // 默认使用镜像源CDN 加速)
includePrerelease: false, // 默认不包含预发布版本
autoStart: false,
startHidden: false,
hiddenOnClose: false,
@@ -27,9 +29,11 @@ export const useAppStore = () => {
store: appStore,
setVersion,
setHasUpdate,
setLatestVersion,
setInited,
setNotice,
setUseMirror,
setIncludePrerelease,
setAutoStart,
setStartHidden,
setHiddenOnClose,
@@ -47,6 +51,9 @@ const setVersion = (version: string) => {
const setHasUpdate = (hasUpdate: boolean) => {
appStore.state.hasUpdate = hasUpdate
}
const setLatestVersion = (latestVersion: string) => {
appStore.state.latestVersion = latestVersion
}
const setInited = (inited: boolean) => {
appStore.state.inited = inited
}
@@ -56,6 +63,9 @@ const setNotice = (notice: string) => {
const setUseMirror = (useMirror: boolean) => {
appStore.state.useMirror = useMirror
}
const setIncludePrerelease = (includePrerelease: boolean) => {
appStore.state.includePrerelease = includePrerelease
}
const setAutoStart = (autoStart: boolean) => {
if (autoStart) {
@@ -84,9 +94,11 @@ const setSteamUsersViewMode = (viewMode: "card" | "list" | "list-large") => {
const resetAppStore = () => {
setVersion(defaultValue.version)
setHasUpdate(defaultValue.hasUpdate)
setLatestVersion(defaultValue.latestVersion)
setInited(defaultValue.inited)
setNotice(defaultValue.notice)
setUseMirror(defaultValue.useMirror)
setIncludePrerelease(defaultValue.includePrerelease)
setAutoStart(defaultValue.autoStart)
void setStartHidden(defaultValue.startHidden)
setHiddenOnClose(defaultValue.hiddenOnClose)