Palette: seven roles (accent, good, caution added; highlight renamed alarm); no hard-coded colours left
background, foreground, subdued, accent (emphasis without danger), and the state gradient good → caution → alarm; surface and onFill derived. smart-serow's four-key themes still load (highlight = alarm; the new roles fall back). Material's scheme follows: buttons in accent instead of the alarm colour. Link state, mock banner, signal tiles, raw log, checklist button and marks, camera page all take palette roles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
This commit is contained in:
@@ -2,13 +2,19 @@
|
|||||||
"dark": {
|
"dark": {
|
||||||
"background": "#000000",
|
"background": "#000000",
|
||||||
"foreground": "#FFFFFF",
|
"foreground": "#FFFFFF",
|
||||||
"highlight": "#FF5555",
|
"subdued": "#808080",
|
||||||
"subdued": "#808080"
|
"accent": "#5EC8F2",
|
||||||
|
"good": "#6CCB5F",
|
||||||
|
"caution": "#FFC107",
|
||||||
|
"alarm": "#FF5555"
|
||||||
},
|
},
|
||||||
"bright": {
|
"bright": {
|
||||||
"background": "#F0F0F0",
|
"background": "#F0F0F0",
|
||||||
"foreground": "#1A1A1A",
|
"foreground": "#1A1A1A",
|
||||||
"highlight": "#CC0000",
|
"subdued": "#606060",
|
||||||
"subdued": "#606060"
|
"accent": "#0277BD",
|
||||||
|
"good": "#2E7D32",
|
||||||
|
"caution": "#8D6E00",
|
||||||
|
"alarm": "#CC0000"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-4
@@ -2,13 +2,19 @@
|
|||||||
"dark": {
|
"dark": {
|
||||||
"background": "#303030",
|
"background": "#303030",
|
||||||
"foreground": "#EAEAEA",
|
"foreground": "#EAEAEA",
|
||||||
"highlight": "#FA1504",
|
"subdued": "#fda052",
|
||||||
"subdued": "#fda052"
|
"accent": "#5EC8F2",
|
||||||
|
"good": "#7CC46A",
|
||||||
|
"caution": "#FFD54F",
|
||||||
|
"alarm": "#FA1504"
|
||||||
},
|
},
|
||||||
"bright": {
|
"bright": {
|
||||||
"background": "#fda052",
|
"background": "#fda052",
|
||||||
"foreground": "#303030",
|
"foreground": "#303030",
|
||||||
"highlight": "#df2100",
|
"subdued": "#EAEAEA",
|
||||||
"subdued": "#EAEAEA"
|
"accent": "#0D47A1",
|
||||||
|
"good": "#1B5E20",
|
||||||
|
"caution": "#5D4000",
|
||||||
|
"alarm": "#df2100"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-3
@@ -17,7 +17,7 @@ Laid out like smart-serow's HUD (`pi/ui/lib/`), which several pieces come from.
|
|||||||
|
|
||||||
| Widget | File | Shows | From |
|
| Widget | File | Shows | From |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `StatBox`, `StatBoxMain` | `widgets/stat_box.dart` | A value, its unit and label, sized from its box's height; highlight on warning, subdued when dim | smart-serow |
|
| `StatBox`, `StatBoxMain` | `widgets/stat_box.dart` | A value, its unit and label, sized from its box's height; alarm on warning, subdued when dim | smart-serow |
|
||||||
| `Telltale` | `widgets/telltale.dart` | An on/off lamp: a label or an icon, lit or dark, "—" when missing | Yukart |
|
| `Telltale` | `widgets/telltale.dart` | An on/off lamp: a label or an icon, lit or dark, "—" when missing | Yukart |
|
||||||
| `WhiskeyMark` | `widgets/whiskey_mark.dart` | Attitude: a horizon tilting with roll, ladder moving with pitch | smart-serow |
|
| `WhiskeyMark` | `widgets/whiskey_mark.dart` | Attitude: a horizon tilting with roll, ladder moving with pitch | 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 |
|
| `AccelGraph` | `widgets/accel_graph.dart` | G-meter: a dot for lateral/longitudinal g, a trace and a peak marker over a window | smart-serow |
|
||||||
@@ -27,8 +27,11 @@ Laid out like smart-serow's HUD (`pi/ui/lib/`), which several pieces come from.
|
|||||||
|
|
||||||
- **The boxed pattern.** A widget takes its share of a row and sizes its type from its own height, so a layout
|
- **The boxed pattern.** A widget takes its share of a row and sizes its type from its own height, so a layout
|
||||||
is a grid of boxes and a value gaining a digit never moves anything. Labels shrink to fit, never wrap.
|
is a grid of boxes and a value gaining a digit never moves anything. Labels shrink to fit, never wrap.
|
||||||
- **Colours by meaning.** `AppTheme.of(context)` gives the palette's `background`, `foreground`, `highlight` and
|
- **Colours by meaning.** `AppTheme.of(context)` gives seven roles: `background`, `foreground`,
|
||||||
`subdued`; no widget names a colour. Themes are smart-serow's JSON (`assets/themes/`), `rei` by default.
|
`subdued` (muted, missing, stale), `accent` (emphasis without danger: buttons, links, the current item),
|
||||||
|
and the state gradient `good` → `caution` → `alarm`; plus the derived `surface` (raised bars) and
|
||||||
|
`onFill(colour)` (text on a fill). No widget names a colour. Themes are JSON in `assets/themes/`, `rei` by
|
||||||
|
default; smart-serow's four-key files still load (their `highlight` is `alarm`).
|
||||||
- **Cosmetic fonts are opt-in.** A widget that wants the HUD face names `fontFamily: hudFont` in its own
|
- **Cosmetic fonts are opt-in.** A widget that wants the HUD face names `fontFamily: hudFont` in its own
|
||||||
`TextStyle`; everything else keeps the basic font, which covers CJK where DIN 1451 does not.
|
`TextStyle`; everything else keeps the basic font, which covers CJK where DIN 1451 does not.
|
||||||
- **Missing is a state** (app guideline 2): a widget given no value draws "—" or subdued, never an error.
|
- **Missing is a state** (app guideline 2): a widget given no value draws "—" or subdued, never an error.
|
||||||
|
|||||||
+4
-2
@@ -10,6 +10,8 @@ import 'dart:typed_data';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'theme/palette.dart';
|
||||||
|
|
||||||
const camviewPort = 7710;
|
const camviewPort = 7710;
|
||||||
|
|
||||||
class CamView extends ChangeNotifier {
|
class CamView extends ChangeNotifier {
|
||||||
@@ -165,7 +167,7 @@ class _CamViewPageState extends State<CamViewPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: AppTheme.of(context).background,
|
||||||
appBar: AppBar(title: const Text('Camera (test)')),
|
appBar: AppBar(title: const Text('Camera (test)')),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: ListenableBuilder(
|
child: ListenableBuilder(
|
||||||
@@ -182,7 +184,7 @@ class _CamViewPageState extends State<CamViewPage> {
|
|||||||
cam.available == false
|
cam.available == false
|
||||||
? 'Camera unavailable on the Orin'
|
? 'Camera unavailable on the Orin'
|
||||||
: cam.status,
|
: cam.status,
|
||||||
style: const TextStyle(color: Colors.white70),
|
style: TextStyle(color: AppTheme.of(context).subdued),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Image.memory(
|
: Image.memory(
|
||||||
|
|||||||
+12
-8
@@ -18,6 +18,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'boundary.dart';
|
import 'boundary.dart';
|
||||||
|
import 'theme/palette.dart';
|
||||||
|
|
||||||
const checklistAssets = 'assets/contracts/checklists/';
|
const checklistAssets = 'assets/contracts/checklists/';
|
||||||
|
|
||||||
@@ -309,14 +310,17 @@ class _ChecklistPageState extends State<ChecklistPage> {
|
|||||||
Widget _panel(BuildContext context) {
|
Widget _panel(BuildContext context) {
|
||||||
final s = run.current;
|
final s = run.current;
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final p = AppTheme.of(context);
|
||||||
final a = run.currentAction;
|
final a = run.currentAction;
|
||||||
|
// The big button by what pressing it does: start (good), stop a running span
|
||||||
|
// (caution: something is in progress), a point mark (accent), skip on (subdued).
|
||||||
final (Color color, IconData icon) = run.stepDone
|
final (Color color, IconData icon) = run.stepDone
|
||||||
? (Colors.blueGrey, Icons.skip_next)
|
? (p.subdued, Icons.skip_next)
|
||||||
: a!.point
|
: a!.point
|
||||||
? (Colors.blue, Icons.flag)
|
? (p.accent, Icons.flag)
|
||||||
: run.inSpan
|
: run.inSpan
|
||||||
? (Colors.red.shade700, Icons.stop)
|
? (p.caution, Icons.stop)
|
||||||
: (Colors.green.shade700, Icons.play_arrow);
|
: (p.good, Icons.play_arrow);
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -339,7 +343,7 @@ class _ChecklistPageState extends State<ChecklistPage> {
|
|||||||
child: FilledButton(
|
child: FilledButton(
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
backgroundColor: color,
|
backgroundColor: color,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: p.onFill(color),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||||
),
|
),
|
||||||
onPressed: run.finished ? null : () => _send(run.press()),
|
onPressed: run.finished ? null : () => _send(run.press()),
|
||||||
@@ -394,7 +398,7 @@ class _ChecklistPageState extends State<ChecklistPage> {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontFeatures: const [FontFeature.tabularFigures()],
|
fontFeatures: const [FontFeature.tabularFigures()],
|
||||||
color: reached ? Colors.lightGreenAccent : Colors.white,
|
color: reached ? AppTheme.of(context).good : AppTheme.of(context).foreground,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -409,9 +413,9 @@ class _ChecklistPageState extends State<ChecklistPage> {
|
|||||||
final pending = b.pending.length;
|
final pending = b.pending.length;
|
||||||
final ack = b.acks.where((x) => x.id == last.id).firstOrNull;
|
final ack = b.acks.where((x) => x.id == last.id).firstOrNull;
|
||||||
final (String status, Color? color) = ack != null
|
final (String status, Color? color) = ack != null
|
||||||
? (ack.ok ? ('✓', null) : ('✗ ${ack.detail}', Colors.redAccent))
|
? (ack.ok ? ('✓', AppTheme.of(context).good) : ('✗ ${ack.detail}', AppTheme.of(context).alarm))
|
||||||
: b.pending.any((p) => p.id == last.id)
|
: b.pending.any((p) => p.id == last.id)
|
||||||
? ('… unacked', Colors.orange)
|
? ('… unacked', AppTheme.of(context).caution)
|
||||||
: ('?', null);
|
: ('?', null);
|
||||||
return Text(
|
return Text(
|
||||||
'"${last.text}" $status${pending > 1 ? ' · $pending waiting' : ''}',
|
'"${last.text}" $status${pending > 1 ? ' · $pending waiting' : ''}',
|
||||||
|
|||||||
+4
-4
@@ -49,13 +49,13 @@ class DashPage extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
if (b.mock)
|
if (b.mock)
|
||||||
Container(
|
Container(
|
||||||
color: p.highlight,
|
color: p.caution,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
child: Text(
|
child: Text(
|
||||||
'MOCK DATA — NOT FROM THE CAR',
|
'MOCK DATA — NOT FROM THE CAR',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: p.background,
|
color: p.onFill(p.caution),
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
letterSpacing: 2,
|
letterSpacing: 2,
|
||||||
),
|
),
|
||||||
@@ -186,8 +186,8 @@ class _DashBar extends StatelessWidget {
|
|||||||
final p = AppTheme.of(context);
|
final p = AppTheme.of(context);
|
||||||
final (text, color) = switch (boundary.state) {
|
final (text, color) = switch (boundary.state) {
|
||||||
LinkState.up => ('LINK', p.subdued),
|
LinkState.up => ('LINK', p.subdued),
|
||||||
LinkState.connecting => ('CONNECTING', p.highlight),
|
LinkState.connecting => ('CONNECTING', p.caution),
|
||||||
LinkState.down => ('NO LINK', p.highlight),
|
LinkState.down => ('NO LINK', p.alarm),
|
||||||
};
|
};
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 40,
|
height: 40,
|
||||||
|
|||||||
+30
-17
@@ -169,14 +169,15 @@ class MockBanner extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final p = AppTheme.of(context);
|
||||||
return Container(
|
return Container(
|
||||||
color: Colors.amber.shade700,
|
color: p.caution,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||||
child: const Text(
|
child: Text(
|
||||||
'MOCK DATA — NOT FROM THE CAR',
|
'MOCK DATA — NOT FROM THE CAR',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black,
|
color: p.onFill(p.caution),
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
letterSpacing: 2,
|
letterSpacing: 2,
|
||||||
@@ -200,10 +201,11 @@ class LinkBar extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final b = boundary;
|
final b = boundary;
|
||||||
|
final p = AppTheme.of(context);
|
||||||
final (color, label) = switch (b.state) {
|
final (color, label) = switch (b.state) {
|
||||||
LinkState.up => (Colors.green, 'CONNECTED'),
|
LinkState.up => (p.good, 'CONNECTED'),
|
||||||
LinkState.connecting => (Colors.orange, 'CONNECTING'),
|
LinkState.connecting => (p.caution, 'CONNECTING'),
|
||||||
LinkState.down => (Colors.red, 'DOWN'),
|
LinkState.down => (p.alarm, 'DOWN'),
|
||||||
};
|
};
|
||||||
// Fixed-width numbers, padded with figure spaces (U+2007, digit-wide): the bar must
|
// Fixed-width numbers, padded with figure spaces (U+2007, digit-wide): the bar must
|
||||||
// not reflow every time a value gains a digit.
|
// not reflow every time a value gains a digit.
|
||||||
@@ -230,7 +232,10 @@ class LinkBar extends StatelessWidget {
|
|||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Chip(
|
Chip(
|
||||||
label: Text(label),
|
label: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(color: color, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
backgroundColor: color.withValues(alpha: 0.3),
|
backgroundColor: color.withValues(alpha: 0.3),
|
||||||
),
|
),
|
||||||
Text(b.url, style: const TextStyle(fontFamily: 'monospace')),
|
Text(b.url, style: const TextStyle(fontFamily: 'monospace')),
|
||||||
@@ -317,7 +322,10 @@ Future<void> orinPowerOptions(BuildContext context, Boundary b) async {
|
|||||||
label: const Text('Reboot'),
|
label: const Text('Reboot'),
|
||||||
),
|
),
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
style: FilledButton.styleFrom(backgroundColor: Colors.red.shade700),
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: AppTheme.of(context).alarm,
|
||||||
|
foregroundColor: AppTheme.of(context).onFill(AppTheme.of(context).alarm),
|
||||||
|
),
|
||||||
onPressed: () => Navigator.of(context).pop('poweroff'),
|
onPressed: () => Navigator.of(context).pop('poweroff'),
|
||||||
icon: const Icon(Icons.power_settings_new),
|
icon: const Icon(Icons.power_settings_new),
|
||||||
label: const Text('Power off'),
|
label: const Text('Power off'),
|
||||||
@@ -403,26 +411,27 @@ class SignalTile extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final p = AppTheme.of(context);
|
||||||
if (missing) {
|
if (missing) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
name,
|
name,
|
||||||
style: const TextStyle(fontFamily: 'monospace', color: Colors.grey),
|
style: TextStyle(fontFamily: 'monospace', color: p.subdued),
|
||||||
),
|
),
|
||||||
subtitle: const Text(
|
subtitle: Text(
|
||||||
'MISSING · not in the latest state',
|
'MISSING · not in the latest state',
|
||||||
style: TextStyle(color: Colors.redAccent),
|
style: TextStyle(color: p.alarm),
|
||||||
),
|
),
|
||||||
trailing: Text.rich(
|
trailing: Text.rich(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
children: [
|
children: [
|
||||||
const TextSpan(
|
TextSpan(
|
||||||
text: '?',
|
text: '?',
|
||||||
style: TextStyle(fontSize: 34, color: Colors.grey),
|
style: TextStyle(fontSize: 34, color: p.subdued),
|
||||||
),
|
),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: ' ${envelope['unit'] ?? ''}',
|
text: ' ${envelope['unit'] ?? ''}',
|
||||||
style: const TextStyle(fontSize: 16, color: Colors.grey),
|
style: TextStyle(fontSize: 16, color: p.subdued),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -439,12 +448,13 @@ class SignalTile extends StatelessWidget {
|
|||||||
fontSize: 34,
|
fontSize: 34,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
fontFeatures: const [FontFeature.tabularFigures()],
|
fontFeatures: const [FontFeature.tabularFigures()],
|
||||||
color: stale || v == null ? Colors.grey : null,
|
color: stale || v == null ? p.subdued : p.foreground,
|
||||||
fontStyle: stale ? FontStyle.italic : null,
|
fontStyle: stale ? FontStyle.italic : null,
|
||||||
);
|
);
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(name, style: const TextStyle(fontFamily: 'monospace')),
|
title: Text(name, style: const TextStyle(fontFamily: 'monospace')),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
|
style: stale ? TextStyle(color: p.caution) : null,
|
||||||
[
|
[
|
||||||
if (stale) quality.toUpperCase(),
|
if (stale) quality.toUpperCase(),
|
||||||
if (v == null) 'no value',
|
if (v == null) 'no value',
|
||||||
@@ -479,11 +489,14 @@ class RawLog extends StatelessWidget {
|
|||||||
if (b.pending.isNotEmpty || b.acks.isNotEmpty) ...[
|
if (b.pending.isNotEmpty || b.acks.isNotEmpty) ...[
|
||||||
Text('Commands', style: Theme.of(context).textTheme.titleSmall),
|
Text('Commands', style: Theme.of(context).textTheme.titleSmall),
|
||||||
for (final p in b.pending)
|
for (final p in b.pending)
|
||||||
Text('… ${p.cmd} ${p.args['text'] ?? ''} (unacked)'),
|
Text(
|
||||||
|
'… ${p.cmd} ${p.args['text'] ?? ''} (unacked)',
|
||||||
|
style: TextStyle(color: AppTheme.of(context).caution),
|
||||||
|
),
|
||||||
for (final a in b.acks)
|
for (final a in b.acks)
|
||||||
Text(
|
Text(
|
||||||
'${a.ok ? '✓' : '✗'} ${a.detail}',
|
'${a.ok ? '✓' : '✗'} ${a.detail}',
|
||||||
style: TextStyle(color: a.ok ? null : Colors.redAccent),
|
style: TextStyle(color: a.ok ? AppTheme.of(context).good : AppTheme.of(context).alarm),
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
],
|
],
|
||||||
|
|||||||
+81
-23
@@ -1,14 +1,22 @@
|
|||||||
// The palette system, from smart-serow's HUD: four semantic colours, each with a dark
|
// The palette system, from smart-serow's HUD, widened to seven roles: each a meaning,
|
||||||
// (low ambient light) and a bright (high ambient light) variant, read from a theme
|
// each with a dark (low ambient light) and a bright (high ambient light) variant, read
|
||||||
// JSON in assets/themes/. Same file format as smart-serow's extra/themes/, so themes
|
// from a theme JSON in assets/themes/. Widgets ask for meanings, never for colours:
|
||||||
// move between the two as they are. Widgets ask for meanings, never for colours:
|
|
||||||
//
|
//
|
||||||
// final p = AppTheme.of(context);
|
// final p = AppTheme.of(context);
|
||||||
// color: p.foreground
|
// color: p.foreground
|
||||||
//
|
//
|
||||||
|
// The roles: background, foreground, subdued (muted), accent (emphasis without danger:
|
||||||
|
// buttons, links, the current item), and the good → caution → alarm gradient for state.
|
||||||
|
// Two more are derived, never authored: surface (raised bars and panels) and onFill
|
||||||
|
// (text on a filled colour).
|
||||||
|
//
|
||||||
|
// smart-serow's four-key files load unchanged: their `highlight` is `alarm`, and the
|
||||||
|
// three newer roles fall back (accent and good to foreground, caution to subdued).
|
||||||
|
//
|
||||||
// Pick the theme at build time: `--dart-define=YUKART_THEME=default` (default: rei, smart-serow's).
|
// Pick the theme at build time: `--dart-define=YUKART_THEME=default` (default: rei, smart-serow's).
|
||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:math' show max, min;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@@ -24,45 +32,87 @@ class Palette {
|
|||||||
const Palette({
|
const Palette({
|
||||||
required this.background,
|
required this.background,
|
||||||
required this.foreground,
|
required this.foreground,
|
||||||
required this.highlight,
|
|
||||||
required this.subdued,
|
required this.subdued,
|
||||||
|
required this.accent,
|
||||||
|
required this.good,
|
||||||
|
required this.caution,
|
||||||
|
required this.alarm,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Behind everything.
|
/// Behind everything.
|
||||||
final Color background;
|
final Color background;
|
||||||
|
|
||||||
/// Values: what is read at a glance.
|
/// Values and primary text: what is read at a glance.
|
||||||
final Color foreground;
|
final Color foreground;
|
||||||
|
|
||||||
/// Warnings, and the one thing that must be seen.
|
/// Labels, units, grids, secondary text, and anything missing or stale.
|
||||||
final Color highlight;
|
|
||||||
|
|
||||||
/// Labels, units, grids, and anything missing or stale.
|
|
||||||
final Color subdued;
|
final Color subdued;
|
||||||
|
|
||||||
|
/// Emphasis without danger: buttons, links, the current item.
|
||||||
|
final Color accent;
|
||||||
|
|
||||||
|
/// Connected, done, healthy.
|
||||||
|
final Color good;
|
||||||
|
|
||||||
|
/// In progress, unconfirmed, mock data.
|
||||||
|
final Color caution;
|
||||||
|
|
||||||
|
/// Down, refused, destructive, over a limit: the one thing that must be seen.
|
||||||
|
final Color alarm;
|
||||||
|
|
||||||
|
/// Raised bars and panels: background with a little foreground mixed in.
|
||||||
|
Color get surface => Color.lerp(background, foreground, 0.08)!;
|
||||||
|
|
||||||
|
/// Text drawn on a filled [fill]: background or foreground, whichever contrasts more.
|
||||||
|
Color onFill(Color fill) {
|
||||||
|
double contrast(Color a, Color b) {
|
||||||
|
final (x, y) = (a.computeLuminance(), b.computeLuminance());
|
||||||
|
return (max(x, y) + 0.05) / (min(x, y) + 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
return contrast(fill, background) >= contrast(fill, foreground)
|
||||||
|
? background
|
||||||
|
: foreground;
|
||||||
|
}
|
||||||
|
|
||||||
static Palette _fromJson(Map<String, dynamic> j) {
|
static Palette _fromJson(Map<String, dynamic> j) {
|
||||||
Color c(String k) =>
|
Color? c(String k) => j[k] is String
|
||||||
Color(0xFF000000 | int.parse((j[k] as String).substring(1), radix: 16));
|
? Color(
|
||||||
|
0xFF000000 | int.parse((j[k] as String).substring(1), radix: 16),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
final foreground = c('foreground')!;
|
||||||
|
final subdued = c('subdued')!;
|
||||||
return Palette(
|
return Palette(
|
||||||
background: c('background'),
|
background: c('background')!,
|
||||||
foreground: c('foreground'),
|
foreground: foreground,
|
||||||
highlight: c('highlight'),
|
subdued: subdued,
|
||||||
subdued: c('subdued'),
|
accent: c('accent') ?? foreground,
|
||||||
|
good: c('good') ?? foreground,
|
||||||
|
caution: c('caution') ?? subdued,
|
||||||
|
// smart-serow calls it highlight.
|
||||||
|
alarm: (c('alarm') ?? c('highlight'))!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// smart-serow's hard-coded last resort, used when no theme file loads.
|
/// The last resort, used when no theme file loads (smart-serow's four, plus three).
|
||||||
static const fallbackDark = Palette(
|
static const fallbackDark = Palette(
|
||||||
background: Color(0xFF000000),
|
background: Color(0xFF000000),
|
||||||
foreground: Color(0xFFFFFFFF),
|
foreground: Color(0xFFFFFFFF),
|
||||||
highlight: Color(0xFFFF5555),
|
|
||||||
subdued: Color(0xFF808080),
|
subdued: Color(0xFF808080),
|
||||||
|
accent: Color(0xFF5EC8F2),
|
||||||
|
good: Color(0xFF6CCB5F),
|
||||||
|
caution: Color(0xFFFFC107),
|
||||||
|
alarm: Color(0xFFFF5555),
|
||||||
);
|
);
|
||||||
static const fallbackBright = Palette(
|
static const fallbackBright = Palette(
|
||||||
background: Color(0xFFF0F0F0),
|
background: Color(0xFFF0F0F0),
|
||||||
foreground: Color(0xFF1A1A1A),
|
foreground: Color(0xFF1A1A1A),
|
||||||
highlight: Color(0xFFCC0000),
|
|
||||||
subdued: Color(0xFF606060),
|
subdued: Color(0xFF606060),
|
||||||
|
accent: Color(0xFF0277BD),
|
||||||
|
good: Color(0xFF2E7D32),
|
||||||
|
caution: Color(0xFF8D6E00),
|
||||||
|
alarm: Color(0xFFCC0000),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +156,8 @@ class Themes extends ChangeNotifier {
|
|||||||
return Themes('fallback', Palette.fallbackDark, Palette.fallbackBright);
|
return Themes('fallback', Palette.fallbackDark, Palette.fallbackBright);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Material's theme for the rest of the app, from the same four colours.
|
/// Material's theme for the rest of the app, from the same roles: buttons and
|
||||||
|
/// selection in accent, errors in alarm, raised surfaces in surface.
|
||||||
ThemeData material() {
|
ThemeData material() {
|
||||||
final p = current;
|
final p = current;
|
||||||
final brightness = _isDark ? Brightness.dark : Brightness.light;
|
final brightness = _isDark ? Brightness.dark : Brightness.light;
|
||||||
@@ -116,13 +167,20 @@ class Themes extends ChangeNotifier {
|
|||||||
scaffoldBackgroundColor: p.background,
|
scaffoldBackgroundColor: p.background,
|
||||||
colorScheme:
|
colorScheme:
|
||||||
ColorScheme.fromSeed(
|
ColorScheme.fromSeed(
|
||||||
seedColor: p.highlight,
|
seedColor: p.accent,
|
||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
).copyWith(
|
).copyWith(
|
||||||
surface: p.background,
|
surface: p.background,
|
||||||
onSurface: p.foreground,
|
onSurface: p.foreground,
|
||||||
primary: p.highlight,
|
surfaceContainerHigh: p.surface,
|
||||||
error: p.highlight,
|
surfaceContainerHighest: p.surface,
|
||||||
|
primary: p.accent,
|
||||||
|
onPrimary: p.onFill(p.accent),
|
||||||
|
primaryContainer: Color.lerp(p.background, p.accent, 0.25),
|
||||||
|
onPrimaryContainer: p.foreground,
|
||||||
|
secondary: p.accent,
|
||||||
|
error: p.alarm,
|
||||||
|
onError: p.onFill(p.alarm),
|
||||||
outline: p.subdued,
|
outline: p.subdued,
|
||||||
onSurfaceVariant: p.subdued,
|
onSurfaceVariant: p.subdued,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ class GpsCompass extends StatelessWidget {
|
|||||||
final theme = AppTheme.of(context);
|
final theme = AppTheme.of(context);
|
||||||
|
|
||||||
// No signal = subdued color, valid = foreground
|
// No signal = subdued color, valid = foreground
|
||||||
final iconColour = _hasSignal ? theme.foreground : theme.highlight;
|
// Heading: foreground; acquiring: caution; no receiver at all: alarm.
|
||||||
|
final iconColour = _hasSignal ? theme.foreground : (_isAcquiring ? theme.caution : theme.alarm);
|
||||||
|
|
||||||
// Convert to radians, 0 = no rotation when no signal
|
// Convert to radians, 0 = no rotation when no signal
|
||||||
final angle = _hasSignal ? (heading! * math.pi / 180.0) : 0.0;
|
final angle = _hasSignal ? (heading! * math.pi / 180.0) : 0.0;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// every readout is a box that takes its share of a row (`flex`) and sizes its type
|
// every readout is a box that takes its share of a row (`flex`) and sizes its type
|
||||||
// from its own height, so a layout is a grid of boxes, and a value changing digits
|
// from its own height, so a layout is a grid of boxes, and a value changing digits
|
||||||
// never moves anything. Value in foreground with tabular figures, unit and label in
|
// never moves anything. Value in foreground with tabular figures, unit and label in
|
||||||
// subdued; a warning turns the value to highlight.
|
// subdued; a warning turns the value to alarm.
|
||||||
//
|
//
|
||||||
// Added for Yukart: [dim]. A missing or stale value is a normal state in a car
|
// Added for Yukart: [dim]. A missing or stale value is a normal state in a car
|
||||||
// (app guideline 2), shown in subdued rather than hidden.
|
// (app guideline 2), shown in subdued rather than hidden.
|
||||||
@@ -28,7 +28,7 @@ class StatBox extends StatelessWidget {
|
|||||||
final String label;
|
final String label;
|
||||||
final int flex;
|
final int flex;
|
||||||
|
|
||||||
/// If it returns true, the value shows in highlight.
|
/// If it returns true, the value shows in alarm.
|
||||||
final bool Function()? isWarning;
|
final bool Function()? isWarning;
|
||||||
|
|
||||||
/// Missing or stale: the value shows in subdued.
|
/// Missing or stale: the value shows in subdued.
|
||||||
@@ -41,7 +41,7 @@ class StatBox extends StatelessWidget {
|
|||||||
final valueColor = dim
|
final valueColor = dim
|
||||||
? theme.subdued
|
? theme.subdued
|
||||||
: warning
|
: warning
|
||||||
? theme.highlight
|
? theme.alarm
|
||||||
: theme.foreground;
|
: theme.foreground;
|
||||||
|
|
||||||
return Expanded(
|
return Expanded(
|
||||||
@@ -168,7 +168,7 @@ class StatBoxMain extends StatelessWidget {
|
|||||||
color: dim
|
color: dim
|
||||||
? theme.subdued
|
? theme.subdued
|
||||||
: warning
|
: warning
|
||||||
? theme.highlight
|
? theme.alarm
|
||||||
: theme.foreground,
|
: theme.foreground,
|
||||||
height: 1,
|
height: 1,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import '../reading.dart';
|
|||||||
import '../theme/palette.dart';
|
import '../theme/palette.dart';
|
||||||
|
|
||||||
/// An on/off lamp in the boxed pattern: the label is the lamp. Lit in foreground
|
/// An on/off lamp in the boxed pattern: the label is the lamp. Lit in foreground
|
||||||
/// (highlight when [warn]), dark in subdued; missing shows as a dash.
|
/// (alarm when [warn]), dark in subdued; missing shows as a dash.
|
||||||
class Telltale extends StatelessWidget {
|
class Telltale extends StatelessWidget {
|
||||||
const Telltale({
|
const Telltale({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -32,7 +32,7 @@ class Telltale extends StatelessWidget {
|
|||||||
final missing = reading.value == null;
|
final missing = reading.value == null;
|
||||||
final lit = reading.on && !reading.dim;
|
final lit = reading.on && !reading.dim;
|
||||||
final color = lit
|
final color = lit
|
||||||
? (warn ? p.highlight : p.foreground)
|
? (warn ? p.alarm : p.foreground)
|
||||||
: p.subdued.withValues(alpha: missing ? 0.35 : 0.6);
|
: p.subdued.withValues(alpha: missing ? 0.35 : 0.6);
|
||||||
return Expanded(
|
return Expanded(
|
||||||
flex: flex,
|
flex: flex,
|
||||||
|
|||||||
Reference in New Issue
Block a user