import clsx from 'clsx'; import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { md5 } from 'js-md5'; import { type as osType } from '@tauri-apps/plugin-os'; import { useEnv } from '@/context/EnvContext'; import { useTranslation } from '@/hooks/useTranslation'; import { useSettingsStore } from '@/store/settingsStore'; import { eventDispatcher } from '@/utils/event'; import { KOSyncClient } from '@/services/sync/KOSyncClient'; import { KOSyncChecksumMethod, KOSyncStrategy } from '@/types/settings'; import { debounce } from '@/utils/debounce'; import { getOSPlatform } from '@/utils/misc'; import SubPageHeader from '../SubPageHeader'; import { SectionTitle, SettingLabel, SettingsSelect } from '../primitives'; interface KOSyncFormProps { onBack: () => void; } const KOSyncForm: React.FC = ({ onBack }) => { const _ = useTranslation(); const { settings, setSettings, saveSettings } = useSettingsStore(); const { envConfig, appService } = useEnv(); const [url, setUrl] = useState(settings.kosync.serverUrl || ''); const [username, setUsername] = useState(settings.kosync.username || ''); const [password, setPassword] = useState(''); const [isConnecting, setIsConnecting] = useState(false); const [deviceName, setDeviceName] = useState(''); const [osName, setOsName] = useState(''); useEffect(() => { const formatOsName = (name: string): string => { if (!name) return ''; if (name.toLowerCase() === 'macos') return 'macOS'; if (name.toLowerCase() === 'ios') return 'iOS'; return name.charAt(0).toUpperCase() + name.slice(1); }; const getOsName = async () => { let name = ''; if (appService?.appPlatform === 'tauri') { name = await osType(); } else { const platform = getOSPlatform(); if (platform !== 'unknown') { name = platform; } } setOsName(formatOsName(name)); }; getOsName(); }, [appService]); useEffect(() => { const defaultName = osName ? `Readest (${osName})` : 'Readest'; setDeviceName(settings.kosync.deviceName || defaultName); }, [settings.kosync.deviceName, osName]); const isConfigured = useMemo(() => !!settings.kosync.userkey, [settings.kosync.userkey]); // eslint-disable-next-line react-hooks/exhaustive-deps const debouncedSaveDeviceName = useCallback( debounce((newDeviceName: string) => { const newSettings = { ...settings, kosync: { ...settings.kosync, deviceName: newDeviceName }, }; setSettings(newSettings); saveSettings(envConfig, newSettings); }, 500), [settings, setSettings, saveSettings, envConfig], ); const handleDeviceNameChange = (e: React.ChangeEvent) => { const newName = e.target.value; setDeviceName(newName); debouncedSaveDeviceName(newName); }; const handleConnect = async () => { setIsConnecting(true); const config = { ...settings.kosync, serverUrl: url, username, userkey: md5(password), password, deviceName, enabled: true, }; const client = new KOSyncClient(config); const result = await client.connect(username, password); if (result.success) { const newSettings = { ...settings, kosync: config }; setSettings(newSettings); await saveSettings(envConfig, newSettings); } else { eventDispatcher.dispatch('toast', { message: `${_('Failed to connect')}: ${_(result.message || 'Connection error')}`, type: 'error', }); } setIsConnecting(false); setPassword(''); }; const handleDisconnect = async () => { const kosync = { ...settings.kosync, userkey: '', enabled: false }; const newSettings = { ...settings, kosync }; setSettings(newSettings); await saveSettings(envConfig, newSettings); setUsername(''); eventDispatcher.dispatch('toast', { message: _('Disconnected'), type: 'info' }); }; const handleToggleEnabled = async () => { const kosync = { ...settings.kosync, enabled: !settings.kosync.enabled }; const newSettings = { ...settings, kosync }; setSettings(newSettings); await saveSettings(envConfig, newSettings); }; const handleStrategyChange = async (e: React.ChangeEvent) => { const kosync = { ...settings.kosync, strategy: e.target.value as KOSyncStrategy }; const newSettings = { ...settings, kosync }; setSettings(newSettings); await saveSettings(envConfig, newSettings); }; const handleChecksumMethodChange = async (e: React.ChangeEvent) => { const kosync = { ...settings.kosync, checksumMethod: e.target.value as KOSyncChecksumMethod, }; const newSettings = { ...settings, kosync }; setSettings(newSettings); await saveSettings(envConfig, newSettings); }; const description: string = isConfigured ? _('Sync as {{userDisplayName}}', { userDisplayName: settings.kosync.username }) : _('Connect to your KOReader Sync server.'); return (
{isConfigured ? (
{/* Each row uses min-h-14 + items-center so toggle/select/input rows render at a uniform height regardless of the embedded control's intrinsic size. Selects and inputs are end-aligned to match modern preferences-panel convention. */} {/* SettingsSelect handles the chromeless treatment, the custom MdArrowDropDown icon at the trailing edge (so the chevron lands at the same X as the toggle's right edge), and the focus/hover signal — see DESIGN.md §5. */}
{_('Sync Strategy')}
{_('Checksum Method')}
{_('Device Name')}
) : (
{ e.preventDefault(); handleConnect(); }} >
{_('Server URL')} setUrl(e.target.value)} />
{_('Username')} setUsername(e.target.value)} autoComplete='username' />
{_('Password')} setPassword(e.target.value)} autoComplete='current-password' />
)}
); }; export default KOSyncForm;