2025-07-29 22:07:53 -05:00
|
|
|
import 'package:easy_localization/easy_localization.dart';
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:fluttertoast/fluttertoast.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/extensions/translate_extensions.dart';
|
2025-10-01 20:38:29 +02:00
|
|
|
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
|
|
|
|
import 'package:immich_mobile/presentation/widgets/people/person_tile.widget.dart';
|
2025-07-29 22:07:53 -05:00
|
|
|
import 'package:immich_mobile/providers/infrastructure/people.provider.dart';
|
2025-09-12 18:56:00 -04:00
|
|
|
import 'package:immich_mobile/utils/debug_print.dart';
|
2025-10-01 20:38:29 +02:00
|
|
|
import 'package:immich_mobile/utils/people.utils.dart';
|
|
|
|
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
2025-07-29 22:07:53 -05:00
|
|
|
|
|
|
|
|
class DriftPersonNameEditForm extends ConsumerStatefulWidget {
|
|
|
|
|
final DriftPerson person;
|
|
|
|
|
|
|
|
|
|
const DriftPersonNameEditForm({super.key, required this.person});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
ConsumerState<DriftPersonNameEditForm> createState() => _DriftPersonNameEditFormState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _DriftPersonNameEditFormState extends ConsumerState<DriftPersonNameEditForm> {
|
|
|
|
|
late TextEditingController _formController;
|
2025-10-01 20:38:29 +02:00
|
|
|
List<DriftPerson> _filteredPeople = [];
|
2025-07-29 22:07:53 -05:00
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
|
|
|
|
_formController = TextEditingController(text: widget.person.name);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-01 20:38:29 +02:00
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
_formController.dispose();
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void onMerge({required BuildContext context, required DriftPerson person, required DriftPerson mergeTarget}) async {
|
|
|
|
|
DriftPerson? response = await showMergeModal(context, person, mergeTarget);
|
|
|
|
|
if (response != null) {
|
|
|
|
|
if (mounted) {
|
|
|
|
|
context.pop<DriftPerson?>(response);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void onEdit(DriftPerson person, String newName) async {
|
2025-07-29 22:07:53 -05:00
|
|
|
try {
|
2025-10-01 20:38:29 +02:00
|
|
|
final result = await ref.read(driftPeopleServiceProvider).updateName(person.id, newName);
|
2025-07-29 22:07:53 -05:00
|
|
|
if (result != 0) {
|
|
|
|
|
ref.invalidate(driftGetAllPeopleProvider);
|
2025-10-01 20:38:29 +02:00
|
|
|
if (mounted) {
|
|
|
|
|
context.pop<DriftPerson>(person);
|
|
|
|
|
}
|
2025-07-29 22:07:53 -05:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2025-09-12 18:56:00 -04:00
|
|
|
dPrint(() => 'Error updating name: $error');
|
2025-07-29 22:07:53 -05:00
|
|
|
|
|
|
|
|
if (!context.mounted) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImmichToast.show(
|
|
|
|
|
context: context,
|
|
|
|
|
msg: 'scaffold_body_error_occurred'.t(context: context),
|
|
|
|
|
gravity: ToastGravity.BOTTOM,
|
|
|
|
|
toastType: ToastType.error,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-01 20:38:29 +02:00
|
|
|
// TODO: Add diacritic filtering? We would need to add a package.
|
|
|
|
|
void _filterPeople(List<DriftPerson> people, String query) {
|
|
|
|
|
final queryParts = query.toLowerCase().split(' ').where((e) => e.isNotEmpty).toList();
|
|
|
|
|
|
|
|
|
|
List<DriftPerson> startsWithMatches = [];
|
|
|
|
|
List<DriftPerson> containsMatches = [];
|
|
|
|
|
|
|
|
|
|
for (final p in people) {
|
|
|
|
|
final nameParts = p.name.toLowerCase().split(' ').where((e) => e.isNotEmpty).toList();
|
|
|
|
|
final allStart = queryParts.every((q) => nameParts.any((n) => n.startsWith(q)));
|
|
|
|
|
final allContain = queryParts.every((q) => nameParts.any((n) => n.contains(q)));
|
|
|
|
|
|
|
|
|
|
if (allStart) {
|
|
|
|
|
// Prioritize names that start with the query
|
|
|
|
|
startsWithMatches.add(p);
|
|
|
|
|
} else if (allContain) {
|
|
|
|
|
containsMatches.add(p);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!mounted) return;
|
|
|
|
|
setState(() {
|
|
|
|
|
// TODO: What happens if there are more than 3 matches with the exact same name?
|
|
|
|
|
_filteredPeople = query.isEmpty ? [] : (startsWithMatches + containsMatches).take(3).toList();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-29 22:07:53 -05:00
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2025-10-01 20:38:29 +02:00
|
|
|
final curatedPeople = ref.watch(driftGetAllPeopleProvider);
|
|
|
|
|
|
2025-07-29 22:07:53 -05:00
|
|
|
return AlertDialog(
|
|
|
|
|
title: const Text("edit_name", style: TextStyle(fontWeight: FontWeight.bold)).tr(),
|
2025-10-01 20:38:29 +02:00
|
|
|
content: curatedPeople.when(
|
|
|
|
|
data: (people) {
|
|
|
|
|
return SingleChildScrollView(
|
|
|
|
|
child: Column(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
TextFormField(
|
|
|
|
|
autofocus: true,
|
|
|
|
|
controller: _formController,
|
|
|
|
|
decoration: InputDecoration(
|
|
|
|
|
hintText: 'add_a_name'.tr(),
|
|
|
|
|
border: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(8))),
|
|
|
|
|
),
|
|
|
|
|
onChanged: (value) => _filterPeople(people, value),
|
|
|
|
|
onTapOutside: (event) => FocusScope.of(context).unfocus(),
|
|
|
|
|
),
|
|
|
|
|
AnimatedSize(
|
|
|
|
|
duration: const Duration(milliseconds: 200),
|
|
|
|
|
child: SizedBox(
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
child: _filteredPeople.isEmpty
|
|
|
|
|
// Tile instead of a blank space to avoid horizontal layout shift
|
|
|
|
|
? LargeLeadingTile(
|
|
|
|
|
leading: const SizedBox.shrink(),
|
|
|
|
|
onTap: () {},
|
|
|
|
|
title: const SizedBox.shrink(),
|
|
|
|
|
disabled: true,
|
|
|
|
|
)
|
|
|
|
|
: Container(
|
|
|
|
|
margin: const EdgeInsets.only(top: 8),
|
|
|
|
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
|
|
|
|
|
child: Column(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: _filteredPeople.map((person) {
|
|
|
|
|
return PersonTile(
|
|
|
|
|
isSelected: false,
|
|
|
|
|
onTap: () {
|
|
|
|
|
if (!mounted) return;
|
|
|
|
|
setState(() {
|
|
|
|
|
_formController.text = person.name;
|
|
|
|
|
});
|
|
|
|
|
_formController.selection = TextSelection.fromPosition(
|
|
|
|
|
TextPosition(offset: _formController.text.length),
|
|
|
|
|
);
|
|
|
|
|
onMerge(context: context, person: widget.person, mergeTarget: person);
|
|
|
|
|
},
|
|
|
|
|
personName: person.name,
|
|
|
|
|
personId: person.id,
|
|
|
|
|
);
|
|
|
|
|
}).toList(),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
loading: () => const Center(child: CircularProgressIndicator()),
|
|
|
|
|
error: (err, stack) => Text('Error: $err'),
|
2025-07-29 22:07:53 -05:00
|
|
|
),
|
|
|
|
|
actions: [
|
|
|
|
|
TextButton(
|
|
|
|
|
onPressed: () => context.pop(null),
|
|
|
|
|
child: Text(
|
|
|
|
|
"cancel",
|
|
|
|
|
style: TextStyle(color: Colors.red[300], fontWeight: FontWeight.bold),
|
|
|
|
|
).tr(),
|
|
|
|
|
),
|
|
|
|
|
TextButton(
|
2025-10-01 20:38:29 +02:00
|
|
|
onPressed: () => onEdit(widget.person, _formController.text),
|
2025-07-29 22:07:53 -05:00
|
|
|
child: Text(
|
|
|
|
|
"save",
|
|
|
|
|
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold),
|
|
|
|
|
).tr(),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|