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

@ -0,0 +1,31 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/services/api.service.dart';
class PartnerUserAvatar extends StatelessWidget {
const PartnerUserAvatar({super.key, required this.partner});
final PartnerUserDto partner;
@override
Widget build(BuildContext context) {
final url = "${Store.get(StoreKey.serverEndpoint)}/users/${partner.id}/profile-image";
final nameFirstLetter = partner.name.isNotEmpty ? partner.name[0] : "";
return CircleAvatar(
radius: 16,
backgroundColor: context.primaryColor.withAlpha(50),
foregroundImage: CachedNetworkImageProvider(
url,
headers: ApiService.getRequestHeaders(),
cacheKey: "user-${partner.id}-profile",
),
// silence errors if user has no profile image, use initials as fallback
onForegroundImageError: (exception, stackTrace) {},
child: Text(nameFirstLetter.toUpperCase()),
);
}
}

View file

@ -0,0 +1,116 @@
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';
import 'package:immich_mobile/providers/infrastructure/people.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:scroll_date_picker/scroll_date_picker.dart';
class DriftPersonBirthdayEditForm extends ConsumerStatefulWidget {
final DriftPerson person;
const DriftPersonBirthdayEditForm({super.key, required this.person});
@override
ConsumerState<DriftPersonBirthdayEditForm> createState() => _DriftPersonNameEditFormState();
}
class _DriftPersonNameEditFormState extends ConsumerState<DriftPersonBirthdayEditForm> {
late DateTime _selectedDate;
@override
void initState() {
super.initState();
_selectedDate = widget.person.birthDate ?? DateTime.now();
}
void saveBirthday() async {
try {
final result = await ref.read(driftPeopleServiceProvider).updateBrithday(widget.person.id, _selectedDate);
if (result != 0) {
ref.invalidate(driftGetAllPeopleProvider);
context.pop<DateTime>(_selectedDate);
}
} catch (error) {
debugPrint('Error updating birthday: $error');
if (!context.mounted) {
return;
}
ImmichToast.show(
context: context,
msg: 'scaffold_body_error_occurred'.t(context: context),
gravity: ToastGravity.BOTTOM,
toastType: ToastType.error,
);
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(
"edit_birthday".t(context: context),
style: const TextStyle(fontWeight: FontWeight.bold),
),
content: SizedBox(
width: double.maxFinite,
height: 300,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
child: ScrollDatePicker(
options: DatePickerOptions(
backgroundColor: context.colorScheme.surfaceContainerHigh,
itemExtent: 50,
diameterRatio: 5,
),
scrollViewOptions: DatePickerScrollViewOptions(
day: ScrollViewDetailOptions(
margin: const EdgeInsets.all(12),
selectedTextStyle: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 16),
),
month: ScrollViewDetailOptions(
margin: const EdgeInsets.all(12),
selectedTextStyle: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 16),
),
year: ScrollViewDetailOptions(
margin: const EdgeInsets.all(12),
selectedTextStyle: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 16),
),
),
selectedDate: _selectedDate,
locale: context.locale,
minimumDate: DateTime(1800, 1, 1),
onDateTimeChanged: (DateTime value) {
setState(() {
_selectedDate = value;
});
},
),
),
),
actions: [
TextButton(
onPressed: () => context.pop(null),
child: Text(
"cancel",
style: TextStyle(color: Colors.red[300], fontWeight: FontWeight.bold),
).tr(),
),
TextButton(
onPressed: () => saveBirthday(),
child: Text(
"save",
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold),
).tr(),
),
],
);
}
}

View file

@ -0,0 +1,82 @@
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';
import 'package:immich_mobile/providers/infrastructure/people.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
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;
@override
void initState() {
super.initState();
_formController = TextEditingController(text: widget.person.name);
}
void onEdit(String personId, String newName) async {
try {
final result = await ref.read(driftPeopleServiceProvider).updateName(personId, newName);
if (result != 0) {
ref.invalidate(driftGetAllPeopleProvider);
context.pop<String>(newName);
}
} catch (error) {
debugPrint('Error updating name: $error');
if (!context.mounted) {
return;
}
ImmichToast.show(
context: context,
msg: 'scaffold_body_error_occurred'.t(context: context),
gravity: ToastGravity.BOTTOM,
toastType: ToastType.error,
);
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text("edit_name", style: TextStyle(fontWeight: FontWeight.bold)).tr(),
content: SingleChildScrollView(
child: TextFormField(
controller: _formController,
textCapitalization: TextCapitalization.words,
autofocus: true,
decoration: InputDecoration(hintText: 'name'.tr(), border: const OutlineInputBorder()),
),
),
actions: [
TextButton(
onPressed: () => context.pop(null),
child: Text(
"cancel",
style: TextStyle(color: Colors.red[300], fontWeight: FontWeight.bold),
).tr(),
),
TextButton(
onPressed: () => onEdit(widget.person.id, _formController.text),
child: Text(
"save",
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold),
).tr(),
),
],
);
}
}

View file

@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
class PersonOptionSheet extends ConsumerWidget {
const PersonOptionSheet({super.key, this.onEditName, this.onEditBirthday});
final VoidCallback? onEditName;
final VoidCallback? onEditBirthday;
@override
Widget build(BuildContext context, WidgetRef ref) {
TextStyle textStyle = Theme.of(context).textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w600);
return SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: ListView(
shrinkWrap: true,
children: [
ListTile(
leading: const Icon(Icons.edit),
title: Text('edit_name'.t(context: context), style: textStyle),
onTap: onEditName,
),
ListTile(
leading: const Icon(Icons.cake),
title: Text('edit_birthday'.t(context: context), style: textStyle),
onTap: onEditBirthday,
),
],
),
),
);
}
}