diff --git a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart index 9f41a0c681..01daf7d2a2 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/translate_extensions.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/presentation/widgets/action_buttons/archive_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/delete_local_action_button.widget.dart'; @@ -16,22 +18,74 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_act import 'package:immich_mobile/presentation/widgets/action_buttons/stack_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/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; -class RemoteAlbumBottomSheet extends ConsumerWidget { +class RemoteAlbumBottomSheet extends ConsumerStatefulWidget { final RemoteAlbum album; const RemoteAlbumBottomSheet({super.key, required this.album}); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _RemoteAlbumBottomSheetState(); +} + +class _RemoteAlbumBottomSheetState extends ConsumerState { + late DraggableScrollableController sheetController; + + @override + void initState() { + super.initState(); + sheetController = DraggableScrollableController(); + } + + @override + void dispose() { + sheetController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { final multiselect = ref.watch(multiSelectProvider); final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash)); + Future addAssetsToAlbum(RemoteAlbum album) async { + final selectedAssets = multiselect.selectedAssets; + if (selectedAssets.isEmpty) { + return; + } + + final addedCount = await ref + .read(remoteAlbumProvider.notifier) + .addAssets(album.id, selectedAssets.map((e) => (e as RemoteAsset).id).toList()); + + if (addedCount != selectedAssets.length) { + ImmichToast.show( + context: context, + msg: 'add_to_album_bottom_sheet_already_exists'.t(context: context, args: {"album": album.name}), + ); + } else { + ImmichToast.show( + context: context, + msg: 'add_to_album_bottom_sheet_added'.t(context: context, args: {"album": album.name}), + ); + } + + ref.read(multiSelectProvider.notifier).reset(); + } + + Future onKeyboardExpand() { + return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut); + } + return BaseBottomSheet( - initialChildSize: 0.25, - maxChildSize: 0.4, + controller: sheetController, + initialChildSize: 0.45, + maxChildSize: 0.85, shouldCloseOnMinExtent: false, actions: [ const ShareActionButton(source: ActionSource.timeline), @@ -52,7 +106,11 @@ class RemoteAlbumBottomSheet extends ConsumerWidget { const DeleteLocalActionButton(source: ActionSource.timeline), const UploadActionButton(source: ActionSource.timeline), ], - RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: album.id), + RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id), + ], + slivers: [ + const AddToAlbumHeader(), + AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand), ], ); } diff --git a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart b/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart index b1e54af62a..cd2dc70dae 100644 --- a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart +++ b/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart @@ -8,12 +8,14 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart'; +import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.dart'; import 'package:immich_mobile/models/asset_selection_state.dart'; import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; import 'package:immich_mobile/widgets/asset_grid/upload_dialog.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/widgets/common/drag_sheet.dart'; import 'package:immich_mobile/entities/album.entity.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/utils/draggable_scroll_controller.dart'; final controlBottomAppBarNotifier = ControlBottomAppBarNotifier(); @@ -45,6 +47,7 @@ class ControlBottomAppBar extends HookConsumerWidget { final bool unfavorite; final bool unarchive; final AssetSelectionState selectionAssetState; + final List selectedAssets; const ControlBottomAppBar({ super.key, @@ -64,6 +67,7 @@ class ControlBottomAppBar extends HookConsumerWidget { this.onRemoveFromAlbum, this.onToggleLocked, this.selectionAssetState = const AssetSelectionState(), + this.selectedAssets = const [], this.enabled = true, this.unarchive = false, this.unfavorite = false, @@ -100,6 +104,18 @@ class ControlBottomAppBar extends HookConsumerWidget { ); } + /// Show existing AddToAlbumBottomSheet + void showAddToAlbumBottomSheet() { + showModalBottomSheet( + elevation: 0, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15.0))), + context: context, + builder: (BuildContext _) { + return AddToAlbumBottomSheet(assets: selectedAssets); + }, + ); + } + void handleRemoteDelete(bool force, Function(bool) deleteCb, {String? alertMsg}) { if (!force) { deleteCb(force); @@ -121,6 +137,15 @@ class ControlBottomAppBar extends HookConsumerWidget { label: "share_link".tr(), onPressed: enabled ? () => onShare(false) : null, ), + if (!isInLockedView && hasRemote && albums.isNotEmpty) + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 100), + child: ControlBoxButton( + iconData: Icons.photo_album, + label: "add_to_album".tr(), + onPressed: enabled ? showAddToAlbumBottomSheet : null, + ), + ), if (hasRemote && onArchive != null) ControlBoxButton( iconData: unarchive ? Icons.unarchive_outlined : Icons.archive_outlined, diff --git a/mobile/lib/widgets/asset_grid/multiselect_grid.dart b/mobile/lib/widgets/asset_grid/multiselect_grid.dart index c28f407e1e..da2957c027 100644 --- a/mobile/lib/widgets/asset_grid/multiselect_grid.dart +++ b/mobile/lib/widgets/asset_grid/multiselect_grid.dart @@ -440,6 +440,7 @@ class MultiselectGrid extends HookConsumerWidget { onUpload: onUpload, enabled: !processing.value, selectionAssetState: selectionAssetState.value, + selectedAssets: selection.value.toList(), onStack: stackEnabled ? onStack : null, onEditTime: editEnabled ? onEditTime : null, onEditLocation: editEnabled ? onEditLocation : null,