mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-27 15:07:45 +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
|
// to acknowledge that the client has processed all the backfill events
|
||||||
case SyncEntityType.syncAckV1:
|
case SyncEntityType.syncAckV1:
|
||||||
return;
|
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:
|
default:
|
||||||
_logger.warning("Unknown sync data type: $type");
|
_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.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.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/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/partner.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart';
|
||||||
|
|
@ -46,6 +48,8 @@ class IsarDatabaseRepository implements IDatabaseRepository {
|
||||||
RemoteAlbumEntity,
|
RemoteAlbumEntity,
|
||||||
RemoteAlbumAssetEntity,
|
RemoteAlbumAssetEntity,
|
||||||
RemoteAlbumUserEntity,
|
RemoteAlbumUserEntity,
|
||||||
|
MemoryEntity,
|
||||||
|
MemoryAssetEntity,
|
||||||
],
|
],
|
||||||
include: {
|
include: {
|
||||||
'package:immich_mobile/infrastructure/entities/merged_asset.drift',
|
'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.albumAssetsV1,
|
||||||
SyncRequestType.albumAssetExifsV1,
|
SyncRequestType.albumAssetExifsV1,
|
||||||
SyncRequestType.albumToAssetsV1,
|
SyncRequestType.albumToAssetsV1,
|
||||||
|
SyncRequestType.memoriesV1,
|
||||||
|
SyncRequestType.memoryToAssetsV1,
|
||||||
],
|
],
|
||||||
).toJson(),
|
).toJson(),
|
||||||
);
|
);
|
||||||
|
|
@ -157,6 +159,10 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
||||||
SyncEntityType.albumToAssetBackfillV1: SyncAlbumToAssetV1.fromJson,
|
SyncEntityType.albumToAssetBackfillV1: SyncAlbumToAssetV1.fromJson,
|
||||||
SyncEntityType.albumToAssetDeleteV1: SyncAlbumToAssetDeleteV1.fromJson,
|
SyncEntityType.albumToAssetDeleteV1: SyncAlbumToAssetDeleteV1.fromJson,
|
||||||
SyncEntityType.syncAckV1: _SyncAckV1.fromJson,
|
SyncEntityType.syncAckV1: _SyncAckV1.fromJson,
|
||||||
|
SyncEntityType.memoryV1: SyncMemoryV1.fromJson,
|
||||||
|
SyncEntityType.memoryDeleteV1: SyncMemoryDeleteV1.fromJson,
|
||||||
|
SyncEntityType.memoryToAssetV1: SyncMemoryAssetV1.fromJson,
|
||||||
|
SyncEntityType.memoryToAssetDeleteV1: SyncMemoryAssetDeleteV1.fromJson,
|
||||||
};
|
};
|
||||||
|
|
||||||
class _SyncAckV1 {
|
class _SyncAckV1 {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/album.model.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/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/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/partner.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album.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_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.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/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/entities/user.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
@ -64,8 +69,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stackTrace) {
|
||||||
_logger.severe('Error: SyncPartnerDeleteV1', e, s);
|
_logger.severe('Error: SyncPartnerDeleteV1', error, stackTrace);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -87,8 +92,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stackTrace) {
|
||||||
_logger.severe('Error: SyncPartnerV1', e, s);
|
_logger.severe('Error: SyncPartnerV1', error, stackTrace);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -98,10 +103,11 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||||
String debugLabel = 'user',
|
String debugLabel = 'user',
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
await _db.remoteAssetEntity
|
await _db.remoteAssetEntity.deleteWhere(
|
||||||
.deleteWhere((row) => row.id.isIn(data.map((e) => e.assetId)));
|
(row) => row.id.isIn(data.map((error) => error.assetId)),
|
||||||
} catch (e, s) {
|
);
|
||||||
_logger.severe('Error: deleteAssetsV1 - $debugLabel', e, s);
|
} catch (error, stackTrace) {
|
||||||
|
_logger.severe('Error: deleteAssetsV1 - $debugLabel', error, stackTrace);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -136,8 +142,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stackTrace) {
|
||||||
_logger.severe('Error: updateAssetsV1 - $debugLabel', e, s);
|
_logger.severe('Error: updateAssetsV1 - $debugLabel', error, stackTrace);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -180,18 +186,23 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stackTrace) {
|
||||||
_logger.severe('Error: updateAssetsExifV1 - $debugLabel', e, s);
|
_logger.severe(
|
||||||
|
'Error: updateAssetsExifV1 - $debugLabel',
|
||||||
|
error,
|
||||||
|
stackTrace,
|
||||||
|
);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteAlbumsV1(Iterable<SyncAlbumDeleteV1> data) async {
|
Future<void> deleteAlbumsV1(Iterable<SyncAlbumDeleteV1> data) async {
|
||||||
try {
|
try {
|
||||||
await _db.remoteAlbumEntity
|
await _db.remoteAlbumEntity.deleteWhere(
|
||||||
.deleteWhere((row) => row.id.isIn(data.map((e) => e.albumId)));
|
(row) => row.id.isIn(data.map((e) => e.albumId)),
|
||||||
} catch (e, s) {
|
);
|
||||||
_logger.severe('Error: deleteAlbumsV1', e, s);
|
} catch (error, stackTrace) {
|
||||||
|
_logger.severe('Error: deleteAlbumsV1', error, stackTrace);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -218,8 +229,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stackTrace) {
|
||||||
_logger.severe('Error: updateAlbumsV1', e, s);
|
_logger.severe('Error: updateAlbumsV1', error, stackTrace);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -237,8 +248,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stackTrace) {
|
||||||
_logger.severe('Error: deleteAlbumUsersV1', e, s);
|
_logger.severe('Error: deleteAlbumUsersV1', error, stackTrace);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -264,8 +275,12 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stackTrace) {
|
||||||
_logger.severe('Error: updateAlbumUsersV1 - $debugLabel', e, s);
|
_logger.severe(
|
||||||
|
'Error: updateAlbumUsersV1 - $debugLabel',
|
||||||
|
error,
|
||||||
|
stackTrace,
|
||||||
|
);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -285,8 +300,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stackTrace) {
|
||||||
_logger.severe('Error: deleteAlbumToAssetsV1', e, s);
|
_logger.severe('Error: deleteAlbumToAssetsV1', error, stackTrace);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -310,8 +325,96 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stackTrace) {
|
||||||
_logger.severe('Error: updateAlbumToAssetsV1 - $debugLabel', e, s);
|
_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;
|
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 {
|
extension on api.AlbumUserRole {
|
||||||
AlbumUserRole toAlbumUserRole() => switch (this) {
|
AlbumUserRole toAlbumUserRole() => switch (this) {
|
||||||
api.AlbumUserRole.editor => AlbumUserRole.editor,
|
api.AlbumUserRole.editor => AlbumUserRole.editor,
|
||||||
|
|
@ -357,7 +467,7 @@ extension on String {
|
||||||
Duration? toDuration() {
|
Duration? toDuration() {
|
||||||
try {
|
try {
|
||||||
final parts = split(':')
|
final parts = split(':')
|
||||||
.map((e) => double.parse(e).toInt())
|
.map((error) => double.parse(error).toInt())
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
|
|
||||||
return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]);
|
return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]);
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,14 @@ final _remoteStats = [
|
||||||
name: 'Remote Albums',
|
name: 'Remote Albums',
|
||||||
load: (db) => db.managers.remoteAlbumEntity.count(),
|
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()
|
@RoutePage()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
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/models/memories/memory.model.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
import 'package:immich_mobile/services/memory.service.dart';
|
import 'package:immich_mobile/services/memory.service.dart';
|
||||||
|
|
||||||
final memoryFutureProvider =
|
final memoryFutureProvider =
|
||||||
|
|
@ -8,3 +10,7 @@ final memoryFutureProvider =
|
||||||
|
|
||||||
return await service.getMemoryLane();
|
return await service.getMemoryLane();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final driftMemoryProvider = Provider<DriftMemoryRepository>(
|
||||||
|
(ref) => DriftMemoryRepository(ref.watch(driftProvider)),
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,14 @@ void main() {
|
||||||
debugLabel: any(named: 'debugLabel'),
|
debugLabel: any(named: 'debugLabel'),
|
||||||
),
|
),
|
||||||
).thenAnswer(successHandler);
|
).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(
|
sut = SyncStreamService(
|
||||||
syncApiRepository: mockSyncApiRepo,
|
syncApiRepository: mockSyncApiRepo,
|
||||||
|
|
@ -227,5 +235,94 @@ void main() {
|
||||||
verify(() => mockSyncApiRepo.ack(["2"])).called(1);
|
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"),
|
data: SyncPartnerDeleteV1(sharedById: "3", sharedWithId: "4"),
|
||||||
ack: "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