268 lines
11 KiB
TypeScript
268 lines
11 KiB
TypeScript
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>
|
||
)
|
||
}
|