Files
game-marathon/desktop/src/renderer/pages/SettingsPage.tsx

268 lines
11 KiB
TypeScript
Raw Normal View History

2026-01-10 08:24:55 +07:00
import { useEffect, useState } from 'react'
import { Settings, Power, Monitor, Clock, Globe, LogOut, Download, RefreshCw, Check, AlertCircle } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { useAuthStore } from '../store/auth'
import { GlassCard } from '../components/ui/GlassCard'
import { NeonButton } from '../components/ui/NeonButton'
import { Input } from '../components/ui/Input'
import type { AppSettings } from '@shared/types'
export function SettingsPage() {
const navigate = useNavigate()
const { user, logout } = useAuthStore()
const [settings, setSettings] = useState<AppSettings | null>(null)
const [isSaving, setIsSaving] = useState(false)
const [appVersion, setAppVersion] = useState('')
const [updateStatus, setUpdateStatus] = useState<'idle' | 'checking' | 'available' | 'not-available' | 'error'>('idle')
const [updateVersion, setUpdateVersion] = useState('')
const [updateError, setUpdateError] = useState('')
useEffect(() => {
window.electronAPI.getSettings().then(setSettings)
window.electronAPI.getAppVersion().then(setAppVersion)
}, [])
const handleCheckForUpdates = async () => {
setUpdateStatus('checking')
setUpdateError('')
try {
const result = await window.electronAPI.checkForUpdates()
if (result.error) {
setUpdateStatus('error')
setUpdateError(result.error)
} else if (result.available) {
setUpdateStatus('available')
setUpdateVersion(result.version || '')
} else {
setUpdateStatus('not-available')
}
} catch (err) {
setUpdateStatus('error')
setUpdateError('Ошибка проверки')
}
}
const handleInstallUpdate = () => {
window.electronAPI.installUpdate()
}
const handleToggle = async (key: keyof AppSettings, value: boolean) => {
if (!settings) return
setIsSaving(true)
try {
await window.electronAPI.saveSettings({ [key]: value })
setSettings({ ...settings, [key]: value })
} finally {
setIsSaving(false)
}
}
const handleApiUrlChange = async (url: string) => {
if (!settings) return
setSettings({ ...settings, apiUrl: url })
}
const handleApiUrlSave = async () => {
if (!settings) return
setIsSaving(true)
try {
await window.electronAPI.saveSettings({ apiUrl: settings.apiUrl })
} finally {
setIsSaving(false)
}
}
const handleLogout = async () => {
await logout()
navigate('/login')
}
if (!settings) {
return (
<div className="flex items-center justify-center py-12">
<div className="animate-spin w-8 h-8 border-2 border-neon-500 border-t-transparent rounded-full" />
</div>
)
}
return (
<div className="space-y-4">
{/* Header */}
<div className="flex items-center gap-2">
<Settings className="w-6 h-6 text-neon-500" />
<h1 className="text-xl font-display font-bold text-white">Настройки</h1>
</div>
{/* User info */}
<GlassCard variant="neon" className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-neon-500/20 flex items-center justify-center">
<span className="text-lg font-bold text-neon-400">
{user?.nickname?.charAt(0).toUpperCase() || 'U'}
</span>
</div>
<div>
<p className="font-medium text-white">{user?.nickname}</p>
<p className="text-xs text-gray-400">@{user?.login}</p>
</div>
</div>
<NeonButton variant="ghost" size="sm" icon={<LogOut className="w-4 h-4" />} onClick={handleLogout}>
Выйти
</NeonButton>
</GlassCard>
{/* Settings */}
<div className="space-y-2">
{/* Auto-launch */}
<GlassCard variant="dark" className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-accent-500/10 flex items-center justify-center">
<Power className="w-5 h-5 text-accent-500" />
</div>
<div>
<p className="font-medium text-white">Автозапуск</p>
<p className="text-xs text-gray-400">Запускать при старте Windows</p>
</div>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={settings.autoLaunch}
onChange={(e) => handleToggle('autoLaunch', e.target.checked)}
className="sr-only peer"
disabled={isSaving}
/>
<div className="w-11 h-6 bg-dark-600 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-neon-500"></div>
</label>
</GlassCard>
{/* Minimize to tray */}
<GlassCard variant="dark" className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-neon-500/10 flex items-center justify-center">
<Monitor className="w-5 h-5 text-neon-500" />
</div>
<div>
<p className="font-medium text-white">Сворачивать в трей</p>
<p className="text-xs text-gray-400">При закрытии скрывать в трей</p>
</div>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={settings.minimizeToTray}
onChange={(e) => handleToggle('minimizeToTray', e.target.checked)}
className="sr-only peer"
disabled={isSaving}
/>
<div className="w-11 h-6 bg-dark-600 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-neon-500"></div>
</label>
</GlassCard>
{/* Tracking interval */}
<GlassCard variant="dark">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 rounded-lg bg-pink-500/10 flex items-center justify-center">
<Clock className="w-5 h-5 text-pink-500" />
</div>
<div>
<p className="font-medium text-white">Интервал проверки</p>
<p className="text-xs text-gray-400">Как часто проверять процессы</p>
</div>
</div>
<select
value={settings.trackingInterval}
onChange={(e) => {
const value = Number(e.target.value)
setSettings({ ...settings, trackingInterval: value })
window.electronAPI.saveSettings({ trackingInterval: value })
}}
className="w-full bg-dark-700 border border-dark-600 rounded-lg px-4 py-2 text-white"
>
<option value={3000}>3 секунды</option>
<option value={5000}>5 секунд</option>
<option value={10000}>10 секунд</option>
<option value={30000}>30 секунд</option>
</select>
</GlassCard>
{/* Updates */}
<GlassCard variant="dark">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-green-500/10 flex items-center justify-center">
<Download className="w-5 h-5 text-green-500" />
</div>
<div>
<p className="font-medium text-white">Обновления</p>
<p className="text-xs text-gray-400">
{updateStatus === 'checking' && 'Проверка...'}
{updateStatus === 'available' && `Доступна v${updateVersion}`}
{updateStatus === 'not-available' && 'Актуальная версия'}
{updateStatus === 'error' && (updateError || 'Ошибка')}
{updateStatus === 'idle' && `Текущая версия: v${appVersion}`}
</p>
</div>
</div>
{updateStatus === 'available' ? (
<NeonButton size="sm" onClick={handleInstallUpdate}>
Установить
</NeonButton>
) : (
<button
onClick={handleCheckForUpdates}
disabled={updateStatus === 'checking'}
className="p-2 rounded-lg bg-dark-700 text-gray-400 hover:text-white hover:bg-dark-600 transition-colors disabled:opacity-50"
>
{updateStatus === 'checking' ? (
<RefreshCw className="w-5 h-5 animate-spin" />
) : updateStatus === 'not-available' ? (
<Check className="w-5 h-5 text-green-500" />
) : updateStatus === 'error' ? (
<AlertCircle className="w-5 h-5 text-red-500" />
) : (
<RefreshCw className="w-5 h-5" />
)}
</button>
)}
</div>
</GlassCard>
{/* API URL (for developers) */}
<GlassCard variant="dark">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 rounded-lg bg-gray-500/10 flex items-center justify-center">
<Globe className="w-5 h-5 text-gray-400" />
</div>
<div>
<p className="font-medium text-white">API URL</p>
<p className="text-xs text-gray-400">Для разработки</p>
</div>
</div>
<div className="flex gap-2">
<Input
value={settings.apiUrl}
onChange={(e) => handleApiUrlChange(e.target.value)}
placeholder="http://localhost:8000/api/v1"
className="flex-1"
/>
<NeonButton
variant="secondary"
size="sm"
onClick={handleApiUrlSave}
disabled={isSaving}
>
Сохранить
</NeonButton>
</div>
</GlassCard>
</div>
{/* Version */}
<p className="text-center text-gray-500 text-xs pt-4">
Game Marathon Tracker v{appVersion || '...'}
</p>
</div>
)
}