import 'package:flutter/material.dart'; import 'app_theme_business_object.dart'; import 'live_info_business_object.dart'; class LiveInfoPanel extends StatelessWidget { const LiveInfoPanel({ super.key, required this.liveInfo, required this.themeColors, required this.intervalOptions, }); final LiveInfoBusinessObject liveInfo; final AppThemeColors themeColors; final List intervalOptions; @override Widget build(BuildContext context) { final LiveInfoSnapshot? snapshot = liveInfo.snapshot; final LiveInfoTrackInfo? current = snapshot?.current; final LiveInfoTrackInfo? next = snapshot?.next; final LiveInfoShowInfo? currentShow = snapshot?.currentShow.isEmpty ?? true ? null : snapshot!.currentShow.first; final LiveInfoShowInfo? nextShow = snapshot?.nextShow.isEmpty ?? true ? null : snapshot!.nextShow.first; return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ themeColors.controlsGradientStart.withValues(alpha: 0.9), themeColors.controlsGradientEnd.withValues(alpha: 0.95), ], ), boxShadow: [ BoxShadow( color: themeColors.controlsShadow, blurRadius: 20, offset: const Offset(0, 8), ), ], ), child: Padding( padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.graphic_eq_rounded, color: themeColors.statusText, size: 20, ), const SizedBox(width: 8), Expanded( child: Text( 'Live Studio Feed', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w800, color: themeColors.statusText, ), ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 5, ), decoration: BoxDecoration( borderRadius: BorderRadius.circular(999), color: Colors.black.withValues(alpha: 0.12), ), child: Text( snapshot?.sourceEnabled ?? 'Unknown Source', style: Theme.of(context).textTheme.labelSmall?.copyWith( fontWeight: FontWeight.w700, ), ), ), const SizedBox(width: 8), PopupMenuButton( tooltip: 'Set refresh interval', initialValue: liveInfo.refreshIntervalSeconds, onSelected: liveInfo.setRefreshIntervalSeconds, itemBuilder: (BuildContext context) { return intervalOptions .map( (int seconds) => PopupMenuItem( value: seconds, child: Text('$seconds seconds'), ), ) .toList(growable: false); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 6, ), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: Colors.black.withValues(alpha: 0.08), ), child: Text( '${liveInfo.refreshIntervalSeconds}s', style: Theme.of(context).textTheme.labelMedium?.copyWith( fontWeight: FontWeight.w700, ), ), ), ), IconButton( tooltip: 'Refresh now', onPressed: liveInfo.isLoading ? null : () => liveInfo.refresh(), icon: const Icon(Icons.refresh_rounded), ), ], ), const SizedBox(height: 8), if (liveInfo.errorMessage != null) Padding( padding: const EdgeInsets.only(bottom: 8), child: Text( 'Live info warning: ${liveInfo.errorMessage}', maxLines: 2, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.error, fontWeight: FontWeight.w600, ), ), ), Expanded( child: Row( children: [ Expanded( child: _TrackCard( label: 'Now Playing', track: current, titleColor: themeColors.statusText, ), ), const SizedBox(width: 8), Expanded( child: _TrackCard( label: 'Coming Next', track: next, titleColor: themeColors.statusText, ), ), ], ), ), const SizedBox(height: 8), _ShowCard( label: 'On Air Show', show: currentShow, titleColor: themeColors.statusText, ), const SizedBox(height: 6), _ShowCard( label: 'Up Next Show', show: nextShow, titleColor: themeColors.statusText, ), const SizedBox(height: 10), Text( 'Recently Played (runtime)', style: Theme.of( context, ).textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w700), ), const SizedBox(height: 6), Expanded( child: Container( decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(12), ), child: liveInfo.recentPlayed.isEmpty ? Center( child: Text( liveInfo.isLoading ? 'Loading recent items...' : 'No recent tracks captured yet', style: Theme.of(context).textTheme.bodySmall, ), ) : ListView.separated( itemCount: liveInfo.recentPlayed.length, separatorBuilder: (BuildContext context, int index) => Divider( height: 1, color: Colors.black.withValues(alpha: 0.08), ), itemBuilder: (BuildContext context, int index) { final RecentPlayedItem item = liveInfo.recentPlayed[index]; return ListTile( dense: true, contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 0, ), title: Text( item.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyMedium ?.copyWith(fontWeight: FontWeight.w600), ), subtitle: Text( item.subtitle.isEmpty ? _formatTimeWindow(item.starts, item.ends) : '${item.subtitle} | ${_formatTimeWindow(item.starts, item.ends)}', maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall, ), leading: const Icon(Icons.queue_music_rounded), ); }, ), ), ), const SizedBox(height: 6), Text( liveInfo.lastUpdatedAt == null ? 'Waiting for API response...' : 'Last update: ${liveInfo.lastUpdatedAt!.toLocal().toString().substring(11, 19)} • ${snapshot?.timezone ?? '--'}', style: Theme.of(context).textTheme.labelSmall, ), ], ), ), ); } } class _TrackCard extends StatelessWidget { const _TrackCard({ required this.label, required this.track, required this.titleColor, }); final String label; final LiveInfoTrackInfo? track; final Color titleColor; @override Widget build(BuildContext context) { final String title = track?.displayTitle ?? 'Unavailable'; final String subtitle = (track?.displaySubtitle ?? '').trim(); final String window = _formatTimeWindow(track?.starts, track?.ends); return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(14), border: Border.all(color: Colors.black.withValues(alpha: 0.12)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: Theme.of(context).textTheme.labelMedium?.copyWith( fontWeight: FontWeight.w700, color: titleColor, ), ), const SizedBox(height: 6), Text( title, maxLines: 2, overflow: TextOverflow.ellipsis, style: Theme.of( context, ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), ), const SizedBox(height: 3), Text( subtitle.isEmpty ? 'No metadata provided' : subtitle, maxLines: 2, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall, ), const Spacer(), Text( window, style: Theme.of( context, ).textTheme.labelMedium?.copyWith(fontWeight: FontWeight.w600), ), ], ), ); } } class _ShowCard extends StatelessWidget { const _ShowCard({ required this.label, required this.show, required this.titleColor, }); final String label; final LiveInfoShowInfo? show; final Color titleColor; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), color: Colors.black.withValues(alpha: 0.06), border: Border.all(color: Colors.black.withValues(alpha: 0.08)), ), child: Row( children: [ Icon(Icons.mic_external_on_rounded, color: titleColor, size: 18), const SizedBox(width: 8), Expanded( child: Text( '$label: ${show?.name ?? 'No scheduled show'}', maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of( context, ).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), ), ), const SizedBox(width: 8), Text( _formatTimeWindow(show?.starts, show?.ends), style: Theme.of(context).textTheme.labelSmall, ), ], ), ); } } String _formatTimeWindow(String? starts, String? ends) { String formatOne(String? value) { if (value == null || value.length < 16) { return '--:--'; } return value.substring(11, 16); } return '${formatOne(starts)} - ${formatOne(ends)}'; }