[feat] better installer + changelog test version switch + better video config view

This commit is contained in:
2025-11-05 11:19:43 +08:00
parent ea0a42dc43
commit 41008cf13c
19 changed files with 1499 additions and 183 deletions

View File

@@ -1,13 +1,18 @@
"use client"
import { useState } from "react"
import { MarkdownRender } from "@/components/markdown"
import { Card, CardBody, CardHeader, CardIcon } from "@/components/window/Card"
import { Card, CardBody, CardHeader, CardIcon, CardTool } from "@/components/window/Card"
import { createClient } from "@/utils/supabase/client"
import { NewspaperFolding } from "@icon-park/react"
import useSWR from "swr"
import { Chip, Skeleton } from "@heroui/react"
import { Chip, Skeleton, Tabs, Tab } from "@heroui/react"
import { Key } from "@react-types/shared"
export default function Page() {
const [selectedKey, setSelectedKey] = useState<Key>("stable")
const showTestVersions = selectedKey === "test"
return (
<section className="flex flex-col gap-4 overflow-hidden">
<Card className="overflow-hidden">
@@ -15,68 +20,107 @@ export default function Page() {
<CardIcon>
<NewspaperFolding />
</CardIcon>
{/* <CardTool>
<ToolButton onClick={async () => {}}>读取</ToolButton>
</CardTool> */}
<CardTool>
<Tabs
selectedKey={selectedKey}
onSelectionChange={setSelectedKey}
size="sm"
radius="full"
classNames={{
base: "min-w-0",
tabList: "gap-1",
tab: "min-w-0 px-3",
tabContent: "text-xs",
}}
>
<Tab key="stable" title="正式版" />
<Tab key="test" title="测试版" />
</Tabs>
</CardTool>
</CardHeader>
<CardBody className="overflow-y-hidden">
<ReleaseNotes />
<ReleaseNotes showTestVersions={showTestVersions} />
</CardBody>
</Card>
</section>
)
}
const ReleaseNotes = () => {
const ReleaseNotes = ({ showTestVersions }: { showTestVersions: boolean }) => {
const noticeFetcher = async () => {
const supabase = createClient()
const { data /* , error */ } = await supabase
let query = supabase
.from("ReleaseNote")
.select("version, content, created_at")
.eq("stable", true)
.select("version, content, created_at, stable")
if (!showTestVersions) {
query = query.eq("stable", true)
}
const { data /* , error */ } = await query
.order("created_at", { ascending: false })
.range(0, 10)
return data
}
const { data: releases /* , error */, isLoading } = useSWR("/api/release-notes", noticeFetcher)
if (isLoading) return (
<div className="grid h-full grid-cols-1 gap-2 overflow-y-auto rounded-lg grid-flow-dense lg:grid-cols-2 xl:grid-cols-3 pb-9 hide-scrollbar">
<Skeleton className="h-32 rounded-lg"></Skeleton>
<Skeleton className="h-32 rounded-lg"></Skeleton>
<Skeleton className="h-32 rounded-lg"></Skeleton>
</div>
const { data: releases /* , error */, isLoading } = useSWR(
`/api/release-notes?test=${showTestVersions}`,
noticeFetcher
)
return (
<ul className="grid h-full grid-cols-1 gap-2 overflow-y-auto rounded-lg grid-flow-dense lg:grid-cols-2 xl:grid-cols-3 pb-9 hide-scrollbar">
{releases?.map((release, index) => (
<li key={index}>
{/* <Link href={`/releases/${release.version}`} className="w-full"> */}
<Card className="w-full h-full pt-3 transition bg-white/60 text-zinc-900 dark:bg-white/5 dark:text-white">
<CardHeader>
<h3 className="w-full text-2xl font-semibold">CS工具箱 {release.version}</h3>
<span className="flex items-center gap-3">
{/* <Chip size="sm" className="bg-zinc-200">
版本:{release?.version}
</Chip> */}
<Chip size="sm" className="bg-zinc-200 dark:bg-white/10">
{release.created_at ? new Date(release.created_at as string).toLocaleString() : "未知时间"}
</Chip>
</span>
</CardHeader>
<CardBody className="gap-3">
<div className="">
<MarkdownRender>{release.content || "无内容"}</MarkdownRender>
</div>
</CardBody>
</Card>
{/* </Link> */}
</li>
))}
<ul
className="grid h-full grid-cols-1 gap-2 overflow-y-auto rounded-lg grid-flow-dense lg:grid-cols-2 xl:grid-cols-3 pb-9 hide-scrollbar"
>
{isLoading ? (
<>
<li><Skeleton className="h-32 rounded-lg"></Skeleton></li>
<li><Skeleton className="h-32 rounded-lg"></Skeleton></li>
<li><Skeleton className="h-32 rounded-lg"></Skeleton></li>
</>
) : (
releases?.map((release) => {
const isStable = release.stable === true
return (
<li key={release.version}>
{/* <Link href={`/releases/${release.version}`} className="w-full"> */}
<Card
className="w-full h-full pt-3 transition bg-white/60 text-zinc-900 dark:bg-white/5 dark:text-white"
>
<CardHeader>
<div className="flex items-center justify-between w-full gap-2">
<h3 className="text-2xl font-semibold">CS工具箱 {release.version}</h3>
<Chip
size="sm"
variant="bordered"
className={
isStable
? "border-blue-400 text-blue-600 dark:border-blue-500 dark:text-blue-400"
: "border-orange-400 text-orange-600 dark:border-orange-500 dark:text-orange-400"
}
>
{isStable ? "正式版" : "测试版"}
</Chip>
</div>
<span className="flex items-center gap-3">
<Chip size="sm" className="bg-zinc-200 dark:bg-white/10">
{release.created_at ? new Date(release.created_at as string).toLocaleString() : "未知时间"}
</Chip>
</span>
</CardHeader>
<CardBody className="gap-3">
<div className="">
<MarkdownRender>{release.content || "无内容"}</MarkdownRender>
</div>
</CardBody>
</Card>
{/* </Link> */}
</li>
)
})
)}
</ul>
)
}

View File

@@ -1,37 +1,53 @@
"use client"
import { useAppStore } from "@/store/app"
import { Switch } from "@heroui/react"
import { UpdateChecker } from "@/components/cstb/UpdateChecker"
export default function Page() {
const app = useAppStore()
// 从环境变量或配置中获取更新服务器地址
// 这里可以改为从 store 或配置文件中读取
const customEndpoint = process.env.NEXT_PUBLIC_UPDATE_ENDPOINT || ""
const githubRepo = process.env.NEXT_PUBLIC_GITHUB_REPO || ""
return (
<div className="flex flex-col items-start gap-3 pt-2 pb-1">
<p>{app.state.version}</p>
<p>{app.state.hasUpdate ? "有" : "无"}</p>
<p>使{app.state.useMirror ? "" : ""}</p>
<Switch
isSelected={app.state.autoStart}
size="sm"
onChange={(e) => app.setAutoStart(e.target.checked)}
>
{app.state.autoStart ? "开" : "关"}
</Switch>
<Switch
isSelected={app.state.startHidden}
size="sm"
onChange={(e) => app.setStartHidden(e.target.checked)}
>
{app.state.startHidden ? "开" : "关"}
</Switch>
{/* hiddenOnClose */}
<Switch
isSelected={app.state.hiddenOnClose}
size="sm"
onChange={(e) => app.setHiddenOnClose(e.target.checked)}
>
{app.state.hiddenOnClose ? "开" : "关"}
</Switch>
<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>
<div className="w-full border-t border-zinc-200 dark:border-zinc-800 pt-4">
<h3 className="text-sm font-semibold mb-3"></h3>
<UpdateChecker customEndpoint={customEndpoint} githubRepo={githubRepo} />
</div>
<div className="w-full border-t border-zinc-200 dark:border-zinc-800 pt-4 space-y-3">
<h3 className="text-sm font-semibold mb-3"></h3>
<Switch
isSelected={app.state.autoStart}
size="sm"
onChange={(e) => app.setAutoStart(e.target.checked)}
>
{app.state.autoStart ? "开" : "关"}
</Switch>
<Switch
isSelected={app.state.startHidden}
size="sm"
onChange={(e) => app.setStartHidden(e.target.checked)}
>
{app.state.startHidden ? "开" : "关"}
</Switch>
<Switch
isSelected={app.state.hiddenOnClose}
size="sm"
onChange={(e) => app.setHiddenOnClose(e.target.checked)}
>
{app.state.hiddenOnClose ? "开" : "关"}
</Switch>
</div>
</div>
)
}

View File

@@ -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 }) {
>
<NextThemesProvider attribute="class" defaultTheme="light">
<ToastProvider toastOffset={10} placement="top-center" toastProps={{ timeout: 3000 }} />
{children}
<AuthProvider>{children}</AuthProvider>
</NextThemesProvider>
</HeroUIProvider>
)

View File

@@ -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",

View File

@@ -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<UpdateInfo | null>(null)
const [downloadProgress, setDownloadProgress] = useState(0)
const [installerPath, setInstallerPath] = useState<string | null>(null)
const { isOpen, onOpen, onOpenChange } = useDisclosure()
// 检查更新
const handleCheckUpdate = async () => {
setChecking(true)
setUpdateInfo(null)
setDownloadProgress(0)
setInstallerPath(null)
try {
const result = await invoke<UpdateInfo | null>("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<string>("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) => (
<span key={i}>
{line}
<br />
</span>
))
}
return (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<Button
size="sm"
color="primary"
variant="flat"
startContent={checking ? undefined : <Refresh />}
isLoading={checking}
onPress={handleCheckUpdate}
>
{checking ? "检查中..." : "检查更新"}
</Button>
{app.state.hasUpdate && (
<span className="text-sm text-green-600 dark:text-green-400 flex items-center gap-1">
<CheckCircle size={16} />
</span>
)}
</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">
<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>
</ModalHeader>
<ModalBody>
<div className="space-y-3">
<div>
<p className="text-sm font-semibold mb-1"></p>
<div className="text-sm text-zinc-600 dark:text-zinc-400 whitespace-pre-wrap">
{formatNotes(updateInfo?.notes)}
</div>
</div>
{updateInfo?.pub_date && (
<p className="text-xs text-zinc-500">{new Date(updateInfo.pub_date).toLocaleString("zh-CN")}</p>
)}
</div>
</ModalBody>
<ModalFooter>
<Button color="danger" variant="light" 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>
</>
)}
</ModalContent>
</Modal>
</div>
)
}

