-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(mobile): Immich image provider (#7016)
* Adds image provider * uses image provider * wip load preview * wip everything but activity asset thumbnail needs some help with a remote id * Immich provider used in gallery * First draft of the immich image provider, working nicely! * Removed OriginalImageProvider * Fixes for thumbnails * feat(mobile): thumbhash support (#7028) * feat(mobile): thumbhash support * perf(mobile): store bmp thumbhash bytes in Isar --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> * Uses octoimage for fade in and placeholders * fixes thumbnails, removes unused values, adds better thumbnail size * removes thumbhash support for now * Forgot one thumbhash removal * Use big thumbnail for local image on ios * fix(mobile): Multipart image loading for iOS double swipe (#7064) * uses local thumb first * Multipart thumbnail * Clean up file delete * await file delete * Fynn's comments, made thumbnail smaller and doesn't crash on erroring out on thumbnail * lint --------- Co-authored-by: Marty Fuhry <marty@fuhry.farm> Co-authored-by: Alex <alex.tran1502@gmail.com> * Moves http client to global private place for reuse * Got rid of usePreview for local image providers since we always show a thumbnail anyway first * linter --------- Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex <alex.tran1502@gmail.com> Co-authored-by: Marty Fuhry <marty@fuhry.farm>
- Loading branch information
1 parent
4b3f8d1
commit 9b4a770
Showing
21 changed files
with
540 additions
and
364 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
106 changes: 106 additions & 0 deletions
106
mobile/lib/modules/asset_viewer/image_providers/immich_local_image_provider.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import 'dart:async'; | ||
import 'dart:io'; | ||
import 'dart:ui' as ui; | ||
|
||
import 'package:cached_network_image/cached_network_image.dart'; | ||
|
||
import 'package:flutter/foundation.dart'; | ||
import 'package:flutter/painting.dart'; | ||
import 'package:immich_mobile/shared/models/asset.dart'; | ||
import 'package:photo_manager/photo_manager.dart'; | ||
|
||
/// The local image provider for an asset | ||
/// Only viable | ||
class ImmichLocalImageProvider extends ImageProvider<Asset> { | ||
final Asset asset; | ||
|
||
ImmichLocalImageProvider({ | ||
required this.asset, | ||
}) : assert(asset.local != null, 'Only usable when asset.local is set'); | ||
|
||
/// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key | ||
/// that describes the precise image to load. | ||
@override | ||
Future<Asset> obtainKey(ImageConfiguration configuration) { | ||
return SynchronousFuture(asset); | ||
} | ||
|
||
@override | ||
ImageStreamCompleter loadImage(Asset key, ImageDecoderCallback decode) { | ||
final chunkEvents = StreamController<ImageChunkEvent>(); | ||
return MultiImageStreamCompleter( | ||
codec: _codec(key, decode, chunkEvents), | ||
scale: 1.0, | ||
chunkEvents: chunkEvents.stream, | ||
informationCollector: () sync* { | ||
yield ErrorDescription(asset.fileName); | ||
}, | ||
); | ||
} | ||
|
||
// Streams in each stage of the image as we ask for it | ||
Stream<ui.Codec> _codec( | ||
Asset key, | ||
ImageDecoderCallback decode, | ||
StreamController<ImageChunkEvent> chunkEvents, | ||
) async* { | ||
// Load a small thumbnail | ||
final thumbBytes = await asset.local?.thumbnailDataWithSize( | ||
const ThumbnailSize.square(256), | ||
quality: 80, | ||
); | ||
if (thumbBytes != null) { | ||
final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes); | ||
final codec = await decode(buffer); | ||
yield codec; | ||
} else { | ||
debugPrint("Loading thumb for ${asset.fileName} failed"); | ||
} | ||
|
||
if (asset.isImage) { | ||
/// Using 2K thumbnail for local iOS image to avoid double swiping issue | ||
if (Platform.isIOS) { | ||
final largeImageBytes = await asset.local | ||
?.thumbnailDataWithSize(const ThumbnailSize(3840, 2160)); | ||
if (largeImageBytes == null) { | ||
throw StateError( | ||
"Loading thumb for local photo ${asset.fileName} failed", | ||
); | ||
} | ||
final buffer = await ui.ImmutableBuffer.fromUint8List(largeImageBytes); | ||
final codec = await decode(buffer); | ||
yield codec; | ||
} else { | ||
// Use the original file for Android | ||
final File? file = await asset.local?.originFile; | ||
if (file == null) { | ||
throw StateError("Opening file for asset ${asset.fileName} failed"); | ||
} | ||
try { | ||
final buffer = await ui.ImmutableBuffer.fromFilePath(file.path); | ||
final codec = await decode(buffer); | ||
yield codec; | ||
} catch (error) { | ||
throw StateError("Loading asset ${asset.fileName} failed"); | ||
} finally { | ||
if (Platform.isIOS) { | ||
// Clean up this file | ||
await file.delete(); | ||
} | ||
} | ||
} | ||
} | ||
|
||
chunkEvents.close(); | ||
} | ||
|
||
@override | ||
bool operator ==(Object other) { | ||
if (other is! ImmichLocalImageProvider) return false; | ||
if (identical(this, other)) return true; | ||
return asset == other.asset; | ||
} | ||
|
||
@override | ||
int get hashCode => asset.hashCode; | ||
} |
145 changes: 145 additions & 0 deletions
145
mobile/lib/modules/asset_viewer/image_providers/immich_remote_image_provider.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
import 'dart:async'; | ||
import 'dart:io'; | ||
import 'dart:ui' as ui; | ||
|
||
import 'package:cached_network_image/cached_network_image.dart'; | ||
import 'package:openapi/api.dart' as api; | ||
|
||
import 'package:flutter/foundation.dart'; | ||
import 'package:flutter/painting.dart'; | ||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; | ||
import 'package:immich_mobile/shared/models/asset.dart'; | ||
import 'package:immich_mobile/shared/models/store.dart'; | ||
import 'package:immich_mobile/utils/image_url_builder.dart'; | ||
|
||
/// Our Image Provider HTTP client to make the request | ||
final _httpClient = HttpClient()..autoUncompress = false; | ||
|
||
/// The remote image provider | ||
class ImmichRemoteImageProvider extends ImageProvider<String> { | ||
/// The [Asset.remoteId] of the asset to fetch | ||
final String assetId; | ||
|
||
// If this is a thumbnail, we stop at loading the | ||
// smallest version of the remote image | ||
final bool isThumbnail; | ||
|
||
ImmichRemoteImageProvider({ | ||
required this.assetId, | ||
this.isThumbnail = false, | ||
}); | ||
|
||
/// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key | ||
/// that describes the precise image to load. | ||
@override | ||
Future<String> obtainKey(ImageConfiguration configuration) { | ||
return SynchronousFuture('$assetId,$isThumbnail'); | ||
} | ||
|
||
@override | ||
ImageStreamCompleter loadImage(String key, ImageDecoderCallback decode) { | ||
final id = key.split(',').first; | ||
final chunkEvents = StreamController<ImageChunkEvent>(); | ||
return MultiImageStreamCompleter( | ||
codec: _codec(id, decode, chunkEvents), | ||
scale: 1.0, | ||
chunkEvents: chunkEvents.stream, | ||
); | ||
} | ||
|
||
/// Whether to show the original file or load a compressed version | ||
bool get _useOriginal => Store.get( | ||
AppSettingsEnum.loadOriginal.storeKey, | ||
AppSettingsEnum.loadOriginal.defaultValue, | ||
); | ||
|
||
/// Whether to load the preview thumbnail first or not | ||
bool get _loadPreview => Store.get( | ||
AppSettingsEnum.loadPreview.storeKey, | ||
AppSettingsEnum.loadPreview.defaultValue, | ||
); | ||
|
||
// Streams in each stage of the image as we ask for it | ||
Stream<ui.Codec> _codec( | ||
String key, | ||
ImageDecoderCallback decode, | ||
StreamController<ImageChunkEvent> chunkEvents, | ||
) async* { | ||
// Load a preview to the chunk events | ||
if (_loadPreview || isThumbnail) { | ||
final preview = getThumbnailUrlForRemoteId( | ||
assetId, | ||
type: api.ThumbnailFormat.WEBP, | ||
); | ||
|
||
yield await _loadFromUri( | ||
Uri.parse(preview), | ||
decode, | ||
chunkEvents, | ||
); | ||
} | ||
|
||
// Guard thumnbail rendering | ||
if (isThumbnail) { | ||
await chunkEvents.close(); | ||
return; | ||
} | ||
|
||
// Load the higher resolution version of the image | ||
final url = getThumbnailUrlForRemoteId( | ||
assetId, | ||
type: api.ThumbnailFormat.JPEG, | ||
); | ||
final codec = await _loadFromUri(Uri.parse(url), decode, chunkEvents); | ||
yield codec; | ||
|
||
// Load the final remote image | ||
if (_useOriginal) { | ||
// Load the original image | ||
final url = getImageUrlFromId(assetId); | ||
final codec = await _loadFromUri(Uri.parse(url), decode, chunkEvents); | ||
yield codec; | ||
} | ||
await chunkEvents.close(); | ||
} | ||
|
||
// Loads the codec from the URI and sends the events to the [chunkEvents] stream | ||
Future<ui.Codec> _loadFromUri( | ||
Uri uri, | ||
ImageDecoderCallback decode, | ||
StreamController<ImageChunkEvent> chunkEvents, | ||
) async { | ||
final request = await _httpClient.getUrl(uri); | ||
request.headers.add( | ||
'x-immich-user-token', | ||
Store.get(StoreKey.accessToken), | ||
); | ||
final response = await request.close(); | ||
// Chunks of the completed image can be shown | ||
final data = await consolidateHttpClientResponseBytes( | ||
response, | ||
onBytesReceived: (cumulative, total) { | ||
chunkEvents.add( | ||
ImageChunkEvent( | ||
cumulativeBytesLoaded: cumulative, | ||
expectedTotalBytes: total, | ||
), | ||
); | ||
}, | ||
); | ||
|
||
// Decode the response | ||
final buffer = await ui.ImmutableBuffer.fromUint8List(data); | ||
return decode(buffer); | ||
} | ||
|
||
@override | ||
bool operator ==(Object other) { | ||
if (other is! ImmichRemoteImageProvider) return false; | ||
if (identical(this, other)) return true; | ||
return assetId == other.assetId; | ||
} | ||
|
||
@override | ||
int get hashCode => assetId.hashCode; | ||
} |
Oops, something went wrong.