From e8bd03e010fed1948aa4e6a630eab1c3ce7edb65 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Sun, 7 Dec 2025 00:52:19 +0100 Subject: [PATCH] feat: add IPTV playlist selection screen, settings tabs, and admin panel --- lib/features/admin/screens/admin_panel.dart | 136 ++++---- .../screens/playlist_selection_screen.dart | 241 +++++++------ lib/features/iptv/widgets/settings_tab.dart | 329 +++++++++++------- .../iptv/widgets/streaming_settings_tab.dart | 40 ++- 4 files changed, 437 insertions(+), 309 deletions(-) diff --git a/lib/features/admin/screens/admin_panel.dart b/lib/features/admin/screens/admin_panel.dart index 8cf5788..20e7b75 100644 --- a/lib/features/admin/screens/admin_panel.dart +++ b/lib/features/admin/screens/admin_panel.dart @@ -43,60 +43,76 @@ class _AdminPanelState extends ConsumerState @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: AppColors.background, - body: SafeArea( - child: Column( - children: [ - // Custom Glass Header - Container( - padding: const EdgeInsets.all(24), - child: Row( - children: [ - GlassCard( - borderRadius: 12, - padding: const EdgeInsets.all(8), - showBorder: false, - onTap: () => context.go('/playlists'), - child: const Icon(Icons.arrow_back, color: AppColors.textSecondary), - ), - const SizedBox(width: 16), - Text( - 'Administration', - style: Theme.of(context).textTheme.headlineMedium, - ), - const Spacer(), - // Custom Tab Bar Indicator - Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: AppColors.surface, - borderRadius: BorderRadius.circular(50), + extendBodyBehindAppBar: true, + body: Container( + decoration: const BoxDecoration( + gradient: RadialGradient( + center: Alignment.center, + radius: 1.5, + colors: [ + Color(0xFF2C2C2E), // Dark Grey (Apple TV Surface) + Color(0xFF000000), // Pure Black + ], + stops: [0.0, 1.0], + ), + ), + child: SafeArea( + child: Column( + children: [ + // Custom Glass Header + Container( + padding: const EdgeInsets.all(24), + child: Row( + children: [ + GlassCard( + borderRadius: 12, + padding: const EdgeInsets.all(8), + showBorder: false, + onTap: () => context.go('/playlists'), + child: const Icon(Icons.arrow_back, color: Colors.white70), ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - _buildTabButton(0, 'Playlists', Icons.playlist_play), - const SizedBox(width: 4), - _buildTabButton(1, 'Users', Icons.people), - ], + const SizedBox(width: 16), + Text( + 'Administration', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + ), ), - ), - ], + const Spacer(), + // Custom Tab Bar Indicator + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(50), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildTabButton(0, 'Playlists', Icons.playlist_play), + const SizedBox(width: 4), + _buildTabButton(1, 'Users', Icons.people), + ], + ), + ), + ], + ), ), - ), - - // Content - Expanded( - child: TabBarView( - controller: _tabController, - physics: const NeverScrollableScrollPhysics(), // Custom tabs handle ref - children: const [ - _PlaylistsTab(), - _UsersTab(), - ], + + // Content + Expanded( + child: TabBarView( + controller: _tabController, + physics: const NeverScrollableScrollPhysics(), // Custom tabs handle ref + children: const [ + _PlaylistsTab(), + _UsersTab(), + ], + ), ), - ), - ], + ], + ), ), ), ); @@ -121,7 +137,7 @@ class _AdminPanelState extends ConsumerState Icon( icon, size: 18, - color: isSelected ? Colors.black : AppColors.textSecondary, + color: isSelected ? Colors.black : Colors.white70, ), const SizedBox(width: 8), Text( @@ -129,7 +145,7 @@ class _AdminPanelState extends ConsumerState style: GoogleFonts.inter( fontWeight: FontWeight.w600, fontSize: 14, - color: isSelected ? Colors.black : AppColors.textSecondary, + color: isSelected ? Colors.black : Colors.white70, ), ), ], @@ -156,7 +172,7 @@ class _PlaylistsTab extends ConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('${playlistsAsync.asData?.value.length ?? 0} Playlists', style: Theme.of(context).textTheme.bodyLarge), + Text('${playlistsAsync.asData?.value.length ?? 0} Playlists', style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.white70)), GradientButton( label: 'Add Playlist', icon: Icons.add, @@ -212,9 +228,9 @@ class _PlaylistsTab extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(playlist.name, style: Theme.of(context).textTheme.titleMedium), + Text(playlist.name, style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.white)), const SizedBox(height: 4), - Text(playlist.dns, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: AppColors.textTertiary)), + Text(playlist.dns, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.white60)), ], ), ), @@ -222,7 +238,7 @@ class _PlaylistsTab extends ConsumerWidget { children: [ IconButton( icon: const Icon(Icons.edit_outlined, size: 20), - color: AppColors.textSecondary, + color: Colors.white70, onPressed: () => _showPlaylistDialog(context, ref, playlist), tooltip: 'Edit', ), @@ -337,7 +353,7 @@ class _UsersTab extends ConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('${usersAsync.asData?.value.length ?? 0} Users', style: Theme.of(context).textTheme.bodyLarge), + Text('${usersAsync.asData?.value.length ?? 0} Users', style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.white70)), GradientButton( label: 'Create User', icon: Icons.person_add, @@ -394,7 +410,7 @@ class _UsersTab extends ConsumerWidget { children: [ Row( children: [ - Text(user.username, style: Theme.of(context).textTheme.titleMedium), + Text(user.username, style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.white)), if (user.isAdmin) ...[ const SizedBox(width: 8), Container( @@ -409,12 +425,12 @@ class _UsersTab extends ConsumerWidget { ], ), const SizedBox(height: 4), - Text('ID: ${user.id}', style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: AppColors.textTertiary)), + Text('ID: ${user.id}', style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.white60)), ], ), ), PopupMenuButton( - icon: const Icon(Icons.more_vert, color: AppColors.textSecondary), + icon: const Icon(Icons.more_vert, color: Colors.white70), color: AppColors.surface, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), onSelected: (value) { diff --git a/lib/features/iptv/screens/playlist_selection_screen.dart b/lib/features/iptv/screens/playlist_selection_screen.dart index 49e1c69..e536c54 100644 --- a/lib/features/iptv/screens/playlist_selection_screen.dart +++ b/lib/features/iptv/screens/playlist_selection_screen.dart @@ -15,26 +15,30 @@ final playlistsProvider = FutureProvider>((ref) async { class PlaylistSelectionScreen extends ConsumerWidget { const PlaylistSelectionScreen({super.key}); + @override @override Widget build(BuildContext context, WidgetRef ref) { final currentUser = ref.watch(authProvider).currentUser; final playlistsAsync = ref.watch(playlistsProvider); return Scaffold( + extendBodyBehindAppBar: true, appBar: AppBar( title: Text( 'Select Playlist', - style: GoogleFonts.roboto(fontWeight: FontWeight.w600), + style: GoogleFonts.roboto(fontWeight: FontWeight.w600, color: Colors.white), ), + backgroundColor: Colors.transparent, + elevation: 0, actions: [ if (currentUser?.isAdmin ?? false) IconButton( - icon: const Icon(Icons.admin_panel_settings), + icon: const Icon(Icons.admin_panel_settings, color: Colors.white70), onPressed: () => context.go('/admin'), tooltip: 'Admin Panel', ), IconButton( - icon: const Icon(Icons.logout), + icon: const Icon(Icons.logout, color: Colors.white70), onPressed: () async { await ref.read(authProvider.notifier).logout(); if (context.mounted) { @@ -45,83 +49,96 @@ class PlaylistSelectionScreen extends ConsumerWidget { ), ], ), - body: playlistsAsync.when( - loading: () => const Center(child: CircularProgressIndicator()), - error: (error, stack) => Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.error_outline, size: 64, color: Colors.red.shade400), - const SizedBox(height: 16), - Text('Error loading playlists', style: GoogleFonts.roboto(fontSize: 18)), - const SizedBox(height: 8), - ElevatedButton( - onPressed: () => ref.refresh(playlistsProvider), - child: const Text('Retry'), - ), + body: Container( + decoration: const BoxDecoration( + gradient: RadialGradient( + center: Alignment.center, + radius: 1.5, + colors: [ + Color(0xFF2C2C2E), // Dark Grey (Apple TV Surface) + Color(0xFF000000), // Pure Black ], + stops: [0.0, 1.0], ), ), - data: (playlists) { - if (playlists.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.playlist_remove, - size: 64, - color: Colors.grey.shade400, - ), - const SizedBox(height: 16), - Text( - 'No playlists available', - style: GoogleFonts.roboto( - fontSize: 18, - color: Colors.grey.shade600, - ), - ), - const SizedBox(height: 8), - Text( - currentUser?.isAdmin ?? false - ? 'Add playlists in Admin Panel' - : 'Contact administrator', - style: GoogleFonts.roboto( - fontSize: 14, - color: Colors.grey.shade500, - ), - ), - ], - ), - ); - } - - return GridView.builder( - padding: const EdgeInsets.all(16), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - crossAxisSpacing: 16, - mainAxisSpacing: 16, - childAspectRatio: 1.5, + child: playlistsAsync.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + 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)), + const SizedBox(height: 8), + ElevatedButton( + onPressed: () => ref.refresh(playlistsProvider), + child: const Text('Retry'), + ), + ], ), - itemCount: playlists.length, - itemBuilder: (context, index) { - final playlist = playlists[index]; - return _PlaylistCard( - playlist: playlist, - onTap: () { - context.go('/dashboard', extra: playlist); - }, + ), + data: (playlists) { + if (playlists.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.playlist_remove, + size: 64, + color: Colors.white24, + ), + const SizedBox(height: 16), + Text( + 'No playlists available', + style: GoogleFonts.roboto( + fontSize: 18, + color: Colors.white60, + ), + ), + const SizedBox(height: 8), + Text( + currentUser?.isAdmin ?? false + ? 'Add playlists in Admin Panel' + : 'Contact administrator', + style: GoogleFonts.roboto( + fontSize: 14, + color: Colors.white38, + ), + ), + ], + ), ); - }, - ); - }, + } + + return GridView.builder( + padding: const EdgeInsets.fromLTRB(24, 100, 24, 24), // Top padding for transparent AppBar + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 24, + mainAxisSpacing: 24, + childAspectRatio: 1.5, + ), + itemCount: playlists.length, + itemBuilder: (context, index) { + final playlist = playlists[index]; + return _PlaylistCard( + playlist: playlist, + onTap: () { + context.go('/dashboard', extra: playlist); + }, + ); + }, + ); + }, + ), ), ); } } -class _PlaylistCard extends StatelessWidget { +class _PlaylistCard extends StatefulWidget { final PlaylistConfig playlist; final VoidCallback onTap; @@ -130,54 +147,77 @@ class _PlaylistCard extends StatelessWidget { required this.onTap, }); + @override + State<_PlaylistCard> createState() => _PlaylistCardState(); +} + +class _PlaylistCardState extends State<_PlaylistCard> { + bool _isHovered = false; + @override Widget build(BuildContext context) { - return Card( - elevation: 4, - clipBehavior: Clip.antiAlias, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: InkWell( - onTap: onTap, - child: Container( + return MouseRegion( + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: widget.onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Colors.blue.shade700, - Colors.blue.shade900, - ], + color: _isHovered + ? Colors.white.withOpacity(0.15) + : Colors.white.withOpacity(0.05), // Glass effect + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: _isHovered + ? Colors.white.withOpacity(0.5) + : Colors.white.withOpacity(0.1), + width: 1.5, ), + boxShadow: _isHovered ? [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + ) + ] : [], ), - padding: const EdgeInsets.all(20), + padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon( - Icons.playlist_play, - size: 48, - color: Colors.white.withOpacity(0.9), + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.2), // Accent color tint + shape: BoxShape.circle, + ), + child: const Icon( + Icons.playlist_play, + size: 32, + color: Colors.white, // White icon + ), ), - const SizedBox(height: 12), + const SizedBox(height: 16), Text( - playlist.name, - style: GoogleFonts.roboto( - fontSize: 18, + widget.playlist.name, + style: GoogleFonts.outfit( // Using Outfit for modern look + fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), - const SizedBox(height: 4), + const SizedBox(height: 6), Text( - Uri.tryParse(playlist.dns)?.host ?? playlist.dns, - style: GoogleFonts.roboto( - fontSize: 12, - color: Colors.white.withOpacity(0.7), + Uri.tryParse(widget.playlist.dns)?.host ?? widget.playlist.dns, + style: GoogleFonts.inter( + fontSize: 13, + color: Colors.white54, ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -189,3 +229,4 @@ class _PlaylistCard extends StatelessWidget { ); } } + diff --git a/lib/features/iptv/widgets/settings_tab.dart b/lib/features/iptv/widgets/settings_tab.dart index 7c3ddc6..9c0f373 100644 --- a/lib/features/iptv/widgets/settings_tab.dart +++ b/lib/features/iptv/widgets/settings_tab.dart @@ -68,9 +68,21 @@ class _SettingsTabState extends ConsumerState with SingleTickerProv children: [ // TabBar Container( - color: Theme.of(context).colorScheme.surface, + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.05), + borderRadius: BorderRadius.circular(50), + ), child: TabBar( controller: _tabController, + indicator: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(50), + ), + labelColor: Colors.white, + unselectedLabelColor: Colors.white54, + dividerColor: Colors.transparent, + overlayColor: MaterialStateProperty.all(Colors.transparent), tabs: const [ Tab(icon: Icon(Icons.filter_list), text: 'Filtres'), Tab(icon: Icon(Icons.stream), text: 'Streaming'), @@ -109,82 +121,97 @@ class _SettingsTabState extends ConsumerState with SingleTickerProv padding: const EdgeInsets.all(16), children: [ // Theme selection card - Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.dark_mode, - color: AppColors.primary, + Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.05), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white.withOpacity(0.1)), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.dark_mode, + color: Colors.white, + ), + const SizedBox(width: 12), + Text( + 'Thème', + style: GoogleFonts.inter( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.white, ), - const SizedBox(width: 12), - Text( - 'Thème', - style: GoogleFonts.inter( - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - const SizedBox(height: 16), - - // Theme options - _buildThemeOption( - context: context, - title: 'Système', - subtitle: 'Suivre les paramètres du système', - icon: Icons.settings_suggest, - isSelected: themeState.appThemeMode == AppThemeMode.system, - onTap: () => themeNotifier.setThemeMode(AppThemeMode.system), - ), - const SizedBox(height: 8), - _buildThemeOption( - context: context, - title: 'Sombre', - subtitle: 'Interface sombre premium', - icon: Icons.dark_mode, - isSelected: themeState.appThemeMode == AppThemeMode.dark, - onTap: () => themeNotifier.setThemeMode(AppThemeMode.dark), - ), - const SizedBox(height: 8), - _buildThemeOption( - context: context, - title: 'Clair', - subtitle: 'Interface claire et lumineuse', - icon: Icons.light_mode, - isSelected: themeState.appThemeMode == AppThemeMode.light, - onTap: () => themeNotifier.setThemeMode(AppThemeMode.light), - ), - ], - ), + ), + ], + ), + const SizedBox(height: 16), + + // Theme options + _buildThemeOption( + context: context, + title: 'Système', + subtitle: 'Suivre les paramètres du système', + icon: Icons.settings_suggest, + isSelected: themeState.appThemeMode == AppThemeMode.system, + onTap: () => themeNotifier.setThemeMode(AppThemeMode.system), + ), + const SizedBox(height: 8), + _buildThemeOption( + context: context, + title: 'Sombre', + subtitle: 'Interface sombre premium', + icon: Icons.dark_mode, + isSelected: themeState.appThemeMode == AppThemeMode.dark, + onTap: () => themeNotifier.setThemeMode(AppThemeMode.dark), + ), + const SizedBox(height: 8), + _buildThemeOption( + context: context, + title: 'Clair', + subtitle: 'Interface claire et lumineuse', + icon: Icons.light_mode, + isSelected: themeState.appThemeMode == AppThemeMode.light, + onTap: () => themeNotifier.setThemeMode(AppThemeMode.light), + ), + ], ), ), const SizedBox(height: 16), // Quick toggle card - Card( + Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.05), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white.withOpacity(0.1)), + ), child: ListTile( leading: Icon( isDark ? Icons.dark_mode : Icons.light_mode, - color: AppColors.primary, + color: Colors.white, ), title: Text( 'Mode actuel', - style: GoogleFonts.inter(fontWeight: FontWeight.w500), + style: GoogleFonts.inter(fontWeight: FontWeight.w500, color: Colors.white), ), subtitle: Text( isDark ? 'Thème sombre actif' : 'Thème clair actif', + style: TextStyle(color: Colors.white70), ), trailing: Switch( value: isDark, onChanged: (_) => themeNotifier.toggleTheme(), activeColor: AppColors.primary, + trackColor: MaterialStateProperty.resolveWith( + (states) => states.contains(MaterialState.selected) + ? AppColors.primary.withOpacity(0.5) + : Colors.grey.shade800, + ), ), ), ), @@ -268,21 +295,27 @@ class _SettingsTabState extends ConsumerState with SingleTickerProv padding: const EdgeInsets.all(16), children: [ // User info card - Card( + Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.05), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white.withOpacity(0.1)), + ), + padding: const EdgeInsets.all(4), child: ListTile( - leading: const Icon(Icons.person), + leading: const Icon(Icons.person, color: Colors.white), title: Text( 'Connecté en tant que', - style: GoogleFonts.roboto(fontSize: 12, color: Colors.grey), + style: GoogleFonts.roboto(fontSize: 12, color: Colors.white70), ), subtitle: Text( currentUser?.username ?? 'Unknown', - style: GoogleFonts.roboto(fontWeight: FontWeight.w600), + style: GoogleFonts.roboto(fontWeight: FontWeight.w600, color: Colors.white), ), trailing: currentUser?.isAdmin ?? false ? Chip( - label: Text('Admin', style: GoogleFonts.roboto(fontSize: 11)), - backgroundColor: Colors.blue.shade100, + label: Text('Admin', style: GoogleFonts.roboto(fontSize: 11, color: Colors.black)), + backgroundColor: Colors.white, ) : null, ), @@ -295,6 +328,7 @@ class _SettingsTabState extends ConsumerState with SingleTickerProv style: GoogleFonts.roboto( fontWeight: FontWeight.bold, fontSize: 16, + color: Colors.white, ), ), const SizedBox(height: 4), @@ -302,7 +336,7 @@ class _SettingsTabState extends ConsumerState with SingleTickerProv 'Seules les catégories contenant un de ces mots-clés seront affichées. Séparez par des virgules.', style: GoogleFonts.roboto( fontSize: 12, - color: Colors.grey.shade600, + color: Colors.white54, ), ), const SizedBox(height: 12), @@ -357,11 +391,16 @@ class _SettingsTabState extends ConsumerState with SingleTickerProv // Admin panel if (currentUser?.isAdmin ?? false) ...[ - Card( + Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.05), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white.withOpacity(0.1)), + ), child: ListTile( - leading: const Icon(Icons.admin_panel_settings), - title: Text('Panneau Admin', style: GoogleFonts.roboto()), - trailing: const Icon(Icons.chevron_right), + leading: const Icon(Icons.admin_panel_settings, color: Colors.white), + title: Text('Panneau Admin', style: GoogleFonts.roboto(color: Colors.white)), + trailing: const Icon(Icons.chevron_right, color: Colors.white54), onTap: () => context.go('/admin'), ), ), @@ -369,24 +408,34 @@ class _SettingsTabState extends ConsumerState with SingleTickerProv ], // Change playlist - Card( + Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.05), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white.withOpacity(0.1)), + ), child: ListTile( - leading: const Icon(Icons.playlist_play), - title: Text('Changer de Playlist', style: GoogleFonts.roboto()), - trailing: const Icon(Icons.chevron_right), + leading: const Icon(Icons.playlist_play, 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'), ), ), const SizedBox(height: 8), // About - Card( + Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.05), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white.withOpacity(0.1)), + ), child: ListTile( - leading: const Icon(Icons.info_outline), - title: Text('À propos', style: GoogleFonts.roboto()), + leading: const Icon(Icons.info_outline, color: Colors.white), + title: Text('À propos', style: GoogleFonts.roboto(color: Colors.white)), subtitle: Text( 'XtremFlow IPTV v1.0.0', - style: GoogleFonts.roboto(fontSize: 12), + style: GoogleFonts.roboto(fontSize: 12, color: Colors.white54), ), ), ), @@ -417,69 +466,81 @@ class _SettingsTabState extends ConsumerState with SingleTickerProv required ValueChanged onChanged, required VoidCallback onClear, }) { - return Card( - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(icon, size: 18, color: Colors.blue.shade700), - const SizedBox(width: 8), - Text( - title, - style: GoogleFonts.roboto( - fontWeight: FontWeight.w600, - fontSize: 13, - ), + return Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.05), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white.withOpacity(0.1)), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, size: 18, color: Colors.blueAccent), + const SizedBox(width: 8), + Text( + title, + style: GoogleFonts.roboto( + fontWeight: FontWeight.w600, + fontSize: 13, + color: Colors.white, ), - ], - ), - const SizedBox(height: 8), - TextField( - controller: controller, - decoration: InputDecoration( - hintText: 'Ex: FR,FRANCE,HD,SPORT', - hintStyle: GoogleFonts.roboto(fontSize: 12), - border: const OutlineInputBorder(), - isDense: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 8, - ), - suffixIcon: controller.text.isNotEmpty - ? IconButton( - icon: const Icon(Icons.clear, size: 16), - onPressed: onClear, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ) - : null, - ), - style: GoogleFonts.roboto(fontSize: 12), - onChanged: onChanged, - ), - if (keywords.isNotEmpty) ...[ - const SizedBox(height: 6), - Wrap( - spacing: 4, - runSpacing: 4, - children: keywords.map((keyword) { - return Chip( - label: Text( - keyword, - style: GoogleFonts.roboto(fontSize: 10), - ), - padding: EdgeInsets.zero, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - visualDensity: VisualDensity.compact, - ); - }).toList(), ), ], + ), + const SizedBox(height: 12), + TextField( + controller: controller, + decoration: InputDecoration( + hintText: 'Ex: FR,FRANCE,HD,SPORT', + hintStyle: GoogleFonts.roboto(fontSize: 12, color: Colors.white30), + filled: true, + fillColor: Colors.black26, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + isDense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 12, + ), + suffixIcon: controller.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear, size: 16, color: Colors.white54), + onPressed: onClear, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ) + : null, + ), + style: GoogleFonts.roboto(fontSize: 12, color: Colors.white), + onChanged: onChanged, + ), + if (keywords.isNotEmpty) ...[ + const SizedBox(height: 12), + Wrap( + spacing: 6, + runSpacing: 6, + children: keywords.map((keyword) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: Colors.blueAccent.withOpacity(0.2), + borderRadius: BorderRadius.circular(50), + border: Border.all(color: Colors.blueAccent.withOpacity(0.5)), + ), + child: Text( + keyword, + style: GoogleFonts.roboto(fontSize: 11, color: Colors.white), + ), + ); + }).toList(), + ), ], - ), + ], ), ); } diff --git a/lib/features/iptv/widgets/streaming_settings_tab.dart b/lib/features/iptv/widgets/streaming_settings_tab.dart index f786cff..ccf37c5 100644 --- a/lib/features/iptv/widgets/streaming_settings_tab.dart +++ b/lib/features/iptv/widgets/streaming_settings_tab.dart @@ -21,6 +21,7 @@ class StreamingSettingsTab extends ConsumerWidget { style: GoogleFonts.roboto( fontSize: 18, fontWeight: FontWeight.bold, + color: Colors.white, ), ), const SizedBox(height: 8), @@ -28,7 +29,7 @@ class StreamingSettingsTab extends ConsumerWidget { 'Ces paramètres s\'appliquent uniquement aux flux TV en direct.', style: GoogleFonts.roboto( fontSize: 12, - color: Colors.grey, + color: Colors.white54, ), ), const SizedBox(height: 24), @@ -95,17 +96,18 @@ class StreamingSettingsTab extends ConsumerWidget { ConnectionTimeout.long => 'Long (60s)', }, ), - const Divider(height: 1), + Divider(height: 1, color: Colors.white.withOpacity(0.1)), SwitchListTile( title: Text( 'Reconnexion automatique', - style: GoogleFonts.roboto(fontSize: 14), + style: GoogleFonts.roboto(fontSize: 14, color: Colors.white), ), subtitle: Text( 'Se reconnecter en cas de coupure', - style: GoogleFonts.roboto(fontSize: 12, color: Colors.grey), + style: GoogleFonts.roboto(fontSize: 12, color: Colors.white54), ), value: settings.autoReconnect, + activeColor: Colors.blueAccent, onChanged: (v) => notifier.setAutoReconnect(v), ), ], @@ -130,17 +132,18 @@ class StreamingSettingsTab extends ConsumerWidget { TranscodingMode.disabled => 'Désactivé (direct)', }, ), - const Divider(height: 1), + Divider(height: 1, color: Colors.white.withOpacity(0.1)), SwitchListTile( title: Text( 'Préférer lecture directe', - style: GoogleFonts.roboto(fontSize: 14), + style: GoogleFonts.roboto(fontSize: 14, color: Colors.white), ), subtitle: Text( 'Utiliser le flux original si compatible', - style: GoogleFonts.roboto(fontSize: 12, color: Colors.grey), + style: GoogleFonts.roboto(fontSize: 12, color: Colors.white54), ), value: settings.preferDirectPlay, + activeColor: Colors.blueAccent, onChanged: (v) => notifier.setPreferDirectPlay(v), ), ], @@ -203,9 +206,12 @@ class StreamingSettingsTab extends ConsumerWidget { required IconData icon, required List children, }) { - return Card( - elevation: 2, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + return Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.05), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white.withOpacity(0.1)), + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -213,19 +219,20 @@ class StreamingSettingsTab extends ConsumerWidget { padding: const EdgeInsets.all(16), child: Row( children: [ - Icon(icon, size: 20, color: Theme.of(context).colorScheme.primary), + Icon(icon, size: 20, color: Colors.blueAccent), const SizedBox(width: 8), Text( title, style: GoogleFonts.roboto( fontSize: 16, fontWeight: FontWeight.w600, + color: Colors.white, ), ), ], ), ), - const Divider(height: 1), + Divider(height: 1, color: Colors.white.withOpacity(0.1)), ...children, ], ), @@ -241,21 +248,24 @@ class StreamingSettingsTab extends ConsumerWidget { required String Function(T) labelBuilder, }) { return ListTile( - title: Text(title, style: GoogleFonts.roboto(fontSize: 14)), - subtitle: Text(subtitle, style: GoogleFonts.roboto(fontSize: 12, color: Colors.grey)), + title: Text(title, style: GoogleFonts.roboto(fontSize: 14, color: Colors.white)), + subtitle: Text(subtitle, style: GoogleFonts.roboto(fontSize: 12, color: Colors.white54)), trailing: DropdownButton( value: value, underline: const SizedBox(), + dropdownColor: const Color(0xFF1C1C1E), // Dark background for dropdown items: items.map((item) { return DropdownMenuItem( value: item, child: Text( labelBuilder(item), - style: GoogleFonts.roboto(fontSize: 13), + style: GoogleFonts.roboto(fontSize: 13, color: Colors.white), ), ); }).toList(), onChanged: onChanged, + style: GoogleFonts.roboto(color: Colors.white), + iconEnabledColor: Colors.white70, ), ); }