Files
SpotiFLAC-Mobile/lib/theme/app_theme.dart
zarzet f511f30ad0 feat: add resolve API with SongLink fallback, fix multi-artist tags (#288), and cleanup
Resolve API (api.zarz.moe):
- Refactor songlink.go: Spotify URLs use resolve API, non-Spotify uses SongLink API
- Add SongLink fallback when resolve API fails for Spotify (two-layer resilience)
- Remove dead code: page parser, XOR-obfuscated keys, legacy helpers

Multi-artist tag fix (#288):
- Add RewriteSplitArtistTags() in Go to rewrite ARTIST/ALBUMARTIST as split Vorbis comments
- Wire method channel handler in Android (MainActivity.kt) and iOS (AppDelegate.swift)
- Add PlatformBridge.rewriteSplitArtistTags() in Dart
- Call native FLAC rewriter after FFmpeg embed when split_vorbis mode is active
- Extract deezerTrackArtistDisplay() helper to use Contributors in album/playlist tracks

Code cleanup:
- Remove unused imports, dead code, and redundant comments across Go and Dart
- Fix build: remove stale getQobuzDebugKey() reference in deezer_download.go
2026-04-01 02:49:19 +07:00

254 lines
8.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:spotiflac_android/models/theme_settings.dart';
class AppTheme {
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,
),
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: scheme.brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
systemNavigationBarColor: isAmoled
? Colors.black
: scheme.surfaceContainer,
systemNavigationBarIconBrightness: scheme.brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
),
);
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);
}