Compare commits

...

27 commits

Author SHA1 Message Date
shalong-tanwen
fcfeb6dce1 chore: pull main 2023-12-04 00:00:39 +05:30
shalong-tanwen
2bc16ffe71 remove syncDuration 2023-11-07 02:19:15 +05:30
shalong-tanwen
bb81c5a99a chore: pull main 2023-11-07 01:58:35 +05:30
shalong-tanwen
785fc2af90 mobile: use grey placeholders on thumb scroll 2023-10-24 20:56:51 +05:30
Alex Tran
f14880ba53 Merge branch 'main' of github.com:immich-app/immich into feat/mobile-thumbhash-scroll 2023-10-23 20:39:28 -05:00
shalong-tanwen
9e63554889 mobile: wrap thumbnail image with renderboundary 2023-10-23 13:55:10 +05:30
shalong-tanwen
ce2ecbea40 mobile: show thumbhash on scroll 2023-10-23 13:28:42 +05:30
shalong-tanwen
c08d9a7001 chore: pull main 2023-10-23 09:53:05 +05:30
covalent
b76b18fd2f merged thumbhash_mobile with upstream 2023-08-04 19:56:42 -04:00
Alex Tran
3084c292d2 Merge branch 'main' of github.com:immich-app/immich into pr/lukehmcc/2913 2023-07-16 21:56:21 -05:00
Alex Tran
76c0d53b1d Merge branch 'main' of github.com:immich-app/immich into pr/lukehmcc/2913 2023-07-08 09:58:05 -05:00
covalent
c9a0e73d87 add missing trailing comma 2023-07-01 13:28:06 -04:00
covalent
67c7a5a2ff add back proper error handling 2023-07-01 13:24:02 -04:00
covalent
031e3592ee remove unnecesary import 2023-06-30 10:00:59 -04:00
covalent
ceceedc138 change fade in to ImageFade library 2023-06-30 09:55:16 -04:00
Alex Tran
2308a7fe04 Merge branch 'main' of github.com:immich-app/immich into pr/lukehmcc/2913 2023-06-28 22:36:22 -05:00
Alex Tran
f60d5e3bef Merge branch 'main' of github.com:immich-app/immich into pr/lukehmcc/2913 2023-06-27 20:30:44 -05:00
Luke McCarthy
d9a66663b8 remove needless imports 2023-06-27 09:43:11 -04:00
Luke McCarthy
38b945111c Merge branch 'main' of https://github.com/immich-app/immich into thumbhash_mobile 2023-06-27 09:33:44 -04:00
Luke McCarthy
8f8b083a56 change thumbhash to List<byte> from String 2023-06-27 09:26:53 -04:00
Luke McCarthy
115fd62878 update canUpdate to take into account thumbhash 2023-06-24 12:36:36 -04:00
Luke McCarthy
49fae9c4cc add migration and update gitignore to ignore isar 2023-06-23 15:34:34 -04:00
Luke McCarthy
14df94fbb8 update migration and remove isar files 2023-06-23 09:11:54 -04:00
Luke McCarthy
c35de5ad74 fix tests 2023-06-22 11:02:44 -04:00
Luke McCarthy
d1011f96ad update generated files 2023-06-22 10:55:25 -04:00
Luke McCarthy
4fab2bcf63 Merge branch 'main' of https://github.com/immich-app/immich into thumbhash_mobile 2023-06-22 10:51:54 -04:00
Luke McCarthy
000c2bf43f add thumbhash support to mobile app 2023-06-21 22:49:19 -04:00
9 changed files with 291 additions and 66 deletions

2
mobile/.gitignore vendored
View file

@ -54,3 +54,5 @@ ios/fastlane/report.xml
default.isar
default.isar.lock
libisar.so
default.isar-lck
isar.dll

View file

