Files
game-marathon/desktop/src/renderer/pages/SettingsPage.tsx
2026-01-10 08:48:52 +07:00

268 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}