mirror of
https://github.com/immich-app/immich.git
synced 2024-12-24 10:37:28 +02:00
313 lines
11 KiB
Dart
313 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/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/immich_loading_indicator.dart';
|
||
|
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
|
||
|
import 'package:immich_mobile/utils/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;
|
||
|
const ActivitiesPage(
|
||
|
this.albumId, {
|
||
|
this.appBarTitle = "",
|
||
|
this.assetId,
|
||
|
this.withAssetThumbs = true,
|
||
|
this.isOwner = 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 = Theme.of(context).brightness == Brightness.dark
|
||
|
? Colors.white
|
||
|
: Colors.black;
|
||
|
final textStyle = Theme.of(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.firstName} ${activity.user.lastName}",
|
||
|
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: BorderRadius.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,
|
||
|
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: '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.maybeWhen(
|
||
|
orElse: () {
|
||
|
return const Center(child: ImmichLoadingIndicator());
|
||
|
},
|
||
|
data: (data) {
|
||
|
final liked = data.firstWhereOrNull(
|
||
|
(a) =>
|
||
|
a.type == ActivityType.like &&
|
||
|
a.user.id == currentUser?.id &&
|
||
|
a.assetId == assetId,
|
||
|
);
|
||
|
|
||
|
return 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: Theme.of(context).scaffoldBackgroundColor,
|
||
|
child: buildTextField(liked?.id),
|
||
|
),
|
||
|
),
|
||
|
],
|
||
|
);
|
||
|
},
|
||
|
),
|
||
|
);
|
||
|
}
|
||
|
}
|