diff --git a/lib/core/theme/app_colors.dart b/lib/core/theme/app_colors.dart index 0464c75..09cb19b 100644 --- a/lib/core/theme/app_colors.dart +++ b/lib/core/theme/app_colors.dart @@ -1,125 +1,99 @@ import 'package:flutter/material.dart'; -/// XtremFlow Premium Color Palette +/// XtremFlow Cinematic Minimalist Palette /// -/// Modern theme with cyan/teal gradients - supports light and dark modes +/// A deep, dark, and neutral palette designed for content-first experiences. +/// Focuses on #0A0A0A backgrounds and subtle surfaces. class AppColors { AppColors._(); - // ============ PRIMARY GRADIENT ============ - /// Cyan primary - main accent color - static const Color primary = Color(0xFF00BCD4); + // ============ BACKGROUNDS ============ + /// Pitch black for OLED/Main backgrounds + static const Color background = Color(0xFF0A0A0A); - /// Teal secondary - gradient end - static const Color secondary = Color(0xFF00ACC1); + /// Dark grey for cards/sheets + static const Color surface = Color(0xFF141414); - /// Primary gradient for buttons and highlights + /// Slightly lighter for hover states or active items + static const Color surfaceLight = Color(0xFF1F1F1F); + + /// For inputs or secondary surfaces + static const Color surfaceVariant = Color(0xFF2A2A2A); + + // ============ ACCENTS (MINIMALIST) ============ + /// Primary Brand Color (Electric Blue/Violet for modern feel) + /// Replacing the old Gradient Cyan with a solid, punchy color. + static const Color primary = Color(0xFF3F51B5); // Indigo + + /// Secondary accent + static const Color accent = Color(0xFF7986CB); + + /// Primary Gradient (Subtle, for buttons only) static const LinearGradient primaryGradient = LinearGradient( - colors: [primary, secondary], + colors: [primary, accent], begin: Alignment.topLeft, end: Alignment.bottomRight, ); - // ============ DARK MODE SURFACE COLORS ============ - /// Deep dark background - static const Color backgroundDark = Color(0xFF0A0E1A); + // ============ TEXT ============ + /// Pure white for high contrast headers + static const Color textPrimary = Color(0xFFFFFFFF); - /// Card/elevated surface (dark) - static const Color surfaceDark = Color(0xFF121829); + /// Light grey for body text + static const Color textSecondary = Color(0xFFB3B3B3); - /// Lighter surface variant (dark) - static const Color surfaceVariantDark = Color(0xFF1E2638); + /// Darker grey for hints/disabled + static const Color textTertiary = Color(0xFF808080); - /// Border/divider color (dark) - static const Color borderDark = Color(0xFF2A3441); + // ============ FUNCTIONAL COLORS ============ + static const Color success = Color(0xFF4CAF50); + static const Color warning = Color(0xFFFFC107); + static const Color error = Color(0xFFCF6679); + static const Color info = Color(0xFF2196F3); + static const Color live = Color(0xFFFF0000); // Standard Red for LIVE - // ============ LIGHT MODE SURFACE COLORS ============ - /// Light background - static const Color backgroundLight = Color(0xFFF5F7FA); - - /// Card/elevated surface (light) - static const Color surfaceLight = Color(0xFFFFFFFF); - - /// 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 - static const Color live = Color(0xFFFF6B6B); - - /// Success green - static const Color success = Color(0xFF4ADE80); - - /// Warning amber - static const Color warning = Color(0xFFFBBF24); - - /// Error red - static const Color error = Color(0xFFEF4444); - - /// Info blue - static const Color info = Color(0xFF3B82F6); - - // ============ CATEGORY COLORS ============ - static const List categoryColors = [ - Color(0xFF6366F1), // Indigo - Color(0xFFEC4899), // Pink - Color(0xFF8B5CF6), // Purple - Color(0xFF14B8A6), // Teal - Color(0xFFF59E0B), // Amber - Color(0xFF10B981), // Emerald - ]; + // ============ BORDERS ============ + static const Color border = Color(0xFF333333); // ============ GLASSMORPHISM ============ - static const Color glassBackground = Color(0x1AFFFFFF); - static const Color glassBorder = Color(0x33FFFFFF); - static const Color glassBackgroundLight = Color(0x40FFFFFF); - static const Color glassBorderLight = Color(0x20000000); + static final Color glassBackground = const Color(0xFF1E1E1E).withOpacity(0.7); + static final Color glassBorder = const Color(0xFFFFFFFF).withOpacity(0.1); - // ============ DARK THEME COLOR SCHEME ============ + // ============ CATEGORY COLORS (MUTED) ============ + static const List categoryColors = [ + Color(0xFF5C6BC0), // Indigo + Color(0xFFAB47BC), // Purple + Color(0xFFEF5350), // Red + Color(0xFF26A69A), // Teal + Color(0xFFFFA726), // Orange + Color(0xFF78909C), // Blue Grey + ]; + + // ============ THEME SCHEMES ============ static ColorScheme get darkColorScheme => const ColorScheme.dark( primary: primary, - secondary: secondary, - surface: surfaceDark, + secondary: accent, + surface: surface, + background: background, error: error, - onPrimary: Color(0xFF000000), - onSecondary: Color(0xFF000000), - onSurface: textPrimaryDark, - onError: Color(0xFFFFFFFF), + onPrimary: Colors.white, + onSecondary: Colors.white, + onSurface: textPrimary, + onError: Colors.white, + brightness: Brightness.dark, ); - // ============ LIGHT THEME COLOR SCHEME ============ + // Minimalist Light Scheme (if ever toggled) static ColorScheme get lightColorScheme => const ColorScheme.light( primary: primary, - secondary: secondary, - surface: surfaceLight, + secondary: accent, + surface: Color(0xFFF5F5F5), + background: Colors.white, error: error, - onPrimary: Color(0xFFFFFFFF), - onSecondary: Color(0xFFFFFFFF), - onSurface: textPrimaryLight, - onError: Color(0xFFFFFFFF), + onPrimary: Colors.white, + onSecondary: Colors.black, + onSurface: Color(0xFF121212), + onError: Colors.white, + brightness: Brightness.light, ); } - diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index f3a60cc..f28350c 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -2,38 +2,28 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'app_colors.dart'; -/// XtremFlow Modern Theme +/// XtremFlow Cinematic Minimalist Theme /// -/// Complete theme with typography, spacing, and component theming +/// Refined, content-first theme. Less noise, more focus. class AppTheme { AppTheme._(); - // ============ SPACING SCALE ============ + // ============ SPACING ============ static const double spacing4 = 4.0; static const double spacing8 = 8.0; static const double spacing12 = 12.0; static const double spacing16 = 16.0; static const double spacing24 = 24.0; static const double spacing32 = 32.0; - static const double spacing48 = 48.0; - // ============ BORDER RADIUS ============ - static const double radiusSm = 8.0; - static const double radiusMd = 12.0; - static const double radiusLg = 16.0; - static const double radiusXl = 24.0; + // ============ RADIUS (Tighter for professional look) ============ + static const double radiusSm = 6.0; + static const double radiusMd = 8.0; + static const double radiusLg = 12.0; + static const double radiusXl = 16.0; static const double radiusFull = 999.0; - // ============ ANIMATION DURATIONS ============ - static const Duration durationFast = Duration(milliseconds: 150); - static const Duration durationNormal = Duration(milliseconds: 250); - static const Duration durationSlow = Duration(milliseconds: 400); - - // ============ ANIMATION CURVES ============ - static const Curve curveDefault = Curves.easeOutCubic; - static const Curve curveEmphasized = Curves.easeInOutCubic; - - // ============ DARK THEME ============ + // ============ DARK THEME (Main) ============ static ThemeData get darkTheme { final baseTheme = ThemeData.dark(useMaterial3: true); @@ -41,66 +31,60 @@ class AppTheme { colorScheme: AppColors.darkColorScheme, scaffoldBackgroundColor: AppColors.background, - // AppBar + // AppBar: Minimalist, blends with background appBarTheme: AppBarTheme( - backgroundColor: AppColors.surface, + backgroundColor: AppColors.background, // Seamless surfaceTintColor: Colors.transparent, elevation: 0, centerTitle: false, + iconTheme: const IconThemeData(color: AppColors.textPrimary), titleTextStyle: GoogleFonts.inter( fontSize: 20, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), - iconTheme: const IconThemeData(color: AppColors.textPrimary), ), - // Navigation Bar (bottom) + // Navigation: Simple, functional navigationBarTheme: NavigationBarThemeData( - backgroundColor: AppColors.surface, + backgroundColor: AppColors.background, surfaceTintColor: Colors.transparent, - indicatorColor: AppColors.primary.withOpacity(0.2), + 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, - ); - } + final isSelected = states.contains(WidgetState.selected); return GoogleFonts.inter( fontSize: 12, - fontWeight: FontWeight.w500, - color: AppColors.textSecondary, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, + color: isSelected ? AppColors.textPrimary : AppColors.textSecondary, ); }), iconTheme: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.selected)) { - return const IconThemeData(color: AppColors.primary); - } - return const IconThemeData(color: AppColors.textSecondary); + final isSelected = states.contains(WidgetState.selected); + return IconThemeData( + color: isSelected ? AppColors.textPrimary : AppColors.textSecondary, + ); }), ), - // Navigation Rail (desktop sidebar) + // NavigationRail (Desktop) navigationRailTheme: NavigationRailThemeData( - backgroundColor: AppColors.surface, - indicatorColor: AppColors.primary.withOpacity(0.2), - selectedIconTheme: const IconThemeData(color: AppColors.primary), + backgroundColor: AppColors.background, + indicatorColor: AppColors.primary.withOpacity(0.15), + selectedIconTheme: const IconThemeData(color: AppColors.textPrimary), unselectedIconTheme: const IconThemeData(color: AppColors.textSecondary), selectedLabelTextStyle: GoogleFonts.inter( - fontSize: 12, + color: AppColors.textPrimary, fontWeight: FontWeight.w600, - color: AppColors.primary, + fontSize: 12, ), unselectedLabelTextStyle: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.w500, color: AppColors.textSecondary, + fontWeight: FontWeight.w500, + fontSize: 12, ), ), - // Cards + // Cards: Flat, subtle border cardTheme: CardThemeData( color: AppColors.surface, surfaceTintColor: Colors.transparent, @@ -112,10 +96,11 @@ class AppTheme { margin: EdgeInsets.zero, ), - // Inputs + // Inputs: Clean, filled inputDecorationTheme: InputDecorationTheme( filled: true, fillColor: AppColors.surfaceVariant, + contentPadding: const EdgeInsets.all(spacing16), border: OutlineInputBorder( borderRadius: BorderRadius.circular(radiusSm), borderSide: const BorderSide(color: AppColors.border), @@ -126,498 +111,83 @@ class AppTheme { ), 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.textDisabled, - fontSize: 14, - ), - labelStyle: GoogleFonts.inter( - color: AppColors.textSecondary, - fontSize: 14, + borderSide: const BorderSide(color: AppColors.primary), ), + hintStyle: GoogleFonts.inter(color: AppColors.textTertiary), + labelStyle: GoogleFonts.inter(color: AppColors.textSecondary), ), - // Buttons - filledButtonTheme: FilledButtonThemeData( - style: FilledButton.styleFrom( - backgroundColor: AppColors.primary, - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric( - horizontal: spacing24, - vertical: spacing12, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(radiusSm), - ), - textStyle: GoogleFonts.inter( - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - ), - - outlinedButtonTheme: OutlinedButtonThemeData( - style: OutlinedButton.styleFrom( - foregroundColor: AppColors.primary, - side: const BorderSide(color: AppColors.primary), - padding: const EdgeInsets.symmetric( - horizontal: spacing24, - vertical: spacing12, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(radiusSm), - ), - textStyle: GoogleFonts.inter( - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - ), - - textButtonTheme: TextButtonThemeData( - style: TextButton.styleFrom( - foregroundColor: AppColors.primary, - textStyle: GoogleFonts.inter( - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - ), - - // Chips - chipTheme: ChipThemeData( - backgroundColor: AppColors.surfaceVariant, - selectedColor: AppColors.primary.withOpacity(0.2), - labelStyle: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.w500, - color: AppColors.textPrimary, - ), - side: const BorderSide(color: AppColors.border), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(radiusFull), - ), - padding: const EdgeInsets.symmetric(horizontal: spacing8), - ), - - // Dialogs - dialogTheme: DialogThemeData( - backgroundColor: AppColors.surface, - surfaceTintColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(radiusLg), - ), - titleTextStyle: GoogleFonts.inter( - fontSize: 18, - fontWeight: FontWeight.w600, - color: AppColors.textPrimary, - ), - contentTextStyle: GoogleFonts.inter( - fontSize: 14, - color: AppColors.textSecondary, - ), - ), - - // Snackbars - snackBarTheme: SnackBarThemeData( - backgroundColor: AppColors.surfaceVariant, - contentTextStyle: GoogleFonts.inter( - fontSize: 14, - color: AppColors.textPrimary, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(radiusSm), - ), - behavior: SnackBarBehavior.floating, - ), - - // Tabs - tabBarTheme: TabBarThemeData( - labelColor: AppColors.primary, - unselectedLabelColor: AppColors.textSecondary, - labelStyle: GoogleFonts.inter( - fontSize: 14, - fontWeight: FontWeight.w600, - ), - unselectedLabelStyle: GoogleFonts.inter( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - indicatorColor: AppColors.primary, - indicatorSize: TabBarIndicatorSize.tab, - dividerColor: AppColors.border, - ), - - // Dividers - dividerTheme: const DividerThemeData( - color: AppColors.border, - 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.textPrimary, - ), - subtitleTextStyle: GoogleFonts.inter( - fontSize: 12, - color: AppColors.textSecondary, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(radiusSm), - ), - ), - - // Progress indicators - progressIndicatorTheme: const ProgressIndicatorThemeData( - color: AppColors.primary, - linearTrackColor: AppColors.surfaceVariant, - circularTrackColor: AppColors.surfaceVariant, - ), - - // Switch - switchTheme: SwitchThemeData( - thumbColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.selected)) { - return AppColors.primary; - } - return AppColors.textSecondary; - }), - trackColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.selected)) { - return AppColors.primary.withOpacity(0.3); - } - return AppColors.surfaceVariant; - }), - ), - - // Text theme - textTheme: TextTheme( - displayLarge: GoogleFonts.inter( - fontSize: 57, - fontWeight: FontWeight.w400, - color: AppColors.textPrimary, - ), - displayMedium: GoogleFonts.inter( - fontSize: 45, - fontWeight: FontWeight.w400, - color: AppColors.textPrimary, - ), - displaySmall: GoogleFonts.inter( - fontSize: 36, - fontWeight: FontWeight.w400, - color: AppColors.textPrimary, - ), - headlineLarge: GoogleFonts.inter( - fontSize: 32, - fontWeight: FontWeight.w600, - color: AppColors.textPrimary, - ), - headlineMedium: GoogleFonts.inter( - fontSize: 28, - fontWeight: FontWeight.w600, - color: AppColors.textPrimary, - ), - headlineSmall: GoogleFonts.inter( - fontSize: 24, - fontWeight: FontWeight.w600, - color: AppColors.textPrimary, - ), - titleLarge: GoogleFonts.inter( - fontSize: 22, - fontWeight: FontWeight.w600, - color: AppColors.textPrimary, - ), - titleMedium: GoogleFonts.inter( - fontSize: 16, - fontWeight: FontWeight.w600, - color: AppColors.textPrimary, - ), - titleSmall: GoogleFonts.inter( - fontSize: 14, - fontWeight: FontWeight.w600, - color: AppColors.textPrimary, - ), - bodyLarge: GoogleFonts.inter( - fontSize: 16, - fontWeight: FontWeight.w400, - color: AppColors.textPrimary, - ), - bodyMedium: GoogleFonts.inter( - fontSize: 14, - fontWeight: FontWeight.w400, - color: AppColors.textPrimary, - ), - bodySmall: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.w400, - color: AppColors.textSecondary, - ), - labelLarge: GoogleFonts.inter( - fontSize: 14, - fontWeight: FontWeight.w600, - color: AppColors.textPrimary, - ), - labelMedium: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.w500, - color: AppColors.textPrimary, - ), - labelSmall: GoogleFonts.inter( - fontSize: 11, - fontWeight: FontWeight.w500, - color: AppColors.textSecondary, - ), - ), - ); - } - - // ============ 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 + // Buttons: Sharp, reliable 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, + textStyle: GoogleFonts.inter(fontWeight: FontWeight.w600), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + ), + ), + + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: AppColors.textPrimary, + side: const BorderSide(color: AppColors.border), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(radiusSm), ), + textStyle: GoogleFonts.inter(fontWeight: FontWeight.w600), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), ), ), - - // 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 + + // Text 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, - ), + displayLarge: GoogleFonts.inter(color: AppColors.textPrimary, fontWeight: FontWeight.bold), + displayMedium: GoogleFonts.inter(color: AppColors.textPrimary, fontWeight: FontWeight.bold), + headlineMedium: GoogleFonts.inter(color: AppColors.textPrimary, fontWeight: FontWeight.w600), + titleLarge: GoogleFonts.inter(color: AppColors.textPrimary, fontWeight: FontWeight.w600), + titleMedium: GoogleFonts.inter(color: AppColors.textPrimary, fontWeight: FontWeight.w500), + bodyLarge: GoogleFonts.inter(color: AppColors.textSecondary, fontSize: 16), + bodyMedium: GoogleFonts.inter(color: AppColors.textSecondary, fontSize: 14), ), ); } -} + // ============ LIGHT THEME (Minimalist) ============ + static ThemeData get lightTheme { + final baseTheme = ThemeData.light(useMaterial3: true); + final colors = AppColors.lightColorScheme; + + return baseTheme.copyWith( + colorScheme: colors, + scaffoldBackgroundColor: colors.background, + appBarTheme: AppBarTheme( + backgroundColor: colors.background, + foregroundColor: colors.onSurface, + elevation: 0, + centerTitle: false, + iconTheme: IconThemeData(color: colors.onSurface), + titleTextStyle: GoogleFonts.inter( + fontSize: 20, + fontWeight: FontWeight.w600, + color: colors.onSurface, + ), + ), + cardTheme: CardThemeData( + color: colors.surface, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(radiusMd), + side: BorderSide(color: Colors.grey.shade300), + ), + ), + textTheme: TextTheme( + bodyLarge: GoogleFonts.inter(color: colors.onSurface), + bodyMedium: GoogleFonts.inter(color: colors.onSurfaceVariant), + ), + // Can expand light theme details later if needed + ); + } +} diff --git a/lib/features/auth/screens/login_screen.dart b/lib/features/auth/screens/login_screen.dart index 17f8eb0..dabf715 100644 --- a/lib/features/auth/screens/login_screen.dart +++ b/lib/features/auth/screens/login_screen.dart @@ -45,241 +45,210 @@ class _LoginScreenState extends ConsumerState { final authState = ref.watch(authProvider); return Scaffold( - body: Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Color(0xFF0F1923), // Deep dark blue - AppColors.background, - Color(0xFF0A1628), // Navy accent - ], - stops: [0.0, 0.5, 1.0], + backgroundColor: AppColors.background, + body: Stack( + children: [ + // Ambient background glow (Subtle) + Positioned( + top: -100, + left: -100, + child: Container( + width: 500, + height: 500, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + AppColors.primary.withOpacity(0.15), + Colors.transparent, + ], + radius: 0.6, + ), + ), + ), ), - ), - child: Stack( - children: [ - // Glow effect top - Positioned( - top: -100, - left: -100, + + // Main content + Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24.0), child: Container( - width: 300, - height: 300, - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: RadialGradient( - colors: [ - AppColors.primary.withOpacity(0.1), - Colors.transparent, - ], - ), - ), - ), - ), - // Glow effect bottom - Positioned( - bottom: -150, - right: -150, - child: Container( - width: 400, - height: 400, - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: RadialGradient( - colors: [ - AppColors.secondary.withOpacity(0.08), - Colors.transparent, - ], - ), - ), - ), - ), - // Main content - Center( - child: SingleChildScrollView( - padding: const EdgeInsets.all(24.0), - child: Container( - constraints: const BoxConstraints(maxWidth: 400), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Logo and title - Container( - width: 80, - height: 80, - decoration: BoxDecoration( - gradient: AppColors.primaryGradient, - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: AppColors.primary.withOpacity(0.4), - blurRadius: 30, - offset: const Offset(0, 10), + constraints: const BoxConstraints(maxWidth: 400), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Logo and title + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColors.border), + boxShadow: [ + BoxShadow( + color: AppColors.primary.withOpacity(0.2), + blurRadius: 30, + offset: const Offset(0, 10), + ), + ], + ), + child: const Icon( + Icons.live_tv, + color: AppColors.primary, + size: 40, + ), + ), + const SizedBox(height: 24), + const Text( + 'XtremFlow', + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.w800, + color: AppColors.textPrimary, + letterSpacing: -0.5, + ), + ), + const SizedBox(height: 8), + const Text( + 'Cinematic Experience', + style: TextStyle( + fontSize: 14, + color: AppColors.textSecondary, + letterSpacing: 2, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 48), + // Login form + GlassCard( + padding: const EdgeInsets.all(32), + borderRadius: 16, + showBorder: true, + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'Welcome Back', + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + TextFormField( + controller: _usernameController, + style: const TextStyle(color: AppColors.textPrimary), + decoration: InputDecoration( + labelText: 'Username', + prefixIcon: const Icon(Icons.person_outline), + fillColor: AppColors.surface, + hoverColor: AppColors.surfaceLight, + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Username required'; + } + return null; + }, + autofocus: true, + textInputAction: TextInputAction.next, + ), + const SizedBox(height: 16), + TextFormField( + controller: _passwordController, + obscureText: _obscurePassword, + style: const TextStyle(color: AppColors.textPrimary), + decoration: InputDecoration( + labelText: 'Password', + prefixIcon: const Icon(Icons.lock_outline), + fillColor: AppColors.surface, + suffixIcon: IconButton( + icon: Icon( + _obscurePassword + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + ), + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Password required'; + } + return null; + }, + textInputAction: TextInputAction.done, + onFieldSubmitted: (_) => _handleLogin(), + ), + if (authState.errorMessage != null) ...[ + const SizedBox(height: 24), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.error.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: AppColors.error.withOpacity(0.3), + ), + ), + child: Row( + children: [ + Icon( + Icons.error_outline, + color: AppColors.error, + size: 20, + ), + const SizedBox(height: 8), + Expanded( + child: Text( + authState.errorMessage!, + style: const TextStyle( + color: AppColors.error, + fontSize: 13, + ), + ), + ), + ], + ), + ), + ], + const SizedBox(height: 32), + GradientButton( + label: 'Sign In', + icon: Icons.arrow_forward, + isLoading: authState.isLoading, + isExpanded: true, + onPressed: _handleLogin, ), ], ), - child: const Icon( - Icons.live_tv, - color: Colors.black, - size: 40, - ), ), - const SizedBox(height: 24), - ShaderMask( - shaderCallback: (bounds) => - AppColors.primaryGradient.createShader(bounds), - child: const Text( - 'XtremFlow', - style: TextStyle( - fontSize: 36, - fontWeight: FontWeight.w800, - color: Colors.white, - ), - ), + ), + const SizedBox(height: 24), + const Text( + 'Default: admin / admin', + style: TextStyle( + fontSize: 12, + color: AppColors.textTertiary, + fontStyle: FontStyle.italic, ), - const SizedBox(height: 8), - Text( - 'Streaming IPTV Premium', - style: TextStyle( - fontSize: 14, - color: AppColors.textSecondary, - letterSpacing: 1, - ), - ), - const SizedBox(height: 48), - // Login form - GlassCard( - padding: const EdgeInsets.all(24), - borderRadius: AppTheme.radiusLg, - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - 'Connexion', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, - color: AppColors.textPrimary, - ), - ), - const SizedBox(height: 24), - TextFormField( - controller: _usernameController, - decoration: InputDecoration( - labelText: 'Nom d\'utilisateur', - prefixIcon: Icon( - Icons.person_outline, - color: AppColors.textSecondary, - ), - ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Nom d\'utilisateur requis'; - } - return null; - }, - autofocus: true, - textInputAction: TextInputAction.next, - ), - const SizedBox(height: 16), - TextFormField( - controller: _passwordController, - obscureText: _obscurePassword, - decoration: InputDecoration( - labelText: 'Mot de passe', - prefixIcon: Icon( - Icons.lock_outline, - color: AppColors.textSecondary, - ), - suffixIcon: IconButton( - icon: Icon( - _obscurePassword - ? Icons.visibility_outlined - : Icons.visibility_off_outlined, - color: AppColors.textSecondary, - ), - onPressed: () { - setState(() { - _obscurePassword = !_obscurePassword; - }); - }, - ), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Mot de passe requis'; - } - return null; - }, - textInputAction: TextInputAction.done, - onFieldSubmitted: (_) => _handleLogin(), - ), - if (authState.errorMessage != null) ...[ - const SizedBox(height: 16), - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: AppColors.error.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: AppColors.error.withOpacity(0.3), - ), - ), - child: Row( - children: [ - Icon( - Icons.error_outline, - color: AppColors.error, - size: 20, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - authState.errorMessage!, - style: TextStyle( - color: AppColors.error, - fontSize: 13, - ), - ), - ), - ], - ), - ), - ], - const SizedBox(height: 24), - GradientButton( - label: 'Se connecter', - icon: Icons.login, - isLoading: authState.isLoading, - isExpanded: true, - onPressed: _handleLogin, - ), - ], - ), - ), - ), - const SizedBox(height: 24), - Text( - 'Identifiants par défaut: admin / admin', - style: TextStyle( - fontSize: 12, - color: AppColors.textDisabled, - fontStyle: FontStyle.italic, - ), - textAlign: TextAlign.center, - ), - ], - ), + textAlign: TextAlign.center, + ), + ], ), ), ), - ], - ), + ), + ], ), ); + } } diff --git a/lib/features/iptv/providers/favorites_provider.dart b/lib/features/iptv/providers/favorites_provider.dart new file mode 100644 index 0000000..04b35c0 --- /dev/null +++ b/lib/features/iptv/providers/favorites_provider.dart @@ -0,0 +1,35 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// Manages the list of favorite channel IDs +class FavoritesNotifier extends StateNotifier> { + FavoritesNotifier() : super([]) { + _loadFavorites(); + } + + static const String _key = 'xtremflow_favorites'; + + Future _loadFavorites() async { + final prefs = await SharedPreferences.getInstance(); + final favorites = prefs.getStringList(_key) ?? []; + state = favorites; + } + + Future toggleFavorite(String streamId) async { + final prefs = await SharedPreferences.getInstance(); + if (state.contains(streamId)) { + state = state.where((id) => id != streamId).toList(); + } else { + state = [...state, streamId]; + } + await prefs.setStringList(_key, state); + } + + bool isFavorite(String streamId) { + return state.contains(streamId); + } +} + +final favoritesProvider = StateNotifierProvider>((ref) { + return FavoritesNotifier(); +}); diff --git a/lib/features/iptv/widgets/channel_card.dart b/lib/features/iptv/widgets/channel_card.dart index ecbb3bc..3175fee 100644 --- a/lib/features/iptv/widgets/channel_card.dart +++ b/lib/features/iptv/widgets/channel_card.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/theme/app_colors.dart'; +import '../providers/favorites_provider.dart'; /// Premium channel card with Netflix-style design /// @@ -9,8 +11,10 @@ import '../../../core/theme/app_colors.dart'; /// - Thumbnail/logo with fallback /// - Live badge with pulse animation /// - EPG overlay +/// - Favorite toggle /// - Hover/tap effects -class ChannelCard extends StatefulWidget { +class ChannelCard extends ConsumerStatefulWidget { + final String streamId; final String name; final String? iconUrl; final String? currentProgram; @@ -21,6 +25,7 @@ class ChannelCard extends StatefulWidget { const ChannelCard({ super.key, + required this.streamId, required this.name, this.iconUrl, this.currentProgram, @@ -31,10 +36,10 @@ class ChannelCard extends StatefulWidget { }); @override - State createState() => _ChannelCardState(); + ConsumerState createState() => _ChannelCardState(); } -class _ChannelCardState extends State +class _ChannelCardState extends ConsumerState with SingleTickerProviderStateMixin { bool _isHovered = false; late AnimationController _pulseController; @@ -61,9 +66,9 @@ class _ChannelCardState extends State @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final isDark = theme.brightness == Brightness.dark; - + final favorites = ref.watch(favoritesProvider); + final isFavorite = favorites.contains(widget.streamId); + return MouseRegion( onEnter: (_) => setState(() => _isHovered = true), onExit: (_) => setState(() => _isHovered = false), @@ -77,49 +82,31 @@ class _ChannelCardState extends State 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), - ], - ), + borderRadius: BorderRadius.circular(8), + color: AppColors.surface, // Clean flat dark surface border: Border.all( color: _isHovered - ? AppColors.primary.withOpacity(0.5) - : (isDark ? AppColors.borderDark : AppColors.borderLight), - width: _isHovered ? 1.5 : 1, + ? AppColors.primary + : AppColors.border, + width: _isHovered ? 2 : 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, + color: AppColors.primary.withOpacity(0.3), + blurRadius: 16, offset: const Offset(0, 4), ), - ], + ] + : [], ), child: ClipRRect( - borderRadius: BorderRadius.circular(11), + borderRadius: BorderRadius.circular(7), // Inner radius child: Stack( fit: StackFit.expand, children: [ // Channel logo/thumbnail - _buildChannelImage(isDark), + _buildChannelImage(), // Gradient overlay for text readability Positioned.fill( @@ -129,10 +116,10 @@ class _ChannelCardState extends State begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ - Colors.transparent, - Colors.black.withOpacity(0.7), + Colors.black.withOpacity(0.2), + Colors.black.withOpacity(0.9), ], - stops: const [0.4, 1.0], + stops: const [0.0, 1.0], ), ), ), @@ -145,8 +132,24 @@ class _ChannelCardState extends State right: 8, child: _buildLiveBadge(), ), + + // Favorite Icon + Positioned( + top: 0, + left: 0, + child: IconButton( + icon: Icon( + isFavorite ? Icons.favorite : Icons.favorite_border, + size: 20, + color: isFavorite ? AppColors.primary : Colors.white70, + ), + onPressed: () { + ref.read(favoritesProvider.notifier).toggleFavorite(widget.streamId); + }, + ), + ), - // Channel info overlay + // Channel info Positioned( left: 10, right: 10, @@ -159,26 +162,20 @@ class _ChannelCardState extends State widget.name, style: const TextStyle( color: Colors.white, - fontSize: 13, - fontWeight: FontWeight.w600, - shadows: [ - Shadow( - color: Colors.black54, - blurRadius: 4, - ), - ], + fontSize: 14, + fontWeight: FontWeight.w700, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), - if (widget.currentProgram != null) ...[ - const SizedBox(height: 2), + if (widget.currentProgram != null && widget.currentProgram!.isNotEmpty) ...[ + const SizedBox(height: 4), Text( widget.currentProgram!, style: TextStyle( - color: Colors.white.withOpacity(0.8), + color: Colors.white.withOpacity(0.7), fontSize: 11, - fontWeight: FontWeight.w400, + fontWeight: FontWeight.w500, ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -195,26 +192,26 @@ class _ChannelCardState extends State ); } - Widget _buildChannelImage(bool isDark) { + Widget _buildChannelImage() { 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), + fit: BoxFit.contain, // Contain usually looks better for channel logos + placeholder: (context, url) => _buildPlaceholder(), + errorWidget: (context, url, error) => _buildPlaceholder(), ); } - return _buildPlaceholder(isDark); + return _buildPlaceholder(); } - Widget _buildPlaceholder(bool isDark) { + Widget _buildPlaceholder() { return Container( - color: isDark ? AppColors.surfaceVariantDark : AppColors.surfaceVariantLight, + color: AppColors.surfaceVariant, child: Center( child: Icon( - Icons.live_tv_rounded, - size: 36, - color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, + Icons.tv, + size: 32, + color: AppColors.textTertiary, ), ), ); @@ -225,40 +222,21 @@ class _ChannelCardState extends State animation: _pulseAnimation, builder: (context, child) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), 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: child, ); }, - 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, - ), - ), - ], + child: const Text( + 'LIVE', + style: TextStyle( + color: Colors.white, + fontSize: 9, + fontWeight: FontWeight.bold, + ), ), ); } diff --git a/lib/features/iptv/widgets/live_tv_tab.dart b/lib/features/iptv/widgets/live_tv_tab.dart index 7122bc5..698de21 100644 --- a/lib/features/iptv/widgets/live_tv_tab.dart +++ b/lib/features/iptv/widgets/live_tv_tab.dart @@ -1,17 +1,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:google_fonts/google_fonts.dart'; import '../providers/xtream_provider.dart'; import '../providers/settings_provider.dart'; +import '../providers/favorites_provider.dart'; import '../screens/player_screen.dart'; import '../../../core/models/iptv_models.dart'; import '../../../core/models/playlist_config.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/utils/responsive_layout.dart'; -import 'epg_widget.dart'; +import 'channel_card.dart'; -/// Live TV tab with category box grid navigation +/// Live TV tab - Enhanced with Search, Favorites and Grid/List View class LiveTVTab extends ConsumerStatefulWidget { final PlaylistConfig playlist; @@ -23,9 +23,34 @@ class LiveTVTab extends ConsumerStatefulWidget { class _LiveTVTabState extends ConsumerState with AutomaticKeepAliveClientMixin { + + // State String? _selectedCategory; int _currentPage = 0; - static const int _itemsPerPage = 50; + static const int _itemsPerPage = 100; // Increased for grid view + + // UI State + bool _isGridView = true; + final TextEditingController _searchController = TextEditingController(); + bool _showFavoritesOnly = false; + String _searchQuery = ''; + + @override + void initState() { + super.initState(); + _searchController.addListener(() { + setState(() { + _searchQuery = _searchController.text.toLowerCase(); + _currentPage = 0; // Reset page on search + }); + }); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } @override bool get wantKeepAlive => true; @@ -35,391 +60,388 @@ class _LiveTVTabState extends ConsumerState super.build(context); final channelsAsync = ref.watch(liveChannelsByPlaylistProvider(widget.playlist)); + final favorites = ref.watch(favoritesProvider); - return channelsAsync.when( - data: (groupedChannels) { - if (groupedChannels.isEmpty) { - return const Center( - child: Text('No live channels available'), - ); - } + return Scaffold( // Use scaffold for toolbar body structure + backgroundColor: Colors.transparent, + body: Column( + children: [ + // Toolbar + _buildToolbar(), - // If a category is selected, show channels list - if (_selectedCategory != null && groupedChannels.containsKey(_selectedCategory)) { - return _buildChannelsList(groupedChannels[_selectedCategory]!); - } + // Content + Expanded( + child: channelsAsync.when( + data: (groupedChannels) { + // 1. Flatten for global search/filter if needed + final allChannels = groupedChannels.values.expand((element) => element).toList(); + + List displayedChannels = []; + bool isGlobalMode = _searchQuery.isNotEmpty || _showFavoritesOnly; - // Otherwise show category grid - return _buildCategoryGrid(groupedChannels); - }, - loading: () => const Center(child: CircularProgressIndicator()), - error: (error, stack) => Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.error_outline, size: 48, color: Colors.red), - const SizedBox(height: 16), - Text('Error loading channels: $error'), - ], - ), + if (isGlobalMode) { + // Global Filter Mode + displayedChannels = allChannels.where((channel) { + final matchesSearch = _searchQuery.isEmpty || + channel.name.toLowerCase().contains(_searchQuery); + final matchesFavorite = !_showFavoritesOnly || + favorites.contains(channel.streamId); + return matchesSearch && matchesFavorite; + }).toList(); + } else if (_selectedCategory != null) { + // Category Mode + displayedChannels = groupedChannels[_selectedCategory] ?? []; + } + + // If no global mode and no category selected, show Category Grid + if (!isGlobalMode && _selectedCategory == null) { + if (groupedChannels.isEmpty) { + return const Center(child: Text('No channels found')); + } + return _buildCategoryGrid(groupedChannels); + } + + // Show Channels (Grid or List) + if (displayedChannels.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.tv_off, size: 64, color: AppColors.textTertiary), + const SizedBox(height: 16), + Text( + _showFavoritesOnly ? 'No favorites found' : 'No channels found', + style: Theme.of(context).textTheme.bodyLarge, + ), + ], + ), + ); + } + + return _buildChannelsView(displayedChannels, isGlobalMode); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (e, s) => Center(child: Text('Error: $e')), + ), + ), + ], + ), + ); + } + + Widget _buildToolbar() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: AppColors.surface, + border: Border(bottom: BorderSide(color: AppColors.border)), + ), + child: Row( + children: [ + // Search Bar + Expanded( + child: TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: 'Search channels...', + prefixIcon: const Icon(Icons.search), + suffixIcon: _searchQuery.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear), + onPressed: () => _searchController.clear(), + ) + : null, + isDense: true, + filled: true, + fillColor: AppColors.surfaceVariant, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + ), + ), + ), + const SizedBox(width: 12), + + // Favorites Toggle + IconButton( + tooltip: 'Favorites Only', + icon: Icon( + _showFavoritesOnly ? Icons.favorite : Icons.favorite_border, + color: _showFavoritesOnly ? AppColors.primary : AppColors.textSecondary, + ), + onPressed: () { + setState(() { + _showFavoritesOnly = !_showFavoritesOnly; + _selectedCategory = null; // Reset category when toggling favorites + _currentPage = 0; + }); + }, + ), + + // View Toggle + IconButton( + tooltip: _isGridView ? 'List View' : 'Grid View', + icon: Icon( + _isGridView ? Icons.view_list : Icons.grid_view, + color: AppColors.textSecondary, + ), + onPressed: () => setState(() => _isGridView = !_isGridView), + ), + ], ), ); } - /// Build the category grid view with box cards Widget _buildCategoryGrid(Map> groupedChannels) { - // Get filter settings final settings = ref.watch(iptvSettingsProvider); - - // Filter categories based on keywords var categories = groupedChannels.keys.toList(); + + // Apply Settings Filter (Hidden categories) if (settings.liveTvKeywords.isNotEmpty) { categories = categories.where((cat) => settings.matchesLiveTvFilter(cat)).toList(); } categories.sort(); - if (categories.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.filter_list_off, size: 48, color: Colors.grey), - const SizedBox(height: 16), - Text( - 'No categories match the filter', - style: GoogleFonts.roboto(color: Colors.grey), - ), - const SizedBox(height: 8), - Text( - 'Filter: ${settings.liveTvCategoryFilter}', - style: GoogleFonts.roboto(fontSize: 12, color: Colors.grey.shade600), - ), - ], - ), - ); - } + final crossAxisCount = ResponsiveLayout.value( + context, + mobile: 2, + tablet: 3, + desktop: 5, + ); - // Responsive grid columns - final gridColumns = ResponsiveLayout.value( + return GridView.builder( + padding: const EdgeInsets.all(16), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + mainAxisSpacing: 16, + crossAxisSpacing: 16, + childAspectRatio: 1.5, + ), + itemCount: categories.length, + itemBuilder: (context, index) { + final category = categories[index]; + final count = groupedChannels[category]!.length; + + return InkWell( + onTap: () => setState(() { + _selectedCategory = category; + _currentPage = 0; + }), + borderRadius: BorderRadius.circular(AppTheme.radiusMd), + child: Container( + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(AppTheme.radiusMd), + border: Border.all(color: AppColors.border), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppColors.surfaceLight, + AppColors.surface, + ], + ), + ), + child: Stack( + children: [ + Positioned( + right: -10, + bottom: -10, + child: Icon( + Icons.tv, + size: 80, + color: Colors.white.withOpacity(0.03), + ), + ), + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + category, + style: GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.textPrimary, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: AppColors.primary.withOpacity(0.2), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '$count channels', + style: GoogleFonts.inter( + fontSize: 12, + color: AppColors.primary, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } + + Widget _buildChannelsView(List channels, bool isGlobalMode) { + // Pagination + final totalPages = (channels.length / _itemsPerPage).ceil(); + final startIndex = _currentPage * _itemsPerPage; + final endIndex = (startIndex + _itemsPerPage > channels.length) + ? channels.length + : startIndex + _itemsPerPage; + final pageChannels = channels.sublist(startIndex, endIndex); + + return Column( + children: [ + // Breadcrumb / Back Navigation + if (!isGlobalMode && _selectedCategory != null) + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + color: AppColors.surfaceVariant.withOpacity(0.5), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios, size: 16), + onPressed: () => setState(() => _selectedCategory = null), + tooltip: 'Back to Categories', + ), + Text( + _selectedCategory!, + style: GoogleFonts.inter( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + const Spacer(), + Text( + '${channels.length} items', + style: GoogleFonts.inter(color: AppColors.textSecondary), + ), + ], + ), + ), + + // Grid/List Content + Expanded( + child: _isGridView + ? _buildGridView(pageChannels) + : _buildListView(pageChannels), + ), + + // Pagination Controls + if (totalPages > 1) + Container( + height: 50, + decoration: BoxDecoration( + color: AppColors.surface, + border: Border(top: BorderSide(color: AppColors.border)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: const Icon(Icons.chevron_left), + onPressed: _currentPage > 0 ? () => setState(() => _currentPage--) : null, + ), + Text( + 'Page ${_currentPage + 1} of $totalPages', + style: GoogleFonts.inter(fontWeight: FontWeight.w500), + ), + IconButton( + icon: const Icon(Icons.chevron_right), + onPressed: _currentPage < totalPages - 1 ? () => setState(() => _currentPage++) : null, + ), + ], + ), + ), + ], + ); + } + + Widget _buildGridView(List channels) { + final columns = ResponsiveLayout.value( context, mobile: 2, tablet: 4, desktop: 6, ); - return Padding( - padding: const EdgeInsets.all(12.0), - child: GridView.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: gridColumns, - crossAxisSpacing: 12, - mainAxisSpacing: 12, - childAspectRatio: 1.6, - ), - itemCount: categories.length, - itemBuilder: (context, index) { - final category = categories[index]; - final channelCount = groupedChannels[category]!.length; - - return _CategoryBox( - categoryName: category, - channelCount: channelCount, - onTap: () { - setState(() { - _selectedCategory = category; - _currentPage = 0; - }); - }, - ); - }, + return GridView.builder( + padding: const EdgeInsets.all(16), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: columns, + mainAxisSpacing: 16, + crossAxisSpacing: 16, + childAspectRatio: 1.4, // Card ratio ), + itemCount: channels.length, + itemBuilder: (context, index) { + final channel = channels[index]; + return ChannelCard( + streamId: channel.streamId, // Now required + name: channel.name, + iconUrl: _getProxiedIconUrl(channel.streamIcon), + currentProgram: null, // ShortEPG requires async fetch + onTap: () => _playChannel(channel), + ); + }, ); } - /// Build channels list for selected category - Widget _buildChannelsList(List channels) { - final totalPages = (channels.length / _itemsPerPage).ceil(); - final startIndex = _currentPage * _itemsPerPage; - final endIndex = (startIndex + _itemsPerPage > channels.length) - ? channels.length - : startIndex + _itemsPerPage; - final paginatedChannels = channels.sublist(startIndex, endIndex); - - return Column( - children: [ - // Header with back button - Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - border: Border( - bottom: BorderSide( - color: Theme.of(context).dividerColor, - ), - ), + Widget _buildListView(List channels) { + return ListView.separated( + padding: const EdgeInsets.all(16), + itemCount: channels.length, + separatorBuilder: (_, __) => const SizedBox(height: 8), + itemBuilder: (context, index) { + final channel = channels[index]; + return SizedBox( + height: 80, // Fixed height for list item + child: ChannelCard( + streamId: channel.streamId, + name: channel.name, + iconUrl: _getProxiedIconUrl(channel.streamIcon), + height: 80, + width: double.infinity, // Full width + onTap: () => _playChannel(channel), ), - child: Row( - children: [ - IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () { - setState(() { - _selectedCategory = null; - _currentPage = 0; - }); - }, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - _selectedCategory ?? '', - style: GoogleFonts.roboto( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ), - Text( - '${channels.length} channels', - style: GoogleFonts.roboto( - fontSize: 14, - color: Colors.grey, - ), - ), - ], - ), - ), - - // Pagination controls - if (totalPages > 1) - Container( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: const Icon(Icons.chevron_left), - onPressed: _currentPage > 0 - ? () => setState(() => _currentPage--) - : null, - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(20), - ), - child: Text( - '${_currentPage + 1} / $totalPages', - style: GoogleFonts.roboto( - fontWeight: FontWeight.w500, - ), - ), - ), - IconButton( - icon: const Icon(Icons.chevron_right), - onPressed: _currentPage < totalPages - 1 - ? () => setState(() => _currentPage++) - : null, - ), - ], - ), - ), - - // Channels list - Expanded( - child: ListView.builder( - itemCount: paginatedChannels.length, - itemBuilder: (context, index) { - final channel = paginatedChannels[index]; - return _buildChannelTile(context, channel); - }, - ), - ), - ], + ); + }, ); } - Widget _buildChannelTile(BuildContext context, Channel channel) { - // Accept both HTTP and HTTPS icons - use proxy for HTTP - final bool hasIcon = channel.streamIcon.isNotEmpty; - String iconUrl = channel.streamIcon; - - // Proxy HTTP images through our server to avoid Mixed Content blocking - if (hasIcon && channel.streamIcon.startsWith('http://')) { - iconUrl = '/api/xtream/${channel.streamIcon}'; + String? _getProxiedIconUrl(String originalUrl) { + if (originalUrl.isEmpty) return null; + if (originalUrl.startsWith('http://')) { + return '/api/xtream/$originalUrl'; } - - final Widget placeholder = Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(8), - ), - child: const Icon(Icons.tv, color: Colors.white54), - ); + return originalUrl; + } - return Card( - margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - child: InkWell( - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => PlayerScreen( - streamId: channel.streamId, - title: channel.name, - playlist: widget.playlist, - streamType: StreamType.live, - ), - ), - ); - }, - borderRadius: BorderRadius.circular(8), - child: Padding( - padding: const EdgeInsets.all(8), - child: Row( - children: [ - // Channel icon - hasIcon - ? ClipRRect( - borderRadius: BorderRadius.circular(8), - child: CachedNetworkImage( - imageUrl: iconUrl, - width: 48, - height: 48, - fit: BoxFit.cover, - placeholder: (context, url) => placeholder, - errorWidget: (context, url, error) => placeholder, - ), - ) - : placeholder, - const SizedBox(width: 12), - - // Channel info + EPG - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - channel.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: GoogleFonts.roboto( - fontWeight: FontWeight.w500, - fontSize: 14, - ), - ), - const SizedBox(height: 4), - // EPG info - EPGWidget( - channelId: channel.streamId, - playlist: widget.playlist, - ), - ], - ), - ), - - // Play button - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - Icons.play_arrow, - color: Theme.of(context).colorScheme.primary, - ), - ), - ], - ), - ), - ), - ); - } -} - -/// Category box card widget -class _CategoryBox extends StatelessWidget { - final String categoryName; - final int channelCount; - final VoidCallback onTap; - - const _CategoryBox({ - required this.categoryName, - required this.channelCount, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return Material( - color: Colors.transparent, - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(10), - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Theme.of(context).colorScheme.primary.withOpacity(0.15), - Theme.of(context).colorScheme.secondary.withOpacity(0.08), - ], - ), - borderRadius: BorderRadius.circular(10), - border: Border.all( - color: Theme.of(context).colorScheme.primary.withOpacity(0.25), - width: 1, - ), - ), - child: Padding( - padding: const EdgeInsets.all(8), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.live_tv, - size: 16, - color: Theme.of(context).colorScheme.primary, - ), - const Spacer(), - Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary.withOpacity(0.2), - borderRadius: BorderRadius.circular(8), - ), - child: Text( - '$channelCount', - style: GoogleFonts.roboto( - fontSize: 10, - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - ], - ), - const Spacer(), - Text( - categoryName, - style: GoogleFonts.roboto( - fontSize: 11, - fontWeight: FontWeight.w600, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), + void _playChannel(Channel channel) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => PlayerScreen( + streamId: channel.streamId, + title: channel.name, + playlist: widget.playlist, + streamType: StreamType.live, ), ), ); diff --git a/lib/features/iptv/widgets/settings_tab.dart b/lib/features/iptv/widgets/settings_tab.dart index 323c533..7c3ddc6 100644 --- a/lib/features/iptv/widgets/settings_tab.dart +++ b/lib/features/iptv/widgets/settings_tab.dart @@ -200,7 +200,8 @@ class _SettingsTabState extends ConsumerState with SingleTickerProv required bool isSelected, required VoidCallback onTap, }) { - final isDark = Theme.of(context).brightness == Brightness.dark; + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; return InkWell( onTap: onTap, @@ -210,11 +211,11 @@ class _SettingsTabState extends ConsumerState with SingleTickerProv padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: isSelected - ? AppColors.primary.withOpacity(0.1) - : (isDark ? AppColors.surfaceVariantDark : AppColors.surfaceVariantLight), + ? colorScheme.primary.withOpacity(0.1) + : colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(12), border: Border.all( - color: isSelected ? AppColors.primary : Colors.transparent, + color: isSelected ? colorScheme.primary : Colors.transparent, width: 2, ), ), @@ -223,8 +224,8 @@ class _SettingsTabState extends ConsumerState with SingleTickerProv Icon( icon, color: isSelected - ? AppColors.primary - : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight), + ? colorScheme.primary + : colorScheme.onSurfaceVariant, ), const SizedBox(width: 12), Expanded( @@ -236,15 +237,15 @@ class _SettingsTabState extends ConsumerState with SingleTickerProv style: GoogleFonts.inter( fontWeight: FontWeight.w600, color: isSelected - ? AppColors.primary - : (isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight), + ? colorScheme.primary + : colorScheme.onSurface, ), ), Text( subtitle, style: GoogleFonts.inter( fontSize: 12, - color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, + color: colorScheme.onSurfaceVariant, ), ), ], @@ -253,7 +254,7 @@ class _SettingsTabState extends ConsumerState with SingleTickerProv if (isSelected) Icon( Icons.check_circle, - color: AppColors.primary, + color: colorScheme.primary, ), ], ), @@ -261,6 +262,7 @@ class _SettingsTabState extends ConsumerState with SingleTickerProv ); } + Widget _buildFiltersTab(BuildContext context, dynamic currentUser, IptvSettings settings) { return ListView( padding: const EdgeInsets.all(16), diff --git a/web/player.html b/web/player.html index 7a3e10c..c32b413 100644 --- a/web/player.html +++ b/web/player.html @@ -1,96 +1,98 @@ - - - IPTV Player - + #debug { + display: none; + /* Hidden in production */ + position: absolute; + bottom: 10px; + left: 10px; + color: #0f0; + font-size: 12px; + font-family: monospace; + background: rgba(0, 0, 0, 0.7); + padding: 8px; + border-radius: 4px; + max-width: 80%; + word-break: break-all; + } +