diff --git a/THEMING_GUIDE.md b/THEMING_GUIDE.md new file mode 100644 index 0000000..f8d3381 --- /dev/null +++ b/THEMING_GUIDE.md @@ -0,0 +1,189 @@ +# 🎨 FlexColorScheme Theming Guide + +This guide explains the enhanced theming system implemented using [FlexColorScheme](https://pub.dev/packages/flex_color_scheme) in the WeatherApp. + +## 🌟 Features + +### Dynamic Weather-Based Theming +- **Automatic theme switching** based on weather conditions +- **Multiple theme variants**: Default (Blue), Stormy (Purple), Sunny (Amber) +- **Material 3 design** with enhanced color schemes + +### Advanced Theming Capabilities +- **Surface blending** for depth and modern appearance +- **Consistent component theming** across all UI elements +- **Custom typography** using Google Fonts (Inter) +- **Responsive design** with proper visual density + +### User Controls +- **Theme mode switching**: Light, Dark, System +- **Manual theme variant selection** +- **Theme settings widget** for user preferences + +## 🏗️ Architecture + +### Theme Configuration (`app_theme.dart`) +```dart +class AppTheme { + static ThemeData get light => FlexThemeData.light( + scheme: FlexScheme.blueM3, + surfaceMode: FlexSurfaceMode.levelSurfacesLowScaffold, + blendLevel: 7, + fontFamily: GoogleFonts.inter().fontFamily, + // ... component configurations + ); +} +``` + +### Theme Management (`theme_provider.dart`) +- **Riverpod state management** for reactive theme changes +- **Automatic weather detection** and theme switching +- **Persistent theme preferences** (can be extended with storage) + +### Weather-Responsive Theming +The app automatically switches themes based on weather conditions: + +| Weather Condition | Theme Variant | Color Scheme | +|-------------------|---------------|-------------| +| Clear/Sunny | Sunny | Warm Amber | +| Rain/Storm/Thunder | Stormy | Deep Purple | +| General/Cloudy | Default | Cool Blue | + +## 📱 UI Components + +### Enhanced Weather Card +- **Gradient backgrounds** using theme colors +- **Adaptive icons** based on weather conditions +- **Information chips** for weather details +- **Material 3 elevation** and shadows + +### Theme Settings Widget +- **Interactive theme switching** +- **Visual theme mode indicators** +- **Weather theme explanations** +- **Real-time preview** of changes + +## 🎯 Key Improvements + +### Visual Enhancements +1. **Consistent Design Language**: All components follow Material 3 guidelines +2. **Enhanced Depth**: Surface blending creates visual hierarchy +3. **Better Contrast**: Improved readability in all lighting conditions +4. **Smooth Transitions**: Animated theme changes for better UX + +### User Experience +1. **Contextual Theming**: Weather-appropriate color schemes +2. **Accessibility**: High contrast ratios and proper touch targets +3. **Customization**: User control over theme preferences +4. **Consistency**: Unified theming across the entire app + +## 🔧 Implementation Details + +### FlexColorScheme Configuration +```dart +FlexThemeData.light( + scheme: FlexScheme.blueM3, + surfaceMode: FlexSurfaceMode.levelSurfacesLowScaffold, + blendLevel: 7, + subThemesData: const FlexSubThemesData( + cardElevation: 4, + cardRadius: 16, + elevatedButtonRadius: 12, + inputDecoratorRadius: 12, + // ... more configurations + ), + useMaterial3: true, +) +``` + +### Theme Provider Integration +```dart +@riverpod +class ThemeNotifier extends _$ThemeNotifier { + void autoSwitchThemeForWeather(String weatherCondition) { + // Automatic theme switching logic + } +} +``` + +## 🚀 Usage Examples + +### Basic Theme Integration +```dart +class MyApp extends StatelessWidget { + Widget build(BuildContext context) { + return Consumer(builder: (context, ref, _) { + final themeState = ref.watch(themeNotifierProvider); + + return MaterialApp.router( + theme: themeState.lightTheme, + darkTheme: themeState.darkTheme, + themeMode: themeState.materialThemeMode, + // ... + ); + }); + } +} +``` + +### Weather-Based Theme Switching +```dart +// Automatic switching in weather widgets +WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(themeNotifierProvider.notifier) + .autoSwitchThemeForWeather(weatherCondition); +}); +``` + +### Manual Theme Control +```dart +// In settings or theme selector +ref.read(themeNotifierProvider.notifier) + .changeThemeVariant(WeatherThemeVariant.stormy); +``` + +## 🎨 Color Schemes + +### Default Theme (Blue M3) +- **Primary**: Material 3 Blue +- **Use Case**: General weather conditions +- **Mood**: Professional, reliable + +### Stormy Theme (Deep Purple) +- **Primary**: Deep Purple shades +- **Use Case**: Rain, thunderstorms, severe weather +- **Mood**: Dramatic, powerful + +### Sunny Theme (Amber) +- **Primary**: Warm amber tones +- **Use Case**: Clear skies, sunny weather +- **Mood**: Cheerful, energetic + +## 🔮 Future Enhancements + +1. **Seasonal Themes**: Different color schemes for seasons +2. **Custom Color Schemes**: User-defined color palettes +3. **Animation Themes**: Weather-appropriate animations +4. **Accessibility Themes**: High contrast and colorblind-friendly options +5. **Time-based Theming**: Different themes for day/night + +## 📚 Resources + +- [FlexColorScheme Documentation](https://docs.flexcolorscheme.com/) +- [Material 3 Design System](https://m3.material.io/) +- [Flutter Theming Guide](https://docs.flutter.dev/ui/design/themes) +- [Riverpod State Management](https://riverpod.dev/) + +## 🤝 Contributing + +When contributing to the theming system: + +1. **Follow Material 3 guidelines** +2. **Test in both light and dark modes** +3. **Ensure accessibility compliance** +4. **Document new theme variants** +5. **Test with different weather conditions** + +--- + +**Note**: This theming system is designed to be extensible. New weather conditions, color schemes, and user preferences can be easily added by following the established patterns. \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index c25d5f6..90f71cd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:weather_app/src/feature/common/providers.dart'; +import 'package:weather_app/src/feature/theme/provider/theme_provider.dart'; void main() { runApp( @@ -14,13 +15,23 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return Consumer(builder: (context, ref, _) { + // Watch theme state for reactive theme changes + final themeState = ref.watch(themeNotifierProvider); + return MaterialApp.router( debugShowCheckedModeBanner: false, - theme: ThemeData(primarySwatch: Colors.blue), + title: 'Weather App', + + // Use FlexColorScheme themes + theme: themeState.lightTheme, + darkTheme: themeState.darkTheme, + themeMode: themeState.materialThemeMode, + + // Router configuration routerDelegate: ref.watch(appRouterProvider).delegate(), routeInformationParser: ref.watch(appRouterProvider).defaultRouteParser(), ); }); } -} +} \ No newline at end of file diff --git a/lib/src/core/theme/app_theme.dart b/lib/src/core/theme/app_theme.dart new file mode 100644 index 0000000..7da2edc --- /dev/null +++ b/lib/src/core/theme/app_theme.dart @@ -0,0 +1,216 @@ +import 'package:flex_color_scheme/flex_color_scheme.dart'; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +/// App theme configuration using FlexColorScheme for enhanced UI/UX +class AppTheme { + // Private constructor to prevent instantiation + const AppTheme._(); + + /// Light theme configuration + static ThemeData get light => FlexThemeData.light( + // Use a weather-appropriate color scheme (Sky blue theme) + scheme: FlexScheme.blueM3, + + // Enhanced surface blending for depth + surfaceMode: FlexSurfaceMode.levelSurfacesLowScaffold, + blendLevel: 7, + + // Custom font using Google Fonts + fontFamily: GoogleFonts.inter().fontFamily, + + // Component theme configurations + subThemesData: const FlexSubThemesData( + // Enhanced component styling + interactionEffects: true, + tintedDisabledControls: true, + + // Card styling for weather cards + cardElevation: 4, + cardRadius: 16, + + // App bar styling + appBarElevation: 2, + appBarTransparent: true, + + // Button styling + elevatedButtonRadius: 12, + filledButtonRadius: 12, + outlinedButtonRadius: 12, + + // Input decoration + inputDecoratorRadius: 12, + inputDecoratorBorderType: FlexInputBorderType.outline, + + // Bottom sheet + bottomSheetRadius: 20, + + // Dialog styling + dialogRadius: 16, + dialogElevation: 8, + + // Chip styling + chipRadius: 8, + + // Navigation bar + navigationBarElevation: 4, + navigationBarHeight: 70, + + // Use Material 3 error colors + useM3ErrorColors: true, + ), + + // Visual density for better touch targets + visualDensity: FlexColorScheme.comfortablePlatformDensity, + + // Use Material 3 + useMaterial3: true, + ); + + /// Dark theme configuration + static ThemeData get dark => FlexThemeData.dark( + // Use the same color scheme for consistency + scheme: FlexScheme.blueM3, + + // Enhanced surface blending for depth in dark mode + surfaceMode: FlexSurfaceMode.levelSurfacesLowScaffold, + blendLevel: 12, + + // Custom font using Google Fonts + fontFamily: GoogleFonts.inter().fontFamily, + + // Component theme configurations (same as light) + subThemesData: const FlexSubThemesData( + interactionEffects: true, + tintedDisabledControls: true, + + cardElevation: 4, + cardRadius: 16, + + appBarElevation: 2, + appBarTransparent: true, + + elevatedButtonRadius: 12, + filledButtonRadius: 12, + outlinedButtonRadius: 12, + + inputDecoratorRadius: 12, + inputDecoratorBorderType: FlexInputBorderType.outline, + + bottomSheetRadius: 20, + + dialogRadius: 16, + dialogElevation: 8, + + chipRadius: 8, + + navigationBarElevation: 4, + navigationBarHeight: 70, + + useM3ErrorColors: true, + ), + + visualDensity: FlexColorScheme.comfortablePlatformDensity, + useMaterial3: true, + ); + + /// Alternative weather-themed color scheme (Thunderstorm theme) + static ThemeData get stormyLight => FlexThemeData.light( + scheme: FlexScheme.deepPurple, + surfaceMode: FlexSurfaceMode.levelSurfacesLowScaffold, + blendLevel: 9, + fontFamily: GoogleFonts.inter().fontFamily, + subThemesData: const FlexSubThemesData( + interactionEffects: true, + cardElevation: 6, + cardRadius: 16, + appBarElevation: 2, + appBarTransparent: true, + elevatedButtonRadius: 12, + inputDecoratorRadius: 12, + bottomSheetRadius: 20, + dialogRadius: 16, + useM3ErrorColors: true, + ), + visualDensity: FlexColorScheme.comfortablePlatformDensity, + useMaterial3: true, + ); + + static ThemeData get stormyDark => FlexThemeData.dark( + scheme: FlexScheme.deepPurple, + surfaceMode: FlexSurfaceMode.levelSurfacesLowScaffold, + blendLevel: 15, + fontFamily: GoogleFonts.inter().fontFamily, + subThemesData: const FlexSubThemesData( + interactionEffects: true, + cardElevation: 6, + cardRadius: 16, + appBarElevation: 2, + appBarTransparent: true, + elevatedButtonRadius: 12, + inputDecoratorRadius: 12, + bottomSheetRadius: 20, + dialogRadius: 16, + useM3ErrorColors: true, + ), + visualDensity: FlexColorScheme.comfortablePlatformDensity, + useMaterial3: true, + ); + + /// Sunny weather theme + static ThemeData get sunnyLight => FlexThemeData.light( + scheme: FlexScheme.amber, + surfaceMode: FlexSurfaceMode.levelSurfacesLowScaffold, + blendLevel: 8, + fontFamily: GoogleFonts.inter().fontFamily, + subThemesData: const FlexSubThemesData( + interactionEffects: true, + cardElevation: 4, + cardRadius: 16, + appBarElevation: 2, + appBarTransparent: true, + elevatedButtonRadius: 12, + inputDecoratorRadius: 12, + bottomSheetRadius: 20, + dialogRadius: 16, + useM3ErrorColors: true, + ), + visualDensity: FlexColorScheme.comfortablePlatformDensity, + useMaterial3: true, + ); + + static ThemeData get sunnyDark => FlexThemeData.dark( + scheme: FlexScheme.amber, + surfaceMode: FlexSurfaceMode.levelSurfacesLowScaffold, + blendLevel: 10, + fontFamily: GoogleFonts.inter().fontFamily, + subThemesData: const FlexSubThemesData( + interactionEffects: true, + cardElevation: 4, + cardRadius: 16, + appBarElevation: 2, + appBarTransparent: true, + elevatedButtonRadius: 12, + inputDecoratorRadius: 12, + bottomSheetRadius: 20, + dialogRadius: 16, + useM3ErrorColors: true, + ), + visualDensity: FlexColorScheme.comfortablePlatformDensity, + useMaterial3: true, + ); +} + +/// Theme mode enumeration +enum AppThemeMode { + light, + dark, + system, +} + +/// Theme variant enumeration for weather-based theming +enum WeatherThemeVariant { + default_, // Default blue theme + stormy, // Purple theme for storms + sunny, // Amber theme for sunny weather +} \ No newline at end of file diff --git a/lib/src/feature/theme/provider/theme_provider.dart b/lib/src/feature/theme/provider/theme_provider.dart new file mode 100644 index 0000000..67c0e3f --- /dev/null +++ b/lib/src/feature/theme/provider/theme_provider.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:weather_app/src/core/theme/app_theme.dart'; + +part 'theme_provider.g.dart'; + +/// Theme state class to hold current theme configuration +class ThemeState { + final AppThemeMode themeMode; + final WeatherThemeVariant themeVariant; + final ThemeData lightTheme; + final ThemeData darkTheme; + + const ThemeState({ + required this.themeMode, + required this.themeVariant, + required this.lightTheme, + required this.darkTheme, + }); + + /// Get current theme mode for MaterialApp + ThemeMode get materialThemeMode { + switch (themeMode) { + case AppThemeMode.light: + return ThemeMode.light; + case AppThemeMode.dark: + return ThemeMode.dark; + case AppThemeMode.system: + return ThemeMode.system; + } + } + + ThemeState copyWith({ + AppThemeMode? themeMode, + WeatherThemeVariant? themeVariant, + ThemeData? lightTheme, + ThemeData? darkTheme, + }) { + return ThemeState( + themeMode: themeMode ?? this.themeMode, + themeVariant: themeVariant ?? this.themeVariant, + lightTheme: lightTheme ?? this.lightTheme, + darkTheme: darkTheme ?? this.darkTheme, + ); + } +} + +/// Theme provider for managing app themes +@riverpod +class ThemeNotifier extends _$ThemeNotifier { + @override + ThemeState build() { + return ThemeState( + themeMode: AppThemeMode.system, + themeVariant: WeatherThemeVariant.default_, + lightTheme: AppTheme.light, + darkTheme: AppTheme.dark, + ); + } + + /// Change theme mode (light, dark, system) + void changeThemeMode(AppThemeMode mode) { + state = state.copyWith(themeMode: mode); + } + + /// Change theme variant based on weather condition + void changeThemeVariant(WeatherThemeVariant variant) { + final (lightTheme, darkTheme) = _getThemesForVariant(variant); + + state = state.copyWith( + themeVariant: variant, + lightTheme: lightTheme, + darkTheme: darkTheme, + ); + } + + /// Get themes based on variant + (ThemeData, ThemeData) _getThemesForVariant(WeatherThemeVariant variant) { + switch (variant) { + case WeatherThemeVariant.default_: + return (AppTheme.light, AppTheme.dark); + case WeatherThemeVariant.stormy: + return (AppTheme.stormyLight, AppTheme.stormyDark); + case WeatherThemeVariant.sunny: + return (AppTheme.sunnyLight, AppTheme.sunnyDark); + } + } + + /// Auto-switch theme based on weather condition + void autoSwitchThemeForWeather(String weatherCondition) { + final condition = weatherCondition.toLowerCase(); + + WeatherThemeVariant variant = WeatherThemeVariant.default_; + + if (condition.contains('storm') || + condition.contains('thunder') || + condition.contains('rain') || + condition.contains('drizzle')) { + variant = WeatherThemeVariant.stormy; + } else if (condition.contains('clear') || + condition.contains('sunny') || + condition.contains('sun')) { + variant = WeatherThemeVariant.sunny; + } + + if (variant != state.themeVariant) { + changeThemeVariant(variant); + } + } + + /// Toggle between light and dark mode + void toggleThemeMode() { + switch (state.themeMode) { + case AppThemeMode.light: + changeThemeMode(AppThemeMode.dark); + break; + case AppThemeMode.dark: + changeThemeMode(AppThemeMode.system); + break; + case AppThemeMode.system: + changeThemeMode(AppThemeMode.light); + break; + } + } +} \ No newline at end of file diff --git a/lib/src/feature/theme/widget/theme_settings_widget.dart b/lib/src/feature/theme/widget/theme_settings_widget.dart new file mode 100644 index 0000000..f7eacfe --- /dev/null +++ b/lib/src/feature/theme/widget/theme_settings_widget.dart @@ -0,0 +1,182 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:weather_app/src/core/theme/app_theme.dart'; +import 'package:weather_app/src/feature/theme/provider/theme_provider.dart'; + +/// Theme settings widget for controlling app themes +class ThemeSettingsWidget extends ConsumerWidget { + const ThemeSettingsWidget({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final themeState = ref.watch(themeNotifierProvider); + final themeNotifier = ref.read(themeNotifierProvider.notifier); + + return Card( + margin: const EdgeInsets.all(16), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Icon( + Icons.palette, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 8), + Text( + 'Theme Settings', + style: Theme.of(context).textTheme.titleLarge, + ), + const Spacer(), + IconButton( + onPressed: () => themeNotifier.toggleThemeMode(), + icon: Icon(_getThemeIcon(themeState.themeMode)), + tooltip: 'Toggle Theme Mode', + ), + ], + ), + const SizedBox(height: 16), + + // Theme Mode Selection + Text( + 'Theme Mode', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: AppThemeMode.values.map((mode) { + final isSelected = themeState.themeMode == mode; + return FilterChip( + selected: isSelected, + label: Text(_getThemeModeName(mode)), + onSelected: (selected) { + if (selected) { + themeNotifier.changeThemeMode(mode); + } + }, + ); + }).toList(), + ), + + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 16), + + // Weather Theme Variant Selection + Text( + 'Weather Theme', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Column( + children: WeatherThemeVariant.values.map((variant) { + final isSelected = themeState.themeVariant == variant; + return RadioListTile( + title: Text(_getVariantName(variant)), + subtitle: Text(_getVariantDescription(variant)), + value: variant, + groupValue: themeState.themeVariant, + onChanged: (value) { + if (value != null) { + themeNotifier.changeThemeVariant(value); + } + }, + secondary: Icon(_getVariantIcon(variant)), + ); + }).toList(), + ), + + const SizedBox(height: 16), + + // Info text + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon( + Icons.info_outline, + size: 16, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Weather themes automatically switch based on current weather conditions', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + IconData _getThemeIcon(AppThemeMode mode) { + switch (mode) { + case AppThemeMode.light: + return Icons.light_mode; + case AppThemeMode.dark: + return Icons.dark_mode; + case AppThemeMode.system: + return Icons.auto_mode; + } + } + + String _getThemeModeName(AppThemeMode mode) { + switch (mode) { + case AppThemeMode.light: + return 'Light'; + case AppThemeMode.dark: + return 'Dark'; + case AppThemeMode.system: + return 'System'; + } + } + + String _getVariantName(WeatherThemeVariant variant) { + switch (variant) { + case WeatherThemeVariant.default_: + return 'Default'; + case WeatherThemeVariant.stormy: + return 'Stormy'; + case WeatherThemeVariant.sunny: + return 'Sunny'; + } + } + + String _getVariantDescription(WeatherThemeVariant variant) { + switch (variant) { + case WeatherThemeVariant.default_: + return 'Cool blue theme for general weather'; + case WeatherThemeVariant.stormy: + return 'Deep purple theme for storms and rain'; + case WeatherThemeVariant.sunny: + return 'Warm amber theme for sunny weather'; + } + } + + IconData _getVariantIcon(WeatherThemeVariant variant) { + switch (variant) { + case WeatherThemeVariant.default_: + return Icons.wb_cloudy; + case WeatherThemeVariant.stormy: + return Icons.thunderstorm; + case WeatherThemeVariant.sunny: + return Icons.wb_sunny; + } + } +} \ No newline at end of file diff --git a/lib/src/feature/weather/widget/enhanced_weather_card.dart b/lib/src/feature/weather/widget/enhanced_weather_card.dart new file mode 100644 index 0000000..d399ff6 --- /dev/null +++ b/lib/src/feature/weather/widget/enhanced_weather_card.dart @@ -0,0 +1,243 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:weather_app/src/feature/theme/provider/theme_provider.dart'; + +/// Enhanced weather card that adapts to FlexColorScheme theming +class EnhancedWeatherCard extends ConsumerWidget { + final String cityName; + final String temperature; + final String weatherCondition; + final String description; + final String iconCode; + final double? humidity; + final double? windSpeed; + final double? feelsLike; + final VoidCallback? onTap; + + const EnhancedWeatherCard({ + super.key, + required this.cityName, + required this.temperature, + required this.weatherCondition, + required this.description, + required this.iconCode, + this.humidity, + this.windSpeed, + this.feelsLike, + this.onTap, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final themeNotifier = ref.read(themeNotifierProvider.notifier); + + // Auto-switch theme based on weather condition + WidgetsBinding.instance.addPostFrameCallback((_) { + themeNotifier.autoSwitchThemeForWeather(weatherCondition); + }); + + return Card( + elevation: 8, + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + colorScheme.primaryContainer, + colorScheme.primaryContainer.withOpacity(0.7), + ], + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Header with city name and weather icon + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + cityName, + style: theme.textTheme.headlineSmall?.copyWith( + color: colorScheme.onPrimaryContainer, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + description, + style: theme.textTheme.bodyMedium?.copyWith( + color: colorScheme.onPrimaryContainer.withOpacity(0.8), + ), + ), + ], + ), + ), + // Weather icon placeholder (you can replace with actual icon) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: colorScheme.primary.withOpacity(0.2), + borderRadius: BorderRadius.circular(16), + ), + child: Icon( + _getWeatherIcon(weatherCondition), + size: 48, + color: colorScheme.primary, + ), + ), + ], + ), + + const SizedBox(height: 24), + + // Temperature display + Row( + children: [ + Text( + temperature, + style: theme.textTheme.displayMedium?.copyWith( + color: colorScheme.onPrimaryContainer, + fontWeight: FontWeight.w300, + ), + ), + const Spacer(), + if (feelsLike != null) + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + 'Feels like', + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.onPrimaryContainer.withOpacity(0.7), + ), + ), + Text( + '${feelsLike!.round()}°', + style: theme.textTheme.titleMedium?.copyWith( + color: colorScheme.onPrimaryContainer, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + ), + + const SizedBox(height: 24), + + // Additional weather info + if (humidity != null || windSpeed != null) + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: colorScheme.surface.withOpacity(0.5), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + if (humidity != null) ..[ + Expanded( + child: _InfoChip( + icon: Icons.water_drop, + label: 'Humidity', + value: '${humidity!.round()}%', + colorScheme: colorScheme, + ), + ), + if (windSpeed != null) const SizedBox(width: 16), + ], + if (windSpeed != null) + Expanded( + child: _InfoChip( + icon: Icons.air, + label: 'Wind', + value: '${windSpeed!.round()} m/s', + colorScheme: colorScheme, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + IconData _getWeatherIcon(String condition) { + final lowerCondition = condition.toLowerCase(); + + if (lowerCondition.contains('clear') || lowerCondition.contains('sunny')) { + return Icons.wb_sunny; + } else if (lowerCondition.contains('cloud')) { + return Icons.wb_cloudy; + } else if (lowerCondition.contains('rain') || lowerCondition.contains('drizzle')) { + return Icons.grain; + } else if (lowerCondition.contains('storm') || lowerCondition.contains('thunder')) { + return Icons.thunderstorm; + } else if (lowerCondition.contains('snow')) { + return Icons.ac_unit; + } else if (lowerCondition.contains('mist') || lowerCondition.contains('fog')) { + return Icons.blur_on; + } + + return Icons.wb_cloudy; + } +} + +/// Info chip widget for displaying weather details +class _InfoChip extends StatelessWidget { + final IconData icon; + final String label; + final String value; + final ColorScheme colorScheme; + + const _InfoChip({ + required this.icon, + required this.label, + required this.value, + required this.colorScheme, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: 20, + color: colorScheme.primary, + ), + const SizedBox(height: 4), + Text( + label, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onSurface.withOpacity(0.7), + ), + ), + Text( + value, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: colorScheme.onSurface, + fontWeight: FontWeight.w600, + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index d8e9822..7089870 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,6 +24,7 @@ dependencies: auto_route: ^9.2.2 cupertino_icons: ^1.0.8 device_info_plus: ^11.1.1 + flex_color_scheme: ^8.3.0 flutter: sdk: flutter flutter_hooks: ^0.20.5 @@ -83,4 +84,4 @@ flutter: # weight: 700 # # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages + # see https://flutter.dev/custom-fonts/#from-packages \ No newline at end of file