183 lines
6.5 KiB
Dart
183 lines
6.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'gapped_track_shape.dart';
|
|
import 'mono_tokens.dart';
|
|
|
|
ThemeData monoTheme({required bool dark, bool oled = false}) {
|
|
// neutral greys tuned for crisp contrast
|
|
final ({Color bg, Color surface, Color outline, Color text, Color textMuted}) c;
|
|
if (oled) {
|
|
c = (
|
|
bg: const Color(0xFF000000), // Pure black for OLED
|
|
surface: const Color(0xFF0A0A0A), // Very dark gray
|
|
outline: const Color(0x1FFFFFFF),
|
|
text: const Color(0xFFEDEDED),
|
|
textMuted: const Color(0x99EDEDED),
|
|
);
|
|
} else if (dark) {
|
|
c = (
|
|
bg: const Color(0xFF0E0F12),
|
|
surface: const Color(0xFF15171C),
|
|
outline: const Color(0x1FFFFFFF),
|
|
text: const Color(0xFFEDEDED),
|
|
textMuted: const Color(0x99EDEDED),
|
|
);
|
|
} else {
|
|
c = (
|
|
bg: const Color(0xFFF7F7F8),
|
|
surface: const Color(0xFFFFFFFF),
|
|
outline: const Color(0x19000000),
|
|
text: const Color(0xFF111111),
|
|
textMuted: const Color(0x99111111),
|
|
);
|
|
}
|
|
|
|
final isDark = dark || oled;
|
|
|
|
final buttonStyle = ButtonStyle(
|
|
padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(horizontal: 18, vertical: 14)),
|
|
elevation: const WidgetStatePropertyAll(0),
|
|
backgroundColor: WidgetStatePropertyAll(c.text),
|
|
foregroundColor: WidgetStatePropertyAll(isDark ? c.bg : Colors.white),
|
|
shape: const WidgetStatePropertyAll(StadiumBorder()),
|
|
);
|
|
|
|
final base = ThemeData(
|
|
useMaterial3: true,
|
|
brightness: isDark ? Brightness.dark : Brightness.light,
|
|
colorScheme: ColorScheme(
|
|
brightness: isDark ? Brightness.dark : Brightness.light,
|
|
primary: c.text,
|
|
onPrimary: isDark ? c.bg : Colors.white,
|
|
secondary: c.text,
|
|
onSecondary: c.bg,
|
|
surface: c.surface,
|
|
onSurface: c.text,
|
|
error: const Color(0xFFB00020),
|
|
onError: Colors.white,
|
|
tertiary: c.text,
|
|
onTertiary: c.bg,
|
|
primaryContainer: c.surface,
|
|
onPrimaryContainer: c.text,
|
|
secondaryContainer: c.surface,
|
|
onSecondaryContainer: c.text,
|
|
surfaceContainerHighest: c.surface,
|
|
surfaceContainerLow: c.bg,
|
|
surfaceDim: c.bg,
|
|
surfaceBright: c.surface,
|
|
outline: c.outline,
|
|
shadow: Colors.transparent,
|
|
scrim: Colors.black,
|
|
inverseSurface: c.text,
|
|
onInverseSurface: c.bg,
|
|
inversePrimary: c.bg,
|
|
),
|
|
// remove "Material feel"
|
|
splashFactory: NoSplash.splashFactory,
|
|
highlightColor: Colors.transparent,
|
|
dividerColor: c.outline,
|
|
scaffoldBackgroundColor: c.bg,
|
|
appBarTheme: AppBarTheme(
|
|
backgroundColor: c.bg,
|
|
elevation: 0,
|
|
scrolledUnderElevation: 0,
|
|
centerTitle: false,
|
|
foregroundColor: c.text,
|
|
titleTextStyle: TextStyle(color: c.text, fontSize: 18, fontWeight: FontWeight.w700, letterSpacing: -0.2),
|
|
),
|
|
textTheme: Typography.englishLike2021
|
|
.apply(bodyColor: c.text, displayColor: c.text)
|
|
.copyWith(
|
|
displayLarge: const TextStyle(fontWeight: FontWeight.w700, letterSpacing: -0.5),
|
|
titleMedium: const TextStyle(fontWeight: FontWeight.w600),
|
|
bodyMedium: TextStyle(color: c.text),
|
|
bodySmall: TextStyle(color: c.textMuted),
|
|
),
|
|
cardTheme: CardThemeData(
|
|
color: c.surface,
|
|
elevation: 0,
|
|
margin: EdgeInsets.zero,
|
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(14))),
|
|
),
|
|
inputDecorationTheme: _inputDecorationTheme(c.text, c.textMuted),
|
|
elevatedButtonTheme: ElevatedButtonThemeData(style: buttonStyle),
|
|
filledButtonTheme: FilledButtonThemeData(style: buttonStyle),
|
|
sliderTheme: SliderThemeData(
|
|
trackHeight: 16,
|
|
trackGap: 6,
|
|
thumbSize: const WidgetStatePropertyAll(Size(4, 20)),
|
|
thumbShape: const HandleThumbShape(),
|
|
trackShape: const GappedTrackShape(),
|
|
tickMarkShape: const RoundSliderTickMarkShape(tickMarkRadius: 2),
|
|
// ignore: deprecated_member_use — opting into the 2024 slider appearance until the default flips
|
|
year2023: false,
|
|
),
|
|
dividerTheme: DividerThemeData(space: 0, thickness: 1, color: c.outline),
|
|
listTileTheme: ListTileThemeData(
|
|
dense: true,
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
|
iconColor: c.text,
|
|
textColor: c.text,
|
|
),
|
|
navigationBarTheme: NavigationBarThemeData(
|
|
backgroundColor: c.bg,
|
|
elevation: 0,
|
|
indicatorColor: Colors.transparent,
|
|
labelTextStyle: WidgetStatePropertyAll(TextStyle(color: c.textMuted, fontSize: 11)),
|
|
iconTheme: WidgetStateProperty.resolveWith((states) {
|
|
final active = states.contains(WidgetState.selected);
|
|
return IconThemeData(opacity: active ? 1 : 0.6, size: 22, color: c.text);
|
|
}),
|
|
),
|
|
// Floating snackbars auto-offset above the Scaffold's bottom NavigationBar,
|
|
// so they don't cover it on mobile. Background color tracks the theme to
|
|
// avoid jarring brightness on HDR playback / dark mode.
|
|
snackBarTheme: SnackBarThemeData(
|
|
behavior: SnackBarBehavior.floating,
|
|
backgroundColor: c.surface,
|
|
contentTextStyle: TextStyle(color: c.text),
|
|
actionTextColor: c.text,
|
|
elevation: 6,
|
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
|
|
insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
|
),
|
|
);
|
|
|
|
return base.copyWith(
|
|
extensions: [
|
|
MonoTokens(
|
|
radiusSm: 8,
|
|
radiusMd: 12,
|
|
space: 12,
|
|
fast: const Duration(milliseconds: 120),
|
|
normal: const Duration(milliseconds: 200),
|
|
slow: const Duration(milliseconds: 300),
|
|
bg: c.bg,
|
|
surface: c.surface,
|
|
outline: c.outline,
|
|
text: c.text,
|
|
textMuted: c.textMuted,
|
|
splashFactory: NoSplash.splashFactory,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// Brighter fill on focus so input focus is visible inside TV overscan.
|
|
InputDecorationTheme _inputDecorationTheme(Color text, Color textMuted) {
|
|
final unfocusedFill = text.withValues(alpha: 0.08);
|
|
final focusedFill = text.withValues(alpha: 0.18);
|
|
const border = OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(12)), borderSide: BorderSide.none);
|
|
return InputDecorationTheme(
|
|
filled: true,
|
|
fillColor: WidgetStateColor.resolveWith(
|
|
(states) => states.contains(WidgetState.focused) ? focusedFill : unfocusedFill,
|
|
),
|
|
isDense: true,
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
|
border: border,
|
|
enabledBorder: border,
|
|
focusedBorder: border,
|
|
hintStyle: TextStyle(color: textMuted),
|
|
);
|
|
}
|