250 lines
9.6 KiB
TypeScript
250 lines
9.6 KiB
TypeScript
import { createFileRoute } from '@tanstack/react-router';
|
|
import { useTheme } from '@/components/theme-provider';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Switch } from '@/components/ui/switch';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { getAppVersion, getPlatform } from '@/actions/app';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { setAppLanguage } from '@/actions/language';
|
|
import { useAppConfig } from '@/hooks/useAppConfig';
|
|
import { Loader2 } from 'lucide-react';
|
|
import { useState, useEffect } from 'react';
|
|
import { ProxyConfig } from '@/types/config';
|
|
|
|
function SettingsPage() {
|
|
const { theme, setTheme } = useTheme();
|
|
const { t, i18n } = useTranslation();
|
|
const { config, isLoading, saveConfig } = useAppConfig();
|
|
|
|
// Local state for configuration editing
|
|
const [proxyConfig, setProxyConfig] = useState<ProxyConfig | undefined>(undefined);
|
|
|
|
// Sync config to local state when loaded
|
|
useEffect(() => {
|
|
if (config) {
|
|
setProxyConfig(config.proxy);
|
|
}
|
|
}, [config]);
|
|
|
|
const { data: appVersion } = useQuery({
|
|
queryKey: ['app', 'version'],
|
|
queryFn: getAppVersion,
|
|
});
|
|
|
|
const { data: platform } = useQuery({
|
|
queryKey: ['app', 'platform'],
|
|
queryFn: getPlatform,
|
|
});
|
|
|
|
const handleLanguageChange = (value: string) => {
|
|
setAppLanguage(value, i18n);
|
|
};
|
|
|
|
// Helper to update proxyConfig and auto-save
|
|
const updateProxyConfig = async (newProxyConfig: ProxyConfig) => {
|
|
setProxyConfig(newProxyConfig);
|
|
if (config) {
|
|
await saveConfig({ ...config, proxy: newProxyConfig });
|
|
}
|
|
};
|
|
|
|
if (isLoading || !proxyConfig) {
|
|
return (
|
|
<div className="flex h-screen items-center justify-center">
|
|
<Loader2 className="animate-spin" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="scrollbar-hide container mx-auto h-[calc(100vh-theme(spacing.16))] max-w-4xl space-y-8 overflow-y-auto p-6">
|
|
<div>
|
|
<h2 className="text-3xl font-bold tracking-tight">{t('settings.title')}</h2>
|
|
<p className="text-muted-foreground mt-1">{t('settings.description')}</p>
|
|
</div>
|
|
|
|
<Tabs defaultValue="general" className="w-full">
|
|
<TabsList className="grid w-full grid-cols-2">
|
|
<TabsTrigger value="general">{t('settings.general', 'General')}</TabsTrigger>
|
|
<TabsTrigger value="proxy">{t('settings.proxy_tab', 'Proxy')}</TabsTrigger>
|
|
</TabsList>
|
|
|
|
{/* --- GENERAL TAB --- */}
|
|
<TabsContent value="general" className="space-y-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{t('settings.appearance.title')}</CardTitle>
|
|
<CardDescription>{t('settings.appearance.description')}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
<div className="flex items-center justify-between space-x-2">
|
|
<div className="space-y-1">
|
|
<Label htmlFor="dark-mode">{t('settings.darkMode')}</Label>
|
|
<p className="text-muted-foreground text-sm">
|
|
{t('settings.darkModeDescription')}
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
id="dark-mode"
|
|
checked={theme === 'dark'}
|
|
onCheckedChange={(checked) => setTheme(checked ? 'dark' : 'light')}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between space-x-2">
|
|
<div className="space-y-1">
|
|
<Label htmlFor="language">{t('settings.language.title')}</Label>
|
|
<p className="text-muted-foreground text-sm">
|
|
{t('settings.language.description')}
|
|
</p>
|
|
</div>
|
|
<Select
|
|
value={i18n.language}
|
|
onValueChange={handleLanguageChange}
|
|
key={i18n.language}
|
|
>
|
|
<SelectTrigger className="w-[180px]">
|
|
<SelectValue placeholder={t('settings.language.title')} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="en">{t('settings.language.english')}</SelectItem>
|
|
<SelectItem value="zh-CN">{t('settings.language.chinese')}</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Account Settings Card */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{t('settings.account.title', 'Account Settings')}</CardTitle>
|
|
<CardDescription>
|
|
{t('settings.account.description', 'Configure automatic account refresh and sync.')}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{/* Auto Refresh Quota */}
|
|
<div className="flex items-center justify-between rounded-lg border p-4">
|
|
<div className="space-y-1">
|
|
<Label>{t('settings.account.auto_refresh', 'Auto Refresh Quota')}</Label>
|
|
<p className="text-xs text-gray-500">
|
|
{t(
|
|
'settings.account.auto_refresh_desc',
|
|
'Periodically refresh quota info for all accounts',
|
|
)}
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={config?.auto_refresh || false}
|
|
onCheckedChange={async (checked) => {
|
|
if (config) {
|
|
await saveConfig({ ...config, auto_refresh: checked });
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* Auto Sync Account */}
|
|
<div className="flex items-center justify-between rounded-lg border p-4">
|
|
<div className="space-y-1">
|
|
<Label>{t('settings.account.auto_sync', 'Auto Sync Current Account')}</Label>
|
|
<p className="text-xs text-gray-500">
|
|
{t(
|
|
'settings.account.auto_sync_desc',
|
|
'Periodically sync active account information',
|
|
)}
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={config?.auto_sync || false}
|
|
onCheckedChange={async (checked) => {
|
|
if (config) {
|
|
await saveConfig({ ...config, auto_sync: checked });
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{t('settings.about.title')}</CardTitle>
|
|
<CardDescription>{t('settings.about.description')}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
|
<div className="text-muted-foreground">{t('settings.version')}</div>
|
|
<div className="font-medium">{appVersion || 'Unknown'}</div>
|
|
|
|
<div className="text-muted-foreground">{t('settings.platform')}</div>
|
|
<div className="font-medium capitalize">{platform || 'Unknown'}</div>
|
|
|
|
<div className="text-muted-foreground">{t('settings.license')}</div>
|
|
<div className="font-medium">CC BY-NC-SA 4.0</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
{/* --- PROXY TAB (Upstream Proxy Config Only) --- */}
|
|
<TabsContent value="proxy" className="space-y-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{t('settings.proxy.title')}</CardTitle>
|
|
<CardDescription>{t('settings.proxy.description')}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="flex items-center justify-between space-x-2">
|
|
<div className="space-y-1">
|
|
<Label htmlFor="upstream-proxy-enabled">{t('settings.proxy.enable')}</Label>
|
|
</div>
|
|
<Switch
|
|
id="upstream-proxy-enabled"
|
|
checked={proxyConfig.upstream_proxy.enabled}
|
|
onCheckedChange={(checked) =>
|
|
updateProxyConfig({
|
|
...proxyConfig,
|
|
upstream_proxy: { ...proxyConfig.upstream_proxy, enabled: checked },
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="upstream-proxy-url">{t('settings.proxy.url')}</Label>
|
|
<Input
|
|
id="upstream-proxy-url"
|
|
placeholder="http://127.0.0.1:7890"
|
|
value={proxyConfig.upstream_proxy.url}
|
|
onChange={(e) =>
|
|
updateProxyConfig({
|
|
...proxyConfig,
|
|
upstream_proxy: { ...proxyConfig.upstream_proxy, url: e.target.value },
|
|
})
|
|
}
|
|
disabled={!proxyConfig.upstream_proxy.enabled}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export const Route = createFileRoute('/settings')({
|
|
component: SettingsPage,
|
|
});
|