1
0
forked from Cutlery/immich

Merge branch 'refactor/immich-image-provider' of github.com:immich-app/immich into refactor/immich-image-provider

This commit is contained in:
Marty Fuhry
2024-02-12 09:08:19 -05:00
5 changed files with 318 additions and 13 deletions
+16 -1
View File
@@ -35,7 +35,8 @@ class Asset {
isReadOnly = remote.isReadOnly,
isOffline = remote.isOffline,
stackParentId = remote.stackParentId,
stackCount = remote.stackCount;
stackCount = remote.stackCount,
thumbhash = _decodeThumbhash(remote.thumbhash);
Asset.local(AssetEntity local, List<int> hash)
: localId = local.id,
@@ -88,6 +89,7 @@ class Asset {
this.stackCount = 0,
this.isReadOnly = false,
this.isOffline = false,
this.thumbhash,
});
@ignore
@@ -116,6 +118,8 @@ class Asset {
/// because Isar cannot sort lists of byte arrays
String checksum;
List<byte>? thumbhash;
@Index(unique: false, replace: false, type: IndexType.hash)
String? remoteId;
@@ -271,6 +275,7 @@ class Asset {
a.exifInfo?.latitude != exifInfo?.latitude ||
a.exifInfo?.longitude != exifInfo?.longitude ||
// no local stack count or different count from remote
a.thumbhash != thumbhash ||
((stackCount == null && a.stackCount != null) ||
(stackCount != null &&
a.stackCount != null &&
@@ -332,6 +337,7 @@ class Asset {
isReadOnly: a.isReadOnly,
isOffline: a.isOffline,
exifInfo: a.exifInfo?.copyWith(id: id) ?? exifInfo,
thumbhash: a.thumbhash,
);
} else {
// add only missing values (and set isLocal to true)
@@ -368,6 +374,7 @@ class Asset {
ExifInfo? exifInfo,
String? stackParentId,
int? stackCount,
List<byte>? thumbhash,
}) =>
Asset(
id: id ?? this.id,
@@ -392,6 +399,7 @@ class Asset {
exifInfo: exifInfo ?? this.exifInfo,
stackParentId: stackParentId ?? this.stackParentId,
stackCount: stackCount ?? this.stackCount,
thumbhash: thumbhash ?? this.thumbhash,
);
Future<void> put(Isar db) async {
@@ -504,3 +512,10 @@ extension AssetsHelper on IsarCollection<Asset> {
return where().anyOf(ids, (q, String e) => q.localIdEqualTo(e));
}
}
List<byte>? _decodeThumbhash(String? hash) {
if (hash == null) {
return null;
}
return base64.decode(base64.normalize(hash)).toList();
}
+191 -11
View File
@@ -102,19 +102,24 @@ const AssetSchema = CollectionSchema(
name: r'stackParentId',
type: IsarType.string,
),
r'type': PropertySchema(
r'thumbhash': PropertySchema(
id: 17,
name: r'thumbhash',
type: IsarType.byteList,
),
r'type': PropertySchema(
id: 18,
name: r'type',
type: IsarType.byte,
enumMap: _AssettypeEnumValueMap,
),
r'updatedAt': PropertySchema(
id: 18,
id: 19,
name: r'updatedAt',
type: IsarType.dateTime,
),
r'width': PropertySchema(
id: 19,
id: 20,
name: r'width',
type: IsarType.int,
)
@@ -210,6 +215,12 @@ int _assetEstimateSize(
bytesCount += 3 + value.length * 3;
}
}
{
final value = object.thumbhash;
if (value != null) {
bytesCount += 3 + value.length;
}
}
return bytesCount;
}
@@ -236,9 +247,10 @@ void _assetSerialize(
writer.writeString(offsets[14], object.remoteId);
writer.writeLong(offsets[15], object.stackCount);
writer.writeString(offsets[16], object.stackParentId);
writer.writeByte(offsets[17], object.type.index);
writer.writeDateTime(offsets[18], object.updatedAt);
writer.writeInt(offsets[19], object.width);
writer.writeByteList(offsets[17], object.thumbhash);
writer.writeByte(offsets[18], object.type.index);
writer.writeDateTime(offsets[19], object.updatedAt);
writer.writeInt(offsets[20], object.width);
}
Asset _assetDeserialize(
@@ -266,10 +278,11 @@ Asset _assetDeserialize(
remoteId: reader.readStringOrNull(offsets[14]),
stackCount: reader.readLongOrNull(offsets[15]),
stackParentId: reader.readStringOrNull(offsets[16]),
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[17])] ??
thumbhash: reader.readByteList(offsets[17]),
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[18])] ??
AssetType.other,
updatedAt: reader.readDateTime(offsets[18]),
width: reader.readIntOrNull(offsets[19]),
updatedAt: reader.readDateTime(offsets[19]),
width: reader.readIntOrNull(offsets[20]),
);
return object;
}
@@ -316,11 +329,13 @@ P _assetDeserializeProp<P>(
case 16:
return (reader.readStringOrNull(offset)) as P;
case 17:
return (reader.readByteList(offset)) as P;
case 18:
return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ??
AssetType.other) as P;
case 18:
return (reader.readDateTime(offset)) as P;
case 19:
return (reader.readDateTime(offset)) as P;
case 20:
return (reader.readIntOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
@@ -2078,6 +2093,159 @@ extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> thumbhashIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'thumbhash',
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> thumbhashIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'thumbhash',
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> thumbhashElementEqualTo(
int value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'thumbhash',
value: value,
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> thumbhashElementGreaterThan(
int value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'thumbhash',
value: value,
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> thumbhashElementLessThan(
int value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'thumbhash',
value: value,
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> thumbhashElementBetween(
int lower,
int upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'thumbhash',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> thumbhashLengthEqualTo(
int length) {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'thumbhash',
length,
true,
length,
true,
);
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> thumbhashIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'thumbhash',
0,
true,
0,
true,
);
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> thumbhashIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'thumbhash',
0,
false,
999999,
true,
);
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> thumbhashLengthLessThan(
int length, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'thumbhash',
0,
true,
length,
include,
);
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> thumbhashLengthGreaterThan(
int length, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'thumbhash',
length,
include,
999999,
true,
);
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> thumbhashLengthBetween(
int lower,
int upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'thumbhash',
lower,
includeLower,
upper,
includeUpper,
);
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> typeEqualTo(
AssetType value) {
return QueryBuilder.apply(this, (query) {
@@ -2864,6 +3032,12 @@ extension AssetQueryWhereDistinct on QueryBuilder<Asset, Asset, QDistinct> {
});
}
QueryBuilder<Asset, Asset, QDistinct> distinctByThumbhash() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'thumbhash');
});
}
QueryBuilder<Asset, Asset, QDistinct> distinctByType() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'type');
@@ -2992,6 +3166,12 @@ extension AssetQueryProperty on QueryBuilder<Asset, Asset, QQueryProperty> {
});
}
QueryBuilder<Asset, List<int>?, QQueryOperations> thumbhashProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'thumbhash');
});
}
QueryBuilder<Asset, AssetType, QQueryOperations> typeProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'type');
+102 -1
View File
@@ -7,9 +7,10 @@ import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:photo_manager_image_provider/photo_manager_image_provider.dart';
import 'package:thumbhash/thumbhash.dart' as thumbhash;
/// Renders an Asset using local data if available, else remote data
class ImmichImage extends StatelessWidget {
class ImmichImage extends StatefulWidget {
const ImmichImage(
this.asset, {
this.width,
@@ -162,3 +163,103 @@ class ImmichImage extends StatelessWidget {
}
}
}
class _ImmichImageState extends State<ImmichImage> {
// Creating the Uint8List from the List<bytes> during each build results in flickers during
// the fade transition. Calculate the hash in the initState and cache it for further builds
Uint8List? thumbHashBytes;
static const _placeholderDimension = 300.0;
@override
void initState() {
super.initState();
if (widget.asset?.thumbhash != null) {
final bytes = Uint8List.fromList(widget.asset!.thumbhash!);
final rgbaImage = thumbhash.thumbHashToRGBA(bytes);
thumbHashBytes = thumbhash.rgbaToBmp(rgbaImage);
}
}
@override
Widget build(BuildContext context) {
if (widget.asset == null) {
return Container(
decoration: const BoxDecoration(
color: Colors.grey,
),
child: SizedBox(
width: widget.width,
height: widget.height,
child: const Center(
child: Icon(Icons.no_photography),
),
),
);
}
final Asset asset = widget.asset!;
return Image(
image: ImmichImage.imageProvider(
asset: asset,
isThumbnail: widget.isThumbnail,
),
width: widget.width,
height: widget.height,
fit: widget.fit,
loadingBuilder: (_, child, loadingProgress) {
return AnimatedOpacity(
opacity: loadingProgress != null ? 0 : 1,
duration: const Duration(seconds: 1),
curve: Curves.easeOut,
child: child,
);
},
frameBuilder: (_, child, frame, wasSynchronouslyLoaded) {
if (wasSynchronouslyLoaded || frame != null) {
return child;
}
return Stack(
alignment: Alignment.center,
children: [
if (widget.useGrayBoxPlaceholder)
const SizedBox.square(
dimension: _placeholderDimension,
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.grey),
),
),
if (thumbHashBytes != null)
Image.memory(
thumbHashBytes!,
width: _placeholderDimension,
height: _placeholderDimension,
fit: BoxFit.cover,
),
if (widget.useProgressIndicator)
const Center(
child: CircularProgressIndicator(),
),
],
);
},
errorBuilder: (context, error, stackTrace) {
if (error is PlatformException &&
error.code == "The asset not found!") {
debugPrint(
"Asset ${asset.localId} does not exist anymore on device!",
);
} else {
debugPrint(
"Error getting thumb for assetId=${asset.localId}: $error",
);
}
return Icon(
Icons.image_not_supported_outlined,
color: context.primaryColor,
);
},
);
}
}
+8
View File
@@ -1467,6 +1467,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.1"
thumbhash:
dependency: "direct main"
description:
name: thumbhash
sha256: "5f6d31c5279ca0b5caa81ec10aae8dcaab098d82cb699ea66ada4ed09c794a37"
url: "https://pub.dev"
source: hosted
version: "0.1.0+1"
time:
dependency: transitive
description:
+1
View File
@@ -56,6 +56,7 @@ dependencies:
wakelock_plus: ^1.1.4
flutter_local_notifications: ^16.3.2
timezone: ^0.9.2
thumbhash: 0.1.0+1
openapi:
path: openapi