From 0fc4a15f4020531f63cc8ea3b787f6f279ebd946 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Sat, 6 Dec 2025 17:20:07 +0100 Subject: [PATCH] feat: Add XtremFlow color palette for light/dark themes and initial Flutter web HTML with a themed loading screen. --- lib/core/providers/theme_provider.dart | 111 ++++++++ lib/core/theme/app_colors.dart | 85 ++++-- lib/core/theme/app_theme.dart | 232 +++++++++++++++ .../iptv/widgets/category_carousel.dart | 223 +++++++++++++++ lib/features/iptv/widgets/channel_card.dart | 264 ++++++++++++++++++ lib/features/iptv/widgets/settings_tab.dart | 172 +++++++++++- lib/main.dart | 7 +- web/index.html | 80 +++++- 8 files changed, 1133 insertions(+), 41 deletions(-) create mode 100644 lib/core/providers/theme_provider.dart create mode 100644 lib/features/iptv/widgets/category_carousel.dart create mode 100644 lib/features/iptv/widgets/channel_card.dart diff --git a/lib/core/providers/theme_provider.dart b/lib/core/providers/theme_provider.dart new file mode 100644 index 0000000..4a97d09 --- /dev/null +++ b/lib/core/providers/theme_provider.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// Theme mode preference options +enum AppThemeMode { + system, // Follow system preference + light, // Force light mode + dark, // Force dark mode +} + +/// Keys for SharedPreferences storage +class _ThemeKeys { + static const String themeMode = 'app_theme_mode'; +} + +/// Theme state containing current mode +class ThemeState { + final AppThemeMode appThemeMode; + final ThemeMode themeMode; + + const ThemeState({ + this.appThemeMode = AppThemeMode.dark, + this.themeMode = ThemeMode.dark, + }); + + ThemeState copyWith({ + AppThemeMode? appThemeMode, + ThemeMode? themeMode, + }) { + return ThemeState( + appThemeMode: appThemeMode ?? this.appThemeMode, + themeMode: themeMode ?? this.themeMode, + ); + } +} + +/// Theme provider for managing light/dark mode with persistence +class ThemeNotifier extends StateNotifier { + ThemeNotifier() : super(const ThemeState()) { + _loadTheme(); + } + + /// Load saved theme preference from SharedPreferences + Future _loadTheme() async { + try { + final prefs = await SharedPreferences.getInstance(); + final savedMode = prefs.getString(_ThemeKeys.themeMode); + + if (savedMode != null) { + final appMode = AppThemeMode.values.firstWhere( + (e) => e.name == savedMode, + orElse: () => AppThemeMode.dark, + ); + state = state.copyWith( + appThemeMode: appMode, + themeMode: _mapToThemeMode(appMode), + ); + } + } catch (e) { + debugPrint('Error loading theme: $e'); + } + } + + /// Set theme mode and persist to SharedPreferences + Future setThemeMode(AppThemeMode mode) async { + state = state.copyWith( + appThemeMode: mode, + themeMode: _mapToThemeMode(mode), + ); + + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_ThemeKeys.themeMode, mode.name); + } catch (e) { + debugPrint('Error saving theme: $e'); + } + } + + /// Quick toggle between light and dark (skip system) + Future toggleTheme() async { + final newMode = state.appThemeMode == AppThemeMode.dark + ? AppThemeMode.light + : AppThemeMode.dark; + await setThemeMode(newMode); + } + + /// Check if currently in dark mode + bool get isDarkMode { + return state.themeMode == ThemeMode.dark || + (state.themeMode == ThemeMode.system && + WidgetsBinding.instance.platformDispatcher.platformBrightness == Brightness.dark); + } + + /// Map AppThemeMode to Flutter's ThemeMode + ThemeMode _mapToThemeMode(AppThemeMode mode) { + switch (mode) { + case AppThemeMode.system: + return ThemeMode.system; + case AppThemeMode.light: + return ThemeMode.light; + case AppThemeMode.dark: + return ThemeMode.dark; + } + } +} + +/// Provider for theme state +final themeProvider = StateNotifierProvider((ref) { + return ThemeNotifier(); +}); diff --git a/lib/core/theme/app_colors.dart b/lib/core/theme/app_colors.dart index 3860942..0464c75 100644 --- a/lib/core/theme/app_colors.dart +++ b/lib/core/theme/app_colors.dart @@ -2,16 +2,16 @@ import 'package:flutter/material.dart'; /// XtremFlow Premium Color Palette /// -/// Modern dark theme with cyan/teal gradients +/// Modern theme with cyan/teal gradients - supports light and dark modes class AppColors { AppColors._(); // ============ PRIMARY GRADIENT ============ /// Cyan primary - main accent color - static const Color primary = Color(0xFF00D9FF); + static const Color primary = Color(0xFF00BCD4); /// Teal secondary - gradient end - static const Color secondary = Color(0xFF00BFA5); + static const Color secondary = Color(0xFF00ACC1); /// Primary gradient for buttons and highlights static const LinearGradient primaryGradient = LinearGradient( @@ -20,31 +20,51 @@ class AppColors { end: Alignment.bottomRight, ); - // ============ SURFACE COLORS ============ + // ============ DARK MODE SURFACE COLORS ============ /// Deep dark background - static const Color background = Color(0xFF0A0E14); + static const Color backgroundDark = Color(0xFF0A0E1A); - /// Card/elevated surface - static const Color surface = Color(0xFF141A22); + /// Card/elevated surface (dark) + static const Color surfaceDark = Color(0xFF121829); - /// Lighter surface variant (hover states, inputs) - static const Color surfaceVariant = Color(0xFF1E2630); + /// Lighter surface variant (dark) + static const Color surfaceVariantDark = Color(0xFF1E2638); - /// Border/divider color - static const Color border = Color(0xFF2A3441); - - /// Overlay color (modals, dialogs) - static const Color overlay = Color(0xCC0A0E14); + /// Border/divider color (dark) + static const Color borderDark = Color(0xFF2A3441); - // ============ TEXT COLORS ============ - /// Primary text - white with high opacity - static const Color textPrimary = Color(0xFFF5F5F5); + // ============ LIGHT MODE SURFACE COLORS ============ + /// Light background + static const Color backgroundLight = Color(0xFFF5F7FA); - /// Secondary text - muted - static const Color textSecondary = Color(0xFFB0BEC5); + /// Card/elevated surface (light) + static const Color surfaceLight = Color(0xFFFFFFFF); - /// Disabled/hint text - static const Color textDisabled = Color(0xFF607D8B); + /// Lighter surface variant (light) + static const Color surfaceVariantLight = Color(0xFFF0F2F5); + + /// Border/divider color (light) + static const Color borderLight = Color(0xFFE2E8F0); + + // ============ DARK MODE TEXT COLORS ============ + static const Color textPrimaryDark = Color(0xFFF5F5F5); + static const Color textSecondaryDark = Color(0xFF94A3B8); + static const Color textDisabledDark = Color(0xFF64748B); + + // ============ LIGHT MODE TEXT COLORS ============ + static const Color textPrimaryLight = Color(0xFF1A1A2E); + static const Color textSecondaryLight = Color(0xFF64748B); + static const Color textDisabledLight = Color(0xFF94A3B8); + + // ============ LEGACY ALIASES (for backwards compatibility) ============ + static const Color background = backgroundDark; + static const Color surface = surfaceDark; + static const Color surfaceVariant = surfaceVariantDark; + static const Color border = borderDark; + static const Color textPrimary = textPrimaryDark; + static const Color textSecondary = textSecondaryDark; + static const Color textDisabled = textDisabledDark; + static const Color overlay = Color(0xCC0A0E14); // ============ ACCENT COLORS ============ /// Live indicator red @@ -63,7 +83,6 @@ class AppColors { static const Color info = Color(0xFF3B82F6); // ============ CATEGORY COLORS ============ - /// Colors for category chips/tags static const List categoryColors = [ Color(0xFF6366F1), // Indigo Color(0xFFEC4899), // Pink @@ -74,21 +93,33 @@ class AppColors { ]; // ============ GLASSMORPHISM ============ - /// Glass effect background color static const Color glassBackground = Color(0x1AFFFFFF); - - /// Glass border color static const Color glassBorder = Color(0x33FFFFFF); + static const Color glassBackgroundLight = Color(0x40FFFFFF); + static const Color glassBorderLight = Color(0x20000000); // ============ DARK THEME COLOR SCHEME ============ static ColorScheme get darkColorScheme => const ColorScheme.dark( primary: primary, secondary: secondary, - surface: surface, + surface: surfaceDark, error: error, onPrimary: Color(0xFF000000), onSecondary: Color(0xFF000000), - onSurface: textPrimary, + onSurface: textPrimaryDark, + onError: Color(0xFFFFFFFF), + ); + + // ============ LIGHT THEME COLOR SCHEME ============ + static ColorScheme get lightColorScheme => const ColorScheme.light( + primary: primary, + secondary: secondary, + surface: surfaceLight, + error: error, + onPrimary: Color(0xFFFFFFFF), + onSecondary: Color(0xFFFFFFFF), + onSurface: textPrimaryLight, onError: Color(0xFFFFFFFF), ); } + diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index 607cfd2..f3a60cc 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -388,4 +388,236 @@ class AppTheme { ), ); } + + // ============ LIGHT THEME ============ + static ThemeData get lightTheme { + final baseTheme = ThemeData.light(useMaterial3: true); + + return baseTheme.copyWith( + colorScheme: AppColors.lightColorScheme, + scaffoldBackgroundColor: AppColors.backgroundLight, + + // AppBar + appBarTheme: AppBarTheme( + backgroundColor: AppColors.surfaceLight, + surfaceTintColor: Colors.transparent, + elevation: 0, + centerTitle: false, + titleTextStyle: GoogleFonts.inter( + fontSize: 20, + fontWeight: FontWeight.w600, + color: AppColors.textPrimaryLight, + ), + iconTheme: const IconThemeData(color: AppColors.textPrimaryLight), + ), + + // Navigation Bar (bottom) + navigationBarTheme: NavigationBarThemeData( + backgroundColor: AppColors.surfaceLight, + surfaceTintColor: Colors.transparent, + indicatorColor: AppColors.primary.withOpacity(0.15), + labelTextStyle: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w600, + color: AppColors.primary, + ); + } + return GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w500, + color: AppColors.textSecondaryLight, + ); + }), + iconTheme: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return const IconThemeData(color: AppColors.primary); + } + return const IconThemeData(color: AppColors.textSecondaryLight); + }), + ), + + // Navigation Rail (desktop sidebar) + navigationRailTheme: NavigationRailThemeData( + backgroundColor: AppColors.surfaceLight, + indicatorColor: AppColors.primary.withOpacity(0.15), + selectedIconTheme: const IconThemeData(color: AppColors.primary), + unselectedIconTheme: const IconThemeData(color: AppColors.textSecondaryLight), + selectedLabelTextStyle: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w600, + color: AppColors.primary, + ), + unselectedLabelTextStyle: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w500, + color: AppColors.textSecondaryLight, + ), + ), + + // Cards + cardTheme: CardThemeData( + color: AppColors.surfaceLight, + surfaceTintColor: Colors.transparent, + elevation: 2, + shadowColor: Colors.black.withOpacity(0.08), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(radiusMd), + ), + margin: EdgeInsets.zero, + ), + + // Inputs + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: AppColors.surfaceVariantLight, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(radiusSm), + borderSide: const BorderSide(color: AppColors.borderLight), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(radiusSm), + borderSide: const BorderSide(color: AppColors.borderLight), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(radiusSm), + borderSide: const BorderSide(color: AppColors.primary, width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(radiusSm), + borderSide: const BorderSide(color: AppColors.error), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: spacing16, + vertical: spacing12, + ), + hintStyle: GoogleFonts.inter( + color: AppColors.textDisabledLight, + fontSize: 14, + ), + labelStyle: GoogleFonts.inter( + color: AppColors.textSecondaryLight, + fontSize: 14, + ), + ), + + // Buttons + filledButtonTheme: FilledButtonThemeData( + style: FilledButton.styleFrom( + backgroundColor: AppColors.primary, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: spacing24, + vertical: spacing12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(radiusSm), + ), + textStyle: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + + // Dialogs + dialogTheme: DialogThemeData( + backgroundColor: AppColors.surfaceLight, + surfaceTintColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(radiusLg), + ), + titleTextStyle: GoogleFonts.inter( + fontSize: 18, + fontWeight: FontWeight.w600, + color: AppColors.textPrimaryLight, + ), + contentTextStyle: GoogleFonts.inter( + fontSize: 14, + color: AppColors.textSecondaryLight, + ), + ), + + // Dividers + dividerTheme: const DividerThemeData( + color: AppColors.borderLight, + thickness: 1, + space: 1, + ), + + // List tiles + listTileTheme: ListTileThemeData( + tileColor: Colors.transparent, + contentPadding: const EdgeInsets.symmetric( + horizontal: spacing16, + vertical: spacing8, + ), + titleTextStyle: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.textPrimaryLight, + ), + subtitleTextStyle: GoogleFonts.inter( + fontSize: 12, + color: AppColors.textSecondaryLight, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(radiusSm), + ), + ), + + // Progress indicators + progressIndicatorTheme: const ProgressIndicatorThemeData( + color: AppColors.primary, + linearTrackColor: AppColors.surfaceVariantLight, + circularTrackColor: AppColors.surfaceVariantLight, + ), + + // Text theme + textTheme: TextTheme( + displayLarge: GoogleFonts.inter( + fontSize: 57, + fontWeight: FontWeight.w400, + color: AppColors.textPrimaryLight, + ), + headlineLarge: GoogleFonts.inter( + fontSize: 32, + fontWeight: FontWeight.w600, + color: AppColors.textPrimaryLight, + ), + titleLarge: GoogleFonts.inter( + fontSize: 22, + fontWeight: FontWeight.w600, + color: AppColors.textPrimaryLight, + ), + titleMedium: GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.textPrimaryLight, + ), + bodyLarge: GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.w400, + color: AppColors.textPrimaryLight, + ), + bodyMedium: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.textPrimaryLight, + ), + bodySmall: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w400, + color: AppColors.textSecondaryLight, + ), + labelLarge: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.textPrimaryLight, + ), + ), + ); + } } + diff --git a/lib/features/iptv/widgets/category_carousel.dart b/lib/features/iptv/widgets/category_carousel.dart new file mode 100644 index 0000000..f1b0c29 --- /dev/null +++ b/lib/features/iptv/widgets/category_carousel.dart @@ -0,0 +1,223 @@ +import 'package:flutter/material.dart'; +import '../../../core/theme/app_colors.dart'; + +/// Horizontal carousel for displaying channels by category +/// +/// Netflix-style horizontal scrolling list with: +/// - Category title with count +/// - Smooth horizontal scrolling +/// - Lazy loading support +/// - Fade edges for visual polish +class CategoryCarousel extends StatelessWidget { + final String title; + final int? itemCount; + final List items; + final Widget Function(BuildContext context, T item, int index) itemBuilder; + final double itemWidth; + final double itemHeight; + final double spacing; + final EdgeInsets? padding; + final VoidCallback? onSeeAllTap; + + const CategoryCarousel({ + super.key, + required this.title, + required this.items, + required this.itemBuilder, + this.itemCount, + this.itemWidth = 180, + this.itemHeight = 120, + this.spacing = 12, + this.padding, + this.onSeeAllTap, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + + if (items.isEmpty) { + return const SizedBox.shrink(); + } + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Category header + Padding( + padding: padding ?? const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + title, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: isDark + ? AppColors.textPrimaryDark + : AppColors.textPrimaryLight, + ), + ), + if (itemCount != null) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: AppColors.primary.withOpacity(0.15), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '$itemCount', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: AppColors.primary, + ), + ), + ), + ], + ], + ), + if (onSeeAllTap != null) + TextButton( + onPressed: onSeeAllTap, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Voir tout', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: AppColors.primary, + ), + ), + const SizedBox(width: 4), + const Icon( + Icons.arrow_forward_ios_rounded, + size: 12, + color: AppColors.primary, + ), + ], + ), + ), + ], + ), + ), + + const SizedBox(height: 12), + + // Horizontal scrolling list with fade edges + SizedBox( + height: itemHeight, + child: ShaderMask( + shaderCallback: (Rect bounds) { + return LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + Colors.transparent, + Colors.white, + Colors.white, + Colors.transparent, + ], + stops: const [0.0, 0.02, 0.98, 1.0], + ).createShader(bounds); + }, + blendMode: BlendMode.dstIn, + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.symmetric( + horizontal: (padding?.left ?? 16), + ), + physics: const BouncingScrollPhysics(), + itemCount: items.length, + separatorBuilder: (_, __) => SizedBox(width: spacing), + itemBuilder: (context, index) { + return SizedBox( + width: itemWidth, + height: itemHeight, + child: itemBuilder(context, items[index], index), + ); + }, + ), + ), + ), + ], + ); + } +} + +/// Simple category chip for filtering +class CategoryChipButton extends StatelessWidget { + final String label; + final bool isSelected; + final VoidCallback? onTap; + final IconData? icon; + + const CategoryChipButton({ + super.key, + required this.label, + this.isSelected = false, + this.onTap, + this.icon, + }); + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + gradient: isSelected ? AppColors.primaryGradient : null, + color: isSelected + ? null + : (isDark ? AppColors.surfaceVariantDark : AppColors.surfaceVariantLight), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: isSelected + ? Colors.transparent + : (isDark ? AppColors.borderDark : AppColors.borderLight), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) ...[ + Icon( + icon, + size: 16, + color: isSelected + ? Colors.white + : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight), + ), + const SizedBox(width: 6), + ], + Text( + label, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: isSelected + ? Colors.white + : (isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/iptv/widgets/channel_card.dart b/lib/features/iptv/widgets/channel_card.dart new file mode 100644 index 0000000..03acd45 --- /dev/null +++ b/lib/features/iptv/widgets/channel_card.dart @@ -0,0 +1,264 @@ +import 'package:flutter/material.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import '../../../core/theme/app_colors.dart'; + +/// Premium channel card with Netflix-style design +/// +/// Features: +/// - Glassmorphism background +/// - Thumbnail/logo with fallback +/// - Live badge with pulse animation +/// - EPG overlay +/// - Hover/tap effects +class ChannelCard extends StatefulWidget { + final String name; + final String? iconUrl; + final String? currentProgram; + final bool isLive; + final VoidCallback? onTap; + final double? width; + final double? height; + + const ChannelCard({ + super.key, + required this.name, + this.iconUrl, + this.currentProgram, + this.isLive = true, + this.onTap, + this.width, + this.height, + }); + + @override + State createState() => _ChannelCardState(); +} + +class _ChannelCardState extends State + with SingleTickerProviderStateMixin { + bool _isHovered = false; + late AnimationController _pulseController; + late Animation _pulseAnimation; + + @override + void initState() { + super.initState(); + _pulseController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1500), + )..repeat(reverse: true); + + _pulseAnimation = Tween(begin: 0.8, end: 1.0).animate( + CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut), + ); + } + + @override + void dispose() { + _pulseController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + + return MouseRegion( + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + child: GestureDetector( + onTap: widget.onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeOutCubic, + width: widget.width ?? 180, + height: widget.height ?? 120, + transform: Matrix4.identity()..scale(_isHovered ? 1.05 : 1.0), + transformAlignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: isDark + ? [ + AppColors.surfaceDark.withOpacity(0.95), + AppColors.surfaceVariantDark.withOpacity(0.9), + ] + : [ + Colors.white.withOpacity(0.95), + AppColors.surfaceVariantLight.withOpacity(0.9), + ], + ), + border: Border.all( + color: _isHovered + ? AppColors.primary.withOpacity(0.5) + : (isDark ? AppColors.borderDark : AppColors.borderLight), + width: _isHovered ? 1.5 : 1, + ), + boxShadow: _isHovered + ? [ + BoxShadow( + color: AppColors.primary.withOpacity(0.2), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ] + : [ + BoxShadow( + color: Colors.black.withOpacity(isDark ? 0.3 : 0.1), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(11), + child: Stack( + fit: StackFit.expand, + children: [ + // Channel logo/thumbnail + _buildChannelImage(isDark), + + // Gradient overlay for text readability + Positioned.fill( + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black.withOpacity(0.7), + ], + stops: const [0.4, 1.0], + ), + ), + ), + ), + + // Live badge + if (widget.isLive) + Positioned( + top: 8, + right: 8, + child: _buildLiveBadge(), + ), + + // Channel info overlay + Positioned( + left: 10, + right: 10, + bottom: 10, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.name, + style: const TextStyle( + color: Colors.white, + fontSize: 13, + fontWeight: FontWeight.w600, + shadows: [ + Shadow( + color: Colors.black54, + blurRadius: 4, + ), + ], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (widget.currentProgram != null) ...[ + const SizedBox(height: 2), + Text( + widget.currentProgram!, + style: TextStyle( + color: Colors.white.withOpacity(0.8), + fontSize: 11, + fontWeight: FontWeight.w400, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildChannelImage(bool isDark) { + if (widget.iconUrl != null && widget.iconUrl!.isNotEmpty) { + return CachedNetworkImage( + imageUrl: widget.iconUrl!, + fit: BoxFit.cover, + placeholder: (context, url) => _buildPlaceholder(isDark), + errorWidget: (context, url, error) => _buildPlaceholder(isDark), + ); + } + return _buildPlaceholder(isDark); + } + + Widget _buildPlaceholder(bool isDark) { + return Container( + color: isDark ? AppColors.surfaceVariantDark : AppColors.surfaceVariantLight, + child: Center( + child: Icon( + Icons.live_tv_rounded, + size: 36, + color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, + ), + ), + ); + } + + Widget _buildLiveBadge() { + return AnimatedBuilder( + animation: _pulseAnimation, + builder: (context, child) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: AppColors.live.withOpacity(_pulseAnimation.value), + borderRadius: BorderRadius.circular(4), + boxShadow: [ + BoxShadow( + color: AppColors.live.withOpacity(0.4 * _pulseAnimation.value), + blurRadius: 8, + spreadRadius: 1, + ), + ], + ), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.circle, + size: 6, + color: Colors.white, + ), + SizedBox(width: 4), + Text( + 'LIVE', + style: TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.w700, + letterSpacing: 0.5, + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/features/iptv/widgets/settings_tab.dart b/lib/features/iptv/widgets/settings_tab.dart index 444e504..323c533 100644 --- a/lib/features/iptv/widgets/settings_tab.dart +++ b/lib/features/iptv/widgets/settings_tab.dart @@ -5,8 +5,10 @@ import 'package:google_fonts/google_fonts.dart'; import '../../auth/providers/auth_provider.dart'; import '../providers/settings_provider.dart'; import 'streaming_settings_tab.dart'; +import '../../../core/providers/theme_provider.dart'; +import '../../../core/theme/app_colors.dart'; -/// Main Settings tab with sub-tabs for Filters and Streaming +/// Main Settings tab with sub-tabs for Filters, Streaming, and Appearance class SettingsTab extends ConsumerStatefulWidget { const SettingsTab({super.key}); @@ -24,7 +26,7 @@ class _SettingsTabState extends ConsumerState with SingleTickerProv @override void initState() { super.initState(); - _tabController = TabController(length: 2, vsync: this); + _tabController = TabController(length: 3, vsync: this); _liveTvController = TextEditingController(); _moviesController = TextEditingController(); _seriesController = TextEditingController(); @@ -72,6 +74,7 @@ class _SettingsTabState extends ConsumerState with SingleTickerProv tabs: const [ Tab(icon: Icon(Icons.filter_list), text: 'Filtres'), Tab(icon: Icon(Icons.stream), text: 'Streaming'), + Tab(icon: Icon(Icons.palette), text: 'Apparence'), ], ), ), @@ -86,6 +89,9 @@ class _SettingsTabState extends ConsumerState with SingleTickerProv // Streaming Tab const StreamingSettingsTab(), + + // Appearance Tab + _buildAppearanceTab(context), ], ), ), @@ -93,6 +99,168 @@ class _SettingsTabState extends ConsumerState with SingleTickerProv ); } + /// Build Appearance tab with theme toggle + Widget _buildAppearanceTab(BuildContext context) { + final themeState = ref.watch(themeProvider); + final themeNotifier = ref.read(themeProvider.notifier); + final isDark = Theme.of(context).brightness == Brightness.dark; + + return ListView( + 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, + ), + 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), + + // Quick toggle card + Card( + child: ListTile( + leading: Icon( + isDark ? Icons.dark_mode : Icons.light_mode, + color: AppColors.primary, + ), + title: Text( + 'Mode actuel', + style: GoogleFonts.inter(fontWeight: FontWeight.w500), + ), + subtitle: Text( + isDark ? 'Thème sombre actif' : 'Thème clair actif', + ), + trailing: Switch( + value: isDark, + onChanged: (_) => themeNotifier.toggleTheme(), + activeColor: AppColors.primary, + ), + ), + ), + ], + ); + } + + Widget _buildThemeOption({ + required BuildContext context, + required String title, + required String subtitle, + required IconData icon, + required bool isSelected, + required VoidCallback onTap, + }) { + final isDark = Theme.of(context).brightness == Brightness.dark; + + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: isSelected + ? AppColors.primary.withOpacity(0.1) + : (isDark ? AppColors.surfaceVariantDark : AppColors.surfaceVariantLight), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected ? AppColors.primary : Colors.transparent, + width: 2, + ), + ), + child: Row( + children: [ + Icon( + icon, + color: isSelected + ? AppColors.primary + : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: GoogleFonts.inter( + fontWeight: FontWeight.w600, + color: isSelected + ? AppColors.primary + : (isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight), + ), + ), + Text( + subtitle, + style: GoogleFonts.inter( + fontSize: 12, + color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, + ), + ), + ], + ), + ), + if (isSelected) + Icon( + Icons.check_circle, + color: AppColors.primary, + ), + ], + ), + ), + ); + } + Widget _buildFiltersTab(BuildContext context, dynamic currentUser, IptvSettings settings) { return ListView( padding: const EdgeInsets.all(16), diff --git a/lib/main.dart b/lib/main.dart index 5d0dbfc..5523572 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'core/router/app_router.dart'; import 'core/theme/app_theme.dart'; +import 'core/providers/theme_provider.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -14,14 +15,16 @@ class MyApp extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final router = ref.watch(routerProvider); + final themeState = ref.watch(themeProvider); return MaterialApp.router( title: 'XtremFlow IPTV', debugShowCheckedModeBanner: false, - theme: AppTheme.darkTheme, + theme: AppTheme.lightTheme, darkTheme: AppTheme.darkTheme, - themeMode: ThemeMode.dark, // Force dark mode for premium look + themeMode: themeState.themeMode, routerConfig: router, ); } } + diff --git a/web/index.html b/web/index.html index 024b61a..889b725 100644 --- a/web/index.html +++ b/web/index.html @@ -5,35 +5,95 @@ - + - + + XtremFlow IPTV + + + + + -
Loading XtremFlow...
+
+ +
+
Loading...
+