fix(flutter): clear analyze --fatal-infos issues

Three classes:

1. album_cover_cache.dart docstring used <applicationCacheDirectory>
   and <albumId> which the analyzer reads as unintended HTML. Wrapped
   the path in backticks and used {} placeholders.

2. settings AppearanceSection used RadioListTile.groupValue + onChanged,
   deprecated in Flutter 3.32+. Wrapped the radios in a RadioGroup
   ancestor (the new API) so individual tiles only declare value +
   activeColor. Test updated to read groupValue off the RadioGroup
   ancestor.

3. theme_extension.dart factories built FabledSwordTheme(...) without
   const, even though all args are static const tokens. Added const to
   the outer constructor calls in both .dark() and .light() — this
   propagates const to the inner TextStyle(...) calls automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 16:16:35 -04:00
parent dd848e76e8
commit 84a638a41f
4 changed files with 35 additions and 25 deletions
@@ -10,7 +10,7 @@ import 'package:path_provider/path_provider.dart';
/// and doesn't carry our Bearer header, so we pre-fetch via the /// and doesn't carry our Bearer header, so we pre-fetch via the
/// authenticated dio and hand the system a local file path instead. /// authenticated dio and hand the system a local file path instead.
/// ///
/// Cache layout: <applicationCacheDirectory>/album_covers/<albumId>.jpg. /// Cache layout: `{applicationCacheDirectory}/album_covers/{albumId}.jpg`.
/// No explicit eviction — covers are tiny and OS clears app cache when /// No explicit eviction — covers are tiny and OS clears app cache when
/// space is tight. /// space is tight.
class AlbumCoverCache { class AlbumCoverCache {
@@ -503,26 +503,32 @@ class _AppearanceSection extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final fs = Theme.of(context).extension<FabledSwordTheme>()!; final fs = Theme.of(context).extension<FabledSwordTheme>()!;
final current = ref.watch(themeModeProvider).value ?? AppThemeMode.system; final current = ref.watch(themeModeProvider).value ?? AppThemeMode.system;
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ // RadioGroup is the post-Flutter-3.32 API: the ancestor owns
const _SectionHeader('Appearance'), // groupValue/onChanged so individual RadioListTiles don't have
for (final mode in AppThemeMode.values) // to repeat them. Pre-3.32 RadioListTile.groupValue/onChanged
RadioListTile<AppThemeMode>( // are deprecated.
key: Key('appearance_${mode.name}'), return RadioGroup<AppThemeMode>(
title: Text(_label(mode), style: TextStyle(color: fs.parchment)), groupValue: current,
subtitle: mode == AppThemeMode.system onChanged: (m) {
? Text('Match the device setting', if (m != null) {
style: TextStyle(color: fs.ash)) ref.read(themeModeProvider.notifier).set(m);
: null, }
value: mode, },
groupValue: current, child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
activeColor: fs.accent, const _SectionHeader('Appearance'),
onChanged: (m) { for (final mode in AppThemeMode.values)
if (m != null) { RadioListTile<AppThemeMode>(
ref.read(themeModeProvider.notifier).set(m); key: Key('appearance_${mode.name}'),
} title: Text(_label(mode), style: TextStyle(color: fs.parchment)),
}, subtitle: mode == AppThemeMode.system
), ? Text('Match the device setting',
]); style: TextStyle(color: fs.ash))
: null,
value: mode,
activeColor: fs.accent,
),
]),
);
} }
String _label(AppThemeMode m) => switch (m) { String _label(AppThemeMode m) => switch (m) {
@@ -30,7 +30,7 @@ class FabledSwordTheme extends ThemeExtension<FabledSwordTheme> {
final Color warning, error, info; final Color warning, error, info;
final TextStyle display, body, mono; final TextStyle display, body, mono;
factory FabledSwordTheme.dark() => FabledSwordTheme( factory FabledSwordTheme.dark() => const FabledSwordTheme(
accent: FabledSwordFlatTokens.accent, accent: FabledSwordFlatTokens.accent,
obsidian: FabledSwordDarkTokens.obsidian, obsidian: FabledSwordDarkTokens.obsidian,
iron: FabledSwordDarkTokens.iron, iron: FabledSwordDarkTokens.iron,
@@ -50,7 +50,7 @@ class FabledSwordTheme extends ThemeExtension<FabledSwordTheme> {
mono: TextStyle(fontFamily: FabledSwordFlatTokens.fontMono), mono: TextStyle(fontFamily: FabledSwordFlatTokens.fontMono),
); );
factory FabledSwordTheme.light() => FabledSwordTheme( factory FabledSwordTheme.light() => const FabledSwordTheme(
accent: FabledSwordFlatTokens.accent, accent: FabledSwordFlatTokens.accent,
obsidian: FabledSwordLightTokens.obsidian, obsidian: FabledSwordLightTokens.obsidian,
iron: FabledSwordLightTokens.iron, iron: FabledSwordLightTokens.iron,
@@ -49,11 +49,15 @@ void main() {
expect(find.byKey(const Key('appearance_light')), findsOneWidget); expect(find.byKey(const Key('appearance_light')), findsOneWidget);
expect(find.byKey(const Key('appearance_dark')), findsOneWidget); expect(find.byKey(const Key('appearance_dark')), findsOneWidget);
// The dark radio should be selected. // The RadioGroup ancestor owns groupValue post-Flutter-3.32; the
// selected mode is read off it rather than off individual tiles.
final group = t.widget<RadioGroup<AppThemeMode>>(
find.byType(RadioGroup<AppThemeMode>),
);
expect(group.groupValue, AppThemeMode.dark);
final darkRadio = t.widget<RadioListTile<AppThemeMode>>( final darkRadio = t.widget<RadioListTile<AppThemeMode>>(
find.byKey(const Key('appearance_dark')), find.byKey(const Key('appearance_dark')),
); );
expect(darkRadio.groupValue, AppThemeMode.dark);
expect(darkRadio.value, AppThemeMode.dark); expect(darkRadio.value, AppThemeMode.dark);
}); });