mirror of
https://github.com/samsonjs/immich.git
synced 2026-03-25 09:15:56 +00:00
feat(mobile): drift user metadata sync (#19894)
* feat(mobile): drift user metadata sync * change text to jsonb * update primary key --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
daea57f7d2
commit
0acbf1199a
10 changed files with 346 additions and 12 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.
|
|
@ -1,5 +1,12 @@
|
|||
import 'dart:ui';
|
||||
|
||||
enum UserMetadataKey {
|
||||
// do not change this order!
|
||||
onboarding,
|
||||
preferences,
|
||||
license,
|
||||
}
|
||||
|
||||
enum AvatarColor {
|
||||
// do not change this order or reuse indices for other purposes, adding is OK
|
||||
primary("primary"),
|
||||
|
|
@ -31,7 +38,45 @@ enum AvatarColor {
|
|||
};
|
||||
}
|
||||
|
||||
class UserPreferences {
|
||||
class Onboarding {
|
||||
final bool isOnboarded;
|
||||
|
||||
const Onboarding({required this.isOnboarded});
|
||||
|
||||
Onboarding copyWith({bool? isOnboarded}) {
|
||||
return Onboarding(isOnboarded: isOnboarded ?? this.isOnboarded);
|
||||
}
|
||||
|
||||
Map<String, Object?> toMap() {
|
||||
final onboarding = <String, Object?>{};
|
||||
onboarding["isOnboarded"] = isOnboarded;
|
||||
return onboarding;
|
||||
}
|
||||
|
||||
factory Onboarding.fromMap(Map<String, Object?> map) {
|
||||
return Onboarding(isOnboarded: map["isOnboarded"] as bool);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''Onboarding {
|
||||
isOnboarded: $isOnboarded,
|
||||
}''';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant Onboarding other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return isOnboarded == other.isOnboarded;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => isOnboarded.hashCode;
|
||||
}
|
||||
|
||||
// TODO: wait to be overwritten
|
||||
class Preferences {
|
||||
final bool foldersEnabled;
|
||||
final bool memoriesEnabled;
|
||||
final bool peopleEnabled;
|
||||
|
|
@ -41,7 +86,7 @@ class UserPreferences {
|
|||
final AvatarColor userAvatarColor;
|
||||
final bool showSupportBadge;
|
||||
|
||||
const UserPreferences({
|
||||
const Preferences({
|
||||
this.foldersEnabled = false,
|
||||
this.memoriesEnabled = true,
|
||||
this.peopleEnabled = true,
|
||||
|
|
@ -52,7 +97,7 @@ class UserPreferences {
|
|||
this.showSupportBadge = true,
|
||||
});
|
||||
|
||||
UserPreferences copyWith({
|
||||
Preferences copyWith({
|
||||
bool? foldersEnabled,
|
||||
bool? memoriesEnabled,
|
||||
bool? peopleEnabled,
|
||||
|
|
@ -62,7 +107,7 @@ class UserPreferences {
|
|||
AvatarColor? userAvatarColor,
|
||||
bool? showSupportBadge,
|
||||
}) {
|
||||
return UserPreferences(
|
||||
return Preferences(
|
||||
foldersEnabled: foldersEnabled ?? this.foldersEnabled,
|
||||
memoriesEnabled: memoriesEnabled ?? this.memoriesEnabled,
|
||||
peopleEnabled: peopleEnabled ?? this.peopleEnabled,
|
||||
|
|
@ -87,8 +132,8 @@ class UserPreferences {
|
|||
return preferences;
|
||||
}
|
||||
|
||||
factory UserPreferences.fromMap(Map<String, Object?> map) {
|
||||
return UserPreferences(
|
||||
factory Preferences.fromMap(Map<String, Object?> map) {
|
||||
return Preferences(
|
||||
foldersEnabled: map["folders-Enabled"] as bool? ?? false,
|
||||
memoriesEnabled: map["memories-Enabled"] as bool? ?? true,
|
||||
peopleEnabled: map["people-Enabled"] as bool? ?? true,
|
||||
|
|
@ -102,4 +147,173 @@ class UserPreferences {
|
|||
showSupportBadge: map["purchase-ShowSupportBadge"] as bool? ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''Preferences: {
|
||||
foldersEnabled: $foldersEnabled,
|
||||
memoriesEnabled: $memoriesEnabled,
|
||||
peopleEnabled: $peopleEnabled,
|
||||
ratingsEnabled: $ratingsEnabled,
|
||||
sharedLinksEnabled: $sharedLinksEnabled,
|
||||
tagsEnabled: $tagsEnabled,
|
||||
userAvatarColor: $userAvatarColor,
|
||||
showSupportBadge: $showSupportBadge,
|
||||
}''';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant Preferences other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.foldersEnabled == foldersEnabled &&
|
||||
other.memoriesEnabled == memoriesEnabled &&
|
||||
other.peopleEnabled == peopleEnabled &&
|
||||
other.ratingsEnabled == ratingsEnabled &&
|
||||
other.sharedLinksEnabled == sharedLinksEnabled &&
|
||||
other.tagsEnabled == tagsEnabled &&
|
||||
other.userAvatarColor == userAvatarColor &&
|
||||
other.showSupportBadge == showSupportBadge;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return foldersEnabled.hashCode ^
|
||||
memoriesEnabled.hashCode ^
|
||||
peopleEnabled.hashCode ^
|
||||
ratingsEnabled.hashCode ^
|
||||
sharedLinksEnabled.hashCode ^
|
||||
tagsEnabled.hashCode ^
|
||||
userAvatarColor.hashCode ^
|
||||
showSupportBadge.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
class License {
|
||||
final DateTime activatedAt;
|
||||
final String activationKey;
|
||||
final String licenseKey;
|
||||
|
||||
const License({
|
||||
required this.activatedAt,
|
||||
required this.activationKey,
|
||||
required this.licenseKey,
|
||||
});
|
||||
|
||||
License copyWith({
|
||||
DateTime? activatedAt,
|
||||
String? activationKey,
|
||||
String? licenseKey,
|
||||
}) {
|
||||
return License(
|
||||
activatedAt: activatedAt ?? this.activatedAt,
|
||||
activationKey: activationKey ?? this.activationKey,
|
||||
licenseKey: licenseKey ?? this.licenseKey,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, Object?> toMap() {
|
||||
final license = <String, Object?>{};
|
||||
license["activatedAt"] = activatedAt;
|
||||
license["activationKey"] = activationKey;
|
||||
license["licenseKey"] = licenseKey;
|
||||
return license;
|
||||
}
|
||||
|
||||
factory License.fromMap(Map<String, Object?> map) {
|
||||
return License(
|
||||
activatedAt: map["activatedAt"] as DateTime,
|
||||
activationKey: map["activationKey"] as String,
|
||||
licenseKey: map["licenseKey"] as String,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''License {
|
||||
activatedAt: $activatedAt,
|
||||
activationKey: $activationKey,
|
||||
licenseKey: $licenseKey,
|
||||
}''';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant License other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return activatedAt == other.activatedAt &&
|
||||
activationKey == other.activationKey &&
|
||||
licenseKey == other.licenseKey;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
activatedAt.hashCode ^ activationKey.hashCode ^ licenseKey.hashCode;
|
||||
}
|
||||
|
||||
// Model for a user metadata stored in the server
|
||||
class UserMetadata {
|
||||
final String userId;
|
||||
final UserMetadataKey key;
|
||||
final Onboarding? onboarding;
|
||||
final Preferences? preferences;
|
||||
final License? license;
|
||||
|
||||
const UserMetadata({
|
||||
required this.userId,
|
||||
required this.key,
|
||||
this.onboarding,
|
||||
this.preferences,
|
||||
this.license,
|
||||
}) : assert(
|
||||
onboarding != null || preferences != null || license != null,
|
||||
'One of onboarding, preferences and license must be provided',
|
||||
);
|
||||
|
||||
UserMetadata copyWith({
|
||||
String? userId,
|
||||
UserMetadataKey? key,
|
||||
Onboarding? onboarding,
|
||||
Preferences? preferences,
|
||||
License? license,
|
||||
}) {
|
||||
return UserMetadata(
|
||||
userId: userId ?? this.userId,
|
||||
key: key ?? this.key,
|
||||
onboarding: onboarding ?? this.onboarding,
|
||||
preferences: preferences ?? this.preferences,
|
||||
license: license ?? this.license,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''UserMetadata: {
|
||||
userId: $userId,
|
||||
key: $key,
|
||||
onboarding: ${onboarding ?? "<NA>"},
|
||||
preferences: ${preferences ?? "<NA>"},
|
||||
license: ${license ?? "<NA>"},
|
||||
}''';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant UserMetadata other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.userId == userId &&
|
||||
other.key == key &&
|
||||
other.onboarding == onboarding &&
|
||||
other.preferences == preferences &&
|
||||
other.license == license;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return userId.hashCode ^
|
||||
key.hashCode ^
|
||||
onboarding.hashCode ^
|
||||
preferences.hashCode ^
|
||||
license.hashCode;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,6 +179,14 @@ class SyncStreamService {
|
|||
data.cast(),
|
||||
debugLabel: 'partner',
|
||||
);
|
||||
case SyncEntityType.userMetadataV1:
|
||||
return _syncStreamRepository.updateUserMetadatasV1(
|
||||
data.cast(),
|
||||
);
|
||||
case SyncEntityType.userMetadataDeleteV1:
|
||||
return _syncStreamRepository.deleteUserMetadatasV1(
|
||||
data.cast(),
|
||||
);
|
||||
default:
|
||||
_logger.warning("Unknown sync data type: $type");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,14 +8,16 @@ class UserMetadataEntity extends Table with DriftDefaultsMixin {
|
|||
|
||||
TextColumn get userId =>
|
||||
text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get preferences => text().map(userPreferenceConverter)();
|
||||
|
||||
IntColumn get key => intEnum<UserMetadataKey>()();
|
||||
|
||||
BlobColumn get value => blob().map(userMetadataConverter)();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {userId};
|
||||
Set<Column> get primaryKey => {userId, key};
|
||||
}
|
||||
|
||||
final JsonTypeConverter2<UserPreferences, String, Object?>
|
||||
userPreferenceConverter = TypeConverter.json2(
|
||||
fromJson: (json) => UserPreferences.fromMap(json as Map<String, Object?>),
|
||||
toJson: (pref) => pref.toMap(),
|
||||
final JsonTypeConverter2<Map<String, Object?>, Uint8List, Object?>
|
||||
userMetadataConverter = TypeConverter.jsonb(
|
||||
fromJson: (json) => json as Map<String, Object?>,
|
||||
);
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -56,6 +56,7 @@ class SyncApiRepository {
|
|||
SyncRequestType.memoryToAssetsV1,
|
||||
SyncRequestType.stacksV1,
|
||||
SyncRequestType.partnerStacksV1,
|
||||
SyncRequestType.userMetadataV1,
|
||||
],
|
||||
).toJson(),
|
||||
);
|
||||
|
|
@ -170,6 +171,8 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
|||
SyncEntityType.partnerStackV1: SyncStackV1.fromJson,
|
||||
SyncEntityType.partnerStackBackfillV1: SyncStackV1.fromJson,
|
||||
SyncEntityType.partnerStackDeleteV1: SyncStackDeleteV1.fromJson,
|
||||
SyncEntityType.userMetadataV1: SyncUserMetadataV1.fromJson,
|
||||
SyncEntityType.userMetadataDeleteV1: SyncUserMetadataDeleteV1.fromJson,
|
||||
};
|
||||
|
||||
class _SyncAckV1 {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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/domain/models/user_metadata.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart';
|
||||
|
|
@ -14,6 +15,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.d
|
|||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart' as api show AssetVisibility, AlbumUserRole;
|
||||
|
|
@ -461,6 +463,53 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateUserMetadatasV1(
|
||||
Iterable<SyncUserMetadataV1> data,
|
||||
) async {
|
||||
try {
|
||||
await _db.batch((batch) {
|
||||
for (final userMetadata in data) {
|
||||
final companion = UserMetadataEntityCompanion(
|
||||
value: Value(userMetadata.value as Map<String, Object?>),
|
||||
);
|
||||
|
||||
batch.insert(
|
||||
_db.userMetadataEntity,
|
||||
companion.copyWith(
|
||||
userId: Value(userMetadata.userId),
|
||||
key: Value(userMetadata.key.toUserMetadataKey()),
|
||||
),
|
||||
onConflict: DoUpdate((_) => companion),
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: deleteUserMetadatasV1', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteUserMetadatasV1(
|
||||
Iterable<SyncUserMetadataDeleteV1> data,
|
||||
) async {
|
||||
try {
|
||||
await _db.batch((batch) {
|
||||
for (final userMetadata in data) {
|
||||
batch.delete(
|
||||
_db.userMetadataEntity,
|
||||
UserMetadataEntityCompanion(
|
||||
userId: Value(userMetadata.userId),
|
||||
key: Value(userMetadata.key.toUserMetadataKey()),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: deleteUserMetadatasV1', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension on AssetTypeEnum {
|
||||
|
|
@ -506,6 +555,15 @@ extension on api.AssetVisibility {
|
|||
};
|
||||
}
|
||||
|
||||
extension on String {
|
||||
UserMetadataKey toUserMetadataKey() => switch (this) {
|
||||
"onboarding" => UserMetadataKey.onboarding,
|
||||
"preferences" => UserMetadataKey.preferences,
|
||||
"license" => UserMetadataKey.license,
|
||||
_ => throw Exception('Unknown UserMetadataKey value: $this'),
|
||||
};
|
||||
}
|
||||
|
||||
extension on String {
|
||||
Duration? toDuration() {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
|
||||
class DriftUserMetadataRepository extends DriftDatabaseRepository {
|
||||
final Drift _db;
|
||||
const DriftUserMetadataRepository(this._db) : super(_db);
|
||||
|
||||
Future<List<UserMetadata>> getUserMetadata(String userId) {
|
||||
final query = _db.userMetadataEntity.select()
|
||||
..where((e) => e.userId.equals(userId));
|
||||
|
||||
return query.map((userMetadata) {
|
||||
return userMetadata.toDto();
|
||||
}).get();
|
||||
}
|
||||
}
|
||||
|
||||
extension on UserMetadataEntityData {
|
||||
UserMetadata toDto() => switch (key) {
|
||||
UserMetadataKey.onboarding => UserMetadata(
|
||||
userId: userId,
|
||||
key: key,
|
||||
onboarding: Onboarding.fromMap(value),
|
||||
),
|
||||
UserMetadataKey.preferences => UserMetadata(
|
||||
userId: userId,
|
||||
key: key,
|
||||
preferences: Preferences.fromMap(value),
|
||||
),
|
||||
UserMetadataKey.license => UserMetadata(
|
||||
userId: userId,
|
||||
key: key,
|
||||
license: License.fromMap(value),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/user_metadata.repository.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
|
||||
final userMetadataRepository = Provider<DriftUserMetadataRepository>(
|
||||
(ref) => DriftUserMetadataRepository(ref.watch(driftProvider)),
|
||||
);
|
||||
|
|
@ -101,6 +101,10 @@ void main() {
|
|||
debugLabel: any(named: 'debugLabel'),
|
||||
),
|
||||
).thenAnswer(successHandler);
|
||||
when(() => mockSyncStreamRepo.updateUserMetadatasV1(any()))
|
||||
.thenAnswer(successHandler);
|
||||
when(() => mockSyncStreamRepo.deleteUserMetadatasV1(any()))
|
||||
.thenAnswer(successHandler);
|
||||
|
||||
sut = SyncStreamService(
|
||||
syncApiRepository: mockSyncApiRepo,
|
||||
|
|
|
|||
Loading…
Reference in a new issue