forked from Cutlery/immich
		
	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:
		
							parent
							
								
									635eee9e5e
								
							
						
					
					
						commit
						2e5cd986dd
					
				@ -111,6 +111,7 @@
 | 
			
		||||
  "control_bottom_app_bar_create_new_album": "Create new album",
 | 
			
		||||
  "control_bottom_app_bar_delete": "Delete",
 | 
			
		||||
  "control_bottom_app_bar_favorite": "Favorite",
 | 
			
		||||
  "control_bottom_app_bar_archive": "Archive",
 | 
			
		||||
  "control_bottom_app_bar_share": "Share",
 | 
			
		||||
  "create_album_page_untitled": "Untitled",
 | 
			
		||||
  "create_shared_album_page_create": "Create",
 | 
			
		||||
@ -139,6 +140,7 @@
 | 
			
		||||
  "home_page_add_to_album_success": "Added {added} assets to album {album}.",
 | 
			
		||||
  "home_page_building_timeline": "Building the timeline",
 | 
			
		||||
  "home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
 | 
			
		||||
  "home_page_archive_err_local": "Can not archive local assets yet, skipping",
 | 
			
		||||
  "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
 | 
			
		||||
  "image_viewer_page_state_provider_download_error": "Download Error",
 | 
			
		||||
  "image_viewer_page_state_provider_download_success": "Download Success",
 | 
			
		||||
@ -147,6 +149,7 @@
 | 
			
		||||
  "library_page_favorites": "Favorites",
 | 
			
		||||
  "library_page_new_album": "New album",
 | 
			
		||||
  "library_page_sharing": "Sharing",
 | 
			
		||||
  "library_page_archive": "Archive",
 | 
			
		||||
  "library_page_sort_created": "Most recently created",
 | 
			
		||||
  "library_page_sort_title": "Album title",
 | 
			
		||||
  "login_form_api_exception": "API exception. Please check the server URL and try again.",
 | 
			
		||||
@ -268,5 +271,6 @@
 | 
			
		||||
  "advanced_settings_troubleshooting_title": "Troubleshooting",
 | 
			
		||||
  "advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
 | 
			
		||||
  "description_input_submit_error": "Error updating description, check the log for more details",
 | 
			
		||||
  "description_input_hint_text": "Add description..."
 | 
			
		||||
  "description_input_hint_text": "Add description...",
 | 
			
		||||
  "archive_page_title": "Archive ({})"
 | 
			
		||||
}
 | 
			
		||||
