Status bar: part lamps (CAN, OBD, IMU, CAM), drawn signal and battery icons
WiFi and LTE show signal in four steps with Android's traffic arrows; the battery is filled to its level in good (charging), caution (under 40 %) or alarm (under 20 %). OBD, IMU and CAM are lamps by CAN's rule; CAM reads orin-status' new cam_up and vision_up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,12 +12,21 @@ what the Orin publishes.
|
|||||||
|
|
||||||
## 1. Orin status in a top banner, like a phone's
|
## 1. Orin status in a top banner, like a phone's
|
||||||
|
|
||||||
**First cut (2026-10-15):** `widgets/status_bar.dart`, drawn by the shell over every view (the
|
**Built (2026-10-15):** `widgets/status_bar.dart`, drawn by the shell over every view (the
|
||||||
Dash's own top line is gone; its menu button moved in). Left: the menu (Dash only) and the
|
Dash's own top line is gone; its menu button moved in). Left: the menu (Dash only) and the
|
||||||
link. Right: Orin temperature (only from 80 °C), CAN (alarm when absent or bus-off), OBD
|
link. Right:
|
||||||
(only while the ELM is connected), GNSS fix icon and satellite count, LTE bars and
|
- Orin temperature, only from 80 °C.
|
||||||
technology (only with a modem), WiFi bars, UPS battery and % (caution on battery, alarm
|
- Lamps for the parts, by one rule (foreground up, caution degraded, alarm down, subdued
|
||||||
under 20 %), then the dark/bright button. Tapping the strip does nothing yet.
|
unknown): CAN (`can_up`, bus state), OBD (`elm_connected`), IMU (its values fresh or
|
||||||
|
stale), CAM (`cam_up`, caution when `vision_up` is false; both new in orin-status).
|
||||||
|
- GNSS fix icon and satellite count.
|
||||||
|
- LTE (only with a modem) and WiFi: signal in four steps, Android's up/down traffic arrows
|
||||||
|
(lit above 2 kbit/s).
|
||||||
|
- UPS battery, filled to its level: good while charging, caution under 40 %, alarm under
|
||||||
|
20 %, foreground otherwise.
|
||||||
|
- The dark/bright button.
|
||||||
|
|
||||||
|
Tapping the strip does nothing yet.
|
||||||
What was asked:
|
What was asked:
|
||||||
|
|
||||||
A thin strip across the top of the app (the Dash's own top line included) showing the Orin
|
A thin strip across the top of the app (the Dash's own top line included) showing the Orin
|
||||||
|
|||||||
+2
-1
@@ -29,7 +29,8 @@ pirate-station-aide's (main views, then the rarely used ones grouped as Tools).
|
|||||||
| `AccelGraph` | `widgets/accel_graph.dart` | G-meter: a dot for lateral/longitudinal g, a trace and a peak marker over a window | smart-serow |
|
| `AccelGraph` | `widgets/accel_graph.dart` | G-meter: a dot for lateral/longitudinal g, a trace and a peak marker over a window | smart-serow |
|
||||||
| `GpsCompass` | `widgets/gps_compass.dart` | Heading arrow and bearing; "ACQ" while acquiring, "N/A" without | smart-serow, from gps-speedo |
|
| `GpsCompass` | `widgets/gps_compass.dart` | Heading arrow and bearing; "ACQ" while acquiring, "N/A" without | smart-serow, from gps-speedo |
|
||||||
| `LinkChip`, `LinkDetails` | `widgets/link_status.dart` | The link's state in good / caution / alarm; address, last-message age, clock offset, USB link | Yukart |
|
| `LinkChip`, `LinkDetails` | `widgets/link_status.dart` | The link's state in good / caution / alarm; address, last-message age, clock offset, USB link | Yukart |
|
||||||
| `StatusBar` | `widgets/status_bar.dart` | The Orin like a phone's status bar, over every view: link, then hot temperature, CAN, OBD (while connected), GNSS fix and satellites, LTE, WiFi, UPS; and the dark/bright button | Yukart |
|
| `StatusBar` | `widgets/status_bar.dart` | The Orin like a phone's status bar, over every view: link; hot temperature; CAN, OBD, IMU, CAM lamps; GNSS fix and satellites; LTE and WiFi signal with traffic arrows; UPS battery; the dark/bright button | Yukart |
|
||||||
|
| `BatteryIcon`, `SignalIcon`, `TrafficArrows` | `widgets/status_icons.dart` | The status bar's drawn icons: a battery filled to its level in a role colour, four-step WiFi and cellular signal, Android's up/down traffic pair | Yukart |
|
||||||
| `MockBanner` | `widgets/mock_banner.dart` | "MOCK DATA — NOT FROM THE CAR" in caution, over every page while the gateway is mocking | Yukart |
|
| `MockBanner` | `widgets/mock_banner.dart` | "MOCK DATA — NOT FROM THE CAR" in caution, over every page while the gateway is mocking | Yukart |
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|||||||
+159
-105
@@ -1,17 +1,22 @@
|
|||||||
// The Orin at a glance, the way a phone's status bar shows the phone: the link on the
|
// The Orin at a glance, the way a phone's status bar shows the phone: the link on the
|
||||||
// left, then its links out and its power on the right, and the dark/bright switch.
|
// left, then its parts, links out and power on the right, and the dark/bright switch.
|
||||||
// Drawn by the shell above every view (DASH-NEXT.md item 1).
|
// Drawn by the shell above every view (DASH-NEXT.md item 1).
|
||||||
//
|
//
|
||||||
// An indicator shows its state in palette roles: foreground when fine, caution when
|
// Colours are palette roles: foreground when fine, good for charging, caution when
|
||||||
// degraded or on battery, alarm when down, subdued when stale or unknown. Things
|
// degraded or low, alarm when down, subdued when unknown. The parts (CAN, OBD, IMU, CAM)
|
||||||
// that are only news when wrong (temperature) appear only then; ones that are only
|
// are lamps by one rule; WiFi and LTE show signal in four steps and Android's traffic
|
||||||
// news when present (the ELM, a modem) appear only then.
|
// arrows; the battery's fill shows its level in its colour. Temperature appears only
|
||||||
|
// when hot, LTE only with a modem.
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../link/boundary.dart';
|
import '../link/boundary.dart';
|
||||||
import '../link/reading.dart';
|
import '../link/reading.dart';
|
||||||
import '../theme/palette.dart';
|
import '../theme/palette.dart';
|
||||||
|
import 'status_icons.dart';
|
||||||
|
|
||||||
|
/// A part's state, for its lamp.
|
||||||
|
enum _Part { up, degraded, down, unknown }
|
||||||
|
|
||||||
class StatusBar extends StatelessWidget {
|
class StatusBar extends StatelessWidget {
|
||||||
const StatusBar({
|
const StatusBar({
|
||||||
@@ -28,6 +33,9 @@ class StatusBar extends StatelessWidget {
|
|||||||
|
|
||||||
static const height = 32.0;
|
static const height = 32.0;
|
||||||
|
|
||||||
|
/// Traffic arrows light above this rate (kbit/s): over background chatter.
|
||||||
|
static const trafficKbps = 2.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final p = AppTheme.of(context);
|
final p = AppTheme.of(context);
|
||||||
@@ -57,16 +65,14 @@ class StatusBar extends StatelessWidget {
|
|||||||
// Without the link, every Orin value is old news.
|
// Without the link, every Orin value is old news.
|
||||||
if (up) ...[
|
if (up) ...[
|
||||||
..._temperature(p, r('orin_tj_temp_c')),
|
..._temperature(p, r('orin_tj_temp_c')),
|
||||||
..._can(p, r('can_up'), r('can_bus_state')),
|
_lamp(p, 'CAN', _can(r('can_up'), r('can_bus_state'))),
|
||||||
..._elm(p, r('elm_connected')),
|
_lamp(p, 'OBD', _flag(r('elm_connected'))),
|
||||||
|
_lamp(p, 'IMU', _fresh(b, 'imu_roll_deg')),
|
||||||
|
_lamp(p, 'CAM', _cam(r('cam_up'), r('vision_up'))),
|
||||||
|
const SizedBox(width: 4),
|
||||||
..._gnss(p, r('gnss_fix'), r('gnss_satellites')),
|
..._gnss(p, r('gnss_fix'), r('gnss_satellites')),
|
||||||
..._lte(
|
..._lte(p, r),
|
||||||
p,
|
..._wifi(p, r),
|
||||||
r('net_lte_state'),
|
|
||||||
r('net_lte_tech'),
|
|
||||||
r('net_lte_signal_pct'),
|
|
||||||
),
|
|
||||||
..._wifi(p, r('net_wifi_up'), r('net_wifi_signal_dbm')),
|
|
||||||
..._ups(p, r('ups_state'), r('ups_battery_pct')),
|
..._ups(p, r('ups_state'), r('ups_battery_pct')),
|
||||||
],
|
],
|
||||||
_VariantButton(themes: themes),
|
_VariantButton(themes: themes),
|
||||||
@@ -77,44 +83,68 @@ class StatusBar extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One indicator: an icon, maybe a short text, and the gap after it.
|
/// One indicator: its pieces, and the gap after it.
|
||||||
static List<Widget> _item(Color c, {IconData? icon, String? text}) => [
|
static List<Widget> _item(List<Widget> pieces) => [
|
||||||
if (icon != null) Icon(icon, color: c),
|
...pieces,
|
||||||
if (icon != null && text != null) const SizedBox(width: 2),
|
|
||||||
if (text != null) Text(text, style: TextStyle(color: c)),
|
|
||||||
const SizedBox(width: 14),
|
const SizedBox(width: 14),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
static Widget _lamp(Palette p, String label, _Part s) => Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 10),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: switch (s) {
|
||||||
|
_Part.up => p.foreground,
|
||||||
|
_Part.degraded => p.caution,
|
||||||
|
_Part.down => p.alarm,
|
||||||
|
_Part.unknown => p.subdued,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
static _Part _flag(Reading on) => on.value == null
|
||||||
|
? _Part.unknown
|
||||||
|
: on.dim
|
||||||
|
? _Part.unknown
|
||||||
|
: on.on
|
||||||
|
? _Part.up
|
||||||
|
: _Part.down;
|
||||||
|
|
||||||
|
static _Part _can(Reading up, Reading state) => switch (_flag(up)) {
|
||||||
|
_Part.up => switch (state.value) {
|
||||||
|
'BUS-OFF' || 'STOPPED' => _Part.down,
|
||||||
|
'ERROR-WARNING' || 'ERROR-PASSIVE' => _Part.degraded,
|
||||||
|
_ => _Part.up,
|
||||||
|
},
|
||||||
|
final s => s,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A part known only by its values arriving: fresh is up, stale is down.
|
||||||
|
static _Part _fresh(Boundary b, String name) {
|
||||||
|
final e = b.signals[name];
|
||||||
|
if (e is! Map) return _Part.unknown;
|
||||||
|
return (e['quality'] as String? ?? 'ok') == 'ok' ? _Part.up : _Part.down;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The camera: present and served is up; present but not served, degraded.
|
||||||
|
static _Part _cam(Reading cam, Reading vision) => switch (_flag(cam)) {
|
||||||
|
_Part.up => _flag(vision) == _Part.down ? _Part.degraded : _Part.up,
|
||||||
|
final s => s,
|
||||||
|
};
|
||||||
|
|
||||||
/// Only when hot: caution from 80 °C, alarm from 90 (the Orin throttles near 99).
|
/// Only when hot: caution from 80 °C, alarm from 90 (the Orin throttles near 99).
|
||||||
static List<Widget> _temperature(Palette p, Reading t) {
|
static List<Widget> _temperature(Palette p, Reading t) {
|
||||||
final c = t.number;
|
final c = t.number;
|
||||||
if (t.dim || c == null || c < 80) return [];
|
if (t.dim || c == null || c < 80) return [];
|
||||||
return _item(
|
final color = c >= 90 ? p.alarm : p.caution;
|
||||||
c >= 90 ? p.alarm : p.caution,
|
return _item([
|
||||||
icon: Icons.thermostat,
|
Icon(Icons.thermostat, color: color),
|
||||||
text: '${c.round()}°',
|
Text('${c.round()}°', style: TextStyle(color: color)),
|
||||||
);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<Widget> _can(Palette p, Reading up, Reading state) {
|
|
||||||
if (up.value == null) return _item(p.subdued, text: 'CAN');
|
|
||||||
final color = switch ((up.on, state.value)) {
|
|
||||||
(false, _) || (_, 'BUS-OFF') || (_, 'STOPPED') => p.alarm,
|
|
||||||
(_, 'ERROR-WARNING') || (_, 'ERROR-PASSIVE') => p.caution,
|
|
||||||
_ => p.foreground,
|
|
||||||
};
|
|
||||||
return _item(up.dim ? p.subdued : color, text: 'CAN');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The OBD-II adapter, only while connected (it is for calibration, not always on).
|
|
||||||
static List<Widget> _elm(Palette p, Reading c) => c.on
|
|
||||||
? _item(
|
|
||||||
c.dim ? p.subdued : p.foreground,
|
|
||||||
icon: Icons.bluetooth_connected,
|
|
||||||
text: 'OBD',
|
|
||||||
)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
static List<Widget> _gnss(Palette p, Reading fix, Reading sats) {
|
static List<Widget> _gnss(Palette p, Reading fix, Reading sats) {
|
||||||
final n = sats.number?.round();
|
final n = sats.number?.round();
|
||||||
final (icon, color) = switch (fix.value) {
|
final (icon, color) = switch (fix.value) {
|
||||||
@@ -123,85 +153,109 @@ class StatusBar extends StatelessWidget {
|
|||||||
'none' => (Icons.gps_not_fixed, p.caution),
|
'none' => (Icons.gps_not_fixed, p.caution),
|
||||||
_ => (Icons.gps_off, p.subdued),
|
_ => (Icons.gps_off, p.subdued),
|
||||||
};
|
};
|
||||||
return _item(
|
final c = fix.dim ? p.subdued : color;
|
||||||
fix.dim ? p.subdued : color,
|
return _item([
|
||||||
icon: icon,
|
Icon(icon, color: c),
|
||||||
text: n == null ? null : '$n',
|
if (n != null) ...[
|
||||||
);
|
const SizedBox(width: 2),
|
||||||
|
Text('$n', style: TextStyle(color: c)),
|
||||||
|
],
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The modem, when there is one: bars from its signal, and the technology.
|
static Widget _arrows(Reading Function(String) r, String net, Color c) =>
|
||||||
static List<Widget> _lte(
|
TrafficArrows(
|
||||||
Palette p,
|
up: (r('net_${net}_tx_kbps').number ?? 0) > trafficKbps,
|
||||||
Reading state,
|
down: (r('net_${net}_rx_kbps').number ?? 0) > trafficKbps,
|
||||||
Reading tech,
|
color: c,
|
||||||
Reading pct,
|
);
|
||||||
) {
|
|
||||||
|
/// The modem, when there is one: signal, traffic, and the technology.
|
||||||
|
static List<Widget> _lte(Palette p, Reading Function(String) r) {
|
||||||
|
final state = r('net_lte_state');
|
||||||
final s = state.value as String?;
|
final s = state.value as String?;
|
||||||
if (s == null) return [];
|
if (s == null) return [];
|
||||||
final q = pct.number ?? 0;
|
final q = r('net_lte_signal_pct').number ?? 0;
|
||||||
final t = (tech.value as String? ?? '').toUpperCase();
|
final t = (r('net_lte_tech').value as String? ?? '').toUpperCase();
|
||||||
final (icon, color) = switch (s) {
|
final color = state.dim
|
||||||
'connected' => (
|
? p.subdued
|
||||||
q >= 60
|
: switch (s) {
|
||||||
? Icons.signal_cellular_alt
|
'connected' => p.foreground,
|
||||||
: q >= 30
|
|
||||||
? Icons.signal_cellular_alt_2_bar
|
|
||||||
: Icons.signal_cellular_alt_1_bar,
|
|
||||||
p.foreground,
|
|
||||||
),
|
|
||||||
'registered' ||
|
'registered' ||
|
||||||
'searching' ||
|
'searching' ||
|
||||||
'connecting' ||
|
'connecting' ||
|
||||||
'enabling' => (Icons.signal_cellular_alt_1_bar, p.caution),
|
'enabling' => p.caution,
|
||||||
_ => (Icons.signal_cellular_off, p.subdued),
|
_ => p.subdued,
|
||||||
};
|
};
|
||||||
return _item(
|
final connected = s == 'connected';
|
||||||
state.dim ? p.subdued : color,
|
final bars = q >= 80
|
||||||
icon: icon,
|
? 4
|
||||||
text: t.isEmpty ? null : t,
|
: q >= 60
|
||||||
);
|
? 3
|
||||||
|
: q >= 40
|
||||||
|
? 2
|
||||||
|
: q >= 20
|
||||||
|
? 1
|
||||||
|
: 0;
|
||||||
|
return _item([
|
||||||
|
if (t.isNotEmpty) ...[
|
||||||
|
Text(t, style: TextStyle(color: color, fontSize: 11)),
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
],
|
||||||
|
SignalIcon.cellular(
|
||||||
|
bars: s == 'disabled' || s == 'failed' ? null : bars,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
if (connected) ...[const SizedBox(width: 1), _arrows(r, 'lte', color)],
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<Widget> _wifi(Palette p, Reading up, Reading dbm) {
|
static List<Widget> _wifi(Palette p, Reading Function(String) r) {
|
||||||
if (!up.on) return _item(p.subdued, icon: Icons.wifi_off);
|
final up = r('net_wifi_up');
|
||||||
final d = dbm.number ?? -100;
|
if (!up.on) {
|
||||||
final icon = d >= -60
|
return _item([SignalIcon.wifi(bars: null, color: p.subdued)]);
|
||||||
? Icons.wifi
|
}
|
||||||
: d >= -70
|
final d = r('net_wifi_signal_dbm').number ?? -100;
|
||||||
? Icons.wifi_2_bar
|
final bars = d >= -55
|
||||||
: Icons.wifi_1_bar;
|
? 4
|
||||||
return _item(up.dim ? p.subdued : p.foreground, icon: icon);
|
: d >= -65
|
||||||
|
? 3
|
||||||
|
: d >= -75
|
||||||
|
? 2
|
||||||
|
: d >= -85
|
||||||
|
? 1
|
||||||
|
: 0;
|
||||||
|
final color = up.dim ? p.subdued : p.foreground;
|
||||||
|
return _item([
|
||||||
|
SignalIcon.wifi(bars: bars, color: color),
|
||||||
|
const SizedBox(width: 1),
|
||||||
|
_arrows(r, 'wifi', color),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The UPS: caution on battery (the car is off), alarm under 20 %.
|
/// The UPS: the fill in good while charging, caution under 40 %, alarm under 20 %.
|
||||||
static List<Widget> _ups(Palette p, Reading state, Reading pct) {
|
static List<Widget> _ups(Palette p, Reading state, Reading pct) {
|
||||||
final n = pct.number;
|
final n = pct.number;
|
||||||
if (n == null) return _item(p.subdued, icon: Icons.battery_unknown);
|
final fill = n == null || pct.dim
|
||||||
const bars = [
|
? p.subdued
|
||||||
Icons.battery_0_bar,
|
: state.value == 'charging'
|
||||||
Icons.battery_1_bar,
|
? p.good
|
||||||
Icons.battery_2_bar,
|
: n < 20
|
||||||
Icons.battery_3_bar,
|
|
||||||
Icons.battery_4_bar,
|
|
||||||
Icons.battery_5_bar,
|
|
||||||
Icons.battery_6_bar,
|
|
||||||
Icons.battery_full,
|
|
||||||
];
|
|
||||||
final charging = state.value == 'charging';
|
|
||||||
final icon = charging
|
|
||||||
? Icons.battery_charging_full
|
|
||||||
: bars[(n / 100 * 7).round().clamp(0, 7)];
|
|
||||||
final color = n < 20
|
|
||||||
? p.alarm
|
? p.alarm
|
||||||
: state.value == 'discharging'
|
: n < 40
|
||||||
? p.caution
|
? p.caution
|
||||||
: p.foreground;
|
: p.foreground;
|
||||||
return _item(
|
return _item([
|
||||||
pct.dim ? p.subdued : color,
|
BatteryIcon(
|
||||||
icon: icon,
|
level: n == null ? null : n / 100,
|
||||||
text: '${n.round()}%',
|
fill: fill,
|
||||||
);
|
outline: pct.dim ? p.subdued : p.foreground,
|
||||||
|
),
|
||||||
|
if (n != null) ...[
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text('${n.round()}%', style: TextStyle(color: fill)),
|
||||||
|
],
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
// The status bar's drawn icons, Android-like: a battery whose fill shows level and
|
||||||
|
// colour, WiFi and cellular signal in four steps, and the traffic arrow pair. Drawn
|
||||||
|
// rather than taken from the icon font, which has no four-step cellular icon and no
|
||||||
|
// separately coloured battery fill.
|
||||||
|
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A horizontal battery: [outline] for the case, the inside filled to [level]
|
||||||
|
/// (0–1, null unknown) in [fill].
|
||||||
|
class BatteryIcon extends StatelessWidget {
|
||||||
|
const BatteryIcon({
|
||||||
|
super.key,
|
||||||
|
required this.level,
|
||||||
|
required this.fill,
|
||||||
|
required this.outline,
|
||||||
|
});
|
||||||
|
final double? level;
|
||||||
|
final Color fill;
|
||||||
|
final Color outline;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => CustomPaint(
|
||||||
|
size: const Size(24, 12),
|
||||||
|
painter: _Battery(level, fill, outline),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Battery extends CustomPainter {
|
||||||
|
_Battery(this.level, this.fill, this.outline);
|
||||||
|
final double? level;
|
||||||
|
final Color fill;
|
||||||
|
final Color outline;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas c, Size s) {
|
||||||
|
const nub = 2.0;
|
||||||
|
final body = Rect.fromLTWH(0.75, 0.75, s.width - nub - 1.5, s.height - 1.5);
|
||||||
|
c.drawRRect(
|
||||||
|
RRect.fromRectAndRadius(body, const Radius.circular(2.5)),
|
||||||
|
Paint()
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 1.5
|
||||||
|
..color = outline,
|
||||||
|
);
|
||||||
|
c.drawRRect(
|
||||||
|
RRect.fromRectAndRadius(
|
||||||
|
Rect.fromLTWH(s.width - nub, s.height * 0.3, nub, s.height * 0.4),
|
||||||
|
const Radius.circular(1),
|
||||||
|
),
|
||||||
|
Paint()..color = outline,
|
||||||
|
);
|
||||||
|
final l = level;
|
||||||
|
if (l == null) return;
|
||||||
|
final inner = body.deflate(2);
|
||||||
|
c.drawRRect(
|
||||||
|
RRect.fromRectAndRadius(
|
||||||
|
Rect.fromLTWH(
|
||||||
|
inner.left,
|
||||||
|
inner.top,
|
||||||
|
inner.width * l.clamp(0, 1),
|
||||||
|
inner.height,
|
||||||
|
),
|
||||||
|
const Radius.circular(1),
|
||||||
|
),
|
||||||
|
Paint()..color = fill,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(_Battery o) =>
|
||||||
|
o.level != level || o.fill != fill || o.outline != outline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signal in [bars] of four (null: off, drawn crossed), WiFi's fan or cellular's
|
||||||
|
/// triangle; the unlit part faint.
|
||||||
|
class SignalIcon extends StatelessWidget {
|
||||||
|
const SignalIcon.wifi({super.key, required this.bars, required this.color})
|
||||||
|
: wifi = true;
|
||||||
|
const SignalIcon.cellular({
|
||||||
|
super.key,
|
||||||
|
required this.bars,
|
||||||
|
required this.color,
|
||||||
|
}) : wifi = false;
|
||||||
|
final int? bars;
|
||||||
|
final Color color;
|
||||||
|
final bool wifi;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => CustomPaint(
|
||||||
|
size: const Size(18, 16),
|
||||||
|
painter: _Signal(bars, color, wifi),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Signal extends CustomPainter {
|
||||||
|
_Signal(this.bars, this.color, this.wifi);
|
||||||
|
final int? bars;
|
||||||
|
final Color color;
|
||||||
|
final bool wifi;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas c, Size s) {
|
||||||
|
final faint = Paint()..color = color.withValues(alpha: 0.3);
|
||||||
|
final lit = Paint()..color = color;
|
||||||
|
final n = bars ?? 0;
|
||||||
|
if (wifi) {
|
||||||
|
// A quarter-circle fan standing on its point; lit from the point outwards.
|
||||||
|
final apex = Offset(s.width / 2, s.height);
|
||||||
|
Path fan(double r) => Path()
|
||||||
|
..moveTo(apex.dx, apex.dy)
|
||||||
|
..arcTo(
|
||||||
|
Rect.fromCircle(center: apex, radius: r),
|
||||||
|
-3 * math.pi / 4,
|
||||||
|
math.pi / 2,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
..close();
|
||||||
|
final full = s.height;
|
||||||
|
c.drawPath(fan(full), faint);
|
||||||
|
if (n > 0) c.drawPath(fan(full * (0.25 + 0.75 * n / 4)), lit);
|
||||||
|
} else {
|
||||||
|
// A right triangle rising to the right; lit from the left.
|
||||||
|
final tri = Path()
|
||||||
|
..moveTo(0, s.height)
|
||||||
|
..lineTo(s.width, s.height)
|
||||||
|
..lineTo(s.width, 0)
|
||||||
|
..close();
|
||||||
|
c.drawPath(tri, faint);
|
||||||
|
if (n > 0) {
|
||||||
|
c.save();
|
||||||
|
c.clipRect(
|
||||||
|
Rect.fromLTWH(0, 0, s.width * (0.2 + 0.8 * n / 4), s.height),
|
||||||
|
);
|
||||||
|
c.drawPath(tri, lit);
|
||||||
|
c.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bars == null) {
|
||||||
|
// Off: a stroke through it.
|
||||||
|
c.drawLine(
|
||||||
|
Offset(s.width * 0.15, s.height * 0.15),
|
||||||
|
Offset(s.width * 0.85, s.height * 0.95),
|
||||||
|
Paint()
|
||||||
|
..color = color
|
||||||
|
..strokeWidth = 1.5,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(_Signal o) =>
|
||||||
|
o.bars != bars || o.color != color || o.wifi != wifi;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Android's traffic pair beside a signal icon: up lit while sending, down while
|
||||||
|
/// receiving.
|
||||||
|
class TrafficArrows extends StatelessWidget {
|
||||||
|
const TrafficArrows({
|
||||||
|
super.key,
|
||||||
|
required this.up,
|
||||||
|
required this.down,
|
||||||
|
required this.color,
|
||||||
|
});
|
||||||
|
final bool up;
|
||||||
|
final bool down;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) =>
|
||||||
|
CustomPaint(size: const Size(6, 14), painter: _Arrows(up, down, color));
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Arrows extends CustomPainter {
|
||||||
|
_Arrows(this.up, this.down, this.color);
|
||||||
|
final bool up;
|
||||||
|
final bool down;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas c, Size s) {
|
||||||
|
Paint p(bool on) =>
|
||||||
|
Paint()..color = on ? color : color.withValues(alpha: 0.25);
|
||||||
|
final h = s.height / 2 - 1;
|
||||||
|
c.drawPath(
|
||||||
|
Path()
|
||||||
|
..moveTo(0, h)
|
||||||
|
..lineTo(s.width, h)
|
||||||
|
..lineTo(s.width / 2, 0)
|
||||||
|
..close(),
|
||||||
|
p(up),
|
||||||
|
);
|
||||||
|
c.drawPath(
|
||||||
|
Path()
|
||||||
|
..moveTo(0, s.height - h)
|
||||||
|
..lineTo(s.width, s.height - h)
|
||||||
|
..lineTo(s.width / 2, s.height)
|
||||||
|
..close(),
|
||||||
|
p(down),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(_Arrows o) =>
|
||||||
|
o.up != up || o.down != down || o.color != color;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user