immich/mobile/lib/presentation/widgets/images/remote_image_provider.dart
Mert 1803692eab
feat(mobile): native clients (#21459)
* platform clients

* uppercase http method

* fix hot reload

* custom user agent

* init before app launch

* set defaults

* move to bootstrap

* unrelated change

* disable disk cache by default

* optimized decoding

* remove incremental

* android impl

* memory optimization

* lock approach is slower on ios

* conditional cronet

* clarify parameter

* enable disk cache

* set user agent

* flutter-side decode

* optimized http

* fixed locking

* refactor

* potential race conditions

* embedded cronet

* refactor, fix capacity handling

* fast path for known content length

* ios optimizations

* re-enable cache

* formatting

* bump concurrency

* clear cache button

* fix eviction race condition

* add extra cancellation check

* tighten dispose

* better error handling

* fix disposal

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2026-01-24 19:34:29 +00:00

121 lines
4 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/domain/services/setting.service.dart';
import 'package:immich_mobile/infrastructure/loaders/image_request.dart';
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_stream_completer.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
class RemoteThumbProvider extends CancellableImageProvider<RemoteThumbProvider>
with CancellableImageProviderMixin<RemoteThumbProvider> {
final String assetId;
final String thumbhash;
RemoteThumbProvider({required this.assetId, required this.thumbhash});
@override
Future<RemoteThumbProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture(this);
}
@override
ImageStreamCompleter loadImage(RemoteThumbProvider key, ImageDecoderCallback decode) {
return OneFramePlaceholderImageStreamCompleter(
_codec(key, decode),
informationCollector: () => <DiagnosticsNode>[
DiagnosticsProperty<ImageProvider>('Image provider', this),
DiagnosticsProperty<String>('Asset Id', key.assetId),
],
onDispose: cancel,
);
}
Stream<ImageInfo> _codec(RemoteThumbProvider key, ImageDecoderCallback decode) {
final request = this.request = RemoteImageRequest(
uri: getThumbnailUrlForRemoteId(key.assetId, thumbhash: key.thumbhash),
headers: ApiService.getRequestHeaders(),
);
return loadRequest(request, decode);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is RemoteThumbProvider) {
return assetId == other.assetId && thumbhash == other.thumbhash;
}
return false;
}
@override
int get hashCode => assetId.hashCode ^ thumbhash.hashCode;
}
class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImageProvider>
with CancellableImageProviderMixin<RemoteFullImageProvider> {
final String assetId;
final String thumbhash;
RemoteFullImageProvider({required this.assetId, required this.thumbhash});
@override
Future<RemoteFullImageProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture(this);
}
@override
ImageStreamCompleter loadImage(RemoteFullImageProvider key, ImageDecoderCallback decode) {
return OneFramePlaceholderImageStreamCompleter(
_codec(key, decode),
initialImage: getInitialImage(RemoteThumbProvider(assetId: key.assetId, thumbhash: key.thumbhash)),
informationCollector: () => <DiagnosticsNode>[
DiagnosticsProperty<ImageProvider>('Image provider', this),
DiagnosticsProperty<String>('Asset Id', key.assetId),
],
onDispose: cancel,
);
}
Stream<ImageInfo> _codec(RemoteFullImageProvider key, ImageDecoderCallback decode) async* {
yield* initialImageStream();
if (isCancelled) {
PaintingBinding.instance.imageCache.evict(this);
return;
}
final headers = ApiService.getRequestHeaders();
final request = this.request = RemoteImageRequest(
uri: getThumbnailUrlForRemoteId(key.assetId, type: AssetMediaSize.preview, thumbhash: key.thumbhash),
headers: headers,
);
yield* loadRequest(request, decode);
if (isCancelled) {
PaintingBinding.instance.imageCache.evict(this);
return;
}
if (AppSetting.get(Setting.loadOriginal)) {
final request = this.request = RemoteImageRequest(uri: getOriginalUrlForRemoteId(key.assetId), headers: headers);
yield* loadRequest(request, decode);
}
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is RemoteFullImageProvider) {
return assetId == other.assetId && thumbhash == other.thumbhash;
}
return false;
}
@override
int get hashCode => assetId.hashCode ^ thumbhash.hashCode;
}