From 780edd2d3bfb219284307481ad277607e044f2a4 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 8 Jan 2026 18:52:27 +0100 Subject: [PATCH] fix: environment widget alignment and sun position night labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Consistent 170px minHeight across Weather, Air Quality, Forecast widgets - Swap sunrise/sunset labels at night to match arc direction - Weather header shows "Weather" instead of location 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CHANGELOG.md | 17 ++ lib/shared/widgets/air_quality_widget.dart | 136 ++++++++------- lib/shared/widgets/forecast_widget.dart | 27 +-- lib/shared/widgets/sun_position_widget.dart | 22 +-- lib/shared/widgets/weather_widget.dart | 176 ++++++++++++-------- pubspec.yaml | 2 +- 6 files changed, 225 insertions(+), 155 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 921a6bf..ef7a6f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.5.7] - 2026-01-08 + +### Fixed + +- **Environment widget alignment** - Consistent card heights across all environment widgets + - Added `ConstrainedBox(minHeight: 170)` to Weather, Air Quality, and Forecast widgets + - All cards now match Sun Position widget height when displaying data + +- **Sun position night labels** - Swap sunrise/sunset labels at night + - During day: Sunrise on left, Sunset on right (day arc) + - At night: Sunset on left, Sunrise on right (night arc) + +### Changed + +- **Weather widget header** - Changed from location name to "Weather" for consistency + - Location now displayed in content area below temperature + ## [1.5.6] - 2026-01-07 ### Changed diff --git a/lib/shared/widgets/air_quality_widget.dart b/lib/shared/widgets/air_quality_widget.dart index 8e82311..bf11640 100644 --- a/lib/shared/widgets/air_quality_widget.dart +++ b/lib/shared/widgets/air_quality_widget.dart @@ -67,77 +67,93 @@ class AirQualityWidget extends StatelessWidget { ), ], ), - const SizedBox(height: 16), + const SizedBox(height: 8), - // AQI Display - Row( - children: [ - // AQI number with colored background - Container( - width: 64, - height: 64, - decoration: BoxDecoration( - color: aqi.level.color.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: aqi.level.color.withValues(alpha: 0.3), - width: 2, - ), - ), - child: Center( - child: Text( - '${aqi.index}', - style: - Theme.of(context).textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - color: aqi.level.color, - ), - ), - ), - ), - const SizedBox(width: 16), - - // Level info - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + // Content area with minimum height to match other widgets + ConstrainedBox( + constraints: const BoxConstraints(minHeight: 170), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 8), + // AQI Display + Row( children: [ - Text( - aqi.level.label, - style: - Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, + // AQI number with colored background + Container( + width: 64, + height: 64, + decoration: BoxDecoration( + color: aqi.level.color.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: aqi.level.color.withValues(alpha: 0.3), + width: 2, + ), + ), + child: Center( + child: Text( + '${aqi.index}', + style: Theme.of(context) + .textTheme + .headlineMedium + ?.copyWith( + fontWeight: FontWeight.bold, color: aqi.level.color, ), + ), + ), ), - const SizedBox(height: 4), - Text( - aqi.level.description, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, + const SizedBox(width: 16), + + // Level info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + aqi.level.label, + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + fontWeight: FontWeight.w600, + color: aqi.level.color, + ), ), - maxLines: 2, - overflow: TextOverflow.ellipsis, + const SizedBox(height: 4), + Text( + aqi.level.description, + style: + Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), ), ], ), - ), - ], - ), - // Pollutants - if (aqi.pollutants.isNotEmpty) ...[ - const SizedBox(height: 16), - const Divider(height: 1), - const SizedBox(height: 12), - Wrap( - spacing: 16, - runSpacing: 8, - children: aqi.pollutants - .map((p) => _PollutantChip(pollutant: p)) - .toList(), + // Pollutants + if (aqi.pollutants.isNotEmpty) ...[ + const SizedBox(height: 16), + const Divider(height: 1), + const SizedBox(height: 12), + Wrap( + spacing: 16, + runSpacing: 8, + children: aqi.pollutants + .map((p) => _PollutantChip(pollutant: p)) + .toList(), + ), + ], + ], ), - ], + ), ], ), ), diff --git a/lib/shared/widgets/forecast_widget.dart b/lib/shared/widgets/forecast_widget.dart index cb62614..0eaa8c5 100644 --- a/lib/shared/widgets/forecast_widget.dart +++ b/lib/shared/widgets/forecast_widget.dart @@ -106,18 +106,23 @@ class ForecastWidget extends StatelessWidget { ), ], ), - const SizedBox(height: 16), + const SizedBox(height: 8), - // Forecast days - horizontal scroll - SizedBox( - height: 100, - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: forecast!.length, - separatorBuilder: (_, i) => const SizedBox(width: 12), - itemBuilder: (context, index) { - return _ForecastDayCard(day: forecast![index]); - }, + // Content area with minimum height to match other widgets + ConstrainedBox( + constraints: const BoxConstraints(minHeight: 170), + child: Center( + child: SizedBox( + height: 110, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: forecast!.length, + separatorBuilder: (_, i) => const SizedBox(width: 12), + itemBuilder: (context, index) { + return _ForecastDayCard(day: forecast![index]); + }, + ), + ), ), ), ], diff --git a/lib/shared/widgets/sun_position_widget.dart b/lib/shared/widgets/sun_position_widget.dart index 63e335d..541a189 100644 --- a/lib/shared/widgets/sun_position_widget.dart +++ b/lib/shared/widgets/sun_position_widget.dart @@ -122,6 +122,8 @@ class _SunPositionWidgetState extends State { const SizedBox(height: 8), // Arc with integrated horizon labels + // At night: sunset on left (night start), sunrise on right (night end) + // During day: sunrise on left (day start), sunset on right (day end) SizedBox( height: 170, child: LayoutBuilder( @@ -138,15 +140,15 @@ class _SunPositionWidgetState extends State { ), ), ), - // Sunrise widget at left horizon (10px up for balance) + // Left horizon: Sunrise during day, Sunset at night Positioned( left: 0, bottom: 10, child: _HorizonTimeDisplay( - icon: Icons.wb_twilight, - label: 'Sunrise', - time: _sunrise, - iconColor: Colors.orange, + icon: _isDaytime ? Icons.wb_twilight : Icons.nights_stay, + label: _isDaytime ? 'Sunrise' : 'Sunset', + time: _isDaytime ? _sunrise : _sunset, + iconColor: _isDaytime ? Colors.orange : Colors.deepOrange, alignment: CrossAxisAlignment.start, ), ), @@ -159,15 +161,15 @@ class _SunPositionWidgetState extends State { child: _DaylightDisplay(minutes: _daylightMinutes), ), ), - // Sunset widget at right horizon (10px up for balance) + // Right horizon: Sunset during day, Sunrise at night Positioned( right: 0, bottom: 10, child: _HorizonTimeDisplay( - icon: Icons.nights_stay, - label: 'Sunset', - time: _sunset, - iconColor: Colors.deepOrange, + icon: _isDaytime ? Icons.nights_stay : Icons.wb_twilight, + label: _isDaytime ? 'Sunset' : 'Sunrise', + time: _isDaytime ? _sunset : _sunrise, + iconColor: _isDaytime ? Colors.deepOrange : Colors.orange, alignment: CrossAxisAlignment.end, ), ), diff --git a/lib/shared/widgets/weather_widget.dart b/lib/shared/widgets/weather_widget.dart index 3fad334..7cec80f 100644 --- a/lib/shared/widgets/weather_widget.dart +++ b/lib/shared/widgets/weather_widget.dart @@ -59,7 +59,7 @@ class WeatherWidget extends StatelessWidget { const SizedBox(width: 8), Expanded( child: Text( - weather.location, + 'Weather', style: Theme.of(context).textTheme.titleSmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -69,89 +69,119 @@ class WeatherWidget extends StatelessWidget { ), ], ), - const SizedBox(height: 16), + const SizedBox(height: 8), - // Main weather display (matching AQI layout) - Row( - children: [ - // Weather icon in box (like AQI number box) - Container( - width: 64, - height: 64, - decoration: BoxDecoration( - color: (weather.iconColor ?? colorScheme.primary) - .withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: (weather.iconColor ?? colorScheme.primary) - .withValues(alpha: 0.3), - width: 2, - ), - ), - child: Center( - child: Icon( - weather.icon, - size: 32, - color: weather.iconColor ?? colorScheme.primary, - ), - ), - ), - const SizedBox(width: 16), - - // Temperature and condition - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + // Content area with minimum height to match other widgets + ConstrainedBox( + constraints: const BoxConstraints(minHeight: 170), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 8), + // Main weather display (matching AQI layout) + Row( children: [ - Text( - '${weather.temperature.round()}°${weather.unit.symbol}', - style: - Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - ), + // Weather icon in box (like AQI number box) + Container( + width: 64, + height: 64, + decoration: BoxDecoration( + color: (weather.iconColor ?? colorScheme.primary) + .withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: (weather.iconColor ?? colorScheme.primary) + .withValues(alpha: 0.3), + width: 2, + ), + ), + child: Center( + child: Icon( + weather.icon, + size: 32, + color: weather.iconColor ?? colorScheme.primary, + ), + ), ), - const SizedBox(height: 4), - Text( - weather.condition, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, + const SizedBox(width: 16), + + // Temperature and condition + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${weather.temperature.round()}°${weather.unit.symbol}', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + fontWeight: FontWeight.w600, + ), ), - maxLines: 2, - overflow: TextOverflow.ellipsis, + const SizedBox(height: 4), + Text( + weather.condition, + style: + Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + if (weather.location.isNotEmpty) ...[ + const SizedBox(height: 2), + Text( + weather.location, + style: Theme.of(context) + .textTheme + .labelSmall + ?.copyWith( + color: colorScheme.outline, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), ), ], ), - ), - ], - ), - // Details - if (weather.humidity != null || weather.windSpeed != null) ...[ - const SizedBox(height: 16), - const Divider(height: 1), - const SizedBox(height: 12), - Wrap( - spacing: 16, - runSpacing: 8, - children: [ - if (weather.humidity != null) - _DetailChip( - label: 'Humidity', - value: '${weather.humidity}%', - ), - if (weather.windSpeed != null) - _DetailChip( - label: 'Wind', - value: '${weather.windSpeed!.round()} ${weather.windUnit}', - ), - if (weather.feelsLike != null) - _DetailChip( - label: 'Feels', - value: '${weather.feelsLike!.round()}°${weather.unit.symbol}', + // Details + if (weather.humidity != null || weather.windSpeed != null) ...[ + const SizedBox(height: 16), + const Divider(height: 1), + const SizedBox(height: 12), + Wrap( + spacing: 16, + runSpacing: 8, + children: [ + if (weather.humidity != null) + _DetailChip( + label: 'Humidity', + value: '${weather.humidity}%', + ), + if (weather.windSpeed != null) + _DetailChip( + label: 'Wind', + value: + '${weather.windSpeed!.round()} ${weather.windUnit}', + ), + if (weather.feelsLike != null) + _DetailChip( + label: 'Feels', + value: + '${weather.feelsLike!.round()}°${weather.unit.symbol}', + ), + ], ), + ], ], ), - ], + ), ], ), ), diff --git a/pubspec.yaml b/pubspec.yaml index 6a4e07b..969a9ce 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.5.6+1 +version: 1.5.7+1 environment: sdk: ^3.10.4