status_cluster.dart 5.1 KB

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