@ -192,12 +192,15 @@ class ImmichAppState extends ConsumerState<ImmichApp>
 | 
			
		||||
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
 | 
			
		||||
 | 
			
		||||
    // Sets the navigation bar color
 | 
			
		||||
    SystemUiOverlayStyle overlayStyle = const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent);
 | 
			
		||||
    SystemUiOverlayStyle overlayStyle = const SystemUiOverlayStyle(
 | 
			
		||||
      systemNavigationBarColor: Colors.transparent,
 | 
			
		||||
    );
 | 
			
		||||
    if (Platform.isAndroid) {
 | 
			
		||||
      // Android 8 does not support transparent app bars
 | 
			
		||||
      final info = await DeviceInfoPlugin().androidInfo;
 | 
			
		||||
      if (info.version.sdkInt <= 26) {
 | 
			
		||||
        overlayStyle = MediaQuery.of(context).platformBrightness == Brightness.light
 | 
			
		||||
        overlayStyle =
 | 
			
		||||
            MediaQuery.of(context).platformBrightness == Brightness.light
 | 
			
		||||
                ? SystemUiOverlayStyle.light
 | 
			
		||||
                : SystemUiOverlayStyle.dark;
 | 
			
		||||
      }
 | 
			
		||||
@ -213,9 +216,6 @@ class ImmichAppState extends ConsumerState<ImmichApp>
 | 
			
		||||
      // needs to be delayed so that EasyLocalization is working
 | 
			
		||||
      ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -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,8 +50,7 @@ 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((_) {
 | 
			
		||||
          Future.delayed(transitionDuration).then((_) {
 | 
			
		||||
            enableHeroAnimations.value = true;
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -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(),
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,7 @@ import 'package:immich_mobile/modules/album/views/library_page.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/album/views/select_additional_user_for_sharing_page.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/album/views/select_user_for_sharing_page.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/album/views/sharing_page.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/archive/views/archive_page.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/asset_viewer/views/gallery_viewer.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/backup/views/album_preview_page.dart';
 | 
			
		||||
@ -128,6 +129,13 @@ part 'router.gr.dart';
 | 
			
		||||
    AutoRoute(
 | 
			
		||||
      page: AppLogDetailPage,
 | 
			
		||||
    ),
 | 
			
		||||
    AutoRoute(
 | 
			
		||||
      page: ArchivePage,
 | 
			
		||||
      guards: [
 | 
			
		||||
        AuthGuard,
 | 
			
		||||
        DuplicateGuard,
 | 
			
		||||
      ],
 | 
			
		||||
    ),
 | 
			
		||||
  ],
 | 
			
		||||
)
 | 
			
		||||
class AppRouter extends _$AppRouter {
 | 
			
		||||
 | 
			
		||||
@ -240,6 +240,12 @@ class _$AppRouter extends RootStackRouter {
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    ArchiveRoute.name: (routeData) {
 | 
			
		||||
      return MaterialPageX<dynamic>(
 | 
			
		||||
        routeData: routeData,
 | 
			
		||||
        child: const ArchivePage(),
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    HomeRoute.name: (routeData) {
 | 
			
		||||
      return MaterialPageX<dynamic>(
 | 
			
		||||
        routeData: routeData,
 | 
			
		||||
@ -499,6 +505,14 @@ class _$AppRouter extends RootStackRouter {
 | 
			
		||||
          AppLogDetailRoute.name,
 | 
			
		||||
          path: '/app-log-detail-page',
 | 
			
		||||
        ),
 | 
			
		||||
        RouteConfig(
 | 
			
		||||
          ArchiveRoute.name,
 | 
			
		||||
          path: '/archive-page',
 | 
			
		||||
          guards: [
 | 
			
		||||
            authGuard,
 | 
			
		||||
            duplicateGuard,
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1022,6 +1036,18 @@ class AppLogDetailRouteArgs {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// generated route for
 | 
			
		||||
/// [ArchivePage]
 | 
			
		||||
class ArchiveRoute extends PageRouteInfo<void> {
 | 
			
		||||
  const ArchiveRoute()
 | 
			
		||||
      : super(
 | 
			
		||||
          ArchiveRoute.name,
 | 
			
		||||
          path: '/archive-page',
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
  static const String name = 'ArchiveRoute';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// generated route for
 | 
			
		||||
/// [HomePage]
 | 
			
		||||
class HomeRoute extends PageRouteInfo<void> {
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,8 @@ class Asset {
 | 
			
		||||
        ownerId = fastHash(remote.ownerId),
 | 
			
		||||
        exifInfo =
 | 
			
		||||
            remote.exifInfo != null ? ExifInfo.fromDto(remote.exifInfo!) : null,
 | 
			
		||||
        isFavorite = remote.isFavorite;
 | 
			
		||||
        isFavorite = remote.isFavorite,
 | 
			
		||||
        isArchived = remote.isArchived;
 | 
			
		||||
 | 
			
		||||
  Asset.local(AssetEntity local)
 | 
			
		||||
      : localId = local.id,
 | 
			
		||||
@ -44,6 +45,7 @@ class Asset {
 | 
			
		||||
        fileModifiedAt = local.modifiedDateTime,
 | 
			
		||||
        updatedAt = local.modifiedDateTime,
 | 
			
		||||
        isFavorite = local.isFavorite,
 | 
			
		||||
        isArchived = false,
 | 
			
		||||
        fileCreatedAt = local.createDateTime {
 | 
			
		||||
    if (fileCreatedAt.year == 1970) {
 | 
			
		||||
      fileCreatedAt = fileModifiedAt;
 | 
			
		||||
@ -70,6 +72,7 @@ class Asset {
 | 
			
		||||
    this.exifInfo,
 | 
			
		||||
    required this.isFavorite,
 | 
			
		||||
    required this.isLocal,
 | 
			
		||||
    required this.isArchived,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @ignore
 | 
			
		||||
@ -132,6 +135,8 @@ class Asset {
 | 
			
		||||
 | 
			
		||||
  bool isLocal;
 | 
			
		||||
 | 
			
		||||
  bool isArchived;
 | 
			
		||||
 | 
			
		||||
  @ignore
 | 
			
		||||
  ExifInfo? exifInfo;
 | 
			
		||||
 | 
			
		||||
@ -168,7 +173,8 @@ class Asset {
 | 
			
		||||
        fileName == other.fileName &&
 | 
			
		||||
        livePhotoVideoId == other.livePhotoVideoId &&
 | 
			
		||||
        isFavorite == other.isFavorite &&
 | 
			
		||||
        isLocal == other.isLocal;
 | 
			
		||||
        isLocal == other.isLocal &&
 | 
			
		||||
        isArchived == other.isArchived;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@ -189,7 +195,8 @@ class Asset {
 | 
			
		||||
      fileName.hashCode ^
 | 
			
		||||
      livePhotoVideoId.hashCode ^
 | 
			
		||||
      isFavorite.hashCode ^
 | 
			
		||||
      isLocal.hashCode;
 | 
			
		||||
      isLocal.hashCode ^
 | 
			
		||||
      isArchived.hashCode;
 | 
			
		||||
 | 
			
		||||
  bool updateFromAssetEntity(AssetEntity ae) {
 | 
			
		||||
    // TODO check more fields;
 | 
			
		||||
@ -217,6 +224,9 @@ class Asset {
 | 
			
		||||
    height ??= a.height;
 | 
			
		||||
    exifInfo ??= a.exifInfo;
 | 
			
		||||
    exifInfo?.id = id;
 | 
			
		||||
    if (!isRemote) {
 | 
			
		||||
      isArchived = a.isArchived;
 | 
			
		||||
    }
 | 
			
		||||
    return this;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -271,7 +281,8 @@ class Asset {
 | 
			
		||||
  "isFavorite": $isFavorite, 
 | 
			
		||||
  "isLocal": $isLocal,
 | 
			
		||||
  "width": ${width ?? "N/A"},
 | 
			
		||||
  "height": ${height ?? "N/A"}
 | 
			
		||||
  "height": ${height ?? "N/A"},
 | 
			
		||||
  "isArchived": $isArchived
 | 
			
		||||
}""";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -47,49 +47,54 @@ const AssetSchema = CollectionSchema(
 | 
			
		||||
      name: r'height',
 | 
			
		||||
      type: IsarType.int,
 | 
			
		||||
    ),
 | 
			
		||||
    r'isFavorite': PropertySchema(
 | 
			
		||||
    r'isArchived': PropertySchema(
 | 
			
		||||
      id: 6,
 | 
			
		||||
      name: r'isArchived',
 | 
			
		||||
      type: IsarType.bool,
 | 
			
		||||
    ),
 | 
			
		||||
    r'isFavorite': PropertySchema(
 | 
			
		||||
      id: 7,
 | 
			
		||||
      name: r'isFavorite',
 | 
			
		||||
      type: IsarType.bool,
 | 
			
		||||
    ),
 | 
			
		||||
    r'isLocal': PropertySchema(
 | 
			
		||||
      id: 7,
 | 
			
		||||
      id: 8,
 | 
			
		||||
      name: r'isLocal',
 | 
			
		||||
      type: IsarType.bool,
 | 
			
		||||
    ),
 | 
			
		||||
    r'livePhotoVideoId': PropertySchema(
 | 
			
		||||
      id: 8,
 | 
			
		||||
      id: 9,
 | 
			
		||||
      name: r'livePhotoVideoId',
 | 
			
		||||
      type: IsarType.string,
 | 
			
		||||
    ),
 | 
			
		||||
    r'localId': PropertySchema(
 | 
			
		||||
      id: 9,
 | 
			
		||||
      id: 10,
 | 
			
		||||
      name: r'localId',
 | 
			
		||||
      type: IsarType.string,
 | 
			
		||||
    ),
 | 
			
		||||
    r'ownerId': PropertySchema(
 | 
			
		||||
      id: 10,
 | 
			
		||||
      id: 11,
 | 
			
		||||
      name: r'ownerId',
 | 
			
		||||
      type: IsarType.long,
 | 
			
		||||
    ),
 | 
			
		||||
    r'remoteId': PropertySchema(
 | 
			
		||||
      id: 11,
 | 
			
		||||
      id: 12,
 | 
			
		||||
      name: r'remoteId',
 | 
			
		||||
      type: IsarType.string,
 | 
			
		||||
    ),
 | 
			
		||||
    r'type': PropertySchema(
 | 
			
		||||
      id: 12,
 | 
			
		||||
      id: 13,
 | 
			
		||||
      name: r'type',
 | 
			
		||||
      type: IsarType.byte,
 | 
			
		||||
      enumMap: _AssettypeEnumValueMap,
 | 
			
		||||
    ),
 | 
			
		||||
    r'updatedAt': PropertySchema(
 | 
			
		||||
      id: 13,
 | 
			
		||||
      id: 14,
 | 
			
		||||
      name: r'updatedAt',
 | 
			
		||||
      type: IsarType.dateTime,
 | 
			
		||||
    ),
 | 
			
		||||
    r'width': PropertySchema(
 | 
			
		||||
      id: 14,
 | 
			
		||||
      id: 15,
 | 
			
		||||
      name: r'width',
 | 
			
		||||
      type: IsarType.int,
 | 
			
		||||
    )
 | 
			
		||||
@ -175,15 +180,16 @@ void _assetSerialize(
 | 
			
		||||
  writer.writeDateTime(offsets[3], object.fileModifiedAt);
 | 
			
		||||
  writer.writeString(offsets[4], object.fileName);
 | 
			
		||||
  writer.writeInt(offsets[5], object.height);
 | 
			
		||||
  writer.writeBool(offsets[6], object.isFavorite);
 | 
			
		||||
  writer.writeBool(offsets[7], object.isLocal);
 | 
			
		||||
  writer.writeString(offsets[8], object.livePhotoVideoId);
 | 
			
		||||
  writer.writeString(offsets[9], object.localId);
 | 
			
		||||
  writer.writeLong(offsets[10], object.ownerId);
 | 
			
		||||
  writer.writeString(offsets[11], object.remoteId);
 | 
			
		||||
  writer.writeByte(offsets[12], object.type.index);
 | 
			
		||||
  writer.writeDateTime(offsets[13], object.updatedAt);
 | 
			
		||||
  writer.writeInt(offsets[14], object.width);
 | 
			
		||||
  writer.writeBool(offsets[6], object.isArchived);
 | 
			
		||||
  writer.writeBool(offsets[7], object.isFavorite);
 | 
			
		||||
  writer.writeBool(offsets[8], object.isLocal);
 | 
			
		||||
  writer.writeString(offsets[9], object.livePhotoVideoId);
 | 
			
		||||
  writer.writeString(offsets[10], object.localId);
 | 
			
		||||
  writer.writeLong(offsets[11], object.ownerId);
 | 
			
		||||
  writer.writeString(offsets[12], object.remoteId);
 | 
			
		||||
  writer.writeByte(offsets[13], object.type.index);
 | 
			
		||||
  writer.writeDateTime(offsets[14], object.updatedAt);
 | 
			
		||||
  writer.writeInt(offsets[15], object.width);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Asset _assetDeserialize(
 | 
			
		||||
@ -199,16 +205,17 @@ Asset _assetDeserialize(
 | 
			
		||||
    fileModifiedAt: reader.readDateTime(offsets[3]),
 | 
			
		||||
    fileName: reader.readString(offsets[4]),
 | 
			
		||||
    height: reader.readIntOrNull(offsets[5]),
 | 
			
		||||
    isFavorite: reader.readBool(offsets[6]),
 | 
			
		||||
    isLocal: reader.readBool(offsets[7]),
 | 
			
		||||
    livePhotoVideoId: reader.readStringOrNull(offsets[8]),
 | 
			
		||||
    localId: reader.readString(offsets[9]),
 | 
			
		||||
    ownerId: reader.readLong(offsets[10]),
 | 
			
		||||
    remoteId: reader.readStringOrNull(offsets[11]),
 | 
			
		||||
    type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[12])] ??
 | 
			
		||||
    isArchived: reader.readBool(offsets[6]),
 | 
			
		||||
    isFavorite: reader.readBool(offsets[7]),
 | 
			
		||||
    isLocal: reader.readBool(offsets[8]),
 | 
			
		||||
    livePhotoVideoId: reader.readStringOrNull(offsets[9]),
 | 
			
		||||
    localId: reader.readString(offsets[10]),
 | 
			
		||||
    ownerId: reader.readLong(offsets[11]),
 | 
			
		||||
    remoteId: reader.readStringOrNull(offsets[12]),
 | 
			
		||||
    type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[13])] ??
 | 
			
		||||
        AssetType.other,
 | 
			
		||||
    updatedAt: reader.readDateTime(offsets[13]),
 | 
			
		||||
    width: reader.readIntOrNull(offsets[14]),
 | 
			
		||||
    updatedAt: reader.readDateTime(offsets[14]),
 | 
			
		||||
    width: reader.readIntOrNull(offsets[15]),
 | 
			
		||||
  );
 | 
			
		||||
  object.id = id;
 | 
			
		||||
  return object;
 | 
			
		||||
@ -238,19 +245,21 @@ P _assetDeserializeProp<P>(
 | 
			
		||||
    case 7:
 | 
			
		||||
      return (reader.readBool(offset)) as P;
 | 
			
		||||
    case 8:
 | 
			
		||||
      return (reader.readStringOrNull(offset)) as P;
 | 
			
		||||
      return (reader.readBool(offset)) as P;
 | 
			
		||||
    case 9:
 | 
			
		||||
      return (reader.readString(offset)) as P;
 | 
			
		||||
    case 10:
 | 
			
		||||
      return (reader.readLong(offset)) as P;
 | 
			
		||||
    case 11:
 | 
			
		||||
      return (reader.readStringOrNull(offset)) as P;
 | 
			
		||||
    case 10:
 | 
			
		||||
      return (reader.readString(offset)) as P;
 | 
			
		||||
    case 11:
 | 
			
		||||
      return (reader.readLong(offset)) as P;
 | 
			
		||||
    case 12:
 | 
			
		||||
      return (reader.readStringOrNull(offset)) as P;
 | 
			
		||||
    case 13:
 | 
			
		||||
      return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ??
 | 
			
		||||
          AssetType.other) as P;
 | 
			
		||||
    case 13:
 | 
			
		||||
      return (reader.readDateTime(offset)) as P;
 | 
			
		||||
    case 14:
 | 
			
		||||
      return (reader.readDateTime(offset)) as P;
 | 
			
		||||
    case 15:
 | 
			
		||||
      return (reader.readIntOrNull(offset)) as P;
 | 
			
		||||
    default:
 | 
			
		||||
      throw IsarError('Unknown property with id $propertyId');
 | 
			
		||||
@ -1024,6 +1033,16 @@ extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<Asset, Asset, QAfterFilterCondition> isArchivedEqualTo(
 | 
			
		||||
      bool value) {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addFilterCondition(FilterCondition.equalTo(
 | 
			
		||||
        property: r'isArchived',
 | 
			
		||||
        value: value,
 | 
			
		||||
      ));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<Asset, Asset, QAfterFilterCondition> isFavoriteEqualTo(
 | 
			
		||||
      bool value) {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
@ -1771,6 +1790,18 @@ extension AssetQuerySortBy on QueryBuilder<Asset, Asset, QSortBy> {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsArchived() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addSortBy(r'isArchived', Sort.asc);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsArchivedDesc() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addSortBy(r'isArchived', Sort.desc);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsFavorite() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addSortBy(r'isFavorite', Sort.asc);
 | 
			
		||||
@ -1965,6 +1996,18 @@ extension AssetQuerySortThenBy on QueryBuilder<Asset, Asset, QSortThenBy> {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsArchived() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addSortBy(r'isArchived', Sort.asc);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsArchivedDesc() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addSortBy(r'isArchived', Sort.desc);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsFavorite() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addSortBy(r'isFavorite', Sort.asc);
 | 
			
		||||
@ -2112,6 +2155,12 @@ extension AssetQueryWhereDistinct on QueryBuilder<Asset, Asset, QDistinct> {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<Asset, Asset, QDistinct> distinctByIsArchived() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addDistinctBy(r'isArchived');
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<Asset, Asset, QDistinct> distinctByIsFavorite() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addDistinctBy(r'isFavorite');
 | 
			
		||||
@ -2214,6 +2263,12 @@ extension AssetQueryProperty on QueryBuilder<Asset, Asset, QQueryProperty> {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<Asset, bool, QQueryOperations> isArchivedProperty() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addPropertyName(r'isArchived');
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<Asset, bool, QQueryOperations> isFavoriteProperty() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addPropertyName(r'isFavorite');
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/exif_info.dart';
 | 
			
		||||
@ -19,6 +20,8 @@ import 'package:logging/logging.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
import 'package:photo_manager/photo_manager.dart';
 | 
			
		||||
 | 
			
		||||
/// State does not contain archived assets.
 | 
			
		||||
/// Use database provider if you want to access the isArchived assets
 | 
			
		||||
class AssetsState {
 | 
			
		||||
  final List<Asset> allAssets;
 | 
			
		||||
  final RenderList? renderList;
 | 
			
		||||
@ -76,6 +79,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
 | 
			
		||||
      GroupAssetsBy
 | 
			
		||||
          .values[_settingsService.getSetting(AppSettingsEnum.groupAssetsBy)],
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    state = await AssetsState.fromAssetList(newAssetList)
 | 
			
		||||
        .withRenderDataStructure(layout);
 | 
			
		||||
  }
 | 
			
		||||
@ -112,6 +116,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
 | 
			
		||||
      }
 | 
			
		||||
      final bool newRemote = await _assetService.refreshRemoteAssets();
 | 
			
		||||
      final bool newLocal = await _albumService.refreshDeviceAlbums();
 | 
			
		||||
      debugPrint("newRemote: $newRemote, newLocal: $newLocal");
 | 
			
		||||
      log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms");
 | 
			
		||||
      stopwatch.reset();
 | 
			
		||||
      if (!newRemote &&
 | 
			
		||||
@ -139,6 +144,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
 | 
			
		||||
  Future<List<Asset>> _getUserAssets(int userId) => _db.assets
 | 
			
		||||
      .filter()
 | 
			
		||||
      .ownerIdEqualTo(userId)
 | 
			
		||||
      .isArchivedEqualTo(false)
 | 
			
		||||
      .sortByFileCreatedAtDesc()
 | 
			
		||||
      .findAll();
 | 
			
		||||
 | 
			
		||||
@ -224,13 +230,46 @@ class AssetNotifier extends StateNotifier<AssetsState> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    final index = state.allAssets.indexWhere((a) => asset.id == a.id);
 | 
			
		||||
    if (index > 0) {
 | 
			
		||||
    if (index != -1) {
 | 
			
		||||
      state.allAssets[index] = newAsset;
 | 
			
		||||
      _updateAssetsState(state.allAssets);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return newAsset.isFavorite;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> toggleArchive(Iterable<Asset> assets, bool status) async {
 | 
			
		||||
    final newAssets = await Future.wait(
 | 
			
		||||
      assets.map((a) => _assetService.changeArchiveStatus(a, status)),
 | 
			
		||||
    );
 | 
			
		||||
    int i = 0;
 | 
			
		||||
    bool unArchived = false;
 | 
			
		||||
    for (Asset oldAsset in assets) {
 | 
			
		||||
      final newAsset = newAssets[i++];
 | 
			
		||||
      if (newAsset == null) {
 | 
			
		||||
        log.severe("Change archive status failed for asset ${oldAsset.id}");
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      final index = state.allAssets.indexWhere((a) => oldAsset.id == a.id);
 | 
			
		||||
      if (newAsset.isArchived) {
 | 
			
		||||
        // remove from state
 | 
			
		||||
        if (index != -1) {
 | 
			
		||||
          state.allAssets.removeAt(index);
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        // add to state is difficult because the list is sorted
 | 
			
		||||
        unArchived = true;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (unArchived) {
 | 
			
		||||
      final User me = Store.get(StoreKey.currentUser);
 | 
			
		||||
      await _stateUpdateLock.run(
 | 
			
		||||
        () async => _updateAssetsState(await _getUserAssets(me.isarId)),
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      _updateAssetsState(state.allAssets);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
final assetProvider = StateNotifierProvider<AssetNotifier, AssetsState>((ref) {
 | 
			
		||||
 | 
			
		||||
@ -121,10 +121,21 @@ class AssetService {
 | 
			
		||||
  ) async {
 | 
			
		||||
    final dto =
 | 
			
		||||
        await _apiService.assetApi.updateAsset(asset.remoteId!, updateAssetDto);
 | 
			
		||||
    return dto == null ? null : Asset.remote(dto);
 | 
			
		||||
    if (dto != null) {
 | 
			
		||||
      final updated = Asset.remote(dto).updateFromDb(asset);
 | 
			
		||||
      if (updated.isInDb) {
 | 
			
		||||
        await _db.writeTxn(() => updated.put(_db));
 | 
			
		||||
      }
 | 
			
		||||
      return updated;
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<Asset?> changeFavoriteStatus(Asset asset, bool isFavorite) {
 | 
			
		||||
    return updateAsset(asset, UpdateAssetDto(isFavorite: isFavorite));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<Asset?> changeArchiveStatus(Asset asset, bool isArchive) {
 | 
			
		||||
    return updateAsset(asset, UpdateAssetDto(isArchived: isArchive));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
build:
 | 
			
		||||
	flutter packages pub run build_runner build
 | 
			
		||||
	flutter packages pub run build_runner build --delete-conflicting-outputs
 | 
			
		||||
 | 
			
		||||
watch:
 | 
			
		||||
	flutter packages pub run build_runner watch --delete-conflicting-outputs
 | 
			
		||||
 | 
			
		||||
@ -24,6 +24,7 @@ void main() {
 | 
			
		||||
        fileName: '',
 | 
			
		||||
        isFavorite: false,
 | 
			
		||||
        isLocal: false,
 | 
			
		||||
        isArchived: false,
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -26,6 +26,7 @@ Asset _getTestAsset(int id, bool favorite) {
 | 
			
		||||
    type: AssetType.image,
 | 
			
		||||
    fileName: '',
 | 
			
		||||
    isFavorite: favorite,
 | 
			
		||||
    isArchived: false,
 | 
			
		||||
  );
 | 
			
		||||
  a.id = id;
 | 
			
		||||
  return a;
 | 
			
		||||
 | 
			
		||||
@ -32,6 +32,7 @@ void main() {
 | 
			
		||||
      fileName: localId,
 | 
			
		||||
      isFavorite: false,
 | 
			
		||||
      isLocal: isLocal,
 | 
			
		||||
      isArchived: false,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -141,7 +141,7 @@ class {{{classname}}} {
 | 
			
		||||
        {{{name}}}: json[r'{{{baseName}}}'] is List
 | 
			
		||||
          ? (json[r'{{{baseName}}}'] as List).map((e) =>
 | 
			
		||||
              {{#items.complexType}}
 | 
			
		||||
              {{items.complexType}}.listFromJson(json[r'{{{baseName}}}']){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}
 | 
			
		||||
              {{items.complexType}}.listFromJson(json[r'{{{baseName}}}']){{#required}}{{^isNullable}}!{{/isNullable}}{{#uniqueItems}}.toSet(){{/uniqueItems}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}
 | 
			
		||||
              {{/items.complexType}}
 | 
			
		||||
              {{^items.complexType}}
 | 
			
		||||
              e == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const  <{{items.items.dataType}}>[]{{/items.isNullable}} : (e as List).cast<{{items.items.dataType}}>()
 | 
			
		||||
@ -150,7 +150,7 @@ class {{{classname}}} {
 | 
			
		||||
          :  {{#isNullable}}null{{/isNullable}}{{^isNullable}}const []{{/isNullable}},
 | 
			
		||||
            {{/items.isArray}}
 | 
			
		||||
            {{^items.isArray}}
 | 
			
		||||
        {{{name}}}: {{{complexType}}}.listFromJson(json[r'{{{baseName}}}']){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
 | 
			
		||||
        {{{name}}}: {{{complexType}}}.listFromJson(json[r'{{{baseName}}}']){{#required}}{{^isNullable}}!{{/isNullable}}{{#uniqueItems}}.toSet(){{/uniqueItems}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
 | 
			
		||||
            {{/items.isArray}}
 | 
			
		||||
          {{/isArray}}
 | 
			
		||||
          {{^isArray}}
 | 
			
		||||
@ -197,7 +197,7 @@ class {{{classname}}} {
 | 
			
		||||
        {{^complexType}}
 | 
			
		||||
          {{#isArray}}
 | 
			
		||||
            {{#isEnum}}
 | 
			
		||||
        {{{name}}}: {{{items.datatypeWithEnum}}}.listFromJson(json[r'{{{baseName}}}']){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
 | 
			
		||||
        {{{name}}}: {{{items.datatypeWithEnum}}}.listFromJson(json[r'{{{baseName}}}']){{#required}}{{^isNullable}}!{{/isNullable}}{{#uniqueItems}}.toSet(){{/uniqueItems}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
 | 
			
		||||
            {{/isEnum}}
 | 
			
		||||
            {{^isEnum}}
 | 
			
		||||
        {{{name}}}: json[r'{{{baseName}}}'] is {{#uniqueItems}}Set{{/uniqueItems}}{{^uniqueItems}}List{{/uniqueItems}}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user