Files
tatlock-ui/lib/shared/widgets/weather_widget.dart
T
Jeroen SchweitzerandClaude Opus 4.5 9c9ec472ef feat(dashboard): add gauge, weather, and air quality widgets
- Create shared GaugeWidget with circular progress and customizable colors
- Add GaugeRow for displaying multiple gauges in a responsive layout
- Create WeatherWidget with temperature, condition, and details
- Create AirQualityWidget with AQI levels and pollutant readings
- Update DashboardContent with System Stats gauges and Environment section
- Both widgets use mock data, ready for API integration

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 23:09:15 +01:00

259 lines
7.1 KiB
Dart

import 'package:flutter/material.dart';
/// Weather widget displaying current conditions.
///
/// Currently uses mock data. Will be connected to weather API in future.
class WeatherWidget extends StatelessWidget {
const WeatherWidget({
super.key,
this.data,
this.compact = false,
});
/// Weather data to display. Uses mock data if null.
final WeatherData? data;
/// Whether to use compact layout.
final bool compact;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final weather = data ?? WeatherData.mock();
if (compact) {
return _buildCompact(context, colorScheme, weather);
}
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Header
Row(
children: [
Icon(
Icons.location_on,
size: 16,
color: colorScheme.onSurfaceVariant,
),
const SizedBox(width: 4),
Expanded(
child: Text(
weather.location,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 12),
// Main weather display
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
weather.icon,
size: 48,
color: weather.iconColor ?? colorScheme.primary,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${weather.temperature.round()}°${weather.unit.symbol}',
style:
Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
weather.condition,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
],
),
// Details
if (weather.humidity != null || weather.windSpeed != null) ...[
const SizedBox(height: 12),
const Divider(height: 1),
const SizedBox(height: 12),
Row(
children: [
if (weather.humidity != null)
Expanded(
child: _DetailItem(
icon: Icons.water_drop_outlined,
label: 'Humidity',
value: '${weather.humidity}%',
),
),
if (weather.windSpeed != null)
Expanded(
child: _DetailItem(
icon: Icons.air,
label: 'Wind',
value:
'${weather.windSpeed!.round()} ${weather.windUnit}',
),
),
if (weather.feelsLike != null)
Expanded(
child: _DetailItem(
icon: Icons.thermostat,
label: 'Feels like',
value:
'${weather.feelsLike!.round()}°${weather.unit.symbol}',
),
),
],
),
],
],
),
),
);
}
Widget _buildCompact(
BuildContext context,
ColorScheme colorScheme,
WeatherData weather,
) {
return Card(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
weather.icon,
size: 24,
color: weather.iconColor ?? colorScheme.primary,
),
const SizedBox(width: 8),
Text(
'${weather.temperature.round()}°${weather.unit.symbol}',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
}
class _DetailItem extends StatelessWidget {
const _DetailItem({
required this.icon,
required this.label,
required this.value,
});
final IconData icon;
final String label;
final String value;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 16,
color: colorScheme.onSurfaceVariant,
),
const SizedBox(width: 4),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
value,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
Text(
label,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: colorScheme.outline,
fontSize: 10,
),
),
],
),
],
);
}
}
/// Temperature unit for weather display.
enum TemperatureUnit {
celsius('C'),
fahrenheit('F');
const TemperatureUnit(this.symbol);
final String symbol;
}
/// Weather data model.
class WeatherData {
const WeatherData({
required this.location,
required this.temperature,
required this.condition,
required this.icon,
this.unit = TemperatureUnit.celsius,
this.humidity,
this.windSpeed,
this.windUnit = 'km/h',
this.feelsLike,
this.iconColor,
});
final String location;
final double temperature;
final String condition;
final IconData icon;
final TemperatureUnit unit;
final int? humidity;
final double? windSpeed;
final String windUnit;
final double? feelsLike;
final Color? iconColor;
/// Mock weather data for development.
factory WeatherData.mock() {
return const WeatherData(
location: 'Rotterdam, NL',
temperature: 8,
condition: 'Partly Cloudy',
icon: Icons.cloud,
humidity: 72,
windSpeed: 18,
feelsLike: 5,
iconColor: Colors.blueGrey,
);
}
}