-
版本号:{app.state.version}
-
是否有更新:{app.state.hasUpdate ? "有" : "无"}
-
是否使用镜像源:{app.state.useMirror ? "是" : "否"}
-
app.setAutoStart(e.target.checked)}
- >
- 开机自启动 {app.state.autoStart ? "开" : "关"}
-
-
app.setStartHidden(e.target.checked)}
- >
- 静默启动 {app.state.startHidden ? "开" : "关"}
-
- {/* hiddenOnClose */}
-
app.setHiddenOnClose(e.target.checked)}
- >
- 关闭时最小化到托盘 {app.state.hiddenOnClose ? "开" : "关"}
-
+
+
+
版本号:{app.state.version}
+
是否有更新:{app.state.hasUpdate ? "有" : "无"}
+
是否使用镜像源:{app.state.useMirror ? "是" : "否"}
+
+
+
+
更新检查
+
+
+
+
+
启动设置
+ app.setAutoStart(e.target.checked)}
+ >
+ 开机自启动 {app.state.autoStart ? "开" : "关"}
+
+ app.setStartHidden(e.target.checked)}
+ >
+ 静默启动 {app.state.startHidden ? "开" : "关"}
+
+ app.setHiddenOnClose(e.target.checked)}
+ >
+ 关闭时最小化到托盘 {app.state.hiddenOnClose ? "开" : "关"}
+
+
)
}
diff --git a/src/app/providers.tsx b/src/app/providers.tsx
index 572b6c2..7147359 100644
--- a/src/app/providers.tsx
+++ b/src/app/providers.tsx
@@ -4,6 +4,7 @@ import { ToastProvider } from "@heroui/toast"
import { platform } from "@tauri-apps/plugin-os"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import { useEffect, useState } from "react"
+import { AuthProvider } from "@/components/auth/AuthProvider"
export default function Providers({ children }: { children: React.ReactNode }) {
const [os, setOs] = useState("windows")
@@ -17,7 +18,7 @@ export default function Providers({ children }: { children: React.ReactNode }) {
>
- {children}
+ {children}
)
diff --git a/src/components/cstb/FpsTest.tsx b/src/components/cstb/FpsTest.tsx
index 314e356..d8fe9ad 100644
--- a/src/components/cstb/FpsTest.tsx
+++ b/src/components/cstb/FpsTest.tsx
@@ -11,7 +11,7 @@ const BENCHMARK_MAPS = [
name: "de_dust2_benchmark",
workshopId: "3240880604",
map: "de_dust2_benchmark",
- label: "Dust2 Benchmark",
+ label: "Dust2",
},
{
name: "de_ancient",
diff --git a/src/components/cstb/UpdateChecker.tsx b/src/components/cstb/UpdateChecker.tsx
new file mode 100644
index 0000000..5097320
--- /dev/null
+++ b/src/components/cstb/UpdateChecker.tsx
@@ -0,0 +1,238 @@
+"use client"
+
+import { useState } from "react"
+import { Button, Progress, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/react"
+import { Download, Refresh, CheckCircle } from "@icon-park/react"
+import { invoke } from "@tauri-apps/api/core"
+import { relaunch } from "@tauri-apps/api/process"
+import { addToast } from "@heroui/react"
+import { useAppStore } from "@/store/app"
+
+interface UpdateInfo {
+ version: string
+ notes?: string
+ pub_date?: string
+ download_url: string
+ signature?: string
+}
+
+interface UpdateCheckerProps {
+ customEndpoint?: string
+ githubRepo?: string
+}
+
+export function UpdateChecker({ customEndpoint, githubRepo }: UpdateCheckerProps) {
+ const app = useAppStore()
+ const [checking, setChecking] = useState(false)
+ const [downloading, setDownloading] = useState(false)
+ const [updateInfo, setUpdateInfo] = useState
(null)
+ const [downloadProgress, setDownloadProgress] = useState(0)
+ const [installerPath, setInstallerPath] = useState(null)
+ const { isOpen, onOpen, onOpenChange } = useDisclosure()
+
+ // 检查更新
+ const handleCheckUpdate = async () => {
+ setChecking(true)
+ setUpdateInfo(null)
+ setDownloadProgress(0)
+ setInstallerPath(null)
+
+ try {
+ const result = await invoke("check_app_update", {
+ customEndpoint: customEndpoint || null,
+ githubRepo: githubRepo || null,
+ })
+
+ if (result) {
+ setUpdateInfo(result)
+ app.setHasUpdate(true)
+ onOpen()
+ addToast({
+ title: "发现新版本",
+ description: `版本 ${result.version} 可用`,
+ color: "success",
+ })
+ } else {
+ app.setHasUpdate(false)
+ addToast({
+ title: "已是最新版本",
+ color: "default",
+ })
+ }
+ } catch (error) {
+ console.error("检查更新失败:", error)
+ addToast({
+ title: "检查更新失败",
+ description: String(error),
+ color: "danger",
+ })
+ } finally {
+ setChecking(false)
+ }
+ }
+
+ // 下载更新
+ const handleDownloadUpdate = async () => {
+ if (!updateInfo) return
+
+ setDownloading(true)
+ setDownloadProgress(0)
+
+ try {
+ // 注意:这里没有实现进度回调,实际项目中可以使用事件监听
+ const path = await invoke("download_app_update", {
+ downloadUrl: updateInfo.download_url,
+ })
+
+ setInstallerPath(path)
+ setDownloadProgress(100)
+ addToast({
+ title: "下载完成",
+ description: "准备安装更新",
+ color: "success",
+ })
+ } catch (error) {
+ console.error("下载更新失败:", error)
+ addToast({
+ title: "下载失败",
+ description: String(error),
+ color: "danger",
+ })
+ setDownloadProgress(0)
+ } finally {
+ setDownloading(false)
+ }
+ }
+
+ // 安装更新
+ const handleInstallUpdate = async () => {
+ if (!installerPath) return
+
+ try {
+ await invoke("install_app_update", {
+ installerPath: installerPath,
+ })
+
+ addToast({
+ title: "安装已启动",
+ description: "应用将在安装完成后重启",
+ color: "success",
+ })
+
+ // 等待一小段时间后重启
+ setTimeout(async () => {
+ await relaunch()
+ }, 1000)
+ } catch (error) {
+ console.error("安装更新失败:", error)
+ addToast({
+ title: "安装失败",
+ description: String(error),
+ color: "danger",
+ })
+ }
+ }
+
+ // 格式化更新说明(Markdown 转 HTML)
+ const formatNotes = (notes?: string) => {
+ if (!notes) return "无更新说明"
+ // 简单的 Markdown 处理:换行
+ return notes.split("\n").map((line, i) => (
+
+ {line}
+
+
+ ))
+ }
+
+ return (
+
+
+ }
+ isLoading={checking}
+ onPress={handleCheckUpdate}
+ >
+ {checking ? "检查中..." : "检查更新"}
+
+ {app.state.hasUpdate && (
+
+
+ 有新版本可用
+
+ )}
+
+
+ {downloading && (
+
+ )}
+
+
+
+ {(onClose) => (
+ <>
+
+
+ 发现新版本
+ v{updateInfo?.version}
+
+
+
+
+
+
更新说明:
+
+ {formatNotes(updateInfo?.notes)}
+
+
+ {updateInfo?.pub_date && (
+
发布时间:{new Date(updateInfo.pub_date).toLocaleString("zh-CN")}
+ )}
+
+
+
+
+ {!downloading && !installerPath && (
+ }
+ onPress={async () => {
+ await handleDownloadUpdate()
+ }}
+ >
+ 下载更新
+
+ )}
+ {installerPath && (
+
+ )}
+
+ >
+ )}
+
+
+
+ )
+}
diff --git a/src/components/cstb/VideoSetting.tsx b/src/components/cstb/VideoSetting.tsx
index 252f99a..376d5c4 100644
--- a/src/components/cstb/VideoSetting.tsx
+++ b/src/components/cstb/VideoSetting.tsx
@@ -1,18 +1,47 @@
import { CloseSmall, Down, Edit, Plus, SettingConfig, Up } from "@icon-park/react"
-import { useEffect, useState } from "react"
+import { useEffect, useState, useCallback } from "react"
import { Card, CardBody, CardHeader, CardIcon, CardTool } from "../window/Card"
import { ToolButton } from "../window/ToolButton"
-import { addToast, NumberInput, Tab, Tabs, Tooltip } from "@heroui/react"
+import { addToast, NumberInput, Tab, Tabs, Tooltip, Chip } from "@heroui/react"
import { motion } from "framer-motion"
import { useToolStore, VideoSetting as VideoConfig, VideoSettingTemplate } from "@/store/tool"
import { useSteamStore } from "@/store/steam"
-import { useDebounce } from "ahooks"
+import { useDebounce, useDebounceFn } from "ahooks"
+import { invoke } from "@tauri-apps/api/core"
const VideoSetting = () => {
const [hide, setHide] = useState(false)
const [edit, setEdit] = useState(false)
+ const [isGameRunning, setIsGameRunning] = useState(false)
const tool = useToolStore()
const steam = useSteamStore()
+
+ // 检测游戏是否运行
+ const checkGameRunning = useCallback(async () => {
+ try {
+ // 尝试检测cs2.exe进程
+ const result = await invoke("check_process_running", { processName: "cs2.exe" }).catch(() => false)
+ setIsGameRunning(result)
+ return result
+ } catch {
+ // 如果命令不存在,使用简单的检测方法
+ setIsGameRunning(false)
+ return false
+ }
+ }, [])
+
+ // 防抖的读取函数
+ const { run: debouncedGetVideoConfig } = useDebounceFn(
+ async () => {
+ if (steam.state.steamDirValid && steam.currentUser()) {
+ await tool.getVideoConfig(steam.state.steamDir, steam.currentUser()?.steam_id32 || 0)
+ addToast({ title: "读取成功" })
+ } else {
+ addToast({ title: "请先选择用户", color: "danger" })
+ }
+ },
+ { wait: 500, leading: true, trailing: false }
+ )
const videoSettings = (video: VideoConfig) => {
return [
{
@@ -249,6 +278,16 @@ const VideoSetting = () => {
const [vconfig, setVconfig] = useState(tool.state.videoSetting)
+ // 初始化时检测游戏运行状态
+ useEffect(() => {
+ void checkGameRunning()
+ // 定期检测游戏运行状态
+ const interval = setInterval(() => {
+ void checkGameRunning()
+ }, 2000)
+ return () => clearInterval(interval)
+ }, [checkGameRunning])
+
useEffect(() => {
if (steam.state.steamDirValid && steam.currentUser())
void tool.getVideoConfig(steam.state.steamDir, steam.currentUser()?.steam_id32 || 0)
@@ -270,6 +309,16 @@ const VideoSetting = () => {
视频设置
+ {isGameRunning && (
+
+ 游戏运行中
+
+ )}
{/* {tool.state.VideoSettings.map((option, index) => (
@@ -302,6 +351,16 @@ const VideoSetting = () => {
{
+ // 检查游戏是否运行
+ const gameRunning = await checkGameRunning()
+ if (gameRunning) {
+ addToast({
+ title: "无法应用设置",
+ description: "检测到游戏正在运行,请先关闭游戏后再应用设置",
+ color: "warning"
+ })
+ return
+ }
await tool.setVideoConfig(
steam.state.steamDir,
steam.currentUser()?.steam_id32 || 0,
@@ -314,6 +373,7 @@ const VideoSetting = () => {
setEdit(false)
addToast({ title: "应用设置成功" })
}}
+ isDisabled={isGameRunning}
>
应用
@@ -345,15 +405,7 @@ const VideoSetting = () => {
{
- if (steam.state.steamDirValid && steam.currentUser()) {
- await tool.getVideoConfig(
- steam.state.steamDir,
- steam.currentUser()?.steam_id32 || 0
- )
- addToast({ title: "读取成功" })
- } else addToast({ title: "请先选择用户", color: "danger" })
- }}
+ onClick={debouncedGetVideoConfig}
>
读取
@@ -380,86 +432,86 @@ const VideoSetting = () => {
transition={{ duration: 0.2 }}
>
-
- -
- 分辨率
-
- {
- const _ = edit
- ? setVconfig({
- ...vconfig,
- defaultres: value.toString(),
- })
- : tool.setVideoSetting({
- ...tool.state.videoSetting,
- defaultres: value.toString(),
- })
- }}
- isDisabled={!edit}
- radius="full"
- step={10}
- className="max-w-28"
- classNames={{ inputWrapper: "h-10" }}
- />
- {
- const _ = edit
- ? setVconfig({
- ...vconfig,
- defaultresheight: value.toString(),
- })
- : tool.setVideoSetting({
- ...tool.state.videoSetting,
- defaultresheight: value.toString(),
- })
- }}
- isDisabled={!edit}
- radius="full"
- step={10}
- className="max-w-28"
- classNames={{ inputWrapper: "h-10" }}
- />
-
-
- {videoSettings(edit ? vconfig : tool.state.videoSetting).map((vid, index) => (
- -
- {vid.title}
- {
- // console.log(vid.type, key)
- // 修改 vconfig 名为 vid.type 的 value为 key
- const _ =
- edit && key
- ? setVconfig({
- ...vconfig,
- [vid.type]: vid.mapping(key.toString()),
- })
- : null
- }}
- >
- {vid.options.map((opt, _) => (
-
- ))}
-
+ {edit ? (
+ // 编辑状态:显示完整的可编辑控件
+
+ -
+ 分辨率
+
+ {
+ setVconfig({
+ ...vconfig,
+ defaultres: value.toString(),
+ })
+ }}
+ radius="full"
+ step={10}
+ className="max-w-28"
+ classNames={{ inputWrapper: "h-10" }}
+ />
+ {
+ setVconfig({
+ ...vconfig,
+ defaultresheight: value.toString(),
+ })
+ }}
+ radius="full"
+ step={10}
+ className="max-w-28"
+ classNames={{ inputWrapper: "h-10" }}
+ />
+
- ))}
-
+ {videoSettings(vconfig).map((vid, index) => (
+ -
+ {vid.title}
+ {
+ if (key) {
+ setVconfig({
+ ...vconfig,
+ [vid.type]: vid.mapping(key.toString()),
+ })
+ }
+ }}
+ >
+ {vid.options.map((opt, _) => (
+
+ ))}
+
+
+ ))}
+
+ ) : (
+ // 非编辑状态:显示精简的只读信息
+
+
+
+ 分辨率
+
+ {tool.state.videoSetting.defaultres} × {tool.state.videoSetting.defaultresheight}
+
+
+ {videoSettings(tool.state.videoSetting).map((vid, index) => (
+
+ {vid.title}
+ {vid.value}
+
+ ))}
+
+
+ )}
)}
diff --git a/src/components/window/Nav.tsx b/src/components/window/Nav.tsx
index a17f0f8..eb50519 100644
--- a/src/components/window/Nav.tsx
+++ b/src/components/window/Nav.tsx
@@ -21,6 +21,7 @@ import { saveAllNow } from "@tauri-store/valtio"
import { useSteamStore } from "@/store/steam"
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/react"
import { window } from "@tauri-apps/api"
+import { AuthButton } from "@/components/auth/AuthButton"
const Nav = () => {
const { theme, setTheme } = useTheme()
@@ -91,6 +92,8 @@ const Nav = () => {
+
+
{/* { platform() === "windows" && ( */}
@@ -182,4 +185,8 @@ function ResetModal() {
)
}
+function AuthButtonWrapper() {
+ return
+}
+
export default Nav
diff --git a/src/components/window/SideBar.tsx b/src/components/window/SideBar.tsx
index 1f9d742..caa0869 100644
--- a/src/components/window/SideBar.tsx
+++ b/src/components/window/SideBar.tsx
@@ -1,5 +1,5 @@
"use client"
-import { cn } from "@heroui/react"
+import { cn, Tooltip } from "@heroui/react"
import { Home, MonitorOne, Movie, NewspaperFolding, Setting, Terminal, Toolkit } from "@icon-park/react"
import { usePathname, useRouter } from "next/navigation"
import type { ReactNode } from "react"
@@ -9,6 +9,17 @@ import { getVersion } from "@tauri-apps/api/app"
import { useAppStore } from "@/store/app"
import { useSteamStore } from "@/store/steam"
+// 路由到页面名称的映射
+const routeNames: Record = {
+ "/home": "首页",
+ "/dynamic": "动态",
+ "/tool": "工具",
+ "/console": "控制台",
+ "/gear": "硬件外设",
+ "/movie": "录像",
+ "/preference": "偏好设置",
+}
+
interface SideButtonProps {
route: string
className?: string
@@ -23,26 +34,29 @@ const SideButton = ({
}: SideButtonProps & React.ButtonHTMLAttributes) => {
const router = useRouter()
const path = usePathname()
+ const pageName = routeNames[route] || route
return (
-