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