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>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
40db92d9d5
commit
9c9ec472ef
@@ -0,0 +1,221 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A circular gauge widget for displaying percentage values.
|
||||
///
|
||||
/// Commonly used for system stats like CPU, Memory, Disk usage.
|
||||
class GaugeWidget extends StatelessWidget {
|
||||
const GaugeWidget({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.label,
|
||||
this.size = 120,
|
||||
this.strokeWidth = 10,
|
||||
this.color,
|
||||
this.backgroundColor,
|
||||
this.showPercentage = true,
|
||||
this.icon,
|
||||
});
|
||||
|
||||
/// The value to display (0.0 - 1.0).
|
||||
final double value;
|
||||
|
||||
/// Label displayed below the gauge.
|
||||
final String label;
|
||||
|
||||
/// Size of the gauge widget.
|
||||
final double size;
|
||||
|
||||
/// Width of the gauge arc stroke.
|
||||
final double strokeWidth;
|
||||
|
||||
/// Color of the filled arc. Uses primary color if not specified.
|
||||
final Color? color;
|
||||
|
||||
/// Color of the background arc. Uses surfaceVariant if not specified.
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// Whether to show the percentage text in the center.
|
||||
final bool showPercentage;
|
||||
|
||||
/// Optional icon to show above the percentage.
|
||||
final IconData? icon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final effectiveColor = color ?? colorScheme.primary;
|
||||
final effectiveBackgroundColor =
|
||||
backgroundColor ?? colorScheme.surfaceContainerHighest;
|
||||
|
||||
// Clamp value between 0 and 1
|
||||
final clampedValue = value.clamp(0.0, 1.0);
|
||||
|
||||
return SizedBox(
|
||||
width: size,
|
||||
height: size + 24, // Extra space for label
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: CustomPaint(
|
||||
painter: _GaugePainter(
|
||||
value: clampedValue,
|
||||
color: effectiveColor,
|
||||
backgroundColor: effectiveBackgroundColor,
|
||||
strokeWidth: strokeWidth,
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Icon(
|
||||
icon,
|
||||
size: size * 0.2,
|
||||
color: effectiveColor,
|
||||
),
|
||||
SizedBox(height: size * 0.02),
|
||||
],
|
||||
if (showPercentage)
|
||||
Text(
|
||||
'${(clampedValue * 100).round()}%',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: size * 0.18,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GaugePainter extends CustomPainter {
|
||||
_GaugePainter({
|
||||
required this.value,
|
||||
required this.color,
|
||||
required this.backgroundColor,
|
||||
required this.strokeWidth,
|
||||
});
|
||||
|
||||
final double value;
|
||||
final Color color;
|
||||
final Color backgroundColor;
|
||||
final double strokeWidth;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = Offset(size.width / 2, size.height / 2);
|
||||
final radius = (size.width - strokeWidth) / 2;
|
||||
|
||||
// Start from top (-90 degrees) and sweep clockwise
|
||||
const startAngle = -math.pi / 2;
|
||||
const sweepAngle = 2 * math.pi;
|
||||
|
||||
// Background arc
|
||||
final backgroundPaint = Paint()
|
||||
..color = backgroundColor
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = strokeWidth
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
canvas.drawArc(
|
||||
Rect.fromCircle(center: center, radius: radius),
|
||||
startAngle,
|
||||
sweepAngle,
|
||||
false,
|
||||
backgroundPaint,
|
||||
);
|
||||
|
||||
// Value arc
|
||||
if (value > 0) {
|
||||
final valuePaint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = strokeWidth
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
canvas.drawArc(
|
||||
Rect.fromCircle(center: center, radius: radius),
|
||||
startAngle,
|
||||
sweepAngle * value,
|
||||
false,
|
||||
valuePaint,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_GaugePainter oldDelegate) {
|
||||
return oldDelegate.value != value ||
|
||||
oldDelegate.color != color ||
|
||||
oldDelegate.backgroundColor != backgroundColor ||
|
||||
oldDelegate.strokeWidth != strokeWidth;
|
||||
}
|
||||
}
|
||||
|
||||
/// A row of gauge widgets with consistent sizing.
|
||||
class GaugeRow extends StatelessWidget {
|
||||
const GaugeRow({
|
||||
super.key,
|
||||
required this.gauges,
|
||||
this.gaugeSize = 100,
|
||||
});
|
||||
|
||||
final List<GaugeData> gauges;
|
||||
final double gaugeSize;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
alignment: WrapAlignment.center,
|
||||
children: gauges
|
||||
.map(
|
||||
(data) => GaugeWidget(
|
||||
value: data.value,
|
||||
label: data.label,
|
||||
size: gaugeSize,
|
||||
color: data.color,
|
||||
icon: data.icon,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Data class for gauge configuration.
|
||||
class GaugeData {
|
||||
const GaugeData({
|
||||
required this.value,
|
||||
required this.label,
|
||||
this.color,
|
||||
this.icon,
|
||||
});
|
||||
|
||||
final double value;
|
||||
final String label;
|
||||
final Color? color;
|
||||
final IconData? icon;
|
||||
}
|
||||
Reference in New Issue
Block a user