feat(mobile): shared album activities (#4833)

* fix(server): global activity like duplicate search

* mobile: user_circle_avatar - fallback to text icon if no profile pic available

* mobile: use favourite icon in search "your activity"

* feat(mobile): shared album activities

* mobile: align hearts with user profile icon

* styling

* replace bottom sheet with dismissible

* add auto focus to the input

---------

Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
shenlong 2023-11-06 15:46:26 +00:00 committed by GitHub
parent c74ea7282a
commit 26fd9d7e5f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 890 additions and 15 deletions

View file

@ -0,0 +1,90 @@
import 'package:immich_mobile/shared/models/user.dart';
import 'package:openapi/api.dart';
enum ActivityType { comment, like }
class Activity {
final String id;
final String? assetId;
final String? comment;
final DateTime createdAt;
final ActivityType type;
final User user;
const Activity({
required this.id,
this.assetId,
this.comment,
required this.createdAt,
required this.type,
required this.user,
});
Activity copyWith({
String? id,
String? assetId,
String? comment,
DateTime? createdAt,
ActivityType? type,
User? user,
}) {
return Activity(
id: id ?? this.id,
assetId: assetId ?? this.assetId,
comment: comment ?? this.comment,
createdAt: createdAt ?? this.createdAt,
type: type ?? this.type,
user: user ?? this.user,
);
}
Activity.fromDto(ActivityResponseDto dto)
: id = dto.id,
assetId = dto.assetId,
comment = dto.comment,
createdAt = dto.createdAt,
type = dto.type == ActivityResponseDtoTypeEnum.comment
? ActivityType.comment
: ActivityType.like,
user = User(
email: dto.user.email,
firstName: dto.user.firstName,
lastName: dto.user.lastName,
profileImagePath: dto.user.profileImagePath,
id: dto.user.id,
// Placeholder values
isAdmin: false,
updatedAt: DateTime.now(),
isPartnerSharedBy: false,
isPartnerSharedWith: false,
memoryEnabled: false,
);
@override
String toString() {
return 'Activity(id: $id, assetId: $assetId, comment: $comment, createdAt: $createdAt, type: $type, user: $user)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Activity &&
other.id == id &&
other.assetId == assetId &&
other.comment == comment &&
other.createdAt == createdAt &&
other.type == type &&
other.user == user;
}
@override
int get hashCode {
return id.hashCode ^
assetId.hashCode ^
comment.hashCode ^
createdAt.hashCode ^
type.hashCode ^
user.hashCode;
}
}

View file

@ -0,0 +1,130 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/activities/models/activity.model.dart';
import 'package:immich_mobile/modules/activities/services/activity.service.dart';
class ActivityNotifier extends StateNotifier<AsyncValue<List<Activity>>> {
final Ref _ref;
final ActivityService _activityService;
final String albumId;
final String? assetId;
ActivityNotifier(
this._ref,
this._activityService,
this.albumId,
this.assetId,
) : super(
const AsyncData([]),
) {
fetchActivity();
}
Future<void> fetchActivity() async {
state = const AsyncLoading();
state = await AsyncValue.guard(
() => _activityService.getAllActivities(albumId, assetId),
);
}
Future<void> removeActivity(String id) async {
final activities = state.asData?.value ?? [];
if (await _activityService.removeActivity(id)) {
final removedActivity = activities.firstWhere((a) => a.id == id);
activities.remove(removedActivity);
state = AsyncData(activities);
if (removedActivity.type == ActivityType.comment) {
_ref
.read(
activityStatisticsStateProvider(
(albumId: albumId, assetId: assetId),
).notifier,
)
.removeActivity();
}
}
}
Future<void> addComment(String comment) async {
final activity = await _activityService.addActivity(
albumId,
ActivityType.comment,
assetId: assetId,
comment: comment,
);
if (activity != null) {
final activities = state.asData?.value ?? [];
state = AsyncData([...activities, activity]);
_ref
.read(
activityStatisticsStateProvider(
(albumId: albumId, assetId: assetId),
).notifier,
)
.addActivity();
if (assetId != null) {
// Add a count to the current album's provider as well
_ref
.read(
activityStatisticsStateProvider(
(albumId: albumId, assetId: null),
).notifier,
)
.addActivity();
}
}
}
Future<void> addLike() async {
final activity = await _activityService
.addActivity(albumId, ActivityType.like, assetId: assetId);
if (activity != null) {
final activities = state.asData?.value ?? [];
state = AsyncData([...activities, activity]);
}
}
}
class ActivityStatisticsNotifier extends StateNotifier<int> {
final String albumId;
final String? assetId;
final ActivityService _activityService;
ActivityStatisticsNotifier(this._activityService, this.albumId, this.assetId)
: super(0) {
fetchStatistics();
}
Future<void> fetchStatistics() async {
state = await _activityService.getStatistics(albumId, assetId: assetId);
}
Future<void> addActivity() async {
state = state + 1;
}
Future<void> removeActivity() async {
state = state - 1;
}
}
typedef ActivityParams = ({String albumId, String? assetId});
final activityStateProvider = StateNotifierProvider.autoDispose
.family<ActivityNotifier, AsyncValue<List<Activity>>, ActivityParams>(
(ref, args) {
return ActivityNotifier(
ref,
ref.watch(activityServiceProvider),
args.albumId,
args.assetId,
);
});
final activityStatisticsStateProvider = StateNotifierProvider.autoDispose
.family<ActivityStatisticsNotifier, int, ActivityParams>((ref, args) {
return ActivityStatisticsNotifier(
ref.watch(activityServiceProvider),
args.albumId,
args.assetId,
);
});

View file

@ -0,0 +1,85 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/activities/models/activity.model.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
final activityServiceProvider =
Provider((ref) => ActivityService(ref.watch(apiServiceProvider)));
class ActivityService {
final ApiService _apiService;
final Logger _log = Logger("ActivityService");
ActivityService(this._apiService);
Future<List<Activity>> getAllActivities(
String albumId,
String? assetId,
) async {
try {
final list = await _apiService.activityApi
.getActivities(albumId, assetId: assetId);
return list != null ? list.map(Activity.fromDto).toList() : [];
} catch (e) {
_log.severe(
"failed to fetch activities for albumId - $albumId; assetId - $assetId -> $e",
);
rethrow;
}
}
Future<int> getStatistics(String albumId, {String? assetId}) async {
try {
final dto = await _apiService.activityApi
.getActivityStatistics(albumId, assetId: assetId);
return dto?.comments ?? 0;
} catch (e) {
_log.severe(
"failed to fetch activity statistics for albumId - $albumId; assetId - $assetId -> $e",
);
}
return 0;
}
Future<bool> removeActivity(String id) async {
try {
await _apiService.activityApi.deleteActivity(id);
return true;
} catch (e) {
_log.severe(
"failed to remove activity id - $id -> $e",
);
}
return false;
}
Future<Activity?> addActivity(
String albumId,
ActivityType type, {
String? assetId,
String? comment,
}) async {
try {
final dto = await _apiService.activityApi.createActivity(
ActivityCreateDto(
albumId: albumId,
type: type == ActivityType.comment
? ReactionType.comment
: ReactionType.like,
assetId: assetId,
comment: comment,
),
);
if (dto != null) {
return Activity.fromDto(dto);
}
} catch (e) {
_log.severe(
"failed to add activity for albumId - $albumId; assetId - $assetId -> $e",
);
}
return null;
}
}

View file

@ -0,0 +1,312 @@
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),
),
),
],
);
},
),
);
}
}

