mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-28 00:35:32 +08:00
feat(settings): enhance settings page with accordion layout and tool versions
Major settings page improvements: AboutSection: - Add local tool version detection (Claude, Codex, Gemini) - Display installed vs latest version comparison with visual indicators - Show update availability badges and environment check cards SettingsPage: - Reorganize advanced settings into collapsible accordion sections - Add proxy control panel with inline status toggle - Integrate auto-failover configuration with accordion UI - Add database and cost calculation config sections DirectorySettings & WindowSettings: - Minor styling adjustments for consistency settings.ts API: - Add getToolVersions() wrapper for new backend command
This commit is contained in:
@@ -1,5 +1,14 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Download, ExternalLink, Info, Loader2, RefreshCw } from "lucide-react";
|
||||
import {
|
||||
Download,
|
||||
ExternalLink,
|
||||
Info,
|
||||
Loader2,
|
||||
RefreshCw,
|
||||
Terminal,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
@@ -7,16 +16,27 @@ import { getVersion } from "@tauri-apps/api/app";
|
||||
import { settingsApi } from "@/lib/api";
|
||||
import { useUpdate } from "@/contexts/UpdateContext";
|
||||
import { relaunchApp } from "@/lib/updater";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
interface AboutSectionProps {
|
||||
isPortable: boolean;
|
||||
}
|
||||
|
||||
interface ToolVersion {
|
||||
name: string;
|
||||
version: string | null;
|
||||
latest_version: string | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
// ... (use hooks as before) ...
|
||||
const { t } = useTranslation();
|
||||
const [version, setVersion] = useState<string | null>(null);
|
||||
const [isLoadingVersion, setIsLoadingVersion] = useState(true);
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
const [toolVersions, setToolVersions] = useState<ToolVersion[]>([]);
|
||||
const [isLoadingTools, setIsLoadingTools] = useState(true);
|
||||
|
||||
const {
|
||||
hasUpdate,
|
||||
@@ -31,18 +51,24 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
let active = true;
|
||||
const load = async () => {
|
||||
try {
|
||||
const loaded = await getVersion();
|
||||
const [appVersion, tools] = await Promise.all([
|
||||
getVersion(),
|
||||
settingsApi.getToolVersions(),
|
||||
]);
|
||||
|
||||
if (active) {
|
||||
setVersion(loaded);
|
||||
setVersion(appVersion);
|
||||
setToolVersions(tools);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[AboutSection] Failed to get version", error);
|
||||
console.error("[AboutSection] Failed to load info", error);
|
||||
if (active) {
|
||||
setVersion(null);
|
||||
}
|
||||
} finally {
|
||||
if (active) {
|
||||
setIsLoadingVersion(false);
|
||||
setIsLoadingTools(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -53,6 +79,8 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// ... (handlers like handleOpenReleaseNotes, handleCheckUpdate) ...
|
||||
|
||||
const handleOpenReleaseNotes = useCallback(async () => {
|
||||
try {
|
||||
const targetVersion = updateInfo?.availableVersion ?? version ?? "";
|
||||
@@ -125,7 +153,7 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
const displayVersion = version ?? t("common.unknown");
|
||||
|
||||
return (
|
||||
<section className="space-y-4">
|
||||
<section className="space-y-6">
|
||||
<header className="space-y-1">
|
||||
<h3 className="text-sm font-medium">{t("common.about")}</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
@@ -133,24 +161,28 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="space-y-4 rounded-lg border border-border-default p-4">
|
||||
<div className="rounded-xl border border-border bg-card/50 p-6 space-y-6">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-foreground">CC Switch</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("common.version")}{" "}
|
||||
{isLoadingVersion ? (
|
||||
<Loader2 className="inline h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
`v${displayVersion}`
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-lg font-semibold text-foreground">CC Switch</h4>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="gap-1.5 bg-background">
|
||||
<span className="text-muted-foreground">
|
||||
{t("common.version")}
|
||||
</span>
|
||||
{isLoadingVersion ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<span className="font-medium">{`v${displayVersion}`}</span>
|
||||
)}
|
||||
</Badge>
|
||||
{isPortable && (
|
||||
<Badge variant="secondary" className="gap-1.5">
|
||||
<Info className="h-3 w-3" />
|
||||
{t("settings.portableMode")}
|
||||
</Badge>
|
||||
)}
|
||||
</p>
|
||||
{isPortable ? (
|
||||
<p className="inline-flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Info className="h-3 w-3" />
|
||||
{t("settings.portableMode")}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
@@ -159,6 +191,7 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleOpenReleaseNotes}
|
||||
className="h-9"
|
||||
>
|
||||
<ExternalLink className="mr-2 h-4 w-4" />
|
||||
{t("settings.releaseNotes")}
|
||||
@@ -168,7 +201,7 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
size="sm"
|
||||
onClick={handleCheckUpdate}
|
||||
disabled={isChecking || isDownloading}
|
||||
className="min-w-[140px]"
|
||||
className="min-w-[140px] h-9"
|
||||
>
|
||||
{isDownloading ? (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
@@ -194,18 +227,71 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasUpdate && updateInfo ? (
|
||||
<div className="rounded-md bg-muted/40 px-3 py-2 text-xs text-muted-foreground">
|
||||
<p>
|
||||
{hasUpdate && updateInfo && (
|
||||
<div className="rounded-lg bg-primary/10 border border-primary/20 px-4 py-3 text-sm">
|
||||
<p className="font-medium text-primary mb-1">
|
||||
{t("settings.updateAvailable", {
|
||||
version: updateInfo.availableVersion,
|
||||
})}
|
||||
</p>
|
||||
{updateInfo.notes ? (
|
||||
<p className="mt-1 line-clamp-3">{updateInfo.notes}</p>
|
||||
) : null}
|
||||
{updateInfo.notes && (
|
||||
<p className="text-muted-foreground line-clamp-3 leading-relaxed">
|
||||
{updateInfo.notes}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-medium text-muted-foreground px-1">
|
||||
本地环境检查
|
||||
</h4>
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
{isLoadingTools
|
||||
? Array.from({ length: 3 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="h-20 rounded-xl border border-border bg-card/50 animate-pulse"
|
||||
/>
|
||||
))
|
||||
: toolVersions.map((tool) => (
|
||||
<div
|
||||
key={tool.name}
|
||||
className="flex flex-col gap-2 rounded-xl border border-border bg-card/50 p-4 transition-colors hover:bg-muted/50"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium capitalize">
|
||||
{tool.name}
|
||||
</span>
|
||||
</div>
|
||||
{tool.version ? (
|
||||
<div className="flex items-center gap-1.5">
|
||||
{tool.latest_version &&
|
||||
tool.version !== tool.latest_version && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 border border-yellow-500/20">
|
||||
Update: {tool.latest_version}
|
||||
</span>
|
||||
)}
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
</div>
|
||||
) : (
|
||||
<AlertCircle className="h-4 w-4 text-yellow-500" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div
|
||||
className="text-xs font-mono truncate"
|
||||
title={tool.version || tool.error || "Unknown"}
|
||||
>
|
||||
{tool.version ? tool.version : tool.error || "未安装"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -50,7 +50,7 @@ export function DirectorySettings({
|
||||
<Input
|
||||
value={appConfigDir ?? resolvedDirs.appConfig ?? ""}
|
||||
placeholder={t("settings.browsePlaceholderApp")}
|
||||
className="font-mono text-xs"
|
||||
className="text-xs"
|
||||
onChange={(event) => onAppConfigChange(event.target.value)}
|
||||
/>
|
||||
<Button
|
||||
@@ -161,7 +161,7 @@ function DirectoryInput({
|
||||
<Input
|
||||
value={displayValue}
|
||||
placeholder={placeholder}
|
||||
className="font-mono text-xs"
|
||||
className="text-xs"
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Loader2, Save } from "lucide-react";
|
||||
import {
|
||||
Loader2,
|
||||
Save,
|
||||
FolderSearch,
|
||||
Activity,
|
||||
Coins,
|
||||
Database,
|
||||
Server,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -8,6 +16,12 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { settingsApi } from "@/lib/api";
|
||||
@@ -26,6 +40,9 @@ import { useSettings } from "@/hooks/useSettings";
|
||||
import { useImportExport } from "@/hooks/useImportExport";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { SettingsFormState } from "@/hooks/useSettings";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useProxyStatus } from "@/hooks/useProxyStatus";
|
||||
|
||||
interface SettingsDialogProps {
|
||||
open: boolean;
|
||||
@@ -155,6 +172,26 @@ export function SettingsPage({
|
||||
|
||||
const isBusy = useMemo(() => isLoading && !settings, [isLoading, settings]);
|
||||
|
||||
const {
|
||||
isRunning,
|
||||
start: startProxy,
|
||||
stop: stopProxy,
|
||||
isPending: isProxyPending,
|
||||
} = useProxyStatus();
|
||||
const [failoverEnabled, setFailoverEnabled] = useState(true);
|
||||
|
||||
const handleToggleProxy = async (checked: boolean) => {
|
||||
try {
|
||||
if (!checked) {
|
||||
await stopProxy();
|
||||
} else {
|
||||
await startProxy();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Toggle proxy failed:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-[56rem] flex flex-col h-[calc(100vh-8rem)] overflow-hidden px-6">
|
||||
{isBusy ? (
|
||||
@@ -199,64 +236,225 @@ export function SettingsPage({
|
||||
|
||||
<TabsContent value="advanced" className="space-y-6 mt-0 pb-6">
|
||||
{settings ? (
|
||||
<>
|
||||
<DirectorySettings
|
||||
appConfigDir={appConfigDir}
|
||||
resolvedDirs={resolvedDirs}
|
||||
onAppConfigChange={updateAppConfigDir}
|
||||
onBrowseAppConfig={browseAppConfigDir}
|
||||
onResetAppConfig={resetAppConfigDir}
|
||||
claudeDir={settings.claudeConfigDir}
|
||||
codexDir={settings.codexConfigDir}
|
||||
geminiDir={settings.geminiConfigDir}
|
||||
onDirectoryChange={updateDirectory}
|
||||
onBrowseDirectory={browseDirectory}
|
||||
onResetDirectory={resetDirectory}
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<Accordion
|
||||
type="multiple"
|
||||
defaultValue={[]}
|
||||
className="w-full space-y-4"
|
||||
>
|
||||
<AccordionItem
|
||||
value="directory"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<FolderSearch className="h-5 w-5 text-primary" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
配置文件目录
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
管理 Claude、Codex 和 Gemini 的配置存储路径
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<DirectorySettings
|
||||
appConfigDir={appConfigDir}
|
||||
resolvedDirs={resolvedDirs}
|
||||
onAppConfigChange={updateAppConfigDir}
|
||||
onBrowseAppConfig={browseAppConfigDir}
|
||||
onResetAppConfig={resetAppConfigDir}
|
||||
claudeDir={settings.claudeConfigDir}
|
||||
codexDir={settings.codexConfigDir}
|
||||
geminiDir={settings.geminiConfigDir}
|
||||
onDirectoryChange={updateDirectory}
|
||||
onBrowseDirectory={browseDirectory}
|
||||
onResetDirectory={resetDirectory}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
{/* 代理服务面板 */}
|
||||
<ProxyPanel />
|
||||
<AccordionItem
|
||||
value="proxy"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex flex-1 items-center justify-between pr-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Server className="h-5 w-5 text-green-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
本地代理
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
控制代理服务开关、查看状态与端口信息
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center gap-4"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Badge
|
||||
variant={isRunning ? "default" : "secondary"}
|
||||
className="gap-1.5 h-6"
|
||||
>
|
||||
<Activity
|
||||
className={`h-3 w-3 ${isRunning ? "animate-pulse" : ""}`}
|
||||
/>
|
||||
{isRunning ? "运行中" : "已停止"}
|
||||
</Badge>
|
||||
<Switch
|
||||
checked={isRunning}
|
||||
onCheckedChange={handleToggleProxy}
|
||||
disabled={isProxyPending}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-0 border-t border-border/50">
|
||||
<ProxyPanel />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
{/* 模型定价配置 */}
|
||||
<PricingConfigPanel />
|
||||
<AccordionItem
|
||||
value="test"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Activity className="h-5 w-5 text-indigo-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
模型测试配置
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
配置模型测试使用的默认模型和提示词
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<ModelTestConfigPanel />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
{/* 模型测试配置 */}
|
||||
<ModelTestConfigPanel />
|
||||
<AccordionItem
|
||||
value="failover"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex flex-1 items-center justify-between pr-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Activity className="h-5 w-5 text-orange-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
自动故障转移
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
配置自动故障转移和熔断策略
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center gap-4"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Removed status text as requested */}
|
||||
<Switch
|
||||
checked={failoverEnabled}
|
||||
onCheckedChange={setFailoverEnabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<AutoFailoverConfigPanel
|
||||
enabled={failoverEnabled}
|
||||
onEnabledChange={setFailoverEnabled}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
{/* 自动故障转移配置 */}
|
||||
<AutoFailoverConfigPanel />
|
||||
<AccordionItem
|
||||
value="pricing"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Coins className="h-5 w-5 text-yellow-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
成本定价
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
管理各模型 Token 计费规则
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<PricingConfigPanel />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<ImportExportSection
|
||||
status={importStatus}
|
||||
selectedFile={selectedFile}
|
||||
errorMessage={errorMessage}
|
||||
backupId={backupId}
|
||||
isImporting={isImporting}
|
||||
onSelectFile={selectImportFile}
|
||||
onImport={importConfig}
|
||||
onExport={exportConfig}
|
||||
onClear={clearSelection}
|
||||
/>
|
||||
<div className="pt-6 border-t border-gray-200 dark:border-white/10">
|
||||
<AccordionItem
|
||||
value="data"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Database className="h-5 w-5 text-blue-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
数据管理
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
导入导出配置与备份恢复
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<ImportExportSection
|
||||
status={importStatus}
|
||||
selectedFile={selectedFile}
|
||||
errorMessage={errorMessage}
|
||||
backupId={backupId}
|
||||
isImporting={isImporting}
|
||||
onSelectFile={selectImportFile}
|
||||
onImport={importConfig}
|
||||
onExport={exportConfig}
|
||||
onClear={clearSelection}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
<div className="pt-4">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="w-full"
|
||||
className="w-full h-12 text-base font-medium"
|
||||
disabled={isSaving}
|
||||
>
|
||||
{isSaving ? (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
{t("settings.saving")}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
<Save className="mr-2 h-5 w-5" />
|
||||
{t("common.save")}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
) : null}
|
||||
</TabsContent>
|
||||
|
||||
@@ -275,10 +473,7 @@ export function SettingsPage({
|
||||
open={showRestartPrompt}
|
||||
onOpenChange={(open) => !open && handleRestartLater()}
|
||||
>
|
||||
<DialogContent
|
||||
zIndex="alert"
|
||||
className="max-w-md glass border-white/10"
|
||||
>
|
||||
<DialogContent zIndex="alert" className="max-w-md glass border-border">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("settings.restartRequired")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
@@ -291,7 +486,7 @@ export function SettingsPage({
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={handleRestartLater}
|
||||
className="hover:bg-white/5"
|
||||
className="hover:bg-muted/50"
|
||||
>
|
||||
{t("settings.restartLater")}
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { SettingsFormState } from "@/hooks/useSettings";
|
||||
import { AppWindow, MonitorUp, Power } from "lucide-react";
|
||||
|
||||
interface WindowSettingsProps {
|
||||
settings: SettingsFormState;
|
||||
@@ -12,40 +13,46 @@ export function WindowSettings({ settings, onChange }: WindowSettingsProps) {
|
||||
|
||||
return (
|
||||
<section className="space-y-4">
|
||||
<header className="space-y-1">
|
||||
<div className="flex items-center gap-2 pb-2 border-b border-border/40">
|
||||
<AppWindow className="h-4 w-4 text-primary" />
|
||||
<h3 className="text-sm font-medium">{t("settings.windowBehavior")}</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("settings.windowBehaviorHint")}
|
||||
</p>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<ToggleRow
|
||||
title={t("settings.launchOnStartup")}
|
||||
description={t("settings.launchOnStartupDescription")}
|
||||
checked={!!settings.launchOnStartup}
|
||||
onCheckedChange={(value) => onChange({ launchOnStartup: value })}
|
||||
/>
|
||||
<div className="space-y-3">
|
||||
<ToggleRow
|
||||
icon={<Power className="h-4 w-4 text-orange-500" />}
|
||||
title={t("settings.launchOnStartup")}
|
||||
description={t("settings.launchOnStartupDescription")}
|
||||
checked={!!settings.launchOnStartup}
|
||||
onCheckedChange={(value) => onChange({ launchOnStartup: value })}
|
||||
/>
|
||||
|
||||
<ToggleRow
|
||||
title={t("settings.minimizeToTray")}
|
||||
description={t("settings.minimizeToTrayDescription")}
|
||||
checked={settings.minimizeToTrayOnClose}
|
||||
onCheckedChange={(value) => onChange({ minimizeToTrayOnClose: value })}
|
||||
/>
|
||||
<ToggleRow
|
||||
icon={<AppWindow className="h-4 w-4 text-blue-500" />}
|
||||
title={t("settings.minimizeToTray")}
|
||||
description={t("settings.minimizeToTrayDescription")}
|
||||
checked={settings.minimizeToTrayOnClose}
|
||||
onCheckedChange={(value) =>
|
||||
onChange({ minimizeToTrayOnClose: value })
|
||||
}
|
||||
/>
|
||||
|
||||
<ToggleRow
|
||||
title={t("settings.enableClaudePluginIntegration")}
|
||||
description={t("settings.enableClaudePluginIntegrationDescription")}
|
||||
checked={!!settings.enableClaudePluginIntegration}
|
||||
onCheckedChange={(value) =>
|
||||
onChange({ enableClaudePluginIntegration: value })
|
||||
}
|
||||
/>
|
||||
<ToggleRow
|
||||
icon={<MonitorUp className="h-4 w-4 text-purple-500" />}
|
||||
title={t("settings.enableClaudePluginIntegration")}
|
||||
description={t("settings.enableClaudePluginIntegrationDescription")}
|
||||
checked={!!settings.enableClaudePluginIntegration}
|
||||
onCheckedChange={(value) =>
|
||||
onChange({ enableClaudePluginIntegration: value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
interface ToggleRowProps {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
description?: string;
|
||||
checked: boolean;
|
||||
@@ -53,18 +60,24 @@ interface ToggleRowProps {
|
||||
}
|
||||
|
||||
function ToggleRow({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
checked,
|
||||
onCheckedChange,
|
||||
}: ToggleRowProps) {
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-4 rounded-lg border border-border-default p-4">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium leading-none">{title}</p>
|
||||
{description ? (
|
||||
<p className="text-xs text-muted-foreground">{description}</p>
|
||||
) : null}
|
||||
<div className="flex items-center justify-between gap-4 rounded-xl border border-border bg-card/50 p-4 transition-colors hover:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-background ring-1 ring-border">
|
||||
{icon}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium leading-none">{title}</p>
|
||||
{description ? (
|
||||
<p className="text-xs text-muted-foreground">{description}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={checked}
|
||||
|
||||
@@ -115,4 +115,15 @@ export const settingsApi = {
|
||||
async getAutoLaunchStatus(): Promise<boolean> {
|
||||
return await invoke("get_auto_launch_status");
|
||||
},
|
||||
|
||||
async getToolVersions(): Promise<
|
||||
Array<{
|
||||
name: string;
|
||||
version: string | null;
|
||||
latest_version: string | null;
|
||||
error: string | null;
|
||||
}>
|
||||
> {
|
||||
return await invoke("get_tool_versions");
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user