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:
Daimolean 2025-07-15 22:17:35 +08:00 committed by GitHub
parent daea57f7d2
commit 0acbf1199a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 495 additions and 106 deletions

File diff suppressed because one or more lines are too long

View File

@ -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;
}
}

View File

@ -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");
}

View File

@ -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?>,
);

View File

@ -4,21 +4,24 @@ import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
as i1;
import 'package:immich_mobile/domain/models/user_metadata.model.dart' as i2;
import 'dart:typed_data' as i3;
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'
as i3;
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
as i4;
import 'package:drift/internal/modular.dart' as i5;
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
as i5;
import 'package:drift/internal/modular.dart' as i6;
typedef $$UserMetadataEntityTableCreateCompanionBuilder
= i1.UserMetadataEntityCompanion Function({
required String userId,
required i2.UserPreferences preferences,
required i2.UserMetadataKey key,
required Map<String, Object?> value,
});
typedef $$UserMetadataEntityTableUpdateCompanionBuilder
= i1.UserMetadataEntityCompanion Function({
i0.Value<String> userId,
i0.Value<i2.UserPreferences> preferences,
i0.Value<i2.UserMetadataKey> key,
i0.Value<Map<String, Object?>> value,
});
final class $$UserMetadataEntityTableReferences extends i0.BaseReferences<
@ -28,26 +31,26 @@ final class $$UserMetadataEntityTableReferences extends i0.BaseReferences<
$$UserMetadataEntityTableReferences(
super.$_db, super.$_table, super.$_typedResult);
static i4.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) =>
i5.ReadDatabaseContainer(db)
.resultSet<i4.$UserEntityTable>('user_entity')
static i5.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) =>
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
.createAlias(i0.$_aliasNameGenerator(
i5.ReadDatabaseContainer(db)
i6.ReadDatabaseContainer(db)
.resultSet<i1.$UserMetadataEntityTable>(
'user_metadata_entity')
.userId,
i5.ReadDatabaseContainer(db)
.resultSet<i4.$UserEntityTable>('user_entity')
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
.id));
i4.$$UserEntityTableProcessedTableManager get userId {
i5.$$UserEntityTableProcessedTableManager get userId {
final $_column = $_itemColumn<String>('user_id')!;
final manager = i4
final manager = i5
.$$UserEntityTableTableManager(
$_db,
i5.ReadDatabaseContainer($_db)
.resultSet<i4.$UserEntityTable>('user_entity'))
i6.ReadDatabaseContainer($_db)
.resultSet<i5.$UserEntityTable>('user_entity'))
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_userIdTable($_db));
if (item == null) return manager;
@ -65,26 +68,31 @@ class $$UserMetadataEntityTableFilterComposer
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnWithTypeConverterFilters<i2.UserPreferences, i2.UserPreferences,
String>
get preferences => $composableBuilder(
column: $table.preferences,
i0.ColumnWithTypeConverterFilters<i2.UserMetadataKey, i2.UserMetadataKey, int>
get key => $composableBuilder(
column: $table.key,
builder: (column) => i0.ColumnWithTypeConverterFilters(column));
i4.$$UserEntityTableFilterComposer get userId {
final i4.$$UserEntityTableFilterComposer composer = $composerBuilder(
i0.ColumnWithTypeConverterFilters<Map<String, Object?>, Map<String, Object>,
i3.Uint8List>
get value => $composableBuilder(
column: $table.value,
builder: (column) => i0.ColumnWithTypeConverterFilters(column));
i5.$$UserEntityTableFilterComposer get userId {
final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.userId,
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i4.$$UserEntityTableFilterComposer(
i5.$$UserEntityTableFilterComposer(
$db: $db,
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@ -103,24 +111,26 @@ class $$UserMetadataEntityTableOrderingComposer
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<String> get preferences => $composableBuilder(
column: $table.preferences,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<int> get key => $composableBuilder(
column: $table.key, builder: (column) => i0.ColumnOrderings(column));
i4.$$UserEntityTableOrderingComposer get userId {
final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder(
i0.ColumnOrderings<i3.Uint8List> get value => $composableBuilder(
column: $table.value, builder: (column) => i0.ColumnOrderings(column));
i5.$$UserEntityTableOrderingComposer get userId {
final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.userId,
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i4.$$UserEntityTableOrderingComposer(
i5.$$UserEntityTableOrderingComposer(
$db: $db,
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@ -139,24 +149,27 @@ class $$UserMetadataEntityTableAnnotationComposer
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumnWithTypeConverter<i2.UserPreferences, String>
get preferences => $composableBuilder(
column: $table.preferences, builder: (column) => column);
i0.GeneratedColumnWithTypeConverter<i2.UserMetadataKey, int> get key =>
$composableBuilder(column: $table.key, builder: (column) => column);
i4.$$UserEntityTableAnnotationComposer get userId {
final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
i0.GeneratedColumnWithTypeConverter<Map<String, Object?>, i3.Uint8List>
get value =>
$composableBuilder(column: $table.value, builder: (column) => column);
i5.$$UserEntityTableAnnotationComposer get userId {
final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.userId,
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i4.$$UserEntityTableAnnotationComposer(
i5.$$UserEntityTableAnnotationComposer(
$db: $db,
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@ -193,19 +206,23 @@ class $$UserMetadataEntityTableTableManager extends i0.RootTableManager<
$db: db, $table: table),
updateCompanionCallback: ({
i0.Value<String> userId = const i0.Value.absent(),
i0.Value<i2.UserPreferences> preferences = const i0.Value.absent(),
i0.Value<i2.UserMetadataKey> key = const i0.Value.absent(),
i0.Value<Map<String, Object?>> value = const i0.Value.absent(),
}) =>
i1.UserMetadataEntityCompanion(
userId: userId,
preferences: preferences,
key: key,
value: value,
),
createCompanionCallback: ({
required String userId,
required i2.UserPreferences preferences,
required i2.UserMetadataKey key,
required Map<String, Object?> value,
}) =>
i1.UserMetadataEntityCompanion.insert(
userId: userId,
preferences: preferences,
key: key,
value: value,
),
withReferenceMapper: (p0) => p0
.map((e) => (
@ -266,7 +283,7 @@ typedef $$UserMetadataEntityTableProcessedTableManager
i1.UserMetadataEntityData,
i0.PrefetchHooks Function({bool userId})>;
class $UserMetadataEntityTable extends i3.UserMetadataEntity
class $UserMetadataEntityTable extends i4.UserMetadataEntity
with i0.TableInfo<$UserMetadataEntityTable, i1.UserMetadataEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
@ -282,14 +299,20 @@ class $UserMetadataEntityTable extends i3.UserMetadataEntity
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE'));
@override
late final i0.GeneratedColumnWithTypeConverter<i2.UserPreferences, String>
preferences = i0.GeneratedColumn<String>(
'preferences', aliasedName, false,
type: i0.DriftSqlType.string, requiredDuringInsert: true)
.withConverter<i2.UserPreferences>(
i1.$UserMetadataEntityTable.$converterpreferences);
late final i0.GeneratedColumnWithTypeConverter<i2.UserMetadataKey, int> key =
i0.GeneratedColumn<int>('key', aliasedName, false,
type: i0.DriftSqlType.int, requiredDuringInsert: true)
.withConverter<i2.UserMetadataKey>(
i1.$UserMetadataEntityTable.$converterkey);
@override
List<i0.GeneratedColumn> get $columns => [userId, preferences];
late final i0
.GeneratedColumnWithTypeConverter<Map<String, Object?>, i3.Uint8List>
value = i0.GeneratedColumn<i3.Uint8List>('value', aliasedName, false,
type: i0.DriftSqlType.blob, requiredDuringInsert: true)
.withConverter<Map<String, Object?>>(
i1.$UserMetadataEntityTable.$convertervalue);
@override
List<i0.GeneratedColumn> get $columns => [userId, key, value];
@override
String get aliasedName => _alias ?? actualTableName;
@override
@ -311,7 +334,7 @@ class $UserMetadataEntityTable extends i3.UserMetadataEntity
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {userId};
Set<i0.GeneratedColumn> get $primaryKey => {userId, key};
@override
i1.UserMetadataEntityData map(Map<String, dynamic> data,
{String? tablePrefix}) {
@ -319,9 +342,12 @@ class $UserMetadataEntityTable extends i3.UserMetadataEntity
return i1.UserMetadataEntityData(
userId: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}user_id'])!,
preferences: i1.$UserMetadataEntityTable.$converterpreferences.fromSql(
attachedDatabase.typeMapping.read(
i0.DriftSqlType.string, data['${effectivePrefix}preferences'])!),
key: i1.$UserMetadataEntityTable.$converterkey.fromSql(attachedDatabase
.typeMapping
.read(i0.DriftSqlType.int, data['${effectivePrefix}key'])!),
value: i1.$UserMetadataEntityTable.$convertervalue.fromSql(
attachedDatabase.typeMapping
.read(i0.DriftSqlType.blob, data['${effectivePrefix}value'])!),
);
}
@ -330,8 +356,11 @@ class $UserMetadataEntityTable extends i3.UserMetadataEntity
return $UserMetadataEntityTable(attachedDatabase, alias);
}
static i0.JsonTypeConverter2<i2.UserPreferences, String, Object?>
$converterpreferences = i3.userPreferenceConverter;
static i0.JsonTypeConverter2<i2.UserMetadataKey, int, int> $converterkey =
const i0.EnumIndexConverter<i2.UserMetadataKey>(
i2.UserMetadataKey.values);
static i0.JsonTypeConverter2<Map<String, Object?>, i3.Uint8List, Object?>
$convertervalue = i4.userMetadataConverter;
@override
bool get withoutRowId => true;
@override
@ -341,16 +370,21 @@ class $UserMetadataEntityTable extends i3.UserMetadataEntity
class UserMetadataEntityData extends i0.DataClass
implements i0.Insertable<i1.UserMetadataEntityData> {
final String userId;
final i2.UserPreferences preferences;
final i2.UserMetadataKey key;
final Map<String, Object?> value;
const UserMetadataEntityData(
{required this.userId, required this.preferences});
{required this.userId, required this.key, required this.value});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['user_id'] = i0.Variable<String>(userId);
{
map['preferences'] = i0.Variable<String>(
i1.$UserMetadataEntityTable.$converterpreferences.toSql(preferences));
map['key'] = i0.Variable<int>(
i1.$UserMetadataEntityTable.$converterkey.toSql(key));
}
{
map['value'] = i0.Variable<i3.Uint8List>(
i1.$UserMetadataEntityTable.$convertervalue.toSql(value));
}
return map;
}
@ -360,8 +394,10 @@ class UserMetadataEntityData extends i0.DataClass
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return UserMetadataEntityData(
userId: serializer.fromJson<String>(json['userId']),
preferences: i1.$UserMetadataEntityTable.$converterpreferences
.fromJson(serializer.fromJson<Object?>(json['preferences'])),
key: i1.$UserMetadataEntityTable.$converterkey
.fromJson(serializer.fromJson<int>(json['key'])),
value: i1.$UserMetadataEntityTable.$convertervalue
.fromJson(serializer.fromJson<Object?>(json['value'])),
);
}
@override
@ -369,24 +405,28 @@ class UserMetadataEntityData extends i0.DataClass
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'userId': serializer.toJson<String>(userId),
'preferences': serializer.toJson<Object?>(i1
.$UserMetadataEntityTable.$converterpreferences
.toJson(preferences)),
'key': serializer
.toJson<int>(i1.$UserMetadataEntityTable.$converterkey.toJson(key)),
'value': serializer.toJson<Object?>(
i1.$UserMetadataEntityTable.$convertervalue.toJson(value)),
};
}
i1.UserMetadataEntityData copyWith(
{String? userId, i2.UserPreferences? preferences}) =>
{String? userId,
i2.UserMetadataKey? key,
Map<String, Object?>? value}) =>
i1.UserMetadataEntityData(
userId: userId ?? this.userId,
preferences: preferences ?? this.preferences,
key: key ?? this.key,
value: value ?? this.value,
);
UserMetadataEntityData copyWithCompanion(
i1.UserMetadataEntityCompanion data) {
return UserMetadataEntityData(
userId: data.userId.present ? data.userId.value : this.userId,
preferences:
data.preferences.present ? data.preferences.value : this.preferences,
key: data.key.present ? data.key.value : this.key,
value: data.value.present ? data.value.value : this.value,
);
}
@ -394,49 +434,60 @@ class UserMetadataEntityData extends i0.DataClass
String toString() {
return (StringBuffer('UserMetadataEntityData(')
..write('userId: $userId, ')
..write('preferences: $preferences')
..write('key: $key, ')
..write('value: $value')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(userId, preferences);
int get hashCode => Object.hash(userId, key, value);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.UserMetadataEntityData &&
other.userId == this.userId &&
other.preferences == this.preferences);
other.key == this.key &&
other.value == this.value);
}
class UserMetadataEntityCompanion
extends i0.UpdateCompanion<i1.UserMetadataEntityData> {
final i0.Value<String> userId;
final i0.Value<i2.UserPreferences> preferences;
final i0.Value<i2.UserMetadataKey> key;
final i0.Value<Map<String, Object?>> value;
const UserMetadataEntityCompanion({
this.userId = const i0.Value.absent(),
this.preferences = const i0.Value.absent(),
this.key = const i0.Value.absent(),
this.value = const i0.Value.absent(),
});
UserMetadataEntityCompanion.insert({
required String userId,
required i2.UserPreferences preferences,
required i2.UserMetadataKey key,
required Map<String, Object?> value,
}) : userId = i0.Value(userId),
preferences = i0.Value(preferences);
key = i0.Value(key),
value = i0.Value(value);
static i0.Insertable<i1.UserMetadataEntityData> custom({
i0.Expression<String>? userId,
i0.Expression<String>? preferences,
i0.Expression<int>? key,
i0.Expression<i3.Uint8List>? value,
}) {
return i0.RawValuesInsertable({
if (userId != null) 'user_id': userId,
if (preferences != null) 'preferences': preferences,
if (key != null) 'key': key,
if (value != null) 'value': value,
});
}
i1.UserMetadataEntityCompanion copyWith(
{i0.Value<String>? userId, i0.Value<i2.UserPreferences>? preferences}) {
{i0.Value<String>? userId,
i0.Value<i2.UserMetadataKey>? key,
i0.Value<Map<String, Object?>>? value}) {
return i1.UserMetadataEntityCompanion(
userId: userId ?? this.userId,
preferences: preferences ?? this.preferences,
key: key ?? this.key,
value: value ?? this.value,
);
}
@ -446,10 +497,13 @@ class UserMetadataEntityCompanion
if (userId.present) {
map['user_id'] = i0.Variable<String>(userId.value);
}
if (preferences.present) {
map['preferences'] = i0.Variable<String>(i1
.$UserMetadataEntityTable.$converterpreferences
.toSql(preferences.value));
if (key.present) {
map['key'] = i0.Variable<int>(
i1.$UserMetadataEntityTable.$converterkey.toSql(key.value));
}
if (value.present) {
map['value'] = i0.Variable<i3.Uint8List>(
i1.$UserMetadataEntityTable.$convertervalue.toSql(value.value));
}
return map;
}
@ -458,7 +512,8 @@ class UserMetadataEntityCompanion
String toString() {
return (StringBuffer('UserMetadataEntityCompanion(')
..write('userId: $userId, ')
..write('preferences: $preferences')
..write('key: $key, ')
..write('value: $value')
..write(')'))
.toString();
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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),
),
};
}

View File

@ -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)),
);

View File

@ -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,