import { InlineView } from "@/components/surface/surface-scope"; import { useCallback, useEffect, useMemo, useState, type ReactNode, } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { AlertTriangle, Cloud, CloudOff, Loader2, RefreshCw, Unlink, } from "lucide-react"; import { Button } from "@/components/button"; import { Switch } from "@/components/switch"; import { useSyncStatus } from "@/hooks/use-sync-status"; import { refreshSyncStatus, setSyncStatus } from "@/lib/sync-status"; import { notifySyncChanged } from "@/lib/linked-server"; import { invalidateServerStatusCache } from "@/lib/hosts-request-cache"; import { getSyncConflicts, getSyncErrors, retrySyncErrors, settleSyncConflict, syncNow, unlinkServer, updateSyncSettings, type SyncBasicAuth, type SyncConflictItem, type SyncEntityInfo, type SyncErrorItem, type SyncProxyHeader, type SyncStatus, } from "@/api/sync-api"; import { LinkServerDialog } from "./LinkServerDialog"; import { ProxySettingsFields } from "./ProxySettingsFields"; type DesktopSettings = { defaultConnectionOrigin: "local" | "remote" }; function Section({ title, description, children, }: { title: string; description?: string; children: ReactNode; }) { return (
{title} {description && ( {description} )}
{children}
); } function useRelativeTime() { const { t } = useTranslation(); return (value: string | null | undefined) => { if (!value) return t("sync.never"); const seconds = Math.max( 0, Math.round((Date.now() - new Date(value).getTime()) / 1000), ); if (seconds < 60) return t("sync.justNow"); if (seconds < 3600) return t("sync.minutesAgo", { count: Math.round(seconds / 60) }); if (seconds < 86400) return t("sync.hoursAgo", { count: Math.round(seconds / 3600) }); return new Date(value).toLocaleString(); }; } function entityLabel( t: (key: string, options?: Record) => string, entity: SyncEntityInfo, ): string { return t(`sync.entities.${entity.type}`, { defaultValue: entity.type }); } export function SyncPanel() { const { t } = useTranslation(); const status = useSyncStatus(); const relativeTime = useRelativeTime(); const [linkOpen, setLinkOpen] = useState(false); const [reloginOpen, setReloginOpen] = useState(false); const [unlinkOpen, setUnlinkOpen] = useState(false); const [keepData, setKeepData] = useState(true); const [busy, setBusy] = useState(null); const [conflicts, setConflicts] = useState([]); const [errors, setErrors] = useState([]); const [desktopSettings, setDesktopSettings] = useState({ defaultConnectionOrigin: "local", }); const [proxyDraft, setProxyDraft] = useState<{ customHeaders: SyncProxyHeader[]; basicAuth: SyncBasicAuth | null; allowInvalidCertificate: boolean; } | null>(null); const apply = useCallback((next: SyncStatus) => { setSyncStatus(next); notifySyncChanged(); }, []); useEffect(() => { window.electronAPI ?.invoke?.("get-desktop-settings") .then((settings) => { if (settings) setDesktopSettings(settings as DesktopSettings); }) .catch(() => {}); }, []); const conflictCount = status?.conflicts ?? 0; const errorCount = status?.errors ?? 0; useEffect(() => { if (!status?.linked) return; getSyncConflicts() .then(setConflicts) .catch(() => {}); }, [status?.linked, conflictCount, status?.lastSyncAt]); useEffect(() => { if (!status?.linked) return; getSyncErrors() .then(setErrors) .catch(() => {}); }, [status?.linked, errorCount, status?.lastSyncAt]); const run = async (key: string, action: () => Promise) => { setBusy(key); try { await action(); } catch { toast.error(t("sync.actionFailed")); } finally { setBusy(null); } }; const groups = useMemo(() => { const byOwner = new Map(); for (const entity of status?.entities ?? []) { const key = entity.owner === "core" ? "core" : entity.owner; const list = byOwner.get(key) ?? []; list.push(entity); byOwner.set(key, list); } return [...byOwner.entries()].sort(([a], [b]) => a === "core" ? -1 : b === "core" ? 1 : a.localeCompare(b), ); }, [status?.entities]); if (!status) { return (
); } const linkDialog = ( ); if (!status.linked) { return (
{t("sync.unlinkedTitle")}

{t("sync.unlinkedBody")}

  • {t("sync.unlinkedPointHosts")}
  • {t("sync.unlinkedPointOffline")}
  • {t("sync.unlinkedPointAccount")}
{linkDialog}
); } const state = status.status ?? "idle"; const stateText = state === "syncing" ? t("sync.state.syncing") : state === "offline" ? t("sync.state.offline") : state === "error" ? t("sync.state.error") : state === "signed_out" ? t("sync.state.signedOut") : t("sync.state.synced", { time: relativeTime(status.lastSyncAt) }); const toggleType = (type: string, enabled: boolean) => run(`type:${type}`, async () => { const disabled = new Set( (status.entities ?? []) .filter((entity) => !entity.enabled) .map((entity) => entity.type), ); if (enabled) disabled.delete(type); else disabled.add(type); apply(await updateSyncSettings({ disabledTypes: [...disabled] })); }); const handleOriginChange = async (origin: "local" | "remote") => { const next = { ...desktopSettings, defaultConnectionOrigin: origin }; setDesktopSettings(next); await window.electronAPI?.invoke?.("save-desktop-settings", next); invalidateServerStatusCache(); window.dispatchEvent(new CustomEvent("hosts:refresh")); }; const draft = proxyDraft ?? { customHeaders: status.customHeaders ?? [], basicAuth: status.basicAuth ?? null, allowInvalidCertificate: !!status.allowInvalidCertificate, }; return (
{status.serverName || t("sync.server")} {status.serverUrl}
{t("sync.account")} {status.account?.username || "—"}
{t("sync.status")} {state === "syncing" && ( )} {stateText}
{status.lastError && state !== "idle" && state !== "syncing" && (

{status.lastError}

)} {!!status.pending && (

{t("sync.pending", { count: status.pending })}

)}
{state === "signed_out" ? ( ) : ( )}
{conflicts.length > 0 && (
{conflicts.map((conflict) => (
{conflict.name} {t(`sync.entities.${conflict.entityType}`, { defaultValue: conflict.entityType, })}
))}
)} {errors.length > 0 && (
{errors.map((error) => (
{t(`sync.entities.${error.entityType}`, { defaultValue: error.entityType, })} {error.reason === "permission" ? t("sync.errorPermission") : error.reason}
))}
)}
{groups.map(([owner, entities]) => (
{owner === "core" ? t("sync.coreGroup") : (entities[0]?.ownerName ?? owner)} {entities.map((entity) => (
{entityLabel(t, entity)} {!entity.onServer && ( {t("sync.notOnServer")} )} {entity.onServer && !entity.onDevice && ( {t("sync.notOnDevice")} )}
void toggleType(entity.type, checked) } />
))}
))}

{t("sync.localOnlyHint")}

{(["local", "remote"] as const).map((origin) => ( ))}
setProxyDraft({ ...draft, ...next })} />
{t("sync.proxy.allowInvalidCertificate")} {t("sync.proxy.allowInvalidCertificateHint")}
setProxyDraft({ ...draft, allowInvalidCertificate: checked }) } />
{proxyDraft && ( )}
} >

{t("sync.unlinkDescription")}

{( [ { value: true, title: t("sync.unlinkKeepTitle"), body: t("sync.unlinkKeepBody"), }, { value: false, title: t("sync.unlinkRemoveTitle"), body: t("sync.unlinkRemoveBody"), }, ] as const ).map((option) => ( ))}
{linkDialog} ); }