feature(mobile): sync assets, albums & users to local database on device (#1759)

* feature(mobile): sync assets, albums & users to local database on device

* try to fix tests

* move DB sync operations to new SyncService

* clear db on user logout

* fix reason for endless loading timeline

* fix error when deleting album

* fix thumbnail of device albums

* add a few comments

* fix Hive box not open in album service when loading local assets

* adjust tests to int IDs

* fix bug: show all albums when Recent is selected

* update generated api

* reworked Recents album isAll handling

* guard against wrongly interleaved sync operations

* fix: timeline asset ordering (sort asset state by created at)

* fix: sort assets in albums by created at
This commit is contained in:
Fynn Petersen-Frey
2023-03-03 23:38:30 +01:00
committed by GitHub
parent 8f11529a75
commit 8708867c1c
61 changed files with 9024 additions and 893 deletions

View File

@@ -1,132 +1,153 @@
import 'package:flutter/cupertino.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 {
Album.remote(AlbumResponseDto dto)
: remoteId = dto.id,
name = dto.albumName,
createdAt = DateTime.parse(dto.createdAt),
// TODO add modifiedAt to server
modifiedAt = DateTime.parse(dto.createdAt),
shared = dto.shared,
ownerId = dto.ownerId,
albumThumbnailAssetId = dto.albumThumbnailAssetId,
assetCount = dto.assetCount,
sharedUsers = dto.sharedUsers.map((e) => User.fromDto(e)).toList(),
assets = dto.assets.map(Asset.remote).toList();
@protected
Album({
this.remoteId,
this.localId,
required this.name,
required this.ownerId,
required this.createdAt,
required this.modifiedAt,
required this.shared,
required this.assetCount,
this.albumThumbnailAssetId,
this.sharedUsers = const [],
this.assets = const [],
});
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;
String ownerId;
DateTime createdAt;
DateTime modifiedAt;
bool shared;
String? albumThumbnailAssetId;
int assetCount;
List<User> sharedUsers = const [];
List<Asset> assets = const [];
final IsarLink<User> owner = IsarLink<User>();
final IsarLink<Asset> thumbnail = IsarLink<Asset>();
final IsarLinks<User> sharedUsers = IsarLinks<User>();
final IsarLinks<Asset> assets = IsarLinks<Asset>();
List<Asset> _sortedAssets = [];
@ignore
List<Asset> get sortedAssets => _sortedAssets;
@ignore
bool get isRemote => remoteId != null;
@ignore
bool get isLocal => localId != null;
String get id => isRemote ? remoteId! : localId!;
@ignore
int get assetCount => assets.length;
@ignore
String? get ownerId => owner.value?.id;
Future<void> loadSortedAssets() async {
_sortedAssets = await assets.filter().sortByFileCreatedAt().findAll();
}
@override
bool operator ==(other) {
if (other is! Album) return false;
return remoteId == other.remoteId &&
return id == other.id &&
remoteId == other.remoteId &&
localId == other.localId &&
name == other.name &&
createdAt == other.createdAt &&
modifiedAt == other.modifiedAt &&
shared == other.shared &&
ownerId == other.ownerId &&
albumThumbnailAssetId == other.albumThumbnailAssetId;
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 ^
ownerId.hashCode ^
albumThumbnailAssetId.hashCode;
owner.value.hashCode ^
thumbnail.value.hashCode ^
sharedUsers.length.hashCode ^
assets.length.hashCode;
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json["remoteId"] = remoteId;
json["localId"] = localId;
json["name"] = name;
json["ownerId"] = ownerId;
json["createdAt"] = createdAt.millisecondsSinceEpoch;
json["modifiedAt"] = modifiedAt.millisecondsSinceEpoch;
json["shared"] = shared;
json["albumThumbnailAssetId"] = albumThumbnailAssetId;
json["assetCount"] = assetCount;
json["sharedUsers"] = sharedUsers;
json["assets"] = assets;
return json;
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 Album? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return Album(
remoteId: json["remoteId"],
localId: json["localId"],
name: json["name"],
ownerId: json["ownerId"],
createdAt: DateTime.fromMillisecondsSinceEpoch(
json["createdAt"],
isUtc: true,
),
modifiedAt: DateTime.fromMillisecondsSinceEpoch(
json["modifiedAt"],
isUtc: true,
),
shared: json["shared"],
albumThumbnailAssetId: json["albumThumbnailAssetId"],
assetCount: json["assetCount"],
sharedUsers: _listFromJson<User>(json["sharedUsers"], User.fromJson),
assets: _listFromJson<Asset>(json["assets"], Asset.fromJson),
);
static Future<Album> remote(AlbumResponseDto dto) async {
final Isar db = Isar.getInstance()!;
final Album a = Album(
remoteId: dto.id,
name: dto.albumName,
createdAt: DateTime.parse(dto.createdAt),
modifiedAt: DateTime.parse(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();
}
return null;
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;
}
}
List<T> _listFromJson<T>(
dynamic json,
T? Function(dynamic) fromJson,
) {
final result = <T>[];
if (json is List && json.isNotEmpty) {
for (final entry in json) {
final value = fromJson(entry);
if (value != null) {
result.add(value);
}
}
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();
}
return result;
}
extension AssetPathEntityHelper on AssetPathEntity {
Future<List<Asset>> getAssets({
int start = 0,
int end = 0x7fffffffffffffff,
}) async {
final assetEntities = await getAssetListRange(start: start, end: end);
return assetEntities.map(Asset.local).toList();
}
}
extension AlbumResponseDtoHelper on AlbumResponseDto {
List<Asset> getAssets() => assets.map(Asset.remote).toList();
}