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:
44
mobile/lib/modules/search/providers/people.provider.dart
Normal file
44
mobile/lib/modules/search/providers/people.provider.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/modules/search/services/person.service.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final personAssetsProvider = FutureProvider.family
|
||||
.autoDispose<RenderList, String>((ref, personId) async {
|
||||
final PersonService personService = ref.watch(personServiceProvider);
|
||||
|
||||
final assets = await personService.getPersonAssets(personId);
|
||||
|
||||
if (assets == null) {
|
||||
return RenderList.empty();
|
||||
}
|
||||
|
||||
return RenderList.fromAssets(assets, GroupAssetsBy.auto);
|
||||
});
|
||||
|
||||
final getCuratedPeopleProvider =
|
||||
FutureProvider.autoDispose<List<PersonResponseDto>>((ref) async {
|
||||
final PersonService personService = ref.watch(personServiceProvider);
|
||||
|
||||
final curatedPeople = await personService.getCuratedPeople();
|
||||
|
||||
return curatedPeople ?? [];
|
||||
});
|
||||
|
||||
class UpdatePersonName {
|
||||
final String id;
|
||||
final String name;
|
||||
|
||||
UpdatePersonName(this.id, this.name);
|
||||
}
|
||||
|
||||
final updatePersonNameProvider =
|
||||
StateProvider.family<void, UpdatePersonName>((ref, dto) async {
|
||||
final PersonService personService = ref.watch(personServiceProvider);
|
||||
|
||||
final person = await personService.updateName(dto.id, dto.name);
|
||||
|
||||
if (person != null && person.name == dto.name) {
|
||||
ref.invalidate(getCuratedPeopleProvider);
|
||||
}
|
||||
});
|
||||
56
mobile/lib/modules/search/services/person.service.dart
Normal file
56
mobile/lib/modules/search/services/person.service.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final personServiceProvider = Provider(
|
||||
(ref) => PersonService(
|
||||
ref.watch(apiServiceProvider),
|
||||
),
|
||||
);
|
||||
|
||||
class PersonService {
|
||||
final ApiService _apiService;
|
||||
|
||||
PersonService(this._apiService);
|
||||
|
||||
Future<List<PersonResponseDto>?> getCuratedPeople() async {
|
||||
try {
|
||||
return await _apiService.personApi.getAllPeople();
|
||||
} catch (e) {
|
||||
debugPrint("Error [getCuratedPeople] ${e.toString()}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Asset>?> getPersonAssets(String id) async {
|
||||
try {
|
||||
final assets = await _apiService.personApi.getPersonAssets(id);
|
||||
|
||||
if (assets == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return assets.map((e) => Asset.remote(e)).toList();
|
||||
} catch (e) {
|
||||
debugPrint("Error [getPersonAssets] ${e.toString()}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<PersonResponseDto?> updateName(String id, String name) async {
|
||||
try {
|
||||
return await _apiService.personApi.updatePerson(
|
||||
id,
|
||||
PersonUpdateDto(
|
||||
name: name,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint("Error [updateName] ${e.toString()}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
114
mobile/lib/modules/search/ui/curated_people_row.dart
Normal file
114
mobile/lib/modules/search/ui/curated_people_row.dart
Normal file
@@ -0,0 +1,114 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/modules/search/models/curated_content.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
|
||||
class CuratedPeopleRow extends StatelessWidget {
|
||||
final List<CuratedContent> content;
|
||||
|
||||
/// Callback with the content and the index when tapped
|
||||
final Function(CuratedContent, int)? onTap;
|
||||
final Function(CuratedContent, int)? onNameTap;
|
||||
|
||||
const CuratedPeopleRow({
|
||||
super.key,
|
||||
required this.content,
|
||||
this.onTap,
|
||||
required this.onNameTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const imageSize = 85.0;
|
||||
|
||||
// Guard empty [content]
|
||||
if (content.isEmpty) {
|
||||
// Return empty thumbnail
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: SizedBox(
|
||||
width: imageSize,
|
||||
height: imageSize,
|
||||
child: ThumbnailWithInfo(
|
||||
textInfo: '',
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
top: 8,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final person = content[index];
|
||||
final headers = {
|
||||
"Authorization": "Bearer ${Store.get(StoreKey.accessToken)}"
|
||||
};
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 18.0),
|
||||
child: SizedBox(
|
||||
width: imageSize,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => onTap?.call(person, index),
|
||||
child: 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (person.label == "")
|
||||
GestureDetector(
|
||||
onTap: () => onNameTap?.call(person, index),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
"Add name",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
person.label,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13.0,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: content.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,16 @@ import 'package:immich_mobile/modules/search/models/curated_content.dart';
|
||||
import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
|
||||
class ExploreGrid extends StatelessWidget {
|
||||
final List<CuratedContent> curatedContent;
|
||||
final bool isPeople;
|
||||
|
||||
const ExploreGrid({
|
||||
super.key,
|
||||
required this.curatedContent,
|
||||
this.isPeople = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -36,16 +40,25 @@ class ExploreGrid extends StatelessWidget {
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final content = curatedContent[index];
|
||||
final thumbnailRequestUrl =
|
||||
'${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/${content.id}';
|
||||
final thumbnailRequestUrl = isPeople
|
||||
? getFaceThumbnailUrl(content.id)
|
||||
: '${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/${content.id}';
|
||||
|
||||
return ThumbnailWithInfo(
|
||||
imageUrl: thumbnailRequestUrl,
|
||||
textInfo: content.label,
|
||||
borderRadius: 0,
|
||||
onTap: () {
|
||||
AutoRouter.of(context).push(
|
||||
SearchResultRoute(searchTerm: 'm:${content.label}'),
|
||||
);
|
||||
isPeople
|
||||
? AutoRouter.of(context).push(
|
||||
PersonResultRoute(
|
||||
personId: content.id,
|
||||
personName: content.label,
|
||||
),
|
||||
)
|
||||
: AutoRouter.of(context).push(
|
||||
SearchResultRoute(searchTerm: 'm:${content.label}'),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
82
mobile/lib/modules/search/ui/person_name_edit_form.dart
Normal file
82
mobile/lib/modules/search/ui/person_name_edit_form.dart
Normal file
@@ -0,0 +1,82 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
|
||||
|
||||
class PersonNameEditFormResult {
|
||||
final bool success;
|
||||
final String updatedName;
|
||||
|
||||
PersonNameEditFormResult(this.success, this.updatedName);
|
||||
}
|
||||
|
||||
class PersonNameEditForm extends HookConsumerWidget {
|
||||
final String personId;
|
||||
final String personName;
|
||||
|
||||
const PersonNameEditForm({
|
||||
super.key,
|
||||
required this.personId,
|
||||
required this.personName,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final controller = useTextEditingController(text: personName);
|
||||
|
||||
return AlertDialog(
|
||||
title: const Text(
|
||||
"Add a name",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
content: SingleChildScrollView(
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Name',
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(),
|
||||
onPressed: () {
|
||||
Navigator.of(context, rootNavigator: true)
|
||||
.pop<PersonNameEditFormResult>(
|
||||
PersonNameEditFormResult(false, ''),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
"Cancel",
|
||||
style: TextStyle(
|
||||
color: Colors.red[300],
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
ref.read(
|
||||
updatePersonNameProvider(
|
||||
UpdatePersonName(personId, controller.text),
|
||||
),
|
||||
);
|
||||
|
||||
Navigator.of(context, rootNavigator: true)
|
||||
.pop<PersonNameEditFormResult>(
|
||||
PersonNameEditFormResult(true, controller.text),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
"Save",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
46
mobile/lib/modules/search/ui/search_row_title.dart
Normal file
46
mobile/lib/modules/search/ui/search_row_title.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SearchRowTitle extends StatelessWidget {
|
||||
final Function() onViewAllPressed;
|
||||
final String title;
|
||||
final double top;
|
||||
|
||||
const SearchRowTitle({
|
||||
super.key,
|
||||
required this.onViewAllPressed,
|
||||
required this.title,
|
||||
this.top = 12,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 16.0,
|
||||
right: 16.0,
|
||||
top: top,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: onViewAllPressed,
|
||||
child: Text(
|
||||
'search_page_view_all_button',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14.0,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
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