mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(mobile): Archive feature on mobile (#2258)
* update asset to include isArchive property * Not display archived assets on timeline * replace share button to archive button * Added archive page * Add bottom nav bar * clean up homepage * remove deadcode * improve on sync is archive * show archive asset correctly * better merge condition * Added back renderList to re-rendering don't jump around * Better way to handle showing archive assets * complete ArchiveSelectionNotifier * toggle archive * remove deadcode * fix unit tests * update assets in DB when changing assets * update asset state to reflect archived status * allow to archive assets via multi-select from timeline * fixed logic * Add options to bulk unarchive * regenerate api * Change position of toast message --------- Co-authored-by: Fynn Petersen-Frey <zoodyy@users.noreply.github.com>
This commit is contained in:
		@@ -51,14 +51,14 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
 | 
			
		||||
          ImmichToast.show(
 | 
			
		||||
            context: context,
 | 
			
		||||
            msg: 'add_to_album_bottom_sheet_already_exists'.tr(
 | 
			
		||||
              namedArgs: { "album": album.name },
 | 
			
		||||
              namedArgs: {"album": album.name},
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
        } else {
 | 
			
		||||
          ImmichToast.show(
 | 
			
		||||
            context: context,
 | 
			
		||||
            msg: 'add_to_album_bottom_sheet_added'.tr(
 | 
			
		||||
              namedArgs: { "album": album.name },
 | 
			
		||||
              namedArgs: {"album": album.name},
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
@@ -71,6 +71,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Card(
 | 
			
		||||
      elevation: 0,
 | 
			
		||||
      shape: const RoundedRectangleBorder(
 | 
			
		||||
        borderRadius: BorderRadius.only(
 | 
			
		||||
          topLeft: Radius.circular(15),
 | 
			
		||||
@@ -99,8 +100,15 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
 | 
			
		||||
                        style: Theme.of(context).textTheme.displayMedium,
 | 
			
		||||
                      ),
 | 
			
		||||
                      TextButton.icon(
 | 
			
		||||
                        icon: const Icon(Icons.add),
 | 
			
		||||
                        label: Text('common_create_new_album'.tr()),
 | 
			
		||||
                        icon: Icon(
 | 
			
		||||
                          Icons.add,
 | 
			
		||||
                          color: Theme.of(context).primaryColor,
 | 
			
		||||
                        ),
 | 
			
		||||
                        label: Text(
 | 
			
		||||
                          'common_create_new_album'.tr(),
 | 
			
		||||
                          style:
 | 
			
		||||
                              TextStyle(color: Theme.of(context).primaryColor),
 | 
			
		||||
                        ),
 | 
			
		||||
                        onPressed: () {
 | 
			
		||||
                          ref
 | 
			
		||||
                              .watch(assetSelectionProvider.notifier)
 | 
			
		||||
 
 | 
			
		||||
@@ -43,7 +43,8 @@ class LibraryPage extends HookConsumerWidget {
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    final selectedAlbumSortOrder = useState(settings.getSetting(AppSettingsEnum.selectedAlbumSortOrder));
 | 
			
		||||
    final selectedAlbumSortOrder =
 | 
			
		||||
        useState(settings.getSetting(AppSettingsEnum.selectedAlbumSortOrder));
 | 
			
		||||
 | 
			
		||||
    List<Album> sortedAlbums() {
 | 
			
		||||
      if (selectedAlbumSortOrder.value == 0) {
 | 
			
		||||
@@ -179,13 +180,13 @@ class LibraryPage extends HookConsumerWidget {
 | 
			
		||||
              label,
 | 
			
		||||
              style: TextStyle(
 | 
			
		||||
                fontWeight: FontWeight.bold,
 | 
			
		||||
                fontSize: 12.0,
 | 
			
		||||
                color: isDarkMode ? Colors.white : Colors.black,
 | 
			
		||||
                fontSize: 13.0,
 | 
			
		||||
                color: isDarkMode ? Colors.white : Colors.grey[800],
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          style: OutlinedButton.styleFrom(
 | 
			
		||||
            padding: const EdgeInsets.all(12),
 | 
			
		||||
            padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
 | 
			
		||||
            backgroundColor: isDarkMode ? Colors.grey[900] : Colors.grey[50],
 | 
			
		||||
            side: BorderSide(
 | 
			
		||||
              color: isDarkMode ? Colors.grey[800]! : Colors.grey[300]!,
 | 
			
		||||
@@ -225,8 +226,8 @@ class LibraryPage extends HookConsumerWidget {
 | 
			
		||||
                  }),
 | 
			
		||||
                  const SizedBox(width: 12.0),
 | 
			
		||||
                  buildLibraryNavButton(
 | 
			
		||||
                      "library_page_sharing".tr(), Icons.group_outlined, () {
 | 
			
		||||
                    AutoRouter.of(context).navigate(const SharingRoute());
 | 
			
		||||
                      "library_page_archive".tr(), Icons.archive_outlined, () {
 | 
			
		||||
                    AutoRouter.of(context).navigate(const ArchiveRoute());
 | 
			
		||||
                  }),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,55 @@
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/asset.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
 | 
			
		||||
import 'package:isar/isar.dart';
 | 
			
		||||
 | 
			
		||||
class ArchiveSelectionNotifier extends StateNotifier<Set<int>> {
 | 
			
		||||
  ArchiveSelectionNotifier(this.db, this.assetNotifier) : super({}) {
 | 
			
		||||
    state = db.assets
 | 
			
		||||
        .filter()
 | 
			
		||||
        .isArchivedEqualTo(true)
 | 
			
		||||
        .findAllSync()
 | 
			
		||||
        .map((e) => e.id)
 | 
			
		||||
        .toSet();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  final Isar db;
 | 
			
		||||
  final AssetNotifier assetNotifier;
 | 
			
		||||
 | 
			
		||||
  void _setArchiveForAssetId(int id, bool archive) {
 | 
			
		||||
    if (!archive) {
 | 
			
		||||
      state = state.difference({id});
 | 
			
		||||
    } else {
 | 
			
		||||
      state = state.union({id});
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool _isArchive(int id) {
 | 
			
		||||
    return state.contains(id);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> toggleArchive(Asset asset) async {
 | 
			
		||||
    if (!asset.isRemote) return;
 | 
			
		||||
 | 
			
		||||
    _setArchiveForAssetId(asset.id, !_isArchive(asset.id));
 | 
			
		||||
 | 
			
		||||
    await assetNotifier.toggleArchive(
 | 
			
		||||
      [asset],
 | 
			
		||||
      state.contains(asset.id),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> addToArchives(Iterable<Asset> assets) {
 | 
			
		||||
    state = state.union(assets.map((a) => a.id).toSet());
 | 
			
		||||
    return assetNotifier.toggleArchive(assets, true);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
final archiveProvider =
 | 
			
		||||
    StateNotifierProvider<ArchiveSelectionNotifier, Set<int>>((ref) {
 | 
			
		||||
  return ArchiveSelectionNotifier(
 | 
			
		||||
    ref.watch(dbProvider),
 | 
			
		||||
    ref.watch(assetProvider.notifier),
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										0
									
								
								mobile/lib/modules/archive/ui/store_ui_here.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								mobile/lib/modules/archive/ui/store_ui_here.txt
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										124
									
								
								mobile/lib/modules/archive/views/archive_page.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								mobile/lib/modules/archive/views/archive_page.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,124 @@
 | 
			
		||||
import 'package:auto_route/auto_route.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
 | 
			
		||||
import 'package:fluttertoast/fluttertoast.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/asset.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/store.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/user.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
 | 
			
		||||
import 'package:isar/isar.dart';
 | 
			
		||||
 | 
			
		||||
class ArchivePage extends HookConsumerWidget {
 | 
			
		||||
  const ArchivePage({super.key});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    final User me = Store.get(StoreKey.currentUser);
 | 
			
		||||
    final query = ref
 | 
			
		||||
        .watch(dbProvider)
 | 
			
		||||
        .assets
 | 
			
		||||
        .filter()
 | 
			
		||||
        .ownerIdEqualTo(me.isarId)
 | 
			
		||||
        .isArchivedEqualTo(true);
 | 
			
		||||
    final stream = query.watch();
 | 
			
		||||
    final archivedAssets = useState<List<Asset>>([]);
 | 
			
		||||
    final selectionEnabledHook = useState(false);
 | 
			
		||||
    final selection = useState(<Asset>{});
 | 
			
		||||
 | 
			
		||||
    useEffect(
 | 
			
		||||
      () {
 | 
			
		||||
        query.findAll().then((value) => archivedAssets.value = value);
 | 
			
		||||
        final subscription = stream.listen((e) {
 | 
			
		||||
          archivedAssets.value = e;
 | 
			
		||||
        });
 | 
			
		||||
        // Cancel the subscription when the widget is disposed
 | 
			
		||||
        return subscription.cancel;
 | 
			
		||||
      },
 | 
			
		||||
      [],
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    void selectionListener(
 | 
			
		||||
      bool multiselect,
 | 
			
		||||
      Set<Asset> selectedAssets,
 | 
			
		||||
    ) {
 | 
			
		||||
      selectionEnabledHook.value = multiselect;
 | 
			
		||||
      selection.value = selectedAssets;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    AppBar buildAppBar() {
 | 
			
		||||
      return AppBar(
 | 
			
		||||
        leading: IconButton(
 | 
			
		||||
          onPressed: () => AutoRouter.of(context).pop(),
 | 
			
		||||
          icon: const Icon(Icons.arrow_back_ios_rounded),
 | 
			
		||||
        ),
 | 
			
		||||
        centerTitle: true,
 | 
			
		||||
        automaticallyImplyLeading: false,
 | 
			
		||||
        title: const Text(
 | 
			
		||||
          'archive_page_title',
 | 
			
		||||
        ).tr(args: [archivedAssets.value.length.toString()]),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Widget buildBottomBar() {
 | 
			
		||||
      return Align(
 | 
			
		||||
        alignment: Alignment.bottomCenter,
 | 
			
		||||
        child: SizedBox(
 | 
			
		||||
          height: 64,
 | 
			
		||||
          child: Card(
 | 
			
		||||
            child: Column(
 | 
			
		||||
              children: [
 | 
			
		||||
                ListTile(
 | 
			
		||||
                  shape: RoundedRectangleBorder(
 | 
			
		||||
                    borderRadius: BorderRadius.circular(10),
 | 
			
		||||
                  ),
 | 
			
		||||
                  leading: const Icon(
 | 
			
		||||
                    Icons.unarchive_rounded,
 | 
			
		||||
                  ),
 | 
			
		||||
                  title:
 | 
			
		||||
                      const Text("Unarchive", style: TextStyle(fontSize: 14)),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    if (selection.value.isNotEmpty) {
 | 
			
		||||
                      ref
 | 
			
		||||
                          .watch(assetProvider.notifier)
 | 
			
		||||
                          .toggleArchive(selection.value, false);
 | 
			
		||||
 | 
			
		||||
                      final assetOrAssets =
 | 
			
		||||
                          selection.value.length > 1 ? 'assets' : 'asset';
 | 
			
		||||
                      ImmichToast.show(
 | 
			
		||||
                        context: context,
 | 
			
		||||
                        msg:
 | 
			
		||||
                            'Moved ${selection.value.length} $assetOrAssets to library',
 | 
			
		||||
                        gravity: ToastGravity.CENTER,
 | 
			
		||||
                      );
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    selectionEnabledHook.value = false;
 | 
			
		||||
                  },
 | 
			
		||||
                )
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: buildAppBar(),
 | 
			
		||||
      body: Stack(
 | 
			
		||||
        children: [
 | 
			
		||||
          ImmichAssetGrid(
 | 
			
		||||
            assets: archivedAssets.value,
 | 
			
		||||
            listener: selectionListener,
 | 
			
		||||
            selectionActive: selectionEnabledHook.value,
 | 
			
		||||
          ),
 | 
			
		||||
          if (selectionEnabledHook.value) buildBottomBar()
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -9,8 +9,6 @@ class TopControlAppBar extends HookConsumerWidget {
 | 
			
		||||
    required this.asset,
 | 
			
		||||
    required this.onMoreInfoPressed,
 | 
			
		||||
    required this.onDownloadPressed,
 | 
			
		||||
    required this.onSharePressed,
 | 
			
		||||
    required this.onDeletePressed,
 | 
			
		||||
    required this.onAddToAlbumPressed,
 | 
			
		||||
    required this.onToggleMotionVideo,
 | 
			
		||||
    required this.isPlayingMotionVideo,
 | 
			
		||||
@@ -22,10 +20,8 @@ class TopControlAppBar extends HookConsumerWidget {
 | 
			
		||||
  final Function onMoreInfoPressed;
 | 
			
		||||
  final VoidCallback? onDownloadPressed;
 | 
			
		||||
  final VoidCallback onToggleMotionVideo;
 | 
			
		||||
  final VoidCallback onDeletePressed;
 | 
			
		||||
  final VoidCallback onAddToAlbumPressed;
 | 
			
		||||
  final VoidCallback onFavorite;
 | 
			
		||||
  final Function onSharePressed;
 | 
			
		||||
  final bool isPlayingMotionVideo;
 | 
			
		||||
  final bool isFavorite;
 | 
			
		||||
 | 
			
		||||
@@ -34,15 +30,15 @@ class TopControlAppBar extends HookConsumerWidget {
 | 
			
		||||
    const double iconSize = 18.0;
 | 
			
		||||
 | 
			
		||||
    Widget buildFavoriteButton() {
 | 
			
		||||
        return IconButton(
 | 
			
		||||
          onPressed: () {
 | 
			
		||||
            onFavorite();
 | 
			
		||||
          },
 | 
			
		||||
          icon: Icon(
 | 
			
		||||
            isFavorite ? Icons.star : Icons.star_border,
 | 
			
		||||
            color: Colors.grey[200],
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      return IconButton(
 | 
			
		||||
        onPressed: () {
 | 
			
		||||
          onFavorite();
 | 
			
		||||
        },
 | 
			
		||||
        icon: Icon(
 | 
			
		||||
          isFavorite ? Icons.star : Icons.star_border,
 | 
			
		||||
          color: Colors.grey[200],
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return AppBar(
 | 
			
		||||
@@ -86,15 +82,6 @@ class TopControlAppBar extends HookConsumerWidget {
 | 
			
		||||
              color: Colors.grey[200],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        IconButton(
 | 
			
		||||
          onPressed: () {
 | 
			
		||||
            onSharePressed();
 | 
			
		||||
          },
 | 
			
		||||
          icon: Icon(
 | 
			
		||||
            Icons.ios_share_rounded,
 | 
			
		||||
            color: Colors.grey[200],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        if (asset.isRemote)
 | 
			
		||||
          IconButton(
 | 
			
		||||
            onPressed: () {
 | 
			
		||||
@@ -105,15 +92,6 @@ class TopControlAppBar extends HookConsumerWidget {
 | 
			
		||||
              color: Colors.grey[200],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        IconButton(
 | 
			
		||||
          onPressed: () {
 | 
			
		||||
            onDeletePressed();
 | 
			
		||||
          },
 | 
			
		||||
          icon: Icon(
 | 
			
		||||
            Icons.delete_outline_rounded,
 | 
			
		||||
            color: Colors.grey[200],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        IconButton(
 | 
			
		||||
          onPressed: () {
 | 
			
		||||
            onMoreInfoPressed();
 | 
			
		||||
 
 | 
			
		||||
@@ -231,11 +231,10 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
    void addToAlbum(Asset addToAlbumAsset) {
 | 
			
		||||
      showModalBottomSheet(
 | 
			
		||||
        elevation: 0,
 | 
			
		||||
        shape: RoundedRectangleBorder(
 | 
			
		||||
          borderRadius: BorderRadius.circular(15.0),
 | 
			
		||||
        ),
 | 
			
		||||
        barrierColor: Colors.transparent,
 | 
			
		||||
        backgroundColor: Colors.transparent,
 | 
			
		||||
        context: context,
 | 
			
		||||
        builder: (BuildContext _) {
 | 
			
		||||
          return AddToAlbumBottomSheet(
 | 
			
		||||
@@ -267,6 +266,19 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    shareAsset() {
 | 
			
		||||
      ref
 | 
			
		||||
          .watch(imageViewerStateProvider.notifier)
 | 
			
		||||
          .shareAsset(assetList[indexOfAsset.value], context);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleArchive(Asset asset) {
 | 
			
		||||
      ref
 | 
			
		||||
          .watch(assetProvider.notifier)
 | 
			
		||||
          .toggleArchive([asset], !asset.isArchived);
 | 
			
		||||
      AutoRouter.of(context).pop();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    buildAppBar() {
 | 
			
		||||
      final show = (showAppBar.value || // onTap has the final say
 | 
			
		||||
              (showAppBar.value && !isZoomed.value)) &&
 | 
			
		||||
@@ -297,16 +309,9 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
			
		||||
                          context,
 | 
			
		||||
                        );
 | 
			
		||||
                  },
 | 
			
		||||
            onSharePressed: () {
 | 
			
		||||
              ref
 | 
			
		||||
                  .watch(imageViewerStateProvider.notifier)
 | 
			
		||||
                  .shareAsset(assetList[indexOfAsset.value], context);
 | 
			
		||||
            },
 | 
			
		||||
            onToggleMotionVideo: (() {
 | 
			
		||||
              isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
 | 
			
		||||
            }),
 | 
			
		||||
            onDeletePressed: () =>
 | 
			
		||||
                handleDelete((assetList[indexOfAsset.value])),
 | 
			
		||||
            onAddToAlbumPressed: () =>
 | 
			
		||||
                addToAlbum(assetList[indexOfAsset.value]),
 | 
			
		||||
          ),
 | 
			
		||||
@@ -314,6 +319,59 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    buildBottomBar() {
 | 
			
		||||
      final show = (showAppBar.value || // onTap has the final say
 | 
			
		||||
              (showAppBar.value && !isZoomed.value)) &&
 | 
			
		||||
          !isPlayingVideo.value;
 | 
			
		||||
      final currentAsset = assetList[indexOfAsset.value];
 | 
			
		||||
 | 
			
		||||
      return AnimatedOpacity(
 | 
			
		||||
        duration: const Duration(milliseconds: 100),
 | 
			
		||||
        opacity: show ? 1.0 : 0.0,
 | 
			
		||||
        child: BottomNavigationBar(
 | 
			
		||||
          backgroundColor: Colors.black.withOpacity(0.4),
 | 
			
		||||
          unselectedIconTheme: const IconThemeData(color: Colors.white),
 | 
			
		||||
          selectedIconTheme: const IconThemeData(color: Colors.white),
 | 
			
		||||
          unselectedLabelStyle: const TextStyle(color: Colors.black),
 | 
			
		||||
          selectedLabelStyle: const TextStyle(color: Colors.black),
 | 
			
		||||
          showSelectedLabels: false,
 | 
			
		||||
          showUnselectedLabels: false,
 | 
			
		||||
          items: [
 | 
			
		||||
            const BottomNavigationBarItem(
 | 
			
		||||
              icon: Icon(Icons.ios_share_rounded),
 | 
			
		||||
              label: 'Share',
 | 
			
		||||
              tooltip: 'Share',
 | 
			
		||||
            ),
 | 
			
		||||
            BottomNavigationBarItem(
 | 
			
		||||
              icon: currentAsset.isArchived
 | 
			
		||||
                  ? const Icon(Icons.unarchive_rounded)
 | 
			
		||||
                  : const Icon(Icons.archive_outlined),
 | 
			
		||||
              label: 'Archive',
 | 
			
		||||
              tooltip: 'Archive',
 | 
			
		||||
            ),
 | 
			
		||||
            const BottomNavigationBarItem(
 | 
			
		||||
              icon: Icon(Icons.delete_outline),
 | 
			
		||||
              label: 'Delete',
 | 
			
		||||
              tooltip: 'Delete',
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
          onTap: (index) {
 | 
			
		||||
            switch (index) {
 | 
			
		||||
              case 0:
 | 
			
		||||
                shareAsset();
 | 
			
		||||
                break;
 | 
			
		||||
              case 1:
 | 
			
		||||
                handleArchive(assetList[indexOfAsset.value]);
 | 
			
		||||
                break;
 | 
			
		||||
              case 2:
 | 
			
		||||
                handleDelete(assetList[indexOfAsset.value]);
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      backgroundColor: Colors.black,
 | 
			
		||||
      body: WillPopScope(
 | 
			
		||||
@@ -481,6 +539,12 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
			
		||||
              right: 0,
 | 
			
		||||
              child: buildAppBar(),
 | 
			
		||||
            ),
 | 
			
		||||
            Positioned(
 | 
			
		||||
              bottom: 0,
 | 
			
		||||
              left: 0,
 | 
			
		||||
              right: 0,
 | 
			
		||||
              child: buildBottomBar(),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
 
 | 
			
		||||
@@ -109,7 +109,7 @@ class RenderList {
 | 
			
		||||
 | 
			
		||||
    final groups = _groupAssets(allAssets, groupBy);
 | 
			
		||||
 | 
			
		||||
    groups.entries.sortedBy((e) =>e.key).reversed.forEach((entry) {
 | 
			
		||||
    groups.entries.sortedBy((e) => e.key).reversed.forEach((entry) {
 | 
			
		||||
      final date = entry.key;
 | 
			
		||||
      final assets = entry.value;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -50,10 +50,9 @@ class ImmichAssetGrid extends HookConsumerWidget {
 | 
			
		||||
          // Unfortunately, using the transition animation itself didn't
 | 
			
		||||
          // seem to work reliably. So instead, wait until the duration of the
 | 
			
		||||
          // animation has elapsed to re-enable the hero animations
 | 
			
		||||
          Future.delayed(transitionDuration)
 | 
			
		||||
              .then((_) {
 | 
			
		||||
                enableHeroAnimations.value = true;
 | 
			
		||||
              });
 | 
			
		||||
          Future.delayed(transitionDuration).then((_) {
 | 
			
		||||
            enableHeroAnimations.value = true;
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
        return null;
 | 
			
		||||
      },
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import 'package:immich_mobile/shared/models/album.dart';
 | 
			
		||||
class ControlBottomAppBar extends ConsumerWidget {
 | 
			
		||||
  final Function onShare;
 | 
			
		||||
  final Function onFavorite;
 | 
			
		||||
  final Function onArchive;
 | 
			
		||||
  final Function onDelete;
 | 
			
		||||
  final Function(Album album) onAddToAlbum;
 | 
			
		||||
  final void Function() onCreateNewAlbum;
 | 
			
		||||
@@ -20,6 +21,7 @@ class ControlBottomAppBar extends ConsumerWidget {
 | 
			
		||||
    Key? key,
 | 
			
		||||
    required this.onShare,
 | 
			
		||||
    required this.onFavorite,
 | 
			
		||||
    required this.onArchive,
 | 
			
		||||
    required this.onDelete,
 | 
			
		||||
    required this.sharedAlbums,
 | 
			
		||||
    required this.albums,
 | 
			
		||||
@@ -62,6 +64,11 @@ class ControlBottomAppBar extends ConsumerWidget {
 | 
			
		||||
              );
 | 
			
		||||
            },
 | 
			
		||||
          ),
 | 
			
		||||
          ControlBoxButton(
 | 
			
		||||
            iconData: Icons.archive,
 | 
			
		||||
            label: "control_bottom_app_bar_archive".tr(),
 | 
			
		||||
            onPressed: () => onArchive(),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -94,7 +94,6 @@ class HomePage extends HookConsumerWidget {
 | 
			
		||||
          barrierDismissible: false,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // ref.watch(shareServiceProvider).shareAssets(selection.value.toList());
 | 
			
		||||
        selectionEnabledHook.value = false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@@ -132,6 +131,24 @@ class HomePage extends HookConsumerWidget {
 | 
			
		||||
        selectionEnabledHook.value = false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      void onArchiveAsset() {
 | 
			
		||||
        final remoteAssets = remoteOnlySelection(
 | 
			
		||||
          localErrorMessage: 'home_page_archive_err_local'.tr(),
 | 
			
		||||
        );
 | 
			
		||||
        if (remoteAssets.isNotEmpty) {
 | 
			
		||||
          ref.watch(assetProvider.notifier).toggleArchive(remoteAssets, true);
 | 
			
		||||
 | 
			
		||||
          final assetOrAssets = remoteAssets.length > 1 ? 'assets' : 'asset';
 | 
			
		||||
          ImmichToast.show(
 | 
			
		||||
            context: context,
 | 
			
		||||
            msg: 'Moved ${remoteAssets.length} $assetOrAssets to archive',
 | 
			
		||||
            gravity: ToastGravity.CENTER,
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        selectionEnabledHook.value = false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      void onDelete() {
 | 
			
		||||
        ref.watch(assetProvider.notifier).deleteAssets(selection.value);
 | 
			
		||||
        selectionEnabledHook.value = false;
 | 
			
		||||
@@ -265,7 +282,7 @@ class HomePage extends HookConsumerWidget {
 | 
			
		||||
                ? buildLoadingIndicator()
 | 
			
		||||
                : ImmichAssetGrid(
 | 
			
		||||
                    renderList: ref.watch(assetProvider).renderList!,
 | 
			
		||||
                    assets: ref.watch(assetProvider).allAssets,
 | 
			
		||||
                    assets: ref.read(assetProvider).allAssets,
 | 
			
		||||
                    assetsPerRow: appSettingService
 | 
			
		||||
                        .getSetting(AppSettingsEnum.tilesPerRow),
 | 
			
		||||
                    showStorageIndicator: appSettingService
 | 
			
		||||
@@ -278,6 +295,7 @@ class HomePage extends HookConsumerWidget {
 | 
			
		||||
              ControlBottomAppBar(
 | 
			
		||||
                onShare: onShareAssets,
 | 
			
		||||
                onFavorite: onFavoriteAssets,
 | 
			
		||||
                onArchive: onArchiveAsset,
 | 
			
		||||
                onDelete: onDelete,
 | 
			
		||||
                onAddToAlbum: onAddToAlbum,
 | 
			
		||||
                albums: albums,
 | 
			
		||||
@@ -291,9 +309,7 @@ class HomePage extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: !selectionEnabledHook.value
 | 
			
		||||
          ? HomePageAppBar(
 | 
			
		||||
              onPopBack: reloadAllAsset,
 | 
			
		||||
            )
 | 
			
		||||
          ? HomePageAppBar(onPopBack: reloadAllAsset)
 | 
			
		||||
          : null,
 | 
			
		||||
      drawer: const ProfileDrawer(),
 | 
			
		||||
      body: buildBody(),
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user