feat: Add initial mobile dashboard screen with Xtream data and UI state providers.

This commit is contained in:
Michael committed 2025-12-20 16:05:01 +01:00
1 parent 3145af9e8d
commit b249074db1
30 files changed
+1217 -582

No files matched your search

+28 -17
View File
@@ -33,7 +33,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen>
Icons.live_tv,
Icons.movie,
Icons.tv,
Icons.settings
Icons.settings,
];
@override
@@ -85,7 +85,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen>
child: Container(
height: 50,
constraints: const BoxConstraints(
maxWidth: 600), // Prevent taking too much space
maxWidth: 600,
), // Prevent taking too much space
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(30),
@@ -105,7 +106,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen>
unselectedLabelColor:
Colors.white70, // Fixed: Improved readability
labelStyle: GoogleFonts.inter(
fontWeight: FontWeight.w700, fontSize: 13),
fontWeight: FontWeight.w700,
fontSize: 13,
),
overlayColor: WidgetStateProperty.all(Colors.transparent),
tabs: List.generate(_tabs.length, (index) {
return Tab(
@@ -182,16 +185,21 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen>
children: [
// Padding top to account for the transparent AppBar
Padding(
padding: const EdgeInsets.only(top: 80),
child: LiveTVTab(playlist: widget.playlist)),
padding: const EdgeInsets.only(top: 80),
child: LiveTVTab(playlist: widget.playlist),
),
Padding(
padding: const EdgeInsets.only(top: 80),
child: MoviesTab(playlist: widget.playlist)),
padding: const EdgeInsets.only(top: 80),
child: MoviesTab(playlist: widget.playlist),
),
Padding(
padding: const EdgeInsets.only(top: 80),
child: SeriesTab(playlist: widget.playlist)),
padding: const EdgeInsets.only(top: 80),
child: SeriesTab(playlist: widget.playlist),
),
const Padding(
padding: EdgeInsets.only(top: 80), child: SettingsTab()),
padding: EdgeInsets.only(top: 80),
child: SettingsTab(),
),
],
),
],
@@ -206,10 +214,12 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen>
width: 36,
height: 36,
decoration: BoxDecoration(
gradient: const LinearGradient(colors: [
Color(0xFF43cea2),
Color(0xFF185a9d)
]), // Green to Blue
gradient: const LinearGradient(
colors: [
Color(0xFF43cea2),
Color(0xFF185a9d),
],
), // Green to Blue
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.play_arrow_rounded, color: Colors.white),
@@ -222,9 +232,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen>
Text(
'Xtrem',
style: GoogleFonts.inter(
fontWeight: FontWeight.w800,
fontSize: 16,
color: Colors.white),
fontWeight: FontWeight.w800,
fontSize: 16,
color: Colors.white,
),
),
Text(
'Flow',
+151 -79
View File
@@ -2,9 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:universal_html/html.dart' as html;
import '../../../core/shims/ui_web.dart' as ui_web;
import 'dart:convert';
import 'dart:async';
import 'package:http/http.dart' as http;
import '../providers/xtream_provider.dart';
import '../providers/playback_positions_provider.dart';
import '../providers/settings_provider.dart';
@@ -187,7 +185,8 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
if (usingTranscoding) {
debugPrint(
'Seeking in transcoded VOD - reloading player at ${value.round()}s');
'Seeking in transcoded VOD - reloading player at ${value.round()}s',
);
ref
.read(playbackPositionsProvider.notifier)
.savePosition(_contentId, value, _totalDuration);
@@ -365,9 +364,13 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
final encodedUrl = Uri.encodeComponent(
widget.streamType == StreamType.vod
? xtreamService.getVodStreamUrl(
currentStreamId, widget.containerExtension)
currentStreamId,
widget.containerExtension,
)
: xtreamService.getSeriesStreamUrl(
currentStreamId, widget.containerExtension),
currentStreamId,
widget.containerExtension,
),
);
String qualityParam = 'medium';
@@ -419,7 +422,8 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
startTime = positions.getPosition(_contentId);
if (startTime > 0) {
debugPrint(
'PlayerScreen: Resuming from ${startTime.toStringAsFixed(0)}s');
'PlayerScreen: Resuming from ${startTime.toStringAsFixed(0)}s',
);
}
}
@@ -556,20 +560,25 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
),
),
IconButton(
icon: const Icon(Icons.close,
color: Colors.white54),
icon: const Icon(
Icons.close,
color: Colors.white54,
),
onPressed: () => Navigator.pop(context),
tooltip: 'Fermer',
),
],
),
const SizedBox(height: 24),
const Text('FORMAT D\'IMAGE',
style: TextStyle(
color: AppColors.textSecondary,
fontSize: 12,
fontWeight: FontWeight.bold,
letterSpacing: 1)),
const Text(
'FORMAT D\'IMAGE',
style: TextStyle(
color: AppColors.textSecondary,
fontSize: 12,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(4),
@@ -579,23 +588,38 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
),
child: Row(
children: [
_buildRatioOption('Original', 'contain',
Icons.crop_original, setDialogState),
_buildRatioOption('Remplir', 'cover',
Icons.crop_free, setDialogState),
_buildRatioOption('Étirer', 'fill',
Icons.aspect_ratio, setDialogState),
_buildRatioOption(
'Original',
'contain',
Icons.crop_original,
setDialogState,
),
_buildRatioOption(
'Remplir',
'cover',
Icons.crop_free,
setDialogState,
),
_buildRatioOption(
'Étirer',
'fill',
Icons.aspect_ratio,
setDialogState,
),
],
),
),
if (_audioTracks.isNotEmpty) ...[
const SizedBox(height: 32),
const Text('AUDIO',
style: TextStyle(
color: AppColors.textSecondary,
fontSize: 12,
fontWeight: FontWeight.bold,
letterSpacing: 1)),
const Text(
'AUDIO',
style: TextStyle(
color: AppColors.textSecondary,
fontSize: 12,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
const SizedBox(height: 12),
Container(
constraints:
@@ -604,7 +628,8 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
color: Colors.black26,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.white.withOpacity(0.05)),
color: Colors.white.withOpacity(0.05),
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
@@ -615,24 +640,30 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
color: Colors.transparent,
child: ListTile(
title: Text(
track['label'] ??
'Piste ${track['id']}',
style: const TextStyle(
color: Colors.white,
fontSize: 14)),
subtitle: Text(track['lang'] ?? '',
style: const TextStyle(
color: Colors.white54,
fontSize: 12)),
track['label'] ??
'Piste ${track['id']}',
style: const TextStyle(
color: Colors.white,
fontSize: 14,
),
),
subtitle: Text(
track['lang'] ?? '',
style: const TextStyle(
color: Colors.white54,
fontSize: 12,
),
),
onTap: () {
_setAudioTrack(track['id']);
setDialogState(() {});
},
dense: true,
leading: const Icon(
Icons.audiotrack,
size: 18,
color: Colors.white54),
Icons.audiotrack,
size: 18,
color: Colors.white54,
),
hoverColor:
Colors.white.withOpacity(0.1),
),
@@ -644,12 +675,15 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
),
],
const SizedBox(height: 32),
const Text('AFFICHAGE',
style: TextStyle(
color: AppColors.textSecondary,
fontSize: 12,
fontWeight: FontWeight.bold,
letterSpacing: 1)),
const Text(
'AFFICHAGE',
style: TextStyle(
color: AppColors.textSecondary,
fontSize: 12,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
const SizedBox(height: 12),
SwitchListTile(
value: showClock,
@@ -659,11 +693,17 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
});
settingsNotifier.setShowClock(val);
},
title: const Text('Afficher l\'heure',
style: TextStyle(
color: Colors.white, fontSize: 14)),
secondary: const Icon(Icons.access_time,
color: Colors.white54),
title: const Text(
'Afficher l\'heure',
style: TextStyle(
color: Colors.white,
fontSize: 14,
),
),
secondary: const Icon(
Icons.access_time,
color: Colors.white54,
),
activeThumbColor: AppColors.primary,
contentPadding: EdgeInsets.zero,
),
@@ -682,7 +722,11 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
}
Widget _buildRatioOption(
String label, String value, IconData icon, StateSetter setDialogState) {
String label,
String value,
IconData icon,
StateSetter setDialogState,
) {
final isSelected = _aspectRatio == value;
return Expanded(
child: GestureDetector(
@@ -705,16 +749,20 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
boxShadow: isSelected
? [
const BoxShadow(
color: Colors.black12,
blurRadius: 4,
offset: Offset(0, 2))
color: Colors.black12,
blurRadius: 4,
offset: Offset(0, 2),
),
]
: null,
),
child: Column(
children: [
Icon(icon,
color: isSelected ? Colors.white : Colors.white54, size: 20),
Icon(
icon,
color: isSelected ? Colors.white : Colors.white54,
size: 20,
),
const SizedBox(height: 4),
Text(
label,
@@ -767,14 +815,21 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline,
size: 64, color: Colors.red),
const Icon(
Icons.error_outline,
size: 64,
color: Colors.red,
),
const SizedBox(height: 16),
const Text('Failed to load stream',
style: TextStyle(color: Colors.white, fontSize: 18)),
const Text(
'Failed to load stream',
style: TextStyle(color: Colors.white, fontSize: 18),
),
const SizedBox(height: 8),
Text(_errorMessage!,
style: const TextStyle(color: Colors.white70)),
Text(
_errorMessage!,
style: const TextStyle(color: Colors.white70),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
@@ -858,7 +913,8 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.symmetric(
vertical: 12),
vertical: 12,
),
child: Column(
children: [
PointerInterceptor(
@@ -887,10 +943,12 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
trackHeight: 4,
thumbShape:
const RoundSliderThumbShape(
enabledThumbRadius: 6),
enabledThumbRadius: 6,
),
overlayShape:
const RoundSliderOverlayShape(
overlayRadius: 12),
overlayRadius: 12,
),
activeTrackColor: Colors.white,
inactiveTrackColor:
Colors.white24,
@@ -961,8 +1019,9 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
Colors.black.withOpacity(0.5),
shape: BoxShape.circle,
border: Border.all(
color: Colors.white,
width: 2),
color: Colors.white,
width: 2,
),
),
child: Icon(
_isPlaying
@@ -1040,10 +1099,12 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
data: SliderThemeData(
thumbShape:
const RoundSliderThumbShape(
enabledThumbRadius: 8),
enabledThumbRadius: 8,
),
overlayShape:
const RoundSliderOverlayShape(
overlayRadius: 16),
overlayRadius: 16,
),
activeTrackColor: AppColors.primary,
inactiveTrackColor: Colors.white24,
thumbColor: AppColors.primary,
@@ -1060,31 +1121,40 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12),
horizontal: 12,
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
_formatDuration(Duration(
_formatDuration(
Duration(
seconds: _currentPosition
.isFinite
? _currentPosition
.toInt()
: 0)),
: 0,
),
),
style: const TextStyle(
color: Colors.white,
fontSize: 12),
color: Colors.white,
fontSize: 12,
),
),
Text(
_formatDuration(Duration(
_formatDuration(
Duration(
seconds: _totalDuration
.isFinite
? _totalDuration.toInt()
: 0)),
: 0,
),
),
style: const TextStyle(
color: Colors.white,
fontSize: 12),
color: Colors.white,
fontSize: 12,
),
),
],
),
@@ -1113,7 +1183,9 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
fontWeight: FontWeight.bold,
shadows: [
Shadow(
color: Colors.black, blurRadius: 8)
color: Colors.black,
blurRadius: 8,
),
],
),
),
@@ -27,7 +27,9 @@ class PlaylistSelectionScreen extends ConsumerWidget {
title: Text(
'Select Playlist',
style: GoogleFonts.roboto(
fontWeight: FontWeight.w600, color: Colors.white),
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
backgroundColor: Colors.transparent,
elevation: 0,
@@ -71,9 +73,13 @@ class PlaylistSelectionScreen extends ConsumerWidget {
children: [
Icon(Icons.error_outline, size: 64, color: Colors.red.shade400),
const SizedBox(height: 16),
Text('Error loading playlists',
style: GoogleFonts.roboto(
fontSize: 18, color: Colors.white70)),
Text(
'Error loading playlists',
style: GoogleFonts.roboto(
fontSize: 18,
color: Colors.white70,
),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () => ref.refresh(playlistsProvider),
@@ -118,7 +124,11 @@ class PlaylistSelectionScreen extends ConsumerWidget {
return GridView.builder(
padding: const EdgeInsets.fromLTRB(
24, 100, 24, 24), // Top padding for transparent AppBar
24,
100,
24,
24,
), // Top padding for transparent AppBar
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 24,
@@ -331,8 +331,10 @@ class XtreamService {
}
/// Get movies with pagination support
Future<List<xm.Movie>> getMoviesPaginated(
{int offset = 0, int limit = 100}) async {
Future<List<xm.Movie>> getMoviesPaginated({
int offset = 0,
int limit = 100,
}) async {
if (_currentPlaylist == null) throw Exception('No playlist configured');
try {
@@ -411,8 +413,10 @@ class XtreamService {
}
/// Get series with pagination support (returns flat list)
Future<List<xm.Series>> getSeriesPaginated(
{int offset = 0, int limit = 100}) async {
Future<List<xm.Series>> getSeriesPaginated({
int offset = 0,
int limit = 100,
}) async {
if (_currentPlaylist == null) throw Exception('No playlist configured');
try {
@@ -74,7 +74,7 @@ class XtreamServiceMobile {
print('XtreamServiceMobile: Will use IP $_resolvedIp for ${uri.host}');
} else {
print(
'XtreamServiceMobile: DNS resolution failed, will use hostname directly');
'XtreamServiceMobile: DNS resolution failed, will use hostname directly',);
}
}
}
@@ -162,8 +162,9 @@ class XtreamServiceMobile {
);
if (response.data is! Map<String, dynamic>) {
if (response.data is String)
if (response.data is String) {
throw Exception('Auth Error: ${response.data}');
}
throw Exception('Invalid auth response format');
}
return response.data as Map<String, dynamic>;
@@ -188,8 +189,9 @@ class XtreamServiceMobile {
);
if (response.data is! List) {
if (response.data is String)
if (response.data is String) {
throw Exception('Category Error: ${response.data}');
}
return {};
}
final List<dynamic> categories = response.data as List<dynamic>;
@@ -229,8 +231,9 @@ class XtreamServiceMobile {
);
if (response.data is! List) {
if (response.data is String)
if (response.data is String) {
throw Exception('Live TV Error: ${response.data}');
}
return {};
}
final List<dynamic> streams = response.data as List<dynamic>;
@@ -325,7 +328,7 @@ class XtreamServiceMobile {
/// Get movies with pagination support (uses in-memory cache for performance)
Future<List<xm.Movie>> getMoviesPaginated(
{int offset = 0, int limit = 100}) async {
{int offset = 0, int limit = 100,}) async {
if (_currentPlaylist == null) throw Exception('No playlist configured');
try {
@@ -345,8 +348,9 @@ class XtreamServiceMobile {
);
if (response.data is! List) {
if (response.data is String)
if (response.data is String) {
throw Exception('VOD Error: ${response.data}');
}
_cachedMoviesRaw = [];
} else {
_cachedMoviesRaw = response.data as List<dynamic>;
@@ -419,7 +423,7 @@ class XtreamServiceMobile {
/// Get series with pagination support (uses in-memory cache for performance)
Future<List<xm.Series>> getSeriesPaginated(
{int offset = 0, int limit = 100}) async {
{int offset = 0, int limit = 100,}) async {
if (_currentPlaylist == null) throw Exception('No playlist configured');
try {
@@ -439,8 +443,9 @@ class XtreamServiceMobile {
);
if (response.data is! List) {
if (response.data is String)
if (response.data is String) {
throw Exception('Series Error: ${response.data}');
}
_cachedSeriesRaw = [];
} else {
_cachedSeriesRaw = response.data as List<dynamic>;
@@ -580,7 +585,7 @@ class XtreamServiceMobile {
/// Get short EPG as ShortEPG object (for EPGWidget)
/// Caches results for 12 hours by default
Future<xm.ShortEPG> getShortEPG(String streamId,
{bool forceRefresh = false}) async {
{bool forceRefresh = false,}) async {
if (_currentPlaylist == null) throw Exception('No playlist configured');
try {
+25 -10
View File
@@ -116,8 +116,10 @@ class _LiveTVTabState extends ConsumerState<LiveTVTab>
_searchQuery.isEmpty &&
!_showFavoritesOnly) ...[
IconButton(
icon: const Icon(Icons.arrow_back,
color: AppColors.textPrimary),
icon: const Icon(
Icons.arrow_back,
color: AppColors.textPrimary,
),
onPressed: () {
setState(() {
_selectedCategory = null;
@@ -156,14 +158,19 @@ class _LiveTVTabState extends ConsumerState<LiveTVTab>
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
const Icon(Icons.search,
size: 20, color: AppColors.textSecondary),
const Icon(
Icons.search,
size: 20,
color: AppColors.textSecondary,
),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: _searchController,
style: const TextStyle(
fontSize: 14, color: Colors.white),
fontSize: 14,
color: Colors.white,
),
decoration: const InputDecoration(
hintText: 'Rechercher...',
hintStyle: TextStyle(color: Colors.white54),
@@ -176,8 +183,11 @@ class _LiveTVTabState extends ConsumerState<LiveTVTab>
if (_searchQuery.isNotEmpty)
GestureDetector(
onTap: () => _searchController.clear(),
child: const Icon(Icons.close,
size: 16, color: AppColors.textSecondary),
child: const Icon(
Icons.close,
size: 16,
color: AppColors.textSecondary,
),
),
],
),
@@ -250,7 +260,9 @@ class _LiveTVTabState extends ConsumerState<LiveTVTab>
}
Widget _buildCategoryGrid(
List<String> categories, Map<String, List<Channel>> groupedChannels) {
List<String> categories,
Map<String, List<Channel>> groupedChannels,
) {
final columns = ResponsiveLayout.value(
context,
mobile: 2,
@@ -307,8 +319,11 @@ class _LiveTVTabState extends ConsumerState<LiveTVTab>
color: AppColors.primary.withOpacity(0.1),
shape: BoxShape.circle,
),
child: const Icon(Icons.folder_open,
color: AppColors.primary, size: 32),
child: const Icon(
Icons.folder_open,
color: AppColors.primary,
size: 32,
),
),
const SizedBox(height: 12),
Text(
+17 -9
View File
@@ -1,7 +1,6 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../core/models/playlist_config.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_theme.dart';
@@ -238,14 +237,19 @@ class _MoviesTabState extends ConsumerState<MoviesTab> {
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
const Icon(Icons.search,
size: 20, color: AppColors.textSecondary),
const Icon(
Icons.search,
size: 20,
color: AppColors.textSecondary,
),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: _searchController,
style: const TextStyle(
fontSize: 14, color: Colors.white),
fontSize: 14,
color: Colors.white,
),
decoration: const InputDecoration(
hintText: 'Rechercher...',
hintStyle: TextStyle(color: Colors.white54),
@@ -260,17 +264,21 @@ class _MoviesTabState extends ConsumerState<MoviesTab> {
),
if (_isSearching)
const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(strokeWidth: 2)),
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),
child: const Icon(
Icons.close,
size: 16,
color: AppColors.textSecondary,
),
),
],
),
+20 -11
View File
@@ -1,7 +1,6 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../core/models/playlist_config.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_theme.dart';
@@ -230,14 +229,19 @@ class _SeriesTabState extends ConsumerState<SeriesTab> {
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
const Icon(Icons.search,
size: 20, color: AppColors.textSecondary),
const Icon(
Icons.search,
size: 20,
color: AppColors.textSecondary,
),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: _searchController,
style: const TextStyle(
fontSize: 14, color: Colors.white),
fontSize: 14,
color: Colors.white,
),
decoration: const InputDecoration(
hintText: 'Rechercher...',
hintStyle: TextStyle(color: Colors.white54),
@@ -252,17 +256,21 @@ class _SeriesTabState extends ConsumerState<SeriesTab> {
),
if (_isSearching)
const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(strokeWidth: 2)),
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),
child: const Icon(
Icons.close,
size: 16,
color: AppColors.textSecondary,
),
),
],
),
@@ -280,8 +288,9 @@ class _SeriesTabState extends ConsumerState<SeriesTab> {
child: HeroCarousel(
items: heroItems,
onTap: (item) {
final series = _series.firstWhere((s) =>
s.name == item.title); // Fallback by title if ID mismatch
final series = _series.firstWhere(
(s) => s.name == item.title,
); // Fallback by title if ID mismatch
_openSeries(series);
},
),
+43 -19
View File
@@ -96,8 +96,9 @@ class _SettingsTabState extends ConsumerState<SettingsTab>
const Tab(icon: Icon(Icons.palette), text: 'Apparence'),
if (currentUser?.isAdmin ?? false)
const Tab(
icon: Icon(Icons.admin_panel_settings),
text: 'Administration'),
icon: Icon(Icons.admin_panel_settings),
text: 'Administration',
),
],
),
),
@@ -212,7 +213,9 @@ class _SettingsTabState extends ConsumerState<SettingsTab>
title: Text(
'Mode actuel',
style: GoogleFonts.inter(
fontWeight: FontWeight.w500, color: Colors.white),
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
subtitle: Text(
isDark ? 'Thème sombre actif' : 'Thème clair actif',
@@ -305,7 +308,10 @@ class _SettingsTabState extends ConsumerState<SettingsTab>
}
Widget _buildFiltersTab(
BuildContext context, dynamic currentUser, IptvSettings settings) {
BuildContext context,
dynamic currentUser,
IptvSettings settings,
) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
@@ -326,13 +332,19 @@ class _SettingsTabState extends ConsumerState<SettingsTab>
subtitle: Text(
currentUser?.username ?? 'Unknown',
style: GoogleFonts.roboto(
fontWeight: FontWeight.w600, color: Colors.white),
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
trailing: currentUser?.isAdmin ?? false
? Chip(
label: Text('Admin',
style: GoogleFonts.roboto(
fontSize: 11, color: Colors.black)),
label: Text(
'Admin',
style: GoogleFonts.roboto(
fontSize: 11,
color: Colors.black,
),
),
backgroundColor: Colors.white,
)
: null,
@@ -416,8 +428,10 @@ class _SettingsTabState extends ConsumerState<SettingsTab>
),
child: ListTile(
leading: const Icon(Icons.playlist_play, color: Colors.white),
title: Text('Changer de Playlist',
style: GoogleFonts.roboto(color: Colors.white)),
title: Text(
'Changer de Playlist',
style: GoogleFonts.roboto(color: Colors.white),
),
trailing: const Icon(Icons.chevron_right, color: Colors.white54),
onTap: () => context.go('/playlists'),
),
@@ -433,8 +447,10 @@ class _SettingsTabState extends ConsumerState<SettingsTab>
),
child: ListTile(
leading: const Icon(Icons.info_outline, color: Colors.white),
title: Text('À propos',
style: GoogleFonts.roboto(color: Colors.white)),
title: Text(
'À propos',
style: GoogleFonts.roboto(color: Colors.white),
),
subtitle: Text(
'XtremFlow IPTV v1.0.0',
style: GoogleFonts.roboto(fontSize: 12, color: Colors.white54),
@@ -452,8 +468,10 @@ class _SettingsTabState extends ConsumerState<SettingsTab>
),
child: ListTile(
leading: const Icon(Icons.cleaning_services, color: Colors.orange),
title: Text('Maintenance',
style: GoogleFonts.roboto(color: Colors.white)),
title: Text(
'Maintenance',
style: GoogleFonts.roboto(color: Colors.white),
),
subtitle: Text(
'En cas de problèmes d\'affichage ou de mise à jour',
style: GoogleFonts.roboto(fontSize: 12, color: Colors.white54),
@@ -465,8 +483,10 @@ class _SettingsTabState extends ConsumerState<SettingsTab>
context: context,
builder: (context) => AlertDialog(
backgroundColor: const Color(0xFF1E1E1E),
title: const Text('Vider le cache ?',
style: TextStyle(color: Colors.white)),
title: const Text(
'Vider le cache ?',
style: TextStyle(color: Colors.white),
),
content: const Text(
'Cette action va recharger l\'application et effacer les données temporaires.\n'
'Vos réglages et favoris sont sauvegardés sur le serveur.',
@@ -479,7 +499,8 @@ class _SettingsTabState extends ConsumerState<SettingsTab>
),
FilledButton(
style: FilledButton.styleFrom(
backgroundColor: Colors.orange),
backgroundColor: Colors.orange,
),
onPressed: () {
Navigator.pop(context);
@@ -580,8 +601,11 @@ class _SettingsTabState extends ConsumerState<SettingsTab>
),
suffixIcon: controller.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear,
size: 16, color: Colors.white54),
icon: const Icon(
Icons.clear,
size: 16,
color: Colors.white54,
),
onPressed: onClear,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
@@ -248,10 +248,14 @@ class StreamingSettingsTab extends ConsumerWidget {
required String Function(T) labelBuilder,
}) {
return ListTile(
title: Text(title,
style: GoogleFonts.roboto(fontSize: 14, color: Colors.white)),
subtitle: Text(subtitle,
style: GoogleFonts.roboto(fontSize: 12, color: Colors.white54)),
title: Text(
title,
style: GoogleFonts.roboto(fontSize: 14, color: Colors.white),
),
subtitle: Text(
subtitle,
style: GoogleFonts.roboto(fontSize: 12, color: Colors.white54),
),
trailing: DropdownButton<T>(
value: value,
underline: const SizedBox(),