فهرست منبع

refactor(mobile): log asyncvalue errors (#5327)

* refactor: scaffoldwhen to log errors during scaffold body render

* refactor: onError and onLoading scaffoldbody

* refactor: more scaffold body to custom extension

* refactor: add skiploadingonrefresh

* Snackbar color

---------

Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
shenlong 1 سال پیش
والد
کامیت
513f252a0c
26فایلهای تغییر یافته به همراه203 افزوده شده و 215 حذف شده
  1. 19 9
      mobile/lib/extensions/asyncvalue_extensions.dart
  2. 4 7
      mobile/lib/modules/activities/views/activities_page.dart
  3. 1 3
      mobile/lib/modules/album/views/album_options_part.dart
  4. 4 10
      mobile/lib/modules/album/views/album_viewer_page.dart
  5. 3 6
      mobile/lib/modules/album/views/asset_selection_page.dart
  6. 3 7
      mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart
  7. 9 8
      mobile/lib/modules/album/views/select_user_for_sharing_page.dart
  8. 34 42
      mobile/lib/modules/archive/views/archive_page.dart
  9. 8 2
      mobile/lib/modules/asset_viewer/ui/advanced_bottom_sheet.dart
  10. 3 0
      mobile/lib/modules/backup/views/backup_controller_page.dart
  11. 15 20
      mobile/lib/modules/favorite/views/favorites_page.dart
  12. 3 7
      mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart
  13. 3 5
      mobile/lib/modules/partner/views/partner_detail_page.dart
  14. 3 7
      mobile/lib/modules/search/views/all_motion_videos_page.dart
  15. 3 7
      mobile/lib/modules/search/views/all_people_page.dart
  16. 3 7
      mobile/lib/modules/search/views/all_videos_page.dart
  17. 3 7
      mobile/lib/modules/search/views/curated_location_page.dart
  18. 1 3
      mobile/lib/modules/search/views/person_result_page.dart
  19. 3 7
      mobile/lib/modules/search/views/recently_added_page.dart
  20. 8 9
      mobile/lib/modules/search/views/search_page.dart
  21. 16 6
      mobile/lib/modules/shared_link/ui/shared_link_item.dart
  22. 6 1
      mobile/lib/modules/shared_link/views/shared_link_edit_page.dart
  23. 8 6
      mobile/lib/modules/shared_link/views/shared_link_page.dart
  24. 10 16
      mobile/lib/modules/trash/views/trash_page.dart
  25. 14 11
      mobile/lib/shared/ui/scaffold_error_body.dart
  26. 16 2
      mobile/lib/shared/views/app_log_detail_page.dart

+ 19 - 9
mobile/lib/extensions/asyncvalue_extensions.dart

@@ -4,22 +4,32 @@ import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:immich_mobile/shared/ui/scaffold_error_body.dart';
 import 'package:logging/logging.dart';
 
-extension ScaffoldBody<T> on AsyncValue<T> {
-  static final Logger _scaffoldBodyLog = Logger("ScaffoldBody");
+extension LogOnError<T> on AsyncValue<T> {
+  static final Logger _asyncErrorLogger = Logger("AsyncValue");
 
-  Widget scaffoldBodyWhen({
+  Widget widgetWhen({
+    bool skipLoadingOnRefresh = true,
+    Widget Function()? onLoading,
+    Widget Function(Object? error, StackTrace? stack)? onError,
     required Widget Function(T data) onData,
-    Widget? onError,
   }) {
     if (isLoading) {
-      return const Center(
-        child: ImmichLoadingIndicator(),
-      );
+      bool skip = false;
+      if (isRefreshing) {
+        skip = skipLoadingOnRefresh;
+      }
+
+      if (!skip) {
+        return onLoading?.call() ??
+            const Center(
+              child: ImmichLoadingIndicator(),
+            );
+      }
     }
 
     if (hasError && !hasValue) {
-      _scaffoldBodyLog.severe("Error occured in AsyncValue", error, stackTrace);
-      return onError ?? const ScaffoldErrorBody();
+      _asyncErrorLogger.severe("Error occured", error, stackTrace);
+      return onError?.call(error, stackTrace) ?? const ScaffoldErrorBody();
     }
 
     return onData(requireValue);

+ 4 - 7
mobile/lib/modules/activities/views/activities_page.dart

@@ -4,12 +4,12 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/activities/models/activity.model.dart';
 import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
 import 'package:immich_mobile/shared/models/store.dart';
 import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
 import 'package:immich_mobile/extensions/datetime_extensions.dart';
 import 'package:immich_mobile/utils/image_url_builder.dart';
@@ -88,7 +88,7 @@ class ActivitiesPage extends HookConsumerWidget {
               width: 40,
               height: 30,
               decoration: BoxDecoration(
-                borderRadius: BorderRadius.circular(4),
+                borderRadius: const BorderRadius.all(Radius.circular(4)),
                 image: DecorationImage(
                   image: CachedNetworkImageProvider(
                     getThumbnailUrlForRemoteId(
@@ -231,11 +231,8 @@ class ActivitiesPage extends HookConsumerWidget {
 
     return Scaffold(
       appBar: AppBar(title: Text(appBarTitle)),
-      body: activities.maybeWhen(
-        orElse: () {
-          return const Center(child: ImmichLoadingIndicator());
-        },
-        data: (data) {
+      body: activities.widgetWhen(
+        onData: (data) {
           final liked = data.firstWhereOrNull(
             (a) =>
                 a.type == ActivityType.like &&

+ 1 - 3
mobile/lib/modules/album/views/album_options_part.dart

@@ -180,9 +180,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
       appBar: AppBar(
         leading: IconButton(
           icon: const Icon(Icons.arrow_back_ios_new_rounded),
-          onPressed: () {
-            context.autoPop(null);
-          },
+          onPressed: () => context.autoPop(null),
         ),
         centerTitle: true,
         title: Text("translated_text_options".tr()),

+ 4 - 10
mobile/lib/modules/album/views/album_viewer_page.dart

@@ -4,6 +4,7 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
 import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
@@ -17,7 +18,6 @@ import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/shared/models/album.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:immich_mobile/shared/providers/asset.provider.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
 import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
 
@@ -260,13 +260,11 @@ class AlbumViewerPage extends HookConsumerWidget {
         error: (error, stackTrace) => AppBar(title: const Text("Error")),
         loading: () => AppBar(),
       ),
-      body: album.when(
-        data: (data) => WillPopScope(
+      body: album.widgetWhen(
+        onData: (data) => WillPopScope(
           onWillPop: onWillPop,
           child: GestureDetector(
-            onTap: () {
-              titleFocusNode.unfocus();
-            },
+            onTap: () => titleFocusNode.unfocus(),
             child: ImmichAssetGrid(
               renderList: data.renderList,
               listener: selectionListener,
@@ -285,10 +283,6 @@ class AlbumViewerPage extends HookConsumerWidget {
             ),
           ),
         ),
-        error: (e, _) => Center(child: Text("Error loading album info!\n$e")),
-        loading: () => const Center(
-          child: ImmichLoadingIndicator(),
-        ),
       ),
     );
   }

+ 3 - 6
mobile/lib/modules/album/views/asset_selection_page.dart

@@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
 import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart';
@@ -85,12 +86,8 @@ class AssetSelectionPage extends HookConsumerWidget {
             ),
         ],
       ),
-      body: renderList.when(
-        data: (data) => buildBody(data),
-        error: (error, stackTrace) => Center(
-          child: Text(error.toString()),
-        ),
-        loading: () => const Center(child: CircularProgressIndicator()),
+      body: renderList.widgetWhen(
+        onData: (data) => buildBody(data),
       ),
     );
   }

+ 3 - 7
mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart

@@ -2,11 +2,11 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart';
 import 'package:immich_mobile/shared/models/album.dart';
 import 'package:immich_mobile/shared/models/user.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
 
 class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
@@ -137,8 +137,8 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
           ),
         ],
       ),
-      body: suggestedShareUsers.when(
-        data: (users) {
+      body: suggestedShareUsers.widgetWhen(
+        onData: (users) {
           for (var sharedUsers in album.sharedUsers) {
             users.removeWhere(
               (u) => u.id == sharedUsers.id || u.id == album.ownerId,
@@ -147,10 +147,6 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
 
           return buildUserList(users);
         },
-        error: (e, _) => Text("Error loading suggested users $e"),
-        loading: () => const Center(
-          child: ImmichLoadingIndicator(),
-        ),
       ),
     );
   }

+ 9 - 8
mobile/lib/modules/album/views/select_user_for_sharing_page.dart

@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
 import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
@@ -9,7 +10,6 @@ import 'package:immich_mobile/modules/album/providers/suggested_shared_users.pro
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:immich_mobile/shared/models/user.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
 
 class SelectUserForSharingPage extends HookConsumerWidget {
@@ -42,7 +42,12 @@ class SelectUserForSharingPage extends HookConsumerWidget {
 
       ScaffoldMessenger(
         child: SnackBar(
-          content: const Text('select_user_for_sharing_page_err_album').tr(),
+          content: Text(
+            'select_user_for_sharing_page_err_album',
+            style: context.textTheme.bodyLarge?.copyWith(
+              color: context.primaryColor,
+            ),
+          ).tr(),
         ),
       );
     }
@@ -166,14 +171,10 @@ class SelectUserForSharingPage extends HookConsumerWidget {
           ),
         ],
       ),
-      body: suggestedShareUsers.when(
-        data: (users) {
+      body: suggestedShareUsers.widgetWhen(
+        onData: (users) {
           return buildUserList(users);
         },
-        error: (e, _) => Text("Error loading suggested users $e"),
-        loading: () => const Center(
-          child: ImmichLoadingIndicator(),
-        ),
       ),
     );
   }

+ 34 - 42
mobile/lib/modules/archive/views/archive_page.dart

@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/archive/providers/archive_asset_provider.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
@@ -48,37 +49,33 @@ class ArchivePage extends HookConsumerWidget {
           child: SizedBox(
             height: 64,
             child: Card(
-              child: Column(
-                children: [
-                  ListTile(
-                    shape: RoundedRectangleBorder(
-                      borderRadius: BorderRadius.circular(10),
-                    ),
-                    leading: const Icon(
-                      Icons.unarchive_rounded,
-                    ),
-                    title: Text(
-                      'control_bottom_app_bar_unarchive'.tr(),
-                      style: const TextStyle(fontSize: 14),
-                    ),
-                    onTap: processing.value
-                        ? null
-                        : () async {
-                            processing.value = true;
-                            try {
-                              await handleArchiveAssets(
-                                ref,
-                                context,
-                                selection.value.toList(),
-                                shouldArchive: false,
-                              );
-                            } finally {
-                              processing.value = false;
-                              selectionEnabledHook.value = false;
-                            }
-                          },
-                  ),
-                ],
+              child: ListTile(
+                shape: const RoundedRectangleBorder(
+                  borderRadius: BorderRadius.all(Radius.circular(10)),
+                ),
+                leading: const Icon(
+                  Icons.unarchive_rounded,
+                ),
+                title: Text(
+                  'control_bottom_app_bar_unarchive'.tr(),
+                  style: const TextStyle(fontSize: 14),
+                ),
+                onTap: processing.value
+                    ? null
+                    : () async {
+                        processing.value = true;
+                        try {
+                          await handleArchiveAssets(
+                            ref,
+                            context,
+                            selection.value.toList(),
+                            shouldArchive: false,
+                          );
+                        } finally {
+                          processing.value = false;
+                          selectionEnabledHook.value = false;
+                        }
+                      },
               ),
             ),
           ),
@@ -86,18 +83,13 @@ class ArchivePage extends HookConsumerWidget {
       );
     }
 
-    return archivedAssets.when(
-      loading: () => Scaffold(
-        appBar: buildAppBar("?"),
-        body: const Center(child: CircularProgressIndicator()),
-      ),
-      error: (error, stackTrace) => Scaffold(
-        appBar: buildAppBar("Error"),
-        body: Center(child: Text(error.toString())),
+    return Scaffold(
+      appBar: archivedAssets.maybeWhen(
+        data: (data) => buildAppBar(data.totalAssets.toString()),
+        orElse: () => buildAppBar("?"),
       ),
-      data: (data) => Scaffold(
-        appBar: buildAppBar(data.totalAssets.toString()),
-        body: data.isEmpty
+      body: archivedAssets.widgetWhen(
+        onData: (data) => data.isEmpty
             ? Center(
                 child: Text('archive_page_no_archived_assets'.tr()),
               )

+ 8 - 2
mobile/lib/modules/asset_viewer/ui/advanced_bottom_sheet.dart

@@ -62,8 +62,14 @@ class AdvancedBottomSheet extends HookConsumerWidget {
                                   ClipboardData(text: assetDetail.toString()),
                                 ).then((_) {
                                   ScaffoldMessenger.of(context).showSnackBar(
-                                    const SnackBar(
-                                      content: Text("Copied to clipboard"),
+                                    SnackBar(
+                                      content: Text(
+                                        "Copied to clipboard",
+                                        style: context.textTheme.bodyLarge
+                                            ?.copyWith(
+                                          color: context.primaryColor,
+                                        ),
+                                      ),
                                     ),
                                   );
                                 });

+ 3 - 0
mobile/lib/modules/backup/views/backup_controller_page.dart

@@ -229,6 +229,9 @@ class BackupControllerPage extends HookConsumerWidget {
       final snackBar = SnackBar(
         content: Text(
           msg.tr(),
+          style: context.textTheme.bodyLarge?.copyWith(
+            color: context.primaryColor,
+          ),
         ),
         backgroundColor: Colors.red,
       );

+ 15 - 20
mobile/lib/modules/favorite/views/favorites_page.dart

@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
@@ -62,22 +63,18 @@ class FavoritesPage extends HookConsumerWidget {
           child: SizedBox(
             height: 64,
             child: Card(
-              child: Column(
-                children: [
-                  ListTile(
-                    shape: RoundedRectangleBorder(
-                      borderRadius: BorderRadius.circular(10),
-                    ),
-                    leading: const Icon(
-                      Icons.star_border,
-                    ),
-                    title: const Text(
-                      "Unfavorite",
-                      style: TextStyle(fontSize: 14),
-                    ),
-                    onTap: processing.value ? null : unfavorite,
-                  ),
-                ],
+              child: ListTile(
+                shape: const RoundedRectangleBorder(
+                  borderRadius: BorderRadius.all(Radius.circular(10)),
+                ),
+                leading: const Icon(
+                  Icons.star_border,
+                ),
+                title: const Text(
+                  "Unfavorite",
+                  style: TextStyle(fontSize: 14),
+                ),
+                onTap: processing.value ? null : unfavorite,
               ),
             ),
           ),
@@ -87,10 +84,8 @@ class FavoritesPage extends HookConsumerWidget {
 
     return Scaffold(
       appBar: buildAppBar(),
-      body: ref.watch(favoriteAssetsProvider).when(
-            loading: () => const Center(child: CircularProgressIndicator()),
-            error: (error, stackTrace) => Center(child: Text(error.toString())),
-            data: (data) => data.isEmpty
+      body: ref.watch(favoriteAssetsProvider).widgetWhen(
+            onData: (data) => data.isEmpty
                 ? Center(
                     child: Text('favorites_page_no_favorites'.tr()),
                   )

+ 3 - 7
mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart

@@ -5,13 +5,13 @@ import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid_view.dart';
 import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
 import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
 
 class ImmichAssetGrid extends HookConsumerWidget {
@@ -130,12 +130,8 @@ class ImmichAssetGrid extends HookConsumerWidget {
     if (renderList != null) return buildAssetGridView(renderList!);
 
     final renderListFuture = ref.watch(renderListProvider(assets!));
-    return renderListFuture.when(
-      data: (renderList) => buildAssetGridView(renderList),
-      error: (err, stack) => Center(child: Text("$err")),
-      loading: () => const Center(
-        child: ImmichLoadingIndicator(),
-      ),
+    return renderListFuture.widgetWhen(
+      onData: (renderList) => buildAssetGridView(renderList),
     );
   }
 }

+ 3 - 5
mobile/lib/modules/partner/views/partner_detail_page.dart

@@ -1,11 +1,11 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 import 'package:immich_mobile/modules/partner/providers/partner.provider.dart';
 import 'package:immich_mobile/shared/models/user.dart';
 import 'package:immich_mobile/shared/providers/asset.provider.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:immich_mobile/shared/ui/immich_toast.dart';
 
 class PartnerDetailPage extends HookConsumerWidget {
@@ -71,8 +71,8 @@ class PartnerDetailPage extends HookConsumerWidget {
           ),
         ],
       ),
-      body: assets.when(
-        data: (renderList) => renderList.isEmpty
+      body: assets.widgetWhen(
+        onData: (renderList) => renderList.isEmpty
             ? Padding(
                 padding: const EdgeInsets.all(16),
                 child: Text(
@@ -84,8 +84,6 @@ class PartnerDetailPage extends HookConsumerWidget {
                 onRefresh: () =>
                     ref.read(assetProvider.notifier).getPartnerAssets(partner),
               ),
-        error: (e, _) => Text("Error loading partners:\n$e"),
-        loading: () => const Center(child: ImmichLoadingIndicator()),
       ),
     );
   }

+ 3 - 7
mobile/lib/modules/search/views/all_motion_videos_page.dart

@@ -1,10 +1,10 @@
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 import 'package:immich_mobile/modules/search/providers/all_motion_photos.provider.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 
 class AllMotionPhotosPage extends HookConsumerWidget {
   const AllMotionPhotosPage({super.key});
@@ -21,14 +21,10 @@ class AllMotionPhotosPage extends HookConsumerWidget {
           icon: const Icon(Icons.arrow_back_ios_rounded),
         ),
       ),
-      body: motionPhotos.when(
-        data: (assets) => ImmichAssetGrid(
+      body: motionPhotos.widgetWhen(
+        onData: (assets) => ImmichAssetGrid(
           assets: assets,
         ),
-        error: (e, s) => Text(e.toString()),
-        loading: () => const Center(
-          child: ImmichLoadingIndicator(),
-        ),
       ),
     );
   }

+ 3 - 7
mobile/lib/modules/search/views/all_people_page.dart

@@ -1,10 +1,10 @@
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/search/providers/people.provider.dart';
 import 'package:immich_mobile/modules/search/ui/explore_grid.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 
 class AllPeoplePage extends HookConsumerWidget {
   const AllPeoplePage({super.key});
@@ -23,12 +23,8 @@ class AllPeoplePage extends HookConsumerWidget {
           icon: const Icon(Icons.arrow_back_ios_rounded),
         ),
       ),
-      body: curatedPeople.when(
-        loading: () => const Center(child: ImmichLoadingIndicator()),
-        error: (err, stack) => Center(
-          child: Text('Error: $err'),
-        ),
-        data: (people) => ExploreGrid(
+      body: curatedPeople.widgetWhen(
+        onData: (people) => ExploreGrid(
           isPeople: true,
           curatedContent: people,
         ),

+ 3 - 7
mobile/lib/modules/search/views/all_videos_page.dart

@@ -1,10 +1,10 @@
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 import 'package:immich_mobile/modules/search/providers/all_video_assets.provider.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 
 class AllVideosPage extends HookConsumerWidget {
   const AllVideosPage({super.key});
@@ -21,14 +21,10 @@ class AllVideosPage extends HookConsumerWidget {
           icon: const Icon(Icons.arrow_back_ios_rounded),
         ),
       ),
-      body: videos.when(
-        data: (assets) => ImmichAssetGrid(
+      body: videos.widgetWhen(
+        onData: (assets) => ImmichAssetGrid(
           assets: assets,
         ),
-        error: (e, s) => Text(e.toString()),
-        loading: () => const Center(
-          child: ImmichLoadingIndicator(),
-        ),
       ),
     );
   }

+ 3 - 7
mobile/lib/modules/search/views/curated_location_page.dart

@@ -1,11 +1,11 @@
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/search/models/curated_content.dart';
 import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
 import 'package:immich_mobile/modules/search/ui/explore_grid.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:openapi/api.dart';
 
 class CuratedLocationPage extends HookConsumerWidget {
@@ -26,12 +26,8 @@ class CuratedLocationPage extends HookConsumerWidget {
           icon: const Icon(Icons.arrow_back_ios_rounded),
         ),
       ),
-      body: curatedLocation.when(
-        loading: () => const Center(child: ImmichLoadingIndicator()),
-        error: (err, stack) => Center(
-          child: Text('Error: $err'),
-        ),
-        data: (curatedLocations) => ExploreGrid(
+      body: curatedLocation.widgetWhen(
+        onData: (curatedLocations) => ExploreGrid(
           curatedContent: curatedLocations
               .map(
                 (l) => CuratedContent(

+ 1 - 3
mobile/lib/modules/search/views/person_result_page.dart

@@ -8,7 +8,6 @@ import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'
 import 'package:immich_mobile/modules/search/providers/people.provider.dart';
 import 'package:immich_mobile/modules/search/ui/person_name_edit_form.dart';
 import 'package:immich_mobile/shared/models/store.dart' as isar_store;
-import 'package:immich_mobile/shared/ui/scaffold_error_body.dart';
 import 'package:immich_mobile/utils/image_url_builder.dart';
 
 class PersonResultPage extends HookConsumerWidget {
@@ -112,7 +111,7 @@ class PersonResultPage extends HookConsumerWidget {
           ),
         ],
       ),
-      body: ref.watch(personAssetsProvider(personId)).scaffoldBodyWhen(
+      body: ref.watch(personAssetsProvider(personId)).widgetWhen(
             onData: (renderList) => ImmichAssetGrid(
               renderList: renderList,
               topWidget: Padding(
@@ -137,7 +136,6 @@ class PersonResultPage extends HookConsumerWidget {
                 ),
               ),
             ),
-            onError: const ScaffoldErrorBody(icon: Icons.person_off_outlined),
           ),
     );
   }

+ 3 - 7
mobile/lib/modules/search/views/recently_added_page.dart

@@ -1,10 +1,10 @@
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 import 'package:immich_mobile/modules/search/providers/recently_added.provider.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 
 class RecentlyAddedPage extends HookConsumerWidget {
   const RecentlyAddedPage({super.key});
@@ -21,14 +21,10 @@ class RecentlyAddedPage extends HookConsumerWidget {
           icon: const Icon(Icons.arrow_back_ios_rounded),
         ),
       ),
-      body: recents.when(
-        data: (searchResponse) => ImmichAssetGrid(
+      body: recents.widgetWhen(
+        onData: (searchResponse) => ImmichAssetGrid(
           assets: searchResponse,
         ),
-        error: (e, s) => Text(e.toString()),
-        loading: () => const Center(
-          child: ImmichLoadingIndicator(),
-        ),
       ),
     );
   }

+ 8 - 9
mobile/lib/modules/search/views/search_page.dart

@@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/search/models/curated_content.dart';
 import 'package:immich_mobile/modules/search/providers/people.provider.dart';
@@ -15,7 +16,7 @@ import 'package:immich_mobile/modules/search/ui/search_row_title.dart';
 import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/shared/providers/server_info.provider.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
+import 'package:immich_mobile/shared/ui/scaffold_error_body.dart';
 
 // ignore: must_be_immutable
 class SearchPage extends HookConsumerWidget {
@@ -73,10 +74,9 @@ class SearchPage extends HookConsumerWidget {
     buildPeople() {
       return SizedBox(
         height: imageSize,
-        child: curatedPeople.when(
-          loading: () => const Center(child: ImmichLoadingIndicator()),
-          error: (err, stack) => Center(child: Text('Error: $err')),
-          data: (people) => CuratedPeopleRow(
+        child: curatedPeople.widgetWhen(
+          onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
+          onData: (people) => CuratedPeopleRow(
             content: people.take(12).toList(),
             onTap: (content, index) {
               context.autoPush(
@@ -97,10 +97,9 @@ class SearchPage extends HookConsumerWidget {
     buildPlaces() {
       return SizedBox(
         height: imageSize,
-        child: curatedLocation.when(
-          loading: () => const Center(child: ImmichLoadingIndicator()),
-          error: (err, stack) => Center(child: Text('Error: $err')),
-          data: (locations) => CuratedPlacesRow(
+        child: curatedLocation.widgetWhen(
+          onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
+          onData: (locations) => CuratedPlacesRow(
             isMapEnabled: isMapEnabled,
             content: locations
                 .map(

+ 16 - 6
mobile/lib/modules/shared_link/ui/shared_link_item.dart

@@ -46,9 +46,11 @@ class SharedLinkItem extends ConsumerWidget {
       } else if (difference.inHours > 0) {
         expiresText = "shared_link_expires_hours".plural(difference.inHours);
       } else if (difference.inMinutes > 0) {
-        expiresText = "shared_link_expires_minutes".plural(difference.inMinutes);
+        expiresText =
+            "shared_link_expires_minutes".plural(difference.inMinutes);
       } else if (difference.inSeconds > 0) {
-        expiresText = "shared_link_expires_seconds".plural(difference.inSeconds);
+        expiresText =
+            "shared_link_expires_seconds".plural(difference.inSeconds);
       }
     }
     return Text(
@@ -85,7 +87,12 @@ class SharedLinkItem extends ConsumerWidget {
       ).then((_) {
         ScaffoldMessenger.of(context).showSnackBar(
           SnackBar(
-            content: const Text("shared_link_clipboard_copied_massage").tr(),
+            content: Text(
+              "shared_link_clipboard_copied_massage",
+              style: context.textTheme.bodyLarge?.copyWith(
+                color: context.primaryColor,
+              ),
+            ).tr(),
             duration: const Duration(seconds: 2),
           ),
         );
@@ -162,9 +169,12 @@ class SharedLinkItem extends ConsumerWidget {
     Widget buildBottomInfo() {
       return Row(
         children: [
-          if (sharedLink.allowUpload) buildInfoChip("shared_link_info_chip_upload".tr()),
-          if (sharedLink.allowDownload) buildInfoChip("shared_link_info_chip_download".tr()),
-          if (sharedLink.showMetadata) buildInfoChip("shared_link_info_chip_metadata".tr()),
+          if (sharedLink.allowUpload)
+            buildInfoChip("shared_link_info_chip_upload".tr()),
+          if (sharedLink.allowDownload)
+            buildInfoChip("shared_link_info_chip_download".tr()),
+          if (sharedLink.showMetadata)
+            buildInfoChip("shared_link_info_chip_metadata".tr()),
         ],
       );
     }

+ 6 - 1
mobile/lib/modules/shared_link/views/shared_link_edit_page.dart

@@ -275,7 +275,12 @@ class SharedLinkEditPage extends HookConsumerWidget {
       ).then((_) {
         ScaffoldMessenger.of(context).showSnackBar(
           SnackBar(
-            content: const Text("shared_link_clipboard_copied_massage").tr(),
+            content: Text(
+              "shared_link_clipboard_copied_massage",
+              style: context.textTheme.bodyLarge?.copyWith(
+                color: context.primaryColor,
+              ),
+            ).tr(),
             duration: const Duration(seconds: 2),
           ),
         );

+ 8 - 6
mobile/lib/modules/shared_link/views/shared_link_page.dart

@@ -2,11 +2,11 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/shared_link/models/shared_link.dart';
 import 'package:immich_mobile/modules/shared_link/providers/shared_link.provider.dart';
 import 'package:immich_mobile/modules/shared_link/ui/shared_link_item.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 
 class SharedLinkPage extends HookConsumerWidget {
   const SharedLinkPage({Key? key}) : super(key: key);
@@ -18,7 +18,10 @@ class SharedLinkPage extends HookConsumerWidget {
     useEffect(
       () {
         ref.read(sharedLinksStateProvider.notifier).fetchLinks();
-        return () => ref.invalidate(sharedLinksStateProvider);
+        return () {
+          if (!context.mounted) return;
+          ref.invalidate(sharedLinksStateProvider);
+        };
       },
       [],
     );
@@ -113,11 +116,10 @@ class SharedLinkPage extends HookConsumerWidget {
         centerTitle: false,
       ),
       body: SafeArea(
-        child: sharedLinks.when(
-          data: (links) =>
+        child: sharedLinks.widgetWhen(
+          onError: (error, stackTrace) => buildNoShares(),
+          onData: (links) =>
               links.isNotEmpty ? buildSharesList(links) : buildNoShares(),
-          error: (error, stackTrace) => buildNoShares(),
-          loading: () => const Center(child: ImmichLoadingIndicator()),
         ),
       ),
     );

+ 10 - 16
mobile/lib/modules/trash/views/trash_page.dart

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
@@ -229,18 +230,13 @@ class TrashPage extends HookConsumerWidget {
       );
     }
 
-    return trashedAssets.when(
-      loading: () => Scaffold(
-        appBar: buildAppBar("?"),
-        body: const Center(child: CircularProgressIndicator()),
+    return Scaffold(
+      appBar: trashedAssets.maybeWhen(
+        orElse: () => buildAppBar("?"),
+        data: (data) => buildAppBar(data.totalAssets.toString()),
       ),
-      error: (error, stackTrace) => Scaffold(
-        appBar: buildAppBar("!"),
-        body: Center(child: Text(error.toString())),
-      ),
-      data: (data) => Scaffold(
-        appBar: buildAppBar(data.totalAssets.toString()),
-        body: data.isEmpty
+      body: trashedAssets.widgetWhen(
+        onData: (data) => data.isEmpty
             ? Center(
                 child: Text('trash_page_no_assets'.tr()),
               )
@@ -254,11 +250,9 @@ class TrashPage extends HookConsumerWidget {
                       showMultiSelectIndicator: false,
                       showStack: true,
                       topWidget: Padding(
-                        padding: const EdgeInsets.only(
-                          top: 24,
-                          bottom: 24,
-                          left: 12,
-                          right: 12,
+                        padding: const EdgeInsets.symmetric(
+                          horizontal: 12,
+                          vertical: 24,
                         ),
                         child: const Text(
                           "trash_page_info",

+ 14 - 11
mobile/lib/shared/ui/scaffold_error_body.dart

@@ -4,9 +4,9 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
 
 // Error widget to be used in Scaffold when an AsyncError is received
 class ScaffoldErrorBody extends StatelessWidget {
-  final IconData icon;
+  final bool withIcon;
 
-  const ScaffoldErrorBody({this.icon = Icons.error_outline, super.key});
+  const ScaffoldErrorBody({super.key, this.withIcon = true});
 
   @override
   Widget build(BuildContext context) {
@@ -14,19 +14,22 @@ class ScaffoldErrorBody extends StatelessWidget {
       crossAxisAlignment: CrossAxisAlignment.center,
       mainAxisAlignment: MainAxisAlignment.center,
       children: [
-        const Text(
+        Text(
           "scaffold_body_error_occured",
-          style:
-              TextStyle(fontSize: 14, fontWeight: FontWeight.bold, height: 3),
+          style: context.textTheme.displayMedium,
           textAlign: TextAlign.center,
         ).tr(),
-        Center(
-          child: Icon(
-            icon,
-            size: 100,
-            color: context.themeData.iconTheme.color?.withOpacity(0.5),
+        if (withIcon)
+          Center(
+            child: Padding(
+              padding: const EdgeInsets.only(top: 15),
+              child: Icon(
+                Icons.error_outline,
+                size: 100,
+                color: context.themeData.iconTheme.color?.withOpacity(0.5),
+              ),
+            ),
           ),
-        ),
       ],
     );
   }

+ 16 - 2
mobile/lib/shared/views/app_log_detail_page.dart

@@ -39,7 +39,14 @@ class AppLogDetailPage extends HookConsumerWidget {
                     Clipboard.setData(ClipboardData(text: stackTrace))
                         .then((_) {
                       ScaffoldMessenger.of(context).showSnackBar(
-                        const SnackBar(content: Text("Copied to clipboard")),
+                        SnackBar(
+                          content: Text(
+                            "Copied to clipboard",
+                            style: context.textTheme.bodyLarge?.copyWith(
+                              color: context.primaryColor,
+                            ),
+                          ),
+                        ),
                       );
                     });
                   },
@@ -98,7 +105,14 @@ class AppLogDetailPage extends HookConsumerWidget {
                   onPressed: () {
                     Clipboard.setData(ClipboardData(text: message)).then((_) {
                       ScaffoldMessenger.of(context).showSnackBar(
-                        const SnackBar(content: Text("Copied to clipboard")),
+                        SnackBar(
+                          content: Text(
+                            "Copied to clipboard",
+                            style: context.textTheme.bodyLarge?.copyWith(
+                              color: context.primaryColor,
+                            ),
+                          ),
+                        ),
                       );
                     });
                   },