import React, { useState, useEffect } from 'react'; import MassiveLibraryManager, { MassiveLibraryStatus } from '../services/MassiveLibraryManager'; import './OfflineModeIndicator.css'; interface OfflineModeIndicatorProps { className?: string; mode?: 'full' | 'compact' | 'orb'; } interface ConnectionState { isOnline: boolean; isOfflineMode: boolean; connectionQuality: 'excellent' | 'good' | 'poor' | 'offline'; } interface CacheRefreshState { isRefreshing: boolean; progress: number; totalSongs: number; refreshedSongs: number; } export const OfflineModeIndicator: React.FC = ({ className = '', mode = 'full' }) => { const [connectionState, setConnectionState] = useState({ isOnline: navigator.onLine, isOfflineMode: false, connectionQuality: 'excellent' }); const [cacheStatus, setCacheStatus] = useState(null); const [cacheRefreshState, setCacheRefreshState] = useState({ isRefreshing: false, progress: 0, totalSongs: 0, refreshedSongs: 0 }); const [libraryManager] = useState(() => new MassiveLibraryManager()); // Initialize library manager and load cache status useEffect(() => { const initializeLibrary = async () => { try { await libraryManager.initialize(); const status = await libraryManager.getLibraryStatus(); setCacheStatus(status); } catch (error) { console.error('Failed to initialize library manager:', error); } }; initializeLibrary(); }, [libraryManager]); // Monitor connection state useEffect(() => { const updateConnectionState = async () => { const isOnline = navigator.onLine; let connectionQuality: ConnectionState['connectionQuality'] = 'offline'; if (isOnline) { try { // Test connection quality with a lightweight ping const startTime = Date.now(); const response = await fetch('/api/ping', { method: 'HEAD', signal: AbortSignal.timeout(3000) }); const latency = Date.now() - startTime; if (response.ok) { if (latency < 100) connectionQuality = 'excellent'; else if (latency < 300) connectionQuality = 'good'; else connectionQuality = 'poor'; } else { connectionQuality = 'offline'; } } catch { connectionQuality = 'offline'; } } const isOfflineMode = connectionQuality === 'offline'; setConnectionState({ isOnline, isOfflineMode, connectionQuality }); }; // Initial check updateConnectionState(); // Listen for online/offline events const handleOnline = () => updateConnectionState(); const handleOffline = () => updateConnectionState(); window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); // Periodic connection quality check const intervalId = setInterval(updateConnectionState, 30000); // Check every 30 seconds return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); clearInterval(intervalId); }; }, []); // Listen for cache refresh events (triggered by shuffle toggle) useEffect(() => { const handleCacheRefreshStart = (event: CustomEvent) => { console.log('Cache refresh started:', event.detail); setCacheRefreshState({ isRefreshing: true, progress: 0, totalSongs: event.detail.totalSongs || 50, refreshedSongs: 0 }); }; const handleCacheRefreshProgress = (event: CustomEvent) => { console.log('Cache refresh progress:', event.detail); setCacheRefreshState(prev => ({ ...prev, progress: event.detail.progress || 0, refreshedSongs: event.detail.refreshedSongs || 0 })); }; const handleCacheRefreshComplete = async (event: CustomEvent) => { console.log('Cache refresh completed:', event.detail); setCacheRefreshState(prev => ({ ...prev, isRefreshing: false, progress: 100 })); // Refresh cache status try { const status = await libraryManager.getLibraryStatus(); setCacheStatus(status); } catch (error) { console.error('Failed to refresh cache status:', error); } // Clear progress after a short delay setTimeout(() => { setCacheRefreshState(prev => ({ ...prev, progress: 0, refreshedSongs: 0 })); }, 3000); }; const handleCacheRefreshError = (event: CustomEvent) => { console.error('Cache refresh error:', event.detail); setCacheRefreshState(prev => ({ ...prev, isRefreshing: false, progress: 0 })); }; // Listen for custom events window.addEventListener('cache-refresh-start', handleCacheRefreshStart as EventListener); window.addEventListener('cache-refresh-progress', handleCacheRefreshProgress as EventListener); window.addEventListener('cache-refresh-complete', handleCacheRefreshComplete as EventListener); window.addEventListener('cache-refresh-error', handleCacheRefreshError as EventListener); return () => { window.removeEventListener('cache-refresh-start', handleCacheRefreshStart as EventListener); window.removeEventListener('cache-refresh-progress', handleCacheRefreshProgress as EventListener); window.removeEventListener('cache-refresh-complete', handleCacheRefreshComplete as EventListener); window.removeEventListener('cache-refresh-error', handleCacheRefreshError as EventListener); }; }, [libraryManager]); // Format cache size for display const formatCacheSize = (bytes: number): string => { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; }; // Get connection status icon const getConnectionIcon = () => { switch (connectionState.connectionQuality) { case 'excellent': return '📶'; case 'good': return '📶'; case 'poor': return '📶'; case 'offline': return '📵'; default: return '📶'; } }; // Get connection status color class const getConnectionColorClass = () => { switch (connectionState.connectionQuality) { case 'excellent': return 'connection-excellent'; case 'good': return 'connection-good'; case 'poor': return 'connection-poor'; case 'offline': return 'connection-offline'; default: return 'connection-good'; } }; // Orb mode - just a small colored dot if (mode === 'orb') { return (
{cacheRefreshState.isRefreshing && (
)}
); } return (
{/* Connection Status */}
{getConnectionIcon()} {connectionState.isOfflineMode ? 'Offline Mode' : 'Online'}
{/* Cache Status */} {mode === 'full' && cacheStatus && (
💾 {cacheStatus.songCount} songs cached ({formatCacheSize(cacheStatus.totalSize)})
{connectionState.isOfflineMode && cacheStatus.songCount > 0 && (
🎵 {cacheStatus.songCount} songs available offline
)}
)} {/* Cache Refresh Progress */} {cacheRefreshState.isRefreshing && (
🔄 Refreshing cache...
{mode === 'full' && ( <>
{cacheRefreshState.refreshedSongs} / {cacheRefreshState.totalSongs} songs {Math.round(cacheRefreshState.progress)}%
)}
)} {/* Cache Refresh Complete Notification */} {!cacheRefreshState.isRefreshing && cacheRefreshState.progress === 100 && (
✅ Cache refreshed successfully!
)}
); }; export default OfflineModeIndicator;