View File

@@ -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<boolean>("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<VideoConfig>(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 = () => {
<CardHeader>
<CardIcon>
<SettingConfig />
{isGameRunning && (
<Chip
size="sm"
color="warning"
variant="flat"
className="ml-2"
>
</Chip>
)}
</CardIcon>
<CardTool>
{/* {tool.state.VideoSettings.map((option, index) => (
@@ -302,6 +351,16 @@ const VideoSetting = () => {
</ToolButton>
<ToolButton
onClick={async () => {
// 检查游戏是否运行
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}
>
<Plus />
@@ -345,15 +405,7 @@ const VideoSetting = () => {
</ToolButton>
<ToolButton
onClick={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" })
}}
onClick={debouncedGetVideoConfig}
>
</ToolButton>
@@ -380,86 +432,86 @@ const VideoSetting = () => {
transition={{ duration: 0.2 }}
>
<CardBody>
<ul className="flex flex-wrap gap-3 mt-1">
<li className="flex flex-col gap-1.5">
<span className="ml-2"></span>
<span className="flex gap-3">
<NumberInput
aria-label="width"
value={parseInt(
edit ? vconfig.defaultres : tool.state.videoSetting.defaultres,
10
)}
onValueChange={(value) => {
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" }}
/>
<NumberInput
aria-label="height"
value={parseInt(
edit ? vconfig.defaultresheight : tool.state.videoSetting.defaultresheight,
10
)}
onValueChange={(value) => {
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" }}
/>
</span>
</li>
{videoSettings(edit ? vconfig : tool.state.videoSetting).map((vid, index) => (
<li className="flex flex-col gap-1.5" key={index}>
<span className="ml-2">{vid.title}</span>
<Tabs
size="md"
radius="full"
className="min-w-36"
fullWidth
selectedKey={vid.value}
onSelectionChange={(key) => {
// 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, _) => (
<Tab key={opt} title={opt} titleValue={opt} />
))}
</Tabs>
{edit ? (
// 编辑状态:显示完整的可编辑控件
<ul className="flex flex-wrap gap-3 mt-1">
<li className="flex flex-col gap-1.5">
<span className="ml-2"></span>
<span className="flex gap-3">
<NumberInput
aria-label="width"
value={parseInt(vconfig.defaultres, 10)}
onValueChange={(value) => {
setVconfig({
...vconfig,
defaultres: value.toString(),
})
}}
radius="full"
step={10}
className="max-w-28"
classNames={{ inputWrapper: "h-10" }}
/>
<NumberInput
aria-label="height"
value={parseInt(vconfig.defaultresheight, 10)}
onValueChange={(value) => {
setVconfig({
...vconfig,
defaultresheight: value.toString(),
})
}}
radius="full"
step={10}
className="max-w-28"
classNames={{ inputWrapper: "h-10" }}
/>
</span>
</li>
))}
</ul>
{videoSettings(vconfig).map((vid, index) => (
<li className="flex flex-col gap-1.5" key={index}>
<span className="ml-2">{vid.title}</span>
<Tabs
size="md"
radius="full"
className="min-w-36"
fullWidth
selectedKey={vid.value}
onSelectionChange={(key) => {
if (key) {
setVconfig({
...vconfig,
[vid.type]: vid.mapping(key.toString()),
})
}
}}
>
{vid.options.map((opt, _) => (
<Tab key={opt} title={opt} titleValue={opt} />
))}
</Tabs>
</li>
))}
</ul>
) : (
// 非编辑状态:显示精简的只读信息
<div className="mt-1">
<div className="grid grid-cols-3 md:grid-cols-4 gap-2.5">
<div className="flex flex-col gap-1">
<span className="text-xs text-default-500"></span>
<span className="text-sm font-medium">
{tool.state.videoSetting.defaultres} × {tool.state.videoSetting.defaultresheight}
</span>
</div>
{videoSettings(tool.state.videoSetting).map((vid, index) => (
<div className="flex flex-col gap-1" key={index}>
<span className="text-xs text-default-500">{vid.title}</span>
<span className="text-sm font-medium">{vid.value}</span>
</div>
))}
</div>
</div>
)}
</CardBody>
</motion.div>
)}

View File

@@ -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 = () => {
</Link>
</Tooltip>
<AuthButtonWrapper />
<ResetModal />
{/* { platform() === "windows" && ( */}
@@ -182,4 +185,8 @@ function ResetModal() {
)
}
function AuthButtonWrapper() {
return <AuthButton />
}
export default Nav

