Files
sm-emby-share/lib/shared/theme/app_theme.dart
T

300 lines
9.0 KiB
Dart
Raw Normal View History

2026-07-14 11:11:36 +08:00
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;
}