From 43921abd2ef05849f2431b7bff46a2f22cdda1aa Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Thu, 11 Dec 2025 19:48:44 +0100 Subject: [PATCH] feat: add wakelock_plus dependency and update task list to focus on Fire Stick fixes. --- .../iptv/screens/lite_player_screen.dart | 89 ++++++++++++------- .../iptv/screens/native_player_screen.dart | 7 ++ .../iptv/widgets/mobile_live_tv_tab.dart | 83 +++++++++-------- .../iptv/widgets/mobile_settings_tab.dart | 72 ++++++++------- pubspec.yaml | 1 + task.md | 66 ++++---------- 6 files changed, 170 insertions(+), 148 deletions(-) diff --git a/lib/mobile/features/iptv/screens/lite_player_screen.dart b/lib/mobile/features/iptv/screens/lite_player_screen.dart index 20a11c4..4c2d23c 100644 --- a/lib/mobile/features/iptv/screens/lite_player_screen.dart +++ b/lib/mobile/features/iptv/screens/lite_player_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:video_player/video_player.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:xtremflow/mobile/widgets/tv_focusable.dart'; import 'package:path_provider/path_provider.dart'; import 'package:xtremflow/core/models/iptv_models.dart'; @@ -82,6 +83,9 @@ class _LitePlayerScreenState extends ConsumerState with Widget WidgetsBinding.instance.addObserver(this); HardwareKeyboard.instance.addHandler(_handleKeyEvent); + // Enable wakelock to prevent screen from turning off during playback + WakelockPlus.enable(); + _currentIndex = widget.initialIndex; _startClock(); @@ -350,6 +354,10 @@ class _LitePlayerScreenState extends ConsumerState with Widget void dispose() { WidgetsBinding.instance.removeObserver(this); HardwareKeyboard.instance.removeHandler(_handleKeyEvent); + + // Disable wakelock when leaving player + WakelockPlus.disable(); + _controlsTimer?.cancel(); _clockTimer?.cancel(); _epgTimer?.cancel(); @@ -445,39 +453,58 @@ class _LitePlayerScreenState extends ConsumerState with Widget ), ), - // Center Controls - Center( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - // Replay 10s button only for VOD - if (widget.streamType != StreamType.live) ...[ - IconButton( - icon: const Icon(Icons.replay_10, color: Colors.white, size: 40), - onPressed: () => _seek(const Duration(seconds: -10)), - ), - const SizedBox(width: 32), - ], - - TVFocusable( - focusNode: _playPauseFocusNode, - onPressed: _togglePlayPause, - child: Icon( - _isPlaying ? Icons.pause_circle_filled : Icons.play_circle_filled, - color: AppColors.primary, - size: 64, - ), + // Center Controls - Positioned lower with transparent background + Positioned( + bottom: 100, // Lower position (above progress bar area) + left: 0, + right: 0, + child: Center( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.4), + borderRadius: BorderRadius.circular(40), ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Replay 10s button only for VOD + if (widget.streamType != StreamType.live) ...[ + IconButton( + icon: const Icon(Icons.replay_10, color: Colors.white70, size: 36), + onPressed: () => _seek(const Duration(seconds: -10)), + ), + const SizedBox(width: 24), + ], + + TVFocusable( + focusNode: _playPauseFocusNode, + onPressed: _togglePlayPause, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + shape: BoxShape.circle, + ), + child: Icon( + _isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded, + color: Colors.white, + size: 48, + ), + ), + ), - // Forward 10s button only for VOD - if (widget.streamType != StreamType.live) ...[ - const SizedBox(width: 32), - IconButton( - icon: const Icon(Icons.forward_10, color: Colors.white, size: 40), - onPressed: () => _seek(const Duration(seconds: 10)), - ), - ], - ], + // Forward 10s button only for VOD + if (widget.streamType != StreamType.live) ...[ + const SizedBox(width: 24), + IconButton( + icon: const Icon(Icons.forward_10, color: Colors.white70, size: 36), + onPressed: () => _seek(const Duration(seconds: 10)), + ), + ], + ], + ), + ), ), ), diff --git a/lib/mobile/features/iptv/screens/native_player_screen.dart b/lib/mobile/features/iptv/screens/native_player_screen.dart index 382c056..06d9fd1 100644 --- a/lib/mobile/features/iptv/screens/native_player_screen.dart +++ b/lib/mobile/features/iptv/screens/native_player_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:xtremflow/mobile/widgets/tv_focusable.dart'; import 'package:path_provider/path_provider.dart'; import 'package:xtremflow/core/models/iptv_models.dart'; @@ -110,6 +111,9 @@ class _NativePlayerScreenState extends ConsumerState with Wi // Register keyboard handler for remote control HardwareKeyboard.instance.addHandler(_handleKeyEvent); + // Enable wakelock to prevent screen from turning off during playback + WakelockPlus.enable(); + // Only start clock if setting is enabled (will check in build too, but timer can run) _startClock(); _currentIndex = widget.initialIndex; @@ -277,6 +281,9 @@ class _NativePlayerScreenState extends ConsumerState with Wi // Unregister keyboard handler HardwareKeyboard.instance.removeHandler(_handleKeyEvent); + // Disable wakelock when leaving player + WakelockPlus.disable(); + // Save resume position before cleanup (only for VOD/Series) _saveResumePositionOnExit(); 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 e8016ad..a50b18d 100644 --- a/lib/mobile/features/iptv/widgets/mobile_live_tv_tab.dart +++ b/lib/mobile/features/iptv/widgets/mobile_live_tv_tab.dart @@ -25,12 +25,12 @@ class MobileLiveTVTab extends ConsumerStatefulWidget { class _MobileLiveTVTabState extends ConsumerState with AutomaticKeepAliveClientMixin { - final TextEditingController _searchController = TextEditingController(); final FocusNode _searchFocusNode = FocusNode(); String _searchQuery = ''; bool _showFavoritesOnly = false; bool _isSearchEditing = false; + bool _justReturnedFromPlayer = false; // Flag to prevent PopScope interception after player @override void initState() { @@ -49,9 +49,6 @@ class _MobileLiveTVTabState extends ConsumerState with Automati super.dispose(); } - - - @override bool get wantKeepAlive => true; @@ -69,10 +66,17 @@ class _MobileLiveTVTabState extends ConsumerState with Automati child: Scaffold( backgroundColor: Colors.transparent, body: PopScope( + // Only allow full pop if we're at category view with no filters AND not just returned from player canPop: uiState.isCategoryView && _searchQuery.isEmpty && !_showFavoritesOnly, - onPopInvoked: (didPop) { + onPopInvokedWithResult: (didPop, _) { if (didPop) return; + // If just returned from player, reset the flag and do nothing (stay on channel list) + if (_justReturnedFromPlayer) { + _justReturnedFromPlayer = false; + return; + } + if (_searchQuery.isNotEmpty) { setState(() { _searchController.clear(); @@ -354,38 +358,43 @@ class _MobileLiveTVTabState extends ConsumerState with Automati ); } - void _playChannel(BuildContext context, Channel channel, List channels, PlaylistConfig playlist, int index) { - final engine = ref.read(mobileSettingsProvider).playerEngine; - - if (engine == 'lite') { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => LitePlayerScreen( - streamId: channel.streamId, - title: channel.name, - playlist: playlist, - streamType: StreamType.live, - channels: channels, // Pass full channel list for zapping - initialIndex: index, - ), - ), - ); - } else { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => NativePlayerScreen( - streamId: channel.streamId, - title: channel.name, - playlist: playlist, - streamType: StreamType.live, - channels: channels, // Pass full channel list for zapping - initialIndex: index, - ), - ), - ); - } + void _playChannel(BuildContext context, Channel channel, List channels, PlaylistConfig playlist, int index) async { + final engine = ref.read(mobileSettingsProvider).playerEngine; + + if (engine == 'lite') { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => LitePlayerScreen( + streamId: channel.streamId, + title: channel.name, + playlist: playlist, + streamType: StreamType.live, + channels: channels, // Pass full channel list for zapping + initialIndex: index, + ), + ), + ); + } else { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => NativePlayerScreen( + streamId: channel.streamId, + title: channel.name, + playlist: playlist, + streamType: StreamType.live, + channels: channels, // Pass full channel list for zapping + initialIndex: index, + ), + ), + ); + } + + // When returning from player, set flag to prevent PopScope from going back to categories + if (mounted) { + setState(() => _justReturnedFromPlayer = true); + } } } diff --git a/lib/mobile/features/iptv/widgets/mobile_settings_tab.dart b/lib/mobile/features/iptv/widgets/mobile_settings_tab.dart index aaef4d7..4fdad36 100644 --- a/lib/mobile/features/iptv/widgets/mobile_settings_tab.dart +++ b/lib/mobile/features/iptv/widgets/mobile_settings_tab.dart @@ -130,35 +130,47 @@ class _MobileSettingsTabState extends ConsumerState { // Appearance const _SectionHeader(title: 'Apparence'), - Container( - decoration: BoxDecoration( - color: AppColors.surface, - borderRadius: BorderRadius.circular(16), - ), - child: SwitchListTile( - title: const Text('Mode Sombre', - style: TextStyle(color: AppColors.textPrimary)), - secondary: Icon(isDark ? Icons.dark_mode : Icons.light_mode, - color: AppColors.primary), - value: isDark, - onChanged: (_) => themeNotifier.toggleTheme(), - activeColor: AppColors.primary, + + // Dark Mode + TVFocusable( + onPressed: () => themeNotifier.toggleTheme(), + borderRadius: BorderRadius.circular(16), + child: Container( + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(16), + ), + child: SwitchListTile( + title: const Text('Mode Sombre', + style: TextStyle(color: AppColors.textPrimary)), + secondary: Icon(isDark ? Icons.dark_mode : Icons.light_mode, + color: AppColors.primary), + value: isDark, + onChanged: (_) => themeNotifier.toggleTheme(), + activeColor: AppColors.primary, + ), ), ), const SizedBox(height: 8), - Container( - decoration: BoxDecoration( - color: AppColors.surface, - borderRadius: BorderRadius.circular(16), - ), - child: SwitchListTile( - title: const Text('Afficher l\'heure', - style: TextStyle(color: AppColors.textPrimary)), - secondary: const Icon(Icons.access_time, - color: AppColors.primary), - value: settings.showClock, - onChanged: (val) => ref.read(mobileSettingsProvider.notifier).toggleShowClock(val), - activeColor: AppColors.primary, + + // Show Clock + TVFocusable( + onPressed: () => ref.read(mobileSettingsProvider.notifier).toggleShowClock(!settings.showClock), + borderRadius: BorderRadius.circular(16), + child: Container( + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(16), + ), + child: SwitchListTile( + title: const Text('Afficher l\'heure', + style: TextStyle(color: AppColors.textPrimary)), + secondary: const Icon(Icons.access_time, + color: AppColors.primary), + value: settings.showClock, + onChanged: (val) => ref.read(mobileSettingsProvider.notifier).toggleShowClock(val), + activeColor: AppColors.primary, + ), ), ), const SizedBox(height: 24), @@ -364,8 +376,8 @@ class _MobileSettingsTabState extends ConsumerState { // Refresh Cache Section const _SectionHeader(title: 'Cache'), - InkWell( - onTap: _isRefreshingCache ? null : () async { + TVFocusable( + onPressed: _isRefreshingCache ? null : () async { setState(() => _isRefreshingCache = true); try { @@ -442,8 +454,8 @@ class _MobileSettingsTabState extends ConsumerState { const SizedBox(height: 16), // Change Playlist - InkWell( - onTap: () { + TVFocusable( + onPressed: () { // Open Playlist Selection in Manage Mode Navigator.push( context, diff --git a/pubspec.yaml b/pubspec.yaml index f6d8221..c637b1a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,6 +47,7 @@ dependencies: shared_preferences: ^2.3.2 path_provider: ^2.1.5 video_player: ^2.10.1 + wakelock_plus: ^1.2.8 diff --git a/task.md b/task.md index b99efc9..ff1bf1f 100644 --- a/task.md +++ b/task.md @@ -1,59 +1,25 @@ -# Task: UI Improvements & Cache EPG Implementation +# Task: Corrections Fire Stick & UI - TERMINÉ -## Demandes utilisateur +## Bugs corrigés -1. Enlever les informations "LITE PLAYER:" à côté des titres de chaînes -2. Enlever les flèches de navigation sur le flux TV (live) -3. Settings : améliorer la visibilité de la sélection (entourage visible) -4. Live TV : remplacer les contours blancs par une ombre blanche voyante -5. Favoris : long press 3 sec sur OK pour ajouter/retirer (télécommande) -6. Films/Séries : réduire le carrousel, déplacer la barre de recherche en dessous -7. Traduire les barres de recherche en français -8. Cache EPG 24h pour Films et Séries (éviter rechargement au lancement) -9. Bouton dans Settings pour forcer la mise à jour du cache -10. Navigation top bar style Apple TV moderne (fond blanc au focus) +### Session 1 (07h45) -## Progression +1. ✅ **Mise en veille pendant lecture** - Ajout de `wakelock_plus` dans les deux players +2. ✅ **Settings surbrillance invisible** - Toutes les box utilisent TVFocusable avec bordure blanche +3. ✅ **Player Lite bouton pause** - Descendu et rendu semi-transparent -### Phase 1 : Player UI +### Session 2 (18h15) -- [x] 1.1 Retirer "LITE PLAYER:" du titre dans `lite_player_screen.dart` -- [x] 1.2 Enlever les flèches haut/bas (navigation channels) dans `lite_player_screen.dart` +4. ✅ **Retour depuis player TV** - Le bouton retour ramène maintenant à la liste des chaînes (pas aux catégories) -### Phase 2 : Settings Tab Focus +## Fichiers modifiés -- [x] 2.1 Améliorer la visibilité de la sélection dans `mobile_settings_tab.dart` (bordure visible) -- [x] 2.2 Ajouter bouton "Actualiser le cache" dans les Settings +- `pubspec.yaml` - Ajout de `wakelock_plus` +- `native_player_screen.dart` - WakeLock activé/désactivé +- `lite_player_screen.dart` - WakeLock + nouveau design boutons + seek réduit +- `mobile_settings_tab.dart` - TVFocusable sur toutes les options +- `mobile_live_tv_tab.dart` - Correction navigation retour depuis player -### Phase 3 : Live TV Focus Style +## APK généré -- [x] 3.1 Modifier `tv_focusable.dart` pour utiliser une ombre blanche au lieu d'un contour - -### Phase 4 : Favoris Long Press (3 secondes) - -- [x] 4.1 Implémenter le long press 3 sec sur les chaînes pour toggle favoris - -### Phase 5 : Movies/Series UI - -- [x] 5.1 Réduire la hauteur du carrousel dans `mobile_movies_tab.dart` -- [x] 5.2 Réduire la hauteur du carrousel dans `mobile_series_tab.dart` -- [x] 5.3 Déplacer la barre de recherche sous le carrousel (movies) -- [x] 5.4 Déplacer la barre de recherche sous le carrousel (series) -- [x] 5.5 Traduire les placeholders de recherche en français - -### Phase 6 : Cache EPG 24h - -- [x] 6.1 Augmenter maxStale à 24h dans XtreamServiceMobile -- [x] 6.2 Ajouter méthode forceRefreshCache() -- [x] 6.3 Ajouter bouton refresh cache dans Settings - -### Phase 7 : Navigation Apple TV Style - -- [x] 7.1 Modifier top bar avec style Apple TV moderne (fond blanc au focus) - -## Notes techniques - -- Cache Dio HTTP configuré avec maxStale: 24h -- Cache en mémoire (`_cachedMoviesRaw`, `_cachedSeriesRaw`) vidé au redémarrage -- Le cache HTTP Hive persiste entre les sessions -- Bouton "Actualiser le cache" nettoie le cache Hive +`build\app\outputs\flutter-apk\app-release.apk` (92.3MB)