status_cluster.dart 4.2 KB

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