diff --git a/lib/services/data_service.dart b/lib/services/data_service.dart index 0bc99ca..43cf855 100644 --- a/lib/services/data_service.dart +++ b/lib/services/data_service.dart @@ -927,4 +927,70 @@ class DataService { // If all parsing attempts fail, throw error throw FormatException('Unable to parse RSS date: $dateText'); } + + // Get all fixtures that have videos, sorted by most recent first (past matches only) + static Future> getFixturesWithVideos() async { + try { + debugPrint( + '🎥 [Videos] 🔍 Searching for fixtures with videos across all divisions'); + + // Get all events to search through their divisions + final events = await getEvents(); + final allFixturesWithVideos = []; + + for (final event in events) { + try { + // Load seasons for this event if not already loaded + final eventWithSeasons = await loadEventSeasons(event); + + for (final season in eventWithSeasons.seasons) { + try { + // Get divisions for this event/season + final divisions = await getDivisions(event.id, season.slug); + + for (final division in divisions) { + try { + // Get fixtures for this division + final fixtures = await getFixtures(division.id, + eventId: event.id, season: season.slug); + + // Filter for completed fixtures with videos + final fixturesWithVideos = fixtures + .where((fixture) => + fixture.isCompleted && fixture.videos.isNotEmpty) + .toList(); + + allFixturesWithVideos.addAll(fixturesWithVideos); + + if (fixturesWithVideos.isNotEmpty) { + debugPrint( + '🎥 [Videos] ✅ Found ${fixturesWithVideos.length} fixtures with videos in ${division.name}'); + } + } catch (e) { + debugPrint( + '🎥 [Videos] ⚠️ Failed to load fixtures for division ${division.name}: $e'); + } + } + } catch (e) { + debugPrint( + '🎥 [Videos] ⚠️ Failed to load divisions for ${event.name}/${season.title}: $e'); + } + } + } catch (e) { + debugPrint( + '🎥 [Videos] ⚠️ Failed to load seasons for ${event.name}: $e'); + } + } + + // Sort by most recent first + allFixturesWithVideos.sort((a, b) => b.dateTime.compareTo(a.dateTime)); + + debugPrint( + '🎥 [Videos] ✅ Found ${allFixturesWithVideos.length} total fixtures with videos'); + return allFixturesWithVideos; + } catch (e) { + debugPrint('🎥 [Videos] ❌ Failed to load fixtures with videos: $e'); + rethrow; + } + } } diff --git a/lib/views/main_navigation_view.dart b/lib/views/main_navigation_view.dart index f0a4324..cbc3d85 100644 --- a/lib/views/main_navigation_view.dart +++ b/lib/views/main_navigation_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'home_view.dart'; import 'competitions_view.dart'; +import 'videos_view.dart'; import 'my_touch_view.dart'; class MainNavigationView extends StatefulWidget { @@ -24,11 +25,13 @@ class _MainNavigationViewState extends State { _navigatorKeys = [ GlobalKey(), // News navigator GlobalKey(), // Competitions navigator + GlobalKey(), // Videos navigator GlobalKey(), // My Touch navigator ]; _pages = [ _buildNewsNavigator(), _buildCompetitionsNavigator(), + _buildVideosNavigator(), _buildMyTouchNavigator(), ]; } @@ -57,9 +60,21 @@ class _MainNavigationViewState extends State { ); } - Widget _buildMyTouchNavigator() { + Widget _buildVideosNavigator() { return Navigator( key: _navigatorKeys[2], + onGenerateRoute: (settings) { + return MaterialPageRoute( + builder: (context) => const VideosView(), + settings: settings, + ); + }, + ); + } + + Widget _buildMyTouchNavigator() { + return Navigator( + key: _navigatorKeys[3], onGenerateRoute: (settings) { return MaterialPageRoute( builder: (context) => const MyTouchView(), @@ -77,6 +92,7 @@ class _MainNavigationViewState extends State { children: _pages, ), bottomNavigationBar: BottomNavigationBar( + type: BottomNavigationBarType.fixed, currentIndex: _selectedIndex, onTap: (index) { setState(() { @@ -92,6 +108,10 @@ class _MainNavigationViewState extends State { icon: Icon(Icons.sports), label: 'Events', ), + BottomNavigationBarItem( + icon: Icon(Icons.videocam), + label: 'Videos', + ), BottomNavigationBarItem( icon: Icon(Icons.star), label: 'My Touch', diff --git a/lib/views/videos_view.dart b/lib/views/videos_view.dart new file mode 100644 index 0000000..54ff0da --- /dev/null +++ b/lib/views/videos_view.dart @@ -0,0 +1,365 @@ +import 'package:flutter/material.dart'; +import '../models/fixture.dart'; +import '../services/data_service.dart'; +import '../theme/fit_colors.dart'; +import '../widgets/video_player_dialog.dart'; + +class VideosView extends StatefulWidget { + const VideosView({super.key}); + + @override + State createState() => _VideosViewState(); +} + +class _VideosViewState extends State { + late Future> _videoFixturesFuture; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + _loadVideoFixtures(); + } + + void _loadVideoFixtures() { + setState(() { + _isLoading = true; + }); + + _videoFixturesFuture = DataService.getFixturesWithVideos().then((fixtures) { + setState(() { + _isLoading = false; + }); + return fixtures; + }).catchError((error) { + setState(() { + _isLoading = false; + }); + throw error; + }); + } + + Future _onRefresh() async { + _loadVideoFixtures(); + } + + Future _playVideo( + String videoUrl, String matchTitle, Fixture fixture) async { + final divisionName = await _getDivisionName(fixture.divisionId); + + if (mounted) { + showDialog( + context: context, + builder: (context) => VideoPlayerDialog( + videoUrl: videoUrl, + homeTeamName: fixture.homeTeamName, + awayTeamName: fixture.awayTeamName, + divisionName: divisionName, + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text( + 'Videos', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + backgroundColor: FITColors.primaryBlue, + automaticallyImplyLeading: false, + elevation: 0, + ), + body: RefreshIndicator( + onRefresh: _onRefresh, + child: FutureBuilder>( + future: _videoFixturesFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting || + _isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (snapshot.hasError) { + return _buildErrorView( + 'Failed to load videos', + () => _loadVideoFixtures(), + ); + } + + final fixtures = snapshot.data ?? []; + if (fixtures.isEmpty) { + return _buildEmptyView(); + } + + return ListView.builder( + padding: const EdgeInsets.all(16.0), + itemCount: fixtures.length, + itemBuilder: (context, index) { + final fixture = fixtures[index]; + return _buildVideoCard(fixture); + }, + ); + }, + ), + ), + ); + } + + Widget _buildVideoCard(Fixture fixture) { + final hasMultipleVideos = fixture.videos.length > 1; + + return Card( + margin: const EdgeInsets.only(bottom: 16.0), + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header with match info + Container( + padding: const EdgeInsets.all(16), + decoration: const BoxDecoration( + color: FITColors.primaryBlue, + borderRadius: BorderRadius.vertical( + top: Radius.circular(12), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${fixture.homeTeamName} vs ${fixture.awayTeamName}', + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + _formatDateTime(fixture.dateTime), + style: const TextStyle( + color: Colors.white70, + fontSize: 14, + ), + ), + if (fixture.field.isNotEmpty) ...[ + const SizedBox(height: 2), + Text( + fixture.field, + style: const TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + ], + if (fixture.isCompleted && fixture.resultText.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + 'Final: ${fixture.resultText}', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ], + ), + ), + + // Video buttons + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (hasMultipleVideos) ...[ + const Text( + 'Available Videos:', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + const SizedBox(height: 12), + ], + ...fixture.videos.asMap().entries.map((entry) { + final index = entry.key; + final videoUrl = entry.value; + final videoTitle = hasMultipleVideos + ? 'Video ${index + 1}' + : 'Watch Match Video'; + + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () => _playVideo( + videoUrl, + '${fixture.homeTeamName} vs ${fixture.awayTeamName}', + fixture), + icon: const Icon(Icons.play_circle_fill), + label: Text(videoTitle), + style: ElevatedButton.styleFrom( + backgroundColor: FITColors.primaryBlue, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + ); + }), + ], + ), + ), + ], + ), + ); + } + + Widget _buildEmptyView() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.movie_outlined, + size: 64, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + Text( + 'No Videos Available', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 8), + Text( + 'Check back later for match videos', + style: TextStyle( + fontSize: 16, + color: Colors.grey[500], + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + Widget _buildErrorView(String message, VoidCallback onRetry) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 64, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + Text( + message, + style: TextStyle( + fontSize: 18, + color: Colors.grey[600], + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: onRetry, + style: ElevatedButton.styleFrom( + backgroundColor: FITColors.primaryBlue, + foregroundColor: Colors.white, + ), + child: const Text('Retry'), + ), + ], + ), + ); + } + + Future _getDivisionName(String divisionId) async { + try { + // Get all events and search for the division + final events = await DataService.getEvents(); + + for (final event in events) { + final eventWithSeasons = await DataService.loadEventSeasons(event); + + for (final season in eventWithSeasons.seasons) { + try { + final divisions = + await DataService.getDivisions(event.id, season.slug); + final division = + divisions.where((d) => d.id == divisionId).firstOrNull; + + if (division != null) { + return division.name; + } + } catch (e) { + // Continue searching in other seasons/events + } + } + } + + // Fallback: return formatted division ID + return divisionId + .split('-') + .map((word) => word[0].toUpperCase() + word.substring(1)) + .join(' '); + } catch (e) { + // Fallback: return formatted division ID + return divisionId + .split('-') + .map((word) => word[0].toUpperCase() + word.substring(1)) + .join(' '); + } + } + + String _formatDateTime(DateTime dateTime) { + final months = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec' + ]; + + final day = dateTime.day; + final month = months[dateTime.month - 1]; + final year = dateTime.year; + final hour = dateTime.hour.toString().padLeft(2, '0'); + final minute = dateTime.minute.toString().padLeft(2, '0'); + + return '$day $month $year, $hour:$minute'; + } +}