mirror of
https://github.com/immich-app/immich.git
synced 2025-01-04 02:46:47 +01:00
feat(mobile): image caching & viewer improvements (#4095)
This commit is contained in:
parent
63b6a71ebd
commit
1c02e1dadf
10 changed files with 283 additions and 212 deletions
|
@ -13,6 +13,7 @@ import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
|
|||
import 'package:immich_mobile/modules/backup/models/duplicated_asset.model.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
|
||||
import 'package:immich_mobile/shared/cache/widgets_binding.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/shared/models/android_device_asset.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
|
@ -37,7 +38,7 @@ import 'package:logging/logging.dart';
|
|||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
ImmichWidgetsBinding();
|
||||
|
||||
final db = await loadDb();
|
||||
await initApp();
|
||||
|
|
|
@ -18,6 +18,7 @@ import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
|
|||
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/upload_dialog.dart';
|
||||
import 'package:immich_mobile/shared/cache/original_image_provider.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||
|
@ -31,8 +32,7 @@ import 'package:immich_mobile/shared/models/asset.dart';
|
|||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:openapi/api.dart' as api;
|
||||
import 'package:openapi/api.dart' show ThumbnailFormat;
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class GalleryViewerPage extends HookConsumerWidget {
|
||||
|
@ -51,6 +51,9 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||
|
||||
final PageController controller;
|
||||
|
||||
static const jpeg = ThumbnailFormat.JPEG;
|
||||
static const webp = ThumbnailFormat.WEBP;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final settings = ref.watch(appSettingsServiceProvider);
|
||||
|
@ -59,9 +62,9 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||
final isZoomed = useState<bool>(false);
|
||||
final isPlayingMotionVideo = useState(false);
|
||||
final isPlayingVideo = useState(false);
|
||||
final progressValue = useState(0.0);
|
||||
Offset? localPosition;
|
||||
final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}';
|
||||
final header = {"Authorization": authToken};
|
||||
final currentIndex = useState(initialIndex);
|
||||
final currentAsset = loadAsset(currentIndex.value);
|
||||
|
||||
|
@ -83,93 +86,52 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||
.watch(assetProvider.notifier)
|
||||
.toggleFavorite([asset], !asset.isFavorite);
|
||||
|
||||
/// Thumbnail image of a remote asset. Required asset.isRemote
|
||||
ImageProvider remoteThumbnailImageProvider(
|
||||
Asset asset,
|
||||
api.ThumbnailFormat type,
|
||||
) {
|
||||
return CachedNetworkImageProvider(
|
||||
getThumbnailUrl(
|
||||
asset,
|
||||
type: type,
|
||||
),
|
||||
cacheKey: getThumbnailCacheKey(
|
||||
asset,
|
||||
type: type,
|
||||
),
|
||||
headers: {"Authorization": authToken},
|
||||
);
|
||||
}
|
||||
|
||||
/// Original (large) image of a remote asset. Required asset.isRemote
|
||||
ImageProvider originalImageProvider(Asset asset) {
|
||||
return CachedNetworkImageProvider(
|
||||
getImageUrl(asset),
|
||||
cacheKey: getImageCacheKey(asset),
|
||||
headers: {"Authorization": authToken},
|
||||
);
|
||||
}
|
||||
|
||||
/// Thumbnail image of a local asset. Required asset.isLocal
|
||||
ImageProvider localThumbnailImageProvider(Asset asset) {
|
||||
return AssetEntityImageProvider(
|
||||
asset.local!,
|
||||
isOriginal: false,
|
||||
thumbnailSize: ThumbnailSize(
|
||||
MediaQuery.of(context).size.width.floor(),
|
||||
MediaQuery.of(context).size.height.floor(),
|
||||
),
|
||||
);
|
||||
}
|
||||
ImageProvider remoteOriginalProvider(Asset asset) =>
|
||||
CachedNetworkImageProvider(
|
||||
getImageUrl(asset),
|
||||
cacheKey: getImageCacheKey(asset),
|
||||
headers: header,
|
||||
);
|
||||
|
||||
/// Original (large) image of a local asset. Required asset.isLocal
|
||||
ImageProvider localImageProvider(Asset asset) {
|
||||
return AssetEntityImageProvider(
|
||||
isOriginal: true,
|
||||
asset.local!,
|
||||
);
|
||||
ImageProvider localOriginalProvider(Asset asset) =>
|
||||
OriginalImageProvider(asset);
|
||||
|
||||
ImageProvider finalImageProvider(Asset asset) {
|
||||
if (ImmichImage.useLocal(asset)) {
|
||||
return localOriginalProvider(asset);
|
||||
} else if (isLoadOriginal.value) {
|
||||
return remoteOriginalProvider(asset);
|
||||
} else if (isLoadPreview.value) {
|
||||
return ImmichImage.remoteThumbnailProvider(asset, jpeg, header);
|
||||
}
|
||||
return ImmichImage.remoteThumbnailProvider(asset, webp, header);
|
||||
}
|
||||
|
||||
Iterable<ImageProvider> allImageProviders(Asset asset) sync* {
|
||||
if (ImmichImage.useLocal(asset)) {
|
||||
yield ImmichImage.localThumbnailProvider(asset);
|
||||
yield localOriginalProvider(asset);
|
||||
} else {
|
||||
yield ImmichImage.remoteThumbnailProvider(asset, webp, header);
|
||||
if (isLoadPreview.value) {
|
||||
yield ImmichImage.remoteThumbnailProvider(asset, jpeg, header);
|
||||
}
|
||||
if (isLoadOriginal.value) {
|
||||
yield remoteOriginalProvider(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void precacheNextImage(int index) {
|
||||
void onError(Object exception, StackTrace? stackTrace) {
|
||||
// swallow error silently
|
||||
}
|
||||
if (index < totalAssets && index >= 0) {
|
||||
final asset = loadAsset(index);
|
||||
|
||||
if (!asset.isRemote ||
|
||||
asset.isLocal && !Store.get(StoreKey.preferRemoteImage, false)) {
|
||||
// Preload the local asset
|
||||
precacheImage(localImageProvider(asset), context);
|
||||
} else {
|
||||
onError(Object exception, StackTrace? stackTrace) {
|
||||
// swallow error silently
|
||||
}
|
||||
// Probably load WEBP either way
|
||||
precacheImage(
|
||||
remoteThumbnailImageProvider(
|
||||
asset,
|
||||
api.ThumbnailFormat.WEBP,
|
||||
),
|
||||
context,
|
||||
onError: onError,
|
||||
);
|
||||
if (isLoadPreview.value) {
|
||||
// Precache the JPEG thumbnail
|
||||
precacheImage(
|
||||
remoteThumbnailImageProvider(
|
||||
asset,
|
||||
api.ThumbnailFormat.JPEG,
|
||||
),
|
||||
context,
|
||||
onError: onError,
|
||||
);
|
||||
}
|
||||
if (isLoadOriginal.value) {
|
||||
// Preload the original asset
|
||||
precacheImage(
|
||||
originalImageProvider(asset),
|
||||
context,
|
||||
onError: onError,
|
||||
);
|
||||
}
|
||||
for (final imageProvider in allImageProviders(asset)) {
|
||||
precacheImage(imageProvider, context, onError: onError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -346,7 +308,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||
activeColor: Colors.white,
|
||||
inactiveColor: Colors.white.withOpacity(0.75),
|
||||
onChanged: (position) {
|
||||
progressValue.value = position;
|
||||
ref.read(videoPlayerControlsProvider.notifier).position = position;
|
||||
},
|
||||
),
|
||||
|
@ -485,27 +446,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||
}
|
||||
});
|
||||
|
||||
ImageProvider imageProvider(Asset asset) {
|
||||
if (!asset.isRemote ||
|
||||
asset.isLocal && !Store.get(StoreKey.preferRemoteImage, false)) {
|
||||
return localImageProvider(asset);
|
||||
} else {
|
||||
if (isLoadOriginal.value) {
|
||||
return originalImageProvider(asset);
|
||||
} else if (isLoadPreview.value) {
|
||||
return remoteThumbnailImageProvider(
|
||||
asset,
|
||||
api.ThumbnailFormat.JPEG,
|
||||
);
|
||||
} else {
|
||||
return remoteThumbnailImageProvider(
|
||||
asset,
|
||||
api.ThumbnailFormat.WEBP,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: WillPopScope(
|
||||
|
@ -531,79 +471,51 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||
itemCount: totalAssets,
|
||||
scrollDirection: Axis.horizontal,
|
||||
onPageChanged: (value) {
|
||||
// Precache image
|
||||
if (currentIndex.value < value) {
|
||||
// Moving forwards, so precache the next asset
|
||||
precacheNextImage(value + 1);
|
||||
} else {
|
||||
// Moving backwards, so precache previous asset
|
||||
precacheNextImage(value - 1);
|
||||
}
|
||||
final next = currentIndex.value < value ? value + 1 : value - 1;
|
||||
precacheNextImage(next);
|
||||
currentIndex.value = value;
|
||||
progressValue.value = 0.0;
|
||||
|
||||
HapticFeedback.selectionClick();
|
||||
},
|
||||
loadingBuilder: isLoadPreview.value
|
||||
? (context, event) {
|
||||
final a = asset();
|
||||
if (!a.isLocal ||
|
||||
(a.isRemote &&
|
||||
Store.get(StoreKey.preferRemoteImage, false))) {
|
||||
// Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to achieve
|
||||
// Three-Stage Loading (WEBP -> JPEG -> Original)
|
||||
final webPThumbnail = CachedNetworkImage(
|
||||
imageUrl: getThumbnailUrl(
|
||||
a,
|
||||
type: api.ThumbnailFormat.WEBP,
|
||||
),
|
||||
cacheKey: getThumbnailCacheKey(
|
||||
a,
|
||||
type: api.ThumbnailFormat.WEBP,
|
||||
),
|
||||
httpHeaders: {'Authorization': authToken},
|
||||
progressIndicatorBuilder: (_, __, ___) =>
|
||||
const Center(
|
||||
child: ImmichLoadingIndicator(),
|
||||
),
|
||||
fadeInDuration: const Duration(milliseconds: 0),
|
||||
fit: BoxFit.contain,
|
||||
errorWidget: (context, url, error) =>
|
||||
const Icon(Icons.image_not_supported_outlined),
|
||||
);
|
||||
loadingBuilder: (context, event, index) {
|
||||
final a = loadAsset(index);
|
||||
if (ImmichImage.useLocal(a)) {
|
||||
return Image(
|
||||
image: ImmichImage.localThumbnailProvider(a),
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
}
|
||||
// Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to achieve
|
||||
// Three-Stage Loading (WEBP -> JPEG -> Original)
|
||||
final webPThumbnail = CachedNetworkImage(
|
||||
imageUrl: getThumbnailUrl(a, type: webp),
|
||||
cacheKey: getThumbnailCacheKey(a, type: webp),
|
||||
httpHeaders: header,
|
||||
progressIndicatorBuilder: (_, __, ___) => const Center(
|
||||
child: ImmichLoadingIndicator(),
|
||||
),
|
||||
fadeInDuration: const Duration(milliseconds: 0),
|
||||
fit: BoxFit.contain,
|
||||
errorWidget: (context, url, error) =>
|
||||
const Icon(Icons.image_not_supported_outlined),
|
||||
);
|
||||
|
||||
if (isLoadOriginal.value) {
|
||||
// loading the preview in the loadingBuilder only
|
||||
// makes sense if the original is loaded in the builder
|
||||
return CachedNetworkImage(
|
||||
imageUrl: getThumbnailUrl(
|
||||
a,
|
||||
type: api.ThumbnailFormat.JPEG,
|
||||
),
|
||||
cacheKey: getThumbnailCacheKey(
|
||||
a,
|
||||
type: api.ThumbnailFormat.JPEG,
|
||||
),
|
||||
httpHeaders: {'Authorization': authToken},
|
||||
fit: BoxFit.contain,
|
||||
fadeInDuration: const Duration(milliseconds: 0),
|
||||
placeholder: (_, __) => webPThumbnail,
|
||||
errorWidget: (_, __, ___) => webPThumbnail,
|
||||
);
|
||||
} else {
|
||||
return webPThumbnail;
|
||||
}
|
||||
} else {
|
||||
return Image(
|
||||
image: localThumbnailImageProvider(a),
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
// loading the preview in the loadingBuilder only
|
||||
// makes sense if the original is loaded in the builder
|
||||
return isLoadPreview.value && isLoadOriginal.value
|
||||
? CachedNetworkImage(
|
||||
imageUrl: getThumbnailUrl(a, type: jpeg),
|
||||
cacheKey: getThumbnailCacheKey(a, type: jpeg),
|
||||
httpHeaders: header,
|
||||
fit: BoxFit.contain,
|
||||
fadeInDuration: const Duration(milliseconds: 0),
|
||||
placeholder: (_, __) => webPThumbnail,
|
||||
errorWidget: (_, __, ___) => webPThumbnail,
|
||||
)
|
||||
: webPThumbnail;
|
||||
},
|
||||
builder: (context, index) {
|
||||
final asset = loadAsset(index);
|
||||
final ImageProvider provider = imageProvider(asset);
|
||||
final ImageProvider provider = finalImageProvider(asset);
|
||||
|
||||
if (asset.isImage && !isPlayingMotionVideo.value) {
|
||||
return PhotoViewGalleryPageOptions(
|
||||
|
|
69
mobile/lib/shared/cache/custom_image_cache.dart
vendored
Normal file
69
mobile/lib/shared/cache/custom_image_cache.dart
vendored
Normal file
|
@ -0,0 +1,69 @@
|
|||
import 'package:flutter/painting.dart';
|
||||
|
||||
import 'original_image_provider.dart';
|
||||
|
||||
/// [ImageCache] that uses two caches for small and large images
|
||||
/// so that a single large image does not evict all small iamges
|
||||
final class CustomImageCache implements ImageCache {
|
||||
final _small = ImageCache();
|
||||
final _large = ImageCache();
|
||||
|
||||
@override
|
||||
int get maximumSize => _small.maximumSize + _large.maximumSize;
|
||||
|
||||
@override
|
||||
int get maximumSizeBytes => _small.maximumSizeBytes + _large.maximumSizeBytes;
|
||||
|
||||
@override
|
||||
set maximumSize(int value) => _small.maximumSize = value;
|
||||
|
||||
@override
|
||||
set maximumSizeBytes(int value) => _small.maximumSize = value;
|
||||
|
||||
@override
|
||||
void clear() {
|
||||
_small.clear();
|
||||
_large.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
void clearLiveImages() {
|
||||
_small.clearLiveImages();
|
||||
_large.clearLiveImages();
|
||||
}
|
||||
|
||||
@override
|
||||
bool containsKey(Object key) =>
|
||||
(key is OriginalImageProvider ? _large : _small).containsKey(key);
|
||||
|
||||
@override
|
||||
int get currentSize => _small.currentSize + _large.currentSize;
|
||||
|
||||
@override
|
||||
int get currentSizeBytes => _small.currentSizeBytes + _large.currentSizeBytes;
|
||||
|
||||
@override
|
||||
bool evict(Object key, {bool includeLive = true}) =>
|
||||
(key is OriginalImageProvider ? _large : _small)
|
||||
.evict(key, includeLive: includeLive);
|
||||
|
||||
@override
|
||||
int get liveImageCount => _small.liveImageCount + _large.liveImageCount;
|
||||
|
||||
@override
|
||||
int get pendingImageCount =>
|
||||
_small.pendingImageCount + _large.pendingImageCount;
|
||||
|
||||
@override
|
||||
ImageStreamCompleter? putIfAbsent(
|
||||
Object key,
|
||||
ImageStreamCompleter Function() loader, {
|
||||
ImageErrorListener? onError,
|
||||
}) =>
|
||||
(key is OriginalImageProvider ? _large : _small)
|
||||
.putIfAbsent(key, loader, onError: onError);
|
||||
|
||||
@override
|
||||
ImageCacheStatus statusForKey(Object key) =>
|
||||
(key is OriginalImageProvider ? _large : _small).statusForKey(key);
|
||||
}
|
73
mobile/lib/shared/cache/original_image_provider.dart
vendored
Normal file
73
mobile/lib/shared/cache/original_image_provider.dart
vendored
Normal file
|
@ -0,0 +1,73 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
|
||||
/// Loads the original image for local assets
|
||||
@immutable
|
||||
final class OriginalImageProvider extends ImageProvider<OriginalImageProvider> {
|
||||
final Asset asset;
|
||||
|
||||
const OriginalImageProvider(this.asset);
|
||||
|
||||
@override
|
||||
Future<OriginalImageProvider> obtainKey(ImageConfiguration configuration) =>
|
||||
SynchronousFuture<OriginalImageProvider>(this);
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(
|
||||
OriginalImageProvider key,
|
||||
ImageDecoderCallback decode,
|
||||
) =>
|
||||
MultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(key, decode),
|
||||
scale: 1.0,
|
||||
informationCollector: () sync* {
|
||||
yield ErrorDescription(asset.fileName);
|
||||
},
|
||||
);
|
||||
|
||||
Future<ui.Codec> _loadAsync(
|
||||
OriginalImageProvider key,
|
||||
ImageDecoderCallback decode,
|
||||
) async {
|
||||
final ui.ImmutableBuffer buffer;
|
||||
if (asset.isImage) {
|
||||
final File? file = await asset.local?.originFile;
|
||||
if (file == null) {
|
||||
throw StateError("Opening file for asset ${asset.fileName} failed");
|
||||
}
|
||||
try {
|
||||
buffer = await ui.ImmutableBuffer.fromFilePath(file.path);
|
||||
} catch (error) {
|
||||
throw StateError("Loading asset ${asset.fileName} failed");
|
||||
}
|
||||
} else {
|
||||
final thumbBytes = await asset.local?.thumbnailData;
|
||||
if (thumbBytes == null) {
|
||||
throw StateError("Loading thumb for video ${asset.fileName} failed");
|
||||
}
|
||||
buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes);
|
||||
}
|
||||
try {
|
||||
final codec = await decode(buffer);
|
||||
debugPrint("Decoded image ${asset.fileName}");
|
||||
return codec;
|
||||
} catch (error) {
|
||||
throw StateError("Decoding asset ${asset.fileName} failed");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other is! OriginalImageProvider) return false;
|
||||
if (identical(this, other)) return true;
|
||||
return asset == other.asset;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => asset.hashCode;
|
||||
}
|
8
mobile/lib/shared/cache/widgets_binding.dart
vendored
Normal file
8
mobile/lib/shared/cache/widgets_binding.dart
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'custom_image_cache.dart';
|
||||
|
||||
final class ImmichWidgetsBinding extends WidgetsFlutterBinding {
|
||||
@override
|
||||
ImageCache createImageCache() => CustomImageCache();
|
||||
}
|
|
@ -178,6 +178,7 @@ class Asset {
|
|||
@override
|
||||
bool operator ==(other) {
|
||||
if (other is! Asset) return false;
|
||||
if (identical(this, other)) return true;
|
||||
return id == other.id &&
|
||||
checksum == other.checksum &&
|
||||
remoteId == other.remoteId &&
|
||||
|
|
|
@ -45,14 +45,9 @@ class ImmichImage extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
final Asset asset = this.asset!;
|
||||
if (!asset.isRemote ||
|
||||
(asset.isLocal && !Store.get(StoreKey.preferRemoteImage, false))) {
|
||||
if (useLocal(asset)) {
|
||||
return Image(
|
||||
image: AssetEntityImageProvider(
|
||||
asset.local!,
|
||||
isOriginal: false,
|
||||
thumbnailSize: const ThumbnailSize.square(250), // like server thumbs
|
||||
),
|
||||
image: localThumbnailProvider(asset),
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
|
@ -148,45 +143,44 @@ class ImmichImage extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
static AssetEntityImageProvider localThumbnailProvider(Asset asset) =>
|
||||
AssetEntityImageProvider(
|
||||
asset.local!,
|
||||
isOriginal: false,
|
||||
thumbnailSize: const ThumbnailSize.square(250),
|
||||
);
|
||||
|
||||
static CachedNetworkImageProvider remoteThumbnailProvider(
|
||||
Asset asset,
|
||||
api.ThumbnailFormat type,
|
||||
Map<String, String> authHeader,
|
||||
) =>
|
||||
CachedNetworkImageProvider(
|
||||
getThumbnailUrl(asset, type: type),
|
||||
cacheKey: getThumbnailCacheKey(asset, type: type),
|
||||
headers: authHeader,
|
||||
);
|
||||
|
||||
/// Precaches this asset for instant load the next time it is shown
|
||||
static Future<void> precacheAsset(
|
||||
Asset asset,
|
||||
BuildContext context, {
|
||||
type = api.ThumbnailFormat.WEBP,
|
||||
}) {
|
||||
final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}';
|
||||
|
||||
if (type == api.ThumbnailFormat.WEBP) {
|
||||
final thumbnailUrl = getThumbnailUrl(asset);
|
||||
final thumbnailCacheKey = getThumbnailCacheKey(asset);
|
||||
final thumbnailProvider = CachedNetworkImageProvider(
|
||||
thumbnailUrl,
|
||||
cacheKey: thumbnailCacheKey,
|
||||
headers: {"Authorization": authToken},
|
||||
);
|
||||
return precacheImage(thumbnailProvider, context);
|
||||
}
|
||||
// Precache the local image
|
||||
if (!asset.isRemote &&
|
||||
(asset.isLocal || !Store.get(StoreKey.preferRemoteImage, false))) {
|
||||
final provider = AssetEntityImageProvider(
|
||||
asset.local!,
|
||||
isOriginal: false,
|
||||
thumbnailSize: const ThumbnailSize.square(250), // like server thumbs
|
||||
);
|
||||
return precacheImage(provider, context);
|
||||
if (useLocal(asset)) {
|
||||
// Precache the local image
|
||||
return precacheImage(localThumbnailProvider(asset), context);
|
||||
} else {
|
||||
final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}';
|
||||
// Precache the remote image since we are not using local images
|
||||
final url = getThumbnailUrl(asset, type: api.ThumbnailFormat.JPEG);
|
||||
final cacheKey =
|
||||
getThumbnailCacheKey(asset, type: api.ThumbnailFormat.JPEG);
|
||||
final provider = CachedNetworkImageProvider(
|
||||
url,
|
||||
cacheKey: cacheKey,
|
||||
headers: {"Authorization": authToken},
|
||||
return precacheImage(
|
||||
remoteThumbnailProvider(asset, type, {"Authorization": authToken}),
|
||||
context,
|
||||
);
|
||||
|
||||
return precacheImage(provider, context);
|
||||
}
|
||||
}
|
||||
|
||||
static bool useLocal(Asset asset) =>
|
||||
!asset.isRemote ||
|
||||
asset.isLocal && !Store.get(StoreKey.preferRemoteImage, false);
|
||||
}
|
||||
|
|
|
@ -235,6 +235,7 @@ class PhotoView extends StatefulWidget {
|
|||
const PhotoView({
|
||||
Key? key,
|
||||
required this.imageProvider,
|
||||
required this.index,
|
||||
this.loadingBuilder,
|
||||
this.backgroundDecoration,
|
||||
this.wantKeepAlive = false,
|
||||
|
@ -304,6 +305,7 @@ class PhotoView extends StatefulWidget {
|
|||
imageProvider = null,
|
||||
gaplessPlayback = false,
|
||||
loadingBuilder = null,
|
||||
index = 0,
|
||||
super(key: key);
|
||||
|
||||
/// Given a [imageProvider] it resolves into an zoomable image widget using. It
|
||||
|
@ -419,6 +421,8 @@ class PhotoView extends StatefulWidget {
|
|||
/// Useful when you want to drag a widget without restrictions.
|
||||
final bool? enablePanAlways;
|
||||
|
||||
final int index;
|
||||
|
||||
bool get _isCustomChild {
|
||||
return child != null;
|
||||
}
|
||||
|
@ -571,6 +575,7 @@ class _PhotoViewState extends State<PhotoView>
|
|||
disableGestures: widget.disableGestures,
|
||||
errorBuilder: widget.errorBuilder,
|
||||
enablePanAlways: widget.enablePanAlways,
|
||||
index: widget.index,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -625,7 +630,7 @@ typedef PhotoViewImageDragStartCallback = Function(
|
|||
PhotoViewControllerValue controllerValue,
|
||||
);
|
||||
|
||||
/// A type definition for a callback when the user drags
|
||||
/// A type definition for a callback when the user drags
|
||||
typedef PhotoViewImageDragUpdateCallback = Function(
|
||||
BuildContext context,
|
||||
DragUpdateDetails details,
|
||||
|
@ -650,4 +655,5 @@ typedef PhotoViewImageScaleEndCallback = Function(
|
|||
typedef LoadingBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
ImageChunkEvent? event,
|
||||
int index,
|
||||
);
|
||||
|
|
|
@ -281,6 +281,7 @@ class _PhotoViewGalleryState extends State<PhotoViewGallery> {
|
|||
)
|
||||
: PhotoView(
|
||||
key: ObjectKey(index),
|
||||
index: index,
|
||||
imageProvider: pageOption.imageProvider,
|
||||
loadingBuilder: widget.loadingBuilder,
|
||||
backgroundDecoration: widget.backgroundDecoration,
|
||||
|
@ -315,7 +316,10 @@ class _PhotoViewGalleryState extends State<PhotoViewGallery> {
|
|||
);
|
||||
}
|
||||
|
||||
PhotoViewGalleryPageOptions _buildPageOption(BuildContext context, int index) {
|
||||
PhotoViewGalleryPageOptions _buildPageOption(
|
||||
BuildContext context,
|
||||
int index,
|
||||
) {
|
||||
if (widget._isBuilder) {
|
||||
return widget.builder!(context, index);
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ class ImageWrapper extends StatefulWidget {
|
|||
required this.disableGestures,
|
||||
required this.errorBuilder,
|
||||
required this.enablePanAlways,
|
||||
required this.index,
|
||||
}) : super(key: key);
|
||||
|
||||
final ImageProvider imageProvider;
|
||||
|
@ -64,6 +65,7 @@ class ImageWrapper extends StatefulWidget {
|
|||
final FilterQuality? filterQuality;
|
||||
final bool? disableGestures;
|
||||
final bool? enablePanAlways;
|
||||
final int index;
|
||||
|
||||
@override
|
||||
createState() => _ImageWrapperState();
|
||||
|
@ -128,6 +130,7 @@ class _ImageWrapperState extends State<ImageWrapper> {
|
|||
_lastException = null;
|
||||
_lastStack = null;
|
||||
}
|
||||
|
||||
synchronousCall ? setupCB() : setState(setupCB);
|
||||
}
|
||||
|
||||
|
@ -212,7 +215,7 @@ class _ImageWrapperState extends State<ImageWrapper> {
|
|||
|
||||
Widget _buildLoading(BuildContext context) {
|
||||
if (widget.loadingBuilder != null) {
|
||||
return widget.loadingBuilder!(context, _loadingProgress);
|
||||
return widget.loadingBuilder!(context, _loadingProgress, widget.index);
|
||||
}
|
||||
|
||||
return PhotoViewDefaultLoading(
|
||||
|
|
Loading…
Reference in a new issue