Initial commit
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import 'package:flutter/material.dart' as material;
|
||||
import 'package:flutter/widgets.dart' as widgets;
|
||||
|
||||
class AppScrollBehavior extends material.MaterialScrollBehavior {
|
||||
const AppScrollBehavior();
|
||||
|
||||
@override
|
||||
widgets.Widget buildScrollbar(
|
||||
widgets.BuildContext context,
|
||||
widgets.Widget child,
|
||||
widgets.ScrollableDetails details,
|
||||
) {
|
||||
switch (details.direction) {
|
||||
case widgets.AxisDirection.up:
|
||||
case widgets.AxisDirection.down:
|
||||
return child;
|
||||
case widgets.AxisDirection.left:
|
||||
case widgets.AxisDirection.right:
|
||||
return super.buildScrollbar(context, child, details);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../providers/appearance_provider.dart';
|
||||
import 'app_theme.dart';
|
||||
|
||||
|
||||
class AppThemeScope extends ConsumerWidget {
|
||||
final Widget child;
|
||||
|
||||
const AppThemeScope({super.key, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final mode = ref.watch(
|
||||
appearanceProvider.select((a) => a.value?.themeMode ?? ThemeMode.system),
|
||||
);
|
||||
final brightness = switch (mode) {
|
||||
ThemeMode.light => Brightness.light,
|
||||
ThemeMode.dark => Brightness.dark,
|
||||
ThemeMode.system => MediaQuery.platformBrightnessOf(context),
|
||||
};
|
||||
return Theme(
|
||||
data: brightness == Brightness.dark ? AppTheme.dark() : AppTheme.light(),
|
||||
child: Material(type: MaterialType.transparency, child: child),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user