From a2b8d5fb0946603ed9c2812942b691a8cee3aaf0 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Tue, 16 Dec 2025 22:37:47 +0100 Subject: [PATCH] feat: enforce player strategy (vod=ultra, live=lite), clean UI and settings --- .../screens/mobile_series_detail_screen.dart | 133 ++--- .../iptv/screens/native_player_screen.dart | 14 - .../iptv/widgets/mobile_live_tv_tab.dart | 487 ++++++++++-------- .../iptv/widgets/mobile_movies_tab.dart | 4 +- .../iptv/widgets/mobile_settings_tab.dart | 143 ++--- 5 files changed, 417 insertions(+), 364 deletions(-) diff --git a/lib/mobile/features/iptv/screens/mobile_series_detail_screen.dart b/lib/mobile/features/iptv/screens/mobile_series_detail_screen.dart index 71d8158..1fb9f9f 100644 --- a/lib/mobile/features/iptv/screens/mobile_series_detail_screen.dart +++ b/lib/mobile/features/iptv/screens/mobile_series_detail_screen.dart @@ -21,10 +21,12 @@ class MobileSeriesDetailScreen extends ConsumerStatefulWidget { }); @override - ConsumerState createState() => _MobileSeriesDetailScreenState(); + ConsumerState createState() => + _MobileSeriesDetailScreenState(); } -class _MobileSeriesDetailScreenState extends ConsumerState { +class _MobileSeriesDetailScreenState + extends ConsumerState { SeriesInfo? _seriesInfo; bool _isLoading = true; String? _error; @@ -52,8 +54,10 @@ class _MobileSeriesDetailScreenState extends ConsumerState Container(color: Colors.grey.shade900), + errorWidget: (_, __, ___) => + Container(color: Colors.grey.shade900), ), // Gradient Overlay Container( @@ -166,17 +176,20 @@ class _MobileSeriesDetailScreenState extends ConsumerState const SizedBox(width: 8), itemBuilder: (context, index) { - final seasonNum = _seriesInfo!.episodes.keys.elementAt(index); + final seasonNum = + _seriesInfo!.episodes.keys.elementAt(index); final isSelected = seasonNum == _selectedSeason; return ChoiceChip( label: Text('Season $seasonNum'), selected: isSelected, onSelected: (selected) { - if (selected) setState(() => _selectedSeason = seasonNum); + if (selected) + setState(() => _selectedSeason = seasonNum); }, - backgroundColor: isDark ? const Color(0xFF1C1C1E) : Colors.grey.shade300, + backgroundColor: isDark + ? const Color(0xFF1C1C1E) + : Colors.grey.shade300, selectedColor: isDark ? Colors.white : Colors.blue, labelStyle: TextStyle( - color: isSelected + color: isSelected ? (isDark ? Colors.black : Colors.white) - : (isDark ? Colors.grey.shade400 : Colors.grey.shade700), - fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, + : (isDark + ? Colors.grey.shade400 + : Colors.grey.shade700), + fontWeight: + isSelected ? FontWeight.w600 : FontWeight.w400, ), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20)), showCheckmark: false, side: BorderSide.none, ); @@ -269,58 +291,45 @@ class _MobileSeriesDetailScreenState extends ConsumerState LitePlayerScreen( - streamId: episode.streamId, - title: "${widget.series.name} S${episode.seasonNum}E${episode.episodeNum}", - playlist: widget.playlist, - streamType: StreamType.series, - containerExtension: episode.containerExtension ?? 'mp4', - seriesId: widget.series.seriesId, - season: episode.seasonNum, - episodeNum: episode.episodeNum, - ), - ), - ); - } else { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => NativePlayerScreen( - streamId: episode.streamId, - title: "${widget.series.name} S${episode.seasonNum}E${episode.episodeNum}", - playlist: widget.playlist, - streamType: StreamType.series, - containerExtension: episode.containerExtension ?? 'mp4', - seriesId: widget.series.seriesId, - season: episode.seasonNum, - episodeNum: episode.episodeNum, - ), - ), - ); - } + + // Enforce Native Player for Series (VOD) + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => NativePlayerScreen( + streamId: episode.streamId, + title: + "${widget.series.name} S${episode.seasonNum}E${episode.episodeNum}", + playlist: widget.playlist, + streamType: StreamType.series, + containerExtension: episode.containerExtension ?? 'mp4', + seriesId: widget.series.seriesId, + season: episode.seasonNum, + episodeNum: episode.episodeNum, + ), + ), + ); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( - border: Border(bottom: BorderSide(color: isDark ? Colors.white10 : Colors.black12)), + border: Border( + bottom: + BorderSide(color: isDark ? Colors.white10 : Colors.black12)), ), child: Row( children: [ @@ -331,11 +340,15 @@ class _MobileSeriesDetailScreenState extends ConsumerState child: Row( mainAxisSize: MainAxisSize.min, children: [ - // FORCE AUDIO BUTTON (First item) - TVFocusable( - focusNode: _audioFocusNode, - onPressed: _cycleAudioTrack, - onFocus: _resetControlsTimer, - borderRadius: BorderRadius.circular(50), - child: IconButton( - icon: const Icon(Icons.audiotrack, - color: Colors.greenAccent, size: 32), - onPressed: _cycleAudioTrack, - ), - ), - const SizedBox(width: 16), - // Prev Channel / Replay 10s if (widget.streamType == StreamType.live && widget.channels != null && 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 0ab6222..a584e11 100644 --- a/lib/mobile/features/iptv/widgets/mobile_live_tv_tab.dart +++ b/lib/mobile/features/iptv/widgets/mobile_live_tv_tab.dart @@ -23,14 +23,15 @@ class MobileLiveTVTab extends ConsumerStatefulWidget { ConsumerState createState() => _MobileLiveTVTabState(); } -class _MobileLiveTVTabState extends ConsumerState with AutomaticKeepAliveClientMixin { - +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 + bool _justReturnedFromPlayer = + false; // Flag to prevent PopScope interception after player @override void initState() { @@ -55,7 +56,8 @@ class _MobileLiveTVTabState extends ConsumerState with Automati @override Widget build(BuildContext context) { super.build(context); - final channelsAsync = ref.watch(mobileLiveChannelsProvider(widget.playlist)); + final channelsAsync = + ref.watch(mobileLiveChannelsProvider(widget.playlist)); final favorites = ref.watch(mobileFavoritesProvider); final settings = ref.watch(mobileSettingsProvider); final uiState = ref.watch(mobileLiveTvUiStateProvider); @@ -66,193 +68,228 @@ 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, - 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(); - _searchQuery = ''; - }); - } else if (_showFavoritesOnly) { - setState(() => _showFavoritesOnly = false); - } else if (!uiState.isCategoryView) { - uiNotifier.state = uiState.copyWith(isCategoryView: true); - } - }, - child: channelsAsync.when( - loading: () => const Center(child: CircularProgressIndicator()), - error: (e, s) => Center( - child: Padding( - padding: const EdgeInsets.all(16), - child: Text( - 'Erreur: $e', - style: const TextStyle(color: Colors.red), - textAlign: TextAlign.center, - ), - ), - ), - data: (groupedChannels) { - // Prepare categories - var categories = groupedChannels.keys.toList(); - if (settings.liveTvKeywords.isNotEmpty) { - categories = categories.where((cat) => settings.matchesLiveTvFilter(cat)).toList(); - } - categories.sort(); + // 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, + onPopInvokedWithResult: (didPop, _) { + if (didPop) return; - // Prepare channels - List displayedChannels = []; - - // Determine mode based on search/favorites/selection - bool showGrid = uiState.isCategoryView && _searchQuery.isEmpty && !_showFavoritesOnly; - - if (!showGrid) { - if (_searchQuery.isNotEmpty) { - displayedChannels = groupedChannels.values.expand((l) => l) - .where((c) => c.name.toLowerCase().contains(_searchQuery)) - .toList(); - } else if (_showFavoritesOnly) { - displayedChannels = groupedChannels.values.expand((l) => l) - .where((c) => favorites.contains(c.streamId)) - .toList(); - } else if (uiState.selectedCategory != null) { - displayedChannels = groupedChannels[uiState.selectedCategory] ?? []; + // If just returned from player, reset the flag and do nothing (stay on channel list) + if (_justReturnedFromPlayer) { + _justReturnedFromPlayer = false; + return; } - } - return SafeArea( - bottom: false, - child: Column( - children: [ - // Header (Search + Title/Back) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Column( - children: [ - // Search Bar - Wrapped in TVFocusable for remote access - TVFocusable( - onPressed: () { - setState(() => _isSearchEditing = true); - WidgetsBinding.instance.addPostFrameCallback((_) { - _searchFocusNode.requestFocus(); - SystemChannels.textInput.invokeMethod('TextInput.show'); - }); - }, - borderRadius: BorderRadius.circular(12), - child: Container( - height: 48, - decoration: BoxDecoration( - color: AppColors.surface, - borderRadius: BorderRadius.circular(12), - border: _isSearchEditing ? Border.all(color: AppColors.primary) : null, - ), - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Row( - children: [ - const Icon(Icons.search, color: AppColors.textSecondary), - const SizedBox(width: 8), - Expanded( - child: ExcludeFocus( - excluding: !_isSearchEditing, - child: TextField( - controller: _searchController, - focusNode: _searchFocusNode, - readOnly: !_isSearchEditing, - style: const TextStyle(color: AppColors.textPrimary), - decoration: const InputDecoration( - hintText: 'Rechercher une chaîne...', - border: InputBorder.none, - isDense: true, - ), - onSubmitted: (_) => setState(() => _isSearchEditing = false), - ), - ), - ), - if (_searchQuery.isNotEmpty) - GestureDetector( - onTap: () => _searchController.clear(), - child: const Icon(Icons.close, color: AppColors.textSecondary), - ), - ], - ), - ), - ), - - const SizedBox(height: 12), - - // Title / Navigation Row - Row( + if (_searchQuery.isNotEmpty) { + setState(() { + _searchController.clear(); + _searchQuery = ''; + }); + } else if (_showFavoritesOnly) { + setState(() => _showFavoritesOnly = false); + } else if (!uiState.isCategoryView) { + uiNotifier.state = uiState.copyWith(isCategoryView: true); + } + }, + child: channelsAsync.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (e, s) => Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + 'Erreur: $e', + style: const TextStyle(color: Colors.red), + textAlign: TextAlign.center, + ), + ), + ), + data: (groupedChannels) { + // Prepare categories + var categories = groupedChannels.keys.toList(); + if (settings.liveTvKeywords.isNotEmpty) { + categories = categories + .where((cat) => settings.matchesLiveTvFilter(cat)) + .toList(); + } + categories.sort(); + + // Prepare channels + List displayedChannels = []; + + // Determine mode based on search/favorites/selection + bool showGrid = uiState.isCategoryView && + _searchQuery.isEmpty && + !_showFavoritesOnly; + + if (!showGrid) { + if (_searchQuery.isNotEmpty) { + displayedChannels = groupedChannels.values + .expand((l) => l) + .where((c) => c.name.toLowerCase().contains(_searchQuery)) + .toList(); + } else if (_showFavoritesOnly) { + displayedChannels = groupedChannels.values + .expand((l) => l) + .where((c) => favorites.contains(c.streamId)) + .toList(); + } else if (uiState.selectedCategory != null) { + displayedChannels = + groupedChannels[uiState.selectedCategory] ?? []; + } + } + + return SafeArea( + bottom: false, + child: Column( + children: [ + // Header (Search + Title/Back) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + child: Column( children: [ - if (!showGrid || _showFavoritesOnly || _searchQuery.isNotEmpty) ...[ - IconButton( - icon: const Icon(Icons.arrow_back, color: AppColors.textPrimary), - onPressed: () { - if (_searchQuery.isNotEmpty) { - _searchController.clear(); - } else if (_showFavoritesOnly) { - setState(() => _showFavoritesOnly = false); - } else { - uiNotifier.state = uiState.copyWith(isCategoryView: true); - } - }, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - const SizedBox(width: 12), - ], - Expanded( - child: Text( - _searchQuery.isNotEmpty ? 'Résultats de recherche' : - _showFavoritesOnly ? 'Favoris' : - showGrid ? 'Catégories' : - uiState.selectedCategory ?? 'Chaînes', - style: const TextStyle( - color: AppColors.textPrimary, - fontWeight: FontWeight.bold, - fontSize: 18, + // Search Bar - Wrapped in TVFocusable for remote access + TVFocusable( + onPressed: () { + setState(() => _isSearchEditing = true); + WidgetsBinding.instance.addPostFrameCallback((_) { + _searchFocusNode.requestFocus(); + SystemChannels.textInput + .invokeMethod('TextInput.show'); + }); + }, + borderRadius: BorderRadius.circular(12), + child: Container( + height: 48, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: _isSearchEditing + ? Border.all(color: AppColors.primary) + : null, + ), + padding: + const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + const Icon(Icons.search, + color: AppColors.textSecondary), + const SizedBox(width: 8), + Expanded( + child: ExcludeFocus( + excluding: !_isSearchEditing, + child: TextField( + controller: _searchController, + focusNode: _searchFocusNode, + readOnly: !_isSearchEditing, + style: const TextStyle( + color: AppColors.textPrimary), + decoration: const InputDecoration( + hintText: 'Rechercher une chaîne...', + border: InputBorder.none, + isDense: true, + ), + onSubmitted: (_) => setState( + () => _isSearchEditing = false), + ), + ), + ), + if (_searchQuery.isNotEmpty) + GestureDetector( + onTap: () => _searchController.clear(), + child: const Icon(Icons.close, + color: AppColors.textSecondary), + ), + ], ), - overflow: TextOverflow.ellipsis, ), ), - IconButton( - icon: Icon( - _showFavoritesOnly ? Icons.favorite : Icons.favorite_border, - color: _showFavoritesOnly ? AppColors.error : AppColors.textSecondary, - ), - onPressed: () => setState(() { - _showFavoritesOnly = !_showFavoritesOnly; - // Reset category view if entering favorites - if (_showFavoritesOnly) uiNotifier.state = uiState.copyWith(isCategoryView: false); - // If exiting favorites, default depends on logic (here back to category grid if was previously) - if (!_showFavoritesOnly) uiNotifier.state = uiState.copyWith(isCategoryView: true); - }), + + const SizedBox(height: 12), + + // Title / Navigation Row + Row( + children: [ + if (!showGrid || + _showFavoritesOnly || + _searchQuery.isNotEmpty) ...[ + IconButton( + icon: const Icon(Icons.arrow_back, + color: AppColors.textPrimary), + onPressed: () { + if (_searchQuery.isNotEmpty) { + _searchController.clear(); + } else if (_showFavoritesOnly) { + setState( + () => _showFavoritesOnly = false); + } else { + uiNotifier.state = uiState.copyWith( + isCategoryView: true); + } + }, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + const SizedBox(width: 12), + ], + Expanded( + child: Text( + _searchQuery.isNotEmpty + ? 'Résultats de recherche' + : _showFavoritesOnly + ? 'Favoris' + : showGrid + ? 'Catégories' + : uiState.selectedCategory ?? + 'Chaînes', + style: const TextStyle( + color: AppColors.textPrimary, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + overflow: TextOverflow.ellipsis, + ), + ), + IconButton( + icon: Icon( + _showFavoritesOnly + ? Icons.favorite + : Icons.favorite_border, + color: _showFavoritesOnly + ? AppColors.error + : AppColors.textSecondary, + ), + onPressed: () => setState(() { + _showFavoritesOnly = !_showFavoritesOnly; + // Reset category view if entering favorites + if (_showFavoritesOnly) + uiNotifier.state = + uiState.copyWith(isCategoryView: false); + // If exiting favorites, default depends on logic (here back to category grid if was previously) + if (!_showFavoritesOnly) + uiNotifier.state = + uiState.copyWith(isCategoryView: true); + }), + ), + ], ), ], ), - ], - ), - ), + ), - // Main Content - Expanded( - child: showGrid - ? _buildCategoryGrid(categories) - : _buildChannelList(displayedChannels), + // Main Content + Expanded( + child: showGrid + ? _buildCategoryGrid(categories) + : _buildChannelList(displayedChannels), + ), + ], ), - ], - ), - ); - }, - ), - ), + ); + }, + ), + ), ), ); } @@ -280,7 +317,8 @@ class _MobileLiveTVTabState extends ConsumerState with Automati final category = categories[index]; return TVFocusable( onPressed: () { - ref.read(mobileLiveTvUiStateProvider.notifier).state = LiveTvUiState( + ref.read(mobileLiveTvUiStateProvider.notifier).state = + LiveTvUiState( selectedCategory: category, isCategoryView: false, ); @@ -310,9 +348,9 @@ class _MobileLiveTVTabState extends ConsumerState with Automati child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon( - Icons.tv, - color: AppColors.primary, + const Icon( + Icons.tv, + color: AppColors.primary, size: 32, ), const SizedBox(height: 12), @@ -359,45 +397,30 @@ class _MobileLiveTVTabState extends ConsumerState with Automati return _MobileChannelCard( channel: channel, playlist: widget.playlist, - onTap: () => _playChannel(context, channel, channels, widget.playlist, index), + onTap: () => + _playChannel(context, channel, channels, widget.playlist, 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, - ), + void _playChannel(BuildContext context, Channel channel, + List channels, PlaylistConfig playlist, int index) async { + // ENFORCE LITE PLAYER FOR LIVE TV + 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); @@ -411,7 +434,7 @@ class _MobileChannelCard extends ConsumerStatefulWidget { final PlaylistConfig playlist; const _MobileChannelCard({ - required this.channel, + required this.channel, required this.onTap, required this.playlist, }); @@ -422,16 +445,17 @@ class _MobileChannelCard extends ConsumerStatefulWidget { class _MobileChannelCardState extends ConsumerState<_MobileChannelCard> { String? _epgTitle; - + @override void initState() { super.initState(); _loadEpg(); } - + Future _loadEpg() async { try { - final service = await ref.read(mobileXtreamServiceProvider(widget.playlist).future); + final service = + await ref.read(mobileXtreamServiceProvider(widget.playlist).future); final epg = await service.getShortEPG(widget.channel.streamId); if (mounted && epg.nowPlaying != null && epg.nowPlaying!.isNotEmpty) { setState(() { @@ -445,17 +469,20 @@ class _MobileChannelCardState extends ConsumerState<_MobileChannelCard> { @override Widget build(BuildContext context) { - final iconUrl = widget.channel.streamIcon.isNotEmpty && widget.channel.streamIcon.startsWith('http') - ? widget.channel.streamIcon + final iconUrl = widget.channel.streamIcon.isNotEmpty && + widget.channel.streamIcon.startsWith('http') + ? widget.channel.streamIcon : null; - + final favorites = ref.watch(mobileFavoritesProvider); final isFavorite = favorites.contains(widget.channel.streamId); return TVFocusable( onPressed: widget.onTap, onLongPress: () { - ref.read(mobileFavoritesProvider.notifier).toggle(widget.channel.streamId); + ref + .read(mobileFavoritesProvider.notifier) + .toggle(widget.channel.streamId); }, child: Container( decoration: BoxDecoration( @@ -489,10 +516,13 @@ class _MobileChannelCardState extends ConsumerState<_MobileChannelCard> { ? CachedNetworkImage( imageUrl: iconUrl, fit: BoxFit.contain, - errorWidget: (_, __, ___) => const Icon(Icons.tv, color: Colors.white38, size: 40), - placeholder: (_, __) => const Icon(Icons.tv, color: Colors.white24, size: 40), + errorWidget: (_, __, ___) => const Icon(Icons.tv, + color: Colors.white38, size: 40), + placeholder: (_, __) => const Icon(Icons.tv, + color: Colors.white24, size: 40), ) - : const Icon(Icons.tv, color: Colors.white38, size: 40), + : const Icon(Icons.tv, + color: Colors.white38, size: 40), ), // Favorite badge if (isFavorite) @@ -505,7 +535,8 @@ class _MobileChannelCardState extends ConsumerState<_MobileChannelCard> { color: Colors.red, borderRadius: BorderRadius.circular(12), ), - child: const Icon(Icons.favorite, color: Colors.white, size: 12), + child: const Icon(Icons.favorite, + color: Colors.white, size: 12), ), ), ], diff --git a/lib/mobile/features/iptv/widgets/mobile_movies_tab.dart b/lib/mobile/features/iptv/widgets/mobile_movies_tab.dart index d1e54f1..659b4d0 100644 --- a/lib/mobile/features/iptv/widgets/mobile_movies_tab.dart +++ b/lib/mobile/features/iptv/widgets/mobile_movies_tab.dart @@ -181,7 +181,7 @@ class _MobileMoviesTabState extends ConsumerState void _playMovie(BuildContext context, Movie movie) { if (widget.playlist == null) return; - // DEBUG: FORCE NATIVE PLAYER (Ignoring settings) + // Enforce Native Player for Movies (VOD) Navigator.push( context, MaterialPageRoute( @@ -311,7 +311,7 @@ class _MobileMoviesTabState extends ConsumerState style: const TextStyle( fontSize: 14, color: AppColors.textPrimary), decoration: const InputDecoration( - hintText: 'Rechercher (DEBUG V3)...', + hintText: 'Rechercher un film...', border: InputBorder.none, isDense: true, contentPadding: EdgeInsets.only(bottom: 11), diff --git a/lib/mobile/features/iptv/widgets/mobile_settings_tab.dart b/lib/mobile/features/iptv/widgets/mobile_settings_tab.dart index fc2782a..bfbaf82 100644 --- a/lib/mobile/features/iptv/widgets/mobile_settings_tab.dart +++ b/lib/mobile/features/iptv/widgets/mobile_settings_tab.dart @@ -51,7 +51,11 @@ class _MobileSettingsTabState extends ConsumerState { } List _parseKeywords(String text) { - return text.split(',').map((s) => s.trim()).where((s) => s.isNotEmpty).toList(); + return text + .split(',') + .map((s) => s.trim()) + .where((s) => s.isNotEmpty) + .toList(); } @override @@ -59,7 +63,7 @@ class _MobileSettingsTabState extends ConsumerState { final settings = ref.watch(mobileSettingsProvider); final themeNotifier = ref.read(themeProvider.notifier); final isDark = Theme.of(context).brightness == Brightness.dark; - + WidgetsBinding.instance.addPostFrameCallback((_) { if (!_initialized) _syncControllers(settings); }); @@ -80,8 +84,8 @@ class _MobileSettingsTabState extends ConsumerState { color: AppColors.primary.withOpacity(0.2), borderRadius: BorderRadius.circular(12), ), - child: const Icon(Icons.play_circle_filled, - color: AppColors.primary, size: 28), + child: const Icon(Icons.play_circle_filled, + color: AppColors.primary, size: 28), ), const SizedBox(width: 16), Column( @@ -107,50 +111,43 @@ class _MobileSettingsTabState extends ConsumerState { ], ), ), - + const SizedBox(height: 16), // === APPARENCE === _buildSectionHeader('Apparence'), - + _buildSettingItem( icon: isDark ? Icons.dark_mode : Icons.light_mode, title: 'Mode Sombre', value: isDark ? 'Activé' : 'Désactivé', onTap: () => themeNotifier.toggleTheme(), ), - + _buildSettingItem( icon: Icons.access_time, title: 'Afficher l\'heure', value: settings.showClock ? 'Activé' : 'Désactivé', - onTap: () => ref.read(mobileSettingsProvider.notifier).toggleShowClock(!settings.showClock), + onTap: () => ref + .read(mobileSettingsProvider.notifier) + .toggleShowClock(!settings.showClock), ), - + const SizedBox(height: 24), // === LECTURE VIDEO === _buildSectionHeader('Lecture Vidéo'), - - _buildSettingItem( - icon: Icons.rocket_launch, - title: 'Moteur Vidéo', - subtitle: settings.playerEngine == 'ultra' ? 'Ultra (MPV) - Puissant' : 'Lite (ExoPlayer) - Léger', - value: settings.playerEngine == 'ultra' ? 'Ultra' : 'Lite', - onTap: () { - final newValue = settings.playerEngine == 'ultra' ? 'lite' : 'ultra'; - ref.read(mobileSettingsProvider.notifier).setPlayerEngine(newValue); - }, - ), - + _buildSettingItem( icon: Icons.bug_report, title: 'Stats pour Nerds', subtitle: 'Affiche FPS, Buffer, Bitrate', value: settings.showDebugStats ? 'Activé' : 'Désactivé', - onTap: () => ref.read(mobileSettingsProvider.notifier).toggleShowDebugStats(!settings.showDebugStats), + onTap: () => ref + .read(mobileSettingsProvider.notifier) + .toggleShowDebugStats(!settings.showDebugStats), ), - + _buildSettingItem( icon: Icons.memory, title: 'Décodeur Vidéo', @@ -160,63 +157,77 @@ class _MobileSettingsTabState extends ConsumerState { final modes = ['auto', 'mediacodec', 'no']; final currentIndex = modes.indexOf(settings.decoderMode); final nextIndex = (currentIndex + 1) % modes.length; - ref.read(mobileSettingsProvider.notifier).setDecoderMode(modes[nextIndex]); + ref + .read(mobileSettingsProvider.notifier) + .setDecoderMode(modes[nextIndex]); }, ), - + _buildSettingItem( icon: Icons.speed, title: 'Buffer (Cache)', - subtitle: settings.bufferDuration == 0 ? 'Automatique' : '${settings.bufferDuration} secondes', - value: settings.bufferDuration == 0 ? 'Auto' : '${settings.bufferDuration}s', + subtitle: settings.bufferDuration == 0 + ? 'Automatique' + : '${settings.bufferDuration} secondes', + value: settings.bufferDuration == 0 + ? 'Auto' + : '${settings.bufferDuration}s', onTap: () { final buffers = [0, 15, 30, 60]; final currentIndex = buffers.indexOf(settings.bufferDuration); final nextIndex = (currentIndex + 1) % buffers.length; - ref.read(mobileSettingsProvider.notifier).setBufferDuration(buffers[nextIndex]); + ref + .read(mobileSettingsProvider.notifier) + .setBufferDuration(buffers[nextIndex]); }, ), - + const SizedBox(height: 24), // === FILTRES === _buildSectionHeader('Filtres de Contenu'), - + _buildFilterInput( label: 'TV en Direct', hint: 'Ex: FR, HD, SPORT', icon: Icons.live_tv, controller: _liveTvController, onChanged: (val) { - ref.read(mobileSettingsProvider.notifier).setLiveTvKeywords(_parseKeywords(val)); + ref + .read(mobileSettingsProvider.notifier) + .setLiveTvKeywords(_parseKeywords(val)); }, ), - + _buildFilterInput( label: 'Films', hint: 'Ex: FR, VF, 4K', icon: Icons.movie, controller: _moviesController, onChanged: (val) { - ref.read(mobileSettingsProvider.notifier).setMoviesKeywords(_parseKeywords(val)); + ref + .read(mobileSettingsProvider.notifier) + .setMoviesKeywords(_parseKeywords(val)); }, ), - + _buildFilterInput( label: 'Séries', hint: 'Ex: FR, VF', icon: Icons.tv, controller: _seriesController, onChanged: (val) { - ref.read(mobileSettingsProvider.notifier).setSeriesKeywords(_parseKeywords(val)); + ref + .read(mobileSettingsProvider.notifier) + .setSeriesKeywords(_parseKeywords(val)); }, ), - + const SizedBox(height: 24), // === CACHE === _buildSectionHeader('Cache'), - + _buildSettingItem( icon: _isRefreshingCache ? Icons.hourglass_empty : Icons.refresh, title: 'Actualiser le cache', @@ -224,12 +235,12 @@ class _MobileSettingsTabState extends ConsumerState { value: _isRefreshingCache ? '...' : 'Appuyer', onTap: _isRefreshingCache ? null : _refreshCache, ), - + const SizedBox(height: 24), // === PLAYLIST === _buildSectionHeader('Playlist'), - + _buildSettingItem( icon: Icons.playlist_play, title: 'Gérer les Playlists', @@ -239,12 +250,13 @@ class _MobileSettingsTabState extends ConsumerState { Navigator.push( context, MaterialPageRoute( - builder: (_) => const MobilePlaylistSelectionScreen(manageMode: true), + builder: (_) => + const MobilePlaylistSelectionScreen(manageMode: true), ), ); }, ), - + const SizedBox(height: 100), ], ), @@ -252,33 +264,41 @@ class _MobileSettingsTabState extends ConsumerState { } // === HELPER METHODS === - + String _getDecoderSubtitle(String mode) { switch (mode) { - case 'auto': return 'Automatique (Recommandé)'; - case 'mediacodec': return 'Matériel (Hardware)'; - case 'no': return 'Logiciel (Software)'; - default: return 'Automatique'; + case 'auto': + return 'Automatique (Recommandé)'; + case 'mediacodec': + return 'Matériel (Hardware)'; + case 'no': + return 'Logiciel (Software)'; + default: + return 'Automatique'; } } - + String _getDecoderValue(String mode) { switch (mode) { - case 'auto': return 'Auto'; - case 'mediacodec': return 'HW'; - case 'no': return 'SW'; - default: return 'Auto'; + case 'auto': + return 'Auto'; + case 'mediacodec': + return 'HW'; + case 'no': + return 'SW'; + default: + return 'Auto'; } } Future _refreshCache() async { setState(() => _isRefreshingCache = true); - + try { final dir = await getApplicationDocumentsDirectory(); final cacheStore = HiveCacheStore(dir.path); await cacheStore.clean(); - + if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( @@ -300,7 +320,7 @@ class _MobileSettingsTabState extends ConsumerState { } // === UI BUILDERS === - + Widget _buildSectionHeader(String title) { return Padding( padding: const EdgeInsets.only(left: 4, bottom: 8, top: 8), @@ -362,7 +382,8 @@ class _MobileSettingsTabState extends ConsumerState { ), ), Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: AppColors.primary.withOpacity(0.15), borderRadius: BorderRadius.circular(8), @@ -423,8 +444,8 @@ class _MobileSettingsTabState extends ConsumerState { Text( controller.text.isEmpty ? hint : controller.text, style: TextStyle( - color: controller.text.isEmpty - ? AppColors.textSecondary + color: controller.text.isEmpty + ? AppColors.textSecondary : AppColors.textPrimary, fontSize: 12, ), @@ -442,14 +463,16 @@ class _MobileSettingsTabState extends ConsumerState { ); } - void _showKeyboardDialog(String label, TextEditingController controller, ValueChanged onChanged) { + void _showKeyboardDialog(String label, TextEditingController controller, + ValueChanged onChanged) { final tempController = TextEditingController(text: controller.text); - + showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: AppColors.surface, - title: Text('Filtres $label', style: const TextStyle(color: AppColors.textPrimary)), + title: Text('Filtres $label', + style: const TextStyle(color: AppColors.textPrimary)), content: TextField( controller: tempController, autofocus: true,