300 lines
9.0 KiB
Dart
300 lines
9.0 KiB
Dart
|
|
import 'package:flutter/material.dart';
|
||
|
|
|
||
|
|
class AppTokens {
|
||
|
|
final double siderWidthExpanded;
|
||
|
|
final double siderWidthCollapsed;
|
||
|
|
final Duration siderTransition;
|
||
|
|
final Color progressBarColor;
|
||
|
|
final Color ratingColor;
|
||
|
|
final Color shellBg;
|
||
|
|
final Color mutedForeground;
|
||
|
|
final Color pageGradientTop;
|
||
|
|
final Color pageGradientBottom;
|
||
|
|
final Color statusOnline;
|
||
|
|
final Color statusWarn;
|
||
|
|
final Color statusOffline;
|
||
|
|
final double panelRadius;
|
||
|
|
final double posterRadius;
|
||
|
|
final double titleBarHeight;
|
||
|
|
final Color separatorColor;
|
||
|
|
final double chromeInset;
|
||
|
|
final double radiusCard;
|
||
|
|
final double cardFillAlpha;
|
||
|
|
final double cardFillHoverAlpha;
|
||
|
|
final double radiusCardCompact;
|
||
|
|
final double bottomNavRadius;
|
||
|
|
final List<Color> warmGradientColors;
|
||
|
|
|
||
|
|
const AppTokens({
|
||
|
|
required this.siderWidthExpanded,
|
||
|
|
required this.siderWidthCollapsed,
|
||
|
|
required this.siderTransition,
|
||
|
|
required this.progressBarColor,
|
||
|
|
required this.ratingColor,
|
||
|
|
required this.shellBg,
|
||
|
|
required this.mutedForeground,
|
||
|
|
required this.pageGradientTop,
|
||
|
|
required this.pageGradientBottom,
|
||
|
|
required this.statusOnline,
|
||
|
|
required this.statusWarn,
|
||
|
|
required this.statusOffline,
|
||
|
|
required this.panelRadius,
|
||
|
|
required this.posterRadius,
|
||
|
|
required this.titleBarHeight,
|
||
|
|
required this.separatorColor,
|
||
|
|
required this.chromeInset,
|
||
|
|
required this.radiusCard,
|
||
|
|
required this.cardFillAlpha,
|
||
|
|
required this.cardFillHoverAlpha,
|
||
|
|
required this.radiusCardCompact,
|
||
|
|
required this.bottomNavRadius,
|
||
|
|
required this.warmGradientColors,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
class AppTheme {
|
||
|
|
static const _seedColor = Color(0xFF7C99C9);
|
||
|
|
|
||
|
|
|
||
|
|
static const _fontFamilyFallback = <String>['Noto Sans SC'];
|
||
|
|
|
||
|
|
static const lightTokens = AppTokens(
|
||
|
|
siderWidthExpanded: 220,
|
||
|
|
siderWidthCollapsed: 64,
|
||
|
|
siderTransition: Duration(milliseconds: 240),
|
||
|
|
progressBarColor: Color(0xFFFFFFFF),
|
||
|
|
ratingColor: Color(0xFFF6C453),
|
||
|
|
shellBg: Color(0xFFFFFFFF),
|
||
|
|
mutedForeground: Color(0xFF8C8C8C),
|
||
|
|
pageGradientTop: Color(0xFFF3F5FA),
|
||
|
|
pageGradientBottom: Color(0xFFFFFFFF),
|
||
|
|
statusOnline: Color(0xFF22C55E),
|
||
|
|
statusWarn: Color(0xFFFB9E0B),
|
||
|
|
statusOffline: Color(0xFFEF4444),
|
||
|
|
panelRadius: 0,
|
||
|
|
posterRadius: 4,
|
||
|
|
titleBarHeight: 46,
|
||
|
|
separatorColor: Color(0x18000000),
|
||
|
|
chromeInset: 12,
|
||
|
|
radiusCard: 8,
|
||
|
|
cardFillAlpha: 0.025,
|
||
|
|
cardFillHoverAlpha: 0.05,
|
||
|
|
radiusCardCompact: 16,
|
||
|
|
bottomNavRadius: 24,
|
||
|
|
warmGradientColors: [
|
||
|
|
Color(0xFFFFF8F0),
|
||
|
|
Color(0xFFFFE8D6),
|
||
|
|
Color(0xFFFFD6C0),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
|
||
|
|
static const darkTokens = AppTokens(
|
||
|
|
siderWidthExpanded: 220,
|
||
|
|
siderWidthCollapsed: 64,
|
||
|
|
siderTransition: Duration(milliseconds: 240),
|
||
|
|
progressBarColor: Color(0xFFFFFFFF),
|
||
|
|
ratingColor: Color(0xFFF6C453),
|
||
|
|
shellBg: Color(0xFF000000),
|
||
|
|
mutedForeground: Color(0xFFA3A3A3),
|
||
|
|
pageGradientTop: Color(0xFF06080C),
|
||
|
|
pageGradientBottom: Color(0xFF000000),
|
||
|
|
statusOnline: Color(0xFF22C55E),
|
||
|
|
statusWarn: Color(0xFFFB9E0B),
|
||
|
|
statusOffline: Color(0xFFEF4444),
|
||
|
|
panelRadius: 0,
|
||
|
|
posterRadius: 4,
|
||
|
|
titleBarHeight: 46,
|
||
|
|
separatorColor: Color(0x1AFFFFFF),
|
||
|
|
chromeInset: 12,
|
||
|
|
radiusCard: 8,
|
||
|
|
cardFillAlpha: 0.025,
|
||
|
|
cardFillHoverAlpha: 0.05,
|
||
|
|
radiusCardCompact: 16,
|
||
|
|
bottomNavRadius: 24,
|
||
|
|
warmGradientColors: [
|
||
|
|
Color(0xFF2A1810),
|
||
|
|
Color(0xFF1A1208),
|
||
|
|
Color(0xFF0A0A0A),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
|
||
|
|
static ThemeData light() {
|
||
|
|
final scheme =
|
||
|
|
ColorScheme.fromSeed(
|
||
|
|
seedColor: _seedColor,
|
||
|
|
brightness: Brightness.light,
|
||
|
|
).copyWith(
|
||
|
|
surface: const Color(0xFFFFFFFF),
|
||
|
|
surfaceContainerHighest: const Color(0xFFF5F5F5),
|
||
|
|
outlineVariant: const Color(0xFFE8E8E8),
|
||
|
|
);
|
||
|
|
return _build(scheme, lightTokens);
|
||
|
|
}
|
||
|
|
|
||
|
|
static ThemeData dark() {
|
||
|
|
final scheme =
|
||
|
|
ColorScheme.fromSeed(
|
||
|
|
seedColor: _seedColor,
|
||
|
|
brightness: Brightness.dark,
|
||
|
|
).copyWith(
|
||
|
|
surface: const Color(0xFF000000),
|
||
|
|
surfaceContainerHighest: const Color(0xFF1A1A1A),
|
||
|
|
outlineVariant: const Color(0xFF2A2A2A),
|
||
|
|
);
|
||
|
|
return _build(scheme, darkTokens);
|
||
|
|
}
|
||
|
|
|
||
|
|
static ThemeData _build(ColorScheme scheme, AppTokens tokens) {
|
||
|
|
final isLight = scheme.brightness == Brightness.light;
|
||
|
|
final onSurface = isLight
|
||
|
|
? const Color(0xFF0A0A0A)
|
||
|
|
: const Color(0xFFF5F5F5);
|
||
|
|
final secondaryText = isLight
|
||
|
|
? const Color(0xFF6B6B6B)
|
||
|
|
: const Color(0xFF8C8C8C);
|
||
|
|
|
||
|
|
return ThemeData(
|
||
|
|
colorScheme: scheme,
|
||
|
|
useMaterial3: true,
|
||
|
|
fontFamilyFallback: _fontFamilyFallback,
|
||
|
|
scaffoldBackgroundColor: tokens.shellBg,
|
||
|
|
canvasColor: tokens.shellBg,
|
||
|
|
cardColor: tokens.shellBg,
|
||
|
|
dividerColor: tokens.separatorColor,
|
||
|
|
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||
|
|
textTheme: TextTheme(
|
||
|
|
titleLarge: TextStyle(
|
||
|
|
fontSize: 36,
|
||
|
|
height: 1.05,
|
||
|
|
fontWeight: FontWeight.w800,
|
||
|
|
letterSpacing: -0.5,
|
||
|
|
color: onSurface,
|
||
|
|
),
|
||
|
|
titleMedium: TextStyle(
|
||
|
|
fontSize: 20,
|
||
|
|
height: 1.15,
|
||
|
|
fontWeight: FontWeight.w700,
|
||
|
|
letterSpacing: -0.2,
|
||
|
|
color: onSurface,
|
||
|
|
),
|
||
|
|
titleSmall: TextStyle(
|
||
|
|
fontSize: 14,
|
||
|
|
height: 1.2,
|
||
|
|
fontWeight: FontWeight.w600,
|
||
|
|
letterSpacing: 0.1,
|
||
|
|
color: onSurface,
|
||
|
|
),
|
||
|
|
bodyLarge: TextStyle(
|
||
|
|
fontSize: 15,
|
||
|
|
height: 1.55,
|
||
|
|
fontWeight: FontWeight.w400,
|
||
|
|
letterSpacing: 0.1,
|
||
|
|
color: onSurface,
|
||
|
|
),
|
||
|
|
bodyMedium: TextStyle(
|
||
|
|
fontSize: 13.5,
|
||
|
|
height: 1.45,
|
||
|
|
fontWeight: FontWeight.w400,
|
||
|
|
letterSpacing: 0.1,
|
||
|
|
color: onSurface,
|
||
|
|
),
|
||
|
|
bodySmall: TextStyle(
|
||
|
|
fontSize: 12,
|
||
|
|
height: 1.35,
|
||
|
|
fontWeight: FontWeight.w400,
|
||
|
|
letterSpacing: 0.15,
|
||
|
|
color: secondaryText,
|
||
|
|
),
|
||
|
|
labelLarge: TextStyle(
|
||
|
|
fontSize: 13,
|
||
|
|
height: 1.2,
|
||
|
|
fontWeight: FontWeight.w600,
|
||
|
|
letterSpacing: 0.1,
|
||
|
|
color: onSurface,
|
||
|
|
),
|
||
|
|
labelSmall: TextStyle(
|
||
|
|
fontSize: 11,
|
||
|
|
height: 1.2,
|
||
|
|
fontWeight: FontWeight.w500,
|
||
|
|
letterSpacing: 0.6,
|
||
|
|
color: secondaryText,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
cardTheme: CardThemeData(
|
||
|
|
elevation: 0,
|
||
|
|
color: tokens.shellBg,
|
||
|
|
shape: RoundedRectangleBorder(
|
||
|
|
borderRadius: BorderRadius.circular(tokens.posterRadius),
|
||
|
|
),
|
||
|
|
clipBehavior: Clip.antiAlias,
|
||
|
|
margin: EdgeInsets.zero,
|
||
|
|
),
|
||
|
|
filledButtonTheme: FilledButtonThemeData(
|
||
|
|
style: FilledButton.styleFrom(
|
||
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
||
|
|
style: OutlinedButton.styleFrom(
|
||
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
textButtonTheme: TextButtonThemeData(
|
||
|
|
style: TextButton.styleFrom(
|
||
|
|
foregroundColor: onSurface,
|
||
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
inputDecorationTheme: InputDecorationTheme(
|
||
|
|
filled: false,
|
||
|
|
hintStyle: TextStyle(color: secondaryText.withValues(alpha: 0.6)),
|
||
|
|
border: OutlineInputBorder(
|
||
|
|
borderRadius: BorderRadius.circular(4),
|
||
|
|
borderSide: BorderSide(color: tokens.separatorColor),
|
||
|
|
),
|
||
|
|
enabledBorder: OutlineInputBorder(
|
||
|
|
borderRadius: BorderRadius.circular(4),
|
||
|
|
borderSide: BorderSide(color: tokens.separatorColor),
|
||
|
|
),
|
||
|
|
focusedBorder: OutlineInputBorder(
|
||
|
|
borderRadius: BorderRadius.circular(4),
|
||
|
|
borderSide: BorderSide(color: scheme.primary.withValues(alpha: 0.6)),
|
||
|
|
),
|
||
|
|
contentPadding: const EdgeInsets.symmetric(
|
||
|
|
horizontal: 12,
|
||
|
|
vertical: 12,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
dividerTheme: DividerThemeData(
|
||
|
|
color: tokens.separatorColor,
|
||
|
|
thickness: 1,
|
||
|
|
space: 1,
|
||
|
|
),
|
||
|
|
switchTheme: SwitchThemeData(
|
||
|
|
thumbColor: WidgetStateProperty.resolveWith((states) {
|
||
|
|
if (states.contains(WidgetState.selected)) {
|
||
|
|
return isLight ? Colors.white : const Color(0xFF1C1C1E);
|
||
|
|
}
|
||
|
|
return isLight ? const Color(0xFFBDBDBD) : const Color(0xFF6B6B6B);
|
||
|
|
}),
|
||
|
|
trackColor: WidgetStateProperty.resolveWith((states) {
|
||
|
|
if (states.contains(WidgetState.selected)) {
|
||
|
|
return isLight ? const Color(0xFF1C1C1E) : Colors.white;
|
||
|
|
}
|
||
|
|
return isLight ? const Color(0xFFDEDEDE) : const Color(0xFF3A3A3A);
|
||
|
|
}),
|
||
|
|
trackOutlineColor: WidgetStateProperty.all(Colors.transparent),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
extension AppTokensContext on BuildContext {
|
||
|
|
AppTokens get appTokens => Theme.of(this).brightness == Brightness.dark
|
||
|
|
? AppTheme.darkTokens
|
||
|
|
: AppTheme.lightTokens;
|
||
|
|
}
|