/** * SyncStatusButton - Cloud Sync Status Indicator for Top Bar * * Shows current sync state with cloud icon and colored indicators: * - Green dot: All synced * - Blue dot + spin: Syncing in progress * - Red dot: Error * - Gray dot: No providers connected * * Clicking opens a popover with sync status details and history. */ import React, { useState } from 'react'; import { Cloud, CloudOff, Github, Loader2, RefreshCw, Settings, X, ArrowUp, ArrowDown, Database, Server, } from 'lucide-react'; import { useCloudSync } from '../application/state/useCloudSync'; import type { CloudProvider } from '../domain/sync'; import { useI18n } from '../application/i18n/I18nProvider'; import { cn } from '../lib/utils'; import { Button } from './ui/button'; import { Popover, PopoverContent, PopoverTrigger, } from './ui/popover'; import { toast } from './ui/toast'; // ============================================================================ // Provider Icons // ============================================================================ const GoogleDriveIcon: React.FC<{ className?: string }> = ({ className }) => ( ); const OneDriveIcon: React.FC<{ className?: string }> = ({ className }) => ( ); const providerIcons: Record = { github: , google: , onedrive: , webdav: , s3: , }; const providerNames: Record = { github: 'GitHub Gist', google: 'Google Drive', onedrive: 'OneDrive', webdav: 'WebDAV', s3: 'S3 Compatible', }; // ============================================================================ // Status Dot Component // ============================================================================ interface StatusIndicatorProps { status: 'synced' | 'syncing' | 'error' | 'none'; size?: 'sm' | 'md'; className?: string; } const StatusIndicator: React.FC = ({ status, size = 'sm', className }) => { const sizeClass = size === 'sm' ? 'w-2 h-2' : 'w-2.5 h-2.5'; const baseClasses = cn( 'rounded-full', sizeClass, status === 'syncing' && 'animate-pulse', className ); const colors = { synced: 'bg-green-500', syncing: 'bg-blue-500', error: 'bg-red-500', none: 'bg-muted-foreground/30', }; return ; }; // ============================================================================ // Main SyncStatusButton Component // ============================================================================ interface SyncStatusButtonProps { onOpenSettings?: () => void; onSyncNow?: () => Promise; // Callback to trigger sync with current data className?: string; } export const SyncStatusButton: React.FC = ({ onOpenSettings, onSyncNow, className, }) => { const { t } = useI18n(); const [isOpen, setIsOpen] = useState(false); const [isSyncingManual, setIsSyncingManual] = useState(false); const sync = useCloudSync(); // State is now automatically synced via useSyncExternalStore - no manual refresh needed // Get connected provider (include syncing status as it's still connected) const getConnectedProvider = (): CloudProvider | null => { const isProviderActive = (status: string) => status === 'connected' || status === 'syncing'; if (isProviderActive(sync.providers.github.status)) return 'github'; if (isProviderActive(sync.providers.google.status)) return 'google'; if (isProviderActive(sync.providers.onedrive.status)) return 'onedrive'; if (isProviderActive(sync.providers.webdav.status)) return 'webdav'; if (isProviderActive(sync.providers.s3.status)) return 's3'; return null; }; const connectedProvider = getConnectedProvider(); const providerConnection = connectedProvider ? sync.providers[connectedProvider] : null; // Determine overall status for the button indicator const getOverallStatus = (): StatusIndicatorProps['status'] => { if (sync.isSyncing) return 'syncing'; if (sync.lastError) return 'error'; if (sync.hasAnyConnectedProvider) return 'synced'; return 'none'; }; const overallStatus = getOverallStatus(); // Get the button icon based on state const getButtonIcon = () => { if (sync.isSyncing) return ; if (sync.hasAnyConnectedProvider) return ; return ; }; const formatTime = (timestamp?: number): string => { if (!timestamp) return t('time.never'); const diff = Date.now() - timestamp; if (diff < 60000) return t('time.justNow'); if (diff < 3600000) return t('time.minutesAgo', { minutes: Math.floor(diff / 60000) }); return new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }; // Create a unique key based on sync state to force re-render const syncStateKey = `${sync.localVersion}-${sync.remoteVersion}-${sync.syncHistory.length}`; return ( {getButtonIcon()} {/* Status indicator dot */} {/* Header */} {overallStatus === 'synced' && ( )} {overallStatus === 'syncing' && ( )} {overallStatus === 'error' && ( )} {overallStatus === 'none' && ( )} {overallStatus === 'synced' && t('sync.active')} {overallStatus === 'syncing' && t('sync.syncing')} {overallStatus === 'error' && t('sync.error')} {overallStatus === 'none' && t('sync.notConfigured')} {onOpenSettings && ( { setIsOpen(false); onOpenSettings(); }} className="p-1 rounded hover:bg-muted transition-colors" title={t('sync.settings')} > )} {/* Content based on state */} {!sync.hasAnyConnectedProvider ? ( // No provider connected {t('sync.notConfigured')} {t('sync.autoSync.noProvider')} { setIsOpen(false); onOpenSettings?.(); }} > {t('sync.settings')} ) : ( // Provider connected - show details {/* Connected Provider Info */} {connectedProvider && providerConnection && ( {providerIcons[connectedProvider]} {providerNames[connectedProvider]} {providerConnection.account?.avatarUrl && ( )} {providerConnection.account?.name || providerConnection.account?.email || t('sync.connected')} )} {/* Version Info */} {t('cloudSync.conflict.local')} v{sync.localVersion} {formatTime(sync.localUpdatedAt)} {t('cloudSync.conflict.cloud')} v{sync.remoteVersion} {formatTime(sync.remoteUpdatedAt)} {/* Recent Sync History */} {sync.syncHistory.length > 0 && ( {t('sync.recentActivity')} {sync.syncHistory.slice(0, 5).map((entry) => ( {entry.success ? ( entry.action === 'upload' ? : ) : ( )} {entry.action === 'upload' ? t('sync.history.uploaded') : entry.action === 'download' ? t('sync.history.downloaded') : t('sync.history.resolved')}{' '} v{entry.localVersion} {formatTime(entry.timestamp)} ))} )} {/* Sync Button */} { if (onSyncNow) { setIsSyncingManual(true); try { await onSyncNow(); toast.success(t('sync.toast.completedMessage'), t('sync.cloudSync')); } catch (error) { toast.error( error instanceof Error ? error.message : t('sync.failed'), t('sync.toast.errorTitle'), ); } finally { setIsSyncingManual(false); } } else { setIsOpen(false); onOpenSettings?.(); } }} > {(sync.isSyncing || isSyncingManual) ? ( ) : ( )} {t('sync.syncNow')} )} ); }; export default SyncStatusButton;
{t('sync.notConfigured')}
{t('sync.autoSync.noProvider')}