Files
SpotiFLAC-Mobile/lib/theme/app_theme.dart
zarzet d4b37edc2f feat: add animation utilities and fix regressions in UI refactor
- Add animation_utils.dart with skeleton loaders, staggered list animations,
  animated checkboxes, badge bump, download success overlay, and shared
  page route helper
- Replace CircularProgressIndicator with shimmer skeleton loaders across
  album, artist, playlist, search, store, and extension screens
- Unify page transitions via slidePageRoute (MaterialPageRoute) for
  Android predictive back gesture support
- Extract AnimatedSelectionCheckbox with configurable unselectedColor
  to preserve original transparent/opaque backgrounds per context
- Add swipe-to-dismiss on download queue items with confirmDismiss
  dialog for active downloads to prevent accidental cancellation
- Add Hero animations for cover art transitions between list and detail
- Add AnimatedBadge bump on navigation bar badge count changes
- Add DownloadSuccessOverlay green flash on download completion
- Restore fine-grained ref.watch(.select()) in _CollectionTrackTile
  to avoid full list rebuilds on download history changes
- Fix DownloadSuccessOverlay re-flashing on widget recreation by
  initialising _wasSuccess from initial widget state
- Remove orphan Hero tag in search_screen that had no matching pair
- Chip borderRadius updated from 8 to 20 for consistency
2026-03-26 13:38:07 +07:00

