refactor: bottom sheet action button (#20964)

* fix: incorrect archive action shown in asset viewer'

* Refactor

* use enums syntax and add tests
This commit is contained in:
Alex 2025-08-18 10:20:08 -05:00 committed by GitHub
parent cd6d8fcdfe
commit ec01db5c8b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 906 additions and 42 deletions

View File

@ -44,6 +44,7 @@ class LikeActivityActionButton extends ConsumerWidget {
);
return BaseActionButton(
maxWidth: 60,
iconData: liked != null ? Icons.favorite : Icons.favorite_border,
label: "like".t(context: context),
onPressed: () => onTap(liked),

View File

@ -8,6 +8,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
@ -31,6 +32,7 @@ class ViewerBottomBar extends ConsumerWidget {
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
final isInLockedView = ref.watch(inLockedViewProvider);
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
if (!showControls) {
opacity = 0;
@ -40,10 +42,15 @@ class ViewerBottomBar extends ConsumerWidget {
const ShareActionButton(source: ActionSource.viewer),
if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
if (asset.type == AssetType.image) const EditImageActionButton(),
if (asset.hasRemote && isOwner) const ArchiveActionButton(source: ActionSource.viewer),
asset.isLocalOnly
? const DeleteLocalActionButton(source: ActionSource.viewer)
: const DeleteActionButton(source: ActionSource.viewer, showConfirmation: true),
if (isOwner) ...[
if (asset.hasRemote && isOwner && isArchived)
const UnArchiveActionButton(source: ActionSource.viewer)
else
const ArchiveActionButton(source: ActionSource.viewer),
asset.isLocalOnly
? const DeleteLocalActionButton(source: ActionSource.viewer)
: const DeleteActionButton(source: ActionSource.viewer, showConfirmation: true),
],
];
return IgnorePointer(

View File

@ -6,18 +6,6 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_people_details.widget.dart';
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
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/utils/action_button.utils.dart';
import 'package:immich_mobile/utils/bytes_units.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
@ -45,34 +35,25 @@ class AssetDetailBottomSheet extends ConsumerWidget {
}
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
final isOwner = asset is RemoteAsset && asset.ownerId == ref.watch(currentUserProvider)?.id;
final isInLockedView = ref.watch(inLockedViewProvider);
final currentAlbum = ref.watch(currentRemoteAlbumProvider);
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
final actions = <Widget>[
const ShareActionButton(source: ActionSource.viewer),
if (currentAlbum != null && currentAlbum.isActivityEnabled && currentAlbum.isShared)
const LikeActivityActionButton(),
if (asset.hasRemote) ...[
const ShareLinkActionButton(source: ActionSource.viewer),
const ArchiveActionButton(source: ActionSource.viewer),
if (!asset.hasLocal) const DownloadActionButton(source: ActionSource.viewer),
isTrashEnable
? const TrashActionButton(source: ActionSource.viewer)
: const DeletePermanentActionButton(source: ActionSource.viewer),
const DeleteActionButton(source: ActionSource.viewer),
const MoveToLockFolderActionButton(source: ActionSource.viewer),
],
if (asset.storage == AssetState.local) ...[
const DeleteLocalActionButton(source: ActionSource.viewer),
const UploadActionButton(source: ActionSource.timeline),
],
if (currentAlbum != null) RemoveFromAlbumActionButton(albumId: currentAlbum.id, source: ActionSource.viewer),
];
final buttonContext = ActionButtonContext(
asset: asset,
isOwner: isOwner,
isArchived: isArchived,
isTrashEnabled: isTrashEnable,
isInLockedView: isInLockedView,
currentAlbum: currentAlbum,
source: ActionSource.viewer,
);
final lockedViewActions = <Widget>[];
final actions = ActionButtonBuilder.build(buttonContext);
return BaseBottomSheet(
actions: isInLockedView ? lockedViewActions : actions,
actions: actions,
slivers: const [_AssetDetailBottomSheet()],
controller: controller,
initialChildSize: initialChildSize,

View File

@ -69,10 +69,8 @@ class _BaseDraggableScrollableSheetState extends ConsumerState<BaseBottomSheet>
shouldCloseOnMinExtent: widget.shouldCloseOnMinExtent,
builder: (BuildContext context, ScrollController scrollController) {
return Card(
color: widget.backgroundColor ?? context.colorScheme.surface,
borderOnForeground: false,
clipBehavior: Clip.antiAlias,
elevation: 6.0,
color: widget.backgroundColor ?? context.colorScheme.surfaceContainer,
elevation: 3.0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(18))),
margin: const EdgeInsets.symmetric(horizontal: 0),
child: CustomScrollView(

View File

@ -0,0 +1,160 @@
import 'package:flutter/widgets.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
class ActionButtonContext {
final BaseAsset asset;
final bool isOwner;
final bool isArchived;
final bool isTrashEnabled;
final bool isInLockedView;
final RemoteAlbum? currentAlbum;
final ActionSource source;
const ActionButtonContext({
required this.asset,
required this.isOwner,
required this.isArchived,
required this.isTrashEnabled,
required this.isInLockedView,
required this.currentAlbum,
required this.source,
});
}
enum ActionButtonType {
share,
shareLink,
archive,
unarchive,
download,
trash,
deletePermanent,
delete,
moveToLockFolder,
removeFromLockFolder,
deleteLocal,
upload,
removeFromAlbum,
likeActivity;
bool shouldShow(ActionButtonContext context) {
return switch (this) {
ActionButtonType.share => true,
ActionButtonType.shareLink =>
!context.isInLockedView && //
context.asset.hasRemote,
ActionButtonType.archive =>
context.isOwner && //
!context.isInLockedView && //
context.asset.hasRemote && //
!context.isArchived,
ActionButtonType.unarchive =>
context.isOwner && //
!context.isInLockedView && //
context.asset.hasRemote && //
context.isArchived,
ActionButtonType.download =>
!context.isInLockedView && //
context.asset.hasRemote && //
!context.asset.hasLocal,
ActionButtonType.trash =>
context.isOwner && //
!context.isInLockedView && //
context.asset.hasRemote && //
context.isTrashEnabled,
ActionButtonType.deletePermanent =>
context.isOwner && //
context.asset.hasRemote && //
!context.isTrashEnabled ||
context.isInLockedView,
ActionButtonType.delete =>
context.isOwner && //
!context.isInLockedView && //
context.asset.hasRemote,
ActionButtonType.moveToLockFolder =>
context.isOwner && //
!context.isInLockedView && //
context.asset.hasRemote,
ActionButtonType.removeFromLockFolder =>
context.isOwner && //
context.isInLockedView && //
context.asset.hasRemote,
ActionButtonType.deleteLocal =>
!context.isInLockedView && //
context.asset.storage == AssetState.local,
ActionButtonType.upload =>
!context.isInLockedView && //
context.asset.storage == AssetState.local,
ActionButtonType.removeFromAlbum =>
context.isOwner && //
!context.isInLockedView && //
context.currentAlbum != null,
ActionButtonType.likeActivity =>
!context.isInLockedView &&
context.currentAlbum != null &&
context.currentAlbum!.isActivityEnabled &&
context.currentAlbum!.isShared,
};
}
Widget buildButton(ActionButtonContext context) {
return switch (this) {
ActionButtonType.share => ShareActionButton(source: context.source),
ActionButtonType.shareLink => ShareLinkActionButton(source: context.source),
ActionButtonType.archive => ArchiveActionButton(source: context.source),
ActionButtonType.unarchive => UnArchiveActionButton(source: context.source),
ActionButtonType.download => DownloadActionButton(source: context.source),
ActionButtonType.trash => TrashActionButton(source: context.source),
ActionButtonType.deletePermanent => DeletePermanentActionButton(source: context.source),
ActionButtonType.delete => DeleteActionButton(source: context.source),
ActionButtonType.moveToLockFolder => MoveToLockFolderActionButton(source: context.source),
ActionButtonType.removeFromLockFolder => RemoveFromLockFolderActionButton(source: context.source),
ActionButtonType.deleteLocal => DeleteLocalActionButton(source: context.source),
ActionButtonType.upload => UploadActionButton(source: context.source),
ActionButtonType.removeFromAlbum => RemoveFromAlbumActionButton(
albumId: context.currentAlbum!.id,
source: context.source,
),
ActionButtonType.likeActivity => const LikeActivityActionButton(),
};
}
}
class ActionButtonBuilder {
static const List<ActionButtonType> _actionTypes = [
ActionButtonType.share,
ActionButtonType.shareLink,
ActionButtonType.likeActivity,
ActionButtonType.archive,
ActionButtonType.unarchive,
ActionButtonType.download,
ActionButtonType.trash,
ActionButtonType.deletePermanent,
ActionButtonType.delete,
ActionButtonType.moveToLockFolder,
ActionButtonType.removeFromLockFolder,
ActionButtonType.deleteLocal,
ActionButtonType.upload,
ActionButtonType.removeFromAlbum,
];
static List<Widget> build(ActionButtonContext context) {
return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList();
}
}

View File

@ -0,0 +1,717 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/utils/action_button.utils.dart';
LocalAsset createLocalAsset({
String? remoteId,
String name = 'test.jpg',
String? checksum = 'test-checksum',
AssetType type = AssetType.image,
DateTime? createdAt,
DateTime? updatedAt,
bool isFavorite = false,
}) {
return LocalAsset(
id: 'local-id',
remoteId: remoteId,
name: name,
checksum: checksum,
type: type,
createdAt: createdAt ?? DateTime.now(),
updatedAt: updatedAt ?? DateTime.now(),
isFavorite: isFavorite,
);
}
RemoteAsset createRemoteAsset({
String? localId,
String name = 'test.jpg',
String checksum = 'test-checksum',
AssetType type = AssetType.image,
DateTime? createdAt,
DateTime? updatedAt,
bool isFavorite = false,
}) {
return RemoteAsset(
id: 'remote-id',
localId: localId,
name: name,
checksum: checksum,
type: type,
ownerId: 'owner-id',
createdAt: createdAt ?? DateTime.now(),
updatedAt: updatedAt ?? DateTime.now(),
isFavorite: isFavorite,
);
}
RemoteAlbum createRemoteAlbum({
String id = 'test-album-id',
String name = 'Test Album',
bool isActivityEnabled = false,
bool isShared = false,
}) {
return RemoteAlbum(
id: id,
name: name,
ownerId: 'owner-id',
description: 'Test Description',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
isActivityEnabled: isActivityEnabled,
isShared: isShared,
order: AlbumAssetOrder.asc,
assetCount: 0,
ownerName: 'Test Owner',
);
}
void main() {
group('ActionButtonContext', () {
test('should create context with all required parameters', () {
final asset = createLocalAsset();
final context = ActionButtonContext(
asset: asset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(context.asset, isA<BaseAsset>());
expect(context.isOwner, isTrue);
expect(context.isArchived, isFalse);
expect(context.isTrashEnabled, isTrue);
expect(context.isInLockedView, isFalse);
expect(context.currentAlbum, isNull);
expect(context.source, ActionSource.timeline);
});
});
group('ActionButtonType.shouldShow', () {
late BaseAsset mergedAsset;
setUp(() {
mergedAsset = createLocalAsset(remoteId: 'remote-id');
});
group('share button', () {
test('should show when not in locked view', () {
final context = ActionButtonContext(
asset: mergedAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.share.shouldShow(context), isTrue);
});
test('should show when in locked view', () {
final context = ActionButtonContext(
asset: mergedAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: true,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.share.shouldShow(context), isTrue);
});
});
group('shareLink button', () {
test('should show when not in locked view and asset has remote', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.shareLink.shouldShow(context), isTrue);
});
test('should not show when in locked view', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: true,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.shareLink.shouldShow(context), isFalse);
});
test('should not show when asset has no remote', () {
final localAsset = createLocalAsset();
final context = ActionButtonContext(
asset: localAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.shareLink.shouldShow(context), isFalse);
});
});
group('archive button', () {
test('should show when owner, not locked, has remote, and not archived', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.archive.shouldShow(context), isTrue);
});
test('should not show when not owner', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: false,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.archive.shouldShow(context), isFalse);
});
test('should not show when in locked view', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: true,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.archive.shouldShow(context), isFalse);
});
test('should not show when asset has no remote', () {
final localAsset = createLocalAsset();
final context = ActionButtonContext(
asset: localAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.archive.shouldShow(context), isFalse);
});
test('should not show when already archived', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: true,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.archive.shouldShow(context), isFalse);
});
});
group('unarchive button', () {
test('should show when owner, not locked, has remote, and is archived', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: true,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.unarchive.shouldShow(context), isTrue);
});
test('should not show when not archived', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.unarchive.shouldShow(context), isFalse);
});
test('should not show when not owner', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: false,
isArchived: true,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.unarchive.shouldShow(context), isFalse);
});
});
group('download button', () {
test('should show when not locked, has remote, and no local copy', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.download.shouldShow(context), isTrue);
});
test('should not show when has local copy', () {
final mergedAsset = createLocalAsset(remoteId: 'remote-id');
final context = ActionButtonContext(
asset: mergedAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.download.shouldShow(context), isFalse);
});
test('should not show when in locked view', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: true,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.download.shouldShow(context), isFalse);
});
});
group('trash button', () {
test('should show when owner, not locked, has remote, and trash enabled', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.trash.shouldShow(context), isTrue);
});
test('should not show when trash disabled', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: false,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.trash.shouldShow(context), isFalse);
});
});
group('deletePermanent button', () {
test('should show when owner, not locked, has remote, and trash disabled', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: false,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.deletePermanent.shouldShow(context), isTrue);
});
test('should not show when trash enabled', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.deletePermanent.shouldShow(context), isFalse);
});
});
group('delete button', () {
test('should show when owner, not locked, and has remote', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.delete.shouldShow(context), isTrue);
});
});
group('moveToLockFolder button', () {
test('should show when owner, not locked, and has remote', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.moveToLockFolder.shouldShow(context), isTrue);
});
});
group('deleteLocal button', () {
test('should show when not locked and asset is local only', () {
final localAsset = createLocalAsset();
final context = ActionButtonContext(
asset: localAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.deleteLocal.shouldShow(context), isTrue);
});
test('should not show when asset is not local only', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.deleteLocal.shouldShow(context), isFalse);
});
});
group('upload button', () {
test('should show when not locked and asset is local only', () {
final localAsset = createLocalAsset();
final context = ActionButtonContext(
asset: localAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.upload.shouldShow(context), isTrue);
});
});
group('removeFromAlbum button', () {
test('should show when owner, not locked, and has current album', () {
final album = createRemoteAlbum();
final context = ActionButtonContext(
asset: mergedAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: album,
source: ActionSource.timeline,
);
expect(ActionButtonType.removeFromAlbum.shouldShow(context), isTrue);
});
test('should not show when no current album', () {
final context = ActionButtonContext(
asset: mergedAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.removeFromAlbum.shouldShow(context), isFalse);
});
});
group('likeActivity button', () {
test('should show when not locked, has album, activity enabled, and shared', () {
final album = createRemoteAlbum(isActivityEnabled: true, isShared: true);
final context = ActionButtonContext(
asset: mergedAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: album,
source: ActionSource.timeline,
);
expect(ActionButtonType.likeActivity.shouldShow(context), isTrue);
});
test('should not show when activity not enabled', () {
final album = createRemoteAlbum(isActivityEnabled: false, isShared: true);
final context = ActionButtonContext(
asset: mergedAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: album,
source: ActionSource.timeline,
);
expect(ActionButtonType.likeActivity.shouldShow(context), isFalse);
});
test('should not show when album not shared', () {
final album = createRemoteAlbum(isActivityEnabled: true, isShared: false);
final context = ActionButtonContext(
asset: mergedAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: album,
source: ActionSource.timeline,
);
expect(ActionButtonType.likeActivity.shouldShow(context), isFalse);
});
test('should not show when no album', () {
final context = ActionButtonContext(
asset: mergedAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
expect(ActionButtonType.likeActivity.shouldShow(context), isFalse);
});
});
});
group('ActionButtonType.buildButton', () {
late BaseAsset asset;
late ActionButtonContext context;
setUp(() {
asset = createLocalAsset(remoteId: 'remote-id');
context = ActionButtonContext(
asset: asset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
});
test('should build correct widget for each button type', () {
for (final buttonType in ActionButtonType.values) {
if (buttonType == ActionButtonType.removeFromAlbum) {
final album = createRemoteAlbum();
final contextWithAlbum = ActionButtonContext(
asset: asset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: album,
source: ActionSource.timeline,
);
final widget = buttonType.buildButton(contextWithAlbum);
expect(widget, isA<Widget>());
} else {
final widget = buttonType.buildButton(context);
expect(widget, isA<Widget>());
}
}
});
});
group('ActionButtonBuilder', () {
test('should return buttons that should show', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
final widgets = ActionButtonBuilder.build(context);
expect(widgets, isNotEmpty);
expect(widgets.length, greaterThan(0));
});
test('should include album-specific buttons when album is present', () {
final remoteAsset = createRemoteAsset();
final album = createRemoteAlbum(isActivityEnabled: true, isShared: true);
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: album,
source: ActionSource.timeline,
);
final widgets = ActionButtonBuilder.build(context);
expect(widgets, isNotEmpty);
});
test('should only include local buttons for local assets', () {
final localAsset = createLocalAsset();
final context = ActionButtonContext(
asset: localAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
final widgets = ActionButtonBuilder.build(context);
expect(widgets, isNotEmpty);
});
test('should respect archived state', () {
final remoteAsset = createRemoteAsset();
final archivedContext = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: true,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
final archivedWidgets = ActionButtonBuilder.build(archivedContext);
final nonArchivedContext = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
source: ActionSource.timeline,
);
final nonArchivedWidgets = ActionButtonBuilder.build(nonArchivedContext);
expect(archivedWidgets, isNotEmpty);
expect(nonArchivedWidgets, isNotEmpty);
});
});
}