1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-21 01:39:59 +02:00
immich/mobile/lib/shared/models/album.dart
Fynn Petersen-Frey 73075c64d1
feature(mobile): hash assets & sync via checksum (#2592)
* compare different sha1 implementations

* remove openssl sha1

* sync via checksum

* hash assets in batches

* hash in background, show spinner in tab

* undo tmp changes

* migrate by clearing assets

* ignore duplicate assets

* error handling

* trigger sync/merge after download and update view

* review feedback improvements

* hash in background isolate on iOS

* rework linking assets with existing from DB

* fine-grained errors on unique index violation

* hash lenth validation

* revert compute in background on iOS

* ignore duplicate assets on device

* fix bug with batching based on accumulated size

---------

Co-authored-by: Fynn Petersen-Frey <zoodyy@users.noreply.github.com>
2023-06-10 13:13:59 -05:00

176 lines
4.8 KiB
Dart

import 'package:flutter/cupertino.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:isar/isar.dart';
import 'package:openapi/api.dart';
import 'package:photo_manager/photo_manager.dart';
part 'album.g.dart';
@Collection(inheritance: false)
class Album {
@protected
Album({
this.remoteId,
this.localId,
required this.name,
required this.createdAt,
required this.modifiedAt,
required this.shared,
});
Id id = Isar.autoIncrement;
@Index(unique: false, replace: false, type: IndexType.hash)
String? remoteId;
@Index(unique: false, replace: false, type: IndexType.hash)
String? localId;
String name;
DateTime createdAt;
DateTime modifiedAt;
bool shared;
final IsarLink<User> owner = IsarLink<User>();
final IsarLink<Asset> thumbnail = IsarLink<Asset>();
final IsarLinks<User> sharedUsers = IsarLinks<User>();
final IsarLinks<Asset> assets = IsarLinks<Asset>();
RenderList _renderList = RenderList.empty();
@ignore
RenderList get renderList => _renderList;
@ignore
bool get isRemote => remoteId != null;
@ignore
bool get isLocal => localId != null;
@ignore
int get assetCount => assets.length;
@ignore
String? get ownerId => owner.value?.id;
@ignore
String? get ownerName {
// Guard null owner
if (owner.value == null) {
return null;
}
final name = <String>[];
if (owner.value?.firstName != null) {
name.add(owner.value!.firstName);
}
if (owner.value?.lastName != null) {
name.add(owner.value!.lastName);
}
return name.join(' ');
}
Stream<void> watchRenderList(GroupAssetsBy groupAssetsBy) async* {
final query = assets.filter().sortByFileCreatedAt();
_renderList = await RenderList.fromQuery(query, groupAssetsBy);
yield _renderList;
await for (final _ in query.watchLazy()) {
_renderList = await RenderList.fromQuery(query, groupAssetsBy);
yield _renderList;
}
}
@override
bool operator ==(other) {
if (other is! Album) return false;
return id == other.id &&
remoteId == other.remoteId &&
localId == other.localId &&
name == other.name &&
createdAt.isAtSameMomentAs(other.createdAt) &&
modifiedAt.isAtSameMomentAs(other.modifiedAt) &&
shared == other.shared &&
owner.value == other.owner.value &&
thumbnail.value == other.thumbnail.value &&
sharedUsers.length == other.sharedUsers.length &&
assets.length == other.assets.length;
}
@override
@ignore
int get hashCode =>
id.hashCode ^
remoteId.hashCode ^
localId.hashCode ^
name.hashCode ^
createdAt.hashCode ^
modifiedAt.hashCode ^
shared.hashCode ^
owner.value.hashCode ^
thumbnail.value.hashCode ^
sharedUsers.length.hashCode ^
assets.length.hashCode;
static Album local(AssetPathEntity ape) {
final Album a = Album(
name: ape.name,
createdAt: ape.lastModified?.toUtc() ?? DateTime.now().toUtc(),
modifiedAt: ape.lastModified?.toUtc() ?? DateTime.now().toUtc(),
shared: false,
);
a.owner.value = Store.get(StoreKey.currentUser);
a.localId = ape.id;
return a;
}
static Future<Album> remote(AlbumResponseDto dto) async {
final Isar db = Isar.getInstance()!;
final Album a = Album(
remoteId: dto.id,
name: dto.albumName,
createdAt: dto.createdAt,
modifiedAt: dto.updatedAt,
shared: dto.shared,
);
a.owner.value = await db.users.getById(dto.ownerId);
if (dto.albumThumbnailAssetId != null) {
a.thumbnail.value = await db.assets
.where()
.remoteIdEqualTo(dto.albumThumbnailAssetId)
.findFirst();
}
if (dto.sharedUsers.isNotEmpty) {
final users = await db.users
.getAllById(dto.sharedUsers.map((e) => e.id).toList(growable: false));
a.sharedUsers.addAll(users.cast());
}
if (dto.assets.isNotEmpty) {
final assets =
await db.assets.getAllByRemoteId(dto.assets.map((e) => e.id));
a.assets.addAll(assets);
}
return a;
}
@override
String toString() => name;
}
extension AssetsHelper on IsarCollection<Album> {
Future<void> store(Album a) async {
await put(a);
await a.owner.save();
await a.thumbnail.save();
await a.sharedUsers.save();
await a.assets.save();
}
}
extension AlbumResponseDtoHelper on AlbumResponseDto {
List<Asset> getAssets() => assets.map(Asset.remote).toList();
}
extension AssetPathEntityHelper on AssetPathEntity {
String get eTagKeyAssetCount => "device-album-$id-asset-count";
}