import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:tatlock_ui/features/front_hall/data/models/system_stats_model.dart'; import 'package:tatlock_ui/features/front_hall/presentation/providers/environment_provider.dart'; import 'package:tatlock_ui/features/front_hall/presentation/providers/system_stats_provider.dart'; import 'package:tatlock_ui/shared/theme/stoplight_colors.dart'; import 'package:tatlock_ui/shared/widgets/forecast_widget.dart'; import 'package:tatlock_ui/shared/widgets/sun_position_widget.dart'; import 'package:tatlock_ui/shared/widgets/widgets.dart'; import 'package:tatlock_ui/version.g.dart'; /// Dashboard content shown in Front Hall when mode is dashboard. /// /// Displays system stats with gauges, weather, air quality, and version info. /// Auto-refreshes system stats every 30 seconds. class DashboardContent extends ConsumerStatefulWidget { const DashboardContent({super.key}); @override ConsumerState createState() => _DashboardContentState(); } class _DashboardContentState extends ConsumerState { Timer? _systemStatsTimer; Timer? _environmentTimer; @override void initState() { super.initState(); // Refresh system stats every 30 seconds _systemStatsTimer = Timer.periodic( const Duration(seconds: 30), (_) => ref.invalidate(systemStatsProvider), ); // Refresh environment data every 5 minutes _environmentTimer = Timer.periodic( const Duration(minutes: 5), (_) => ref.invalidate(environmentProvider), ); } @override void dispose() { _systemStatsTimer?.cancel(); _environmentTimer?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final systemStatsAsync = ref.watch(systemStatsProvider); final environmentAsync = ref.watch(environmentProvider); return ListView( padding: const EdgeInsets.all(16), children: [ // Welcome card Card( child: Padding( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.waving_hand, size: 32, color: colorScheme.primary, ), const SizedBox(width: 12), Flexible( child: Text( 'Welcome to Tatlock', style: Theme.of(context).textTheme.headlineSmall, ), ), ], ), const SizedBox(height: 12), Text( 'Your homelab dashboard is ready.', style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: colorScheme.onSurfaceVariant, ), ), ], ), ), ), const SizedBox(height: 16), // System Stats - Gauges _SectionHeader(title: 'System Stats', icon: Icons.monitor_heart), const SizedBox(height: 8), systemStatsAsync.when( data: (stats) => _SystemStatsCard(stats: stats), loading: () => const Card( child: Padding( padding: EdgeInsets.all(32), child: Center(child: CircularProgressIndicator()), ), ), error: (error, _) => Card( child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Icon(Icons.error_outline, color: colorScheme.error), const SizedBox(width: 12), Expanded( child: Text( 'Failed to load system stats', style: TextStyle(color: colorScheme.error), ), ), IconButton( icon: const Icon(Icons.refresh), onPressed: () => ref.invalidate(systemStatsProvider), ), ], ), ), ), ), const SizedBox(height: 24), // Environment - Sun, Weather, Forecast, Air Quality _SectionHeader(title: 'Environment', icon: Icons.eco), const SizedBox(height: 8), environmentAsync.when( data: (envData) => _EnvironmentSection( envData: envData, onRefresh: () => ref.invalidate(environmentProvider), ), loading: () => const Card( child: Padding( padding: EdgeInsets.all(32), child: Center(child: CircularProgressIndicator()), ), ), error: (error, _) => Card( child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Icon(Icons.error_outline, color: colorScheme.error), const SizedBox(width: 12), Expanded( child: Text( 'Failed to load environment data', style: TextStyle(color: colorScheme.error), ), ), IconButton( icon: const Icon(Icons.refresh), onPressed: () => ref.invalidate(environmentProvider), ), ], ), ), ), ), const SizedBox(height: 24), // Version info Center( child: Text( '${AppVersion.name} v${AppVersion.fullVersion}', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.outline, ), ), ), ], ); } } /// Section header with icon and title. class _SectionHeader extends StatelessWidget { const _SectionHeader({ required this.title, required this.icon, }); final String title; final IconData icon; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Row( children: [ Icon( icon, size: 18, color: colorScheme.onSurfaceVariant, ), const SizedBox(width: 8), Text( title, style: Theme.of(context).textTheme.titleSmall?.copyWith( color: colorScheme.onSurfaceVariant, fontWeight: FontWeight.w500, ), ), ], ); } } /// Card displaying system stats with gauges. class _SystemStatsCard extends StatelessWidget { const _SystemStatsCard({required this.stats}); final SystemStats stats; @override Widget build(BuildContext context) { // Build gauges list: CPU, Memory, GPU (if available), then all disks final gauges = [ GaugeData( value: stats.cpu.usagePercent / 100, label: 'CPU', icon: Icons.memory, color: StoplightColors.forPercent(stats.cpu.usagePercent), ), GaugeData( value: stats.memory.usagePercent / 100, label: 'RAM', icon: Icons.storage, color: StoplightColors.forPercent(stats.memory.usagePercent), ), if (stats.gpu.available && stats.gpu.usagePercent != null) GaugeData( value: stats.gpu.usagePercent! / 100, label: 'VRAM', icon: Icons.videocam, color: StoplightColors.forPercent(stats.gpu.usagePercent!), ), // Add a gauge for each disk ...stats.disks.map( (disk) => GaugeData( value: disk.usagePercent / 100, label: _formatDiskLabel(disk), icon: Icons.disc_full, color: StoplightColors.forPercent(disk.usagePercent), ), ), ]; return Card( child: Padding( padding: const EdgeInsets.all(24), child: GaugeRow( gaugeSize: 120, gauges: gauges, ), ), ); } /// Formats disk label from mount point. String _formatDiskLabel(DiskStats disk) { final mount = disk.mountPoint; if (mount == '/') return 'Root'; if (mount == '/hostfs') return 'Host'; if (mount.startsWith('/hostfs/')) return mount.substring(8); if (mount.startsWith('/mnt/')) return mount.substring(5); if (mount.startsWith('/media/')) return mount.substring(7); // Return last path segment final parts = mount.split('/'); return parts.isNotEmpty ? parts.last : mount; } } /// Environment section displaying sun position, weather, forecast, and air quality. class _EnvironmentSection extends StatelessWidget { const _EnvironmentSection({ required this.envData, required this.onRefresh, }); final dynamic envData; final VoidCallback onRefresh; @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final isWide = constraints.maxWidth > 600; return Column( children: [ // Sun Position Widget - prominent at top SunPositionWidget(sunTimes: envData.sunTimes), const SizedBox(height: 12), // Weather and Forecast side-by-side on wide screens if (isWide) Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: WeatherWidget(apiData: envData.weather)), const SizedBox(width: 12), Expanded(child: ForecastWidget(forecast: envData.forecast)), ], ) else Column( children: [ WeatherWidget(apiData: envData.weather), const SizedBox(height: 12), ForecastWidget(forecast: envData.forecast), ], ), // Air Quality - only show if data is available if (envData.airQuality != null) ...[ const SizedBox(height: 12), AirQualityWidget(apiData: envData.airQuality), ], ], ); }, ); } }