2025-08-20 15:36:44 -04:00
|
|
|
part of 'image_request.dart';
|
|
|
|
|
|
|
|
|
|
class RemoteImageRequest extends ImageRequest {
|
2025-08-29 18:26:42 -04:00
|
|
|
static final _client = const NetworkRepository().getHttpClient(
|
|
|
|
|
directoryName: 'thumbnails',
|
|
|
|
|
diskCapacity: kThumbnailDiskCacheSize,
|
|
|
|
|
memoryCapacity: 0,
|
|
|
|
|
maxConnections: 16,
|
|
|
|
|
cacheMode: CacheMode.disk,
|
|
|
|
|
);
|
2025-08-20 15:36:44 -04:00
|
|
|
final String uri;
|
|
|
|
|
final Map<String, String> headers;
|
2025-08-29 18:26:42 -04:00
|
|
|
final abortTrigger = Completer<void>();
|
2025-08-20 15:36:44 -04:00
|
|
|
|
2025-08-29 18:26:42 -04:00
|
|
|
RemoteImageRequest({required this.uri, required this.headers});
|
2025-08-20 15:36:44 -04:00
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Future<ImageInfo?> load(ImageDecoderCallback decode, {double scale = 1.0}) async {
|
|
|
|
|
if (_isCancelled) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2025-08-29 18:26:42 -04:00
|
|
|
final buffer = await _downloadImage();
|
2025-08-20 15:36:44 -04:00
|
|
|
if (buffer == null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2025-08-21 12:18:31 -05:00
|
|
|
|
2025-08-20 15:36:44 -04:00
|
|
|
return await _decodeBuffer(buffer, decode, scale);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (_isCancelled) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rethrow;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-29 18:26:42 -04:00
|
|
|
Future<ImmutableBuffer?> _downloadImage() async {
|
2025-08-20 15:36:44 -04:00
|
|
|
if (_isCancelled) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-31 19:53:17 -04:00
|
|
|
final req = http.AbortableRequest('GET', Uri.parse(uri), abortTrigger: abortTrigger.future);
|
2025-08-29 18:26:42 -04:00
|
|
|
req.headers.addAll(headers);
|
|
|
|
|
final res = await _client.send(req);
|
2025-08-20 15:36:44 -04:00
|
|
|
if (_isCancelled) {
|
2025-08-29 18:26:42 -04:00
|
|
|
_onCancelled();
|
2025-08-20 15:36:44 -04:00
|
|
|
return null;
|
|
|
|
|
}
|
2025-08-23 15:25:12 -05:00
|
|
|
|
2025-08-31 19:53:17 -04:00
|
|
|
if (res.statusCode != 200) {
|
|
|
|
|
throw Exception('Failed to download $uri: ${res.statusCode}');
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-29 18:26:42 -04:00
|
|
|
final stream = res.stream.map((chunk) {
|
2025-08-26 11:49:12 -04:00
|
|
|
if (_isCancelled) {
|
|
|
|
|
throw StateError('Cancelled request');
|
|
|
|
|
}
|
|
|
|
|
return chunk;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
2025-08-29 18:26:42 -04:00
|
|
|
final Uint8List bytes = await _downloadBytes(stream, res.contentLength ?? -1);
|
|
|
|
|
if (_isCancelled) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2025-08-26 11:49:12 -04:00
|
|
|
return await ImmutableBuffer.fromUint8List(bytes);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (_isCancelled) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
rethrow;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<Uint8List> _downloadBytes(Stream<List<int>> stream, int length) async {
|
2025-08-23 15:25:12 -05:00
|
|
|
final Uint8List bytes;
|
2025-08-20 15:36:44 -04:00
|
|
|
int offset = 0;
|
2025-08-26 11:49:12 -04:00
|
|
|
if (length > 0) {
|
2025-08-23 15:25:12 -05:00
|
|
|
// Known content length - use pre-allocated buffer
|
2025-08-26 11:49:12 -04:00
|
|
|
bytes = Uint8List(length);
|
|
|
|
|
await stream.listen((chunk) {
|
2025-08-23 15:25:12 -05:00
|
|
|
bytes.setAll(offset, chunk);
|
|
|
|
|
offset += chunk.length;
|
2025-08-26 11:49:12 -04:00
|
|
|
}, cancelOnError: true).asFuture();
|
2025-08-23 15:25:12 -05:00
|
|
|
} else {
|
|
|
|
|
// Unknown content length - collect chunks dynamically
|
|
|
|
|
final chunks = <List<int>>[];
|
|
|
|
|
int totalLength = 0;
|
2025-08-26 11:49:12 -04:00
|
|
|
await stream.listen((chunk) {
|
2025-08-23 15:25:12 -05:00
|
|
|
chunks.add(chunk);
|
|
|
|
|
totalLength += chunk.length;
|
2025-08-26 11:49:12 -04:00
|
|
|
}, cancelOnError: true).asFuture();
|
2025-08-23 15:25:12 -05:00
|
|
|
|
|
|
|
|
bytes = Uint8List(totalLength);
|
|
|
|
|
for (final chunk in chunks) {
|
|
|
|
|
bytes.setAll(offset, chunk);
|
|
|
|
|
offset += chunk.length;
|
2025-08-20 15:36:44 -04:00
|
|
|
}
|
2025-08-23 15:25:12 -05:00
|
|
|
}
|
|
|
|
|
|
2025-08-26 11:49:12 -04:00
|
|
|
return bytes;
|
2025-08-20 15:36:44 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<ImageInfo?> _decodeBuffer(ImmutableBuffer buffer, ImageDecoderCallback decode, scale) async {
|
|
|
|
|
if (_isCancelled) {
|
|
|
|
|
buffer.dispose();
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
final codec = await decode(buffer);
|
|
|
|
|
if (_isCancelled) {
|
|
|
|
|
buffer.dispose();
|
|
|
|
|
codec.dispose();
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
final frame = await codec.getNextFrame();
|
|
|
|
|
return ImageInfo(image: frame.image, scale: scale);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void _onCancelled() {
|
2025-08-29 18:26:42 -04:00
|
|
|
abortTrigger.complete();
|
2025-08-20 15:36:44 -04:00
|
|
|
}
|
|
|
|
|
}
|