@ -126,25 +126,27 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
}
Widget _buildThumbnailOrPlaceholder(Asset asset, int index) {
return ThumbnailImage(
asset: asset,
index: index,
loadAsset: widget.renderList.loadAsset,
totalAssets: widget.renderList.totalAssets,
multiselectEnabled: widget.selectionActive,
isSelected: widget.selectionActive && _selectedAssets.contains(asset),
onSelect: () => _selectAssets([asset]),
onDeselect: widget.canDeselect ||
widget.preselectedAssets == null ||
!widget.preselectedAssets!.contains(asset)
? () => _deselectAssets([asset])
: null,
useGrayBoxPlaceholder: true,
showStorageIndicator: widget.showStorageIndicator,
heroOffset: widget.heroOffset,
showStack: widget.showStack,
isOwner: widget.isOwner,
sharedAlbumId: widget.sharedAlbumId,
return RepaintBoundary(
child: ThumbnailImage(
asset: asset,
index: index,
loadAsset: widget.renderList.loadAsset,
totalAssets: widget.renderList.totalAssets,
multiselectEnabled: widget.selectionActive,
isSelected: widget.selectionActive && _selectedAssets.contains(asset),
onSelect: () => _selectAssets([asset]),
onDeselect: widget.canDeselect ||
widget.preselectedAssets == null ||
!widget.preselectedAssets!.contains(asset)
? () => _deselectAssets([asset])
: null,
useGrayBoxPlaceholder: true,
showStorageIndicator: widget.showStorageIndicator,
heroOffset: widget.heroOffset,
showStack: widget.showStack,
isOwner: widget.isOwner,
sharedAlbumId: widget.sharedAlbumId,
),
);
}

View file

@ -31,6 +31,8 @@ class Asset {
remote.exifInfo != null ? ExifInfo.fromDto(remote.exifInfo!) : null,
isFavorite = remote.isFavorite,
isArchived = remote.isArchived,
thumbhash =
remote.thumbhash != null ? base64.decode(remote.thumbhash!) : null,
isTrashed = remote.isTrashed,
stackParentId = remote.stackParentId,
stackCount = remote.stackCount;
@ -79,6 +81,7 @@ class Asset {
this.exifInfo,
required this.isFavorite,
required this.isArchived,
required this.thumbhash,
required this.isTrashed,
this.stackParentId,
required this.stackCount,
@ -110,6 +113,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;
@ -197,6 +202,7 @@ class Asset {
if (identical(this, other)) return true;
return id == other.id &&
checksum == other.checksum &&
thumbhash == other.thumbhash &&
remoteId == other.remoteId &&
localId == other.localId &&
ownerId == other.ownerId &&
@ -222,6 +228,7 @@ class Asset {
int get hashCode =>
id.hashCode ^
checksum.hashCode ^
thumbhash.hashCode ^
remoteId.hashCode ^
localId.hashCode ^
ownerId.hashCode ^
@ -251,6 +258,7 @@ class Asset {
a.isLocal && !isLocal ||
width == null && a.width != null ||
height == null && a.height != null ||
thumbhash == null && a.thumbhash != null ||
livePhotoVideoId == null && a.livePhotoVideoId != null ||
stackParentId == null && a.stackParentId != null ||
isFavorite != a.isFavorite ||
@ -295,6 +303,7 @@ class Asset {
isFavorite: isFavorite,
isArchived: isArchived,
isTrashed: isTrashed,
thumbhash: thumbhash,
);
}
} else {
@ -312,6 +321,7 @@ class Asset {
isFavorite: a.isFavorite,
isArchived: a.isArchived,
isTrashed: a.isTrashed,
thumbhash: a.thumbhash,
exifInfo: a.exifInfo?.copyWith(id: id) ?? exifInfo,
);
} else {
@ -329,6 +339,7 @@ class Asset {
Asset _copyWith({
Id? id,
String? checksum,
List<byte>? thumbhash,
String? remoteId,
String? localId,
int? ownerId,
@ -351,6 +362,7 @@ class Asset {
Asset(
id: id ?? this.id,
checksum: checksum ?? this.checksum,
thumbhash: thumbhash ?? this.thumbhash,
remoteId: remoteId ?? this.remoteId,
localId: localId ?? this.localId,
ownerId: ownerId ?? this.ownerId,
@ -406,6 +418,7 @@ class Asset {
{
"id": ${id == Isar.autoIncrement ? '"N/A"' : id},
"remoteId": "${remoteId ?? "N/A"}",
"thumbhash": "$thumbhash",
"localId": "${localId ?? "N/A"}",
"checksum": "$checksum",
"ownerId": $ownerId,

View file

@ -92,19 +92,24 @@ const AssetSchema = CollectionSchema(
name: r'stackParentId',
type: IsarType.string,
),
r'type': PropertySchema(
r'thumbhash': PropertySchema(
id: 15,
name: r'thumbhash',
type: IsarType.byteList,
),
r'type': PropertySchema(
id: 16,
name: r'type',
type: IsarType.byte,
enumMap: _AssettypeEnumValueMap,
),
r'updatedAt': PropertySchema(
id: 16,
id: 17,
name: r'updatedAt',
type: IsarType.dateTime,
),
r'width': PropertySchema(
id: 17,
id: 18,
name: r'width',
type: IsarType.int,
)
@ -200,6 +205,12 @@ int _assetEstimateSize(
bytesCount += 3 + value.length * 3;
}
}
{
final value = object.thumbhash;
if (value != null) {
bytesCount += 3 + value.length;
}
}
return bytesCount;
}
@ -224,9 +235,10 @@ void _assetSerialize(
writer.writeString(offsets[12], object.remoteId);
writer.writeLong(offsets[13], object.stackCount);
writer.writeString(offsets[14], object.stackParentId);
writer.writeByte(offsets[15], object.type.index);
writer.writeDateTime(offsets[16], object.updatedAt);
writer.writeInt(offsets[17], object.width);
writer.writeByteList(offsets[15], object.thumbhash);
writer.writeByte(offsets[16], object.type.index);
writer.writeDateTime(offsets[17], object.updatedAt);
writer.writeInt(offsets[18], object.width);
}
Asset _assetDeserialize(
@ -252,10 +264,11 @@ Asset _assetDeserialize(
remoteId: reader.readStringOrNull(offsets[12]),
stackCount: reader.readLongOrNull(offsets[13]),
stackParentId: reader.readStringOrNull(offsets[14]),
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[15])] ??
thumbhash: reader.readByteList(offsets[15]),
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[16])] ??
AssetType.other,
updatedAt: reader.readDateTime(offsets[16]),
width: reader.readIntOrNull(offsets[17]),
updatedAt: reader.readDateTime(offsets[17]),
width: reader.readIntOrNull(offsets[18]),
);
return object;
}
@ -298,11 +311,13 @@ P _assetDeserializeProp<P>(
case 14:
return (reader.readStringOrNull(offset)) as P;
case 15:
return (reader.readByteList(offset)) as P;
case 16:
return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ??
AssetType.other) as P;
case 16:
return (reader.readDateTime(offset)) as P;
case 17:
return (reader.readDateTime(offset)) as P;
case 18:
return (reader.readIntOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
@ -2040,6 +2055,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) {
@ -2766,6 +2934,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');
@ -2882,6 +3056,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');

View file

@ -2,6 +2,8 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_thumbhash/flutter_thumbhash.dart';
import 'package:image_fade/image_fade.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.dart';
@ -94,47 +96,45 @@ class ImmichImage extends StatelessWidget {
}
final String? token = Store.get(StoreKey.accessToken);
final String thumbnailRequestUrl = getThumbnailUrl(asset, type: type);
return CachedNetworkImage(
imageUrl: thumbnailRequestUrl,
httpHeaders: {"Authorization": "Bearer $token"},
cacheKey: getThumbnailCacheKey(asset, type: type),
Widget placeholderWidget;
if (asset.thumbhash != null) {
placeholderWidget = FittedBox(
fit: BoxFit.fill,
child: Image(
image: ThumbHash.fromIntList(asset.thumbhash!).toImage(),
),
);
} else if (useGrayBoxPlaceholder) {
placeholderWidget = const DecoratedBox(
decoration: BoxDecoration(color: Colors.grey),
);
} else {
placeholderWidget = Transform.scale(
scale: 0.2,
child: const CircularProgressIndicator(),
);
}
return ImageFade(
width: width,
height: height,
// keeping memCacheWidth, memCacheHeight, maxWidthDiskCache and
// maxHeightDiskCache = null allows to simply store the webp thumbnail
// from the server and use it for all rendered thumbnail sizes
image: CachedNetworkImageProvider(
thumbnailRequestUrl,
headers: {"Authorization": "Bearer $token"},
cacheKey: getThumbnailCacheKey(asset, type: type),
),
fit: fit,
fadeInDuration: const Duration(milliseconds: 250),
progressIndicatorBuilder: (context, url, downloadProgress) {
// Show loading if desired
return Stack(
children: [
if (useGrayBoxPlaceholder)
const SizedBox.square(
dimension: 250,
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.grey),
),
),
if (useProgressIndicator)
Transform.scale(
scale: 2,
child: Center(
child: CircularProgressIndicator.adaptive(
strokeWidth: 1,
value: downloadProgress.progress,
),
),
),
],
);
},
errorWidget: (context, url, error) {
duration: const Duration(milliseconds: 300),
syncDuration: const Duration(milliseconds: 0),
placeholder: placeholderWidget,
errorBuilder: (context, error) {
if (error is HttpExceptionWithStatus &&
error.statusCode >= 400 &&
error.statusCode < 500) {
debugPrint("Evicting thumbnail '$url' from cache: $error");
CachedNetworkImage.evictFromCache(url);
debugPrint(
"Evicting thumbnail '$thumbnailRequestUrl' from cache: $error",
);
CachedNetworkImage.evictFromCache(thumbnailRequestUrl);
}
return Icon(
Icons.image_not_supported_outlined,

View file

@ -597,6 +597,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_thumbhash:
dependency: "direct main"
description:
name: flutter_thumbhash
sha256: "0a21f7fc4ffefe79f93b047780ad7d5af42a0f0a49b9ec5fb94005e6deebe4ad"
url: "https://pub.dev"
source: hosted
version: "0.1.0+1"
flutter_udid:
dependency: "direct main"
description:
@ -767,6 +775,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.3.0"
image_fade:
dependency: "direct main"
description:
name: image_fade
sha256: "7296c9c53cd5de98e675ef1e27bdaa4035d6c3a45cf5b86094b2e545689b4ea6"
url: "https://pub.dev"
source: hosted
version: "0.6.2"
image_picker:
dependency: "direct main"
description:
@ -1504,6 +1520,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.0"
thumbhash:
dependency: transitive
description:
name: thumbhash
sha256: "5f6d31c5279ca0b5caa81ec10aae8dcaab098d82cb699ea66ada4ed09c794a37"
url: "https://pub.dev"
source: hosted
version: "0.1.0+1"
time:
dependency: transitive
description:

View file

@ -58,6 +58,8 @@ dependencies:
wakelock_plus: ^1.1.1
flutter_local_notifications: ^15.1.0+1
timezone: ^0.9.2
flutter_thumbhash: 0.1.0+1
image_fade: 0.6.2
openapi:
path: openapi

View file

@ -26,6 +26,7 @@ void main() {
isArchived: false,
isTrashed: false,
stackCount: 0,
thumbhash: null,
),
);
}

View file

@ -36,6 +36,7 @@ void main() {
isArchived: false,
isTrashed: false,
stackCount: 0,
thumbhash: null,
);
}