mirror of https://github.com/immich-app/immich.git
feat(mobile): image caching & viewer improvements (#4095)
parent
63b6a71ebd
commit
1c02e1dadf
@ -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);
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'custom_image_cache.dart';
|
||||||
|
|
||||||
|
final class ImmichWidgetsBinding extends WidgetsFlutterBinding {
|
||||||
|
@override
|
||||||
|
ImageCache createImageCache() => CustomImageCache();
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue