From 6a4a642611c1c721c5a8918f7ee98407ac4e3ab2 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 1 Apr 2026 20:19:40 +0200 Subject: [PATCH] feat: implement IPTV player screen with iframe-based stream support and recordings tab widget --- bin/database/database.dart | 13 ++- bin/services/recording_scheduler.dart | 104 ++++++++++++------ bin/services/simple_recorder.dart | 7 +- lib/core/config/optimization_config.dart | 3 +- lib/core/widgets/glass_container.dart | 10 +- lib/core/widgets/hero_carousel.dart | 6 +- .../iptv/screens/dashboard_screen.dart | 2 +- .../iptv/screens/epg_grid_screen.dart | 14 ++- lib/features/iptv/screens/player_screen.dart | 3 +- .../iptv/services/xtream_service.dart | 5 +- lib/features/iptv/widgets/recordings_tab.dart | 57 +++++++--- .../iptv/widgets/mobile_live_tv_tab.dart | 30 +++-- lib/mobile/widgets/mobile_scaffold.dart | 84 +++++++------- 13 files changed, 218 insertions(+), 120 deletions(-) diff --git a/bin/database/database.dart b/bin/database/database.dart index 54a0458..3366ba5 100644 --- a/bin/database/database.dart +++ b/bin/database/database.dart @@ -511,8 +511,12 @@ class AppDatabase { } /// Mettre à jour le statut et éventuellement le chemin d'un enregistrement - void updateRecordingStatus(String id, String status, - {String? filePath, String? errorReason}) { + void updateRecordingStatus( + String id, + String status, { + String? filePath, + String? errorReason, + }) { final now = DateTime.now().toIso8601String(); _db.execute( ''' @@ -551,14 +555,15 @@ class AppDatabase { 'channel_id': channelId, 'stream_url': streamUrl, 'enabled': 1, - 'created_at': now + 'created_at': now, }; } /// Lister tous les Season Passes List> getAllSeasonPasses() { final result = _db.select( - 'SELECT * FROM season_passes WHERE enabled = 1 ORDER BY created_at DESC'); + 'SELECT * FROM season_passes WHERE enabled = 1 ORDER BY created_at DESC', + ); return result.map((r) => Map.from(r)).toList(); } diff --git a/bin/services/recording_scheduler.dart b/bin/services/recording_scheduler.dart index 7de7282..cbc3150 100644 --- a/bin/services/recording_scheduler.dart +++ b/bin/services/recording_scheduler.dart @@ -26,10 +26,13 @@ class RecordingScheduler { void start() { print( - '[RecordingScheduler] Démarrage du planificateur d\'enregistrements TV'); + '[RecordingScheduler] Démarrage du planificateur d\'enregistrements TV', + ); // Vérifier toutes les 10 secondes (pour un démarrage quasi-immédiat) _timer = Timer.periodic( - const Duration(seconds: 10), (_) => _checkAndRunRecordings()); + const Duration(seconds: 10), + (_) => _checkAndRunRecordings(), + ); // Season Passes : vérifier toutes les 4 heures _seasonPassTimer = Timer.periodic(const Duration(hours: 4), (_) => _checkSeasonPasses()); @@ -56,13 +59,15 @@ class RecordingScheduler { final recordings = _db.getAllRecordings(); print( - '[RecordingScheduler] VÉRIFICATION: now=$now recordings=${recordings.length}'); + '[RecordingScheduler] VÉRIFICATION: now=$now recordings=${recordings.length}', + ); // Si un enregistrement est en cours, vérifier s'il doit s'arrêter if (_currentRecording != null) { if (now.isAfter(_currentRecording!.endTime.toUtc())) { print( - '[RecordingScheduler] Fin de l\'enregistrement : ${_currentRecording!.title}'); + '[RecordingScheduler] Fin de l\'enregistrement : ${_currentRecording!.title}', + ); await _stopCurrentRecording(); } } @@ -73,9 +78,13 @@ class RecordingScheduler { if (recording.status == 'recording' && _currentRecording?.id != recording.id) { print( - '[RecordingScheduler] Enregistrement orphelin détecté: ${recording.id}'); - _db.updateRecordingStatus(recording.id, 'failed', - errorReason: 'Interruption inattendue du serveur'); + '[RecordingScheduler] Enregistrement orphelin détecté: ${recording.id}', + ); + _db.updateRecordingStatus( + recording.id, + 'failed', + errorReason: 'Interruption inattendue du serveur', + ); continue; } @@ -83,28 +92,38 @@ class RecordingScheduler { final startUtc = recording.startTime.toUtc(); final endUtc = recording.endTime.toUtc(); print( - '[RecordingScheduler] "${recording.title}" start=$startUtc end=$endUtc now=$now isAfterStart=${now.isAfter(startUtc)} isBeforeEnd=${now.isBefore(endUtc)}'); + '[RecordingScheduler] "${recording.title}" start=$startUtc end=$endUtc now=$now isAfterStart=${now.isAfter(startUtc)} isBeforeEnd=${now.isBefore(endUtc)}', + ); // Si l'enregistrement est planifié, qu'il est temps de démarrer et qu'il n'est pas déjà fini if (now.isAfter(startUtc) && now.isBefore(endUtc)) { if (_currentRecording != null) { print( - '[RecordingScheduler] Conflit: "${recording.title}" ne peut pas démarrer, "${_currentRecording!.title}" est en cours.'); - _db.updateRecordingStatus(recording.id, 'failed', - errorReason: 'Un autre enregistrement était déjà en cours.'); + '[RecordingScheduler] Conflit: "${recording.title}" ne peut pas démarrer, "${_currentRecording!.title}" est en cours.', + ); + _db.updateRecordingStatus( + recording.id, + 'failed', + errorReason: 'Un autre enregistrement était déjà en cours.', + ); continue; } print( - '[RecordingScheduler] *** DÉMARRAGE DE L\'ENREGISTREMENT : ${recording.title} ***'); + '[RecordingScheduler] *** DÉMARRAGE DE L\'ENREGISTREMENT : ${recording.title} ***', + ); await _startRecording(recording); } // Si l'enregistrement est planifié mais que la date de fin est dépassée (loupé) if (now.isAfter(endUtc)) { print( - '[RecordingScheduler] Enregistrement "${recording.title}" manqué (fin dépassée).'); - _db.updateRecordingStatus(recording.id, 'failed', - errorReason: 'Heure de fin dépassée avant le démarrage'); + '[RecordingScheduler] Enregistrement "${recording.title}" manqué (fin dépassée).', + ); + _db.updateRecordingStatus( + recording.id, + 'failed', + errorReason: 'Heure de fin dépassée avant le démarrage', + ); } } } @@ -126,7 +145,8 @@ class RecordingScheduler { if (dns == null || username == null || password == null) { print( - '[SeasonPass] Config playlist non disponible, vérification annulée'); + '[SeasonPass] Config playlist non disponible, vérification annulée', + ); return; } @@ -193,7 +213,8 @@ class RecordingScheduler { endTime: endTime, ); print( - '[SeasonPass] ✓ Planifié automatiquement: "$title" le ${startTime.toLocal()}'); + '[SeasonPass] ✓ Planifié automatiquement: "$title" le ${startTime.toLocal()}', + ); } } catch (e) { print('[SeasonPass] Erreur pour le pass "${pass['show_title']}": $e'); @@ -257,10 +278,14 @@ class RecordingScheduler { } catch (logError) { // Si on ne peut pas créer le log, on continue quand même (l'enregistrement reste prioritaire) print( - '[RecordingScheduler] AVERTISSEMENT: Impossible de créer le fichier log ($logFilePath): $logError'); + '[RecordingScheduler] AVERTISSEMENT: Impossible de créer le fichier log ($logFilePath): $logError', + ); // On note la filePath dans la DB quand même pour pouvoir retourner le statut - _db.updateRecordingStatus(recording.id, 'recording', - filePath: filePath); + _db.updateRecordingStatus( + recording.id, + 'recording', + filePath: filePath, + ); } // Trouver ffmpeg @@ -285,18 +310,24 @@ class RecordingScheduler { _ffmpegProcess!.exitCode.then((exitCode) async { if (_currentRecording?.id == recording.id) { logSink?.writeln( - '\n[${DateTime.now()}] FFmpeg terminé avec le code $exitCode'); + '\n[${DateTime.now()}] FFmpeg terminé avec le code $exitCode', + ); await logSink?.close(); if (exitCode == 0 || exitCode == 255) { print( - '[RecordingScheduler] Enregistrement terminé: ${recording.title}'); + '[RecordingScheduler] Enregistrement terminé: ${recording.title}', + ); _db.updateRecordingStatus(recording.id, 'completed'); } else { print( - '[RecordingScheduler] Erreur FFmpeg (code: $exitCode) pour ${recording.title}'); - _db.updateRecordingStatus(recording.id, 'failed', - errorReason: 'Erreur FFmpeg code $exitCode. Voir logs.'); + '[RecordingScheduler] Erreur FFmpeg (code: $exitCode) pour ${recording.title}', + ); + _db.updateRecordingStatus( + recording.id, + 'failed', + errorReason: 'Erreur FFmpeg code $exitCode. Voir logs.', + ); } _currentRecording = null; _ffmpegProcess = null; @@ -307,8 +338,11 @@ class RecordingScheduler { } catch (e, st) { // Attraper TOUTES les exceptions pour éviter de crasher le serveur print('[RecordingScheduler] ERREUR dans _startRecording: $e\n$st'); - _db.updateRecordingStatus(recording.id, 'failed', - errorReason: 'Erreur au lancement: $e'); + _db.updateRecordingStatus( + recording.id, + 'failed', + errorReason: 'Erreur au lancement: $e', + ); _currentRecording = null; _ffmpegProcess = null; } @@ -318,7 +352,8 @@ class RecordingScheduler { Future stopRecording(String id) async { if (_ffmpegProcess != null && _currentRecording?.id == id) { print( - '[RecordingScheduler] Arrêt demandé pour: ${_currentRecording!.title}'); + '[RecordingScheduler] Arrêt demandé pour: ${_currentRecording!.title}', + ); _ffmpegProcess!.kill(ProcessSignal.sigterm); _db.updateRecordingStatus(id, 'completed'); _ffmpegProcess = null; @@ -348,11 +383,11 @@ class RecordingScheduler { if (files.length > maxFiles) { print( - '[RecordingScheduler] Rotation de l\'espace disque : suppression des anciens enregistrements'); - files.sort((a, b) => a - .statSync() - .modified - .compareTo(b.statSync().modified)); // Du plus vieux au plus récent + '[RecordingScheduler] Rotation de l\'espace disque : suppression des anciens enregistrements', + ); + files.sort( + (a, b) => a.statSync().modified.compareTo(b.statSync().modified), + ); // Du plus vieux au plus récent // Supprimer les plus anciens pour revenir sous la limite final filesToDelete = files.take(files.length - maxFiles); @@ -362,7 +397,8 @@ class RecordingScheduler { } } catch (e) { print( - '[RecordingScheduler] Erreur lors de la rotation de l\'espace disque : $e'); + '[RecordingScheduler] Erreur lors de la rotation de l\'espace disque : $e', + ); } } } diff --git a/bin/services/simple_recorder.dart b/bin/services/simple_recorder.dart index 58b4a29..852c73f 100644 --- a/bin/services/simple_recorder.dart +++ b/bin/services/simple_recorder.dart @@ -80,8 +80,11 @@ class SimpleRecorder { _active.remove(channelId); print('✅ Recording done: $title'); }).catchError((e) { - db.updateRecordingStatus(recordingId, 'failed', - errorReason: 'FFmpeg error'); + db.updateRecordingStatus( + recordingId, + 'failed', + errorReason: 'FFmpeg error', + ); _active.remove(channelId); }); diff --git a/lib/core/config/optimization_config.dart b/lib/core/config/optimization_config.dart index 4b49e12..f17829d 100644 --- a/lib/core/config/optimization_config.dart +++ b/lib/core/config/optimization_config.dart @@ -295,7 +295,8 @@ class RuntimeOptimizations { hasHighPerformanceDevice = totalMemoryMb > 4000 && storageFreeMb > 5000; print( - '[Device Calibration] Memory: ${freeMemoryMb}MB, Storage: ${storageFreeMb}MB'); + '[Device Calibration] Memory: ${freeMemoryMb}MB, Storage: ${storageFreeMb}MB', + ); } /// Get dynamic cache size based on available memory diff --git a/lib/core/widgets/glass_container.dart b/lib/core/widgets/glass_container.dart index 96af6fb..6572a3a 100644 --- a/lib/core/widgets/glass_container.dart +++ b/lib/core/widgets/glass_container.dart @@ -58,7 +58,9 @@ class GlassContainer extends StatelessWidget { // BackdropFilter is extremely expensive in Flutter Web on mobile Safari. // On mobile web (width < 600), skip the blur entirely. final screenWidth = MediaQuery.of(context).size.width; - final useBlur = blur > 0 && !(Theme.of(context).platform == TargetPlatform.iOS && screenWidth < 600); + final useBlur = blur > 0 && + !(Theme.of(context).platform == TargetPlatform.iOS && + screenWidth < 600); final inner = Container( padding: padding, @@ -157,10 +159,12 @@ class _GlassCardState extends State ); _scaleAnimation = Tween(begin: 1.0, end: 1.04).animate( - CurvedAnimation(parent: _controller, curve: AppTheme.curveDefault)); + CurvedAnimation(parent: _controller, curve: AppTheme.curveDefault), + ); _opacityAnimation = Tween(begin: 1.0, end: 0.8).animate( - CurvedAnimation(parent: _controller, curve: AppTheme.curveDefault)); + CurvedAnimation(parent: _controller, curve: AppTheme.curveDefault), + ); } @override diff --git a/lib/core/widgets/hero_carousel.dart b/lib/core/widgets/hero_carousel.dart index ca9b536..775d938 100644 --- a/lib/core/widgets/hero_carousel.dart +++ b/lib/core/widgets/hero_carousel.dart @@ -285,8 +285,10 @@ class _HeroCarouselState extends State child: Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.play_arrow, - color: AppColors.textPrimary), + const Icon( + Icons.play_arrow, + color: AppColors.textPrimary, + ), const SizedBox(width: 8), Text( 'Play', diff --git a/lib/features/iptv/screens/dashboard_screen.dart b/lib/features/iptv/screens/dashboard_screen.dart index 8850381..3188acc 100644 --- a/lib/features/iptv/screens/dashboard_screen.dart +++ b/lib/features/iptv/screens/dashboard_screen.dart @@ -29,7 +29,7 @@ class _DashboardScreenState extends ConsumerState { 'Movies', 'Series', 'Recordings', - 'Settings' + 'Settings', ]; final List _icons = [ Icons.live_tv_rounded, diff --git a/lib/features/iptv/screens/epg_grid_screen.dart b/lib/features/iptv/screens/epg_grid_screen.dart index ba73b4d..bf733ba 100644 --- a/lib/features/iptv/screens/epg_grid_screen.dart +++ b/lib/features/iptv/screens/epg_grid_screen.dart @@ -196,9 +196,13 @@ class _EpgGridScreenState extends ConsumerState mainAxisAlignment: MainAxisAlignment.center, children: [ Text(day, style: const TextStyle(fontSize: 11)), - Text(dateNum, - style: const TextStyle( - fontSize: 13, fontWeight: FontWeight.bold)), + Text( + dateNum, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + ), + ), ], ), ); @@ -257,7 +261,9 @@ class _EpgGridScreenState extends ConsumerState child: Text( '$hour:00', style: const TextStyle( - fontSize: 11, fontWeight: FontWeight.bold), + fontSize: 11, + fontWeight: FontWeight.bold, + ), ), ), ); diff --git a/lib/features/iptv/screens/player_screen.dart b/lib/features/iptv/screens/player_screen.dart index 5bd5c9f..5f12ff8 100644 --- a/lib/features/iptv/screens/player_screen.dart +++ b/lib/features/iptv/screens/player_screen.dart @@ -506,7 +506,8 @@ class _PlayerScreenState extends ConsumerState { _buildInlineEpg(), const SizedBox(height: 12), const Divider( - color: Colors.white10), + color: Colors.white10, + ), const SizedBox(height: 8), // Control Buttons Row( diff --git a/lib/features/iptv/services/xtream_service.dart b/lib/features/iptv/services/xtream_service.dart index f584859..dc23dd4 100644 --- a/lib/features/iptv/services/xtream_service.dart +++ b/lib/features/iptv/services/xtream_service.dart @@ -183,8 +183,9 @@ class XtreamService { /// Get all live TV channels grouped by category /// /// Returns a Map where key is category name and value is list of channels - Future>> getLiveChannels( - {bool refresh = false}) async { + Future>> getLiveChannels({ + bool refresh = false, + }) async { if (_currentPlaylist == null) throw Exception('No playlist configured'); final options = refresh diff --git a/lib/features/iptv/widgets/recordings_tab.dart b/lib/features/iptv/widgets/recordings_tab.dart index 978338d..0d540d6 100644 --- a/lib/features/iptv/widgets/recordings_tab.dart +++ b/lib/features/iptv/widgets/recordings_tab.dart @@ -7,9 +7,10 @@ import '../../../core/models/iptv_models.dart'; import '../../../core/models/playlist_config.dart'; import '../providers/xtream_provider.dart'; import '../providers/settings_provider.dart'; +import '../screens/player_screen.dart'; // ═══════════════════════════════════════════════════════════════════════════ -// ENTRÉE — Onglet "TV & Enregistrements" avec 3 sous-onglets +// ENTRÉE — Onglet "Enregistrements" // ═══════════════════════════════════════════════════════════════════════════ class RecordingsTab extends StatefulWidget { @@ -27,7 +28,7 @@ class _RecordingsTabState extends State @override void initState() { super.initState(); - _tabController = TabController(length: 3, vsync: this); + _tabController = TabController(length: 1, vsync: this); } @override @@ -73,15 +74,10 @@ class _RecordingsTabState extends State ), unselectedLabelStyle: GoogleFonts.outfit(fontSize: 14), tabs: const [ - Tab(icon: Icon(Icons.grid_view, size: 18), text: 'Guide TV'), Tab( icon: Icon(Icons.fiber_manual_record, size: 18), text: 'Enregistrements', ), - Tab( - icon: Icon(Icons.repeat, size: 18), - text: 'Season Passes', - ), ], ), ], @@ -89,11 +85,8 @@ class _RecordingsTabState extends State ), Expanded( child: TabBarView( - controller: _tabController, children: [ - _EpgGuideView(playlist: widget.playlist), - const _RecordingsListView(), - const _SeasonPassesView(), + _RecordingsListView(playlist: widget.playlist), ], ), ), @@ -298,7 +291,9 @@ class _EpgGuideViewState extends ConsumerState<_EpgGuideView> decoration: InputDecoration( hintText: 'Rechercher...', hintStyle: const TextStyle( - color: Colors.white38, fontSize: 13), + color: Colors.white38, + fontSize: 13, + ), prefixIcon: const Icon( Icons.search, color: Colors.white38, @@ -765,7 +760,8 @@ class _ProgrammeCard extends StatelessWidget { // ═══════════════════════════════════════════════════════════════════════════ class _RecordingsListView extends StatefulWidget { - const _RecordingsListView(); + final PlaylistConfig playlist; + const _RecordingsListView({required this.playlist}); @override State<_RecordingsListView> createState() => _RecordingsListViewState(); } @@ -821,6 +817,31 @@ class _RecordingsListViewState extends State<_RecordingsListView> { _fetchRecordings(); } + Future _playRecording(BuildContext context, Map rec) async { + final streamUrl = rec['stream_url'] as String? ?? ''; + final title = rec['title'] as String? ?? 'Enregistrement'; + + if (streamUrl.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('URL du flux indisponible')), + ); + return; + } + + if (!mounted) return; + Navigator.of(context).push( + MaterialPageRoute( + builder: (ctx) => PlayerScreen( + streamId: streamUrl, + title: title, + playlist: widget.playlist, + streamType: StreamType.vod, + containerExtension: 'ts', + ), + ), + ); + } + Future _showLogs(String id, String title) async { try { final response = await http.get(Uri.parse('/api/recordings/logs/$id')); @@ -1033,6 +1054,16 @@ class _RecordingsListViewState extends State<_RecordingsListView> { rec['title'] ?? '', ), ), + if (status == 'completed') + IconButton( + icon: const Icon( + Icons.play_circle_outline, + color: Colors.greenAccent, + size: 20, + ), + tooltip: 'Lecture', + onPressed: () => _playRecording(context, rec), + ), IconButton( icon: const Icon( Icons.description_outlined, diff --git a/lib/mobile/features/iptv/widgets/mobile_live_tv_tab.dart b/lib/mobile/features/iptv/widgets/mobile_live_tv_tab.dart index 6dad5fd..7040f25 100644 --- a/lib/mobile/features/iptv/widgets/mobile_live_tv_tab.dart +++ b/lib/mobile/features/iptv/widgets/mobile_live_tv_tab.dart @@ -48,8 +48,10 @@ class _MobileLiveTVTabState extends ConsumerState { children: [ const CircularProgressIndicator(), const SizedBox(height: 16), - const Text('Loading channels...', - style: TextStyle(color: Colors.white70)), + const Text( + 'Loading channels...', + style: TextStyle(color: Colors.white70), + ), const SizedBox(height: 24), FutureBuilder( future: Future.delayed(const Duration(seconds: 8)), @@ -57,7 +59,8 @@ class _MobileLiveTVTabState extends ConsumerState { if (snapshot.connectionState == ConnectionState.done) { return TextButton( onPressed: () => ref.refresh( - liveChannelsByPlaylistProvider(widget.playlist)), + liveChannelsByPlaylistProvider(widget.playlist), + ), child: const Text('Taking too long? Tap to retry'), ); } @@ -75,15 +78,20 @@ class _MobileLiveTVTabState extends ConsumerState { children: [ const Icon(Icons.error_outline, color: Colors.red, size: 48), const SizedBox(height: 16), - Text('Failed to load channels', - style: GoogleFonts.inter( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold)), + Text( + 'Failed to load channels', + style: GoogleFonts.inter( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), const SizedBox(height: 8), - Text('$e', - style: const TextStyle(color: Colors.white54), - textAlign: TextAlign.center), + Text( + '$e', + style: const TextStyle(color: Colors.white54), + textAlign: TextAlign.center, + ), const SizedBox(height: 24), ElevatedButton( onPressed: () => ref diff --git a/lib/mobile/widgets/mobile_scaffold.dart b/lib/mobile/widgets/mobile_scaffold.dart index c6e38f5..97d34b8 100644 --- a/lib/mobile/widgets/mobile_scaffold.dart +++ b/lib/mobile/widgets/mobile_scaffold.dart @@ -42,50 +42,50 @@ class MobileScaffold extends ConsumerWidget { ], ), bottomNavigationBar: Container( - decoration: BoxDecoration( - color: AppColors.surface, - border: const Border( - top: BorderSide( - color: AppColors.border, - width: 0.5, - ), - ), - ), - child: BottomNavigationBar( - currentIndex: currentIndex, - onTap: onIndexChanged, - backgroundColor: Colors.transparent, - type: BottomNavigationBarType.fixed, - elevation: 0, - selectedItemColor: AppColors.primary, - unselectedItemColor: AppColors.textSecondary, - selectedLabelStyle: - GoogleFonts.inter(fontWeight: FontWeight.w600, fontSize: 11), - unselectedLabelStyle: - GoogleFonts.inter(fontWeight: FontWeight.w500, fontSize: 11), - items: const [ - BottomNavigationBarItem( - icon: Icon(Icons.live_tv_rounded), - label: 'Live', - ), - BottomNavigationBarItem( - icon: Icon(Icons.movie_outlined), - activeIcon: Icon(Icons.movie_rounded), - label: 'Movies', - ), - BottomNavigationBarItem( - icon: Icon(Icons.video_library_outlined), - activeIcon: Icon(Icons.video_library_rounded), - label: 'Series', - ), - BottomNavigationBarItem( - icon: Icon(Icons.settings_outlined), - activeIcon: Icon(Icons.settings_rounded), - label: 'Settings', - ), - ], + decoration: const BoxDecoration( + color: AppColors.surface, + border: Border( + top: BorderSide( + color: AppColors.border, + width: 0.5, ), ), + ), + child: BottomNavigationBar( + currentIndex: currentIndex, + onTap: onIndexChanged, + backgroundColor: Colors.transparent, + type: BottomNavigationBarType.fixed, + elevation: 0, + selectedItemColor: AppColors.primary, + unselectedItemColor: AppColors.textSecondary, + selectedLabelStyle: + GoogleFonts.inter(fontWeight: FontWeight.w600, fontSize: 11), + unselectedLabelStyle: + GoogleFonts.inter(fontWeight: FontWeight.w500, fontSize: 11), + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.live_tv_rounded), + label: 'Live', + ), + BottomNavigationBarItem( + icon: Icon(Icons.movie_outlined), + activeIcon: Icon(Icons.movie_rounded), + label: 'Movies', + ), + BottomNavigationBarItem( + icon: Icon(Icons.video_library_outlined), + activeIcon: Icon(Icons.video_library_rounded), + label: 'Series', + ), + BottomNavigationBarItem( + icon: Icon(Icons.settings_outlined), + activeIcon: Icon(Icons.settings_rounded), + label: 'Settings', + ), + ], + ), + ), ); } }