feat: people page/sheet/detail (#20309)

This commit is contained in:
Alex 2025-07-29 22:07:53 -05:00 committed by GitHub
parent 268b411a6f
commit 29f16c6a47
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 1562 additions and 97 deletions

View file

@ -6,10 +6,10 @@ 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/presentation/widgets/people/partner_user_avatar.widget.dart';
import 'package:immich_mobile/providers/infrastructure/album.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/infrastructure/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';
@ -144,7 +144,7 @@ class _PeopleCollectionCard extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final people = ref.watch(getAllPeopleProvider);
final people = ref.watch(driftGetAllPeopleProvider);
return LayoutBuilder(
builder: (context, constraints) {
@ -153,7 +153,7 @@ class _PeopleCollectionCard extends ConsumerWidget {
final size = context.width * widthFactor - 20.0;
return GestureDetector(
onTap: () => context.pushRoute(const PeopleCollectionRoute()),
onTap: () => context.pushRoute(const DriftPeopleCollectionRoute()),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [

View file

@ -0,0 +1,130 @@
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/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/infrastructure/people.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/utils/people.utils.dart';
import 'package:immich_mobile/widgets/common/search_field.dart';
@RoutePage()
class DriftPeopleCollectionPage extends ConsumerStatefulWidget {
const DriftPeopleCollectionPage({super.key});
@override
ConsumerState<DriftPeopleCollectionPage> createState() => _DriftPeopleCollectionPageState();
}
class _DriftPeopleCollectionPageState extends ConsumerState<DriftPeopleCollectionPage> {
final FocusNode _formFocus = FocusNode();
String? _search;
@override
void dispose() {
_formFocus.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final people = ref.watch(driftGetAllPeopleProvider);
final headers = ApiService.getRequestHeaders();
return LayoutBuilder(
builder: (context, constraints) {
final isTablet = constraints.maxWidth > 600;
final isPortrait = context.orientation == Orientation.portrait;
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: _search == null,
title: _search != null
? SearchField(
focusNode: _formFocus,
onTapOutside: (_) => _formFocus.unfocus(),
onChanged: (value) => setState(() => _search = value),
filled: true,
hintText: 'filter_people'.tr(),
autofocus: true,
)
: Text('people'.tr()),
actions: [
IconButton(
icon: Icon(_search != null ? Icons.close : Icons.search),
onPressed: () {
setState(() => _search = _search == null ? '' : null);
},
),
],
),
body: SafeArea(
child: people.when(
data: (people) {
if (_search != null) {
people = people.where((person) {
return person.name.toLowerCase().contains(_search!.toLowerCase());
}).toList();
}
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: isTablet ? 6 : 3,
childAspectRatio: 0.85,
mainAxisSpacing: isPortrait && isTablet ? 36 : 0,
),
padding: const EdgeInsets.symmetric(vertical: 32),
itemCount: people.length,
itemBuilder: (context, index) {
final person = people[index];
return Column(
children: [
GestureDetector(
onTap: () {
context.pushRoute(DriftPersonRoute(person: person));
},
child: Material(
shape: const CircleBorder(side: BorderSide.none),
elevation: 3,
child: CircleAvatar(
maxRadius: isTablet ? 100 / 2 : 96 / 2,
backgroundImage: NetworkImage(getFaceThumbnailUrl(person.id), headers: headers),
),
),
),
const SizedBox(height: 12),
GestureDetector(
onTap: () => showNameEditModal(context, person),
child: person.name.isEmpty
? Text(
'add_a_name'.tr(),
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
color: context.colorScheme.primary,
),
)
: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
person.name,
overflow: TextOverflow.ellipsis,
style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500),
),
),
),
],
);
},
);
},
error: (error, stack) => const Text("error"),
loading: () => const Center(child: CircularProgressIndicator()),
),
),
);
},
);
}
}

View file

@ -0,0 +1,97 @@
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/person.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/presentation/widgets/people/person_option_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/user.provider.dart';
import 'package:immich_mobile/utils/people.utils.dart';
import 'package:immich_mobile/widgets/common/person_sliver_app_bar.dart';
@RoutePage()
class DriftPersonPage extends ConsumerStatefulWidget {
final DriftPerson person;
const DriftPersonPage({super.key, required this.person});
@override
ConsumerState<DriftPersonPage> createState() => _DriftPersonPageState();
}
class _DriftPersonPageState extends ConsumerState<DriftPersonPage> {
late DriftPerson _person;
@override
initState() {
super.initState();
_person = widget.person;
}
Future<void> handleEditName(BuildContext context) async {
final newName = await showNameEditModal(context, _person);
if (newName != null && newName.isNotEmpty) {
setState(() {
_person = _person.copyWith(name: newName);
});
}
}
Future<void> handleEditBirthday(BuildContext context) async {
final birthday = await showBirthdayEditModal(context, _person);
if (birthday != null) {
setState(() {
_person = _person.copyWith(birthDate: birthday);
});
}
}
void showOptionSheet(BuildContext context) {
showModalBottomSheet(
context: context,
backgroundColor: context.colorScheme.surface,
isScrollControlled: false,
builder: (context) {
return PersonOptionSheet(
onEditName: () async {
await handleEditName(context);
context.pop();
},
onEditBirthday: () async {
await handleEditBirthday(context);
context.pop();
},
);
},
);
}
@override
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith((ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to view person timeline');
}
final timelineService = ref.watch(timelineFactoryProvider).person(user.id, _person.id);
ref.onDispose(timelineService.dispose);
return timelineService;
}),
],
child: Timeline(
appBar: PersonSliverAppBar(
person: _person,
onNameTap: () => handleEditName(context),
onBirthdayTap: () => handleEditBirthday(context),
onShowOptions: () => showOptionSheet(context),
),
),
);
}
}