feat: environment widgets layout redesign with always-visible widgets
- Desktop: 4-in-a-row layout (30/20/20/30 distribution) - Tablet: 2x2 grid layout - Mobile: Stacked vertically - All widgets show "No data available" state instead of being hidden - Sun position calculates from clock, defaults to 6am/6pm - Environment refresh changed from 5min to 1 hour 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
fffc3d5baf
commit
d200cad8be
@@ -28,10 +28,16 @@ class AirQualityWidget extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
// Show "no data" state when no API data and no legacy data
|
||||
if (apiData == null && data == null) {
|
||||
if (compact) {
|
||||
return _buildCompactNoData(context, colorScheme);
|
||||
}
|
||||
return _buildNoData(context, colorScheme);
|
||||
}
|
||||
|
||||
// Convert API data to local model, or use legacy data
|
||||
final aqi = apiData != null
|
||||
? _fromApiData(apiData!)
|
||||
: (data ?? AirQualityData.mock());
|
||||
final aqi = apiData != null ? _fromApiData(apiData!) : data!;
|
||||
|
||||
if (compact) {
|
||||
return _buildCompact(context, colorScheme, aqi);
|
||||
@@ -178,6 +184,93 @@ class AirQualityWidget extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNoData(BuildContext context, ColorScheme colorScheme) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.air,
|
||||
size: 20,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Air Quality',
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.air_outlined,
|
||||
size: 32,
|
||||
color: colorScheme.outline,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'No data available',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.outline,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCompactNoData(BuildContext context, ColorScheme colorScheme) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.outline.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'--',
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'AQI',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.outline,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PollutantChip extends StatelessWidget {
|
||||
|
||||
@@ -130,9 +130,13 @@ class SunPositionWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
bool get _isDaytime {
|
||||
if (sunTimes?.sunrise == null || sunTimes?.sunset == null) return true;
|
||||
final now = DateTime.now();
|
||||
return now.isAfter(sunTimes!.sunrise!) && now.isBefore(sunTimes!.sunset!);
|
||||
// Use actual sun times if available, otherwise default to 6am/6pm
|
||||
final sunrise = sunTimes?.sunrise ??
|
||||
DateTime(now.year, now.month, now.day, 6, 0);
|
||||
final sunset = sunTimes?.sunset ??
|
||||
DateTime(now.year, now.month, now.day, 18, 0);
|
||||
return now.isAfter(sunrise) && now.isBefore(sunset);
|
||||
}
|
||||
|
||||
String _formatTime(DateTime? time) {
|
||||
@@ -370,14 +374,13 @@ class _SunArcPainter extends CustomPainter {
|
||||
}
|
||||
|
||||
double _calculateSunPosition() {
|
||||
if (sunTimes?.sunrise == null || sunTimes?.sunset == null) {
|
||||
// Default to noon position
|
||||
return 0.5;
|
||||
}
|
||||
|
||||
final now = DateTime.now();
|
||||
final sunrise = sunTimes!.sunrise!;
|
||||
final sunset = sunTimes!.sunset!;
|
||||
|
||||
// Use actual sun times if available, otherwise default to 6am/6pm (12-hour cycles)
|
||||
final sunrise = sunTimes?.sunrise ??
|
||||
DateTime(now.year, now.month, now.day, 6, 0);
|
||||
final sunset = sunTimes?.sunset ??
|
||||
DateTime(now.year, now.month, now.day, 18, 0);
|
||||
|
||||
// Before sunrise
|
||||
if (now.isBefore(sunrise)) {
|
||||
|
||||
@@ -26,10 +26,16 @@ class WeatherWidget extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
// Show "no data" state when no API data and no legacy data
|
||||
if (apiData == null && data == null) {
|
||||
if (compact) {
|
||||
return _buildCompactNoData(context, colorScheme);
|
||||
}
|
||||
return _buildNoData(context, colorScheme);
|
||||
}
|
||||
|
||||
// Convert API data to local model, or use legacy data
|
||||
final weather = apiData != null
|
||||
? _fromApiData(apiData!)
|
||||
: (data ?? WeatherData.mock());
|
||||
final weather = apiData != null ? _fromApiData(apiData!) : data!;
|
||||
|
||||
if (compact) {
|
||||
return _buildCompact(context, colorScheme, weather);
|
||||
@@ -180,6 +186,82 @@ class WeatherWidget extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNoData(BuildContext context, ColorScheme colorScheme) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.wb_sunny_outlined,
|
||||
size: 20,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Weather',
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.cloud_off_outlined,
|
||||
size: 32,
|
||||
color: colorScheme.outline,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'No data available',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.outline,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCompactNoData(BuildContext context, ColorScheme colorScheme) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.cloud_off_outlined,
|
||||
size: 24,
|
||||
color: colorScheme.outline,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'--',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: colorScheme.outline,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DetailChip extends StatelessWidget {
|
||||
|
||||
Reference in New Issue
Block a user