View File

@@ -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<string, string> = {
"/home": "首页",
"/dynamic": "动态",
"/tool": "工具",
"/console": "控制台",
"/gear": "硬件外设",
"/movie": "录像",
"/preference": "偏好设置",
}
interface SideButtonProps {
route: string
className?: string
@@ -23,26 +34,29 @@ const SideButton = ({
}: SideButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
const router = useRouter()
const path = usePathname()
const pageName = routeNames[route] || route
return (
<button
type="button"
onClick={() => router.push(route || "/")}
className={cn(
className,
"p-2.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition relative active:scale-90",
path.startsWith(route) && "bg-black/5 dark:bg-white/5"
)}
{...rest}
>
{children}
<div
<Tooltip content={pageName} showArrow={true} delay={300} placement="right">
<button
type="button"
onClick={() => router.push(route || "/")}
className={cn(
path.startsWith(route) && "opacity-100",
"transition-opacity duration-300 opacity-0 h-3.5 w-0.5 absolute left-0.5 bg-purple-500 rounded-full top-1/2 -translate-y-1/2"
className,
"p-2.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition relative active:scale-90 cursor-pointer",
path.startsWith(route) && "bg-black/5 dark:bg-white/5"
)}
/>
</button>
{...rest}
>
{children}
<div
className={cn(
path.startsWith(route) && "opacity-100",
"transition-opacity duration-300 opacity-0 h-3.5 w-0.5 absolute left-0.5 bg-purple-500 rounded-full top-1/2 -translate-y-1/2"
)}
/>
</button>
</Tooltip>
)
}

View File

@@ -1,12 +1,14 @@
import { appConfigDir } from "@tauri-apps/api/path"
import { commands } from "@tauri-store/shared"
import { appStore } from "./app"
import { authStore } from "./auth"
import { steamStore } from "./steam"
import { toolStore } from "./tool"
import path from "path"
export async function init() {
await appStore.start()
await authStore.start()
await toolStore.start()
await steamStore.start()
const appConfigDirPath = await appConfigDir()