feat: drift partners (#20051)

* feat: drift toggle partner in timeline

* partners operation

* fix: lint
This commit is contained in:
Alex 2025-07-21 16:58:53 -05:00 committed by GitHub
parent 99e5b33969
commit 737e768212
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 703 additions and 23 deletions

View File

@ -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<String, dynamic> toMap() {
return <String, dynamic>{
'id': id,
'email': email,
'name': name,
'inTimeline': inTimeline,
'profileImagePath': profileImagePath,
};
}
factory PartnerUserDto.fromMap(Map<String, dynamic> 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<String, dynamic>);
@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;
}
}

View File

@ -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<List<PartnerUserDto>> getSharedWith(String userId) {
return _driftPartnerRepository.getSharedWith(userId);
}
Future<List<PartnerUserDto>> getSharedBy(String userId) {
return _driftPartnerRepository.getSharedBy(userId);
}
Future<List<PartnerUserDto>> 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<void> 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<void> addPartner(String partnerId, String userId) async {
await _partnerApiRepository.create(partnerId);
await _driftPartnerRepository.create(partnerId, userId);
}
Future<void> removePartner(String partnerId, String userId) async {
await _partnerApiRepository.delete(partnerId);
await _driftPartnerRepository.delete(partnerId, userId);
}
}

View File

@ -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<List<PartnerUserDto>> 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<List<PartnerUserDto>> 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<List<PartnerUserDto>> 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<List<PartnerUserDto>> 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<List<String>> 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 =
<String>{...sharingWithMe, ...sharingWithThem}.toList();
return allPartnerIds;
}
Future<PartnerUserDto?> 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<bool> toggleShowInTimeline(PartnerUserDto partner, String userId) {
return _db.partnerEntity.update().replace(
PartnerEntityCompanion(
sharedById: Value(partner.id),
sharedWithId: Value(userId),
inTimeline: Value(!partner.inTimeline),
),
);
}
Future<int> create(String partnerId, String userId) {
final entity = PartnerEntityCompanion(
sharedById: Value(userId),
sharedWithId: Value(partnerId),
inTimeline: const Value(false),
);
return _db.partnerEntity.insertOne(entity);
}
Future<void> delete(String partnerId, String userId) {
return _db.partnerEntity.deleteWhere(
(t) => t.sharedById.equals(userId) & t.sharedWithId.equals(partnerId),
);
}
}

View File

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

View File

@ -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<UserDto> partners;
final List<PartnerUserDto> 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(

View File

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

View File

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

View File

@ -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<List<PartnerUserDto>> {
late DriftPartnerService _driftPartnerService;
@override
List<PartnerUserDto> build() {
_driftPartnerService = ref.read(driftPartnerServiceProvider);
return [];
}
Future<void> _loadPartners() async {
final currentUser = ref.read(currentUserProvider);
if (currentUser == null) {
return;
}
state = await _driftPartnerService.getSharedWith(currentUser.id);
}
Future<List<PartnerUserDto>> getPartners(String userId) async {
final partners = await _driftPartnerService.getSharedWith(userId);
state = partners;
return partners;
}
Future<void> toggleShowInTimeline(String partnerId, String userId) async {
await _driftPartnerService.toggleShowInTimeline(partnerId, userId);
await _loadPartners();
}
Future<void> 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<void> 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<List<PartnerUserDto>>((ref) {
final currentUser = ref.watch(currentUserProvider);
if (currentUser == null) {
return [];
}
return ref
.watch(driftPartnerServiceProvider)
.getAvailablePartners(currentUser.id);
});
final driftSharedByPartnerProvider =
FutureProvider.autoDispose<List<PartnerUserDto>>((ref) {
final currentUser = ref.watch(currentUserProvider);
if (currentUser == null) {
return [];
}
return ref.watch(driftPartnerServiceProvider).getSharedBy(currentUser.id);
});
final driftSharedWithPartnerProvider =
FutureProvider.autoDispose<List<PartnerUserDto>>((ref) {
final currentUser = ref.watch(currentUserProvider);
if (currentUser == null) {
return [];
}
return ref.watch(driftPartnerServiceProvider).getSharedWith(currentUser.id);
});

View File

@ -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<DriftPartnerRepository>(
(ref) => DriftPartnerRepository(ref.watch(driftProvider)),
);
final driftPartnerServiceProvider = Provider<DriftPartnerService>(
(ref) => DriftPartnerService(
ref.watch(driftPartnerRepositoryProvider),
ref.watch(partnerApiRepositoryProvider),
),
);
final partnerUsersProvider =
NotifierProvider<PartnerNotifier, List<PartnerUserDto>>(
PartnerNotifier.new,
);

View File

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

View File

@ -896,7 +896,7 @@ class DriftPartnerDetailRoute
extends PageRouteInfo<DriftPartnerDetailRouteArgs> {
DriftPartnerDetailRoute({
Key? key,
required UserDto partner,
required PartnerUserDto partner,
List<PageRouteInfo>? 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<void> {
const DriftPartnerRoute({List<PageRouteInfo>? 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<DriftPlaceDetailRouteArgs> {