mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-22 22:58:14 +02:00
fix: restore EPG display in live TV player and channel cards
- Fix EPG not showing in NativePlayerScreen by reusing the shared mobileXtreamServiceProvider (cache already populated by channel cards) instead of creating a fresh service instance per player session - Add periodic EPG refresh timer (every 5 min) matching LitePlayerScreen - Fix empty-title fallback in _buildEPGBox - Force dark theme, invert default live TV player (NativePlayer default), remove next-program display from OSD - Fix back button exit dialog firing from wrong tab (add tab index guards) - Halve card grid count for Movies/Series (6 categories, 8 items) - Revamp Movies/Series search bars to match Live TV aesthetic - Fix first channel row clipping when entering a category (scroll-to-top) - Fix playlist edit dialog TV remote navigation - Fix empty playlist screen button readability - Remove theme switcher from settings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
f07a6e91f9
commit
6bb0d73f38
8 files changed
+564
-350
No files matched your search
@@ -27,6 +27,7 @@ class MobileLiveTVTab extends ConsumerStatefulWidget {
|
||||
|
||||
class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final FocusNode _searchFocusNode = FocusNode();
|
||||
String _searchQuery = '';
|
||||
@@ -60,6 +61,7 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
_searchController.dispose();
|
||||
_searchFocusNode.dispose();
|
||||
_searchTimer?.cancel();
|
||||
@@ -80,13 +82,24 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
final uiState = ref.watch(mobileLiveTvUiStateProvider);
|
||||
final uiNotifier = ref.read(mobileLiveTvUiStateProvider.notifier);
|
||||
|
||||
// Scroll to top when entering a category so first row is fully visible
|
||||
ref.listen(mobileLiveTvUiStateProvider, (previous, next) {
|
||||
if (previous?.isCategoryView == true && !next.isCategoryView) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_scrollController.hasClients) {
|
||||
_scrollController.jumpTo(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Determine if we should show the category grid
|
||||
final bool showGrid = uiState.isCategoryView &&
|
||||
_searchQuery.isEmpty &&
|
||||
!_showFavoritesOnly;
|
||||
|
||||
return Container(
|
||||
color: const Color(0xFF000000), // Pure black background
|
||||
color: const Color(0xFF000000),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: PopScope(
|
||||
@@ -95,6 +108,9 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
onPopInvokedWithResult: (didPop, _) async {
|
||||
if (didPop) return;
|
||||
|
||||
// Only handle back if this tab is currently active (tab index 0)
|
||||
if (ref.read(mobileDashboardIndexProvider) != 0) return;
|
||||
|
||||
// If just returned from player, reset the flag and do nothing (stay on channel list)
|
||||
if (_justReturnedFromPlayer) {
|
||||
_justReturnedFromPlayer = false;
|
||||
@@ -194,6 +210,7 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
return SafeArea(
|
||||
bottom: false,
|
||||
child: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
// Floating Header (Search + Navigation)
|
||||
SliverAppBar(
|
||||
@@ -447,14 +464,13 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NativePlayerScreen(
|
||||
builder: (context) => LitePlayerScreen(
|
||||
streamId: channel.streamId,
|
||||
title: channel.name,
|
||||
playlist: playlist,
|
||||
streamType: StreamType.live,
|
||||
channels: channels, // Pass full channel list for zapping
|
||||
channels: channels,
|
||||
initialIndex: index,
|
||||
forceDeinterlace: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -462,13 +478,14 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => LitePlayerScreen(
|
||||
builder: (context) => NativePlayerScreen(
|
||||
streamId: channel.streamId,
|
||||
title: channel.name,
|
||||
playlist: playlist,
|
||||
streamType: StreamType.live,
|
||||
channels: channels, // Pass full channel list for zapping
|
||||
channels: channels,
|
||||
initialIndex: index,
|
||||
forceDeinterlace: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -264,6 +264,9 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab>
|
||||
onPopInvokedWithResult: (didPop, _) async {
|
||||
if (didPop) return;
|
||||
|
||||
// Only handle back if this tab is currently active (tab index 1)
|
||||
if (ref.read(mobileDashboardIndexProvider) != 1) return;
|
||||
|
||||
// Handle Search Exit
|
||||
if (_searchQuery.isNotEmpty || _isSearching) {
|
||||
_onSearchChanged(''); // clear search
|
||||
@@ -327,94 +330,101 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab>
|
||||
// Pinned Header
|
||||
SliverAppBar(
|
||||
pinned: true,
|
||||
floating: true,
|
||||
backgroundColor: AppDecorations.background(context).gradient is LinearGradient
|
||||
? (AppDecorations.background(context).gradient as LinearGradient).colors.first
|
||||
: Theme.of(context).scaffoldBackgroundColor,
|
||||
elevation: 4,
|
||||
floating: false,
|
||||
snap: false,
|
||||
backgroundColor: const Color(0xFF000000),
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
automaticallyImplyLeading: false,
|
||||
expandedHeight: showingMovies ? 110 : 110,
|
||||
collapsedHeight: showingMovies ? 110 : 110,
|
||||
toolbarHeight: showingMovies ? 120.0 : 110.0,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
background: Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (showingMovies)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
// Search Bar
|
||||
TVFocusable(
|
||||
scale: 1.0,
|
||||
focusColor: Colors.white,
|
||||
onPressed: () {
|
||||
setState(() => _isSearchEditing = true);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_searchFocusNode.requestFocus();
|
||||
SystemChannels.textInput.invokeMethod('TextInput.show');
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
height: 48,
|
||||
decoration: AppDecorations.searchBar(context),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||
onPressed: _onBack,
|
||||
),
|
||||
Icon(Icons.search, color: AppDecorations.textSecondary(context)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_selectedCategoryName ?? 'Films',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold),
|
||||
child: ExcludeFocus(
|
||||
excluding: !_isSearchEditing,
|
||||
child: TextField(
|
||||
cursorColor: Colors.white,
|
||||
controller: _searchController,
|
||||
focusNode: _searchFocusNode,
|
||||
readOnly: !_isSearchEditing,
|
||||
style: TextStyle(color: AppDecorations.textPrimary(context)),
|
||||
decoration: InputDecoration(
|
||||
hintText: _selectedCategoryId != null ? 'Rechercher...' : 'Rechercher un film...',
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
onChanged: _onSearchChanged,
|
||||
onSubmitted: (_) => setState(() => _isSearchEditing = false),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_isSearching) const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
if (_searchQuery.isNotEmpty)
|
||||
GestureDetector(
|
||||
onTap: () { _searchController.clear(); _onSearchChanged(''); },
|
||||
child: Icon(Icons.close, color: AppDecorations.textSecondary(context)),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(height: 10),
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: TVFocusable(
|
||||
scale: 1.0,
|
||||
focusColor: Colors.white,
|
||||
onPressed: () {
|
||||
setState(() => _isSearchEditing = true);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_searchFocusNode.requestFocus();
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
height: 40,
|
||||
decoration: AppDecorations.searchBar(context),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.search, size: 20, color: AppDecorations.textSecondary(context)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ExcludeFocus(
|
||||
excluding: !_isSearchEditing,
|
||||
child: TextField(
|
||||
cursorColor: AppDecorations.textPrimary(context),
|
||||
controller: _searchController,
|
||||
focusNode: _searchFocusNode,
|
||||
readOnly: !_isSearchEditing,
|
||||
style: TextStyle(fontSize: 14, color: AppDecorations.textPrimary(context)),
|
||||
decoration: InputDecoration(
|
||||
hintText: _selectedCategoryId != null ? 'Rechercher...' : 'Rechercher un film...',
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: const 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: Icon(Icons.close, size: 16, color: AppDecorations.textSecondary(context)),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Navigation Row
|
||||
Row(
|
||||
children: [
|
||||
if (showingMovies) ...[
|
||||
IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: AppDecorations.textPrimary(context)),
|
||||
onPressed: _onBack,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
showingMovies
|
||||
? (_selectedCategoryName ?? 'Films')
|
||||
: showingSearch
|
||||
? 'Résultats de recherche'
|
||||
: 'Catégories',
|
||||
style: TextStyle(
|
||||
color: AppDecorations.textPrimary(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -427,7 +437,7 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab>
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 80),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: showingCategories ? 3 : 4,
|
||||
crossAxisCount: showingCategories ? 6 : 8,
|
||||
crossAxisSpacing: 10,
|
||||
mainAxisSpacing: 10,
|
||||
childAspectRatio: showingCategories ? 2.0 : 0.67,
|
||||
|
||||
@@ -251,6 +251,9 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab>
|
||||
onPopInvokedWithResult: (didPop, _) async {
|
||||
if (didPop) return;
|
||||
|
||||
// Only handle back if this tab is currently active (tab index 2)
|
||||
if (ref.read(mobileDashboardIndexProvider) != 2) return;
|
||||
|
||||
if (_searchQuery.isNotEmpty || _isSearching) {
|
||||
_onSearchChanged('');
|
||||
_searchController.clear();
|
||||
@@ -297,94 +300,101 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab>
|
||||
// Pinned Header
|
||||
SliverAppBar(
|
||||
pinned: true,
|
||||
floating: true,
|
||||
backgroundColor: AppDecorations.background(context).gradient is LinearGradient
|
||||
? (AppDecorations.background(context).gradient as LinearGradient).colors.first
|
||||
: Theme.of(context).scaffoldBackgroundColor,
|
||||
elevation: 4,
|
||||
floating: false,
|
||||
snap: false,
|
||||
backgroundColor: const Color(0xFF000000),
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
automaticallyImplyLeading: false,
|
||||
expandedHeight: showingSeries ? 110 : 110,
|
||||
collapsedHeight: showingSeries ? 110 : 110,
|
||||
toolbarHeight: showingSeries ? 120.0 : 110.0,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
background: Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (showingSeries)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
// Search Bar
|
||||
TVFocusable(
|
||||
scale: 1.0,
|
||||
focusColor: Colors.white,
|
||||
onPressed: () {
|
||||
setState(() => _isSearchEditing = true);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_searchFocusNode.requestFocus();
|
||||
SystemChannels.textInput.invokeMethod('TextInput.show');
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
height: 48,
|
||||
decoration: AppDecorations.searchBar(context),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||
onPressed: _onBack,
|
||||
),
|
||||
Icon(Icons.search, color: AppDecorations.textSecondary(context)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_selectedCategoryName ?? 'Séries',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold),
|
||||
child: ExcludeFocus(
|
||||
excluding: !_isSearchEditing,
|
||||
child: TextField(
|
||||
cursorColor: Colors.white,
|
||||
controller: _searchController,
|
||||
focusNode: _searchFocusNode,
|
||||
readOnly: !_isSearchEditing,
|
||||
style: TextStyle(color: AppDecorations.textPrimary(context)),
|
||||
decoration: InputDecoration(
|
||||
hintText: _selectedCategoryId != null ? 'Rechercher...' : 'Rechercher une série...',
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
onChanged: _onSearchChanged,
|
||||
onSubmitted: (_) => setState(() => _isSearchEditing = false),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_isSearching) const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
if (_searchQuery.isNotEmpty)
|
||||
GestureDetector(
|
||||
onTap: () { _searchController.clear(); _onSearchChanged(''); },
|
||||
child: Icon(Icons.close, color: AppDecorations.textSecondary(context)),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(height: 10),
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: TVFocusable(
|
||||
scale: 1.0,
|
||||
focusColor: Colors.white,
|
||||
onPressed: () {
|
||||
setState(() => _isSearchEditing = true);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_searchFocusNode.requestFocus();
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
height: 40,
|
||||
decoration: AppDecorations.searchBar(context),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.search, size: 20, color: AppDecorations.textSecondary(context)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ExcludeFocus(
|
||||
excluding: !_isSearchEditing,
|
||||
child: TextField(
|
||||
cursorColor: AppDecorations.textPrimary(context),
|
||||
controller: _searchController,
|
||||
focusNode: _searchFocusNode,
|
||||
readOnly: !_isSearchEditing,
|
||||
style: TextStyle(fontSize: 14, color: AppDecorations.textPrimary(context)),
|
||||
decoration: InputDecoration(
|
||||
hintText: _selectedCategoryId != null ? 'Rechercher...' : 'Rechercher une série...',
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: const 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: Icon(Icons.close, size: 16, color: AppDecorations.textSecondary(context)),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Navigation Row
|
||||
Row(
|
||||
children: [
|
||||
if (showingSeries) ...[
|
||||
IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: AppDecorations.textPrimary(context)),
|
||||
onPressed: _onBack,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
showingSeries
|
||||
? (_selectedCategoryName ?? 'Séries')
|
||||
: showingSearch
|
||||
? 'Résultats de recherche'
|
||||
: 'Catégories',
|
||||
style: TextStyle(
|
||||
color: AppDecorations.textPrimary(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -396,7 +406,7 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab>
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 80),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: showingCategories ? 3 : 4,
|
||||
crossAxisCount: showingCategories ? 6 : 8,
|
||||
crossAxisSpacing: 10,
|
||||
mainAxisSpacing: 10,
|
||||
childAspectRatio: showingCategories ? 2.0 : 0.67,
|
||||
|
||||
@@ -117,10 +117,6 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
// === APPARENCE ===
|
||||
_buildSectionHeader('Apparence'),
|
||||
|
||||
_buildThemeSwitcher(),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
_buildSettingItem(
|
||||
icon: Icons.access_time,
|
||||
title: 'Afficher l\'heure',
|
||||
@@ -360,8 +356,9 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
final selected = themeState.appThemeMode == mode;
|
||||
final accentColor = Theme.of(context).colorScheme.primary;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => notifier.setThemeMode(mode),
|
||||
child: TVFocusable(
|
||||
onPressed: () => notifier.setThemeMode(mode),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||
|
||||
Reference in new issue
Block a user