status_cluster.dart 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. import 'package:flutter/material.dart';
  2. import '../../../core/theme/app_colors.dart';
  3. import '../../../core/widgets/app_icon.dart';
  4. import '../../../core/widgets/surface_card.dart';
  5. import '../../../data/models/system_status.dart';
  6. /// Quiet battery / Wi-Fi readout. It is information, not a toolbar.
  7. class StatusCluster extends StatelessWidget {
  8. const StatusCluster({super.key, required this.status, this.compact = false});
  9. final SystemStatus status;
  10. final bool compact;
  11. @override
  12. Widget build(BuildContext context) {
  13. final colors = context.colors;
  14. final theme = Theme.of(context);
  15. final batteryColor = status.batteryLevel <= 15 && !status.charging
  16. ? colors.danger
  17. : (status.charging ? colors.accent : colors.ink);
  18. return SurfaceCard(
  19. padding: EdgeInsets.symmetric(
  20. horizontal: compact ? 16 : 20,
  21. vertical: compact ? 10 : 12,
  22. ),
  23. radius: 999,
  24. color: colors.surface.withValues(alpha: 0.82),
  25. showShadow: false,
  26. child: Row(
  27. mainAxisSize: MainAxisSize.min,
  28. children: [
  29. Semantics(
  30. label: status.wifi ? 'Wi-Fi connected' : 'Wi-Fi disconnected',
  31. child: AppIcon(
  32. status.wifi
  33. ? 'assets/icons/wifi.svg'
  34. : 'assets/icons/wifi-off.svg',
  35. size: compact ? 18 : 20,
  36. color: status.wifi ? colors.ink : colors.inkFaint,
  37. ),
  38. ),
  39. SizedBox(width: compact ? 14 : 18),
  40. _BatteryGlyph(
  41. level: status.batteryLevel,
  42. charging: status.charging,
  43. color: batteryColor,
  44. ),
  45. const SizedBox(width: 8),
  46. Text(
  47. '${status.batteryLevel}%',
  48. style: theme.textTheme.labelLarge?.copyWith(
  49. color: colors.ink,
  50. fontSize: compact ? 13 : 14,
  51. ),
  52. ),
  53. if (status.charging) ...[
  54. const SizedBox(width: 6),
  55. Icon(
  56. Icons.bolt_rounded,
  57. size: compact ? 16 : 18,
  58. color: colors.accent,
  59. ),
  60. ],
  61. ],
  62. ),
  63. );
  64. }
  65. }
  66. class _BatteryGlyph extends StatelessWidget {
  67. const _BatteryGlyph({
  68. required this.level,
  69. required this.charging,
  70. required this.color,
  71. });
  72. final int level;
  73. final bool charging;
  74. final Color color;
  75. @override
  76. Widget build(BuildContext context) {
  77. return SizedBox(
  78. width: 28,
  79. height: 15,
  80. child: CustomPaint(
  81. painter: _BatteryPainter(
  82. level: level,
  83. color: color,
  84. charging: charging,
  85. ),
  86. ),
  87. );
  88. }
  89. }
  90. class _BatteryPainter extends CustomPainter {
  91. _BatteryPainter({
  92. required this.level,
  93. required this.color,
  94. required this.charging,
  95. });
  96. final int level;
  97. final Color color;
  98. final bool charging;
  99. @override
  100. void paint(Canvas canvas, Size size) {
  101. final body = Rect.fromLTWH(0, 1, size.width - 4, size.height - 2);
  102. final radius = Radius.circular(body.height / 2);
  103. final outline = Paint()
  104. ..color = color.withValues(alpha: 0.52)
  105. ..style = PaintingStyle.stroke
  106. ..strokeWidth = 1.3;
  107. final cap = Paint()..color = color.withValues(alpha: 0.52);
  108. canvas.drawRRect(RRect.fromRectAndRadius(body, radius), outline);
  109. canvas.drawRRect(
  110. RRect.fromRectAndRadius(
  111. Rect.fromLTWH(
  112. size.width - 3.5,
  113. size.height * 0.34,
  114. 2.5,
  115. size.height * 0.32,
  116. ),
  117. const Radius.circular(1),
  118. ),
  119. cap,
  120. );
  121. final inset = body.deflate(2.4);
  122. final fillWidth = inset.width * (level.clamp(0, 100) / 100);
  123. if (fillWidth > 0) {
  124. final fill = Paint()..color = color;
  125. canvas.drawRRect(
  126. RRect.fromRectAndRadius(
  127. Rect.fromLTWH(inset.left, inset.top, fillWidth, inset.height),
  128. const Radius.circular(2.4),
  129. ),
  130. fill,
  131. );
  132. }
  133. }
  134. @override
  135. bool shouldRepaint(covariant _BatteryPainter oldDelegate) {
  136. return oldDelegate.level != level ||
  137. oldDelegate.color != color ||
  138. oldDelegate.charging != charging;
  139. }
  140. }