From 5fedee7ec361e5f9f5f4a3c5212baed75b86cbb4 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Tue, 16 Dec 2025 22:31:59 +0100 Subject: [PATCH] feat: force native player usage and add debug ui hint --- .../iptv/widgets/mobile_movies_tab.dart | 393 +++++++++--------- 1 file changed, 200 insertions(+), 193 deletions(-) diff --git a/lib/mobile/features/iptv/widgets/mobile_movies_tab.dart b/lib/mobile/features/iptv/widgets/mobile_movies_tab.dart index 93f1925..d1e54f1 100644 --- a/lib/mobile/features/iptv/widgets/mobile_movies_tab.dart +++ b/lib/mobile/features/iptv/widgets/mobile_movies_tab.dart @@ -22,7 +22,8 @@ class MobileMoviesTab extends ConsumerStatefulWidget { ConsumerState createState() => _MobileMoviesTabState(); } -class _MobileMoviesTabState extends ConsumerState with AutomaticKeepAliveClientMixin { +class _MobileMoviesTabState extends ConsumerState + with AutomaticKeepAliveClientMixin { final ScrollController _scrollController = ScrollController(); final TextEditingController _searchController = TextEditingController(); final FocusNode _searchFocusNode = FocusNode(); @@ -34,7 +35,7 @@ class _MobileMoviesTabState extends ConsumerState with Automati bool _isSearching = false; bool _hasMore = true; int _currentOffset = 0; - static const int _pageSize = 50; + static const int _pageSize = 50; Timer? _searchDebounce; @override @@ -60,9 +61,10 @@ class _MobileMoviesTabState extends ConsumerState with Automati void _onScroll() { // Only trigger load when near bottom and not already loading - if (!_isLoading && _hasMore && + if (!_isLoading && + _hasMore && _scrollController.position.pixels >= - _scrollController.position.maxScrollExtent * 0.8) { + _scrollController.position.maxScrollExtent * 0.8) { _loadMoreMovies(); } } @@ -76,7 +78,7 @@ class _MobileMoviesTabState extends ConsumerState with Automati _isSearching = false; } }); - + if (query.length >= 2) { _searchDebounce = Timer(const Duration(milliseconds: 500), () { _performSearch(query); @@ -87,11 +89,12 @@ class _MobileMoviesTabState extends ConsumerState with Automati Future _performSearch(String query) async { if (query.isEmpty) return; setState(() => _isSearching = true); - + try { - final service = await ref.read(mobileXtreamServiceProvider(widget.playlist).future); + final service = + await ref.read(mobileXtreamServiceProvider(widget.playlist).future); final results = await service.searchMovies(query); - + if (mounted && _searchQuery == query) { setState(() { _searchResults = results; @@ -110,13 +113,16 @@ class _MobileMoviesTabState extends ConsumerState with Automati setState(() => _isLoading = true); try { - final service = await ref.read(mobileXtreamServiceProvider(widget.playlist).future); - + final service = + await ref.read(mobileXtreamServiceProvider(widget.playlist).future); + // Add timeout to prevent infinite loading state - final newMovies = await service.getMoviesPaginated( - offset: _currentOffset, - limit: _pageSize, - ).timeout(const Duration(seconds: 15)); + final newMovies = await service + .getMoviesPaginated( + offset: _currentOffset, + limit: _pageSize, + ) + .timeout(const Duration(seconds: 15)); if (mounted) { setState(() { @@ -140,10 +146,11 @@ class _MobileMoviesTabState extends ConsumerState with Automati Future _refresh() async { // Clear service cache to force fresh data try { - final service = await ref.read(mobileXtreamServiceProvider(widget.playlist).future); + final service = + await ref.read(mobileXtreamServiceProvider(widget.playlist).future); service.clearCache(); } catch (_) {} - + setState(() { _movies.clear(); _currentOffset = 0; @@ -174,35 +181,19 @@ class _MobileMoviesTabState extends ConsumerState with Automati void _playMovie(BuildContext context, Movie movie) { if (widget.playlist == null) return; - final engine = ref.read(mobileSettingsProvider).playerEngine; - - if (engine == 'lite') { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => LitePlayerScreen( - streamId: movie.streamId, - title: movie.name, - playlist: widget.playlist!, - streamType: StreamType.vod, - containerExtension: movie.containerExtension, - ), + // DEBUG: FORCE NATIVE PLAYER (Ignoring settings) + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => NativePlayerScreen( + streamId: movie.streamId, + title: movie.name, + playlist: widget.playlist!, + streamType: StreamType.vod, + containerExtension: movie.containerExtension, ), - ); - } else { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => NativePlayerScreen( - streamId: movie.streamId, - title: movie.name, - playlist: widget.playlist!, - streamType: StreamType.vod, - containerExtension: movie.containerExtension, - ), - ), - ); - } + ), + ); } @override @@ -213,7 +204,7 @@ class _MobileMoviesTabState extends ConsumerState with Automati super.build(context); // Required for AutomaticKeepAliveClientMixin final settings = ref.watch(mobileSettingsProvider); final watchHistory = ref.watch(mobileWatchHistoryProvider); - + // Retry load if movies list is empty (fixes issue where EPG loads but tab needs refresh) if (_movies.isEmpty && !_isLoading && _hasMore) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -222,167 +213,183 @@ class _MobileMoviesTabState extends ConsumerState with Automati } }); } - + List displayMovies; if (_searchQuery.isNotEmpty && _searchResults != null) { displayMovies = _searchResults!; } else { displayMovies = settings.moviesKeywords.isEmpty ? _movies - : _movies.where((m) => settings.matchesMoviesFilter(m.categoryName)).toList(); + : _movies + .where((m) => settings.matchesMoviesFilter(m.categoryName)) + .toList(); } // Hero Items (use filtered movies) - final heroItems = displayMovies.take(3).map((m) => HeroItem( - id: m.streamId, - title: m.name, - imageUrl: _getImageUrl(m.streamIcon), - subtitle: m.rating != null ? '${_formatRating(m.rating)} ★' : null, - onMoreInfo: () => _playMovie(context, m), - )).toList(); + final heroItems = displayMovies + .take(3) + .map((m) => HeroItem( + id: m.streamId, + title: m.name, + imageUrl: _getImageUrl(m.streamIcon), + subtitle: + m.rating != null ? '${_formatRating(m.rating)} ★' : null, + onMoreInfo: () => _playMovie(context, m), + )) + .toList(); - return Container( - decoration: const BoxDecoration(gradient: AppColors.appleTvGradient), - child: SafeArea( - bottom: false, - child: RefreshIndicator( - onRefresh: _refresh, - color: AppColors.primary, - backgroundColor: AppColors.surface, - child: CustomScrollView( - controller: _scrollController, - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - // Hero Section (reduced height, moved above search) - if (_searchQuery.isEmpty && heroItems.isNotEmpty) - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(top: 8, bottom: 8), - child: SizedBox( - height: 180, // Reduced from 250 - child: HeroCarousel( - items: heroItems, - onTap: (item) { - try { - final movie = _movies.firstWhere((element) => element.streamId == item.id); - _playMovie(context, movie); - } catch (e) { - // Fallback ignored - } - }, + return Container( + decoration: const BoxDecoration(gradient: AppColors.appleTvGradient), + child: SafeArea( + bottom: false, + child: RefreshIndicator( + onRefresh: _refresh, + color: AppColors.primary, + backgroundColor: AppColors.surface, + child: CustomScrollView( + controller: _scrollController, + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + // Hero Section (reduced height, moved above search) + if (_searchQuery.isEmpty && heroItems.isNotEmpty) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(top: 8, bottom: 8), + child: SizedBox( + height: 180, // Reduced from 250 + child: HeroCarousel( + items: heroItems, + onTap: (item) { + try { + final movie = _movies.firstWhere( + (element) => element.streamId == item.id); + _playMovie(context, movie); + } catch (e) { + // Fallback ignored + } + }, + ), ), - ), - ), - ), + ), + ), - // Search Bar (moved below carousel) - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: TVFocusable( - onPressed: () { - setState(() => _isSearchEditing = true); - WidgetsBinding.instance.addPostFrameCallback((_) { - _searchFocusNode.requestFocus(); - }); - }, - borderRadius: BorderRadius.circular(12), - child: Container( - height: 40, - decoration: BoxDecoration( - color: AppColors.surface, - borderRadius: BorderRadius.circular(12), - border: _isSearchEditing - ? Border.all(color: AppColors.primary) - : Border.all(color: AppColors.border), - ), - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Row( - children: [ - const Icon(Icons.search, size: 20, color: AppColors.textSecondary), - const SizedBox(width: 8), - Expanded( - child: ExcludeFocus( - excluding: !_isSearchEditing, - child: TextField( - controller: _searchController, - focusNode: _searchFocusNode, - readOnly: !_isSearchEditing, - style: const TextStyle(fontSize: 14, color: AppColors.textPrimary), - decoration: const InputDecoration( - hintText: 'Rechercher un film...', - border: InputBorder.none, - isDense: true, - contentPadding: EdgeInsets.only(bottom: 11), - ), - onChanged: _onSearchChanged, - onSubmitted: (_) => setState(() => _isSearchEditing = false), - ), - ), - ), - if (_isSearching) - const SizedBox(width: 12, height: 12, child: CircularProgressIndicator(strokeWidth: 2)), - if (_searchQuery.isNotEmpty) - GestureDetector( - onTap: () { + // Search Bar (moved below carousel) + SliverToBoxAdapter( + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: TVFocusable( + onPressed: () { + setState(() => _isSearchEditing = true); + WidgetsBinding.instance.addPostFrameCallback((_) { + _searchFocusNode.requestFocus(); + }); + }, + borderRadius: BorderRadius.circular(12), + child: Container( + height: 40, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: _isSearchEditing + ? Border.all(color: AppColors.primary) + : Border.all(color: AppColors.border), + ), + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + const Icon(Icons.search, + size: 20, color: AppColors.textSecondary), + const SizedBox(width: 8), + Expanded( + child: ExcludeFocus( + excluding: !_isSearchEditing, + child: TextField( + controller: _searchController, + focusNode: _searchFocusNode, + readOnly: !_isSearchEditing, + style: const TextStyle( + fontSize: 14, color: AppColors.textPrimary), + decoration: const InputDecoration( + hintText: 'Rechercher (DEBUG V3)...', + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.only(bottom: 11), + ), + onChanged: _onSearchChanged, + onSubmitted: (_) => + setState(() => _isSearchEditing = false), + ), + ), + ), + if (_isSearching) + const SizedBox( + width: 12, + height: 12, + child: + CircularProgressIndicator(strokeWidth: 2)), + if (_searchQuery.isNotEmpty) + GestureDetector( + onTap: () { _searchController.clear(); _onSearchChanged(''); - }, - child: const Icon(Icons.close, size: 16, color: AppColors.textSecondary), - ), - ], - ), - ), - ), - ), - ), - - // Grid - SliverPadding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 80), - sliver: SliverGrid( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 8, - crossAxisSpacing: 8, - mainAxisSpacing: 8, - childAspectRatio: 0.7, - ), - delegate: SliverChildBuilderDelegate( - (context, index) { - if (index >= displayMovies.length) return null; - final movie = displayMovies[index]; - final isWatched = watchHistory.isMovieWatched(movie.streamId); - - return MobilePosterCard( - title: movie.name, - imageUrl: _getImageUrl(movie.streamIcon), - rating: _formatRating(movie.rating), - isWatched: isWatched, - onTap: () => _playMovie(context, movie), - onLongPress: () { - ref.read(mobileWatchHistoryProvider.notifier).toggleMovieWatched(movie.streamId); - }, - ); - }, - childCount: displayMovies.length, - ), - ), - ), - - if (_isLoading) - const SliverToBoxAdapter( - child: Padding( - padding: EdgeInsets.all(24), - child: Center(child: CircularProgressIndicator()), - ), - ), - ], - ), - ), - ), - ); + }, + child: const Icon(Icons.close, + size: 16, color: AppColors.textSecondary), + ), + ], + ), + ), + ), + ), + ), + + // Grid + SliverPadding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 80), + sliver: SliverGrid( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 8, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + childAspectRatio: 0.7, + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index >= displayMovies.length) return null; + final movie = displayMovies[index]; + final isWatched = + watchHistory.isMovieWatched(movie.streamId); + + return MobilePosterCard( + title: movie.name, + imageUrl: _getImageUrl(movie.streamIcon), + rating: _formatRating(movie.rating), + isWatched: isWatched, + onTap: () => _playMovie(context, movie), + onLongPress: () { + ref + .read(mobileWatchHistoryProvider.notifier) + .toggleMovieWatched(movie.streamId); + }, + ); + }, + childCount: displayMovies.length, + ), + ), + ), + + if (_isLoading) + const SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.all(24), + child: Center(child: CircularProgressIndicator()), + ), + ), + ], + ), + ), + ), + ); } } - - -