chore: release v1.5.0
Build and Push / release (push) Successful in 3s
Build and Push / build (push) Successful in 3m12s

feat: add dynamic environment widgets

Add live weather, sun position, and forecast widgets to Front Hall dashboard,
powered by data from the Qdrant volatile collection via core-api.

New widgets:
- SunPositionWidget: Animated arc showing sun/moon position with gradient colors
- ForecastWidget: Multi-day weather outlook
- Updated WeatherWidget and AirQualityWidget to accept API data

Infrastructure:
- Environment datasource calling GET /tools/environment
- Environment provider with 5-minute auto-refresh
- Freezed models for environment data

Tests:
- 12 widget tests for environment section
- Updated existing tests with givenEnvironment() harness method

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jeroen Schweitzer
2026-01-06 23:05:24 +01:00
co-authored by Claude Opus 4.5
parent ea6914b5f4
commit fffc3d5baf
14 changed files with 1469 additions and 30 deletions
+67 -3
View File
@@ -1,16 +1,22 @@
import 'package:flutter/material.dart';
import 'package:tatlock_ui/features/front_hall/data/models/environment_model.dart'
as api;
/// Weather widget displaying current conditions.
///
/// Currently uses mock data. Will be connected to weather API in future.
/// Displays weather data from the Core API environment endpoint.
class WeatherWidget extends StatelessWidget {
const WeatherWidget({
super.key,
this.apiData,
this.data,
this.compact = false,
});
/// Weather data to display. Uses mock data if null.
/// Weather data from API. Takes priority over legacy data.
final api.WeatherData? apiData;
/// Legacy weather data to display. Uses mock data if null.
final WeatherData? data;
/// Whether to use compact layout.
@@ -19,7 +25,11 @@ class WeatherWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final weather = data ?? WeatherData.mock();
// Convert API data to local model, or use legacy data
final weather = apiData != null
? _fromApiData(apiData!)
: (data ?? WeatherData.mock());
if (compact) {
return _buildCompact(context, colorScheme, weather);
@@ -206,6 +216,60 @@ class _DetailChip extends StatelessWidget {
}
}
/// Converts API weather data to local WeatherData model.
WeatherData _fromApiData(api.WeatherData data) {
return WeatherData(
location: data.location ?? 'Unknown',
temperature: data.temperature ?? 0,
condition: data.conditions ?? 'Unknown',
icon: _getWeatherIcon(data.conditions, data.icon),
humidity: data.humidity,
windSpeed: data.windSpeed,
feelsLike: data.feelsLike,
iconColor: _getWeatherColor(data.conditions),
);
}
/// Maps weather condition to icon.
IconData _getWeatherIcon(String? conditions, String? iconCode) {
final condition = conditions?.toLowerCase() ?? '';
if (condition.contains('clear') || condition.contains('sunny')) {
return Icons.wb_sunny;
} else if (condition.contains('cloud') || condition.contains('overcast')) {
return Icons.cloud;
} else if (condition.contains('rain') || condition.contains('drizzle')) {
return Icons.grain;
} else if (condition.contains('storm') || condition.contains('thunder')) {
return Icons.thunderstorm;
} else if (condition.contains('snow') || condition.contains('sleet')) {
return Icons.ac_unit;
} else if (condition.contains('fog') || condition.contains('mist')) {
return Icons.blur_on;
} else if (condition.contains('wind')) {
return Icons.air;
}
return Icons.cloud;
}
/// Maps weather condition to color.
Color? _getWeatherColor(String? conditions) {
final condition = conditions?.toLowerCase() ?? '';
if (condition.contains('clear') || condition.contains('sunny')) {
return Colors.amber;
} else if (condition.contains('cloud')) {
return Colors.blueGrey;
} else if (condition.contains('rain')) {
return Colors.blue;
} else if (condition.contains('storm')) {
return Colors.deepPurple;
} else if (condition.contains('snow')) {
return Colors.lightBlue;
}
return null;
}
/// Temperature unit for weather display.
enum TemperatureUnit {
celsius('C'),