mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-30 10:24:58 -04:00 
			
		
		
		
	* refactor: scaffoldwhen to log errors during scaffold body render * refactor: onError and onLoading scaffoldbody * refactor: more scaffold body to custom extension * refactor: add skiploadingonrefresh * Snackbar color --------- Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
		
			
				
	
	
		
			315 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			315 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:cached_network_image/cached_network_image.dart';
 | |
| import 'package:collection/collection.dart';
 | |
| import 'package:easy_localization/easy_localization.dart';
 | |
| 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/extensions/asyncvalue_extensions.dart';
 | |
| import 'package:immich_mobile/extensions/build_context_extensions.dart';
 | |
| import 'package:immich_mobile/modules/activities/models/activity.model.dart';
 | |
| import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
 | |
| import 'package:immich_mobile/shared/models/store.dart';
 | |
| import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
 | |
| import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
 | |
| import 'package:immich_mobile/extensions/datetime_extensions.dart';
 | |
| import 'package:immich_mobile/utils/image_url_builder.dart';
 | |
| 
 | |
| class ActivitiesPage extends HookConsumerWidget {
 | |
|   final String albumId;
 | |
|   final String? assetId;
 | |
|   final bool withAssetThumbs;
 | |
|   final String appBarTitle;
 | |
|   final bool isOwner;
 | |
|   final bool isReadOnly;
 | |
|   const ActivitiesPage(
 | |
|     this.albumId, {
 | |
|     this.appBarTitle = "",
 | |
|     this.assetId,
 | |
|     this.withAssetThumbs = true,
 | |
|     this.isOwner = false,
 | |
|     this.isReadOnly = false,
 | |
|     super.key,
 | |
|   });
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context, WidgetRef ref) {
 | |
|     final provider =
 | |
|         activityStateProvider((albumId: albumId, assetId: assetId));
 | |
|     final activities = ref.watch(provider);
 | |
|     final inputController = useTextEditingController();
 | |
|     final inputFocusNode = useFocusNode();
 | |
|     final listViewScrollController = useScrollController();
 | |
|     final currentUser = Store.tryGet(StoreKey.currentUser);
 | |
| 
 | |
|     useEffect(
 | |
|       () {
 | |
|         inputFocusNode.requestFocus();
 | |
|         return null;
 | |
|       },
 | |
|       [],
 | |
|     );
 | |
| 
 | |
|     buildTitleWithTimestamp(Activity activity, {bool leftAlign = true}) {
 | |
|       final textColor = context.isDarkTheme ? Colors.white : Colors.black;
 | |
|       final textStyle = context.textTheme.bodyMedium
 | |
|           ?.copyWith(color: textColor.withOpacity(0.6));
 | |
| 
 | |
|       return Row(
 | |
|         mainAxisAlignment: leftAlign
 | |
|             ? MainAxisAlignment.start
 | |
|             : MainAxisAlignment.spaceBetween,
 | |
|         mainAxisSize: leftAlign ? MainAxisSize.min : MainAxisSize.max,
 | |
|         children: [
 | |
|           Text(
 | |
|             activity.user.name,
 | |
|             style: textStyle,
 | |
|             overflow: TextOverflow.ellipsis,
 | |
|           ),
 | |
|           if (leftAlign)
 | |
|             Text(
 | |
|               " • ",
 | |
|               style: textStyle,
 | |
|             ),
 | |
|           Expanded(
 | |
|             child: Text(
 | |
|               activity.createdAt.copyWith().timeAgo(),
 | |
|               style: textStyle,
 | |
|               overflow: TextOverflow.ellipsis,
 | |
|               textAlign: leftAlign ? TextAlign.left : TextAlign.right,
 | |
|             ),
 | |
|           ),
 | |
|         ],
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     buildAssetThumbnail(Activity activity) {
 | |
|       return withAssetThumbs && activity.assetId != null
 | |
|           ? Container(
 | |
|               width: 40,
 | |
|               height: 30,
 | |
|               decoration: BoxDecoration(
 | |
|                 borderRadius: const BorderRadius.all(Radius.circular(4)),
 | |
|                 image: DecorationImage(
 | |
|                   image: CachedNetworkImageProvider(
 | |
|                     getThumbnailUrlForRemoteId(
 | |
|                       activity.assetId!,
 | |
|                     ),
 | |
|                     cacheKey: getThumbnailCacheKeyForRemoteId(
 | |
|                       activity.assetId!,
 | |
|                     ),
 | |
|                     headers: {
 | |
|                       "Authorization":
 | |
|                           'Bearer ${Store.get(StoreKey.accessToken)}',
 | |
|                     },
 | |
|                   ),
 | |
|                   fit: BoxFit.cover,
 | |
|                 ),
 | |
|               ),
 | |
|               child: const SizedBox.shrink(),
 | |
|             )
 | |
|           : null;
 | |
|     }
 | |
| 
 | |
|     buildTextField(String? likedId) {
 | |
|       final liked = likedId != null;
 | |
|       return Padding(
 | |
|         padding: const EdgeInsets.only(bottom: 10),
 | |
|         child: TextField(
 | |
|           controller: inputController,
 | |
|           enabled: !isReadOnly,
 | |
|           focusNode: inputFocusNode,
 | |
|           textInputAction: TextInputAction.send,
 | |
|           autofocus: false,
 | |
|           decoration: InputDecoration(
 | |
|             border: InputBorder.none,
 | |
|             focusedBorder: InputBorder.none,
 | |
|             prefixIcon: currentUser != null
 | |
|                 ? Padding(
 | |
|                     padding: const EdgeInsets.symmetric(horizontal: 15),
 | |
|                     child: UserCircleAvatar(
 | |
|                       user: currentUser,
 | |
|                       size: 30,
 | |
|                       radius: 15,
 | |
|                     ),
 | |
|                   )
 | |
|                 : null,
 | |
|             suffixIcon: Padding(
 | |
|               padding: const EdgeInsets.only(right: 10),
 | |
|               child: IconButton(
 | |
|                 icon: Icon(
 | |
|                   liked
 | |
|                       ? Icons.favorite_rounded
 | |
|                       : Icons.favorite_border_rounded,
 | |
|                 ),
 | |
|                 onPressed: () async {
 | |
|                   liked
 | |
|                       ? await ref
 | |
|                           .read(provider.notifier)
 | |
|                           .removeActivity(likedId)
 | |
|                       : await ref.read(provider.notifier).addLike();
 | |
|                 },
 | |
|               ),
 | |
|             ),
 | |
|             suffixIconColor: liked ? Colors.red[700] : null,
 | |
|             hintText: isReadOnly
 | |
|                 ? 'shared_album_activities_input_disable'.tr()
 | |
|                 : 'shared_album_activities_input_hint'.tr(),
 | |
|             hintStyle: TextStyle(
 | |
|               fontWeight: FontWeight.normal,
 | |
|               fontSize: 14,
 | |
|               color: Colors.grey[600],
 | |
|             ),
 | |
|           ),
 | |
|           onEditingComplete: () async {
 | |
|             await ref.read(provider.notifier).addComment(inputController.text);
 | |
|             inputController.clear();
 | |
|             inputFocusNode.unfocus();
 | |
|             listViewScrollController.animateTo(
 | |
|               listViewScrollController.position.maxScrollExtent,
 | |
|               duration: const Duration(milliseconds: 800),
 | |
|               curve: Curves.fastOutSlowIn,
 | |
|             );
 | |
|           },
 | |
|           onTapOutside: (_) => inputFocusNode.unfocus(),
 | |
|         ),
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     getDismissibleWidget(
 | |
|       Widget widget,
 | |
|       Activity activity,
 | |
|       bool canDelete,
 | |
|     ) {
 | |
|       return Dismissible(
 | |
|         key: Key(activity.id),
 | |
|         dismissThresholds: const {
 | |
|           DismissDirection.horizontal: 0.7,
 | |
|         },
 | |
|         direction: DismissDirection.horizontal,
 | |
|         confirmDismiss: (direction) => canDelete
 | |
|             ? showDialog(
 | |
|                 context: context,
 | |
|                 builder: (context) => ConfirmDialog(
 | |
|                   onOk: () {},
 | |
|                   title: "shared_album_activity_remove_title",
 | |
|                   content: "shared_album_activity_remove_content",
 | |
|                   ok: "delete_dialog_ok",
 | |
|                 ),
 | |
|               )
 | |
|             : Future.value(false),
 | |
|         onDismissed: (direction) async =>
 | |
|             await ref.read(provider.notifier).removeActivity(activity.id),
 | |
|         background: Container(
 | |
|           color: canDelete ? Colors.red[400] : Colors.grey[600],
 | |
|           alignment: AlignmentDirectional.centerStart,
 | |
|           child: canDelete
 | |
|               ? const Padding(
 | |
|                   padding: EdgeInsets.all(15),
 | |
|                   child: Icon(
 | |
|                     Icons.delete_sweep_rounded,
 | |
|                     color: Colors.black,
 | |
|                   ),
 | |
|                 )
 | |
|               : null,
 | |
|         ),
 | |
|         secondaryBackground: Container(
 | |
|           color: canDelete ? Colors.red[400] : Colors.grey[600],
 | |
|           alignment: AlignmentDirectional.centerEnd,
 | |
|           child: canDelete
 | |
|               ? const Padding(
 | |
|                   padding: EdgeInsets.all(15),
 | |
|                   child: Icon(
 | |
|                     Icons.delete_sweep_rounded,
 | |
|                     color: Colors.black,
 | |
|                   ),
 | |
|                 )
 | |
|               : null,
 | |
|         ),
 | |
|         child: widget,
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     return Scaffold(
 | |
|       appBar: AppBar(title: Text(appBarTitle)),
 | |
|       body: activities.widgetWhen(
 | |
|         onData: (data) {
 | |
|           final liked = data.firstWhereOrNull(
 | |
|             (a) =>
 | |
|                 a.type == ActivityType.like &&
 | |
|                 a.user.id == currentUser?.id &&
 | |
|                 a.assetId == assetId,
 | |
|           );
 | |
| 
 | |
|           return SafeArea(
 | |
|             child: Stack(
 | |
|               children: [
 | |
|                 ListView.builder(
 | |
|                   controller: listViewScrollController,
 | |
|                   itemCount: data.length + 1,
 | |
|                   itemBuilder: (context, index) {
 | |
|                     // Vertical gap after the last element
 | |
|                     if (index == data.length) {
 | |
|                       return const SizedBox(
 | |
|                         height: 80,
 | |
|                       );
 | |
|                     }
 | |
| 
 | |
|                     final activity = data[index];
 | |
|                     final canDelete =
 | |
|                         activity.user.id == currentUser?.id || isOwner;
 | |
| 
 | |
|                     return Padding(
 | |
|                       padding: const EdgeInsets.all(5),
 | |
|                       child: activity.type == ActivityType.comment
 | |
|                           ? getDismissibleWidget(
 | |
|                               ListTile(
 | |
|                                 minVerticalPadding: 15,
 | |
|                                 leading: UserCircleAvatar(user: activity.user),
 | |
|                                 title: buildTitleWithTimestamp(
 | |
|                                   activity,
 | |
|                                   leftAlign: withAssetThumbs &&
 | |
|                                       activity.assetId != null,
 | |
|                                 ),
 | |
|                                 titleAlignment: ListTileTitleAlignment.top,
 | |
|                                 trailing: buildAssetThumbnail(activity),
 | |
|                                 subtitle: Text(activity.comment!),
 | |
|                               ),
 | |
|                               activity,
 | |
|                               canDelete,
 | |
|                             )
 | |
|                           : getDismissibleWidget(
 | |
|                               ListTile(
 | |
|                                 minVerticalPadding: 15,
 | |
|                                 leading: Container(
 | |
|                                   width: 44,
 | |
|                                   alignment: Alignment.center,
 | |
|                                   child: Icon(
 | |
|                                     Icons.favorite_rounded,
 | |
|                                     color: Colors.red[700],
 | |
|                                   ),
 | |
|                                 ),
 | |
|                                 title: buildTitleWithTimestamp(activity),
 | |
|                                 trailing: buildAssetThumbnail(activity),
 | |
|                               ),
 | |
|                               activity,
 | |
|                               canDelete,
 | |
|                             ),
 | |
|                     );
 | |
|                   },
 | |
|                 ),
 | |
|                 Align(
 | |
|                   alignment: Alignment.bottomCenter,
 | |
|                   child: Container(
 | |
|                     color: context.scaffoldBackgroundColor,
 | |
|                     child: buildTextField(liked?.id),
 | |
|                   ),
 | |
|                 ),
 | |
|               ],
 | |
|             ),
 | |
|           );
 | |
|         },
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 |