mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-11 17:29:26 +02:00
feat: enforce player strategy (vod=ultra, live=lite), clean UI and settings
This commit is contained in:
1 parent
5fedee7ec3
commit
a2b8d5fb09
5 files changed
+203
-150
No files matched your search
@@ -21,10 +21,12 @@ class MobileSeriesDetailScreen extends ConsumerStatefulWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<MobileSeriesDetailScreen> createState() => _MobileSeriesDetailScreenState();
|
||||
ConsumerState<MobileSeriesDetailScreen> createState() =>
|
||||
_MobileSeriesDetailScreenState();
|
||||
}
|
||||
|
||||
class _MobileSeriesDetailScreenState extends ConsumerState<MobileSeriesDetailScreen> {
|
||||
class _MobileSeriesDetailScreenState
|
||||
extends ConsumerState<MobileSeriesDetailScreen> {
|
||||
SeriesInfo? _seriesInfo;
|
||||
bool _isLoading = true;
|
||||
String? _error;
|
||||
@@ -52,8 +54,10 @@ class _MobileSeriesDetailScreenState extends ConsumerState<MobileSeriesDetailScr
|
||||
_error = null;
|
||||
});
|
||||
|
||||
final service = await ref.read(mobileXtreamServiceProvider(widget.playlist).future);
|
||||
final info = await service.getSeriesInfo(widget.series.seriesId.toString());
|
||||
final service =
|
||||
await ref.read(mobileXtreamServiceProvider(widget.playlist).future);
|
||||
final info =
|
||||
await service.getSeriesInfo(widget.series.seriesId.toString());
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
@@ -85,10 +89,15 @@ class _MobileSeriesDetailScreenState extends ConsumerState<MobileSeriesDetailScr
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, size: 48, color: AppColors.error),
|
||||
const Icon(Icons.error_outline,
|
||||
size: 48, color: AppColors.error),
|
||||
const SizedBox(height: 16),
|
||||
Text('Error: $_error', style: const TextStyle(color: AppColors.textSecondary)),
|
||||
TextButton(onPressed: _loadSeriesInfo, child: const Text('Retry')),
|
||||
Text('Error: $_error',
|
||||
style:
|
||||
const TextStyle(color: AppColors.textSecondary)),
|
||||
TextButton(
|
||||
onPressed: _loadSeriesInfo,
|
||||
child: const Text('Retry')),
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -128,7 +137,8 @@ class _MobileSeriesDetailScreenState extends ConsumerState<MobileSeriesDetailScr
|
||||
CachedNetworkImage(
|
||||
imageUrl: _seriesInfo!.cover!,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (_, __, ___) => Container(color: Colors.grey.shade900),
|
||||
errorWidget: (_, __, ___) =>
|
||||
Container(color: Colors.grey.shade900),
|
||||
),
|
||||
// Gradient Overlay
|
||||
Container(
|
||||
@@ -166,17 +176,20 @@ class _MobileSeriesDetailScreenState extends ConsumerState<MobileSeriesDetailScr
|
||||
Row(
|
||||
children: [
|
||||
if (_seriesInfo!.rating != null) ...[
|
||||
const Icon(Icons.star, size: 16, color: Colors.amber),
|
||||
const Icon(Icons.star,
|
||||
size: 16, color: Colors.amber),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
_formatRating(_seriesInfo!.rating!)!,
|
||||
style: const TextStyle(color: AppColors.textSecondary, fontSize: 13),
|
||||
style: const TextStyle(
|
||||
color: AppColors.textSecondary, fontSize: 13),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
Text(
|
||||
'${_seriesInfo!.episodes.keys.length} Seasons',
|
||||
style: const TextStyle(color: AppColors.textSecondary, fontSize: 13),
|
||||
style: const TextStyle(
|
||||
color: AppColors.textSecondary, fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -195,7 +208,8 @@ class _MobileSeriesDetailScreenState extends ConsumerState<MobileSeriesDetailScr
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
_seriesInfo!.plot!,
|
||||
style: const TextStyle(color: AppColors.textSecondary, height: 1.4),
|
||||
style: const TextStyle(
|
||||
color: AppColors.textSecondary, height: 1.4),
|
||||
maxLines: 4,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -215,23 +229,31 @@ class _MobileSeriesDetailScreenState extends ConsumerState<MobileSeriesDetailScr
|
||||
itemCount: _seriesInfo!.episodes.keys.length,
|
||||
separatorBuilder: (_, __) => 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
|
||||
? (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,
|
||||
);
|
||||
@@ -274,38 +296,24 @@ class _MobileSeriesDetailScreenState extends ConsumerState<MobileSeriesDetailScr
|
||||
final theme = Theme.of(context);
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
final textColor = isDark ? Colors.white : Colors.black;
|
||||
final secondaryTextColor = isDark ? Colors.grey.shade400 : Colors.grey.shade600;
|
||||
final surfaceColor = isDark ? const Color(0xFF1C1C1E) : Colors.grey.shade200;
|
||||
final secondaryTextColor =
|
||||
isDark ? Colors.grey.shade400 : Colors.grey.shade600;
|
||||
final surfaceColor =
|
||||
isDark ? const Color(0xFF1C1C1E) : Colors.grey.shade200;
|
||||
final primaryColor = isDark ? Colors.white : Colors.blue;
|
||||
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
// Watch progress is tracked in player at 80% completion
|
||||
final engine = ref.read(mobileSettingsProvider).playerEngine;
|
||||
|
||||
if (engine == 'lite') {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => 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 {
|
||||
// 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}",
|
||||
title:
|
||||
"${widget.series.name} S${episode.seasonNum}E${episode.episodeNum}",
|
||||
playlist: widget.playlist,
|
||||
streamType: StreamType.series,
|
||||
containerExtension: episode.containerExtension ?? 'mp4',
|
||||
@@ -315,12 +323,13 @@ class _MobileSeriesDetailScreenState extends ConsumerState<MobileSeriesDetailScr
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
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<MobileSeriesDetailScr
|
||||
decoration: BoxDecoration(
|
||||
color: isWatched ? primaryColor : surfaceColor,
|
||||
shape: BoxShape.circle,
|
||||
border: isWatched ? null : Border.all(color: primaryColor, width: 2),
|
||||
border: isWatched
|
||||
? null
|
||||
: Border.all(color: primaryColor, width: 2),
|
||||
),
|
||||
child: Icon(
|
||||
isWatched ? Icons.check : Icons.play_arrow,
|
||||
color: isWatched ? (isDark ? Colors.black : Colors.white) : primaryColor,
|
||||
color: isWatched
|
||||
? (isDark ? Colors.black : Colors.white)
|
||||
: primaryColor,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -968,20 +968,6 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
||||
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 &&
|
||||
|
||||
@@ -23,14 +23,15 @@ class MobileLiveTVTab extends ConsumerStatefulWidget {
|
||||
ConsumerState<MobileLiveTVTab> createState() => _MobileLiveTVTabState();
|
||||
}
|
||||
|
||||
class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> with AutomaticKeepAliveClientMixin {
|
||||
|
||||
class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
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<MobileLiveTVTab> 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);
|
||||
@@ -67,7 +69,9 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> with Automati
|
||||
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,
|
||||
canPop: uiState.isCategoryView &&
|
||||
_searchQuery.isEmpty &&
|
||||
!_showFavoritesOnly,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (didPop) return;
|
||||
|
||||
@@ -104,7 +108,9 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> with Automati
|
||||
// Prepare categories
|
||||
var categories = groupedChannels.keys.toList();
|
||||
if (settings.liveTvKeywords.isNotEmpty) {
|
||||
categories = categories.where((cat) => settings.matchesLiveTvFilter(cat)).toList();
|
||||
categories = categories
|
||||
.where((cat) => settings.matchesLiveTvFilter(cat))
|
||||
.toList();
|
||||
}
|
||||
categories.sort();
|
||||
|
||||
@@ -112,19 +118,24 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> with Automati
|
||||
List<Channel> displayedChannels = [];
|
||||
|
||||
// Determine mode based on search/favorites/selection
|
||||
bool showGrid = uiState.isCategoryView && _searchQuery.isEmpty && !_showFavoritesOnly;
|
||||
bool showGrid = uiState.isCategoryView &&
|
||||
_searchQuery.isEmpty &&
|
||||
!_showFavoritesOnly;
|
||||
|
||||
if (!showGrid) {
|
||||
if (_searchQuery.isNotEmpty) {
|
||||
displayedChannels = groupedChannels.values.expand((l) => l)
|
||||
displayedChannels = groupedChannels.values
|
||||
.expand((l) => l)
|
||||
.where((c) => c.name.toLowerCase().contains(_searchQuery))
|
||||
.toList();
|
||||
} else if (_showFavoritesOnly) {
|
||||
displayedChannels = groupedChannels.values.expand((l) => l)
|
||||
displayedChannels = groupedChannels.values
|
||||
.expand((l) => l)
|
||||
.where((c) => favorites.contains(c.streamId))
|
||||
.toList();
|
||||
} else if (uiState.selectedCategory != null) {
|
||||
displayedChannels = groupedChannels[uiState.selectedCategory] ?? [];
|
||||
displayedChannels =
|
||||
groupedChannels[uiState.selectedCategory] ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +145,8 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> with Automati
|
||||
children: [
|
||||
// Header (Search + Title/Back)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
// Search Bar - Wrapped in TVFocusable for remote access
|
||||
@@ -143,7 +155,8 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> with Automati
|
||||
setState(() => _isSearchEditing = true);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_searchFocusNode.requestFocus();
|
||||
SystemChannels.textInput.invokeMethod('TextInput.show');
|
||||
SystemChannels.textInput
|
||||
.invokeMethod('TextInput.show');
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -152,12 +165,16 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> with Automati
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: _isSearchEditing ? Border.all(color: AppColors.primary) : null,
|
||||
border: _isSearchEditing
|
||||
? Border.all(color: AppColors.primary)
|
||||
: null,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.search, color: AppColors.textSecondary),
|
||||
const Icon(Icons.search,
|
||||
color: AppColors.textSecondary),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ExcludeFocus(
|
||||
@@ -166,20 +183,23 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> with Automati
|
||||
controller: _searchController,
|
||||
focusNode: _searchFocusNode,
|
||||
readOnly: !_isSearchEditing,
|
||||
style: const TextStyle(color: AppColors.textPrimary),
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary),
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Rechercher une chaîne...',
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
onSubmitted: (_) => setState(() => _isSearchEditing = false),
|
||||
onSubmitted: (_) => setState(
|
||||
() => _isSearchEditing = false),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_searchQuery.isNotEmpty)
|
||||
GestureDetector(
|
||||
onTap: () => _searchController.clear(),
|
||||
child: const Icon(Icons.close, color: AppColors.textSecondary),
|
||||
child: const Icon(Icons.close,
|
||||
color: AppColors.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -191,16 +211,21 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> with Automati
|
||||
// Title / Navigation Row
|
||||
Row(
|
||||
children: [
|
||||
if (!showGrid || _showFavoritesOnly || _searchQuery.isNotEmpty) ...[
|
||||
if (!showGrid ||
|
||||
_showFavoritesOnly ||
|
||||
_searchQuery.isNotEmpty) ...[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: AppColors.textPrimary),
|
||||
icon: const Icon(Icons.arrow_back,
|
||||
color: AppColors.textPrimary),
|
||||
onPressed: () {
|
||||
if (_searchQuery.isNotEmpty) {
|
||||
_searchController.clear();
|
||||
} else if (_showFavoritesOnly) {
|
||||
setState(() => _showFavoritesOnly = false);
|
||||
setState(
|
||||
() => _showFavoritesOnly = false);
|
||||
} else {
|
||||
uiNotifier.state = uiState.copyWith(isCategoryView: true);
|
||||
uiNotifier.state = uiState.copyWith(
|
||||
isCategoryView: true);
|
||||
}
|
||||
},
|
||||
padding: EdgeInsets.zero,
|
||||
@@ -210,10 +235,14 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> with Automati
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
_searchQuery.isNotEmpty ? 'Résultats de recherche' :
|
||||
_showFavoritesOnly ? 'Favoris' :
|
||||
showGrid ? 'Catégories' :
|
||||
uiState.selectedCategory ?? 'Chaînes',
|
||||
_searchQuery.isNotEmpty
|
||||
? 'Résultats de recherche'
|
||||
: _showFavoritesOnly
|
||||
? 'Favoris'
|
||||
: showGrid
|
||||
? 'Catégories'
|
||||
: uiState.selectedCategory ??
|
||||
'Chaînes',
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -224,15 +253,23 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> with Automati
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
_showFavoritesOnly ? Icons.favorite : Icons.favorite_border,
|
||||
color: _showFavoritesOnly ? AppColors.error : AppColors.textSecondary,
|
||||
_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 (_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);
|
||||
if (!_showFavoritesOnly)
|
||||
uiNotifier.state =
|
||||
uiState.copyWith(isCategoryView: true);
|
||||
}),
|
||||
),
|
||||
],
|
||||
@@ -280,7 +317,8 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> 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,
|
||||
);
|
||||
@@ -359,16 +397,16 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> 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<Channel> channels, PlaylistConfig playlist, int index) async {
|
||||
final engine = ref.read(mobileSettingsProvider).playerEngine;
|
||||
|
||||
if (engine == 'lite') {
|
||||
void _playChannel(BuildContext context, Channel channel,
|
||||
List<Channel> channels, PlaylistConfig playlist, int index) async {
|
||||
// ENFORCE LITE PLAYER FOR LIVE TV
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
@@ -382,21 +420,6 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> with Automati
|
||||
),
|
||||
),
|
||||
);
|
||||
} 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) {
|
||||
@@ -431,7 +454,8 @@ class _MobileChannelCardState extends ConsumerState<_MobileChannelCard> {
|
||||
|
||||
Future<void> _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,7 +469,8 @@ class _MobileChannelCardState extends ConsumerState<_MobileChannelCard> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final iconUrl = widget.channel.streamIcon.isNotEmpty && widget.channel.streamIcon.startsWith('http')
|
||||
final iconUrl = widget.channel.streamIcon.isNotEmpty &&
|
||||
widget.channel.streamIcon.startsWith('http')
|
||||
? widget.channel.streamIcon
|
||||
: null;
|
||||
|
||||
@@ -455,7 +480,9 @@ class _MobileChannelCardState extends ConsumerState<_MobileChannelCard> {
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -181,7 +181,7 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab>
|
||||
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<MobileMoviesTab>
|
||||
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),
|
||||
|
||||
@@ -51,7 +51,11 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
}
|
||||
|
||||
List<String> _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
|
||||
@@ -124,7 +128,9 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
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),
|
||||
@@ -132,23 +138,14 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
// === 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(
|
||||
@@ -160,20 +157,28 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
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]);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -188,7 +193,9 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
icon: Icons.live_tv,
|
||||
controller: _liveTvController,
|
||||
onChanged: (val) {
|
||||
ref.read(mobileSettingsProvider.notifier).setLiveTvKeywords(_parseKeywords(val));
|
||||
ref
|
||||
.read(mobileSettingsProvider.notifier)
|
||||
.setLiveTvKeywords(_parseKeywords(val));
|
||||
},
|
||||
),
|
||||
|
||||
@@ -198,7 +205,9 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
icon: Icons.movie,
|
||||
controller: _moviesController,
|
||||
onChanged: (val) {
|
||||
ref.read(mobileSettingsProvider.notifier).setMoviesKeywords(_parseKeywords(val));
|
||||
ref
|
||||
.read(mobileSettingsProvider.notifier)
|
||||
.setMoviesKeywords(_parseKeywords(val));
|
||||
},
|
||||
),
|
||||
|
||||
@@ -208,7 +217,9 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
icon: Icons.tv,
|
||||
controller: _seriesController,
|
||||
onChanged: (val) {
|
||||
ref.read(mobileSettingsProvider.notifier).setSeriesKeywords(_parseKeywords(val));
|
||||
ref
|
||||
.read(mobileSettingsProvider.notifier)
|
||||
.setSeriesKeywords(_parseKeywords(val));
|
||||
},
|
||||
),
|
||||
|
||||
@@ -239,7 +250,8 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const MobilePlaylistSelectionScreen(manageMode: true),
|
||||
builder: (_) =>
|
||||
const MobilePlaylistSelectionScreen(manageMode: true),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -255,19 +267,27 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,7 +382,8 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
),
|
||||
),
|
||||
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),
|
||||
@@ -442,14 +463,16 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showKeyboardDialog(String label, TextEditingController controller, ValueChanged<String> onChanged) {
|
||||
void _showKeyboardDialog(String label, TextEditingController controller,
|
||||
ValueChanged<String> 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,
|
||||
|
||||
Reference in new issue
Block a user