diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index 4e20acb72a..085449756d 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -67,7 +67,7 @@ custom_lint: - lib/entities/*.entity.dart - lib/repositories/{album,asset,backup,database,etag,exif_info,user,timeline,partner}.repository.dart - lib/infrastructure/entities/*.entity.dart - - lib/infrastructure/repositories/{store,db,log,exif}.repository.dart + - lib/infrastructure/repositories/*.repository.dart - lib/providers/infrastructure/db.provider.dart # acceptable exceptions for the time being (until Isar is fully replaced) - lib/providers/app_life_cycle.provider.dart @@ -93,6 +93,7 @@ custom_lint: - lib/infrastructure/utils/*.converter.dart # acceptable exceptions for the time being - lib/entities/{album,asset,exif_info,user}.entity.dart # to convert DTOs to entities + - lib/infrastructure/utils/*.converter.dart - lib/utils/{image_url_builder,openapi_patching}.dart # utils are fine - test/modules/utils/openapi_patching_test.dart # filename is self-explanatory... - lib/domain/services/sync_stream.service.dart # Making sure to comply with the type from database diff --git a/mobile/lib/domain/interfaces/user.interface.dart b/mobile/lib/domain/interfaces/user.interface.dart new file mode 100644 index 0000000000..03f3ebb63e --- /dev/null +++ b/mobile/lib/domain/interfaces/user.interface.dart @@ -0,0 +1,24 @@ +import 'package:immich_mobile/domain/interfaces/db.interface.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; + +abstract interface class IUserRepository implements IDatabaseRepository { + Future insert(UserDto user); + + Future get(int id); + + Future getByUserId(String id); + + Future> getByUserIds(List ids); + + Future> getAll({SortUserBy? sortBy}); + + Future updateAll(List users); + + Future update(UserDto user); + + Future delete(List ids); + + Future deleteAll(); +} + +enum SortUserBy { id } diff --git a/mobile/lib/domain/models/store.model.dart b/mobile/lib/domain/models/store.model.dart index 06b946b3f6..8a99d68b8f 100644 --- a/mobile/lib/domain/models/store.model.dart +++ b/mobile/lib/domain/models/store.model.dart @@ -1,11 +1,11 @@ -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; /// Key for each possible value in the `Store`. /// Defines the data type for each value enum StoreKey { version._(0), assetETag._(1), - currentUser._(2), + currentUser._(2), deviceIdHash._(3), deviceId._(4), backupFailedSince._(5), diff --git a/mobile/lib/domain/models/user.model.dart b/mobile/lib/domain/models/user.model.dart new file mode 100644 index 0000000000..ceb65f313a --- /dev/null +++ b/mobile/lib/domain/models/user.model.dart @@ -0,0 +1,157 @@ +import 'dart:ui'; + +import 'package:immich_mobile/utils/hash.dart'; + +enum AvatarColor { + // do not change this order or reuse indices for other purposes, adding is OK + primary, + pink, + red, + yellow, + blue, + green, + purple, + orange, + gray, + amber; + + Color toColor({bool isDarkTheme = false}) => switch (this) { + AvatarColor.primary => + isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF), + AvatarColor.pink => const Color.fromARGB(255, 244, 114, 182), + AvatarColor.red => const Color.fromARGB(255, 239, 68, 68), + AvatarColor.yellow => const Color.fromARGB(255, 234, 179, 8), + AvatarColor.blue => const Color.fromARGB(255, 59, 130, 246), + AvatarColor.green => const Color.fromARGB(255, 22, 163, 74), + AvatarColor.purple => const Color.fromARGB(255, 147, 51, 234), + AvatarColor.orange => const Color.fromARGB(255, 234, 88, 12), + AvatarColor.gray => const Color.fromARGB(255, 75, 85, 99), + AvatarColor.amber => const Color.fromARGB(255, 217, 119, 6), + }; +} + +// TODO: Rename to User once Isar is removed +class UserDto { + final String uid; + final String email; + final String name; + final bool isAdmin; + final DateTime updatedAt; + + final String? profileImagePath; + final AvatarColor avatarColor; + + final bool memoryEnabled; + final bool inTimeline; + + final bool isPartnerSharedBy; + final bool isPartnerSharedWith; + + final int quotaUsageInBytes; + final int quotaSizeInBytes; + + int get id => fastHash(uid); + bool get hasQuota => quotaSizeInBytes > 0; + + const UserDto({ + required this.uid, + required this.email, + required this.name, + required this.isAdmin, + required this.updatedAt, + this.profileImagePath, + this.avatarColor = AvatarColor.primary, + this.memoryEnabled = true, + this.inTimeline = false, + this.isPartnerSharedBy = false, + this.isPartnerSharedWith = false, + this.quotaUsageInBytes = 0, + this.quotaSizeInBytes = 0, + }); + + @override + String toString() { + return '''User: { +id: $id, +uid: $uid, +email: $email, +name: $name, +isAdmin: $isAdmin, +updatedAt: $updatedAt, +profileImagePath: ${profileImagePath ?? ''}, +avatarColor: $avatarColor, +memoryEnabled: $memoryEnabled, +inTimeline: $inTimeline, +isPartnerSharedBy: $isPartnerSharedBy, +isPartnerSharedWith: $isPartnerSharedWith, +quotaUsageInBytes: $quotaUsageInBytes, +quotaSizeInBytes: $quotaSizeInBytes, +}'''; + } + + UserDto copyWith({ + String? uid, + String? email, + String? name, + bool? isAdmin, + DateTime? updatedAt, + String? profileImagePath, + AvatarColor? avatarColor, + bool? memoryEnabled, + bool? inTimeline, + bool? isPartnerSharedBy, + bool? isPartnerSharedWith, + int? quotaUsageInBytes, + int? quotaSizeInBytes, + }) => + UserDto( + uid: uid ?? this.uid, + email: email ?? this.email, + name: name ?? this.name, + isAdmin: isAdmin ?? this.isAdmin, + updatedAt: updatedAt ?? this.updatedAt, + profileImagePath: profileImagePath ?? this.profileImagePath, + avatarColor: avatarColor ?? this.avatarColor, + memoryEnabled: memoryEnabled ?? this.memoryEnabled, + inTimeline: inTimeline ?? this.inTimeline, + isPartnerSharedBy: isPartnerSharedBy ?? this.isPartnerSharedBy, + isPartnerSharedWith: isPartnerSharedWith ?? this.isPartnerSharedWith, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + ); + + @override + bool operator ==(covariant UserDto other) { + if (identical(this, other)) return true; + + return other.uid == uid && + other.updatedAt.isAtSameMomentAs(updatedAt) && + other.avatarColor == avatarColor && + other.email == email && + other.name == name && + other.isPartnerSharedBy == isPartnerSharedBy && + other.isPartnerSharedWith == isPartnerSharedWith && + other.profileImagePath == profileImagePath && + other.isAdmin == isAdmin && + other.memoryEnabled == memoryEnabled && + other.inTimeline == inTimeline && + other.quotaUsageInBytes == quotaUsageInBytes && + other.quotaSizeInBytes == quotaSizeInBytes; + } + + @override + int get hashCode => + uid.hashCode ^ + name.hashCode ^ + email.hashCode ^ + updatedAt.hashCode ^ + isAdmin.hashCode ^ + profileImagePath.hashCode ^ + avatarColor.hashCode ^ + memoryEnabled.hashCode ^ + inTimeline.hashCode ^ + isPartnerSharedBy.hashCode ^ + isPartnerSharedWith.hashCode ^ + quotaUsageInBytes.hashCode ^ + quotaSizeInBytes.hashCode; +} diff --git a/mobile/lib/domain/services/store.service.dart b/mobile/lib/domain/services/store.service.dart index 70b9f31c00..73426cbf4e 100644 --- a/mobile/lib/domain/services/store.service.dart +++ b/mobile/lib/domain/services/store.service.dart @@ -74,7 +74,7 @@ class StoreService { return value; } - /// Asynchronously stores the value in the DB and synchronously in the cache + /// Asynchronously stores the value in the Store Future put, T>(U key, T value) async { if (_cache[key.id] == value) return; await _storeRepository.insert(key, value); @@ -84,7 +84,7 @@ class StoreService { /// Watches a specific key for changes Stream watch(StoreKey key) => _storeRepository.watch(key); - /// Removes the value asynchronously from the DB and synchronously from the cache + /// Removes the value asynchronously from the Store Future delete(StoreKey key) async { await _storeRepository.delete(key); _cache.remove(key.id); diff --git a/mobile/lib/entities/album.entity.dart b/mobile/lib/entities/album.entity.dart index 8caff2255f..8b466da1db 100644 --- a/mobile/lib/entities/album.entity.dart +++ b/mobile/lib/entities/album.entity.dart @@ -1,7 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/utils/datetime_comparison.dart'; import 'package:isar/isar.dart'; // ignore: implementation_imports diff --git a/mobile/lib/entities/user.entity.dart b/mobile/lib/entities/user.entity.dart deleted file mode 100644 index 8fa6e83874..0000000000 --- a/mobile/lib/entities/user.entity.dart +++ /dev/null @@ -1,181 +0,0 @@ -import 'dart:ui'; - -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; -import 'package:openapi/api.dart'; - -part 'user.entity.g.dart'; - -@Collection(inheritance: false) -class User { - User({ - required this.id, - required this.updatedAt, - required this.email, - required this.name, - required this.isAdmin, - this.isPartnerSharedBy = false, - this.isPartnerSharedWith = false, - this.profileImagePath = '', - this.avatarColor = AvatarColorEnum.primary, - this.memoryEnabled = true, - this.inTimeline = false, - this.quotaUsageInBytes = 0, - this.quotaSizeInBytes = 0, - }); - - Id get isarId => fastHash(id); - - User.fromUserDto( - UserAdminResponseDto dto, - UserPreferencesResponseDto? preferences, - ) : id = dto.id, - updatedAt = dto.updatedAt, - email = dto.email, - name = dto.name, - isPartnerSharedBy = false, - isPartnerSharedWith = false, - profileImagePath = dto.profileImagePath, - isAdmin = dto.isAdmin, - memoryEnabled = preferences?.memories.enabled ?? false, - avatarColor = dto.avatarColor.toAvatarColor(), - inTimeline = false, - quotaUsageInBytes = dto.quotaUsageInBytes ?? 0, - quotaSizeInBytes = dto.quotaSizeInBytes ?? 0; - - User.fromPartnerDto(PartnerResponseDto dto) - : id = dto.id, - updatedAt = DateTime.now(), - email = dto.email, - name = dto.name, - isPartnerSharedBy = false, - isPartnerSharedWith = false, - profileImagePath = dto.profileImagePath, - isAdmin = false, - memoryEnabled = false, - avatarColor = dto.avatarColor.toAvatarColor(), - inTimeline = dto.inTimeline ?? false, - quotaUsageInBytes = 0, - quotaSizeInBytes = 0; - - /// Base user dto used where the complete user object is not required - User.fromSimpleUserDto(UserResponseDto dto) - : id = dto.id, - email = dto.email, - name = dto.name, - profileImagePath = dto.profileImagePath, - avatarColor = dto.avatarColor.toAvatarColor(), - // Fill the remaining fields with placeholders - isAdmin = false, - inTimeline = false, - memoryEnabled = false, - isPartnerSharedBy = false, - isPartnerSharedWith = false, - updatedAt = DateTime.now(), - quotaUsageInBytes = 0, - quotaSizeInBytes = 0; - - @Index(unique: true, replace: false, type: IndexType.hash) - String id; - DateTime updatedAt; - String email; - String name; - bool isPartnerSharedBy; - bool isPartnerSharedWith; - bool isAdmin; - String profileImagePath; - @Enumerated(EnumType.ordinal) - AvatarColorEnum avatarColor; - bool memoryEnabled; - bool inTimeline; - int quotaUsageInBytes; - int quotaSizeInBytes; - - bool get hasQuota => quotaSizeInBytes > 0; - @Backlink(to: 'owner') - final IsarLinks albums = IsarLinks(); - @Backlink(to: 'sharedUsers') - final IsarLinks sharedAlbums = IsarLinks(); - - @override - bool operator ==(other) { - if (other is! User) return false; - return id == other.id && - updatedAt.isAtSameMomentAs(other.updatedAt) && - avatarColor == other.avatarColor && - email == other.email && - name == other.name && - isPartnerSharedBy == other.isPartnerSharedBy && - isPartnerSharedWith == other.isPartnerSharedWith && - profileImagePath == other.profileImagePath && - isAdmin == other.isAdmin && - memoryEnabled == other.memoryEnabled && - inTimeline == other.inTimeline && - quotaUsageInBytes == other.quotaUsageInBytes && - quotaSizeInBytes == other.quotaSizeInBytes; - } - - @override - @ignore - int get hashCode => - id.hashCode ^ - updatedAt.hashCode ^ - email.hashCode ^ - name.hashCode ^ - isPartnerSharedBy.hashCode ^ - isPartnerSharedWith.hashCode ^ - profileImagePath.hashCode ^ - avatarColor.hashCode ^ - isAdmin.hashCode ^ - memoryEnabled.hashCode ^ - inTimeline.hashCode ^ - quotaUsageInBytes.hashCode ^ - quotaSizeInBytes.hashCode; -} - -enum AvatarColorEnum { - // do not change this order or reuse indices for other purposes, adding is OK - primary, - pink, - red, - yellow, - blue, - green, - purple, - orange, - gray, - amber, -} - -extension AvatarColorEnumHelper on UserAvatarColor { - AvatarColorEnum toAvatarColor() => switch (this) { - UserAvatarColor.primary => AvatarColorEnum.primary, - UserAvatarColor.pink => AvatarColorEnum.pink, - UserAvatarColor.red => AvatarColorEnum.red, - UserAvatarColor.yellow => AvatarColorEnum.yellow, - UserAvatarColor.blue => AvatarColorEnum.blue, - UserAvatarColor.green => AvatarColorEnum.green, - UserAvatarColor.purple => AvatarColorEnum.purple, - UserAvatarColor.orange => AvatarColorEnum.orange, - UserAvatarColor.gray => AvatarColorEnum.gray, - UserAvatarColor.amber => AvatarColorEnum.amber, - _ => AvatarColorEnum.primary, - }; -} - -extension AvatarColorToColorHelper on AvatarColorEnum { - Color toColor([bool isDarkTheme = false]) => switch (this) { - AvatarColorEnum.primary => - isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF), - AvatarColorEnum.pink => const Color.fromARGB(255, 244, 114, 182), - AvatarColorEnum.red => const Color.fromARGB(255, 239, 68, 68), - AvatarColorEnum.yellow => const Color.fromARGB(255, 234, 179, 8), - AvatarColorEnum.blue => const Color.fromARGB(255, 59, 130, 246), - AvatarColorEnum.green => const Color.fromARGB(255, 22, 163, 74), - AvatarColorEnum.purple => const Color.fromARGB(255, 147, 51, 234), - AvatarColorEnum.orange => const Color.fromARGB(255, 234, 88, 12), - AvatarColorEnum.gray => const Color.fromARGB(255, 75, 85, 99), - AvatarColorEnum.amber => const Color.fromARGB(255, 217, 119, 6), - }; -} diff --git a/mobile/lib/extensions/collection_extensions.dart b/mobile/lib/extensions/collection_extensions.dart index d27c9e9500..e02582588b 100644 --- a/mobile/lib/extensions/collection_extensions.dart +++ b/mobile/lib/extensions/collection_extensions.dart @@ -1,8 +1,8 @@ import 'dart:typed_data'; import 'package:collection/collection.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; extension ListExtension on List { List uniqueConsecutive({ @@ -58,11 +58,11 @@ extension AssetListExtension on Iterable { /// Returns the assets that are owned by the user passed to the [owner] param /// If [owner] is null, an empty list is returned Iterable ownedOnly( - User? owner, { + UserDto? owner, { void Function()? errorCallback, }) { if (owner == null) return []; - final userId = owner.isarId; + final userId = owner.id; final bool onlyOwned = every((e) => e.ownerId == userId); if (!onlyOwned) { if (errorCallback != null) errorCallback(); diff --git a/mobile/lib/infrastructure/entities/user.entity.dart b/mobile/lib/infrastructure/entities/user.entity.dart new file mode 100644 index 0000000000..834559b936 --- /dev/null +++ b/mobile/lib/infrastructure/entities/user.entity.dart @@ -0,0 +1,73 @@ +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/utils/hash.dart'; +import 'package:isar/isar.dart'; + +part 'user.entity.g.dart'; + +@Collection(inheritance: false) +class User { + Id get isarId => fastHash(id); + @Index(unique: true, replace: false, type: IndexType.hash) + final String id; + final DateTime updatedAt; + final String email; + final String name; + final bool isPartnerSharedBy; + final bool isPartnerSharedWith; + final bool isAdmin; + final String profileImagePath; + @Enumerated(EnumType.ordinal) + final AvatarColor avatarColor; + final bool memoryEnabled; + final bool inTimeline; + final int quotaUsageInBytes; + final int quotaSizeInBytes; + + const User({ + required this.id, + required this.updatedAt, + required this.email, + required this.name, + required this.isAdmin, + this.isPartnerSharedBy = false, + this.isPartnerSharedWith = false, + this.profileImagePath = '', + this.avatarColor = AvatarColor.primary, + this.memoryEnabled = true, + this.inTimeline = false, + this.quotaUsageInBytes = 0, + this.quotaSizeInBytes = 0, + }); + + static User fromDto(UserDto dto) => User( + id: dto.uid, + updatedAt: dto.updatedAt, + email: dto.email, + name: dto.name, + isAdmin: dto.isAdmin, + isPartnerSharedBy: dto.isPartnerSharedBy, + isPartnerSharedWith: dto.isPartnerSharedWith, + profileImagePath: dto.profileImagePath ?? "", + avatarColor: dto.avatarColor, + memoryEnabled: dto.memoryEnabled, + inTimeline: dto.inTimeline, + quotaUsageInBytes: dto.quotaUsageInBytes, + quotaSizeInBytes: dto.quotaSizeInBytes, + ); + + UserDto toDto() => UserDto( + uid: id, + email: email, + name: name, + isAdmin: isAdmin, + updatedAt: updatedAt, + profileImagePath: profileImagePath.isEmpty ? null : profileImagePath, + avatarColor: avatarColor, + memoryEnabled: memoryEnabled, + inTimeline: inTimeline, + isPartnerSharedBy: isPartnerSharedBy, + isPartnerSharedWith: isPartnerSharedWith, + quotaUsageInBytes: quotaUsageInBytes, + quotaSizeInBytes: quotaSizeInBytes, + ); +} diff --git a/mobile/lib/entities/user.entity.g.dart b/mobile/lib/infrastructure/entities/user.entity.g.dart similarity index 86% rename from mobile/lib/entities/user.entity.g.dart rename to mobile/lib/infrastructure/entities/user.entity.g.dart index a7aaee44bf..37a793b2c3 100644 --- a/mobile/lib/entities/user.entity.g.dart +++ b/mobile/lib/infrastructure/entities/user.entity.g.dart @@ -28,63 +28,58 @@ const UserSchema = CollectionSchema( name: r'email', type: IsarType.string, ), - r'hasQuota': PropertySchema( - id: 2, - name: r'hasQuota', - type: IsarType.bool, - ), r'id': PropertySchema( - id: 3, + id: 2, name: r'id', type: IsarType.string, ), r'inTimeline': PropertySchema( - id: 4, + id: 3, name: r'inTimeline', type: IsarType.bool, ), r'isAdmin': PropertySchema( - id: 5, + id: 4, name: r'isAdmin', type: IsarType.bool, ), r'isPartnerSharedBy': PropertySchema( - id: 6, + id: 5, name: r'isPartnerSharedBy', type: IsarType.bool, ), r'isPartnerSharedWith': PropertySchema( - id: 7, + id: 6, name: r'isPartnerSharedWith', type: IsarType.bool, ), r'memoryEnabled': PropertySchema( - id: 8, + id: 7, name: r'memoryEnabled', type: IsarType.bool, ), r'name': PropertySchema( - id: 9, + id: 8, name: r'name', type: IsarType.string, ), r'profileImagePath': PropertySchema( - id: 10, + id: 9, name: r'profileImagePath', type: IsarType.string, ), r'quotaSizeInBytes': PropertySchema( - id: 11, + id: 10, name: r'quotaSizeInBytes', type: IsarType.long, ), r'quotaUsageInBytes': PropertySchema( - id: 12, + id: 11, name: r'quotaUsageInBytes', type: IsarType.long, ), r'updatedAt': PropertySchema( - id: 13, + id: 12, name: r'updatedAt', type: IsarType.dateTime, ) @@ -109,22 +104,7 @@ const UserSchema = CollectionSchema( ], ) }, - links: { - r'albums': LinkSchema( - id: -8764917375410137318, - name: r'albums', - target: r'Album', - single: false, - linkName: r'owner', - ), - r'sharedAlbums': LinkSchema( - id: -7037628715076287024, - name: r'sharedAlbums', - target: r'Album', - single: false, - linkName: r'sharedUsers', - ) - }, + links: {}, embeddedSchemas: {}, getId: _userGetId, getLinks: _userGetLinks, @@ -153,18 +133,17 @@ void _userSerialize( ) { writer.writeByte(offsets[0], object.avatarColor.index); writer.writeString(offsets[1], object.email); - writer.writeBool(offsets[2], object.hasQuota); - writer.writeString(offsets[3], object.id); - writer.writeBool(offsets[4], object.inTimeline); - writer.writeBool(offsets[5], object.isAdmin); - writer.writeBool(offsets[6], object.isPartnerSharedBy); - writer.writeBool(offsets[7], object.isPartnerSharedWith); - writer.writeBool(offsets[8], object.memoryEnabled); - writer.writeString(offsets[9], object.name); - writer.writeString(offsets[10], object.profileImagePath); - writer.writeLong(offsets[11], object.quotaSizeInBytes); - writer.writeLong(offsets[12], object.quotaUsageInBytes); - writer.writeDateTime(offsets[13], object.updatedAt); + writer.writeString(offsets[2], object.id); + writer.writeBool(offsets[3], object.inTimeline); + writer.writeBool(offsets[4], object.isAdmin); + writer.writeBool(offsets[5], object.isPartnerSharedBy); + writer.writeBool(offsets[6], object.isPartnerSharedWith); + writer.writeBool(offsets[7], object.memoryEnabled); + writer.writeString(offsets[8], object.name); + writer.writeString(offsets[9], object.profileImagePath); + writer.writeLong(offsets[10], object.quotaSizeInBytes); + writer.writeLong(offsets[11], object.quotaUsageInBytes); + writer.writeDateTime(offsets[12], object.updatedAt); } User _userDeserialize( @@ -176,19 +155,19 @@ User _userDeserialize( final object = User( avatarColor: _UseravatarColorValueEnumMap[reader.readByteOrNull(offsets[0])] ?? - AvatarColorEnum.primary, + AvatarColor.primary, email: reader.readString(offsets[1]), - id: reader.readString(offsets[3]), - inTimeline: reader.readBoolOrNull(offsets[4]) ?? false, - isAdmin: reader.readBool(offsets[5]), - isPartnerSharedBy: reader.readBoolOrNull(offsets[6]) ?? false, - isPartnerSharedWith: reader.readBoolOrNull(offsets[7]) ?? false, - memoryEnabled: reader.readBoolOrNull(offsets[8]) ?? true, - name: reader.readString(offsets[9]), - profileImagePath: reader.readStringOrNull(offsets[10]) ?? '', - quotaSizeInBytes: reader.readLongOrNull(offsets[11]) ?? 0, - quotaUsageInBytes: reader.readLongOrNull(offsets[12]) ?? 0, - updatedAt: reader.readDateTime(offsets[13]), + id: reader.readString(offsets[2]), + inTimeline: reader.readBoolOrNull(offsets[3]) ?? false, + isAdmin: reader.readBool(offsets[4]), + isPartnerSharedBy: reader.readBoolOrNull(offsets[5]) ?? false, + isPartnerSharedWith: reader.readBoolOrNull(offsets[6]) ?? false, + memoryEnabled: reader.readBoolOrNull(offsets[7]) ?? true, + name: reader.readString(offsets[8]), + profileImagePath: reader.readStringOrNull(offsets[9]) ?? '', + quotaSizeInBytes: reader.readLongOrNull(offsets[10]) ?? 0, + quotaUsageInBytes: reader.readLongOrNull(offsets[11]) ?? 0, + updatedAt: reader.readDateTime(offsets[12]), ); return object; } @@ -202,32 +181,30 @@ P _userDeserializeProp

( switch (propertyId) { case 0: return (_UseravatarColorValueEnumMap[reader.readByteOrNull(offset)] ?? - AvatarColorEnum.primary) as P; + AvatarColor.primary) as P; case 1: return (reader.readString(offset)) as P; case 2: - return (reader.readBool(offset)) as P; - case 3: return (reader.readString(offset)) as P; - case 4: + case 3: return (reader.readBoolOrNull(offset) ?? false) as P; - case 5: + case 4: return (reader.readBool(offset)) as P; + case 5: + return (reader.readBoolOrNull(offset) ?? false) as P; case 6: return (reader.readBoolOrNull(offset) ?? false) as P; case 7: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 8: return (reader.readBoolOrNull(offset) ?? true) as P; - case 9: + case 8: return (reader.readString(offset)) as P; - case 10: + case 9: return (reader.readStringOrNull(offset) ?? '') as P; + case 10: + return (reader.readLongOrNull(offset) ?? 0) as P; case 11: return (reader.readLongOrNull(offset) ?? 0) as P; case 12: - return (reader.readLongOrNull(offset) ?? 0) as P; - case 13: return (reader.readDateTime(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -247,16 +224,16 @@ const _UseravatarColorEnumValueMap = { 'amber': 9, }; const _UseravatarColorValueEnumMap = { - 0: AvatarColorEnum.primary, - 1: AvatarColorEnum.pink, - 2: AvatarColorEnum.red, - 3: AvatarColorEnum.yellow, - 4: AvatarColorEnum.blue, - 5: AvatarColorEnum.green, - 6: AvatarColorEnum.purple, - 7: AvatarColorEnum.orange, - 8: AvatarColorEnum.gray, - 9: AvatarColorEnum.amber, + 0: AvatarColor.primary, + 1: AvatarColor.pink, + 2: AvatarColor.red, + 3: AvatarColor.yellow, + 4: AvatarColor.blue, + 5: AvatarColor.green, + 6: AvatarColor.purple, + 7: AvatarColor.orange, + 8: AvatarColor.gray, + 9: AvatarColor.amber, }; Id _userGetId(User object) { @@ -264,14 +241,10 @@ Id _userGetId(User object) { } List> _userGetLinks(User object) { - return [object.albums, object.sharedAlbums]; + return []; } -void _userAttach(IsarCollection col, Id id, User object) { - object.albums.attach(col, col.isar.collection(), r'albums', id); - object.sharedAlbums - .attach(col, col.isar.collection(), r'sharedAlbums', id); -} +void _userAttach(IsarCollection col, Id id, User object) {} extension UserByIndex on IsarCollection { Future getById(String id) { @@ -447,7 +420,7 @@ extension UserQueryWhere on QueryBuilder { extension UserQueryFilter on QueryBuilder { QueryBuilder avatarColorEqualTo( - AvatarColorEnum value) { + AvatarColor value) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.equalTo( property: r'avatarColor', @@ -457,7 +430,7 @@ extension UserQueryFilter on QueryBuilder { } QueryBuilder avatarColorGreaterThan( - AvatarColorEnum value, { + AvatarColor value, { bool include = false, }) { return QueryBuilder.apply(this, (query) { @@ -470,7 +443,7 @@ extension UserQueryFilter on QueryBuilder { } QueryBuilder avatarColorLessThan( - AvatarColorEnum value, { + AvatarColor value, { bool include = false, }) { return QueryBuilder.apply(this, (query) { @@ -483,8 +456,8 @@ extension UserQueryFilter on QueryBuilder { } QueryBuilder avatarColorBetween( - AvatarColorEnum lower, - AvatarColorEnum upper, { + AvatarColor lower, + AvatarColor upper, { bool includeLower = true, bool includeUpper = true, }) { @@ -627,15 +600,6 @@ extension UserQueryFilter on QueryBuilder { }); } - QueryBuilder hasQuotaEqualTo(bool value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'hasQuota', - value: value, - )); - }); - } - QueryBuilder idEqualTo( String value, { bool caseSensitive = true, @@ -1285,118 +1249,7 @@ extension UserQueryFilter on QueryBuilder { extension UserQueryObject on QueryBuilder {} -extension UserQueryLinks on QueryBuilder { - QueryBuilder albums(FilterQuery q) { - return QueryBuilder.apply(this, (query) { - return query.link(q, r'albums'); - }); - } - - QueryBuilder albumsLengthEqualTo( - int length) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'albums', length, true, length, true); - }); - } - - QueryBuilder albumsIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'albums', 0, true, 0, true); - }); - } - - QueryBuilder albumsIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'albums', 0, false, 999999, true); - }); - } - - QueryBuilder albumsLengthLessThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'albums', 0, true, length, include); - }); - } - - QueryBuilder albumsLengthGreaterThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'albums', length, include, 999999, true); - }); - } - - QueryBuilder albumsLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength( - r'albums', lower, includeLower, upper, includeUpper); - }); - } - - QueryBuilder sharedAlbums( - FilterQuery q) { - return QueryBuilder.apply(this, (query) { - return query.link(q, r'sharedAlbums'); - }); - } - - QueryBuilder sharedAlbumsLengthEqualTo( - int length) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedAlbums', length, true, length, true); - }); - } - - QueryBuilder sharedAlbumsIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedAlbums', 0, true, 0, true); - }); - } - - QueryBuilder sharedAlbumsIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedAlbums', 0, false, 999999, true); - }); - } - - QueryBuilder sharedAlbumsLengthLessThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedAlbums', 0, true, length, include); - }); - } - - QueryBuilder sharedAlbumsLengthGreaterThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedAlbums', length, include, 999999, true); - }); - } - - QueryBuilder sharedAlbumsLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength( - r'sharedAlbums', lower, includeLower, upper, includeUpper); - }); - } -} +extension UserQueryLinks on QueryBuilder {} extension UserQuerySortBy on QueryBuilder { QueryBuilder sortByAvatarColor() { @@ -1423,18 +1276,6 @@ extension UserQuerySortBy on QueryBuilder { }); } - QueryBuilder sortByHasQuota() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'hasQuota', Sort.asc); - }); - } - - QueryBuilder sortByHasQuotaDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'hasQuota', Sort.desc); - }); - } - QueryBuilder sortById() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'id', Sort.asc); @@ -1593,18 +1434,6 @@ extension UserQuerySortThenBy on QueryBuilder { }); } - QueryBuilder thenByHasQuota() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'hasQuota', Sort.asc); - }); - } - - QueryBuilder thenByHasQuotaDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'hasQuota', Sort.desc); - }); - } - QueryBuilder thenById() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'id', Sort.asc); @@ -1764,12 +1593,6 @@ extension UserQueryWhereDistinct on QueryBuilder { }); } - QueryBuilder distinctByHasQuota() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'hasQuota'); - }); - } - QueryBuilder distinctById( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { @@ -1848,7 +1671,7 @@ extension UserQueryProperty on QueryBuilder { }); } - QueryBuilder avatarColorProperty() { + QueryBuilder avatarColorProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'avatarColor'); }); @@ -1860,12 +1683,6 @@ extension UserQueryProperty on QueryBuilder { }); } - QueryBuilder hasQuotaProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'hasQuota'); - }); - } - QueryBuilder idProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'id'); diff --git a/mobile/lib/infrastructure/repositories/store.repository.dart b/mobile/lib/infrastructure/repositories/store.repository.dart index 5cf6838ee1..1e5a5335d5 100644 --- a/mobile/lib/infrastructure/repositories/store.repository.dart +++ b/mobile/lib/infrastructure/repositories/store.repository.dart @@ -1,9 +1,9 @@ import 'package:immich_mobile/domain/interfaces/store.interface.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/repositories/user.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; import 'package:isar/isar.dart'; class IsarStoreRepository extends IsarDatabaseRepository @@ -78,7 +78,7 @@ class IsarStoreRepository extends IsarDatabaseRepository const (DateTime) => entity.intValue == null ? null : DateTime.fromMillisecondsSinceEpoch(entity.intValue!), - const (User) => await UserRepository(_db).getByDbId(entity.intValue!), + const (UserDto) => await IsarUserRepository(_db).get(entity.intValue!), _ => null, } as T?; @@ -88,8 +88,8 @@ class IsarStoreRepository extends IsarDatabaseRepository const (String) => (null, value as String), const (bool) => ((value as bool) ? 1 : 0, null), const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null), - const (User) => ( - (await UserRepository(_db).update(value as User)).isarId, + const (UserDto) => ( + (await IsarUserRepository(_db).update(value as UserDto)).id, null, ), _ => throw UnsupportedError( diff --git a/mobile/lib/infrastructure/repositories/user.repository.dart b/mobile/lib/infrastructure/repositories/user.repository.dart new file mode 100644 index 0000000000..2cb8023b2a --- /dev/null +++ b/mobile/lib/infrastructure/repositories/user.repository.dart @@ -0,0 +1,80 @@ +import 'package:immich_mobile/domain/interfaces/user.interface.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart' + as entity; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:isar/isar.dart'; + +class IsarUserRepository extends IsarDatabaseRepository + implements IUserRepository { + final Isar _db; + const IsarUserRepository(super.db) : _db = db; + + @override + Future delete(List ids) async { + await transaction(() async { + await _db.users.deleteAll(ids); + }); + } + + @override + Future deleteAll() async { + await transaction(() async { + await _db.users.clear(); + }); + } + + @override + Future get(int id) async { + return (await _db.users.get(id))?.toDto(); + } + + @override + Future> getAll({SortUserBy? sortBy}) async { + return (await _db.users + .where() + .optional( + sortBy != null, + (query) => switch (sortBy!) { + SortUserBy.id => query.sortById(), + }, + ) + .findAll()) + .map((u) => u.toDto()) + .toList(); + } + + @override + Future getByUserId(String id) async { + return (await _db.users.getById(id))?.toDto(); + } + + @override + Future> getByUserIds(List ids) async { + return (await _db.users.getAllById(ids)).map((u) => u?.toDto()).toList(); + } + + @override + Future insert(UserDto user) async { + await transaction(() async { + await _db.users.put(entity.User.fromDto(user)); + }); + return true; + } + + @override + Future update(UserDto user) async { + await transaction(() async { + await _db.users.put(entity.User.fromDto(user)); + }); + return user; + } + + @override + Future updateAll(List users) async { + await transaction(() async { + await _db.users.putAll(users.map(entity.User.fromDto).toList()); + }); + return true; + } +} diff --git a/mobile/lib/infrastructure/utils/user.converter.dart b/mobile/lib/infrastructure/utils/user.converter.dart new file mode 100644 index 0000000000..11f9ddec33 --- /dev/null +++ b/mobile/lib/infrastructure/utils/user.converter.dart @@ -0,0 +1,66 @@ +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:openapi/api.dart'; + +abstract final class UserConverter { + /// Base user dto used where the complete user object is not required + static UserDto fromSimpleUserDto(UserResponseDto dto) => UserDto( + uid: dto.id, + email: dto.email, + name: dto.name, + isAdmin: false, + updatedAt: DateTime.now(), + profileImagePath: dto.profileImagePath, + avatarColor: dto.avatarColor.toAvatarColor(), + ); + + static UserDto fromAdminDto( + UserAdminResponseDto adminDto, [ + UserPreferencesResponseDto? preferenceDto, + ]) => + UserDto( + uid: adminDto.id, + email: adminDto.email, + name: adminDto.name, + isAdmin: adminDto.isAdmin, + updatedAt: adminDto.updatedAt, + profileImagePath: adminDto.profileImagePath, + avatarColor: adminDto.avatarColor.toAvatarColor(), + memoryEnabled: preferenceDto?.memories.enabled ?? true, + inTimeline: false, + isPartnerSharedBy: false, + isPartnerSharedWith: false, + quotaUsageInBytes: adminDto.quotaUsageInBytes ?? 0, + quotaSizeInBytes: adminDto.quotaSizeInBytes ?? 0, + ); + + static UserDto fromPartnerDto(PartnerResponseDto dto) => UserDto( + uid: dto.id, + email: dto.email, + name: dto.name, + isAdmin: false, + updatedAt: DateTime.now(), + profileImagePath: dto.profileImagePath, + avatarColor: dto.avatarColor.toAvatarColor(), + memoryEnabled: false, + inTimeline: dto.inTimeline ?? false, + isPartnerSharedBy: false, + isPartnerSharedWith: false, + quotaUsageInBytes: 0, + quotaSizeInBytes: 0, + ); +} + +extension on UserAvatarColor { + AvatarColor toAvatarColor() => switch (this) { + UserAvatarColor.red => AvatarColor.red, + UserAvatarColor.green => AvatarColor.green, + UserAvatarColor.blue => AvatarColor.blue, + UserAvatarColor.purple => AvatarColor.purple, + UserAvatarColor.orange => AvatarColor.orange, + UserAvatarColor.pink => AvatarColor.pink, + UserAvatarColor.amber => AvatarColor.amber, + UserAvatarColor.yellow => AvatarColor.yellow, + UserAvatarColor.gray => AvatarColor.gray, + UserAvatarColor.primary || _ => AvatarColor.primary, + }; +} diff --git a/mobile/lib/interfaces/album.interface.dart b/mobile/lib/interfaces/album.interface.dart index 3a83a8feb7..c1696eda80 100644 --- a/mobile/lib/interfaces/album.interface.dart +++ b/mobile/lib/interfaces/album.interface.dart @@ -1,6 +1,6 @@ +import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/interfaces/database.interface.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart'; @@ -31,9 +31,9 @@ abstract interface class IAlbumRepository implements IDatabaseRepository { Future count({bool? local}); - Future addUsers(Album album, List users); + Future addUsers(Album album, List users); - Future removeUsers(Album album, List users); + Future removeUsers(Album album, List users); Future addAssets(Album album, List assets); diff --git a/mobile/lib/interfaces/partner.interface.dart b/mobile/lib/interfaces/partner.interface.dart index 995e07c392..8e5fcb7a97 100644 --- a/mobile/lib/interfaces/partner.interface.dart +++ b/mobile/lib/interfaces/partner.interface.dart @@ -1,8 +1,8 @@ -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; abstract class IPartnerRepository { - Future> getSharedWith(); - Future> getSharedBy(); - Stream> watchSharedWith(); - Stream> watchSharedBy(); + Future> getSharedWith(); + Future> getSharedBy(); + Stream> watchSharedWith(); + Stream> watchSharedBy(); } diff --git a/mobile/lib/interfaces/partner_api.interface.dart b/mobile/lib/interfaces/partner_api.interface.dart index bca1baf66d..01149f473c 100644 --- a/mobile/lib/interfaces/partner_api.interface.dart +++ b/mobile/lib/interfaces/partner_api.interface.dart @@ -1,9 +1,9 @@ -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; abstract interface class IPartnerApiRepository { - Future> getAll(Direction direction); - Future create(String id); - Future update(String id, {required bool inTimeline}); + Future> getAll(Direction direction); + Future create(String id); + Future update(String id, {required bool inTimeline}); Future delete(String id); } diff --git a/mobile/lib/interfaces/user.interface.dart b/mobile/lib/interfaces/user.interface.dart deleted file mode 100644 index d099e0e50b..0000000000 --- a/mobile/lib/interfaces/user.interface.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:immich_mobile/entities/user.entity.dart'; -import 'package:immich_mobile/interfaces/database.interface.dart'; - -abstract interface class IUserRepository implements IDatabaseRepository { - Future get(String id); - - Future getByDbId(int id); - - Future> getByIds(List ids); - - Future> getAll({bool self = true, UserSort? sortBy}); - - /// Returns all users whose assets can be accessed (self+partners) - Future> getAllAccessible(); - - Future> upsertAll(List users); - - Future update(User user); - - Future deleteById(List ids); - - Future me(); - - Future clearTable(); -} - -enum UserSort { id } diff --git a/mobile/lib/interfaces/user_api.interface.dart b/mobile/lib/interfaces/user_api.interface.dart index 67ac3c0883..ebbbac8291 100644 --- a/mobile/lib/interfaces/user_api.interface.dart +++ b/mobile/lib/interfaces/user_api.interface.dart @@ -1,9 +1,9 @@ import 'dart:typed_data'; -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; abstract interface class IUserApiRepository { - Future> getAll(); + Future> getAll(); Future<({String profileImagePath})> createProfileImage({ required String name, required Uint8List data, diff --git a/mobile/lib/models/activities/activity.model.dart b/mobile/lib/models/activities/activity.model.dart index 4702753f41..17f70d5d62 100644 --- a/mobile/lib/models/activities/activity.model.dart +++ b/mobile/lib/models/activities/activity.model.dart @@ -1,4 +1,4 @@ -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; enum ActivityType { comment, like } @@ -8,7 +8,7 @@ class Activity { final String? comment; final DateTime createdAt; final ActivityType type; - final User user; + final UserDto user; const Activity({ required this.id, @@ -25,7 +25,7 @@ class Activity { String? comment, DateTime? createdAt, ActivityType? type, - User? user, + UserDto? user, }) { return Activity( id: id ?? this.id, diff --git a/mobile/lib/pages/album/album_additional_shared_user_selection.page.dart b/mobile/lib/pages/album/album_additional_shared_user_selection.page.dart index 5096a8a249..2dc41b396d 100644 --- a/mobile/lib/pages/album/album_additional_shared_user_selection.page.dart +++ b/mobile/lib/pages/album/album_additional_shared_user_selection.page.dart @@ -3,11 +3,11 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; @RoutePage() @@ -21,15 +21,15 @@ class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final AsyncValue> suggestedShareUsers = + final AsyncValue> suggestedShareUsers = ref.watch(otherUsersProvider); - final sharedUsersList = useState>({}); + final sharedUsersList = useState>({}); addNewUsersHandler() { - context.maybePop(sharedUsersList.value.map((e) => e.id).toList()); + context.maybePop(sharedUsersList.value.map((e) => e.uid).toList()); } - buildTileIcon(User user) { + buildTileIcon(UserDto user) { if (sharedUsersList.value.contains(user)) { return CircleAvatar( backgroundColor: context.primaryColor, @@ -45,7 +45,7 @@ class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget { } } - buildUserList(List users) { + buildUserList(List users) { List usersChip = []; for (var user in sharedUsersList.value) { @@ -151,7 +151,7 @@ class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget { onData: (users) { for (var sharedUsers in album.sharedUsers) { users.removeWhere( - (u) => u.id == sharedUsers.id || u.id == album.ownerId, + (u) => u.uid == sharedUsers.id || u.uid == album.ownerId, ); } diff --git a/mobile/lib/pages/album/album_options.page.dart b/mobile/lib/pages/album/album_options.page.dart index 0e9bfeb2ce..a765be50b3 100644 --- a/mobile/lib/pages/album/album_options.page.dart +++ b/mobile/lib/pages/album/album_options.page.dart @@ -4,14 +4,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart' + as entity; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/current_album.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/utils/immich_loading_overlay.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; @@ -26,7 +28,8 @@ class AlbumOptionsPage extends HookConsumerWidget { return const SizedBox(); } - final sharedUsers = useState(album.sharedUsers.toList()); + final sharedUsers = + useState(album.sharedUsers.map((u) => u.toDto()).toList()); final owner = album.owner.value; final userId = ref.watch(authProvider).userId; final activityEnabled = useState(album.activityEnabled); @@ -64,13 +67,13 @@ class AlbumOptionsPage extends HookConsumerWidget { isProcessing.value = false; } - void removeUserFromAlbum(User user) async { + void removeUserFromAlbum(UserDto user) async { isProcessing.value = true; try { await ref.read(albumProvider.notifier).removeUser(album, user); - album.sharedUsers.remove(user); - sharedUsers.value = album.sharedUsers.toList(); + album.sharedUsers.remove(entity.User.fromDto(user)); + sharedUsers.value = album.sharedUsers.map((u) => u.toDto()).toList(); } catch (error) { showErrorMessage(); } @@ -79,10 +82,10 @@ class AlbumOptionsPage extends HookConsumerWidget { isProcessing.value = false; } - void handleUserClick(User user) { + void handleUserClick(UserDto user) { var actions = []; - if (user.id == userId) { + if (user.uid == userId) { actions = [ ListTile( leading: const Icon(Icons.exit_to_app_rounded), @@ -123,8 +126,9 @@ class AlbumOptionsPage extends HookConsumerWidget { buildOwnerInfo() { return ListTile( - leading: - owner != null ? UserCircleAvatar(user: owner) : const SizedBox(), + leading: owner != null + ? UserCircleAvatar(user: owner.toDto()) + : const SizedBox(), title: Text( album.owner.value?.name ?? "", style: const TextStyle( @@ -166,10 +170,10 @@ class AlbumOptionsPage extends HookConsumerWidget { color: context.colorScheme.onSurfaceSecondary, ), ), - trailing: userId == user.id || isOwner + trailing: userId == user.uid || isOwner ? const Icon(Icons.more_horiz_rounded) : const SizedBox(), - onTap: userId == user.id || isOwner + onTap: userId == user.uid || isOwner ? () => handleUserClick(user) : null, ); diff --git a/mobile/lib/pages/album/album_shared_user_icons.dart b/mobile/lib/pages/album/album_shared_user_icons.dart index f417f9fb38..47ea476028 100644 --- a/mobile/lib/pages/album/album_shared_user_icons.dart +++ b/mobile/lib/pages/album/album_shared_user_icons.dart @@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/providers/album/current_album.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; @@ -12,7 +12,7 @@ class AlbumSharedUserIcons extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final sharedUsers = useRef>(const []); + final sharedUsers = useRef>(const []); sharedUsers.value = ref.watch( currentAlbumProvider.select((album) { if (album == null) { @@ -23,7 +23,7 @@ class AlbumSharedUserIcons extends HookConsumerWidget { return sharedUsers.value; } - return album.sharedUsers.toList(growable: false); + return album.sharedUsers.map((u) => u.toDto()).toList(growable: false); }), ); diff --git a/mobile/lib/pages/album/album_shared_user_selection.page.dart b/mobile/lib/pages/album/album_shared_user_selection.page.dart index 61ef267b99..1c797aa449 100644 --- a/mobile/lib/pages/album/album_shared_user_selection.page.dart +++ b/mobile/lib/pages/album/album_shared_user_selection.page.dart @@ -3,14 +3,14 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/album_title.provider.dart'; import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; @RoutePage() @@ -21,7 +21,7 @@ class AlbumSharedUserSelectionPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final sharedUsersList = useState>({}); + final sharedUsersList = useState>({}); final suggestedShareUsers = ref.watch(otherUsersProvider); createSharedAlbum() async { @@ -48,7 +48,7 @@ class AlbumSharedUserSelectionPage extends HookConsumerWidget { ); } - buildTileIcon(User user) { + buildTileIcon(UserDto user) { if (sharedUsersList.value.contains(user)) { return CircleAvatar( backgroundColor: context.primaryColor, @@ -64,7 +64,7 @@ class AlbumSharedUserSelectionPage extends HookConsumerWidget { } } - buildUserList(List users) { + buildUserList(List users) { List usersChip = []; for (var user in sharedUsersList.value) { diff --git a/mobile/lib/pages/albums/albums.page.dart b/mobile/lib/pages/albums/albums.page.dart index e5758c959c..acdfbf385f 100644 --- a/mobile/lib/pages/albums/albums.page.dart +++ b/mobile/lib/pages/albums/albums.page.dart @@ -33,7 +33,7 @@ class AlbumsPage extends HookConsumerWidget { final searchController = useTextEditingController(); final debounceTimer = useRef(null); final filterMode = useState(QuickFilterMode.all); - final userId = ref.watch(currentUserProvider)?.id; + final userId = ref.watch(currentUserProvider)?.uid; final searchFocusNode = useFocusNode(); toggleViewMode() { diff --git a/mobile/lib/pages/common/activities.page.dart b/mobile/lib/pages/common/activities.page.dart index 9678058111..84a8622fa5 100644 --- a/mobile/lib/pages/common/activities.page.dart +++ b/mobile/lib/pages/common/activities.page.dart @@ -7,12 +7,12 @@ import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/activities/activity.model.dart'; import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/widgets/activities/activity_text_field.dart'; -import 'package:immich_mobile/widgets/activities/activity_tile.dart'; -import 'package:immich_mobile/widgets/activities/dismissible_activity.dart'; import 'package:immich_mobile/providers/album/current_album.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/widgets/activities/activity_text_field.dart'; +import 'package:immich_mobile/widgets/activities/activity_tile.dart'; +import 'package:immich_mobile/widgets/activities/dismissible_activity.dart'; @RoutePage() class ActivitiesPage extends HookConsumerWidget { @@ -72,7 +72,7 @@ class ActivitiesPage extends HookConsumerWidget { final activity = data[index]; final canDelete = activity.user.id == user?.id || - album.ownerId == user?.id; + album.ownerId == user?.uid; return Padding( padding: const EdgeInsets.all(5), diff --git a/mobile/lib/pages/library/library.page.dart b/mobile/lib/pages/library/library.page.dart index 31b465ead7..86c411395b 100644 --- a/mobile/lib/pages/library/library.page.dart +++ b/mobile/lib/pages/library/library.page.dart @@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; @@ -163,7 +163,7 @@ class QuickAccessButtons extends ConsumerWidget { class PartnerList extends ConsumerWidget { const PartnerList({super.key, required this.partners}); - final List partners; + final List partners; @override Widget build(BuildContext context, WidgetRef ref) { diff --git a/mobile/lib/pages/library/partner/partner.page.dart b/mobile/lib/pages/library/partner/partner.page.dart index 1e9e801210..faea069c50 100644 --- a/mobile/lib/pages/library/partner/partner.page.dart +++ b/mobile/lib/pages/library/partner/partner.page.dart @@ -2,10 +2,10 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/partner.provider.dart'; import 'package:immich_mobile/services/partner.service.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/user_avatar.dart'; @@ -16,7 +16,7 @@ class PartnerPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final List partners = ref.watch(partnerSharedByProvider); + final List partners = ref.watch(partnerSharedByProvider); final availableUsers = ref.watch(partnerAvailableProvider); addNewUsersHandler() async { @@ -29,13 +29,13 @@ class PartnerPage extends HookConsumerWidget { return; } - final selectedUser = await showDialog( + final selectedUser = await showDialog( context: context, builder: (context) { return SimpleDialog( title: const Text("partner_page_select_partner").tr(), children: [ - for (User u in users) + for (UserDto u in users) SimpleDialogOption( onPressed: () => context.pop(u), child: Row( @@ -67,7 +67,7 @@ class PartnerPage extends HookConsumerWidget { } } - onDeleteUser(User u) { + onDeleteUser(UserDto u) { return showDialog( context: context, builder: (BuildContext context) { @@ -80,7 +80,7 @@ class PartnerPage extends HookConsumerWidget { ); } - buildUserList(List users) { + buildUserList(List users) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/mobile/lib/pages/library/partner/partner_detail.page.dart b/mobile/lib/pages/library/partner/partner_detail.page.dart index f018726fe2..618d31affa 100644 --- a/mobile/lib/pages/library/partner/partner_detail.page.dart +++ b/mobile/lib/pages/library/partner/partner_detail.page.dart @@ -2,11 +2,11 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/multiselect.provider.dart'; import 'package:immich_mobile/providers/partner.provider.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/timeline.provider.dart'; import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -15,7 +15,7 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart'; class PartnerDetailPage extends HookConsumerWidget { const PartnerDetailPage({super.key, required this.partner}); - final User partner; + final UserDto partner; @override Widget build(BuildContext context, WidgetRef ref) { @@ -111,7 +111,7 @@ class PartnerDetailPage extends HookConsumerWidget { ), ), ), - renderListProvider: singleUserTimelineProvider(partner.isarId), + renderListProvider: singleUserTimelineProvider(partner.id), onRefresh: () => ref.read(assetProvider.notifier).getAllAsset(), deleteEnabled: false, favoriteEnabled: false, diff --git a/mobile/lib/pages/photos/photos.page.dart b/mobile/lib/pages/photos/photos.page.dart index b3bfa366f2..c9211e984d 100644 --- a/mobile/lib/pages/photos/photos.page.dart +++ b/mobile/lib/pages/photos/photos.page.dart @@ -7,16 +7,16 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/memories/memory_lane.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; +import 'package:immich_mobile/providers/multiselect.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/providers/timeline.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; +import 'package:immich_mobile/widgets/memories/memory_lane.dart'; @RoutePage() class PhotosPage extends HookConsumerWidget { @@ -110,7 +110,7 @@ class PhotosPage extends HookConsumerWidget { : const SizedBox(), renderListProvider: timelineUsers.length > 1 ? multiUsersTimelineProvider(timelineUsers) - : singleUserTimelineProvider(currentUser?.isarId), + : singleUserTimelineProvider(currentUser?.id), buildLoadingIndicator: buildLoadingIndicator, onRefresh: refreshAssets, stackEnabled: true, diff --git a/mobile/lib/providers/album/album.provider.dart b/mobile/lib/providers/album/album.provider.dart index a2d7db68ec..39f5af7344 100644 --- a/mobile/lib/providers/album/album.provider.dart +++ b/mobile/lib/providers/album/album.provider.dart @@ -2,11 +2,11 @@ import 'dart:async'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/entities/album.entity.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; final isRefreshingRemoteAlbumProvider = StateProvider((ref) => false); @@ -88,7 +88,7 @@ class AlbumNotifier extends StateNotifier> { await albumService.addUsers(album, userIds); } - Future removeUser(Album album, User user) async { + Future removeUser(Album album, UserDto user) async { final isRemoved = await albumService.removeUser(album, user); if (isRemoved && album.sharedUsers.isEmpty) { diff --git a/mobile/lib/providers/album/suggested_shared_users.provider.dart b/mobile/lib/providers/album/suggested_shared_users.provider.dart index fe8a1fccce..75b33a9fbe 100644 --- a/mobile/lib/providers/album/suggested_shared_users.provider.dart +++ b/mobile/lib/providers/album/suggested_shared_users.provider.dart @@ -1,9 +1,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/services/user.service.dart'; -final otherUsersProvider = FutureProvider.autoDispose>((ref) { +final otherUsersProvider = + FutureProvider.autoDispose>((ref) async { UserService userService = ref.watch(userServiceProvider); + final currentUser = ref.watch(currentUserProvider); - return userService.getUsers(); + final allUsers = await userService.getAll(); + allUsers.removeWhere((u) => currentUser?.id == u.id); + return allUsers; }); diff --git a/mobile/lib/providers/auth.provider.dart b/mobile/lib/providers/auth.provider.dart index e2939e89ce..2a140911b0 100644 --- a/mobile/lib/providers/auth.provider.dart +++ b/mobile/lib/providers/auth.provider.dart @@ -2,8 +2,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_udid/flutter_udid.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/user.converter.dart'; import 'package:immich_mobile/models/auth/auth_state.model.dart'; import 'package:immich_mobile/models/auth/login_response.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; @@ -105,7 +106,7 @@ class AuthNotifier extends StateNotifier { String deviceId = Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid; - User? user = Store.tryGet(StoreKey.currentUser); + UserDto? user = Store.tryGet(StoreKey.currentUser); UserAdminResponseDto? userResponse; UserPreferencesResponseDto? userPreferences; @@ -141,18 +142,18 @@ class AuthNotifier extends StateNotifier { // If the user information is successfully retrieved, update the store // Due to the flow of the code, this will always happen on first login - if (userResponse != null) { + if (userResponse == null) { + _log.severe("Unable to get user information from the server."); + } else { await Store.put(StoreKey.deviceId, deviceId); await Store.put(StoreKey.deviceIdHash, fastHash(deviceId)); await Store.put( StoreKey.currentUser, - User.fromUserDto(userResponse, userPreferences), + UserConverter.fromAdminDto(userResponse, userPreferences), ); await Store.put(StoreKey.accessToken, accessToken); - user = User.fromUserDto(userResponse, userPreferences); - } else { - _log.severe("Unable to get user information from the server."); + user = UserConverter.fromAdminDto(userResponse, userPreferences); } // If the user is null, the login was not successful @@ -163,7 +164,7 @@ class AuthNotifier extends StateNotifier { state = state.copyWith( isAuthenticated: true, - userId: user.id, + userId: user.uid, userEmail: user.email, name: user.name, profileImagePath: user.profileImagePath, diff --git a/mobile/lib/providers/infrastructure/store.provider.dart b/mobile/lib/providers/infrastructure/store.provider.dart index 2712208e76..c7f0c04a4f 100644 --- a/mobile/lib/providers/infrastructure/store.provider.dart +++ b/mobile/lib/providers/infrastructure/store.provider.dart @@ -1,11 +1,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/interfaces/store.interface.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'store.provider.g.dart'; -@riverpod +@Riverpod(keepAlive: true) IStoreRepository storeRepository(Ref ref) => IsarStoreRepository(ref.watch(isarProvider)); + +@Riverpod(keepAlive: true) +StoreService storeService(Ref _) => StoreService.I; diff --git a/mobile/lib/providers/infrastructure/store.provider.g.dart b/mobile/lib/providers/infrastructure/store.provider.g.dart index ebf1804704..ffdcd291b6 100644 --- a/mobile/lib/providers/infrastructure/store.provider.g.dart +++ b/mobile/lib/providers/infrastructure/store.provider.g.dart @@ -6,11 +6,11 @@ part of 'store.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$storeRepositoryHash() => r'9f378b96e552151fa14a8c8ce2c30a5f38f436ed'; +String _$storeRepositoryHash() => r'99d24875d30c5e86b1c6caa352a0026167114e62'; /// See also [storeRepository]. @ProviderFor(storeRepository) -final storeRepositoryProvider = AutoDisposeProvider.internal( +final storeRepositoryProvider = Provider.internal( storeRepository, name: r'storeRepositoryProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') @@ -22,6 +22,22 @@ final storeRepositoryProvider = AutoDisposeProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element -typedef StoreRepositoryRef = AutoDisposeProviderRef; +typedef StoreRepositoryRef = ProviderRef; +String _$storeServiceHash() => r'250e10497c42df360e9e1f9a618d0b19c1b5b0a0'; + +/// See also [storeService]. +@ProviderFor(storeService) +final storeServiceProvider = Provider.internal( + storeService, + name: r'storeServiceProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$storeServiceHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef StoreServiceRef = ProviderRef; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/infrastructure/user.provider.dart b/mobile/lib/providers/infrastructure/user.provider.dart new file mode 100644 index 0000000000..6070cc6c35 --- /dev/null +++ b/mobile/lib/providers/infrastructure/user.provider.dart @@ -0,0 +1,11 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/interfaces/user.interface.dart'; +import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'user.provider.g.dart'; + +@Riverpod(keepAlive: true) +IUserRepository userRepository(Ref ref) => + IsarUserRepository(ref.watch(isarProvider)); diff --git a/mobile/lib/providers/infrastructure/user.provider.g.dart b/mobile/lib/providers/infrastructure/user.provider.g.dart new file mode 100644 index 0000000000..4ff3481412 --- /dev/null +++ b/mobile/lib/providers/infrastructure/user.provider.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user.provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$userRepositoryHash() => r'1a2ac726bcc44397dcaecf449084fefd336696d4'; + +/// See also [userRepository]. +@ProviderFor(userRepository) +final userRepositoryProvider = Provider.internal( + userRepository, + name: r'userRepositoryProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$userRepositoryHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef UserRepositoryRef = ProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/partner.provider.dart b/mobile/lib/providers/partner.provider.dart index 282e779432..f210c7fe3f 100644 --- a/mobile/lib/providers/partner.provider.dart +++ b/mobile/lib/providers/partner.provider.dart @@ -2,16 +2,16 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart'; import 'package:immich_mobile/services/partner.service.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; -class PartnerSharedWithNotifier extends StateNotifier> { +class PartnerSharedWithNotifier extends StateNotifier> { final PartnerService _partnerService; - late final StreamSubscription> streamSub; + late final StreamSubscription> streamSub; PartnerSharedWithNotifier(this._partnerService) : super([]) { - Function eq = const ListEquality().equals; + Function eq = const ListEquality().equals; _partnerService.getSharedWith().then((partners) { if (!eq(state, partners)) { state = partners; @@ -25,7 +25,7 @@ class PartnerSharedWithNotifier extends StateNotifier> { }); } - Future updatePartner(User partner, {required bool inTimeline}) { + Future updatePartner(UserDto partner, {required bool inTimeline}) { return _partnerService.updatePartner(partner, inTimeline: inTimeline); } @@ -39,18 +39,18 @@ class PartnerSharedWithNotifier extends StateNotifier> { } final partnerSharedWithProvider = - StateNotifierProvider>((ref) { + StateNotifierProvider>((ref) { return PartnerSharedWithNotifier( ref.watch(partnerServiceProvider), ); }); -class PartnerSharedByNotifier extends StateNotifier> { +class PartnerSharedByNotifier extends StateNotifier> { final PartnerService _partnerService; - late final StreamSubscription> streamSub; + late final StreamSubscription> streamSub; PartnerSharedByNotifier(this._partnerService) : super([]) { - Function eq = const ListEquality().equals; + Function eq = const ListEquality().equals; _partnerService.getSharedBy().then((partners) { if (!eq(state, partners)) { state = partners; @@ -74,15 +74,15 @@ class PartnerSharedByNotifier extends StateNotifier> { } final partnerSharedByProvider = - StateNotifierProvider>((ref) { + StateNotifierProvider>((ref) { return PartnerSharedByNotifier(ref.watch(partnerServiceProvider)); }); final partnerAvailableProvider = - FutureProvider.autoDispose>((ref) async { + FutureProvider.autoDispose>((ref) async { final otherUsers = await ref.watch(otherUsersProvider.future); final currentPartners = ref.watch(partnerSharedByProvider); - final available = Set.of(otherUsers); + final available = Set.of(otherUsers); available.removeAll(currentPartners); return available.toList(); }); diff --git a/mobile/lib/providers/user.provider.dart b/mobile/lib/providers/user.provider.dart index 0a1bc0275a..fb574fa99a 100644 --- a/mobile/lib/providers/user.provider.dart +++ b/mobile/lib/providers/user.provider.dart @@ -2,13 +2,14 @@ import 'dart:async'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/user.converter.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/timeline.service.dart'; -class CurrentUserProvider extends StateNotifier { +class CurrentUserProvider extends StateNotifier { CurrentUserProvider(this._apiService) : super(null) { state = Store.tryGet(StoreKey.currentUser); streamSub = @@ -16,7 +17,7 @@ class CurrentUserProvider extends StateNotifier { } final ApiService _apiService; - late final StreamSubscription streamSub; + late final StreamSubscription streamSub; refresh() async { try { @@ -25,7 +26,7 @@ class CurrentUserProvider extends StateNotifier { if (user != null) { await Store.put( StoreKey.currentUser, - User.fromUserDto(user, userPreferences), + UserConverter.fromAdminDto(user, userPreferences), ); } } catch (_) {} @@ -39,7 +40,7 @@ class CurrentUserProvider extends StateNotifier { } final currentUserProvider = - StateNotifierProvider((ref) { + StateNotifierProvider((ref) { return CurrentUserProvider( ref.watch(apiServiceProvider), ); diff --git a/mobile/lib/repositories/activity_api.repository.dart b/mobile/lib/repositories/activity_api.repository.dart index 8da3759709..868415caf9 100644 --- a/mobile/lib/repositories/activity_api.repository.dart +++ b/mobile/lib/repositories/activity_api.repository.dart @@ -1,5 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/user.converter.dart'; import 'package:immich_mobile/interfaces/activity_api.interface.dart'; import 'package:immich_mobile/models/activities/activity.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; @@ -60,7 +60,7 @@ class ActivityApiRepository extends ApiRepository type: dto.type == ReactionType.comment ? ActivityType.comment : ActivityType.like, - user: User.fromSimpleUserDto(dto.user), + user: UserConverter.fromSimpleUserDto(dto.user), assetId: dto.assetId, comment: dto.comment, ); diff --git a/mobile/lib/repositories/album.repository.dart b/mobile/lib/repositories/album.repository.dart index 1d2df89579..a6657f7637 100644 --- a/mobile/lib/repositories/album.repository.dart +++ b/mobile/lib/repositories/album.repository.dart @@ -1,9 +1,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart' + as entity; import 'package:immich_mobile/interfaces/album.interface.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/providers/db.provider.dart'; @@ -43,11 +45,11 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository { } if (owner == true) { query = query.owner( - (q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId), + (q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).id), ); } else if (owner == false) { query = query.owner( - (q) => q.not().isarIdEqualTo(Store.get(StoreKey.currentUser).isarId), + (q) => q.not().isarIdEqualTo(Store.get(StoreKey.currentUser).id), ); } if (remote == true) { @@ -100,8 +102,9 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository { Future get(int id) => db.albums.get(id); @override - Future removeUsers(Album album, List users) => - txn(() => album.sharedUsers.update(unlink: users)); + Future removeUsers(Album album, List users) => txn( + () => album.sharedUsers.update(unlink: users.map(entity.User.fromDto)), + ); @override Future addAssets(Album album, List assets) => @@ -121,8 +124,8 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository { } @override - Future addUsers(Album album, List users) => - txn(() => album.sharedUsers.update(link: users)); + Future addUsers(Album album, List users) => + txn(() => album.sharedUsers.update(link: users.map(entity.User.fromDto))); @override Future deleteAllLocal() => @@ -141,11 +144,11 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository { switch (filterMode) { case QuickFilterMode.sharedWithMe: query = query.owner( - (q) => q.not().isarIdEqualTo(Store.get(StoreKey.currentUser).isarId), + (q) => q.not().isarIdEqualTo(Store.get(StoreKey.currentUser).id), ); case QuickFilterMode.myAlbums: query = query.owner( - (q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId), + (q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).id), ); case QuickFilterMode.all: break; diff --git a/mobile/lib/repositories/album_api.repository.dart b/mobile/lib/repositories/album_api.repository.dart index 2438304158..a7bbe452e6 100644 --- a/mobile/lib/repositories/album_api.repository.dart +++ b/mobile/lib/repositories/album_api.repository.dart @@ -2,7 +2,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart' + as entity; +import 'package:immich_mobile/infrastructure/utils/user.converter.dart'; import 'package:immich_mobile/interfaces/album_api.interface.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/repositories/api.repository.dart'; @@ -164,11 +166,12 @@ class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository { sortOrder: dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc, ); album.remoteAssetCount = dto.assetCount; - album.owner.value = User.fromSimpleUserDto(dto.owner); + album.owner.value = + entity.User.fromDto(UserConverter.fromSimpleUserDto(dto.owner)); album.remoteThumbnailAssetId = dto.albumThumbnailAssetId; final users = dto.albumUsers - .map((albumUser) => User.fromSimpleUserDto(albumUser.user)); - album.sharedUsers.addAll(users); + .map((albumUser) => UserConverter.fromSimpleUserDto(albumUser.user)); + album.sharedUsers.addAll(users.map(entity.User.fromDto)); final assets = dto.assets.map(Asset.remote).toList(); album.assets.addAll(assets); return album; diff --git a/mobile/lib/repositories/album_media.repository.dart b/mobile/lib/repositories/album_media.repository.dart index f4f31cf14e..f08322e20a 100644 --- a/mobile/lib/repositories/album_media.repository.dart +++ b/mobile/lib/repositories/album_media.repository.dart @@ -3,6 +3,7 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/interfaces/album_media.interface.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:photo_manager/photo_manager.dart' hide AssetType; @@ -86,7 +87,7 @@ class AlbumMediaRepository implements IAlbumMediaRepository { shared: false, activityEnabled: false, ); - album.owner.value = Store.get(StoreKey.currentUser); + album.owner.value = User.fromDto(Store.get(StoreKey.currentUser)); album.localId = assetPathEntity.id; album.isAll = assetPathEntity.isAll; return album; diff --git a/mobile/lib/repositories/asset_media.repository.dart b/mobile/lib/repositories/asset_media.repository.dart index 97d22f3600..0149a8d6c6 100644 --- a/mobile/lib/repositories/asset_media.repository.dart +++ b/mobile/lib/repositories/asset_media.repository.dart @@ -24,7 +24,7 @@ class AssetMediaRepository implements IAssetMediaRepository { final Asset asset = Asset( checksum: "", localId: local.id, - ownerId: Store.get(StoreKey.currentUser).isarId, + ownerId: Store.get(StoreKey.currentUser).id, fileCreatedAt: local.createDateTime, fileModifiedAt: local.modifiedDateTime, updatedAt: local.modifiedDateTime, diff --git a/mobile/lib/repositories/auth.repository.dart b/mobile/lib/repositories/auth.repository.dart index 18e14865aa..f9e82e1635 100644 --- a/mobile/lib/repositories/auth.repository.dart +++ b/mobile/lib/repositories/auth.repository.dart @@ -6,8 +6,8 @@ import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/interfaces/auth.interface.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/providers/db.provider.dart'; diff --git a/mobile/lib/repositories/partner.repository.dart b/mobile/lib/repositories/partner.repository.dart index cae49fee39..5ea10f98c8 100644 --- a/mobile/lib/repositories/partner.repository.dart +++ b/mobile/lib/repositories/partner.repository.dart @@ -1,5 +1,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart' + as entity; import 'package:immich_mobile/interfaces/partner.interface.dart'; import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/repositories/database.repository.dart'; @@ -14,34 +16,40 @@ class PartnerRepository extends DatabaseRepository PartnerRepository(super.db); @override - Future> getSharedBy() { - return db.users - .filter() - .isPartnerSharedByEqualTo(true) - .sortById() - .findAll(); + Future> getSharedBy() async { + return (await db.users + .filter() + .isPartnerSharedByEqualTo(true) + .sortById() + .findAll()) + .map((u) => u.toDto()) + .toList(); } @override - Future> getSharedWith() { - return db.users - .filter() - .isPartnerSharedWithEqualTo(true) - .sortById() - .findAll(); + Future> getSharedWith() async { + return (await db.users + .filter() + .isPartnerSharedWithEqualTo(true) + .sortById() + .findAll()) + .map((u) => u.toDto()) + .toList(); } @override - Stream> watchSharedBy() { - return db.users.filter().isPartnerSharedByEqualTo(true).sortById().watch(); + Stream> watchSharedBy() { + return (db.users.filter().isPartnerSharedByEqualTo(true).sortById().watch()) + .map((users) => users.map((u) => u.toDto()).toList()); } @override - Stream> watchSharedWith() { - return db.users - .filter() - .isPartnerSharedWithEqualTo(true) - .sortById() - .watch(); + Stream> watchSharedWith() { + return (db.users + .filter() + .isPartnerSharedWithEqualTo(true) + .sortById() + .watch()) + .map((users) => users.map((u) => u.toDto()).toList()); } } diff --git a/mobile/lib/repositories/partner_api.repository.dart b/mobile/lib/repositories/partner_api.repository.dart index 1ae16d9d52..367e2a58d7 100644 --- a/mobile/lib/repositories/partner_api.repository.dart +++ b/mobile/lib/repositories/partner_api.repository.dart @@ -1,5 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/infrastructure/utils/user.converter.dart'; import 'package:immich_mobile/interfaces/partner_api.interface.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/repositories/api.repository.dart'; @@ -18,7 +19,7 @@ class PartnerApiRepository extends ApiRepository PartnerApiRepository(this._api); @override - Future> getAll(Direction direction) async { + Future> getAll(Direction direction) async { final response = await checkNull( _api.getPartners( direction == Direction.sharedByMe @@ -26,26 +27,26 @@ class PartnerApiRepository extends ApiRepository : PartnerDirection.with_, ), ); - return response.map(User.fromPartnerDto).toList(); + return response.map(UserConverter.fromPartnerDto).toList(); } @override - Future create(String id) async { + Future create(String id) async { final dto = await checkNull(_api.createPartner(id)); - return User.fromPartnerDto(dto); + return UserConverter.fromPartnerDto(dto); } @override Future delete(String id) => _api.removePartner(id); @override - Future update(String id, {required bool inTimeline}) async { + Future update(String id, {required bool inTimeline}) async { final dto = await checkNull( _api.updatePartner( id, UpdatePartnerDto(inTimeline: inTimeline), ), ); - return User.fromPartnerDto(dto); + return UserConverter.fromPartnerDto(dto); } } diff --git a/mobile/lib/repositories/timeline.repository.dart b/mobile/lib/repositories/timeline.repository.dart index 1b9ee8ad37..1b0471059f 100644 --- a/mobile/lib/repositories/timeline.repository.dart +++ b/mobile/lib/repositories/timeline.repository.dart @@ -2,7 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/interfaces/timeline.interface.dart'; import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/repositories/database.repository.dart'; diff --git a/mobile/lib/repositories/user.repository.dart b/mobile/lib/repositories/user.repository.dart deleted file mode 100644 index ea67b30e0d..0000000000 --- a/mobile/lib/repositories/user.repository.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; -import 'package:immich_mobile/interfaces/user.interface.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:isar/isar.dart'; - -final userRepositoryProvider = - Provider((ref) => UserRepository(ref.watch(dbProvider))); - -class UserRepository extends DatabaseRepository implements IUserRepository { - UserRepository(super.db); - - @override - Future> getByIds(List ids) async => - (await db.users.getAllById(ids)).nonNulls.toList(); - - @override - Future get(String id) => db.users.getById(id); - - @override - Future> getAll({bool self = true, UserSort? sortBy}) { - final baseQuery = db.users.where(); - final int userId = Store.get(StoreKey.currentUser).isarId; - final QueryBuilder afterWhere = - self ? baseQuery.noOp() : baseQuery.isarIdNotEqualTo(userId); - final QueryBuilder query = switch (sortBy) { - null => afterWhere.noOp(), - UserSort.id => afterWhere.sortById(), - }; - return query.findAll(); - } - - @override - Future update(User user) async { - await txn(() => db.users.put(user)); - return user; - } - - @override - Future me() => Future.value(Store.get(StoreKey.currentUser)); - - @override - Future deleteById(List ids) => txn(() => db.users.deleteAll(ids)); - - @override - Future> upsertAll(List users) async { - await txn(() => db.users.putAll(users)); - return users; - } - - @override - Future> getAllAccessible() => db.users - .filter() - .isPartnerSharedWithEqualTo(true) - .or() - .isarIdEqualTo(Store.get(StoreKey.currentUser).isarId) - .findAll(); - - @override - Future getByDbId(int id) async { - return await db.users.get(id); - } - - @override - Future clearTable() async { - await txn(() async { - await db.users.clear(); - }); - } -} diff --git a/mobile/lib/repositories/user_api.repository.dart b/mobile/lib/repositories/user_api.repository.dart index 9641c4e0e6..1b72186c7e 100644 --- a/mobile/lib/repositories/user_api.repository.dart +++ b/mobile/lib/repositories/user_api.repository.dart @@ -2,7 +2,8 @@ import 'dart:typed_data'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:http/http.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/infrastructure/utils/user.converter.dart'; import 'package:immich_mobile/interfaces/user_api.interface.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/repositories/api.repository.dart'; @@ -20,9 +21,9 @@ class UserApiRepository extends ApiRepository implements IUserApiRepository { UserApiRepository(this._api); @override - Future> getAll() async { + Future> getAll() async { final dto = await checkNull(_api.searchUsers()); - return dto.map(User.fromSimpleUserDto).toList(); + return dto.map(UserConverter.fromSimpleUserDto).toList(); } @override diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index cd7a6f6b98..d7edc6fd28 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -2,11 +2,10 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/models/folder/recursive_folder.model.dart'; -import 'package:immich_mobile/pages/library/folder/folder.page.dart'; import 'package:immich_mobile/models/memories/memory.model.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/models/shared_link/shared_link.model.dart'; @@ -37,6 +36,7 @@ import 'package:immich_mobile/pages/editing/edit.page.dart'; import 'package:immich_mobile/pages/editing/filter.page.dart'; import 'package:immich_mobile/pages/library/archive.page.dart'; import 'package:immich_mobile/pages/library/favorite.page.dart'; +import 'package:immich_mobile/pages/library/folder/folder.page.dart'; import 'package:immich_mobile/pages/library/library.page.dart'; import 'package:immich_mobile/pages/library/local_albums.page.dart'; import 'package:immich_mobile/pages/library/partner/partner.page.dart'; diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index f2f169247e..a78371e05e 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -1162,7 +1162,7 @@ class NativeVideoViewerRouteArgs { class PartnerDetailRoute extends PageRouteInfo { PartnerDetailRoute({ Key? key, - required User partner, + required UserDto partner, List? children, }) : super( PartnerDetailRoute.name, @@ -1195,7 +1195,7 @@ class PartnerDetailRouteArgs { final Key? key; - final User partner; + final UserDto partner; @override String toString() { diff --git a/mobile/lib/routing/tab_navigation_observer.dart b/mobile/lib/routing/tab_navigation_observer.dart index b6a845a0b3..edbfe6da4c 100644 --- a/mobile/lib/routing/tab_navigation_observer.dart +++ b/mobile/lib/routing/tab_navigation_observer.dart @@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/user.converter.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/memory.provider.dart'; @@ -39,7 +39,7 @@ class TabNavigationObserver extends AutoRouterObserver { await Store.put( StoreKey.currentUser, - User.fromUserDto(userResponseDto, userPreferences), + UserConverter.fromAdminDto(userResponseDto, userPreferences), ); ref.read(serverInfoProvider.notifier).getServerVersion(); } catch (e) { diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart index 3a44ca7286..057bec307c 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -7,11 +7,13 @@ import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart' + as entity; import 'package:immich_mobile/interfaces/album.interface.dart'; import 'package:immich_mobile/interfaces/album_api.interface.dart'; import 'package:immich_mobile/interfaces/album_media.interface.dart'; @@ -202,12 +204,12 @@ class AlbumService { Future createAlbum( String albumName, Iterable assets, [ - Iterable sharedUsers = const [], + Iterable sharedUsers = const [], ]) async { final Album album = await _albumApiRepository.create( albumName, assetIds: assets.map((asset) => asset.remoteId!), - sharedUserIds: sharedUsers.map((user) => user.id), + sharedUserIds: sharedUsers.map((user) => user.uid), ); await _entityService.fillAlbumWithDatabaseEntities(album); return _albumRepository.create(album); @@ -294,7 +296,7 @@ class AlbumService { Future deleteAlbum(Album album) async { try { - final userId = Store.get(StoreKey.currentUser).isarId; + final userId = Store.get(StoreKey.currentUser).id; if (album.owner.value?.isarId == userId) { await _albumApiRepository.delete(album.remoteId!); } @@ -356,15 +358,15 @@ class AlbumService { Future removeUser( Album album, - User user, + UserDto user, ) async { try { await _albumApiRepository.removeUser( album.remoteId!, - userId: user.id, + userId: user.uid, ); - album.sharedUsers.remove(user); + album.sharedUsers.remove(entity.User.fromDto(user)); await _albumRepository.removeUsers(album, [user]); final a = await _albumRepository.get(album.id); // trigger watcher @@ -388,7 +390,10 @@ class AlbumService { album.sharedUsers.addAll(updatedAlbum.remoteUsers); album.shared = true; - await _albumRepository.addUsers(album, album.sharedUsers.toList()); + await _albumRepository.addUsers( + album, + album.sharedUsers.map((u) => u.toDto()).toList(), + ); await _albumRepository.update(album); return true; diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index 815962efac..f61fdfbaeb 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -5,24 +5,27 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/interfaces/exif.interface.dart'; +import 'package:immich_mobile/domain/interfaces/user.interface.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/interfaces/asset_api.interface.dart'; import 'package:immich_mobile/interfaces/asset_media.interface.dart'; import 'package:immich_mobile/interfaces/backup_album.interface.dart'; import 'package:immich_mobile/interfaces/etag.interface.dart'; -import 'package:immich_mobile/interfaces/user.interface.dart'; import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/repositories/backup.repository.dart'; import 'package:immich_mobile/repositories/etag.repository.dart'; -import 'package:immich_mobile/repositories/user.repository.dart'; import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/backup.service.dart'; @@ -45,6 +48,7 @@ final assetServiceProvider = Provider( ref.watch(userServiceProvider), ref.watch(backupServiceProvider), ref.watch(albumServiceProvider), + ref.watch(storeServiceProvider), ref.watch(assetMediaRepositoryProvider), ), ); @@ -61,6 +65,7 @@ class AssetService { final UserService _userService; final BackupService _backupService; final AlbumService _albumService; + final StoreService _storeService; final IAssetMediaRepository _assetMediaRepository; final log = Logger('AssetService'); @@ -76,6 +81,7 @@ class AssetService { this._userService, this._backupService, this._albumService, + this._storeService, this._assetMediaRepository, ); @@ -83,9 +89,9 @@ class AssetService { /// required. Returns `true` if there were any changes. Future refreshRemoteAssets() async { final syncedUserIds = await _etagRepository.getAllIds(); - final List syncedUsers = syncedUserIds.isEmpty + final List syncedUsers = syncedUserIds.isEmpty ? [] - : await _userRepository.getByIds(syncedUserIds); + : (await _userRepository.getByUserIds(syncedUserIds)).nonNulls.toList(); final Stopwatch sw = Stopwatch()..start(); final bool changes = await _syncService.syncRemoteAssetsToDb( users: syncedUsers, @@ -99,10 +105,10 @@ class AssetService { /// Returns `(null, null)` if changes are invalid -> requires full sync Future<(List? toUpsert, List? toDelete)> - _getRemoteAssetChanges(List users, DateTime since) async { + _getRemoteAssetChanges(List users, DateTime since) async { final dto = AssetDeltaSyncDto( updatedAfter: since, - userIds: users.map((e) => e.id).toList(), + userIds: users.map((e) => e.uid).toList(), ); final changes = await _apiService.syncApi.getDeltaSync(dto); return changes == null || changes.needsFullSync @@ -132,7 +138,7 @@ class AssetService { } /// Returns `null` if the server state did not change, else list of assets - Future?> _getRemoteAssets(User user, DateTime until) async { + Future?> _getRemoteAssets(UserDto user, DateTime until) async { const int chunkSize = 10000; try { final List allAssets = []; @@ -143,7 +149,7 @@ class AssetService { limit: chunkSize, updatedUntil: until, lastId: lastId, - userId: user.id, + userId: user.uid, ); log.fine("Requesting $chunkSize assets from $lastId"); final List? assets = @@ -314,9 +320,9 @@ class AssetService { ); await refreshRemoteAssets(); - final owner = await _userRepository.me(); + final owner = _storeService.get(StoreKey.currentUser); final remoteAssets = await _assetRepository.getAll( - ownerId: owner.isarId, + ownerId: owner.id, state: AssetState.merged, ); @@ -519,13 +525,13 @@ class AssetService { return _assetRepository.watchAsset(id, fireImmediately: fireImmediately); } - Future> getRecentlyAddedAssets() async { - final me = await _userRepository.me(); - return _assetRepository.getRecentlyAddedAssets(me.isarId); + Future> getRecentlyAddedAssets() { + final me = _storeService.get(StoreKey.currentUser); + return _assetRepository.getRecentlyAddedAssets(me.id); } - Future> getMotionAssets() async { - final me = await _userRepository.me(); - return _assetRepository.getMotionAssets(me.isarId); + Future> getMotionAssets() { + final me = _storeService.get(StoreKey.currentUser); + return _assetRepository.getMotionAssets(me.id); } } diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart index fe483384e5..ad281e4a0b 100644 --- a/mobile/lib/services/background.service.dart +++ b/mobile/lib/services/background.service.dart @@ -12,11 +12,15 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/interfaces/exif.interface.dart'; +import 'package:immich_mobile/domain/interfaces/user.interface.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; import 'package:immich_mobile/interfaces/backup_album.interface.dart'; +import 'package:immich_mobile/interfaces/partner.interface.dart'; import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; @@ -32,9 +36,9 @@ import 'package:immich_mobile/repositories/backup.repository.dart'; import 'package:immich_mobile/repositories/etag.repository.dart'; import 'package:immich_mobile/repositories/file_media.repository.dart'; import 'package:immich_mobile/repositories/network.repository.dart'; +import 'package:immich_mobile/repositories/partner.repository.dart'; import 'package:immich_mobile/repositories/partner_api.repository.dart'; import 'package:immich_mobile/repositories/permission.repository.dart'; -import 'package:immich_mobile/repositories/user.repository.dart'; import 'package:immich_mobile/repositories/user_api.repository.dart'; import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/api.service.dart'; @@ -385,7 +389,7 @@ class BackgroundService { AlbumMediaRepository albumMediaRepository = AlbumMediaRepository(); FileMediaRepository fileMediaRepository = FileMediaRepository(); AssetMediaRepository assetMediaRepository = AssetMediaRepository(); - UserRepository userRepository = UserRepository(db); + IUserRepository userRepository = IsarUserRepository(db); UserApiRepository userApiRepository = UserApiRepository(apiService.usersApi); AlbumApiRepository albumApiRepository = @@ -396,6 +400,7 @@ class BackgroundService { HashService(assetRepository, this, albumMediaRepository); EntityService entityService = EntityService(assetRepository, userRepository); + IPartnerRepository partnerRepository = PartnerRepository(db); SyncService syncSerive = SyncService( hashService, entityService, @@ -404,7 +409,9 @@ class BackgroundService { albumRepository, assetRepository, exifInfoRepository, + partnerRepository, userRepository, + StoreService.I, eTagRepository, ); UserService userService = UserService( diff --git a/mobile/lib/services/backup_verification.service.dart b/mobile/lib/services/backup_verification.service.dart index c2e93a678a..e4d5ab4afd 100644 --- a/mobile/lib/services/backup_verification.service.dart +++ b/mobile/lib/services/backup_verification.service.dart @@ -34,7 +34,7 @@ class BackupVerificationService { /// Returns at most [limit] assets that were backed up without exif Future> findWronglyBackedUpAssets({int limit = 100}) async { - final owner = Store.get(StoreKey.currentUser).isarId; + final owner = Store.get(StoreKey.currentUser).id; final List onlyLocal = await _assetRepository.getAll( ownerId: owner, state: AssetState.local, diff --git a/mobile/lib/services/entity.service.dart b/mobile/lib/services/entity.service.dart index ddbe77f8c9..9e61366b94 100644 --- a/mobile/lib/services/entity.service.dart +++ b/mobile/lib/services/entity.service.dart @@ -1,9 +1,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/interfaces/user.interface.dart'; import 'package:immich_mobile/entities/album.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/interfaces/asset.interface.dart'; -import 'package:immich_mobile/interfaces/user.interface.dart'; +import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/user.repository.dart'; class EntityService { final IAssetRepository _assetRepository; @@ -17,7 +18,8 @@ class EntityService { final ownerId = album.ownerId; if (ownerId != null) { // replace owner with user from database - album.owner.value = await _userRepository.get(ownerId); + final user = await _userRepository.getByUserId(ownerId); + album.owner.value = user == null ? null : User.fromDto(user); } final thumbnailAssetId = album.remoteThumbnailAssetId ?? album.thumbnail.value?.remoteId; @@ -29,9 +31,9 @@ class EntityService { if (album.remoteUsers.isNotEmpty) { // replace all users with users from database final users = await _userRepository - .getByIds(album.remoteUsers.map((user) => user.id).toList()); + .getByUserIds(album.remoteUsers.map((user) => user.id).toList()); album.sharedUsers.clear(); - album.sharedUsers.addAll(users); + album.sharedUsers.addAll(users.nonNulls.map(User.fromDto)); album.shared = true; } if (album.remoteAssets.isNotEmpty) { diff --git a/mobile/lib/services/partner.service.dart b/mobile/lib/services/partner.service.dart index 6bd429b51d..cc3631a0b1 100644 --- a/mobile/lib/services/partner.service.dart +++ b/mobile/lib/services/partner.service.dart @@ -1,11 +1,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/domain/interfaces/user.interface.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/interfaces/partner.interface.dart'; import 'package:immich_mobile/interfaces/partner_api.interface.dart'; -import 'package:immich_mobile/interfaces/user.interface.dart'; +import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/repositories/partner.repository.dart'; import 'package:immich_mobile/repositories/partner_api.repository.dart'; -import 'package:immich_mobile/repositories/user.repository.dart'; import 'package:logging/logging.dart'; final partnerServiceProvider = Provider( @@ -28,57 +28,58 @@ class PartnerService { this._partnerRepository, ); - Future> getSharedWith() async { + Future> getSharedWith() async { return _partnerRepository.getSharedWith(); } - Future> getSharedBy() async { + Future> getSharedBy() async { return _partnerRepository.getSharedBy(); } - Stream> watchSharedWith() { + Stream> watchSharedWith() { return _partnerRepository.watchSharedWith(); } - Stream> watchSharedBy() { + Stream> watchSharedBy() { return _partnerRepository.watchSharedBy(); } - Future removePartner(User partner) async { + Future removePartner(UserDto partner) async { try { - await _partnerApiRepository.delete(partner.id); - partner.isPartnerSharedBy = false; - await _userRepository.update(partner); + await _partnerApiRepository.delete(partner.uid); + await _userRepository.update(partner.copyWith(isPartnerSharedBy: false)); } catch (e) { - _log.warning("Failed to remove partner ${partner.id}", e); + _log.warning("Failed to remove partner ${partner.uid}", e); return false; } return true; } - Future addPartner(User partner) async { + Future addPartner(UserDto partner) async { try { - await _partnerApiRepository.create(partner.id); - partner.isPartnerSharedBy = true; - await _userRepository.update(partner); + await _partnerApiRepository.create(partner.uid); + await _userRepository.update(partner.copyWith(isPartnerSharedBy: true)); return true; } catch (e) { - _log.warning("Failed to add partner ${partner.id}", e); + _log.warning("Failed to add partner ${partner.uid}", e); } return false; } - Future updatePartner(User partner, {required bool inTimeline}) async { + Future updatePartner( + UserDto partner, { + required bool inTimeline, + }) async { try { final dto = await _partnerApiRepository.update( - partner.id, + partner.uid, inTimeline: inTimeline, ); - partner.inTimeline = dto.inTimeline; - await _userRepository.update(partner); + await _userRepository + .update(partner.copyWith(inTimeline: dto.inTimeline)); return true; } catch (e) { - _log.warning("Failed to update partner ${partner.id}", e); + _log.warning("Failed to update partner ${partner.uid}", e); } return false; } diff --git a/mobile/lib/services/sync.service.dart b/mobile/lib/services/sync.service.dart index b937dde320..b3edb17c89 100644 --- a/mobile/lib/services/sync.service.dart +++ b/mobile/lib/services/sync.service.dart @@ -3,24 +3,29 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/interfaces/exif.interface.dart'; +import 'package:immich_mobile/domain/interfaces/user.interface.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/extensions/collection_extensions.dart'; import 'package:immich_mobile/interfaces/album.interface.dart'; import 'package:immich_mobile/interfaces/album_api.interface.dart'; import 'package:immich_mobile/interfaces/album_media.interface.dart'; import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/interfaces/etag.interface.dart'; -import 'package:immich_mobile/interfaces/user.interface.dart'; +import 'package:immich_mobile/interfaces/partner.interface.dart'; import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/repositories/album.repository.dart'; import 'package:immich_mobile/repositories/album_api.repository.dart'; import 'package:immich_mobile/repositories/album_media.repository.dart'; import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/repositories/etag.repository.dart'; -import 'package:immich_mobile/repositories/user.repository.dart'; +import 'package:immich_mobile/repositories/partner.repository.dart'; import 'package:immich_mobile/services/entity.service.dart'; import 'package:immich_mobile/services/hash.service.dart'; import 'package:immich_mobile/utils/async_mutex.dart'; @@ -37,7 +42,9 @@ final syncServiceProvider = Provider( ref.watch(albumRepositoryProvider), ref.watch(assetRepositoryProvider), ref.watch(exifRepositoryProvider), + ref.watch(partnerRepositoryProvider), ref.watch(userRepositoryProvider), + ref.watch(storeServiceProvider), ref.watch(etagRepositoryProvider), ), ); @@ -51,6 +58,8 @@ class SyncService { final IAssetRepository _assetRepository; final IExifInfoRepository _exifInfoRepository; final IUserRepository _userRepository; + final IPartnerRepository _partnerRepository; + final StoreService _storeService; final IETagRepository _eTagRepository; final AsyncMutex _lock = AsyncMutex(); final Logger _log = Logger('SyncService'); @@ -63,7 +72,9 @@ class SyncService { this._albumRepository, this._assetRepository, this._exifInfoRepository, + this._partnerRepository, this._userRepository, + this._storeService, this._eTagRepository, ); @@ -71,20 +82,20 @@ class SyncService { /// Syncs users from the server to the local database /// Returns `true`if there were any changes - Future syncUsersFromServer(List users) => + Future syncUsersFromServer(List users) => _lock.run(() => _syncUsersFromServer(users)); /// Syncs remote assets owned by the logged-in user to the DB /// Returns `true` if there were any changes Future syncRemoteAssetsToDb({ - required List users, + required List users, required Future<(List? toUpsert, List? toDelete)> Function( - List users, + List users, DateTime since, ) getChangedAssets, - required FutureOr?> Function(User user, DateTime until) + required FutureOr?> Function(UserDto user, DateTime until) loadAssets, - required FutureOr?> Function() refreshUsers, + required FutureOr?> Function() refreshUsers, }) => _lock.run( () async => @@ -134,16 +145,16 @@ class SyncService { /// Syncs users from the server to the local database /// Returns `true`if there were any changes - Future _syncUsersFromServer(List users) async { - users.sortBy((u) => u.id); - final dbUsers = await _userRepository.getAll(sortBy: UserSort.id); + Future _syncUsersFromServer(List users) async { + users.sortBy((u) => u.uid); + final dbUsers = await _userRepository.getAll(sortBy: SortUserBy.id); final List toDelete = []; - final List toUpsert = []; + final List toUpsert = []; final changes = diffSortedListsSync( users, dbUsers, - compare: (User a, User b) => a.id.compareTo(b.id), - both: (User a, User b) { + compare: (UserDto a, UserDto b) => a.uid.compareTo(b.uid), + both: (UserDto a, UserDto b) { if (!a.updatedAt.isAtSameMomentAs(b.updatedAt) || a.isPartnerSharedBy != b.isPartnerSharedBy || a.isPartnerSharedWith != b.isPartnerSharedWith || @@ -153,13 +164,13 @@ class SyncService { } return false; }, - onlyFirst: (User a) => toUpsert.add(a), - onlySecond: (User b) => toDelete.add(b.isarId), + onlyFirst: (UserDto a) => toUpsert.add(a), + onlySecond: (UserDto b) => toDelete.add(b.id), ); if (changes) { await _userRepository.transaction(() async { - await _userRepository.deleteById(toDelete); - await _userRepository.upsertAll(toUpsert); + await _userRepository.delete(toDelete); + await _userRepository.updateAll(toUpsert); }); } return changes; @@ -185,15 +196,15 @@ class SyncService { /// Efficiently syncs assets via changes. Returns `null` when a full sync is required. Future _syncRemoteAssetChanges( - List users, + List users, Future<(List? toUpsert, List? toDelete)> Function( - List users, + List users, DateTime since, ) getChangedAssets, ) async { - final currentUser = await _userRepository.me(); + final currentUser = _storeService.get(StoreKey.currentUser); final DateTime? since = - (await _eTagRepository.get(currentUser.isarId))?.time?.toUtc(); + (await _eTagRepository.get(currentUser.id))?.time?.toUtc(); if (since == null) return null; final DateTime now = DateTime.now(); final (toUpsert, toDelete) = await getChangedAssets(users, since); @@ -240,10 +251,16 @@ class SyncService { }); } + Future> _getAllAccessibleUsers() async { + final sharedWith = (await _partnerRepository.getSharedWith()).toSet(); + sharedWith.add(_storeService.get(StoreKey.currentUser)); + return sharedWith.toList(); + } + /// Syncs assets by loading and comparing all assets from the server. Future _syncRemoteAssetsFull( - FutureOr?> Function() refreshUsers, - FutureOr?> Function(User user, DateTime until) loadAssets, + FutureOr?> Function() refreshUsers, + FutureOr?> Function(UserDto user, DateTime until) loadAssets, ) async { final serverUsers = await refreshUsers(); if (serverUsers == null) { @@ -251,17 +268,17 @@ class SyncService { return false; } await _syncUsersFromServer(serverUsers); - final List users = await _userRepository.getAllAccessible(); + final List users = await _getAllAccessibleUsers(); bool changes = false; - for (User u in users) { + for (UserDto u in users) { changes |= await _syncRemoteAssetsForUser(u, loadAssets); } return changes; } Future _syncRemoteAssetsForUser( - User user, - FutureOr?> Function(User user, DateTime until) loadAssets, + UserDto user, + FutureOr?> Function(UserDto user, DateTime until) loadAssets, ) async { final DateTime now = DateTime.now().toUtc(); final List? remote = await loadAssets(user, now); @@ -269,7 +286,7 @@ class SyncService { return false; } final List inDb = await _assetRepository.getAll( - ownerId: user.isarId, + ownerId: user.id, sortBy: AssetSort.checksum, ); assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!"); @@ -295,13 +312,13 @@ class SyncService { return true; } - Future _updateUserAssetsETag(List users, DateTime time) { - final etags = users.map((u) => ETag(id: u.id, time: time)).toList(); + Future _updateUserAssetsETag(List users, DateTime time) { + final etags = users.map((u) => ETag(id: u.uid, time: time)).toList(); return _eTagRepository.upsertAll(etags); } - Future _clearUserAssetsETag(List users) { - final ids = users.map((u) => u.id).toList(); + Future _clearUserAssetsETag(List users) { + final ids = users.map((u) => u.uid).toList(); return _eTagRepository.deleteByIds(ids); } @@ -373,26 +390,27 @@ class SyncService { ); // update shared users - final List sharedUsers = album.sharedUsers.toList(growable: false); + final List sharedUsers = + album.sharedUsers.map((u) => u.toDto()).toList(growable: false); sharedUsers.sort((a, b) => a.id.compareTo(b.id)); - final List users = dto.remoteUsers.toList() + final List users = dto.remoteUsers.map((u) => u.toDto()).toList() ..sort((a, b) => a.id.compareTo(b.id)); final List userIdsToAdd = []; - final List usersToUnlink = []; + final List usersToUnlink = []; diffSortedListsSync( users, sharedUsers, - compare: (User a, User b) => a.id.compareTo(b.id), + compare: (UserDto a, UserDto b) => a.id.compareTo(b.id), both: (a, b) => false, - onlyFirst: (User a) => userIdsToAdd.add(a.id), - onlySecond: (User a) => usersToUnlink.add(a), + onlyFirst: (UserDto a) => userIdsToAdd.add(a.uid), + onlySecond: (UserDto a) => usersToUnlink.add(a), ); // for shared album: put missing album assets into local DB final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd); await upsertAssetsWithExif(updated); final assetsToLink = existingInDb + updated; - final usersToLink = await _userRepository.getByIds(userIdsToAdd); + final usersToLink = await _userRepository.getByUserIds(userIdsToAdd); album.name = dto.name; album.shared = dto.shared; @@ -416,7 +434,7 @@ class SyncService { try { await _assetRepository.transaction(() async { await _assetRepository.updateAll(toUpdate); - await _albumRepository.addUsers(album, usersToLink); + await _albumRepository.addUsers(album, usersToLink.nonNulls.toList()); await _albumRepository.removeUsers(album, usersToUnlink); await _albumRepository.addAssets(album, assetsToLink); await _albumRepository.removeAssets(album, toUnlink); @@ -429,7 +447,7 @@ class SyncService { } if (album.shared || dto.shared) { - final userId = (await _userRepository.me()).isarId; + final userId = (_storeService.get(StoreKey.currentUser)).id; final foreign = await _assetRepository.getByAlbum(album, notOwnedBy: [userId]); existing.addAll(foreign); @@ -482,8 +500,7 @@ class SyncService { ); } else if (album.shared) { // delete assets in DB unless they belong to this user or are part of some other shared album or belong to a partner - final userIds = - (await _userRepository.getAllAccessible()).map((user) => user.isarId); + final userIds = (await _getAllAccessibleUsers()).map((user) => user.id); final orphanedAssets = await _assetRepository.getByAlbum(album, notOwnedBy: userIds); deleteCandidates.addAll(orphanedAssets); @@ -566,7 +583,7 @@ class SyncService { // general case, e.g. some assets have been deleted or there are excluded albums on iOS final inDb = await _assetRepository.getByAlbum( dbAlbum, - ownerId: (await _userRepository.me()).isarId, + ownerId: (_storeService.get(StoreKey.currentUser)).id, sortBy: AssetSort.checksum, ); diff --git a/mobile/lib/services/timeline.service.dart b/mobile/lib/services/timeline.service.dart index db85230662..03042e266b 100644 --- a/mobile/lib/services/timeline.service.dart +++ b/mobile/lib/services/timeline.service.dart @@ -1,41 +1,42 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/interfaces/timeline.interface.dart'; -import 'package:immich_mobile/interfaces/user.interface.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; import 'package:immich_mobile/repositories/timeline.repository.dart'; -import 'package:immich_mobile/repositories/user.repository.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; final timelineServiceProvider = Provider((ref) { return TimelineService( ref.watch(timelineRepositoryProvider), - ref.watch(userRepositoryProvider), ref.watch(appSettingsServiceProvider), + ref.watch(storeServiceProvider), ); }); class TimelineService { final ITimelineRepository _timelineRepository; - final IUserRepository _userRepository; final AppSettingsService _appSettingsService; + final StoreService _storeService; const TimelineService( this._timelineRepository, - this._userRepository, this._appSettingsService, + this._storeService, ); Future> getTimelineUserIds() async { - final me = await _userRepository.me(); - return _timelineRepository.getTimelineUserIds(me.isarId); + final me = _storeService.get(StoreKey.currentUser); + return _timelineRepository.getTimelineUserIds(me.id); } Stream> watchTimelineUserIds() async* { - final me = await _userRepository.me(); - yield* _timelineRepository.watchTimelineUsers(me.isarId); + final me = _storeService.get(StoreKey.currentUser); + yield* _timelineRepository.watchTimelineUsers(me.id); } Stream watchHomeTimeline(int userId) { @@ -50,15 +51,15 @@ class TimelineService { } Stream watchArchiveTimeline() async* { - final user = await _userRepository.me(); + final user = _storeService.get(StoreKey.currentUser); - yield* _timelineRepository.watchArchiveTimeline(user.isarId); + yield* _timelineRepository.watchArchiveTimeline(user.id); } Stream watchFavoriteTimeline() async* { - final user = await _userRepository.me(); + final user = _storeService.get(StoreKey.currentUser); - yield* _timelineRepository.watchFavoriteTimeline(user.isarId); + yield* _timelineRepository.watchFavoriteTimeline(user.id); } Stream watchAlbumTimeline(Album album) async* { @@ -69,9 +70,9 @@ class TimelineService { } Stream watchTrashTimeline() async* { - final user = await _userRepository.me(); + final user = _storeService.get(StoreKey.currentUser); - yield* _timelineRepository.watchTrashTimeline(user.isarId); + yield* _timelineRepository.watchTrashTimeline(user.id); } Stream watchAllVideosTimeline() { @@ -96,9 +97,9 @@ class TimelineService { } Stream watchAssetSelectionTimeline() async* { - final user = await _userRepository.me(); + final user = _storeService.get(StoreKey.currentUser); - yield* _timelineRepository.watchAssetSelectionTimeline(user.isarId); + yield* _timelineRepository.watchAssetSelectionTimeline(user.id); } GroupAssetsBy _getGroupByOption() { diff --git a/mobile/lib/services/trash.service.dart b/mobile/lib/services/trash.service.dart index 8d6cdd8bab..338f063fd3 100644 --- a/mobile/lib/services/trash.service.dart +++ b/mobile/lib/services/trash.service.dart @@ -1,12 +1,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/interfaces/asset.interface.dart'; -import 'package:immich_mobile/interfaces/user.interface.dart'; - import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/user.repository.dart'; - import 'package:immich_mobile/services/api.service.dart'; import 'package:openapi/api.dart'; @@ -14,16 +13,20 @@ final trashServiceProvider = Provider((ref) { return TrashService( ref.watch(apiServiceProvider), ref.watch(assetRepositoryProvider), - ref.watch(userRepositoryProvider), + ref.watch(storeServiceProvider), ); }); class TrashService { final ApiService _apiService; final IAssetRepository _assetRepository; - final IUserRepository _userRepository; + final StoreService _storeService; - TrashService(this._apiService, this._assetRepository, this._userRepository); + TrashService( + this._apiService, + this._assetRepository, + this._storeService, + ); Future restoreAssets(Iterable assetList) async { final remoteAssets = assetList.where((a) => a.isRemote); @@ -40,11 +43,11 @@ class TrashService { } Future emptyTrash() async { - final user = await _userRepository.me(); + final user = _storeService.get(StoreKey.currentUser); await _apiService.trashApi.emptyTrash(); - final trashedAssets = await _assetRepository.getTrashAssets(user.isarId); + final trashedAssets = await _assetRepository.getTrashAssets(user.id); final ids = trashedAssets.map((e) => e.remoteId!).toList(); await _assetRepository.transaction(() async { @@ -71,11 +74,11 @@ class TrashService { } Future restoreTrash() async { - final user = await _userRepository.me(); + final user = _storeService.get(StoreKey.currentUser); await _apiService.trashApi.restoreTrash(); - final trashedAssets = await _assetRepository.getTrashAssets(user.isarId); + final trashedAssets = await _assetRepository.getTrashAssets(user.id); final updatedAssets = trashedAssets.map((asset) { asset.isTrashed = false; return asset; diff --git a/mobile/lib/services/user.service.dart b/mobile/lib/services/user.service.dart index 921202ec59..3cb2022aa3 100644 --- a/mobile/lib/services/user.service.dart +++ b/mobile/lib/services/user.service.dart @@ -1,12 +1,12 @@ import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/domain/interfaces/user.interface.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/interfaces/partner_api.interface.dart'; -import 'package:immich_mobile/interfaces/user.interface.dart'; import 'package:immich_mobile/interfaces/user_api.interface.dart'; +import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/repositories/partner_api.repository.dart'; -import 'package:immich_mobile/repositories/user.repository.dart'; import 'package:immich_mobile/repositories/user_api.repository.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:logging/logging.dart'; @@ -31,10 +31,6 @@ class UserService { this._userRepository, ); - Future> getUsers({bool self = false}) { - return _userRepository.getAll(self: self); - } - Future<({String profileImagePath})?> uploadProfileImage(XFile image) async { try { return await _userApiRepository.createProfileImage( @@ -47,17 +43,21 @@ class UserService { } } - Future?> getUsersFromServer() async { - List? users; + Future> getAll() async { + return await _userRepository.getAll(); + } + + Future?> getUsersFromServer() async { + List? users; try { users = await _userApiRepository.getAll(); } catch (e) { _log.warning("Failed to fetch users", e); users = null; } - final List sharedBy = + final List sharedBy = await _partnerApiRepository.getAll(Direction.sharedByMe); - final List sharedWith = + final List sharedWith = await _partnerApiRepository.getAll(Direction.sharedWithMe); if (users == null) { @@ -65,36 +65,44 @@ class UserService { return null; } - users.sortBy((u) => u.id); - sharedBy.sortBy((u) => u.id); - sharedWith.sortBy((u) => u.id); + users.sortBy((u) => u.uid); + sharedBy.sortBy((u) => u.uid); + sharedWith.sortBy((u) => u.uid); + + final updatedSharedBy = []; diffSortedListsSync( users, sharedBy, - compare: (User a, User b) => a.id.compareTo(b.id), - both: (User a, User b) => a.isPartnerSharedBy = true, - onlyFirst: (_) {}, - onlySecond: (_) {}, - ); - - diffSortedListsSync( - users, - sharedWith, - compare: (User a, User b) => a.id.compareTo(b.id), - both: (User a, User b) { - a.isPartnerSharedWith = true; - a.inTimeline = b.inTimeline; + compare: (UserDto a, UserDto b) => a.uid.compareTo(b.uid), + both: (UserDto a, UserDto b) { + updatedSharedBy.add(a.copyWith(isPartnerSharedBy: true)); return true; }, - onlyFirst: (_) {}, - onlySecond: (_) {}, + onlyFirst: (UserDto a) => updatedSharedBy.add(a), + onlySecond: (UserDto b) => updatedSharedBy.add(b), ); - return users; + final updatedSharedWith = []; + + diffSortedListsSync( + updatedSharedBy, + sharedWith, + compare: (UserDto a, UserDto b) => a.uid.compareTo(b.uid), + both: (UserDto a, UserDto b) { + updatedSharedWith.add( + a.copyWith(inTimeline: b.inTimeline, isPartnerSharedWith: true), + ); + return true; + }, + onlyFirst: (UserDto a) => updatedSharedWith.add(a), + onlySecond: (UserDto b) => updatedSharedWith.add(b), + ); + + return updatedSharedWith; } Future clearTable() { - return _userRepository.clearTable(); + return _userRepository.deleteAll(); } } diff --git a/mobile/lib/utils/bootstrap.dart b/mobile/lib/utils/bootstrap.dart index db019798a3..21231becf6 100644 --- a/mobile/lib/utils/bootstrap.dart +++ b/mobile/lib/utils/bootstrap.dart @@ -10,10 +10,10 @@ import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/log.entity.dart'; import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:isar/isar.dart'; diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index 990fc082d5..d2f0a2ac9d 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -5,8 +5,8 @@ import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:isar/isar.dart'; const int targetVersion = 8; diff --git a/mobile/lib/utils/provider_utils.dart b/mobile/lib/utils/provider_utils.dart index 3eac55089d..9a2a441a3e 100644 --- a/mobile/lib/utils/provider_utils.dart +++ b/mobile/lib/utils/provider_utils.dart @@ -1,4 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/repositories/activity_api.repository.dart'; import 'package:immich_mobile/repositories/album_api.repository.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; @@ -13,4 +14,5 @@ void invalidateAllApiRepositoryProviders(WidgetRef ref) { ref.invalidate(albumApiRepositoryProvider); ref.invalidate(personApiRepositoryProvider); ref.invalidate(assetApiRepositoryProvider); + ref.invalidate(timelineUsersIdsProvider); } diff --git a/mobile/lib/widgets/album/album_thumbnail_card.dart b/mobile/lib/widgets/album/album_thumbnail_card.dart index ac62ecee03..ec984d1017 100644 --- a/mobile/lib/widgets/album/album_thumbnail_card.dart +++ b/mobile/lib/widgets/album/album_thumbnail_card.dart @@ -58,7 +58,7 @@ class AlbumThumbnailCard extends StatelessWidget { // Add the owner name to the subtitle String? owner; if (showOwner) { - if (album.ownerId == Store.get(StoreKey.currentUser).id) { + if (album.ownerId == Store.get(StoreKey.currentUser).uid) { owner = 'album_thumbnail_owned'.tr(); } else if (album.ownerName != null) { owner = 'album_thumbnail_shared_by'.tr(args: [album.ownerName!]); diff --git a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart index 256141dc7d..1b225f106f 100644 --- a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart +++ b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart @@ -5,25 +5,25 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/pages/editing/edit.page.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/current_album.provider.dart'; +import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/services/stack.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart'; -import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/services/stack.service.dart'; +import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; +import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; +import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart'; +import 'package:immich_mobile/widgets/common/immich_image.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/pages/editing/edit.page.dart'; class BottomGalleryBar extends ConsumerWidget { final ValueNotifier assetIndex; @@ -49,7 +49,7 @@ class BottomGalleryBar extends ConsumerWidget { if (asset == null) { return const SizedBox(); } - final isOwner = asset.ownerId == ref.watch(currentUserProvider)?.isarId; + final isOwner = asset.ownerId == ref.watch(currentUserProvider)?.id; final showControls = ref.watch(showControlsProvider); final stackId = asset.stackId; diff --git a/mobile/lib/widgets/asset_viewer/description_input.dart b/mobile/lib/widgets/asset_viewer/description_input.dart index 2d9d71ac9e..844e4744b3 100644 --- a/mobile/lib/widgets/asset_viewer/description_input.dart +++ b/mobile/lib/widgets/asset_viewer/description_input.dart @@ -81,7 +81,7 @@ class DescriptionInput extends HookConsumerWidget { } return TextField( - enabled: owner?.isarId == asset.ownerId, + enabled: owner?.id == asset.ownerId, focusNode: focusNode, onTap: () => isFocus.value = true, onChanged: (value) { diff --git a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart index 80be3e46cc..dcef6c79d4 100644 --- a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart +++ b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart @@ -3,22 +3,22 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart'; -import 'package:immich_mobile/providers/tab.provider.dart'; -import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.dart'; -import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/widgets/asset_viewer/top_control_app_bar.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; -import 'package:immich_mobile/providers/trash.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/upload_dialog.dart'; -import 'package:immich_mobile/providers/partner.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/providers/album/current_album.provider.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; +import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; +import 'package:immich_mobile/providers/partner.provider.dart'; +import 'package:immich_mobile/providers/tab.provider.dart'; +import 'package:immich_mobile/providers/trash.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.dart'; +import 'package:immich_mobile/widgets/asset_grid/upload_dialog.dart'; +import 'package:immich_mobile/widgets/asset_viewer/top_control_app_bar.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; class GalleryAppBar extends ConsumerWidget { @@ -33,12 +33,12 @@ class GalleryAppBar extends ConsumerWidget { return const SizedBox(); } final album = ref.watch(currentAlbumProvider); - final isOwner = asset.ownerId == ref.watch(currentUserProvider)?.isarId; + final isOwner = asset.ownerId == ref.watch(currentUserProvider)?.id; final showControls = ref.watch(showControlsProvider); final isPartner = ref .watch(partnerSharedWithProvider) - .map((e) => e.isarId) + .map((e) => e.id) .contains(asset.ownerId); toggleFavorite(Asset asset) => diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart index d51e122954..4d4376b71d 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart @@ -67,8 +67,9 @@ class AppBarProfileInfoBox extends HookConsumerWidget { profileImagePath, ); if (user != null) { - user.profileImagePath = profileImagePath; - await Store.put(StoreKey.currentUser, user); + final updatedUser = + user.copyWith(profileImagePath: profileImagePath); + await Store.put(StoreKey.currentUser, updatedUser); ref.read(currentUserProvider.notifier).refresh(); } } diff --git a/mobile/lib/widgets/common/user_avatar.dart b/mobile/lib/widgets/common/user_avatar.dart index 62491210c9..753a1f5d37 100644 --- a/mobile/lib/widgets/common/user_avatar.dart +++ b/mobile/lib/widgets/common/user_avatar.dart @@ -1,14 +1,14 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/services/api.service.dart'; -Widget userAvatar(BuildContext context, User u, {double? radius}) { +Widget userAvatar(BuildContext context, UserDto u, {double? radius}) { final url = - "${Store.get(StoreKey.serverEndpoint)}/users/${u.id}/profile-image"; + "${Store.get(StoreKey.serverEndpoint)}/users/${u.uid}/profile-image"; final nameFirstLetter = u.name.isNotEmpty ? u.name[0] : ""; return CircleAvatar( radius: radius, diff --git a/mobile/lib/widgets/common/user_circle_avatar.dart b/mobile/lib/widgets/common/user_circle_avatar.dart index 2b7eadf04b..4bc7bfa0f4 100644 --- a/mobile/lib/widgets/common/user_circle_avatar.dart +++ b/mobile/lib/widgets/common/user_circle_avatar.dart @@ -4,15 +4,15 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/widgets/common/transparent_image.dart'; // ignore: must_be_immutable class UserCircleAvatar extends ConsumerWidget { - final User user; + final UserDto user; double radius; double size; @@ -27,13 +27,13 @@ class UserCircleAvatar extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { bool isDarkTheme = context.themeData.brightness == Brightness.dark; final profileImageUrl = - '${Store.get(StoreKey.serverEndpoint)}/users/${user.id}/profile-image?d=${Random().nextInt(1024)}'; + '${Store.get(StoreKey.serverEndpoint)}/users/${user.uid}/profile-image?d=${Random().nextInt(1024)}'; final textIcon = DefaultTextStyle( style: TextStyle( fontWeight: FontWeight.bold, fontSize: 12, - color: isDarkTheme && user.avatarColor == AvatarColorEnum.primary + color: isDarkTheme && user.avatarColor == AvatarColor.primary ? Colors.black : Colors.white, ), @@ -42,7 +42,7 @@ class UserCircleAvatar extends ConsumerWidget { return CircleAvatar( backgroundColor: user.avatarColor.toColor(), radius: radius, - child: user.profileImagePath.isEmpty + child: user.profileImagePath == null ? textIcon : ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(50)), diff --git a/mobile/test/fixtures/album.stub.dart b/mobile/test/fixtures/album.stub.dart index e820f193d5..c6ea199c0f 100644 --- a/mobile/test/fixtures/album.stub.dart +++ b/mobile/test/fixtures/album.stub.dart @@ -1,4 +1,5 @@ import 'package:immich_mobile/entities/album.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'asset.stub.dart'; import 'user.stub.dart'; @@ -26,7 +27,7 @@ final class AlbumStub { shared: true, activityEnabled: false, endDate: DateTime(2020), - )..sharedUsers.addAll([UserStub.admin]); + )..sharedUsers.addAll([User.fromDto(UserStub.admin)]); static final oneAsset = Album( name: "album-with-single-asset", @@ -53,7 +54,7 @@ final class AlbumStub { ) ..assets.addAll([AssetStub.image1, AssetStub.image2]) ..activityEnabled = true - ..owner.value = UserStub.admin; + ..owner.value = User.fromDto(UserStub.admin); static final create2020end2020Album = Album( name: "create2020update2020Album", diff --git a/mobile/test/fixtures/user.stub.dart b/mobile/test/fixtures/user.stub.dart index 92efc93683..88b14dc02e 100644 --- a/mobile/test/fixtures/user.stub.dart +++ b/mobile/test/fixtures/user.stub.dart @@ -1,35 +1,35 @@ -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; abstract final class UserStub { const UserStub._(); - static final admin = User( - id: "admin", - updatedAt: DateTime(2021), + static final admin = UserDto( + uid: "admin", email: "admin@test.com", name: "admin", isAdmin: true, - profileImagePath: '', - avatarColor: AvatarColorEnum.green, + updatedAt: DateTime(2021), + profileImagePath: null, + avatarColor: AvatarColor.green, ); - static final user1 = User( - id: "user1", - updatedAt: DateTime(2022), + static final user1 = UserDto( + uid: "user1", email: "user1@test.com", name: "user1", isAdmin: false, - profileImagePath: '', - avatarColor: AvatarColorEnum.red, + updatedAt: DateTime(2022), + profileImagePath: null, + avatarColor: AvatarColor.red, ); - static final user2 = User( - id: "user2", - updatedAt: DateTime(2023), + static final user2 = UserDto( + uid: "user2", email: "user2@test.com", name: "user2", isAdmin: false, - profileImagePath: '', - avatarColor: AvatarColorEnum.primary, + updatedAt: DateTime(2023), + profileImagePath: null, + avatarColor: AvatarColor.primary, ); } diff --git a/mobile/test/infrastructure/repositories/store_repository_test.dart b/mobile/test/infrastructure/repositories/store_repository_test.dart index 6fd3d3963a..16e0632d83 100644 --- a/mobile/test/infrastructure/repositories/store_repository_test.dart +++ b/mobile/test/infrastructure/repositories/store_repository_test.dart @@ -3,7 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/interfaces/store.interface.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:isar/isar.dart'; @@ -86,7 +86,7 @@ void main() { }); test('converts user', () async { - User? user = await sut.tryGet(StoreKey.currentUser); + UserDto? user = await sut.tryGet(StoreKey.currentUser); expect(user, isNull); await sut.insert(StoreKey.currentUser, _kTestUser); user = await sut.tryGet(StoreKey.currentUser); diff --git a/mobile/test/modules/activity/activities_page_test.dart b/mobile/test/modules/activity/activities_page_test.dart index 6b20692bcd..cf9238d205 100644 --- a/mobile/test/modules/activity/activities_page_test.dart +++ b/mobile/test/modules/activity/activities_page_test.dart @@ -9,7 +9,7 @@ import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/models/activities/activity.model.dart'; import 'package:immich_mobile/pages/common/activities.page.dart'; @@ -96,7 +96,7 @@ void main() { await db.writeTxn(() async { await db.clear(); // Save all assets - await db.users.put(UserStub.admin); + await db.users.put(User.fromDto(UserStub.admin)); await db.assets.putAll([AssetStub.image1, AssetStub.image2]); await db.albums.put(AlbumStub.twoAsset); await AlbumStub.twoAsset.owner.save(); diff --git a/mobile/test/modules/shared/shared_mocks.dart b/mobile/test/modules/shared/shared_mocks.dart index 013232da3e..f50fde7040 100644 --- a/mobile/test/modules/shared/shared_mocks.dart +++ b/mobile/test/modules/shared/shared_mocks.dart @@ -1,13 +1,13 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:mocktail/mocktail.dart'; -class MockCurrentUserProvider extends StateNotifier +class MockCurrentUserProvider extends StateNotifier with Mock implements CurrentUserProvider { MockCurrentUserProvider() : super(null); @override - set state(User? user) => super.state = user; + set state(UserDto? user) => super.state = user; } diff --git a/mobile/test/modules/shared/sync_service_test.dart b/mobile/test/modules/shared/sync_service_test.dart index a58de21613..577b5a0389 100644 --- a/mobile/test/modules/shared/sync_service_test.dart +++ b/mobile/test/modules/shared/sync_service_test.dart @@ -1,16 +1,16 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/interfaces/user.interface.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/interfaces/asset.interface.dart'; -import 'package:immich_mobile/interfaces/user.interface.dart'; import 'package:immich_mobile/services/sync.service.dart'; import 'package:mocktail/mocktail.dart'; @@ -56,8 +56,10 @@ void main() { final MockAlbumMediaRepository albumMediaRepository = MockAlbumMediaRepository(); final MockAlbumApiRepository albumApiRepository = MockAlbumApiRepository(); - final owner = User( - id: "1", + final MockPartnerRepository partnerRepository = MockPartnerRepository(); + + final owner = UserDto( + uid: "1", updatedAt: DateTime.now(), email: "a@b.c", name: "first last", @@ -92,21 +94,22 @@ void main() { albumRepository, assetRepository, exifInfoRepository, + partnerRepository, userRepository, + StoreService.I, eTagRepository, ); - when(() => eTagRepository.get(owner.isarId)) - .thenAnswer((_) async => ETag(id: owner.id, time: DateTime.now())); + when(() => eTagRepository.get(owner.id)) + .thenAnswer((_) async => ETag(id: owner.uid, time: DateTime.now())); when(() => eTagRepository.deleteByIds(["1"])).thenAnswer((_) async {}); when(() => eTagRepository.upsertAll(any())).thenAnswer((_) async {}); - when(() => userRepository.me()).thenAnswer((_) async => owner); - when(() => userRepository.getAll(sortBy: UserSort.id)) - .thenAnswer((_) async => [owner]); - when(() => userRepository.getAllAccessible()) + when(() => partnerRepository.getSharedWith()).thenAnswer((_) async => []); + when(() => userRepository.getAll(sortBy: SortUserBy.id)) .thenAnswer((_) async => [owner]); + when(() => userRepository.getAll()).thenAnswer((_) async => [owner]); when( () => assetRepository.getAll( - ownerId: owner.isarId, + ownerId: owner.id, sortBy: AssetSort.checksum, ), ).thenAnswer((_) async => initialAssets); @@ -180,7 +183,7 @@ void main() { expect(c1, isTrue); when( () => assetRepository.getAll( - ownerId: owner.isarId, + ownerId: owner.id, sortBy: AssetSort.checksum, ), ).thenAnswer((_) async => remoteAssets); @@ -194,7 +197,7 @@ void main() { final currentState = [...remoteAssets]; when( () => assetRepository.getAll( - ownerId: owner.isarId, + ownerId: owner.id, sortBy: AssetSort.checksum, ), ).thenAnswer((_) async => currentState); @@ -252,7 +255,7 @@ void main() { } Future<(List?, List?)> _failDiff( - List user, + List user, DateTime time, ) => Future.value((null, null)); diff --git a/mobile/test/repository.mocks.dart b/mobile/test/repository.mocks.dart index 7443db2815..8672297f1f 100644 --- a/mobile/test/repository.mocks.dart +++ b/mobile/test/repository.mocks.dart @@ -1,4 +1,5 @@ import 'package:immich_mobile/domain/interfaces/exif.interface.dart'; +import 'package:immich_mobile/domain/interfaces/user.interface.dart'; import 'package:immich_mobile/interfaces/album.interface.dart'; import 'package:immich_mobile/interfaces/album_api.interface.dart'; import 'package:immich_mobile/interfaces/album_media.interface.dart'; @@ -9,7 +10,7 @@ import 'package:immich_mobile/interfaces/auth_api.interface.dart'; import 'package:immich_mobile/interfaces/backup_album.interface.dart'; import 'package:immich_mobile/interfaces/etag.interface.dart'; import 'package:immich_mobile/interfaces/file_media.interface.dart'; -import 'package:immich_mobile/interfaces/user.interface.dart'; +import 'package:immich_mobile/interfaces/partner.interface.dart'; import 'package:mocktail/mocktail.dart'; class MockAlbumRepository extends Mock implements IAlbumRepository {} @@ -35,3 +36,5 @@ class MockAlbumApiRepository extends Mock implements IAlbumApiRepository {} class MockAuthApiRepository extends Mock implements IAuthApiRepository {} class MockAuthRepository extends Mock implements IAuthRepository {} + +class MockPartnerRepository extends Mock implements IPartnerRepository {} diff --git a/mobile/test/services/album.service_test.dart b/mobile/test/services/album.service_test.dart index 983b355dcb..5460faaa23 100644 --- a/mobile/test/services/album.service_test.dart +++ b/mobile/test/services/album.service_test.dart @@ -146,7 +146,7 @@ void main() { () => albumApiRepository.create( "name", assetIds: [AssetStub.image1.remoteId!], - sharedUserIds: [UserStub.user1.id], + sharedUserIds: [UserStub.user1.uid], ), ).called(1); verify( @@ -204,7 +204,7 @@ void main() { when( () => albumRepository.addUsers( AlbumStub.emptyAlbum, - AlbumStub.emptyAlbum.sharedUsers.toList(), + AlbumStub.emptyAlbum.sharedUsers.map((u) => u.toDto()).toList(), ), ).thenAnswer((_) async => AlbumStub.emptyAlbum); @@ -214,7 +214,7 @@ void main() { final result = await sut.addUsers( AlbumStub.emptyAlbum, - [UserStub.user2.id], + [UserStub.user2.uid], ); expect(result, true); diff --git a/mobile/test/services/entity.service_test.dart b/mobile/test/services/entity.service_test.dart index 8c8b49a7e0..4d2e4f75dc 100644 --- a/mobile/test/services/entity.service_test.dart +++ b/mobile/test/services/entity.service_test.dart @@ -1,7 +1,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/entities/album.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/services/entity.service.dart'; import 'package:mocktail/mocktail.dart'; + import '../fixtures/asset.stub.dart'; import '../fixtures/user.stub.dart'; import '../repository.mocks.dart'; @@ -33,25 +35,32 @@ void main() { ) ..remoteThumbnailAssetId = AssetStub.image1.remoteId ..assets.addAll([AssetStub.image1, AssetStub.image1]) - ..owner.value = UserStub.user1 - ..sharedUsers.addAll([UserStub.admin, UserStub.admin]); + ..owner.value = User.fromDto(UserStub.user1) + ..sharedUsers.addAll( + [User.fromDto(UserStub.admin), User.fromDto(UserStub.admin)], + ); - when(() => userRepository.get(album.ownerId!)) + when(() => userRepository.get(any())) + .thenAnswer((_) async => UserStub.admin); + when(() => userRepository.getByUserId(any())) .thenAnswer((_) async => UserStub.admin); when(() => assetRepository.getByRemoteId(AssetStub.image1.remoteId!)) .thenAnswer((_) async => AssetStub.image1); - when(() => userRepository.getByIds(any())) + when(() => userRepository.getByUserIds(any())) .thenAnswer((_) async => [UserStub.user1, UserStub.user2]); when(() => assetRepository.getAllByRemoteId(any())) .thenAnswer((_) async => [AssetStub.image1, AssetStub.image2]); await sut.fillAlbumWithDatabaseEntities(album); - expect(album.owner.value, UserStub.admin); + expect(album.owner.value?.toDto(), UserStub.admin); expect(album.thumbnail.value, AssetStub.image1); - expect(album.remoteUsers.toSet(), {UserStub.user1, UserStub.user2}); + expect( + album.remoteUsers.map((u) => u.toDto()).toSet(), + {UserStub.user1, UserStub.user2}, + ); expect(album.remoteAssets.toSet(), {AssetStub.image1, AssetStub.image2}); }); diff --git a/mobile/test/test_utils.dart b/mobile/test/test_utils.dart index 19b2d6e705..a5a89a2440 100644 --- a/mobile/test/test_utils.dart +++ b/mobile/test/test_utils.dart @@ -12,10 +12,10 @@ import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/log.entity.dart'; import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:isar/isar.dart'; import 'package:mocktail/mocktail.dart';