import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mirror_protocol/mirror_protocol.dart'; import 'package:mirror_ui/mirror_ui.dart'; import '../providers/display_state_provider.dart'; import '../providers/layout_provider.dart'; class DashboardScreen extends ConsumerStatefulWidget { const DashboardScreen({super.key}); @override ConsumerState createState() => _DashboardScreenState(); } class _DashboardScreenState extends ConsumerState { late Stream _clockStream; @override void initState() { super.initState(); _clockStream = Stream.periodic( const Duration(seconds: 1), (_) => DateTime.now(), ); } @override Widget build(BuildContext context) { final layoutAsync = ref.watch(layoutProvider); return Container( color: MirrorTheme.background, width: double.infinity, height: double.infinity, child: layoutAsync.when( loading: () => const Center( child: CircularProgressIndicator(color: MirrorTheme.accent), ), error: (_, __) => _buildWithLayout(DashboardLayoutConfig.defaultLayout()), data: (layout) => _buildWithLayout(layout), ), ); } Widget _buildWithLayout(DashboardLayoutConfig layout) { final visibleWidgets = layout.widgets .where((w) => w.visible) .toList() ..sort((a, b) => a.position.compareTo(b.position)); return SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 80, vertical: 60), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ for (int i = 0; i < visibleWidgets.length; i++) ...[ if (i > 0) const SizedBox(height: 32), _buildWidget(visibleWidgets[i]), ], ], ), ); } Widget _buildWidget(DashboardWidgetConfig widget) { return switch (widget.type) { 'clock' => _ClockWidget(clockStream: _clockStream), 'weather' => _WeatherWidget(config: widget.config), 'calendar' => const _CalendarWidget(), 'ha_entities' => _HaEntitiesWidget(config: widget.config), 'now_playing' => const _NowPlayingWidget(), _ => const SizedBox.shrink(), }; } } // --- Clock Widget --- class _ClockWidget extends StatelessWidget { final Stream clockStream; const _ClockWidget({required this.clockStream}); @override Widget build(BuildContext context) { return StreamBuilder( stream: clockStream, builder: (context, snapshot) { final now = snapshot.data ?? DateTime.now(); return Column( children: [ Text( _formatDate(now), style: Theme.of(context).textTheme.headlineSmall?.copyWith( color: MirrorTheme.textSecondary, ), ), const SizedBox(height: 16), Text( _formatTime(now), style: Theme.of(context).textTheme.displayLarge?.copyWith( fontSize: 120, fontWeight: FontWeight.w300, ), ), ], ); }, ); } String _formatTime(DateTime dt) { final hour = dt.hour > 12 ? dt.hour - 12 : (dt.hour == 0 ? 12 : dt.hour); final min = dt.minute.toString().padLeft(2, '0'); return '$hour:$min'; } String _formatDate(DateTime dt) { const days = [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', ]; const months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ]; return '${days[dt.weekday % 7]}, ${months[dt.month - 1]} ${dt.day}'; } } // --- Weather Widget --- class _WeatherWidget extends ConsumerWidget { final Map config; const _WeatherWidget({required this.config}); @override Widget build(BuildContext context, WidgetRef ref) { final dashboard = ref.watch(displayStateProvider.select((s) => s.dashboard)); final weather = dashboard.weather; final temp = weather?.temp ?? 72.0; final condition = weather?.condition ?? 'clear'; final humidity = weather?.humidity ?? 45; return Container( width: double.infinity, padding: const EdgeInsets.all(32), decoration: BoxDecoration( color: MirrorTheme.surface, borderRadius: BorderRadius.circular(16), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( _weatherIcon(condition), size: 56, color: MirrorTheme.accent, ), const SizedBox(width: 32), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${temp.round()}°', style: Theme.of(context).textTheme.displaySmall?.copyWith( fontWeight: FontWeight.w300, ), ), const SizedBox(height: 4), Text( _capitalizeCondition(condition), style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: MirrorTheme.textSecondary, ), ), ], ), const SizedBox(width: 48), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.water_drop_outlined, size: 20, color: MirrorTheme.textMuted), const SizedBox(width: 8), Text( '$humidity%', style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: MirrorTheme.textSecondary, ), ), ], ), ], ), ], ), ); } IconData _weatherIcon(String condition) => switch (condition.toLowerCase()) { 'clear' || 'sunny' => Icons.wb_sunny, 'cloudy' || 'overcast' => Icons.cloud, 'partlycloudy' || 'partly_cloudy' => Icons.cloud_queue, 'rainy' || 'rain' => Icons.water_drop, 'snowy' || 'snow' => Icons.ac_unit, 'stormy' || 'thunderstorm' => Icons.flash_on, 'foggy' || 'fog' => Icons.blur_on, 'windy' => Icons.air, _ => Icons.wb_sunny, }; String _capitalizeCondition(String condition) { if (condition.isEmpty) return ''; return condition[0].toUpperCase() + condition.substring(1); } } // --- Calendar Widget --- class _CalendarWidget extends ConsumerWidget { const _CalendarWidget(); @override Widget build(BuildContext context, WidgetRef ref) { final events = ref.watch( displayStateProvider.select((s) => s.dashboard.calendarEvents)); if (events.isEmpty) { return Container( width: double.infinity, padding: const EdgeInsets.all(32), decoration: BoxDecoration( color: MirrorTheme.surface, borderRadius: BorderRadius.circular(16), ), child: Center( child: Text( 'No upcoming events', style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: MirrorTheme.textMuted, ), ), ), ); } return Container( width: double.infinity, padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: MirrorTheme.surface, borderRadius: BorderRadius.circular(16), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.calendar_today, size: 20, color: MirrorTheme.accent), const SizedBox(width: 12), Text( 'UPCOMING', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: MirrorTheme.accent, letterSpacing: 2, ), ), ], ), const SizedBox(height: 20), ...events.take(3).map((event) => Padding( padding: const EdgeInsets.only(bottom: 16), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (event.startTime.isNotEmpty) SizedBox( width: 80, child: Text( _formatEventTime(event.startTime), style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontFamily: MirrorTheme.fontMono, color: MirrorTheme.textSecondary, ), ), ), if (event.startTime.isNotEmpty) const SizedBox(width: 16), Expanded( child: Text( event.summary, style: Theme.of(context).textTheme.bodyLarge, maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), )), ], ), ); } String _formatEventTime(String isoTime) { try { final dt = DateTime.parse(isoTime); final hour = dt.hour > 12 ? dt.hour - 12 : (dt.hour == 0 ? 12 : dt.hour); final min = dt.minute.toString().padLeft(2, '0'); final period = dt.hour >= 12 ? 'PM' : 'AM'; return '$hour:$min $period'; } catch (_) { return isoTime; } } } // --- HA Entities Widget --- class _HaEntitiesWidget extends ConsumerWidget { final Map config; const _HaEntitiesWidget({required this.config}); @override Widget build(BuildContext context, WidgetRef ref) { final haEntities = ref.watch(displayStateProvider.select((s) => s.dashboard.haEntities)); // Filter to only show entities listed in widget config final configuredEntities = (config['entities'] as List?)?.cast() ?? []; final entitiesToShow = configuredEntities.isEmpty ? haEntities : haEntities.where((e) { final id = e['entity_id'] as String? ?? ''; return configuredEntities.contains(id); }).toList(); if (entitiesToShow.isEmpty) { return Container( width: double.infinity, padding: const EdgeInsets.all(32), decoration: BoxDecoration( color: MirrorTheme.surface, borderRadius: BorderRadius.circular(16), ), child: Center( child: Text( 'No entities configured', style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: MirrorTheme.textMuted, ), ), ), ); } return Container( width: double.infinity, padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: MirrorTheme.surface, borderRadius: BorderRadius.circular(16), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.sensors, size: 20, color: MirrorTheme.accent), const SizedBox(width: 12), Text( 'HOME', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: MirrorTheme.accent, letterSpacing: 2, ), ), ], ), const SizedBox(height: 20), ...entitiesToShow.map((entity) { final attrs = (entity['attributes'] as Map?) ?? {}; final friendlyName = attrs['friendly_name'] as String? ?? (entity['entity_id'] as String? ?? ''); final stateValue = entity['state'] as String? ?? 'unknown'; return Padding( padding: const EdgeInsets.only(bottom: 12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( friendlyName, style: Theme.of(context).textTheme.bodyLarge, maxLines: 1, overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 16), Text( stateValue, style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: MirrorTheme.accent, fontFamily: MirrorTheme.fontMono, ), ), ], ), ); }), ], ), ); } } // --- Now Playing Widget --- class _NowPlayingWidget extends ConsumerWidget { const _NowPlayingWidget(); @override Widget build(BuildContext context, WidgetRef ref) { final playback = ref.watch(displayStateProvider.select((s) => s.playback)); final spotify = ref.watch(displayStateProvider.select((s) => s.spotify)); // Show nothing if nothing is playing final hasPlayback = playback != null && playback.isPlaying; final hasSpotify = spotify != null && spotify.isPlaying; if (!hasPlayback && !hasSpotify) { return const SizedBox.shrink(); } final title = hasSpotify ? (spotify.trackName ?? 'Unknown Track') : (playback?.title ?? ''); final subtitle = hasSpotify ? (spotify.artistName ?? '') : ''; return Container( width: double.infinity, padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: MirrorTheme.surface, borderRadius: BorderRadius.circular(16), ), child: Row( children: [ Container( width: 48, height: 48, decoration: BoxDecoration( color: MirrorTheme.accentDim, borderRadius: BorderRadius.circular(8), ), child: const Icon( Icons.music_note, color: MirrorTheme.accent, size: 24, ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: Theme.of(context).textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.w500, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), if (subtitle.isNotEmpty) ...[ const SizedBox(height: 4), Text( subtitle, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: MirrorTheme.textSecondary, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ], ), ), const Icon( Icons.play_circle_filled, color: MirrorTheme.accent, size: 32, ), ], ), ); } }