Files
plezy/lib/theme/mono_theme.dart
T

190 lines
6.9 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 clickableCursor = WidgetStateProperty.resolveWith<MouseCursor>(
(states) => states.contains(WidgetState.disabled) ? MouseCursor.defer : SystemMouseCursors.click,
);
final buttonStyle = ButtonStyle(
mouseCursor: clickableCursor,
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: .w700, letterSpacing: -0.2),
),
textTheme: Typography.englishLike2021
.apply(bodyColor: c.text, displayColor: c.text)
.copyWith(
displayLarge: const TextStyle(fontWeight: .w700, letterSpacing: -0.5),
titleMedium: const TextStyle(fontWeight: .w600),
bodyMedium: TextStyle(color: c.text),
bodySmall: TextStyle(color: c.textMuted),
),
cardTheme: CardThemeData(
color: c.surface,
elevation: 0,
margin: .zero,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(14))),
),
inputDecorationTheme: _inputDecorationTheme(c.text, c.textMuted),
elevatedButtonTheme: ElevatedButtonThemeData(style: buttonStyle),
filledButtonTheme: FilledButtonThemeData(style: buttonStyle),
textButtonTheme: TextButtonThemeData(style: ButtonStyle(mouseCursor: clickableCursor)),
outlinedButtonTheme: OutlinedButtonThemeData(style: ButtonStyle(mouseCursor: clickableCursor)),
iconButtonTheme: IconButtonThemeData(style: ButtonStyle(mouseCursor: clickableCursor)),
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.all(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),
);
}