Dash page and smart-serow's palette, boxed widgets, attitude and G-meter
Ported from smart-serow's HUD: the four-colour palette with dark/bright variants (same theme JSON format, loaded at runtime; YUKART_THEME picks one), StatBox and StatBoxMain, WhiskeyMark and AccelGraph. The whole app takes its colours from the palette. Dash: attitude, G-meter, RPM, GNSS speed, pedal, coolant and telltales, each from its contracts signal; missing and stale draw in subdued. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
// The palette system, from smart-serow's HUD: four semantic colours, each with a dark
|
||||
// (low ambient light) and a bright (high ambient light) variant, read from a theme
|
||||
// JSON in assets/themes/. Same file format as smart-serow's extra/themes/, so themes
|
||||
// move between the two as they are. Widgets ask for meanings, never for colours:
|
||||
//
|
||||
// final p = AppTheme.of(context);
|
||||
// color: p.foreground
|
||||
//
|
||||
// Pick the theme at build time: `--dart-define=YUKART_THEME=rei` (default: default).
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
const themeName = String.fromEnvironment(
|
||||
'YUKART_THEME',
|
||||
defaultValue: 'default',
|
||||
);
|
||||
|
||||
/// One variant (dark or bright) of a theme.
|
||||
class Palette {
|
||||
const Palette({
|
||||
required this.background,
|
||||
required this.foreground,
|
||||
required this.highlight,
|
||||
required this.subdued,
|
||||
});
|
||||
|
||||
/// Behind everything.
|
||||
final Color background;
|
||||
|
||||
/// Values: what is read at a glance.
|
||||
final Color foreground;
|
||||
|
||||
/// Warnings, and the one thing that must be seen.
|
||||
final Color highlight;
|
||||
|
||||
/// Labels, units, grids, and anything missing or stale.
|
||||
final Color subdued;
|
||||
|
||||
static Palette _fromJson(Map<String, dynamic> j) {
|
||||
Color c(String k) =>
|
||||
Color(0xFF000000 | int.parse((j[k] as String).substring(1), radix: 16));
|
||||
return Palette(
|
||||
background: c('background'),
|
||||
foreground: c('foreground'),
|
||||
highlight: c('highlight'),
|
||||
subdued: c('subdued'),
|
||||
);
|
||||
}
|
||||
|
||||
/// smart-serow's hard-coded last resort, used when no theme file loads.
|
||||
static const fallbackDark = Palette(
|
||||
background: Color(0xFF000000),
|
||||
foreground: Color(0xFFFFFFFF),
|
||||
highlight: Color(0xFFFF5555),
|
||||
subdued: Color(0xFF808080),
|
||||
);
|
||||
static const fallbackBright = Palette(
|
||||
background: Color(0xFFF0F0F0),
|
||||
foreground: Color(0xFF1A1A1A),
|
||||
highlight: Color(0xFFCC0000),
|
||||
subdued: Color(0xFF606060),
|
||||
);
|
||||
}
|
||||
|
||||
/// A theme: both variants, and which one is showing. Listen to it to follow a switch.
|
||||
class Themes extends ChangeNotifier {
|
||||
Themes(this.name, this.dark, this.bright);
|
||||
|
||||
final String name;
|
||||
final Palette dark;
|
||||
final Palette bright;
|
||||
bool _isDark = true;
|
||||
|
||||
bool get isDark => _isDark;
|
||||
Palette get current => _isDark ? dark : bright;
|
||||
|
||||
set isDark(bool v) {
|
||||
if (v == _isDark) return;
|
||||
_isDark = v;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void toggle() => isDark = !_isDark;
|
||||
|
||||
/// [themeName], falling back to `default`, then to the hard-coded palette
|
||||
/// (smart-serow's chain).
|
||||
static Future<Themes> load([String name = themeName]) async {
|
||||
for (final n in {name, 'default'}) {
|
||||
try {
|
||||
final j = jsonDecode(
|
||||
await rootBundle.loadString('assets/themes/$n.json'),
|
||||
) as Map<String, dynamic>;
|
||||
return Themes(
|
||||
n,
|
||||
Palette._fromJson(j['dark'] as Map<String, dynamic>),
|
||||
Palette._fromJson(j['bright'] as Map<String, dynamic>),
|
||||
);
|
||||
} catch (_) {
|
||||
// Missing or malformed: try the next one.
|
||||
}
|
||||
}
|
||||
return Themes('fallback', Palette.fallbackDark, Palette.fallbackBright);
|
||||
}
|
||||
|
||||
/// Material's theme for the rest of the app, from the same four colours.
|
||||
ThemeData material() {
|
||||
final p = current;
|
||||
final brightness = _isDark ? Brightness.dark : Brightness.light;
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: brightness,
|
||||
scaffoldBackgroundColor: p.background,
|
||||
colorScheme:
|
||||
ColorScheme.fromSeed(
|
||||
seedColor: p.highlight,
|
||||
brightness: brightness,
|
||||
).copyWith(
|
||||
surface: p.background,
|
||||
onSurface: p.foreground,
|
||||
primary: p.highlight,
|
||||
error: p.highlight,
|
||||
outline: p.subdued,
|
||||
onSurfaceVariant: p.subdued,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The current palette, for any widget below it: `AppTheme.of(context).foreground`.
|
||||
class AppTheme extends InheritedWidget {
|
||||
const AppTheme({super.key, required this.palette, required super.child});
|
||||
|
||||
final Palette palette;
|
||||
|
||||
static Palette of(BuildContext context) {
|
||||
final t = context.dependOnInheritedWidgetOfExactType<AppTheme>();
|
||||
assert(t != null, 'No AppTheme above this widget');
|
||||
return t?.palette ?? Palette.fallbackDark;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(AppTheme old) => palette != old.palette;
|
||||
}
|
||||
Reference in New Issue
Block a user