View file

@ -3,6 +3,7 @@ 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/modules/activities/providers/activity.provider.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
@ -26,6 +27,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
required this.titleFocusNode,
this.onAddPhotos,
this.onAddUsers,
required this.onActivities,
}) : super(key: key);
final Album album;
@ -35,11 +37,19 @@ class AlbumViewerAppbar extends HookConsumerWidget
final FocusNode titleFocusNode;
final Function(Album album)? onAddPhotos;
final Function(Album album)? onAddUsers;
final Function(Album album) onActivities;
@override
Widget build(BuildContext context, WidgetRef ref) {
final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
final comments = album.shared
? ref.watch(
activityStatisticsStateProvider(
(albumId: album.remoteId!, assetId: null),
),
)
: 0;
deleteAlbum() async {
ImmichLoadingOverlayController.appLoader.show();
@ -310,6 +320,33 @@ class AlbumViewerAppbar extends HookConsumerWidget
);
}
Widget buildActivitiesButton() {
return IconButton(
onPressed: () {
onActivities(album);
},
icon: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(
Icons.mode_comment_outlined,
),
if (comments != 0)
Padding(
padding: const EdgeInsets.only(left: 5),
child: Text(
comments.toString(),
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
),
),
],
),
);
}
buildLeadingButton() {
if (selected.isNotEmpty) {
return IconButton(
@ -353,6 +390,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
title: selected.isNotEmpty ? Text('${selected.length}') : null,
centerTitle: false,
actions: [
if (album.shared) buildActivitiesButton(),
if (album.isRemote)
IconButton(
splashRadius: 25,

View file

@ -232,6 +232,18 @@ class AlbumViewerPage extends HookConsumerWidget {
);
}
onActivitiesPressed(Album album) {
if (album.remoteId != null) {
AutoRouter.of(context).push(
ActivitiesRoute(
albumId: album.remoteId!,
appBarTitle: album.name,
isOwner: userId == album.ownerId,
),
);
}
}
return Scaffold(
appBar: album.when(
data: (data) => AlbumViewerAppbar(
@ -242,6 +254,7 @@ class AlbumViewerPage extends HookConsumerWidget {
selectionDisabled: disableSelection,
onAddPhotos: onAddPhotosPressed,
onAddUsers: onAddUsersPressed,
onActivities: onActivitiesPressed,
),
error: (error, stackTrace) => AppBar(title: const Text("Error")),
loading: () => AppBar(),
@ -266,6 +279,7 @@ class AlbumViewerPage extends HookConsumerWidget {
],
),
isOwner: userId == data.ownerId,
sharedAlbumId: data.remoteId,
),
),
),

View file

@ -1,6 +1,7 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
@ -16,6 +17,8 @@ class TopControlAppBar extends HookConsumerWidget {
required this.onFavorite,
required this.onUploadPressed,
required this.isOwner,
required this.shareAlbumId,
required this.onActivitiesPressed,
}) : super(key: key);
final Asset asset;
@ -24,14 +27,23 @@ class TopControlAppBar extends HookConsumerWidget {
final VoidCallback? onDownloadPressed;
final VoidCallback onToggleMotionVideo;
final VoidCallback onAddToAlbumPressed;
final VoidCallback onActivitiesPressed;
final Function(Asset) onFavorite;
final bool isPlayingMotionVideo;
final bool isOwner;
final String? shareAlbumId;
@override
Widget build(BuildContext context, WidgetRef ref) {
const double iconSize = 22.0;
final a = ref.watch(assetWatcher(asset)).value ?? asset;
final comments = shareAlbumId != null
? ref.watch(
activityStatisticsStateProvider(
(albumId: shareAlbumId!, assetId: asset.remoteId),
),
)
: 0;
Widget buildFavoriteButton(a) {
return IconButton(
@ -94,6 +106,34 @@ class TopControlAppBar extends HookConsumerWidget {
);
}
Widget buildActivitiesButton() {
return IconButton(
onPressed: () {
onActivitiesPressed();
},
icon: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Icons.mode_comment_outlined,
color: Colors.grey[200],
),
if (comments != 0)
Padding(
padding: const EdgeInsets.only(left: 5),
child: Text(
comments.toString(),
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.grey[200],
),
),
),
],
),
);
}
Widget buildUploadButton() {
return IconButton(
onPressed: onUploadPressed,
@ -130,6 +170,7 @@ class TopControlAppBar extends HookConsumerWidget {
if (asset.isLocal && !asset.isRemote) buildUploadButton(),
if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(),
if (asset.isRemote && isOwner) buildAddToAlbumButtom(),
if (shareAlbumId != null) buildActivitiesButton(),
buildMoreInfoButton(),
],
);

View file

@ -49,6 +49,7 @@ class GalleryViewerPage extends HookConsumerWidget {
final int heroOffset;
final bool showStack;
final bool isOwner;
final String? sharedAlbumId;
GalleryViewerPage({
super.key,
@ -58,6 +59,7 @@ class GalleryViewerPage extends HookConsumerWidget {
this.heroOffset = 0,
this.showStack = false,
this.isOwner = true,
this.sharedAlbumId,
}) : controller = PageController(initialPage: initialIndex);
final PageController controller;
@ -327,6 +329,19 @@ class GalleryViewerPage extends HookConsumerWidget {
);
}
handleActivities() {
if (sharedAlbumId != null) {
AutoRouter.of(context).push(
ActivitiesRoute(
albumId: sharedAlbumId!,
assetId: asset().remoteId,
withAssetThumbs: false,
isOwner: isOwner,
),
);
}
}
buildAppBar() {
return IgnorePointer(
ignoring: !ref.watch(showControlsProvider),
@ -355,6 +370,8 @@ class GalleryViewerPage extends HookConsumerWidget {
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
}),
onAddToAlbumPressed: () => addToAlbum(asset()),
shareAlbumId: sharedAlbumId,
onActivitiesPressed: handleActivities,
),
),
),

View file

@ -34,6 +34,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
final bool showDragScroll;
final bool showStack;
final bool isOwner;
final String? sharedAlbumId;
const ImmichAssetGrid({
super.key,
@ -55,6 +56,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
this.showDragScroll = true,
this.showStack = false,
this.isOwner = true,
this.sharedAlbumId,
});
@override
@ -120,6 +122,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
showDragScroll: showDragScroll,
showStack: showStack,
isOwner: isOwner,
sharedAlbumId: sharedAlbumId,
),
);
}

