/** * 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 ( {/* 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 && ( )}
{/* Content based on state */}
{!sync.hasAnyConnectedProvider ? ( // No provider connected

{t('sync.notConfigured')}

{t('sync.autoSync.noProvider')}

) : ( // 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 */}
)}
); }; export default SyncStatusButton;