Skip to content

Commit ec01db5

Browse files
authored
refactor: bottom sheet action button (immich-app#20964)
* fix: incorrect archive action shown in asset viewer' * Refactor * use enums syntax and add tests
1 parent cd6d8fc commit ec01db5

6 files changed

Lines changed: 907 additions & 43 deletions

File tree

mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class LikeActivityActionButton extends ConsumerWidget {
4444
);
4545

4646
return BaseActionButton(
47+
maxWidth: 60,
4748
iconData: liked != null ? Icons.favorite : Icons.favorite_border,
4849
label: "like".t(context: context),
4950
onPressed: () => onTap(liked),

mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_
88
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
99
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart';
1010
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
11+
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
1112
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
1213
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
1314
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
@@ -31,6 +32,7 @@ class ViewerBottomBar extends ConsumerWidget {
3132
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
3233
final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
3334
final isInLockedView = ref.watch(inLockedViewProvider);
35+
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
3436

3537
if (!showControls) {
3638
opacity = 0;
@@ -40,10 +42,15 @@ class ViewerBottomBar extends ConsumerWidget {
4042
const ShareActionButton(source: ActionSource.viewer),
4143
if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
4244
if (asset.type == AssetType.image) const EditImageActionButton(),
43-
if (asset.hasRemote && isOwner) const ArchiveActionButton(source: ActionSource.viewer),
44-
asset.isLocalOnly
45-
? const DeleteLocalActionButton(source: ActionSource.viewer)
46-
: const DeleteActionButton(source: ActionSource.viewer, showConfirmation: true),
45+
if (isOwner) ...[
46+
if (asset.hasRemote && isOwner && isArchived)
47+
const UnArchiveActionButton(source: ActionSource.viewer)
48+
else
49+
const ArchiveActionButton(source: ActionSource.viewer),
50+
asset.isLocalOnly
51+
? const DeleteLocalActionButton(source: ActionSource.viewer)
52+
: const DeleteActionButton(source: ActionSource.viewer, showConfirmation: true),
53+
],
4754
];
4855

4956
return IgnorePointer(

mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart

Lines changed: 16 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,6 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
66
import 'package:immich_mobile/domain/models/exif.model.dart';
77
import 'package:immich_mobile/extensions/build_context_extensions.dart';
88
import 'package:immich_mobile/extensions/translate_extensions.dart';
9-
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
10-
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
11-
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
12-
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
13-
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
14-
import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_action_button.widget.dart';
15-
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
16-
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart';
17-
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
18-
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
19-
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
20-
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
219
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart';
2210
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_people_details.widget.dart';
2311
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
@@ -26,6 +14,8 @@ import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asse
2614
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
2715
import 'package:immich_mobile/providers/routes.provider.dart';
2816
import 'package:immich_mobile/providers/server_info.provider.dart';
17+
import 'package:immich_mobile/providers/user.provider.dart';
18+
import 'package:immich_mobile/utils/action_button.utils.dart';
2919
import 'package:immich_mobile/utils/bytes_units.dart';
3020
import 'package:immich_mobile/widgets/common/immich_toast.dart';
3121

@@ -45,34 +35,25 @@ class AssetDetailBottomSheet extends ConsumerWidget {
4535
}
4636

4737
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
38+
final isOwner = asset is RemoteAsset && asset.ownerId == ref.watch(currentUserProvider)?.id;
4839
final isInLockedView = ref.watch(inLockedViewProvider);
4940
final currentAlbum = ref.watch(currentRemoteAlbumProvider);
41+
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
42+
43+
final buttonContext = ActionButtonContext(
44+
asset: asset,
45+
isOwner: isOwner,
46+
isArchived: isArchived,
47+
isTrashEnabled: isTrashEnable,
48+
isInLockedView: isInLockedView,
49+
currentAlbum: currentAlbum,
50+
source: ActionSource.viewer,
51+
);
5052

51-
final actions = <Widget>[
52-
const ShareActionButton(source: ActionSource.viewer),
53-
if (currentAlbum != null && currentAlbum.isActivityEnabled && currentAlbum.isShared)
54-
const LikeActivityActionButton(),
55-
if (asset.hasRemote) ...[
56-
const ShareLinkActionButton(source: ActionSource.viewer),
57-
const ArchiveActionButton(source: ActionSource.viewer),
58-
if (!asset.hasLocal) const DownloadActionButton(source: ActionSource.viewer),
59-
isTrashEnable
60-
? const TrashActionButton(source: ActionSource.viewer)
61-
: const DeletePermanentActionButton(source: ActionSource.viewer),
62-
const DeleteActionButton(source: ActionSource.viewer),
63-
const MoveToLockFolderActionButton(source: ActionSource.viewer),
64-
],
65-
if (asset.storage == AssetState.local) ...[
66-
const DeleteLocalActionButton(source: ActionSource.viewer),
67-
const UploadActionButton(source: ActionSource.timeline),
68-
],
69-
if (currentAlbum != null) RemoveFromAlbumActionButton(albumId: currentAlbum.id, source: ActionSource.viewer),
70-
];
71-
72-
final lockedViewActions = <Widget>[];
53+
final actions = ActionButtonBuilder.build(buttonContext);
7354

7455
return BaseBottomSheet(
75-
actions: isInLockedView ? lockedViewActions : actions,
56+
actions: actions,
7657
slivers: const [_AssetDetailBottomSheet()],
7758
controller: controller,
7859
initialChildSize: initialChildSize,

mobile/lib/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,8 @@ class _BaseDraggableScrollableSheetState extends ConsumerState<BaseBottomSheet>
6969
shouldCloseOnMinExtent: widget.shouldCloseOnMinExtent,
7070
builder: (BuildContext context, ScrollController scrollController) {
7171
return Card(
72-
color: widget.backgroundColor ?? context.colorScheme.surface,
73-
borderOnForeground: false,
74-
clipBehavior: Clip.antiAlias,
75-
elevation: 6.0,
72+
color: widget.backgroundColor ?? context.colorScheme.surfaceContainer,
73+
elevation: 3.0,
7674
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(18))),
7775
margin: const EdgeInsets.symmetric(horizontal: 0),
7876
child: CustomScrollView(
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import 'package:flutter/widgets.dart';
2+
import 'package:immich_mobile/constants/enums.dart';
3+
import 'package:immich_mobile/domain/models/album/album.model.dart';
4+
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
5+
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
6+
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
7+
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
8+
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
9+
import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_action_button.widget.dart';
10+
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
11+
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart';
12+
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart';
13+
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
14+
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
15+
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
16+
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
17+
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
18+
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
19+
20+
class ActionButtonContext {
21+
final BaseAsset asset;
22+
final bool isOwner;
23+
final bool isArchived;
24+
final bool isTrashEnabled;
25+
final bool isInLockedView;
26+
final RemoteAlbum? currentAlbum;
27+
final ActionSource source;
28+
29+
const ActionButtonContext({
30+
required this.asset,
31+
required this.isOwner,
32+
required this.isArchived,
33+
required this.isTrashEnabled,
34+
required this.isInLockedView,
35+
required this.currentAlbum,
36+
required this.source,
37+
});
38+
}
39+
40+
enum ActionButtonType {
41+
share,
42+
shareLink,
43+
archive,
44+
unarchive,
45+
download,
46+
trash,
47+
deletePermanent,
48+
delete,
49+
moveToLockFolder,
50+
removeFromLockFolder,
51+
deleteLocal,
52+
upload,
53+
removeFromAlbum,
54+
likeActivity;
55+
56+
bool shouldShow(ActionButtonContext context) {
57+
return switch (this) {
58+
ActionButtonType.share => true,
59+
ActionButtonType.shareLink =>
60+
!context.isInLockedView && //
61+
context.asset.hasRemote,
62+
ActionButtonType.archive =>
63+
context.isOwner && //
64+
!context.isInLockedView && //
65+
context.asset.hasRemote && //
66+
!context.isArchived,
67+
ActionButtonType.unarchive =>
68+
context.isOwner && //
69+
!context.isInLockedView && //
70+
context.asset.hasRemote && //
71+
context.isArchived,
72+
ActionButtonType.download =>
73+
!context.isInLockedView && //
74+
context.asset.hasRemote && //
75+
!context.asset.hasLocal,
76+
ActionButtonType.trash =>
77+
context.isOwner && //
78+
!context.isInLockedView && //
79+
context.asset.hasRemote && //
80+
context.isTrashEnabled,
81+
ActionButtonType.deletePermanent =>
82+
context.isOwner && //
83+
context.asset.hasRemote && //
84+
!context.isTrashEnabled ||
85+
context.isInLockedView,
86+
ActionButtonType.delete =>
87+
context.isOwner && //
88+
!context.isInLockedView && //
89+
context.asset.hasRemote,
90+
ActionButtonType.moveToLockFolder =>
91+
context.isOwner && //
92+
!context.isInLockedView && //
93+
context.asset.hasRemote,
94+
ActionButtonType.removeFromLockFolder =>
95+
context.isOwner && //
96+
context.isInLockedView && //
97+
context.asset.hasRemote,
98+
ActionButtonType.deleteLocal =>
99+
!context.isInLockedView && //
100+
context.asset.storage == AssetState.local,
101+
ActionButtonType.upload =>
102+
!context.isInLockedView && //
103+
context.asset.storage == AssetState.local,
104+
ActionButtonType.removeFromAlbum =>
105+
context.isOwner && //
106+
!context.isInLockedView && //
107+
context.currentAlbum != null,
108+
ActionButtonType.likeActivity =>
109+
!context.isInLockedView &&
110+
context.currentAlbum != null &&
111+
context.currentAlbum!.isActivityEnabled &&
112+
context.currentAlbum!.isShared,
113+
};
114+
}
115+
116+
Widget buildButton(ActionButtonContext context) {
117+
return switch (this) {
118+
ActionButtonType.share => ShareActionButton(source: context.source),
119+
ActionButtonType.shareLink => ShareLinkActionButton(source: context.source),
120+
ActionButtonType.archive => ArchiveActionButton(source: context.source),
121+
ActionButtonType.unarchive => UnArchiveActionButton(source: context.source),
122+
ActionButtonType.download => DownloadActionButton(source: context.source),
123+
ActionButtonType.trash => TrashActionButton(source: context.source),
124+
ActionButtonType.deletePermanent => DeletePermanentActionButton(source: context.source),
125+
ActionButtonType.delete => DeleteActionButton(source: context.source),
126+
ActionButtonType.moveToLockFolder => MoveToLockFolderActionButton(source: context.source),
127+
ActionButtonType.removeFromLockFolder => RemoveFromLockFolderActionButton(source: context.source),
128+
ActionButtonType.deleteLocal => DeleteLocalActionButton(source: context.source),
129+
ActionButtonType.upload => UploadActionButton(source: context.source),
130+
ActionButtonType.removeFromAlbum => RemoveFromAlbumActionButton(
131+
albumId: context.currentAlbum!.id,
132+
source: context.source,
133+
),
134+
ActionButtonType.likeActivity => const LikeActivityActionButton(),
135+
};
136+
}
137+
}
138+
139+
class ActionButtonBuilder {
140+
static const List<ActionButtonType> _actionTypes = [
141+
ActionButtonType.share,
142+
ActionButtonType.shareLink,
143+
ActionButtonType.likeActivity,
144+
ActionButtonType.archive,
145+
ActionButtonType.unarchive,
146+
ActionButtonType.download,
147+
ActionButtonType.trash,
148+
ActionButtonType.deletePermanent,
149+
ActionButtonType.delete,
150+
ActionButtonType.moveToLockFolder,
151+
ActionButtonType.removeFromLockFolder,
152+
ActionButtonType.deleteLocal,
153+
ActionButtonType.upload,
154+
ActionButtonType.removeFromAlbum,
155+
];
156+
157+
static List<Widget> build(ActionButtonContext context) {
158+
return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList();
159+
}
160+
}

0 commit comments

Comments
 (0)