242 lines
8.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:spotiflac_android/models/theme_settings.dart';
class AppTheme {
/// Default seed color (Spotify green)
static const Color defaultSeedColor = Color(kDefaultSeedColor);
static ThemeData light({ColorScheme? dynamicScheme, Color? seedColor}) {
final scheme =
dynamicScheme ??
ColorScheme.fromSeed(
seedColor: seedColor ?? defaultSeedColor,
brightness: Brightness.light,
);
return ThemeData(
useMaterial3: true,
colorScheme: scheme,
appBarTheme: _appBarTheme(scheme),
cardTheme: _cardTheme(scheme),
elevatedButtonTheme: _elevatedButtonTheme(scheme),
filledButtonTheme: _filledButtonTheme(scheme),
outlinedButtonTheme: _outlinedButtonTheme(scheme),
textButtonTheme: _textButtonTheme(scheme),
floatingActionButtonTheme: _fabTheme(scheme),
inputDecorationTheme: _inputDecorationTheme(scheme),
listTileTheme: _listTileTheme(scheme),
dialogTheme: _dialogTheme(scheme),
navigationBarTheme: _navigationBarTheme(scheme),
snackBarTheme: _snackBarTheme(scheme),
progressIndicatorTheme: _progressIndicatorTheme(scheme),
switchTheme: _switchTheme(scheme),
chipTheme: _chipTheme(scheme),
dividerTheme: _dividerTheme(scheme),
);
}
static ThemeData dark({
ColorScheme? dynamicScheme,
Color? seedColor,
bool isAmoled = false,
}) {
final scheme =
dynamicScheme ??
ColorScheme.fromSeed(
seedColor: seedColor ?? defaultSeedColor,
brightness: Brightness.dark,
);
return ThemeData(
useMaterial3: true,
colorScheme: scheme,
scaffoldBackgroundColor: isAmoled ? Colors.black : null,
appBarTheme: _appBarTheme(scheme, isAmoled: isAmoled),
cardTheme: _cardTheme(scheme),
elevatedButtonTheme: _elevatedButtonTheme(scheme),
filledButtonTheme: _filledButtonTheme(scheme),
outlinedButtonTheme: _outlinedButtonTheme(scheme),
textButtonTheme: _textButtonTheme(scheme),
floatingActionButtonTheme: _fabTheme(scheme),
inputDecorationTheme: _inputDecorationTheme(scheme),
listTileTheme: _listTileTheme(scheme),
dialogTheme: _dialogTheme(scheme),
navigationBarTheme: _navigationBarTheme(scheme, isAmoled: isAmoled),
snackBarTheme: _snackBarTheme(scheme),
progressIndicatorTheme: _progressIndicatorTheme(scheme),
switchTheme: _switchTheme(scheme),
chipTheme: _chipTheme(scheme),
dividerTheme: _dividerTheme(scheme),
);
}
static AppBarTheme _appBarTheme(
ColorScheme scheme, {
bool isAmoled = false,
}) => AppBarTheme(
elevation: 0,
scrolledUnderElevation: isAmoled ? 0 : 3,
backgroundColor: isAmoled ? Colors.black : scheme.surface,
foregroundColor: scheme.onSurface,
surfaceTintColor: isAmoled ? Colors.transparent : scheme.surfaceTint,
centerTitle: true,
titleTextStyle: TextStyle(
color: scheme.onSurface,
fontSize: 22,
fontWeight: FontWeight.w500,
),
);
static CardThemeData _cardTheme(ColorScheme scheme) => CardThemeData(
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
color: scheme.surfaceContainerLow,
surfaceTintColor: scheme.surfaceTint,
);
static ElevatedButtonThemeData _elevatedButtonTheme(ColorScheme scheme) =>
ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
);
static FilledButtonThemeData _filledButtonTheme(ColorScheme scheme) =>
FilledButtonThemeData(
style: FilledButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
);
static OutlinedButtonThemeData _outlinedButtonTheme(ColorScheme scheme) =>
OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
);
static TextButtonThemeData _textButtonTheme(ColorScheme scheme) =>
TextButtonThemeData(
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
);
static FloatingActionButtonThemeData _fabTheme(ColorScheme scheme) =>
FloatingActionButtonThemeData(
elevation: 3,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
backgroundColor: scheme.primaryContainer,
foregroundColor: scheme.onPrimaryContainer,
);
static InputDecorationTheme _inputDecorationTheme(ColorScheme scheme) =>
InputDecorationTheme(
filled: true,
fillColor: scheme.surfaceContainerHighest.withValues(alpha: 0.3),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(color: scheme.primary, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(color: scheme.error, width: 1),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
);
static ListTileThemeData _listTileTheme(ColorScheme scheme) =>
ListTileThemeData(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
);
static DialogThemeData _dialogTheme(ColorScheme scheme) => DialogThemeData(
elevation: 6,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
backgroundColor: scheme.surfaceContainerHigh,
surfaceTintColor: scheme.surfaceTint,
);
static NavigationBarThemeData _navigationBarTheme(
ColorScheme scheme, {
bool isAmoled = false,
}) => NavigationBarThemeData(
elevation: 0,
backgroundColor: isAmoled ? Colors.black : scheme.surfaceContainer,
indicatorColor: scheme.secondaryContainer,
surfaceTintColor: isAmoled ? Colors.transparent : scheme.surfaceTint,
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
);
static SnackBarThemeData _snackBarTheme(ColorScheme scheme) =>
SnackBarThemeData(
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
backgroundColor: scheme.inverseSurface,
contentTextStyle: TextStyle(color: scheme.onInverseSurface),
);
static ProgressIndicatorThemeData _progressIndicatorTheme(
ColorScheme scheme,
) => ProgressIndicatorThemeData(
color: scheme.primary,
linearTrackColor: scheme.surfaceContainerHighest,
circularTrackColor: scheme.surfaceContainerHighest,
);
static SwitchThemeData _switchTheme(ColorScheme scheme) => SwitchThemeData(
thumbColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return scheme.onPrimary;
}
return scheme.outline;
}),
trackColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return scheme.primary;
}
return scheme.surfaceContainerHighest;
}),
thumbIcon: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return Icon(Icons.check, color: scheme.primary);
}
return null;
}),
);
static ChipThemeData _chipTheme(ColorScheme scheme) => ChipThemeData(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
backgroundColor: scheme.surfaceContainerLow,
selectedColor: scheme.secondaryContainer,
);
static DividerThemeData _dividerTheme(ColorScheme scheme) =>
DividerThemeData(color: scheme.outlineVariant, thickness: 1, space: 1);
}