mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
feat(mobile): Facial recognition (#2507)
* Add API service * Added service, provider * merge main * update pubspec * styling * dev: add person search result page * dev: display person asset on page * dev: add rename form * style form * dev: mechanism to add name to faces * styling * fix bad merge * update api * test * revert * Add header widget * change name * show all people page * fix test * pr feedback * Add name to app bar * feedback * styling
This commit is contained in:
51
mobile/lib/modules/search/views/all_people_page.dart
Normal file
51
mobile/lib/modules/search/views/all_people_page.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
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/modules/search/models/curated_content.dart';
|
||||
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/explore_grid.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||
|
||||
class AllPeoplePage extends HookConsumerWidget {
|
||||
const AllPeoplePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final curatedPeople = ref.watch(getCuratedPeopleProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
'all_people_page_title',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16.0,
|
||||
),
|
||||
).tr(),
|
||||
leading: IconButton(
|
||||
onPressed: () => AutoRouter.of(context).pop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
),
|
||||
body: curatedPeople.when(
|
||||
loading: () => const Center(child: ImmichLoadingIndicator()),
|
||||
error: (err, stack) => Center(
|
||||
child: Text('Error: $err'),
|
||||
),
|
||||
data: (people) => ExploreGrid(
|
||||
isPeople: true,
|
||||
curatedContent: people
|
||||
.map(
|
||||
(person) => CuratedContent(
|
||||
label: person.name,
|
||||
id: person.id,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
152
mobile/lib/modules/search/views/person_result_page.dart
Normal file
152
mobile/lib/modules/search/views/person_result_page.dart
Normal file
@@ -0,0 +1,152 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
||||
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/person_name_edit_form.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart' as isar_store;
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
|
||||
class PersonResultPage extends HookConsumerWidget {
|
||||
final String personId;
|
||||
final String personName;
|
||||
|
||||
const PersonResultPage({
|
||||
super.key,
|
||||
required this.personId,
|
||||
required this.personName,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final name = useState(personName);
|
||||
|
||||
showEditNameDialog() {
|
||||
showDialog<PersonNameEditFormResult>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return PersonNameEditForm(
|
||||
personId: personId,
|
||||
personName: personName,
|
||||
);
|
||||
},
|
||||
).then((result) {
|
||||
if (result != null && result.success) {
|
||||
name.value = result.updatedName;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void buildBottomSheet() {
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
isScrollControlled: false,
|
||||
context: context,
|
||||
useSafeArea: true,
|
||||
builder: (context) {
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.edit_outlined),
|
||||
title: const Text(
|
||||
'Edit name',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
onTap: showEditNameDialog,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
buildTitleBlock() {
|
||||
if (name.value == "") {
|
||||
return GestureDetector(
|
||||
onTap: showEditNameDialog,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Add a name',
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Find them fast by name with search',
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
name.value,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(name.value),
|
||||
leading: IconButton(
|
||||
onPressed: () => AutoRouter.of(context).pop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: buildBottomSheet,
|
||||
icon: const Icon(Icons.more_vert_rounded),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ref.watch(personAssetsProvider(personId)).when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stackTrace) => Center(
|
||||
child: Text(
|
||||
error.toString(),
|
||||
),
|
||||
),
|
||||
data: (data) => data.isEmpty
|
||||
? const Center(
|
||||
child: Text('Opps'),
|
||||
)
|
||||
: ImmichAssetGrid(
|
||||
renderList: data,
|
||||
topWidget: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0, top: 24),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 36,
|
||||
backgroundImage: NetworkImage(
|
||||
getFaceThumbnailUrl(personId),
|
||||
headers: {
|
||||
"Authorization":
|
||||
"Bearer ${isar_store.Store.get(isar_store.StoreKey.accessToken)}"
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: buildTitleBlock(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,16 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/search/models/curated_content.dart';
|
||||
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
|
||||
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/curated_people_row.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/curated_row.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/immich_search_bar.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/person_name_edit_form.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/search_row_title.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class SearchPage extends HookConsumerWidget {
|
||||
@@ -21,10 +24,9 @@ class SearchPage extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled;
|
||||
AsyncValue<List<CuratedLocationsResponseDto>> curatedLocation =
|
||||
ref.watch(getCuratedLocationProvider);
|
||||
AsyncValue<List<CuratedObjectsResponseDto>> curatedObjects =
|
||||
ref.watch(getCuratedObjectProvider);
|
||||
final curatedLocation = ref.watch(getCuratedLocationProvider);
|
||||
final curatedObjects = ref.watch(getCuratedObjectProvider);
|
||||
final curatedPeople = ref.watch(getCuratedPeopleProvider);
|
||||
var isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
||||
double imageSize = MediaQuery.of(context).size.width / 3;
|
||||
|
||||
@@ -54,6 +56,50 @@ class SearchPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
showNameEditModel(
|
||||
String personId,
|
||||
String personName,
|
||||
) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return PersonNameEditForm(personId: personId, personName: personName);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
buildPeople() {
|
||||
return SizedBox(
|
||||
height: MediaQuery.of(context).size.width / 3,
|
||||
child: curatedPeople.when(
|
||||
loading: () => const Center(child: ImmichLoadingIndicator()),
|
||||
error: (err, stack) => Center(child: Text('Error: $err')),
|
||||
data: (people) => CuratedPeopleRow(
|
||||
content: people
|
||||
.map(
|
||||
(person) => CuratedContent(
|
||||
id: person.id,
|
||||
label: person.name,
|
||||
),
|
||||
)
|
||||
.take(12)
|
||||
.toList(),
|
||||
onTap: (content, index) {
|
||||
AutoRouter.of(context).push(
|
||||
PersonResultRoute(
|
||||
personId: content.id,
|
||||
personName: content.label,
|
||||
),
|
||||
);
|
||||
},
|
||||
onNameTap: (person, index) => {
|
||||
showNameEditModel(person.id, person.label),
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
buildPlaces() {
|
||||
return SizedBox(
|
||||
height: imageSize,
|
||||
@@ -130,63 +176,25 @@ class SearchPage extends HookConsumerWidget {
|
||||
children: [
|
||||
ListView(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 4.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"search_page_places",
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
).tr(),
|
||||
TextButton(
|
||||
child: Text(
|
||||
'search_page_view_all_button',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14.0,
|
||||
),
|
||||
).tr(),
|
||||
onPressed: () => AutoRouter.of(context).push(
|
||||
const CuratedLocationRoute(),
|
||||
),
|
||||
),
|
||||
],
|
||||
SearchRowTitle(
|
||||
title: "search_page_people".tr(),
|
||||
onViewAllPressed: () => AutoRouter.of(context).push(
|
||||
const AllPeopleRoute(),
|
||||
),
|
||||
),
|
||||
buildPlaces(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 24.0,
|
||||
bottom: 4.0,
|
||||
left: 16.0,
|
||||
right: 16.0,
|
||||
buildPeople(),
|
||||
SearchRowTitle(
|
||||
title: "search_page_places".tr(),
|
||||
onViewAllPressed: () => AutoRouter.of(context).push(
|
||||
const CuratedLocationRoute(),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"search_page_things",
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
).tr(),
|
||||
TextButton(
|
||||
child: Text(
|
||||
'search_page_view_all_button',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14.0,
|
||||
),
|
||||
).tr(),
|
||||
onPressed: () => AutoRouter.of(context).push(
|
||||
const CuratedObjectRoute(),
|
||||
),
|
||||
),
|
||||
],
|
||||
top: 0,
|
||||
),
|
||||
buildPlaces(),
|
||||
SearchRowTitle(
|
||||
title: "search_page_things".tr(),
|
||||
onViewAllPressed: () => AutoRouter.of(context).push(
|
||||
const CuratedObjectRoute(),
|
||||
),
|
||||
),
|
||||
buildThings(),
|
||||
@@ -200,7 +208,7 @@ class SearchPage extends HookConsumerWidget {
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
Icons.favorite_border,
|
||||
Icons.star_outline,
|
||||
color: categoryIconColor,
|
||||
),
|
||||
title:
|
||||
|
||||
Reference in New Issue
Block a user