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

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