mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-04 10:55:53 +00:00
feat: memories sync (#19644)
* feat: memories sync * Update mobile/lib/infrastructure/repositories/sync_stream.repository.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update mobile/lib/infrastructure/repositories/sync_stream.repository.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * show sync information * tests and pr feedback * pr feedback --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
7855974a29
commit
445f9174ea
16 changed files with 601 additions and 27 deletions
BIN
mobile/drift_schemas/main/drift_schema_v1.json
generated
BIN
mobile/drift_schemas/main/drift_schema_v1.json
generated
Binary file not shown.
202
mobile/lib/domain/models/memory.model.dart
Normal file
202
mobile/lib/domain/models/memory.model.dart
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||
import 'dart:convert';
|
||||
|
||||
enum MemoryTypeEnum {
|
||||
// do not change this order!
|
||||
onThisDay,
|
||||
}
|
||||
|
||||
class MemoryData {
|
||||
final int year;
|
||||
|
||||
const MemoryData({
|
||||
required this.year,
|
||||
});
|
||||
|
||||
MemoryData copyWith({
|
||||
int? year,
|
||||
}) {
|
||||
return MemoryData(
|
||||
year: year ?? this.year,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'year': year,
|
||||
};
|
||||
}
|
||||
|
||||
factory MemoryData.fromMap(Map<String, dynamic> map) {
|
||||
return MemoryData(
|
||||
year: map['year'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory MemoryData.fromJson(String source) =>
|
||||
MemoryData.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||
|
||||
@override
|
||||
String toString() => 'MemoryData(year: $year)';
|
||||
|
||||
@override
|
||||
bool operator ==(covariant MemoryData other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.year == year;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => year.hashCode;
|
||||
}
|
||||
|
||||
// Model for a memory stored in the server
|
||||
class Memory {
|
||||
final String id;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final DateTime? deletedAt;
|
||||
final String ownerId;
|
||||
|
||||
// enum
|
||||
final MemoryTypeEnum type;
|
||||
final MemoryData data;
|
||||
final bool isSaved;
|
||||
final DateTime memoryAt;
|
||||
final DateTime? seenAt;
|
||||
final DateTime? showAt;
|
||||
final DateTime? hideAt;
|
||||
|
||||
const Memory({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.deletedAt,
|
||||
required this.ownerId,
|
||||
required this.type,
|
||||
required this.data,
|
||||
required this.isSaved,
|
||||
required this.memoryAt,
|
||||
this.seenAt,
|
||||
this.showAt,
|
||||
this.hideAt,
|
||||
});
|
||||
|
||||
Memory copyWith({
|
||||
String? id,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String? ownerId,
|
||||
MemoryTypeEnum? type,
|
||||
MemoryData? data,
|
||||
bool? isSaved,
|
||||
DateTime? memoryAt,
|
||||
DateTime? seenAt,
|
||||
DateTime? showAt,
|
||||
DateTime? hideAt,
|
||||
}) {
|
||||
return Memory(
|
||||
id: id ?? this.id,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
deletedAt: deletedAt ?? this.deletedAt,
|
||||
ownerId: ownerId ?? this.ownerId,
|
||||
type: type ?? this.type,
|
||||
data: data ?? this.data,
|
||||
isSaved: isSaved ?? this.isSaved,
|
||||
memoryAt: memoryAt ?? this.memoryAt,
|
||||
seenAt: seenAt ?? this.seenAt,
|
||||
showAt: showAt ?? this.showAt,
|
||||
hideAt: hideAt ?? this.hideAt,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'id': id,
|
||||
'createdAt': createdAt.millisecondsSinceEpoch,
|
||||
'updatedAt': updatedAt.millisecondsSinceEpoch,
|
||||
'deletedAt': deletedAt?.millisecondsSinceEpoch,
|
||||
'ownerId': ownerId,
|
||||
'type': type.index,
|
||||
'data': data.toMap(),
|
||||
'isSaved': isSaved,
|
||||
'memoryAt': memoryAt.millisecondsSinceEpoch,
|
||||
'seenAt': seenAt?.millisecondsSinceEpoch,
|
||||
'showAt': showAt?.millisecondsSinceEpoch,
|
||||
'hideAt': hideAt?.millisecondsSinceEpoch,
|
||||
};
|
||||
}
|
||||
|
||||
factory Memory.fromMap(Map<String, dynamic> map) {
|
||||
return Memory(
|
||||
id: map['id'] as String,
|
||||
createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt'] as int),
|
||||
updatedAt: DateTime.fromMillisecondsSinceEpoch(map['updatedAt'] as int),
|
||||
deletedAt: map['deletedAt'] != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(map['deletedAt'] as int)
|
||||
: null,
|
||||
ownerId: map['ownerId'] as String,
|
||||
type: MemoryTypeEnum.values[map['type'] as int],
|
||||
data: MemoryData.fromMap(map['data'] as Map<String, dynamic>),
|
||||
isSaved: map['isSaved'] as bool,
|
||||
memoryAt: DateTime.fromMillisecondsSinceEpoch(map['memoryAt'] as int),
|
||||
seenAt: map['seenAt'] != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(map['seenAt'] as int)
|
||||
: null,
|
||||
showAt: map['showAt'] != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(map['showAt'] as int)
|
||||
: null,
|
||||
hideAt: map['hideAt'] != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(map['hideAt'] as int)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory Memory.fromJson(String source) =>
|
||||
Memory.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Memory(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, ownerId: $ownerId, type: $type, data: $data, isSaved: $isSaved, memoryAt: $memoryAt, seenAt: $seenAt, showAt: $showAt, hideAt: $hideAt)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant Memory other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.id == id &&
|
||||
other.createdAt == createdAt &&
|
||||
other.updatedAt == updatedAt &&
|
||||
other.deletedAt == deletedAt &&
|
||||
other.ownerId == ownerId &&
|
||||
other.type == type &&
|
||||
other.data == data &&
|
||||
other.isSaved == isSaved &&
|
||||
other.memoryAt == memoryAt &&
|
||||
other.seenAt == seenAt &&
|
||||
other.showAt == showAt &&
|
||||
other.hideAt == hideAt;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return id.hashCode ^
|
||||
createdAt.hashCode ^
|
||||
updatedAt.hashCode ^
|
||||
deletedAt.hashCode ^
|
||||
ownerId.hashCode ^
|
||||
type.hashCode ^
|
||||
data.hashCode ^
|
||||
isSaved.hashCode ^
|
||||
memoryAt.hashCode ^
|
||||
seenAt.hashCode ^
|
||||
showAt.hashCode ^
|
||||
hideAt.hashCode;
|
||||
}
|
||||
}
|
||||
|
|
@ -146,6 +146,14 @@ class SyncStreamService {
|
|||
// to acknowledge that the client has processed all the backfill events
|
||||
case SyncEntityType.syncAckV1:
|
||||
return;
|
||||
case SyncEntityType.memoryV1:
|
||||
return _syncStreamRepository.updateMemoriesV1(data.cast());
|
||||
case SyncEntityType.memoryDeleteV1:
|
||||
return _syncStreamRepository.deleteMemoriesV1(data.cast());
|
||||
case SyncEntityType.memoryToAssetV1:
|
||||
return _syncStreamRepository.updateMemoryAssetsV1(data.cast());
|
||||
case SyncEntityType.memoryToAssetDeleteV1:
|
||||
return _syncStreamRepository.deleteMemoryAssetsV1(data.cast());
|
||||
default:
|
||||
_logger.warning("Unknown sync data type: $type");
|
||||
}
|
||||
|
|
|
|||
36
mobile/lib/infrastructure/entities/memory.entity.dart
Normal file
36
mobile/lib/infrastructure/entities/memory.entity.dart
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
class MemoryEntity extends Table with DriftDefaultsMixin {
|
||||
const MemoryEntity();
|
||||
|
||||
TextColumn get id => text()();
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
DateTimeColumn get deletedAt => dateTime().nullable()();
|
||||
|
||||
TextColumn get ownerId =>
|
||||
text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
IntColumn get type => intEnum<MemoryTypeEnum>()();
|
||||
|
||||
TextColumn get data => text()();
|
||||
|
||||
BoolColumn get isSaved => boolean().withDefault(const Constant(false))();
|
||||
|
||||
DateTimeColumn get memoryAt => dateTime()();
|
||||
|
||||
DateTimeColumn get seenAt => dateTime().nullable()();
|
||||
|
||||
DateTimeColumn get showAt => dateTime().nullable()();
|
||||
|
||||
DateTimeColumn get hideAt => dateTime().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
BIN
mobile/lib/infrastructure/entities/memory.entity.drift.dart
generated
Normal file
BIN
mobile/lib/infrastructure/entities/memory.entity.drift.dart
generated
Normal file
Binary file not shown.
17
mobile/lib/infrastructure/entities/memory_asset.entity.dart
Normal file
17
mobile/lib/infrastructure/entities/memory_asset.entity.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/memory.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
class MemoryAssetEntity extends Table with DriftDefaultsMixin {
|
||||
const MemoryAssetEntity();
|
||||
|
||||
TextColumn get assetId =>
|
||||
text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
TextColumn get memoryId =>
|
||||
text().references(MemoryEntity, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {assetId, memoryId};
|
||||
}
|
||||
BIN
mobile/lib/infrastructure/entities/memory_asset.entity.drift.dart
generated
Normal file
BIN
mobile/lib/infrastructure/entities/memory_asset.entity.drift.dart
generated
Normal file
Binary file not shown.
|
|
@ -7,6 +7,8 @@ import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
|
|||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/memory.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart';
|
||||
|
|
@ -46,6 +48,8 @@ class IsarDatabaseRepository implements IDatabaseRepository {
|
|||
RemoteAlbumEntity,
|
||||
RemoteAlbumAssetEntity,
|
||||
RemoteAlbumUserEntity,
|
||||
MemoryEntity,
|
||||
MemoryAssetEntity,
|
||||
],
|
||||
include: {
|
||||
'package:immich_mobile/infrastructure/entities/merged_asset.drift',
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -0,0 +1,37 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
|
||||
class DriftMemoryRepository extends DriftDatabaseRepository {
|
||||
final Drift _db;
|
||||
const DriftMemoryRepository(this._db) : super(_db);
|
||||
|
||||
Future<List<Memory>> getAll(String userId) {
|
||||
final query = _db.memoryEntity.select()
|
||||
..where((e) => e.ownerId.equals(userId));
|
||||
|
||||
return query.map((memory) {
|
||||
return memory.toDto();
|
||||
}).get();
|
||||
}
|
||||
}
|
||||
|
||||
extension on MemoryEntityData {
|
||||
Memory toDto() {
|
||||
return Memory(
|
||||
id: id,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
deletedAt: deletedAt,
|
||||
ownerId: ownerId,
|
||||
type: type,
|
||||
data: MemoryData.fromJson(data),
|
||||
isSaved: isSaved,
|
||||
memoryAt: memoryAt,
|
||||
seenAt: seenAt,
|
||||
showAt: showAt,
|
||||
hideAt: hideAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -52,6 +52,8 @@ class SyncApiRepository {
|
|||
SyncRequestType.albumAssetsV1,
|
||||
SyncRequestType.albumAssetExifsV1,
|
||||
SyncRequestType.albumToAssetsV1,
|
||||
SyncRequestType.memoriesV1,
|
||||
SyncRequestType.memoryToAssetsV1,
|
||||
],
|
||||
).toJson(),
|
||||
);
|
||||
|
|
@ -157,6 +159,10 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
|||
SyncEntityType.albumToAssetBackfillV1: SyncAlbumToAssetV1.fromJson,
|
||||
SyncEntityType.albumToAssetDeleteV1: SyncAlbumToAssetDeleteV1.fromJson,
|
||||
SyncEntityType.syncAckV1: _SyncAckV1.fromJson,
|
||||
SyncEntityType.memoryV1: SyncMemoryV1.fromJson,
|
||||
SyncEntityType.memoryDeleteV1: SyncMemoryDeleteV1.fromJson,
|
||||
SyncEntityType.memoryToAssetV1: SyncMemoryAssetV1.fromJson,
|
||||
SyncEntityType.memoryToAssetDeleteV1: SyncMemoryAssetDeleteV1.fromJson,
|
||||
};
|
||||
|
||||
class _SyncAckV1 {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
|
@ -64,8 +69,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||
);
|
||||
}
|
||||
});
|
||||
} catch (e, s) {
|
||||
_logger.severe('Error: SyncPartnerDeleteV1', e, s);
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe('Error: SyncPartnerDeleteV1', error, stackTrace);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
|
@ -87,8 +92,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||
);
|
||||
}
|
||||
});
|
||||
} catch (e, s) {
|
||||
_logger.severe('Error: SyncPartnerV1', e, s);
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe('Error: SyncPartnerV1', error, stackTrace);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
|
@ -98,10 +103,11 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||
String debugLabel = 'user',
|
||||
}) async {
|
||||
try {
|
||||
await _db.remoteAssetEntity
|
||||
.deleteWhere((row) => row.id.isIn(data.map((e) => e.assetId)));
|
||||
} catch (e, s) {
|
||||
_logger.severe('Error: deleteAssetsV1 - $debugLabel', e, s);
|
||||
await _db.remoteAssetEntity.deleteWhere(
|
||||
(row) => row.id.isIn(data.map((error) => error.assetId)),
|
||||
);
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe('Error: deleteAssetsV1 - $debugLabel', error, stackTrace);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
|
@ -136,8 +142,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||
);
|
||||
}
|
||||
});
|
||||
} catch (e, s) {
|
||||
_logger.severe('Error: updateAssetsV1 - $debugLabel', e, s);
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe('Error: updateAssetsV1 - $debugLabel', error, stackTrace);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
|
@ -180,18 +186,23 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||
);
|
||||
}
|
||||
});
|
||||
} catch (e, s) {
|
||||
_logger.severe('Error: updateAssetsExifV1 - $debugLabel', e, s);
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe(
|
||||
'Error: updateAssetsExifV1 - $debugLabel',
|
||||
error,
|
||||
stackTrace,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteAlbumsV1(Iterable<SyncAlbumDeleteV1> data) async {
|
||||
try {
|
||||
await _db.remoteAlbumEntity
|
||||
.deleteWhere((row) => row.id.isIn(data.map((e) => e.albumId)));
|
||||
} catch (e, s) {
|
||||
_logger.severe('Error: deleteAlbumsV1', e, s);
|
||||
await _db.remoteAlbumEntity.deleteWhere(
|
||||
(row) => row.id.isIn(data.map((e) => e.albumId)),
|
||||
);
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe('Error: deleteAlbumsV1', error, stackTrace);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
|
@ -218,8 +229,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||
);
|
||||
}
|
||||
});
|
||||
} catch (e, s) {
|
||||
_logger.severe('Error: updateAlbumsV1', e, s);
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe('Error: updateAlbumsV1', error, stackTrace);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
|
@ -237,8 +248,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||
);
|
||||
}
|
||||
});
|
||||
} catch (e, s) {
|
||||
_logger.severe('Error: deleteAlbumUsersV1', e, s);
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe('Error: deleteAlbumUsersV1', error, stackTrace);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
|
@ -264,8 +275,12 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||
);
|
||||
}
|
||||
});
|
||||
} catch (e, s) {
|
||||
_logger.severe('Error: updateAlbumUsersV1 - $debugLabel', e, s);
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe(
|
||||
'Error: updateAlbumUsersV1 - $debugLabel',
|
||||
error,
|
||||
stackTrace,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
|
@ -285,8 +300,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||
);
|
||||
}
|
||||
});
|
||||
} catch (e, s) {
|
||||
_logger.severe('Error: deleteAlbumToAssetsV1', e, s);
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe('Error: deleteAlbumToAssetsV1', error, stackTrace);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
|
@ -310,8 +325,96 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||
);
|
||||
}
|
||||
});
|
||||
} catch (e, s) {
|
||||
_logger.severe('Error: updateAlbumToAssetsV1 - $debugLabel', e, s);
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe(
|
||||
'Error: updateAlbumToAssetsV1 - $debugLabel',
|
||||
error,
|
||||
stackTrace,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateMemoriesV1(Iterable<SyncMemoryV1> data) async {
|
||||
try {
|
||||
await _db.batch((batch) {
|
||||
for (final memory in data) {
|
||||
final companion = MemoryEntityCompanion(
|
||||
createdAt: Value(memory.createdAt),
|
||||
deletedAt: Value(memory.deletedAt),
|
||||
ownerId: Value(memory.ownerId),
|
||||
type: Value(memory.type.toMemoryType()),
|
||||
data: Value(jsonEncode(memory.data)),
|
||||
isSaved: Value(memory.isSaved),
|
||||
memoryAt: Value(memory.memoryAt),
|
||||
seenAt: Value.absentIfNull(memory.seenAt),
|
||||
showAt: Value.absentIfNull(memory.showAt),
|
||||
hideAt: Value.absentIfNull(memory.hideAt),
|
||||
);
|
||||
|
||||
batch.insert(
|
||||
_db.memoryEntity,
|
||||
companion.copyWith(id: Value(memory.id)),
|
||||
onConflict: DoUpdate((_) => companion),
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe('Error: updateMemoriesV1', error, stackTrace);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteMemoriesV1(Iterable<SyncMemoryDeleteV1> data) async {
|
||||
try {
|
||||
await _db.memoryEntity.deleteWhere(
|
||||
(row) => row.id.isIn(data.map((e) => e.memoryId)),
|
||||
);
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe('Error: deleteMemoriesV1', error, stackTrace);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateMemoryAssetsV1(Iterable<SyncMemoryAssetV1> data) async {
|
||||
try {
|
||||
await _db.batch((batch) {
|
||||
for (final asset in data) {
|
||||
final companion = MemoryAssetEntityCompanion(
|
||||
memoryId: Value(asset.memoryId),
|
||||
assetId: Value(asset.assetId),
|
||||
);
|
||||
|
||||
batch.insert(
|
||||
_db.memoryAssetEntity,
|
||||
companion,
|
||||
onConflict: DoNothing(),
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe('Error: updateMemoryAssetsV1', error, stackTrace);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteMemoryAssetsV1(
|
||||
Iterable<SyncMemoryAssetDeleteV1> data,
|
||||
) async {
|
||||
try {
|
||||
await _db.batch((batch) {
|
||||
for (final asset in data) {
|
||||
batch.delete(
|
||||
_db.memoryAssetEntity,
|
||||
MemoryAssetEntityCompanion(
|
||||
memoryId: Value(asset.memoryId),
|
||||
assetId: Value(asset.assetId),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe('Error: deleteMemoryAssetsV1', error, stackTrace);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
|
@ -335,6 +438,13 @@ extension on AssetOrder {
|
|||
};
|
||||
}
|
||||
|
||||
extension on MemoryType {
|
||||
MemoryTypeEnum toMemoryType() => switch (this) {
|
||||
MemoryType.onThisDay => MemoryTypeEnum.onThisDay,
|
||||
_ => throw Exception('Unknown MemoryType value: $this'),
|
||||
};
|
||||
}
|
||||
|
||||
extension on api.AlbumUserRole {
|
||||
AlbumUserRole toAlbumUserRole() => switch (this) {
|
||||
api.AlbumUserRole.editor => AlbumUserRole.editor,
|
||||
|
|
@ -357,7 +467,7 @@ extension on String {
|
|||
Duration? toDuration() {
|
||||
try {
|
||||
final parts = split(':')
|
||||
.map((e) => double.parse(e).toInt())
|
||||
.map((error) => double.parse(error).toInt())
|
||||
.toList(growable: false);
|
||||
|
||||
return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]);
|
||||
|
|
|
|||
|
|
@ -154,6 +154,14 @@ final _remoteStats = [
|
|||
name: 'Remote Albums',
|
||||
load: (db) => db.managers.remoteAlbumEntity.count(),
|
||||
),
|
||||
_Stat(
|
||||
name: 'Memories',
|
||||
load: (db) => db.managers.memoryEntity.count(),
|
||||
),
|
||||
_Stat(
|
||||
name: 'Memories Assets',
|
||||
load: (db) => db.managers.memoryAssetEntity.count(),
|
||||
),
|
||||
];
|
||||
|
||||
@RoutePage()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/memory.repository.dart';
|
||||
import 'package:immich_mobile/models/memories/memory.model.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/services/memory.service.dart';
|
||||
|
||||
final memoryFutureProvider =
|
||||
|
|
@ -8,3 +10,7 @@ final memoryFutureProvider =
|
|||
|
||||
return await service.getMemoryLane();
|
||||
});
|
||||
|
||||
final driftMemoryProvider = Provider<DriftMemoryRepository>(
|
||||
(ref) => DriftMemoryRepository(ref.watch(driftProvider)),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -81,6 +81,14 @@ void main() {
|
|||
debugLabel: any(named: 'debugLabel'),
|
||||
),
|
||||
).thenAnswer(successHandler);
|
||||
when(() => mockSyncStreamRepo.updateMemoriesV1(any()))
|
||||
.thenAnswer(successHandler);
|
||||
when(() => mockSyncStreamRepo.deleteMemoriesV1(any()))
|
||||
.thenAnswer(successHandler);
|
||||
when(() => mockSyncStreamRepo.updateMemoryAssetsV1(any()))
|
||||
.thenAnswer(successHandler);
|
||||
when(() => mockSyncStreamRepo.deleteMemoryAssetsV1(any()))
|
||||
.thenAnswer(successHandler);
|
||||
|
||||
sut = SyncStreamService(
|
||||
syncApiRepository: mockSyncApiRepo,
|
||||
|
|
@ -227,5 +235,94 @@ void main() {
|
|||
verify(() => mockSyncApiRepo.ack(["2"])).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
test("processes memory sync events successfully", () async {
|
||||
final events = [
|
||||
SyncStreamStub.memoryV1,
|
||||
SyncStreamStub.memoryDeleteV1,
|
||||
SyncStreamStub.memoryToAssetV1,
|
||||
SyncStreamStub.memoryToAssetDeleteV1,
|
||||
];
|
||||
|
||||
await simulateEvents(events);
|
||||
|
||||
verifyInOrder([
|
||||
() => mockSyncStreamRepo.updateMemoriesV1(any()),
|
||||
() => mockSyncApiRepo.ack(["5"]),
|
||||
() => mockSyncStreamRepo.deleteMemoriesV1(any()),
|
||||
() => mockSyncApiRepo.ack(["6"]),
|
||||
() => mockSyncStreamRepo.updateMemoryAssetsV1(any()),
|
||||
() => mockSyncApiRepo.ack(["7"]),
|
||||
() => mockSyncStreamRepo.deleteMemoryAssetsV1(any()),
|
||||
() => mockSyncApiRepo.ack(["8"]),
|
||||
]);
|
||||
verifyNever(() => mockAbortCallbackWrapper());
|
||||
});
|
||||
|
||||
test("processes mixed memory and user events in correct order", () async {
|
||||
final events = [
|
||||
SyncStreamStub.memoryDeleteV1,
|
||||
SyncStreamStub.userV1Admin,
|
||||
SyncStreamStub.memoryToAssetV1,
|
||||
SyncStreamStub.memoryV1,
|
||||
];
|
||||
|
||||
await simulateEvents(events);
|
||||
|
||||
verifyInOrder([
|
||||
() => mockSyncStreamRepo.deleteMemoriesV1(any()),
|
||||
() => mockSyncApiRepo.ack(["6"]),
|
||||
() => mockSyncStreamRepo.updateUsersV1(any()),
|
||||
() => mockSyncApiRepo.ack(["1"]),
|
||||
() => mockSyncStreamRepo.updateMemoryAssetsV1(any()),
|
||||
() => mockSyncApiRepo.ack(["7"]),
|
||||
() => mockSyncStreamRepo.updateMemoriesV1(any()),
|
||||
() => mockSyncApiRepo.ack(["5"]),
|
||||
]);
|
||||
verifyNever(() => mockAbortCallbackWrapper());
|
||||
});
|
||||
|
||||
test("handles memory sync failure gracefully", () async {
|
||||
when(() => mockSyncStreamRepo.updateMemoriesV1(any()))
|
||||
.thenThrow(Exception("Memory sync failed"));
|
||||
|
||||
final events = [
|
||||
SyncStreamStub.memoryV1,
|
||||
SyncStreamStub.userV1Admin,
|
||||
];
|
||||
|
||||
expect(
|
||||
() async => await simulateEvents(events),
|
||||
throwsA(isA<Exception>()),
|
||||
);
|
||||
});
|
||||
|
||||
test("processes memory asset events with correct data types", () async {
|
||||
final events = [SyncStreamStub.memoryToAssetV1];
|
||||
|
||||
await simulateEvents(events);
|
||||
|
||||
verify(() => mockSyncStreamRepo.updateMemoryAssetsV1(any())).called(1);
|
||||
verify(() => mockSyncApiRepo.ack(["7"])).called(1);
|
||||
});
|
||||
|
||||
test("processes memory delete events with correct data types", () async {
|
||||
final events = [SyncStreamStub.memoryDeleteV1];
|
||||
|
||||
await simulateEvents(events);
|
||||
|
||||
verify(() => mockSyncStreamRepo.deleteMemoriesV1(any())).called(1);
|
||||
verify(() => mockSyncApiRepo.ack(["6"])).called(1);
|
||||
});
|
||||
|
||||
test("processes memory create/update events with correct data types",
|
||||
() async {
|
||||
final events = [SyncStreamStub.memoryV1];
|
||||
|
||||
await simulateEvents(events);
|
||||
|
||||
verify(() => mockSyncStreamRepo.updateMemoriesV1(any())).called(1);
|
||||
verify(() => mockSyncApiRepo.ack(["5"])).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
43
mobile/test/fixtures/sync_stream.stub.dart
vendored
43
mobile/test/fixtures/sync_stream.stub.dart
vendored
|
|
@ -42,4 +42,47 @@ abstract final class SyncStreamStub {
|
|||
data: SyncPartnerDeleteV1(sharedById: "3", sharedWithId: "4"),
|
||||
ack: "4",
|
||||
);
|
||||
|
||||
static final memoryV1 = SyncEvent(
|
||||
type: SyncEntityType.memoryV1,
|
||||
data: SyncMemoryV1(
|
||||
createdAt: DateTime(2023, 1, 1),
|
||||
data: {"year": 2023, "title": "Test Memory"},
|
||||
deletedAt: null,
|
||||
hideAt: null,
|
||||
id: "memory-1",
|
||||
isSaved: false,
|
||||
memoryAt: DateTime(2023, 1, 1),
|
||||
ownerId: "user-1",
|
||||
seenAt: null,
|
||||
showAt: DateTime(2023, 1, 1),
|
||||
type: MemoryType.onThisDay,
|
||||
updatedAt: DateTime(2023, 1, 1),
|
||||
),
|
||||
ack: "5",
|
||||
);
|
||||
|
||||
static final memoryDeleteV1 = SyncEvent(
|
||||
type: SyncEntityType.memoryDeleteV1,
|
||||
data: SyncMemoryDeleteV1(memoryId: "memory-2"),
|
||||
ack: "6",
|
||||
);
|
||||
|
||||
static final memoryToAssetV1 = SyncEvent(
|
||||
type: SyncEntityType.memoryToAssetV1,
|
||||
data: SyncMemoryAssetV1(
|
||||
assetId: "asset-1",
|
||||
memoryId: "memory-1",
|
||||
),
|
||||
ack: "7",
|
||||
);
|
||||
|
||||
static final memoryToAssetDeleteV1 = SyncEvent(
|
||||
type: SyncEntityType.memoryToAssetDeleteV1,
|
||||
data: SyncMemoryAssetDeleteV1(
|
||||
assetId: "asset-2",
|
||||
memoryId: "memory-1",
|
||||
),
|
||||
ack: "8",
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue