status_cluster.dart 6.0 KB

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