import { useEffect, useState } from 'react'; import { Upload, FolderOpen, Loader2, ChevronsUpDown } from 'lucide-react'; import { GetWebPublishConfig, SaveWebPublishConfig, WebPublishColumns, TestWebPublishFTP, PublishLogNow, GetWebPublishStatus, PickBackupFolder, } from '../../wailsjs/go/main/App'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Checkbox } from '@/components/ui/checkbox'; import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select'; import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuCheckboxItem, } from '@/components/ui/dropdown-menu'; import { useI18n } from '@/lib/i18n'; import { cn } from '@/lib/utils'; type Cfg = { enabled: boolean; format: string; folder: string; file_name: string; title: string; count: number; interval_min: number; columns: string[]; ftp_enabled: boolean; ftp_host: string; ftp_port: number; ftp_user: string; ftp_password: string; ftp_tls: boolean; ftp_folder: string; ftp_file_name: string; }; type Col = { key: string; header: string; group: string }; export function WebPublishPanel() { const { t } = useI18n(); const [cfg, setCfg] = useState(null); const [cols, setCols] = useState([]); // The catalogue is 123 fields, so the picker is a dropdown with a search box // rather than anything laid out on the page. Chosen columns stay visible above // it, in publication order: after picking eight out of a hundred the question // stops being 'what exists' and becomes 'what did I pick, and how will it print'. const [colSearch, setColSearch] = useState(''); const [busy, setBusy] = useState<'' | 'test' | 'publish'>(''); const [msg, setMsg] = useState(''); const [err, setErr] = useState(''); // Raw text for the two numeric boxes: bound straight to the number they could // not be cleared, the same trap as the Recent-QSOs Max field. const [countText, setCountText] = useState(''); const [everyText, setEveryText] = useState(''); useEffect(() => { (async () => { try { const [c, k, st] = await Promise.all([GetWebPublishConfig(), WebPublishColumns(), GetWebPublishStatus()]); setCfg(c as any); setCols((k ?? []) as Col[]); setCountText(String((c as any).count ?? 100)); setEveryText(String((c as any).interval_min ?? 0)); const s: any = st; if (s?.last_err) setErr(s.last_err); else if (s?.last_run) setMsg(t('wpub.lastRun') + ' ' + s.last_run); } catch (e: any) { setErr(String(e?.message ?? e)); } })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); if (!cfg) return

; const set = (p: Partial) => setCfg((c) => (c ? { ...c, ...p } : c)); // Every control saves immediately: this panel has no Save button of its own, // matching the other "saved instantly" panels. const save = async (next: Cfg) => { setCfg(next); try { await SaveWebPublishConfig(next as any); setErr(''); } catch (e: any) { setErr(String(e?.message ?? e)); } }; const patch = (p: Partial) => save({ ...cfg, ...p }); const toggleCol = (key: string) => { const has = cfg.columns?.includes(key); patch({ columns: has ? cfg.columns.filter((c) => c !== key) : [...(cfg.columns ?? []), key] }); }; const run = async (what: 'test' | 'publish') => { setBusy(what); setMsg(''); setErr(''); try { const r = what === 'test' ? await TestWebPublishFTP(cfg as any) : await PublishLogNow(); setMsg(String(r)); } catch (e: any) { setErr(String(e?.message ?? e)); } finally { setBusy(''); } }; return (

{t('wpub.hint')}

{cfg.enabled && (<> {/* ── The file ── */}
set({ folder: e.target.value })} onBlur={() => patch({ folder: cfg.folder })} />
set({ file_name: e.target.value })} onBlur={() => patch({ file_name: cfg.file_name })} /> set({ title: e.target.value })} onBlur={() => patch({ title: cfg.title })} /> setCountText(e.target.value)} onBlur={() => { const n = Math.floor(Number(countText)); if (Number.isFinite(n) && n > 0) patch({ count: n }); else setCountText(String(cfg.count)); }} />
setEveryText(e.target.value)} onBlur={() => { const n = Math.floor(Number(everyText)); if (Number.isFinite(n) && n >= 0) patch({ interval_min: n }); else setEveryText(String(cfg.interval_min)); }} /> {t('wpub.everyHint')}
{/* ── Columns ── */}
{t('wpub.columnsCount', { n: cfg.columns?.length ?? 0, total: cols.length })}
{/* CHOSEN, in publication order — this is the list that answers "what will the page look like", which a sectioned catalogue cannot. */} {(cfg.columns?.length ?? 0) > 0 && (
{cfg.columns.map((k) => { const c = cols.find((x) => x.key === k); return ( ); })}
)} {/* One dropdown, alphabetical, filtered as you type. The catalogue is 123 fields: laid out on the page it buries every other setting, and sorting by anything but the alphabet means hunting. The menu stays open while ticking — picking eight columns should be one visit. */} {/* The search box is OUTSIDE the scrolling area, not sticky inside it. Sticky kept it in place but the rows scrolled over it: a menu item carries its own background and its own stacking, so it wins over a sticky sibling however high its z-index. A header that never scrolls has nothing to lose the fight with. */}
setColSearch(e.target.value)} onKeyDown={(e) => e.stopPropagation()} />
{[...cols] .sort((a, b) => a.header.localeCompare(b.header)) .filter((c) => { const q = colSearch.trim().toLowerCase(); return !q || c.header.toLowerCase().includes(q) || c.key.toLowerCase().includes(q); }) .map((c) => ( toggleCol(c.key)} onSelect={(e) => e.preventDefault()} className="text-xs" > {c.header} {c.group} ))}

{t('wpub.columnsHint')}

{/* ── Upload ── */}
{cfg.ftp_enabled && (
set({ ftp_host: e.target.value })} onBlur={() => patch({ ftp_host: cfg.ftp_host })} /> set({ ftp_port: Number(e.target.value) || 21 })} onBlur={() => patch({ ftp_port: cfg.ftp_port })} />
set({ ftp_user: e.target.value })} onBlur={() => patch({ ftp_user: cfg.ftp_user })} /> set({ ftp_password: e.target.value })} onBlur={() => patch({ ftp_password: cfg.ftp_password })} /> set({ ftp_folder: e.target.value })} onBlur={() => patch({ ftp_folder: cfg.ftp_folder })} /> set({ ftp_file_name: e.target.value })} onBlur={() => patch({ ftp_file_name: cfg.ftp_file_name })} />
)}
{cfg.ftp_enabled && ( )}
)} {msg &&

{msg}

} {err &&

{err}

}
); }