fix(mobile): caching thumbnails to disk (#21275)

This commit is contained in:
Mert 2025-08-26 11:49:12 -04:00 committed by GitHub
parent 19c53609e1
commit e67265cef2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 51 additions and 26 deletions

View file

@ -65,40 +65,53 @@ class RemoteImageRequest extends ImageRequest {
return null; return null;
} }
// Handle unknown content length from reverse proxy final cacheManager = this.cacheManager;
final contentLength = response.contentLength; final streamController = StreamController<List<int>>(sync: true);
final Uint8List bytes; final Stream<List<int>> stream;
int offset = 0; cacheManager?.putStreamedFile(url, streamController.stream);
stream = response.map((chunk) {
if (contentLength >= 0) {
// Known content length - use pre-allocated buffer
bytes = Uint8List(contentLength);
final subscription = response.listen((List<int> chunk) {
// this is important to break the response stream if the request is cancelled
if (_isCancelled) { if (_isCancelled) {
throw StateError('Cancelled request'); throw StateError('Cancelled request');
} }
if (cacheManager != null) {
streamController.add(chunk);
}
return chunk;
});
try {
final Uint8List bytes = await _downloadBytes(stream, response.contentLength);
streamController.close();
return await ImmutableBuffer.fromUint8List(bytes);
} catch (e) {
streamController.addError(e);
streamController.close();
if (_isCancelled) {
return null;
}
rethrow;
}
}
Future<Uint8List> _downloadBytes(Stream<List<int>> stream, int length) async {
final Uint8List bytes;
int offset = 0;
if (length > 0) {
// Known content length - use pre-allocated buffer
bytes = Uint8List(length);
await stream.listen((chunk) {
bytes.setAll(offset, chunk); bytes.setAll(offset, chunk);
offset += chunk.length; offset += chunk.length;
}, cancelOnError: true); }, cancelOnError: true).asFuture();
cacheManager?.putStreamedFile(url, response);
await subscription.asFuture();
} else { } else {
// Unknown content length - collect chunks dynamically // Unknown content length - collect chunks dynamically
final chunks = <List<int>>[]; final chunks = <List<int>>[];
int totalLength = 0; int totalLength = 0;
final subscription = response.listen((List<int> chunk) { await stream.listen((chunk) {
// this is important to break the response stream if the request is cancelled
if (_isCancelled) {
throw StateError('Cancelled request');
}
chunks.add(chunk); chunks.add(chunk);
totalLength += chunk.length; totalLength += chunk.length;
}, cancelOnError: true); }, cancelOnError: true).asFuture();
cacheManager?.putStreamedFile(url, response);
await subscription.asFuture();
// Combine all chunks into a single buffer
bytes = Uint8List(totalLength); bytes = Uint8List(totalLength);
for (final chunk in chunks) { for (final chunk in chunks) {
bytes.setAll(offset, chunk); bytes.setAll(offset, chunk);
@ -106,7 +119,7 @@ class RemoteImageRequest extends ImageRequest {
} }
} }
return await ImmutableBuffer.fromUint8List(bytes); return bytes;
} }
Future<ImageInfo?> _loadCachedFile( Future<ImageInfo?> _loadCachedFile(

View file

@ -94,7 +94,7 @@ class _ThumbnailState extends State<Thumbnail> with SingleTickerProviderStateMix
imageInfo.dispose(); imageInfo.dispose();
return; return;
} }
_fadeController.value = 1.0;
setState(() { setState(() {
_providerImage = imageInfo.image; _providerImage = imageInfo.image;
}); });
@ -115,7 +115,7 @@ class _ThumbnailState extends State<Thumbnail> with SingleTickerProviderStateMix
final imageStream = _imageStream = imageProvider.resolve(ImageConfiguration.empty); final imageStream = _imageStream = imageProvider.resolve(ImageConfiguration.empty);
final imageStreamListener = _imageStreamListener = ImageStreamListener( final imageStreamListener = _imageStreamListener = ImageStreamListener(
(ImageInfo imageInfo, bool synchronousCall) { (ImageInfo imageInfo, bool synchronousCall) {
_stopListeningToStream(); _stopListeningToThumbhashStream();
if (!mounted) { if (!mounted) {
imageInfo.dispose(); imageInfo.dispose();
return; return;

View file

@ -38,9 +38,21 @@ abstract class RemoteCacheManager extends CacheManager {
final file = await store.fileSystem.createFile(path); final file = await store.fileSystem.createFile(path);
final sink = file.openWrite(); final sink = file.openWrite();
try { try {
await source.pipe(sink); await source.listen(sink.add, cancelOnError: true).asFuture();
} catch (e) { } catch (e) {
try {
await sink.close(); await sink.close();
await file.delete();
} catch (e) {
_log.severe('Failed to delete incomplete cache file: $e');
}
return;
}
try {
await sink.flush();
await sink.close();
} catch (e) {
try { try {
await file.delete(); await file.delete();
} catch (e) { } catch (e) {