diff --git a/mobile/lib/domain/models/user.model.dart b/mobile/lib/domain/models/user.model.dart index abf2e5620b..6cafd8d149 100644 --- a/mobile/lib/domain/models/user.model.dart +++ b/mobile/lib/domain/models/user.model.dart @@ -1,3 +1,6 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:convert'; + import 'package:immich_mobile/domain/models/user_metadata.model.dart'; // TODO: Rename to User once Isar is removed @@ -123,3 +126,88 @@ quotaSizeInBytes: $quotaSizeInBytes, quotaUsageInBytes.hashCode ^ quotaSizeInBytes.hashCode; } + +class PartnerUserDto { + final String id; + final String email; + final String name; + final bool inTimeline; + + final String? profileImagePath; + + const PartnerUserDto({ + required this.id, + required this.email, + required this.name, + required this.inTimeline, + this.profileImagePath, + }); + + PartnerUserDto copyWith({ + String? id, + String? email, + String? name, + bool? inTimeline, + String? profileImagePath, + }) { + return PartnerUserDto( + id: id ?? this.id, + email: email ?? this.email, + name: name ?? this.name, + inTimeline: inTimeline ?? this.inTimeline, + profileImagePath: profileImagePath ?? this.profileImagePath, + ); + } + + Map toMap() { + return { + 'id': id, + 'email': email, + 'name': name, + 'inTimeline': inTimeline, + 'profileImagePath': profileImagePath, + }; + } + + factory PartnerUserDto.fromMap(Map map) { + return PartnerUserDto( + id: map['id'] as String, + email: map['email'] as String, + name: map['name'] as String, + inTimeline: map['inTimeline'] as bool, + profileImagePath: map['profileImagePath'] != null + ? map['profileImagePath'] as String + : null, + ); + } + + String toJson() => json.encode(toMap()); + + factory PartnerUserDto.fromJson(String source) => + PartnerUserDto.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'PartnerUserDto(id: $id, email: $email, name: $name, inTimeline: $inTimeline, profileImagePath: $profileImagePath)'; + } + + @override + bool operator ==(covariant PartnerUserDto other) { + if (identical(this, other)) return true; + + return other.id == id && + other.email == email && + other.name == name && + other.inTimeline == inTimeline && + other.profileImagePath == profileImagePath; + } + + @override + int get hashCode { + return id.hashCode ^ + email.hashCode ^ + name.hashCode ^ + inTimeline.hashCode ^ + profileImagePath.hashCode; + } +} diff --git a/mobile/lib/domain/services/partner.service.dart b/mobile/lib/domain/services/partner.service.dart new file mode 100644 index 0000000000..065560c4be --- /dev/null +++ b/mobile/lib/domain/services/partner.service.dart @@ -0,0 +1,61 @@ +import 'package:flutter/foundation.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart'; +import 'package:immich_mobile/repositories/partner_api.repository.dart'; + +class DriftPartnerService { + final DriftPartnerRepository _driftPartnerRepository; + final PartnerApiRepository _partnerApiRepository; + + const DriftPartnerService( + this._driftPartnerRepository, + this._partnerApiRepository, + ); + + Future> getSharedWith(String userId) { + return _driftPartnerRepository.getSharedWith(userId); + } + + Future> getSharedBy(String userId) { + return _driftPartnerRepository.getSharedBy(userId); + } + + Future> getAvailablePartners( + String currentUserId, + ) async { + final otherUsers = + await _driftPartnerRepository.getAvailablePartners(currentUserId); + final currentPartners = + await _driftPartnerRepository.getSharedBy(currentUserId); + final available = otherUsers.where((user) { + return !currentPartners.any((partner) => partner.id == user.id); + }).toList(); + + return available; + } + + Future toggleShowInTimeline(String partnerId, String userId) async { + final partner = await _driftPartnerRepository.getPartner(partnerId, userId); + if (partner == null) { + debugPrint("Partner not found: $partnerId for user: $userId"); + return; + } + + await _partnerApiRepository.update( + partnerId, + inTimeline: !partner.inTimeline, + ); + + await _driftPartnerRepository.toggleShowInTimeline(partner, userId); + } + + Future addPartner(String partnerId, String userId) async { + await _partnerApiRepository.create(partnerId); + await _driftPartnerRepository.create(partnerId, userId); + } + + Future removePartner(String partnerId, String userId) async { + await _partnerApiRepository.delete(partnerId); + await _driftPartnerRepository.delete(partnerId, userId); + } +} diff --git a/mobile/lib/infrastructure/repositories/partner.repository.dart b/mobile/lib/infrastructure/repositories/partner.repository.dart new file mode 100644 index 0000000000..b3b057b035 --- /dev/null +++ b/mobile/lib/infrastructure/repositories/partner.repository.dart @@ -0,0 +1,164 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +class DriftPartnerRepository extends DriftDatabaseRepository { + final Drift _db; + const DriftPartnerRepository(this._db) : super(_db); + + Future> getPartners(String userId) { + final query = _db.select(_db.partnerEntity).join([ + innerJoin( + _db.userEntity, + _db.userEntity.id.equalsExp(_db.partnerEntity.sharedById), + ), + ]) + ..where( + _db.partnerEntity.sharedWithId.equals(userId), + ); + + return query.map((row) { + final user = row.readTable(_db.userEntity); + final partner = row.readTable(_db.partnerEntity); + return PartnerUserDto( + id: user.id, + email: user.email, + name: user.name, + inTimeline: partner.inTimeline, + ); + }).get(); + } + + // Get users who we can share our library with + Future> getAvailablePartners(String currentUserId) { + final query = _db.select(_db.userEntity) + ..where((row) => row.id.equals(currentUserId).not()); + + return query.map((user) { + return PartnerUserDto( + id: user.id, + email: user.email, + name: user.name, + inTimeline: false, + ); + }).get(); + } + + // Get users who are sharing their photos WITH the current user + Future> getSharedWith(String partnerId) { + final query = _db.select(_db.partnerEntity).join([ + innerJoin( + _db.userEntity, + _db.userEntity.id.equalsExp(_db.partnerEntity.sharedById), + ), + ]) + ..where( + _db.partnerEntity.sharedWithId.equals(partnerId), + ); + + return query.map((row) { + final user = row.readTable(_db.userEntity); + final partner = row.readTable(_db.partnerEntity); + return PartnerUserDto( + id: user.id, + email: user.email, + name: user.name, + inTimeline: partner.inTimeline, + ); + }).get(); + } + + // Get users who the current user is sharing their photos TO + Future> getSharedBy(String userId) { + final query = _db.select(_db.partnerEntity).join([ + innerJoin( + _db.userEntity, + _db.userEntity.id.equalsExp(_db.partnerEntity.sharedWithId), + ), + ]) + ..where( + _db.partnerEntity.sharedById.equals(userId), + ); + + return query.map((row) { + final user = row.readTable(_db.userEntity); + final partner = row.readTable(_db.partnerEntity); + return PartnerUserDto( + id: user.id, + email: user.email, + name: user.name, + inTimeline: partner.inTimeline, + ); + }).get(); + } + + Future> getAllPartnerIds(String userId) async { + // Get users who are sharing with me (sharedWithId = userId) + final sharingWithMeQuery = _db.select(_db.partnerEntity) + ..where((tbl) => tbl.sharedWithId.equals(userId)); + final sharingWithMe = + await sharingWithMeQuery.map((row) => row.sharedById).get(); + + // Get users who I am sharing with (sharedById = userId) + final sharingWithThemQuery = _db.select(_db.partnerEntity) + ..where((tbl) => tbl.sharedById.equals(userId)); + final sharingWithThem = + await sharingWithThemQuery.map((row) => row.sharedWithId).get(); + + // Combine both lists and remove duplicates + final allPartnerIds = + {...sharingWithMe, ...sharingWithThem}.toList(); + return allPartnerIds; + } + + Future getPartner(String partnerId, String userId) { + final query = _db.select(_db.partnerEntity).join([ + innerJoin( + _db.userEntity, + _db.userEntity.id.equalsExp(_db.partnerEntity.sharedById), + ), + ]) + ..where( + _db.partnerEntity.sharedById.equals(partnerId) & + _db.partnerEntity.sharedWithId.equals(userId), + ); + + return query.map((row) { + final user = row.readTable(_db.userEntity); + final partner = row.readTable(_db.partnerEntity); + return PartnerUserDto( + id: user.id, + email: user.email, + name: user.name, + inTimeline: partner.inTimeline, + ); + }).getSingleOrNull(); + } + + Future toggleShowInTimeline(PartnerUserDto partner, String userId) { + return _db.partnerEntity.update().replace( + PartnerEntityCompanion( + sharedById: Value(partner.id), + sharedWithId: Value(userId), + inTimeline: Value(!partner.inTimeline), + ), + ); + } + + Future create(String partnerId, String userId) { + final entity = PartnerEntityCompanion( + sharedById: Value(userId), + sharedWithId: Value(partnerId), + inTimeline: const Value(false), + ); + + return _db.partnerEntity.insertOne(entity); + } + + Future delete(String partnerId, String userId) { + return _db.partnerEntity.deleteWhere( + (t) => t.sharedById.equals(userId) & t.sharedWithId.equals(partnerId), + ); + } +} diff --git a/mobile/lib/pages/library/partner/drift_partner.page.dart b/mobile/lib/pages/library/partner/drift_partner.page.dart new file mode 100644 index 0000000000..04efbe066c --- /dev/null +++ b/mobile/lib/pages/library/partner/drift_partner.page.dart @@ -0,0 +1,161 @@ +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/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/partner_user_avatar.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/partner.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; +import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +@RoutePage() +class DriftPartnerPage extends HookConsumerWidget { + const DriftPartnerPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final potentialPartnersAsync = ref.watch(driftAvailablePartnerProvider); + + addNewUsersHandler() async { + final potentialPartners = potentialPartnersAsync.value; + if (potentialPartners == null || potentialPartners.isEmpty) { + ImmichToast.show( + context: context, + msg: "partner_page_no_more_users".tr(), + ); + return; + } + + final selectedUser = await showDialog( + context: context, + builder: (context) { + return SimpleDialog( + title: const Text("partner_page_select_partner").tr(), + children: [ + for (PartnerUserDto partner in potentialPartners) + SimpleDialogOption( + onPressed: () => context.pop(partner), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 8), + child: PartnerUserAvatar(partner: partner), + ), + Text(partner.name), + ], + ), + ), + ], + ); + }, + ); + if (selectedUser != null) { + await ref.read(partnerUsersProvider.notifier).addPartner(selectedUser); + } + } + + onDeleteUser(PartnerUserDto partner) { + return showDialog( + context: context, + builder: (BuildContext context) { + return ConfirmDialog( + title: "stop_photo_sharing", + content: "partner_page_stop_sharing_content" + .tr(namedArgs: {'partner': partner.name}), + onOk: () => + ref.read(partnerUsersProvider.notifier).removePartner(partner), + ); + }, + ); + } + + return Scaffold( + appBar: AppBar( + title: const Text("partners").t(context: context), + elevation: 0, + centerTitle: false, + actions: [ + IconButton( + onPressed: potentialPartnersAsync.whenOrNull( + data: (data) => addNewUsersHandler, + ), + icon: const Icon(Icons.person_add), + tooltip: "add_partner".tr(), + ), + ], + ), + body: _SharedToPartnerList( + onAddPartner: addNewUsersHandler, + onDeletePartner: onDeleteUser, + ), + ); + } +} + +class _SharedToPartnerList extends ConsumerWidget { + final VoidCallback onAddPartner; + final Function(PartnerUserDto partner) onDeletePartner; + + const _SharedToPartnerList({ + required this.onAddPartner, + required this.onDeletePartner, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final partnerAsync = ref.watch(driftSharedByPartnerProvider); + + return partnerAsync.when( + data: (partners) { + if (partners.isEmpty) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: const Text( + "partner_page_empty_message", + style: TextStyle(fontSize: 14), + ).tr(), + ), + Align( + alignment: Alignment.center, + child: ElevatedButton.icon( + onPressed: onAddPartner, + icon: const Icon(Icons.person_add), + label: const Text("add_partner").tr(), + ), + ), + ], + ), + ); + } + + return ListView.builder( + itemCount: partners.length, + itemBuilder: (context, index) { + final partner = partners[index]; + return ListTile( + leading: PartnerUserAvatar(partner: partner), + title: Text(partner.name), + subtitle: Text(partner.email), + trailing: IconButton( + icon: const Icon(Icons.person_remove), + onPressed: () => onDeletePartner(partner), + ), + ); + }, + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center( + child: Text("Error loading partners: $error"), + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/drift_library.page.dart b/mobile/lib/presentation/pages/drift_library.page.dart index 552733980e..eba0a5ea81 100644 --- a/mobile/lib/presentation/pages/drift_library.page.dart +++ b/mobile/lib/presentation/pages/drift_library.page.dart @@ -6,15 +6,15 @@ import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/images/local_album_thumbnail.widget.dart'; +import 'package:immich_mobile/presentation/widgets/partner_user_avatar.widget.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; -import 'package:immich_mobile/providers/partner.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/partner.provider.dart'; import 'package:immich_mobile/providers/search/people.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart'; -import 'package:immich_mobile/widgets/common/user_avatar.dart'; import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -391,7 +391,8 @@ class _QuickAccessButtonList extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final partners = ref.watch(partnerSharedWithProvider); + final partnerSharedWithAsync = ref.watch(driftSharedWithPartnerProvider); + final partners = partnerSharedWithAsync.valueOrNull ?? []; return SliverPadding( padding: const EdgeInsets.only(left: 16, top: 12, right: 16, bottom: 32), @@ -452,7 +453,6 @@ class _QuickAccessButtonList extends ConsumerWidget { fontWeight: FontWeight.w500, ), ), - // TODO: PIN code is needed onTap: () => context.pushRoute(const DriftLockedFolderRoute()), ), ListTile( @@ -466,7 +466,7 @@ class _QuickAccessButtonList extends ConsumerWidget { fontWeight: FontWeight.w500, ), ), - onTap: () => context.pushRoute(const PartnerRoute()), + onTap: () => context.pushRoute(const DriftPartnerRoute()), ), _PartnerList(partners: partners), ], @@ -480,7 +480,7 @@ class _QuickAccessButtonList extends ConsumerWidget { class _PartnerList extends StatelessWidget { const _PartnerList({required this.partners}); - final List partners; + final List partners; @override Widget build(BuildContext context) { @@ -503,7 +503,9 @@ class _PartnerList extends StatelessWidget { left: 12.0, right: 18.0, ), - leading: userAvatar(context, partner, radius: 16), + leading: PartnerUserAvatar( + partner: partner, + ), title: const Text( "partner_list_user_photos", style: TextStyle( diff --git a/mobile/lib/presentation/pages/drift_partner_detail.page.dart b/mobile/lib/presentation/pages/drift_partner_detail.page.dart index baae893d39..6c77a480ea 100644 --- a/mobile/lib/presentation/pages/drift_partner_detail.page.dart +++ b/mobile/lib/presentation/pages/drift_partner_detail.page.dart @@ -6,11 +6,14 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; @RoutePage() class DriftPartnerDetailPage extends StatelessWidget { - final UserDto partner; + final PartnerUserDto partner; const DriftPartnerDetailPage({ super.key, @@ -35,12 +38,7 @@ class DriftPartnerDetailPage extends StatelessWidget { title: partner.name, icon: Icons.person_outline, ), - topSliverWidget: _InfoBox( - onTap: () => { - // TODO: Create DriftUserProvider/DriftUserService to handle this action - }, - inTimeline: partner.inTimeline, - ), + topSliverWidget: _InfoBox(partner: partner), topSliverWidgetHeight: 110, bottomSheet: const PartnerDetailBottomSheet(), ), @@ -48,15 +46,53 @@ class DriftPartnerDetailPage extends StatelessWidget { } } -class _InfoBox extends StatelessWidget { - final VoidCallback onTap; - final bool inTimeline; +class _InfoBox extends ConsumerStatefulWidget { + final PartnerUserDto partner; const _InfoBox({ - required this.onTap, - required this.inTimeline, + required this.partner, }); + @override + ConsumerState<_InfoBox> createState() => _InfoBoxState(); +} + +class _InfoBoxState extends ConsumerState<_InfoBox> { + bool _inTimeline = false; + + @override + void initState() { + super.initState(); + _inTimeline = widget.partner.inTimeline; + } + + _toggleInTimeline() async { + final user = ref.read(currentUserProvider); + if (user == null) { + return; + } + + try { + await ref.read(partnerUsersProvider.notifier).toggleShowInTimeline( + widget.partner.id, + user.id, + ); + + setState(() { + _inTimeline = !_inTimeline; + }); + } catch (error, stack) { + debugPrint("Failed to toggle in timeline: $error $stack"); + ImmichToast.show( + context: context, + toastType: ToastType.error, + durationInSecond: 1, + msg: "Failed to toggle the timeline setting", + ); + return; + } + } + @override Widget build(BuildContext context) { return SliverToBoxAdapter( @@ -96,8 +132,8 @@ class _InfoBox extends StatelessWidget { style: context.textTheme.bodyMedium, ), trailing: Switch( - value: inTimeline, - onChanged: (_) => onTap(), + value: _inTimeline, + onChanged: (_) => _toggleInTimeline(), ), ), ), diff --git a/mobile/lib/presentation/widgets/partner_user_avatar.widget.dart b/mobile/lib/presentation/widgets/partner_user_avatar.widget.dart new file mode 100644 index 0000000000..9be55cae67 --- /dev/null +++ b/mobile/lib/presentation/widgets/partner_user_avatar.widget.dart @@ -0,0 +1,32 @@ +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/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/services/api.service.dart'; + +class PartnerUserAvatar extends StatelessWidget { + const PartnerUserAvatar({super.key, required this.partner}); + + final PartnerUserDto partner; + + @override + Widget build(BuildContext context) { + final url = + "${Store.get(StoreKey.serverEndpoint)}/users/${partner.id}/profile-image"; + final nameFirstLetter = partner.name.isNotEmpty ? partner.name[0] : ""; + return CircleAvatar( + radius: 16, + backgroundColor: context.primaryColor.withAlpha(50), + foregroundImage: CachedNetworkImageProvider( + url, + headers: ApiService.getRequestHeaders(), + cacheKey: "user-${partner.id}-profile", + ), + // silence errors if user has no profile image, use initials as fallback + onForegroundImageError: (exception, stackTrace) {}, + child: Text(nameFirstLetter.toUpperCase()), + ); + } +} diff --git a/mobile/lib/providers/infrastructure/partner.provider.dart b/mobile/lib/providers/infrastructure/partner.provider.dart new file mode 100644 index 0000000000..e1c7ebf960 --- /dev/null +++ b/mobile/lib/providers/infrastructure/partner.provider.dart @@ -0,0 +1,92 @@ +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/domain/services/partner.service.dart'; +import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class PartnerNotifier extends Notifier> { + late DriftPartnerService _driftPartnerService; + + @override + List build() { + _driftPartnerService = ref.read(driftPartnerServiceProvider); + return []; + } + + Future _loadPartners() async { + final currentUser = ref.read(currentUserProvider); + if (currentUser == null) { + return; + } + + state = await _driftPartnerService.getSharedWith(currentUser.id); + } + + Future> getPartners(String userId) async { + final partners = await _driftPartnerService.getSharedWith(userId); + state = partners; + return partners; + } + + Future toggleShowInTimeline(String partnerId, String userId) async { + await _driftPartnerService.toggleShowInTimeline(partnerId, userId); + await _loadPartners(); + } + + Future addPartner(PartnerUserDto partner) async { + final currentUser = ref.read(currentUserProvider); + if (currentUser == null) { + return; + } + + await _driftPartnerService.addPartner(partner.id, currentUser.id); + await _loadPartners(); + ref.invalidate(driftAvailablePartnerProvider); + ref.invalidate(driftSharedByPartnerProvider); + } + + Future removePartner(PartnerUserDto partner) async { + final currentUser = ref.read(currentUserProvider); + if (currentUser == null) { + return; + } + + await _driftPartnerService.removePartner(partner.id, currentUser.id); + await _loadPartners(); + ref.invalidate(driftAvailablePartnerProvider); + ref.invalidate(driftSharedByPartnerProvider); + } +} + +final driftAvailablePartnerProvider = + FutureProvider.autoDispose>((ref) { + final currentUser = ref.watch(currentUserProvider); + if (currentUser == null) { + return []; + } + + return ref + .watch(driftPartnerServiceProvider) + .getAvailablePartners(currentUser.id); +}); + +final driftSharedByPartnerProvider = + FutureProvider.autoDispose>((ref) { + final currentUser = ref.watch(currentUserProvider); + if (currentUser == null) { + return []; + } + + return ref.watch(driftPartnerServiceProvider).getSharedBy(currentUser.id); +}); + +final driftSharedWithPartnerProvider = + FutureProvider.autoDispose>((ref) { + final currentUser = ref.watch(currentUserProvider); + if (currentUser == null) { + return []; + } + + return ref.watch(driftPartnerServiceProvider).getSharedWith(currentUser.id); +}); diff --git a/mobile/lib/providers/infrastructure/user.provider.dart b/mobile/lib/providers/infrastructure/user.provider.dart index ca65f8be14..d328f97600 100644 --- a/mobile/lib/providers/infrastructure/user.provider.dart +++ b/mobile/lib/providers/infrastructure/user.provider.dart @@ -1,10 +1,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/domain/services/partner.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/partner.provider.dart'; import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; +import 'package:immich_mobile/repositories/partner_api.repository.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'user.provider.g.dart'; @@ -23,3 +28,20 @@ UserService userService(Ref ref) => UserService( userApiRepository: ref.watch(userApiRepositoryProvider), storeService: ref.watch(storeServiceProvider), ); + +/// Drifts +final driftPartnerRepositoryProvider = Provider( + (ref) => DriftPartnerRepository(ref.watch(driftProvider)), +); + +final driftPartnerServiceProvider = Provider( + (ref) => DriftPartnerService( + ref.watch(driftPartnerRepositoryProvider), + ref.watch(partnerApiRepositoryProvider), + ), +); + +final partnerUsersProvider = + NotifierProvider>( + PartnerNotifier.new, +); diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index d0f8852dc3..ba31ccef2b 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -51,6 +51,7 @@ 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/locked/locked.page.dart'; import 'package:immich_mobile/pages/library/locked/pin_auth.page.dart'; +import 'package:immich_mobile/pages/library/partner/drift_partner.page.dart'; import 'package:immich_mobile/pages/library/partner/partner.page.dart'; import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart'; import 'package:immich_mobile/pages/library/people/people_collection.page.dart'; @@ -485,6 +486,11 @@ class AppRouter extends RootStackRouter { page: ChangeExperienceRoute.page, guards: [_authGuard, _duplicateGuard], ), + + AutoRoute( + page: DriftPartnerRoute.page, + guards: [_authGuard, _duplicateGuard], + ), AutoRoute( page: DriftUploadDetailRoute.page, guards: [_authGuard, _duplicateGuard], diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index c9ed8a40a3..0e24f776d8 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -896,7 +896,7 @@ class DriftPartnerDetailRoute extends PageRouteInfo { DriftPartnerDetailRoute({ Key? key, - required UserDto partner, + required PartnerUserDto partner, List? children, }) : super( DriftPartnerDetailRoute.name, @@ -920,7 +920,7 @@ class DriftPartnerDetailRouteArgs { final Key? key; - final UserDto partner; + final PartnerUserDto partner; @override String toString() { @@ -928,6 +928,22 @@ class DriftPartnerDetailRouteArgs { } } +/// generated route for +/// [DriftPartnerPage] +class DriftPartnerRoute extends PageRouteInfo { + const DriftPartnerRoute({List? children}) + : super(DriftPartnerRoute.name, initialChildren: children); + + static const String name = 'DriftPartnerRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftPartnerPage(); + }, + ); +} + /// generated route for /// [DriftPlaceDetailPage] class DriftPlaceDetailRoute extends PageRouteInfo {