Compare commits

...
3 Commits
Author SHA1 Message Date
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
7 changed files with 306 additions and 223 deletions
+36 -3
View File
@@ -7,13 +7,46 @@ 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
- **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** - Shows authenticated user in section header for debugging
- Displays `user: {username}` next to "Environment" header when data loads
- Helps diagnose user resolution issues with OIDC authentication
- User display in environment section header (reverted in 1.5.5)
## [1.5.3] - 2026-01-07
@@ -26,6 +26,13 @@ class _DashboardContentState extends ConsumerState<DashboardContent> {
Timer? _systemStatsTimer;
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
void initState() {
super.initState();
@@ -129,62 +136,47 @@ class _DashboardContentState extends ConsumerState<DashboardContent> {
const SizedBox(height: 24),
// Environment - Sun, Weather, Forecast, Air Quality
const _SectionHeader(title: 'Environment', icon: Icons.eco),
const SizedBox(height: 8),
environmentAsync.when(
data: (envData) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SectionHeader(
title: 'Environment',
icon: Icons.eco,
subtitle: envData.user != null ? 'user: ${envData.user}' : null,
),
const SizedBox(height: 8),
_EnvironmentSection(
envData: envData,
onRefresh: () => ref.invalidate(environmentProvider),
),
],
data: (envData) {
// Store and log user once per session
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(
child: Padding(
padding: EdgeInsets.all(32),
child: Center(child: CircularProgressIndicator()),
),
),
loading: () => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _SectionHeader(title: 'Environment', icon: Icons.eco),
const SizedBox(height: 8),
const Card(
child: Padding(
padding: EdgeInsets.all(32),
child: Center(child: CircularProgressIndicator()),
),
),
],
),
error: (error, _) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _SectionHeader(title: 'Environment', icon: Icons.eco),
const SizedBox(height: 8),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(Icons.error_outline, color: colorScheme.error),
const SizedBox(width: 12),
Expanded(
child: Text(
'Failed to load environment data',
style: TextStyle(color: colorScheme.error),
),
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () => ref.invalidate(environmentProvider),
),
],
error: (error, _) => Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(Icons.error_outline, color: colorScheme.error),
const SizedBox(width: 12),
Expanded(
child: Text(
'Failed to load environment data',
style: TextStyle(color: colorScheme.error),
),
),
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () => ref.invalidate(environmentProvider),
),
],
),
],
),
),
),
const SizedBox(height: 24),
@@ -208,12 +200,10 @@ class _SectionHeader extends StatelessWidget {
const _SectionHeader({
required this.title,
required this.icon,
this.subtitle,
});
final String title;
final IconData icon;
final String? subtitle;
@override
Widget build(BuildContext context) {
@@ -234,15 +224,6 @@ class _SectionHeader extends StatelessWidget {
fontWeight: FontWeight.w500,
),
),
if (subtitle != null) ...[
const SizedBox(width: 8),
Text(
subtitle!,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: colorScheme.outline,
),
),
],
],
);
}
+76 -60
View File
@@ -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(),
),
],
],
),
],
),
],
),
),
+16 -11
View File
@@ -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]);
},
),
),
),
),
],
+30 -12
View File
@@ -122,6 +122,8 @@ class _SunPositionWidgetState extends State<SunPositionWidget> {
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<SunPositionWidget> {
),
),
),
// 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<SunPositionWidget> {
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,
),
),
@@ -386,7 +388,15 @@ class _SunArcPainter extends CustomPainter {
// Calculate radius so arc endpoints touch horizon
// 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
// 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);
// 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
final horizonYPos = size.height - horizonY;
+103 -73
View File
@@ -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}',
),
],
),
],
],
),
],
),
],
),
),
+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
# 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.4+1
version: 1.5.7+1
environment:
sdk: ^3.10.4