diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 194871e18d351..c14aa6d748914 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -476,6 +476,7 @@ "search_filter_media_type_video": "Video", "search_filter_people": "People", "search_filter_people_title": "Select people", + "search_filter_people_hint": "Filter people", "search_page_categories": "Categories", "search_page_favorites": "Favorites", "search_page_motion_photos": "Motion Photos", diff --git a/mobile/lib/pages/common/large_leading_tile.dart b/mobile/lib/pages/common/large_leading_tile.dart index f2cb9f19aea32..4f22a5f2b2885 100644 --- a/mobile/lib/pages/common/large_leading_tile.dart +++ b/mobile/lib/pages/common/large_leading_tile.dart @@ -16,6 +16,8 @@ class LargeLeadingTile extends StatelessWidget { this.trailing, this.selected = false, this.disabled = false, + this.selectedTileColor, + this.tileColor, }); final Widget leading; @@ -27,6 +29,9 @@ class LargeLeadingTile extends StatelessWidget { final Widget? trailing; final bool selected; final bool disabled; + final Color? selectedTileColor; + final Color? tileColor; + @override Widget build(BuildContext context) { return InkWell( @@ -35,8 +40,9 @@ class LargeLeadingTile extends StatelessWidget { child: Container( decoration: BoxDecoration( color: selected - ? Theme.of(context).primaryColor.withAlpha(30) - : Colors.transparent, + ? selectedTileColor ?? + Theme.of(context).primaryColor.withAlpha(30) + : tileColor ?? Colors.transparent, borderRadius: BorderRadius.circular(borderRadius), ), child: Row( diff --git a/mobile/lib/widgets/search/search_filter/people_picker.dart b/mobile/lib/widgets/search/search_filter/people_picker.dart index dfc435c807158..9cc74bf93983d 100644 --- a/mobile/lib/widgets/search/search_filter/people_picker.dart +++ b/mobile/lib/widgets/search/search_filter/people_picker.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/interfaces/person_api.interface.dart'; +import 'package:immich_mobile/pages/common/large_leading_tile.dart'; import 'package:immich_mobile/providers/search/people.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; @@ -16,63 +19,138 @@ class PeoplePicker extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - var imageSize = 45.0; + final formFocus = useFocusNode(); + final imageSize = 75.0; + final searchQuery = useState(''); final people = ref.watch(getAllPeopleProvider); final headers = ApiService.getRequestHeaders(); final selectedPeople = useState>(filter ?? {}); - return people.widgetWhen( - onData: (people) { - return ListView.builder( - shrinkWrap: true, - itemCount: people.length, + return Column( + children: [ + Padding( padding: const EdgeInsets.all(8), - itemBuilder: (context, index) { - final person = people[index]; - return Card( - elevation: 0, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(15)), + child: TextField( + focusNode: formFocus, + onChanged: (value) => searchQuery.value = value, + onTapOutside: (_) => formFocus.unfocus(), + decoration: InputDecoration( + contentPadding: const EdgeInsets.only(left: 24), + filled: true, + fillColor: context.primaryColor.withOpacity(0.1), + hintStyle: context.textTheme.bodyLarge?.copyWith( + color: context.themeData.colorScheme.onSurfaceSecondary, ), - child: ListTile( - title: Text( - person.name, - style: context.textTheme.bodyLarge, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + borderSide: BorderSide( + color: context.colorScheme.surfaceContainerHighest, ), - leading: SizedBox( - height: imageSize, - child: Material( - shape: const CircleBorder(side: BorderSide.none), - elevation: 3, - child: CircleAvatar( - maxRadius: imageSize / 2, - backgroundImage: NetworkImage( - getFaceThumbnailUrl(person.id), - headers: headers, - ), - ), - ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + borderSide: BorderSide( + color: context.colorScheme.surfaceContainerHighest, ), - onTap: () { - if (selectedPeople.value.contains(person)) { - selectedPeople.value.remove(person); - } else { - selectedPeople.value.add(person); - } - - selectedPeople.value = {...selectedPeople.value}; - onSelect(selectedPeople.value); - }, - selected: selectedPeople.value.contains(person), - selectedTileColor: context.primaryColor.withOpacity(0.2), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(15)), + ), + disabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + borderSide: BorderSide( + color: context.colorScheme.surfaceContainerHighest, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + borderSide: BorderSide( + color: context.colorScheme.primary.withAlpha(150), ), ), - ); - }, - ); - }, + prefixIcon: Icon( + Icons.search_rounded, + color: context.colorScheme.primary, + ), + hintText: 'search_filter_people_hint'.tr(), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 0), + child: Divider( + color: context.colorScheme.surfaceContainerHighest, + thickness: 1, + ), + ), + Expanded( + child: people.widgetWhen( + onData: (people) { + return ListView.builder( + shrinkWrap: true, + itemCount: people + .where( + (person) => person.name + .toLowerCase() + .contains(searchQuery.value.toLowerCase()), + ) + .length, + padding: const EdgeInsets.all(8), + itemBuilder: (context, index) { + final person = people + .where( + (person) => person.name + .toLowerCase() + .contains(searchQuery.value.toLowerCase()), + ) + .toList()[index]; + final isSelected = selectedPeople.value.contains(person); + + return Padding( + padding: const EdgeInsets.only(bottom: 2.0), + child: LargeLeadingTile( + title: Text( + person.name, + style: context.textTheme.bodyLarge?.copyWith( + fontSize: 20, + fontWeight: FontWeight.w500, + color: isSelected + ? context.colorScheme.onPrimary + : context.colorScheme.onSurface, + ), + ), + leading: SizedBox( + height: imageSize, + child: Material( + shape: const CircleBorder(side: BorderSide.none), + elevation: 3, + child: CircleAvatar( + maxRadius: imageSize / 2, + backgroundImage: NetworkImage( + getFaceThumbnailUrl(person.id), + headers: headers, + ), + ), + ), + ), + onTap: () { + if (selectedPeople.value.contains(person)) { + selectedPeople.value.remove(person); + } else { + selectedPeople.value.add(person); + } + + selectedPeople.value = {...selectedPeople.value}; + onSelect(selectedPeople.value); + }, + selected: isSelected, + selectedTileColor: context.primaryColor, + tileColor: context.primaryColor.withAlpha(25), + ), + ); + }, + ); + }, + ), + ), + ], ); } }