Compare commits

..
5 Commits
Author SHA1 Message Date
Jeroen SchweitzerandClaude Opus 4.5 f31967aa3e feat: bottom-aligned widgets and wind direction
Build and Push / release (push) Successful in 2s
Build and Push / build (push) Successful in 3m27s
- All environment widgets align content from bottom for visual harmony
- Wind chip now shows direction (e.g., "SE 14 km/h")

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 19:10:24 +01:00
Jeroen SchweitzerandClaude Opus 4.5 780edd2d3b fix: environment widget alignment and sun position night labels
Build and Push / release (push) Successful in 3s
Build and Push / build (push) Successful in 3m27s
- 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 <noreply@anthropic.com>
2026-01-08 18:52:27 +01:00
Jeroen SchweitzerandClaude Opus 4.5 eefb491e87 fix: log environment API user only once per session
Build and Push / release (push) Successful in 3s
Build and Push / build (push) Successful in 3m11s
- Store user in static variable for reuse
- Only debugPrint on first successful load

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 15:40:06 +01:00
Jeroen SchweitzerandClaude Opus 4.5 9b2000efc2 fix: sun position arc overflow and revert user display
Build and Push / release (push) Successful in 3s
Build and Push / build (push) Successful in 3m14s
- Constrain arc height to fit within card boundaries
- Scale radius down when arc would overflow on wider displays
- Revert user display in section header, use debugPrint instead

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 15:26:51 +01:00
Jeroen SchweitzerandClaude Opus 4.5 cc6068b759 chore: release v1.5.4
Build and Push / release (push) Successful in 2s
Build and Push / build (push) Successful in 3m9s
Add user display in environment section header for debugging OIDC
user resolution issues.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 14:05:16 +01:00
7 changed files with 301 additions and 161 deletions
+53
View File
@@ -7,6 +7,59 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [1.5.8] - 2026-01-08
### Changed
- **Bottom-aligned widget content** - All environment widgets now align content from the bottom
- Creates consistent visual baseline across Sun Position, Weather, Air Quality, and Forecast cards
- Footers (weather details, pollutants) sit at the same level across cards
### Added
- **Wind direction in Weather** - Wind chip now shows direction (e.g., "SE 14 km/h")
## [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
- **Environment user logging** - Now logs API user only once per session instead of on every refresh
## [1.5.5] - 2026-01-07
### Fixed
- **Sun position arc overflow** - Arc now constrained to fit within card boundaries
- Prevents arc and sun/moon from overflowing on wider displays
- Scales radius down when arc height exceeds available space
### Added
- **Debug logging for environment user** - Logs authenticated user on environment data load
## [1.5.4] - 2026-01-07
### Added
- User display in environment section header (reverted in 1.5.5)
## [1.5.3] - 2026-01-07 ## [1.5.3] - 2026-01-07
### Changed ### Changed
@@ -26,6 +26,13 @@ class _DashboardContentState extends ConsumerState<DashboardContent> {
Timer? _systemStatsTimer; Timer? _systemStatsTimer;
Timer? _environmentTimer; Timer? _environmentTimer;
/// Tracks if we've logged the environment API user (log once per session)
static bool _hasLoggedEnvUser = false;
static String? _envApiUser;
/// Get the environment API user (available after first load)
static String? get envApiUser => _envApiUser;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -129,13 +136,21 @@ class _DashboardContentState extends ConsumerState<DashboardContent> {
const SizedBox(height: 24), const SizedBox(height: 24),
// Environment - Sun, Weather, Forecast, Air Quality // Environment - Sun, Weather, Forecast, Air Quality
_SectionHeader(title: 'Environment', icon: Icons.eco), const _SectionHeader(title: 'Environment', icon: Icons.eco),
const SizedBox(height: 8), const SizedBox(height: 8),
environmentAsync.when( environmentAsync.when(
data: (envData) => _EnvironmentSection( data: (envData) {
envData: envData, // Store and log user once per session
onRefresh: () => ref.invalidate(environmentProvider), if (!_hasLoggedEnvUser && envData.user != null) {
), _envApiUser = envData.user;
_hasLoggedEnvUser = true;
debugPrint('Environment API user: ${envData.user}');
}
return _EnvironmentSection(
envData: envData,
onRefresh: () => ref.invalidate(environmentProvider),
);
},
loading: () => const Card( loading: () => const Card(
child: Padding( child: Padding(
padding: EdgeInsets.all(32), padding: EdgeInsets.all(32),
+75 -60
View File
@@ -67,77 +67,92 @@ class AirQualityWidget extends StatelessWidget {
), ),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 8),
// AQI Display // Content area with fixed height, content aligned to bottom
Row( SizedBox(
children: [ height: 170,
// AQI number with colored background child: Column(
Container( crossAxisAlignment: CrossAxisAlignment.start,
width: 64, mainAxisAlignment: MainAxisAlignment.end,
height: 64, children: [
decoration: BoxDecoration( // AQI Display
color: aqi.level.color.withValues(alpha: 0.15), Row(
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,
children: [ children: [
Text( // AQI number with colored background
aqi.level.label, Container(
style: width: 64,
Theme.of(context).textTheme.titleMedium?.copyWith( height: 64,
fontWeight: FontWeight.w600, 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, color: aqi.level.color,
), ),
),
),
), ),
const SizedBox(height: 4), const SizedBox(width: 16),
Text(
aqi.level.description, // Level info
style: Theme.of(context).textTheme.bodySmall?.copyWith( Expanded(
color: colorScheme.onSurfaceVariant, 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, const SizedBox(height: 4),
overflow: TextOverflow.ellipsis, Text(
aqi.level.description,
style:
Theme.of(context).textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
), ),
], ],
), ),
),
],
),
// Pollutants // Pollutants
if (aqi.pollutants.isNotEmpty) ...[ if (aqi.pollutants.isNotEmpty) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
const Divider(height: 1), const Divider(height: 1),
const SizedBox(height: 12), const SizedBox(height: 12),
Wrap( Wrap(
spacing: 16, spacing: 16,
runSpacing: 8, runSpacing: 8,
children: aqi.pollutants children: aqi.pollutants
.map((p) => _PollutantChip(pollutant: p)) .map((p) => _PollutantChip(pollutant: p))
.toList(), .toList(),
),
],
],
), ),
], ),
], ],
), ),
), ),
+16 -10
View File
@@ -106,18 +106,24 @@ class ForecastWidget extends StatelessWidget {
), ),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 8),
// Forecast days - horizontal scroll // Content area with fixed height, content aligned to bottom
SizedBox( SizedBox(
height: 100, height: 170,
child: ListView.separated( child: Align(
scrollDirection: Axis.horizontal, alignment: Alignment.bottomCenter,
itemCount: forecast!.length, child: SizedBox(
separatorBuilder: (_, i) => const SizedBox(width: 12), height: 110,
itemBuilder: (context, index) { child: ListView.separated(
return _ForecastDayCard(day: forecast![index]); scrollDirection: Axis.horizontal,
}, itemCount: forecast!.length,
separatorBuilder: (_, i) => const SizedBox(width: 12),
itemBuilder: (context, index) {
return _ForecastDayCard(day: forecast![index]);
},
),
),
), ),
), ),
], ],
+30 -12
View File
@@ -122,6 +122,8 @@ class _SunPositionWidgetState extends State<SunPositionWidget> {
const SizedBox(height: 8), const SizedBox(height: 8),
// Arc with integrated horizon labels // 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( SizedBox(
height: 170, height: 170,
child: LayoutBuilder( child: LayoutBuilder(
@@ -138,15 +140,15 @@ class _SunPositionWidgetState extends State<SunPositionWidget> {
), ),
), ),
), ),
// Sunrise widget at left horizon (10px up for balance) // Left horizon: Sunrise during day, Sunset at night
Positioned( Positioned(
left: 0, left: 0,
bottom: 10, bottom: 10,
child: _HorizonTimeDisplay( child: _HorizonTimeDisplay(
icon: Icons.wb_twilight, icon: _isDaytime ? Icons.wb_twilight : Icons.nights_stay,
label: 'Sunrise', label: _isDaytime ? 'Sunrise' : 'Sunset',
time: _sunrise, time: _isDaytime ? _sunrise : _sunset,
iconColor: Colors.orange, iconColor: _isDaytime ? Colors.orange : Colors.deepOrange,
alignment: CrossAxisAlignment.start, alignment: CrossAxisAlignment.start,
), ),
), ),
@@ -159,15 +161,15 @@ class _SunPositionWidgetState extends State<SunPositionWidget> {
child: _DaylightDisplay(minutes: _daylightMinutes), child: _DaylightDisplay(minutes: _daylightMinutes),
), ),
), ),
// Sunset widget at right horizon (10px up for balance) // Right horizon: Sunset during day, Sunrise at night
Positioned( Positioned(
right: 0, right: 0,
bottom: 10, bottom: 10,
child: _HorizonTimeDisplay( child: _HorizonTimeDisplay(
icon: Icons.nights_stay, icon: _isDaytime ? Icons.nights_stay : Icons.wb_twilight,
label: 'Sunset', label: _isDaytime ? 'Sunset' : 'Sunrise',
time: _sunset, time: _isDaytime ? _sunset : _sunrise,
iconColor: Colors.deepOrange, iconColor: _isDaytime ? Colors.deepOrange : Colors.orange,
alignment: CrossAxisAlignment.end, alignment: CrossAxisAlignment.end,
), ),
), ),
@@ -386,7 +388,15 @@ class _SunArcPainter extends CustomPainter {
// Calculate radius so arc endpoints touch horizon // Calculate radius so arc endpoints touch horizon
// For a chord of width W and arc angle θ: R = W / (2 * sin(θ/2)) // For a chord of width W and arc angle θ: R = W / (2 * sin(θ/2))
final radius = horizonWidth / (2 * math.sin(clampedAngle / 2)); var radius = horizonWidth / (2 * math.sin(clampedAngle / 2));
// Constrain arc height to fit within available space (leave 25px margin for sun)
final maxArcHeight = size.height - horizonY - 25;
final arcHeight = radius * (1 - math.cos(clampedAngle / 2));
if (arcHeight > maxArcHeight) {
// Scale radius down to fit
radius = maxArcHeight / (1 - math.cos(clampedAngle / 2));
}
// Arc center is below the horizon for an upward-bulging arc // Arc center is below the horizon for an upward-bulging arc
// Distance from chord to center = R * cos(θ/2) // Distance from chord to center = R * cos(θ/2)
@@ -491,7 +501,15 @@ class _SunArcPainter extends CustomPainter {
final clampedAngle = arcAngle.clamp(math.pi / 6, math.pi); final clampedAngle = arcAngle.clamp(math.pi / 6, math.pi);
// Calculate radius so arc endpoints touch horizon // Calculate radius so arc endpoints touch horizon
final radius = horizonWidth / (2 * math.sin(clampedAngle / 2)); var radius = horizonWidth / (2 * math.sin(clampedAngle / 2));
// Constrain arc height to fit within available space (leave 20px margin for moon)
final maxArcHeight = size.height - horizonY - 20;
final arcHeight = radius * (1 - math.cos(clampedAngle / 2));
if (arcHeight > maxArcHeight) {
// Scale radius down to fit
radius = maxArcHeight / (1 - math.cos(clampedAngle / 2));
}
// Arc center is below the horizon for an upward-bulging arc // Arc center is below the horizon for an upward-bulging arc
final horizonYPos = size.height - horizonY; final horizonYPos = size.height - horizonY;
+106 -73
View File
@@ -59,7 +59,7 @@ class WeatherWidget extends StatelessWidget {
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Text( child: Text(
weather.location, 'Weather',
style: Theme.of(context).textTheme.titleSmall?.copyWith( style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: colorScheme.onSurfaceVariant, 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) // Content area with fixed height, content aligned to bottom
Row( SizedBox(
children: [ height: 170,
// Weather icon in box (like AQI number box) child: Column(
Container( crossAxisAlignment: CrossAxisAlignment.start,
width: 64, mainAxisAlignment: MainAxisAlignment.end,
height: 64, children: [
decoration: BoxDecoration( // Main weather display (matching AQI layout)
color: (weather.iconColor ?? colorScheme.primary) Row(
.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,
children: [ children: [
Text( // Weather icon in box (like AQI number box)
'${weather.temperature.round()}°${weather.unit.symbol}', Container(
style: width: 64,
Theme.of(context).textTheme.titleMedium?.copyWith( height: 64,
fontWeight: FontWeight.w600, 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), const SizedBox(width: 16),
Text(
weather.condition, // Temperature and condition
style: Theme.of(context).textTheme.bodySmall?.copyWith( Expanded(
color: colorScheme.onSurfaceVariant, 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, const SizedBox(height: 4),
overflow: TextOverflow.ellipsis, 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 // Details
if (weather.humidity != null || weather.windSpeed != null) ...[ if (weather.humidity != null || weather.windSpeed != null) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
const Divider(height: 1), const Divider(height: 1),
const SizedBox(height: 12), const SizedBox(height: 12),
Wrap( Wrap(
spacing: 16, spacing: 16,
runSpacing: 8, runSpacing: 8,
children: [ children: [
if (weather.humidity != null) if (weather.humidity != null)
_DetailChip( _DetailChip(
label: 'Humidity', label: 'Humidity',
value: '${weather.humidity}%', value: '${weather.humidity}%',
), ),
if (weather.windSpeed != null) if (weather.windSpeed != null)
_DetailChip( _DetailChip(
label: 'Wind', label: 'Wind',
value: '${weather.windSpeed!.round()} ${weather.windUnit}', value: weather.windDirection != null
), ? '${weather.windDirection} ${weather.windSpeed!.round()} ${weather.windUnit}'
if (weather.feelsLike != null) : '${weather.windSpeed!.round()} ${weather.windUnit}',
_DetailChip( ),
label: 'Feels', if (weather.feelsLike != null)
value: '${weather.feelsLike!.round()}°${weather.unit.symbol}', _DetailChip(
label: 'Feels',
value:
'${weather.feelsLike!.round()}°${weather.unit.symbol}',
),
],
), ),
],
], ],
), ),
], ),
], ],
), ),
), ),
@@ -311,6 +341,7 @@ WeatherData _fromApiData(api.WeatherData data) {
icon: _getWeatherIcon(data.conditions, data.icon), icon: _getWeatherIcon(data.conditions, data.icon),
humidity: data.humidity, humidity: data.humidity,
windSpeed: data.windSpeed, windSpeed: data.windSpeed,
windDirection: data.windDirection,
feelsLike: data.feelsLike, feelsLike: data.feelsLike,
iconColor: _getWeatherColor(data.conditions), iconColor: _getWeatherColor(data.conditions),
); );
@@ -375,6 +406,7 @@ class WeatherData {
this.unit = TemperatureUnit.celsius, this.unit = TemperatureUnit.celsius,
this.humidity, this.humidity,
this.windSpeed, this.windSpeed,
this.windDirection,
this.windUnit = 'km/h', this.windUnit = 'km/h',
this.feelsLike, this.feelsLike,
this.iconColor, this.iconColor,
@@ -387,6 +419,7 @@ class WeatherData {
final TemperatureUnit unit; final TemperatureUnit unit;
final int? humidity; final int? humidity;
final double? windSpeed; final double? windSpeed;
final String? windDirection;
final String windUnit; final String windUnit;
final double? feelsLike; final double? feelsLike;
final Color? iconColor; final Color? iconColor;
+1 -1
View File
@@ -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 # 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 # 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. # of the product and file versions while build-number is used as the build suffix.
version: 1.5.3+1 version: 1.5.8+1
environment: environment:
sdk: ^3.10.4 sdk: ^3.10.4