import React, { useEffect, useState } from 'react' import { Loader2, Plane, Save } from 'lucide-react' import { useTranslation } from '../../i18n' import { useToast } from '../shared/Toast' import { airtrailApi } from '../../api/client' import AirTrailIcon from '../shared/AirTrailIcon' import Section from './Section' import ToggleSwitch from './ToggleSwitch' import { SETTINGS_BUTTON, SETTINGS_BUTTON_PRIMARY, SettingRow, SettingRows, SettingsHint, StatusPill } from './settingsKit' import { EditorField, GRID_2, INPUT } from '../shared/dialogParts' import { fs } from '../shared/DialogShell' /** * Settings → Integrations → AirTrail. Per-user connection to a self-hosted * AirTrail instance (URL + Bearer API key). Mirrors the photo-provider (Immich) * connection layout: the two fields side by side, the switches as rows, then * Save / Test-connection, with the connection state as a pill in the card's band. The key is stored encrypted and never prefilled. */ export default function AirTrailConnectionSection(): React.ReactElement { const { t } = useTranslation() const toast = useToast() const [url, setUrl] = useState('') const [apiKey, setApiKey] = useState('') const [allowInsecureTls, setAllowInsecureTls] = useState(false) const [writeEnabled, setWriteEnabled] = useState(false) const [connected, setConnected] = useState(false) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [testing, setTesting] = useState(false) useEffect(() => { airtrailApi .getSettings() .then(d => { setUrl(d.url || '') setAllowInsecureTls(!!d.allowInsecureTls) setWriteEnabled(!!d.writeEnabled) setConnected(!!d.connected) }) .catch(() => {}) .finally(() => setLoading(false)) }, []) // Send the key only when the user typed a new one — never prefilled, so a blank // field means "keep the stored key". const keyPayload = (): { apiKey?: string } => { const k = apiKey.trim() return k ? { apiKey: k } : {} } const handleSave = async () => { setSaving(true) try { const d = await airtrailApi.saveSettings({ url: url.trim(), allowInsecureTls, writeEnabled, ...keyPayload() }) const status = await airtrailApi.status().catch(() => ({ connected: false })) setConnected(!!status.connected) setApiKey('') if (d?.warning) toast.warning(d.warning) else toast.success(t('settings.airtrail.toast.saved')) } catch (err) { const reason = (err as { response?: { data?: { error?: string } } } | null)?.response?.data?.error toast.error(reason || t('settings.airtrail.toast.saveError')) } finally { setSaving(false) } } const handleTest = async () => { setTesting(true) try { const d = await airtrailApi.test({ url: url.trim(), allowInsecureTls, ...keyPayload() }) setConnected(!!d.connected) if (d.connected) toast.success(t('settings.airtrail.test.success', { count: d.flightCount ?? 0 })) else toast.error(d.error || t('settings.airtrail.test.failed')) } catch { toast.error(t('settings.airtrail.test.failed')) } finally { setTesting(false) } } const canSave = !!url.trim() && (connected || !!apiKey.trim()) const fieldId = (name: string) => `airtrail-${name}` return (
}> {connected ? t('settings.airtrail.connected') : t('settings.airtrail.notConnected')} } >
setUrl(e.target.value)} placeholder="https://airtrail.example.com" className={INPUT} /> setApiKey(e.target.value)} autoComplete="off" placeholder={connected && !apiKey ? '••••••••' : t('settings.airtrail.apiKeyPlaceholder')} className={INPUT} />
setAllowInsecureTls(v => !v)} label={t('settings.airtrail.allowInsecureTls')} />} /> setWriteEnabled(v => !v)} label={t('settings.airtrail.writeBack')} />} />
{t('settings.airtrail.hint')}
) }