84a638a41f
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>
66 lines
2.3 KiB
Dart
66 lines
2.3 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:dio/dio.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
/// Caches album cover bytes to disk so MediaItem.artUri can point at a
|
|
/// file:// URI. Android's MediaSession framework fetches artUri itself
|
|
/// and doesn't carry our Bearer header, so we pre-fetch via the
|
|
/// authenticated dio and hand the system a local file path instead.
|
|
///
|
|
/// Cache layout: `{applicationCacheDirectory}/album_covers/{albumId}.jpg`.
|
|
/// No explicit eviction — covers are tiny and OS clears app cache when
|
|
/// space is tight.
|
|
class AlbumCoverCache {
|
|
AlbumCoverCache({
|
|
required Future<Dio> Function() dioFactory,
|
|
Future<Directory> Function()? cacheDirFactory,
|
|
}) : _dioFactory = dioFactory,
|
|
_cacheDirFactory = cacheDirFactory ?? getApplicationCacheDirectory;
|
|
|
|
final Future<Dio> Function() _dioFactory;
|
|
final Future<Directory> Function() _cacheDirFactory;
|
|
|
|
/// In-flight requests keyed by albumId so concurrent callers for the
|
|
/// same album dedupe to one fetch.
|
|
final Map<String, Future<String?>> _inflight = {};
|
|
|
|
/// Returns local file path to the album cover, or null on any
|
|
/// failure (network, 4xx/5xx, disk full, empty albumId).
|
|
Future<String?> getOrFetch(String albumId) {
|
|
if (albumId.isEmpty) return Future.value(null);
|
|
final existing = _inflight[albumId];
|
|
if (existing != null) return existing;
|
|
final fut = _doFetch(albumId);
|
|
_inflight[albumId] = fut;
|
|
fut.whenComplete(() => _inflight.remove(albumId));
|
|
return fut;
|
|
}
|
|
|
|
Future<String?> _doFetch(String albumId) async {
|
|
try {
|
|
final dir = await _cacheDirFactory();
|
|
final coversDir = Directory('${dir.path}/album_covers');
|
|
await coversDir.create(recursive: true);
|
|
final filePath = '${coversDir.path}/$albumId.jpg';
|
|
final file = File(filePath);
|
|
if (await file.exists()) return filePath;
|
|
|
|
final dio = await _dioFactory();
|
|
final r = await dio.get<List<int>>(
|
|
'/api/albums/$albumId/cover',
|
|
options: Options(responseType: ResponseType.bytes),
|
|
);
|
|
final bytes = r.data;
|
|
if (bytes == null || bytes.isEmpty) return null;
|
|
await file.writeAsBytes(bytes, flush: true);
|
|
return filePath;
|
|
} catch (e) {
|
|
debugPrint('AlbumCoverCache: fetch failed for $albumId: $e');
|
|
return null;
|
|
}
|
|
}
|
|
}
|