View file

@ -39,6 +39,7 @@ class ImmichAssetGridView extends StatefulWidget {
final bool showDragScroll;
final bool showStack;
final bool isOwner;
final String? sharedAlbumId;
const ImmichAssetGridView({
super.key,
@ -60,6 +61,7 @@ class ImmichAssetGridView extends StatefulWidget {
this.showDragScroll = true,
this.showStack = false,
this.isOwner = true,
this.sharedAlbumId,
});
@override
@ -141,6 +143,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
heroOffset: widget.heroOffset,
showStack: widget.showStack,
isOwner: widget.isOwner,
sharedAlbumId: widget.sharedAlbumId,
);
}

View file

@ -21,6 +21,7 @@ class ThumbnailImage extends StatelessWidget {
final Function? onSelect;
final Function? onDeselect;
final int heroOffset;
final String? sharedAlbumId;
const ThumbnailImage({
Key? key,
@ -31,6 +32,7 @@ class ThumbnailImage extends StatelessWidget {
this.showStorageIndicator = true,
this.showStack = false,
this.isOwner = true,
this.sharedAlbumId,
this.useGrayBoxPlaceholder = false,
this.isSelected = false,
this.multiselectEnabled = false,
@ -184,6 +186,7 @@ class ThumbnailImage extends StatelessWidget {
heroOffset: heroOffset,
showStack: showStack,
isOwner: isOwner,
sharedAlbumId: sharedAlbumId,
),
);
}

View file

@ -172,7 +172,7 @@ class SearchPage extends HookConsumerWidget {
),
ListTile(
leading: Icon(
Icons.star_outline,
Icons.favorite_border_rounded,
color: categoryIconColor,
),
title: