mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feature(mobile): allow app to be used offline (#1932)
* feature(mobile): allow app to be used offline * translatable server/network error message * adjust profile drawer error message * call getAllAsset after cold app starts * fix analyzer error * update asset state if length differs --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
		
				
					committed by
					
						
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							54831878e0
						
					
				
				
					commit
					04955a4123
				
			@@ -101,6 +101,7 @@
 | 
			
		||||
  "common_change_password": "Change Password",
 | 
			
		||||
  "common_create_new_album": "Create new album",
 | 
			
		||||
  "common_shared": "Shared",
 | 
			
		||||
  "common_server_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.",
 | 
			
		||||
  "control_bottom_app_bar_add_to_album": "Add to album",
 | 
			
		||||
  "control_bottom_app_bar_album_info": "{} items",
 | 
			
		||||
  "control_bottom_app_bar_album_info_shared": "{} items · Shared",
 | 
			
		||||
 
 | 
			
		||||
@@ -52,6 +52,8 @@ class AlbumThumbnailListTile extends StatelessWidget {
 | 
			
		||||
        ),
 | 
			
		||||
        httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
 | 
			
		||||
        cacheKey: getAlbumThumbNailCacheKey(album, type: ThumbnailFormat.JPEG),
 | 
			
		||||
        errorWidget: (context, url, error) =>
 | 
			
		||||
            const Icon(Icons.image_not_supported_outlined),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ import 'package:immich_mobile/shared/services/asset.service.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/delete_dialog.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/ui/immich_image.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/ui/photo_view/photo_view_gallery.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_computed_scale.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_scale_state.dart';
 | 
			
		||||
@@ -38,8 +39,7 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.assetList,
 | 
			
		||||
    required this.asset,
 | 
			
		||||
  }) : controller =
 | 
			
		||||
      PageController(initialPage: assetList.indexOf(asset));
 | 
			
		||||
  }) : controller = PageController(initialPage: assetList.indexOf(asset));
 | 
			
		||||
 | 
			
		||||
  Asset? assetDetail;
 | 
			
		||||
 | 
			
		||||
@@ -139,12 +139,16 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void precacheNextImage(int index) {
 | 
			
		||||
      if (index < assetList.length && index > 0) {
 | 
			
		||||
      if (index < assetList.length && index >= 0) {
 | 
			
		||||
        final asset = assetList[index];
 | 
			
		||||
 | 
			
		||||
        if (asset.isLocal) {
 | 
			
		||||
          // Preload the local asset
 | 
			
		||||
          precacheImage(localImageProvider(asset), context);
 | 
			
		||||
        } else {
 | 
			
		||||
          onError(Object exception, StackTrace? stackTrace) {
 | 
			
		||||
            // swallow error silently
 | 
			
		||||
          }
 | 
			
		||||
          // Probably load WEBP either way
 | 
			
		||||
          precacheImage(
 | 
			
		||||
            remoteThumbnailImageProvider(
 | 
			
		||||
@@ -152,6 +156,7 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
			
		||||
              api.ThumbnailFormat.WEBP,
 | 
			
		||||
            ),
 | 
			
		||||
            context,
 | 
			
		||||
            onError: onError,
 | 
			
		||||
          );
 | 
			
		||||
          if (isLoadPreview.value) {
 | 
			
		||||
            // Precache the JPEG thumbnail
 | 
			
		||||
@@ -161,6 +166,7 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
			
		||||
                api.ThumbnailFormat.JPEG,
 | 
			
		||||
              ),
 | 
			
		||||
              context,
 | 
			
		||||
              onError: onError,
 | 
			
		||||
            );
 | 
			
		||||
          }
 | 
			
		||||
          if (isLoadOriginal.value) {
 | 
			
		||||
@@ -168,6 +174,7 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
			
		||||
            precacheImage(
 | 
			
		||||
              originalImageProvider(asset),
 | 
			
		||||
              context,
 | 
			
		||||
              onError: onError,
 | 
			
		||||
            );
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
@@ -350,13 +357,19 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
			
		||||
                            type: api.ThumbnailFormat.WEBP,
 | 
			
		||||
                          ),
 | 
			
		||||
                          httpHeaders: {'Authorization': authToken},
 | 
			
		||||
                          progressIndicatorBuilder: (_, __, ___) => const Center(
 | 
			
		||||
                          progressIndicatorBuilder: (_, __, ___) =>
 | 
			
		||||
                              const Center(
 | 
			
		||||
                            child: ImmichLoadingIndicator(),
 | 
			
		||||
                          ),
 | 
			
		||||
                          fadeInDuration: const Duration(milliseconds: 0),
 | 
			
		||||
                          fit: BoxFit.contain,
 | 
			
		||||
                          errorWidget: (context, url, error) =>
 | 
			
		||||
                              const Icon(Icons.image_not_supported_outlined),
 | 
			
		||||
                        );
 | 
			
		||||
 | 
			
		||||
                        if (isLoadOriginal.value) {
 | 
			
		||||
                          // loading the preview in the loadingBuilder only
 | 
			
		||||
                          // makes sense if the original is loaded in the builder
 | 
			
		||||
                          return CachedNetworkImage(
 | 
			
		||||
                            imageUrl: getThumbnailUrl(
 | 
			
		||||
                              asset,
 | 
			
		||||
@@ -370,7 +383,11 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
			
		||||
                            fit: BoxFit.contain,
 | 
			
		||||
                            fadeInDuration: const Duration(milliseconds: 0),
 | 
			
		||||
                            placeholder: (_, __) => webPThumbnail,
 | 
			
		||||
                            errorWidget: (_, __, ___) => webPThumbnail,
 | 
			
		||||
                          );
 | 
			
		||||
                        } else {
 | 
			
		||||
                          return webPThumbnail;
 | 
			
		||||
                        }
 | 
			
		||||
                      } else {
 | 
			
		||||
                        return Image(
 | 
			
		||||
                          image: localThumbnailImageProvider(asset),
 | 
			
		||||
@@ -389,17 +406,23 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
			
		||||
                  } else {
 | 
			
		||||
                    if (isLoadOriginal.value) {
 | 
			
		||||
                      provider = originalImageProvider(assetList[index]);
 | 
			
		||||
                    } else {
 | 
			
		||||
                    } else if (isLoadPreview.value) {
 | 
			
		||||
                      provider = remoteThumbnailImageProvider(
 | 
			
		||||
                        assetList[index],
 | 
			
		||||
                        api.ThumbnailFormat.JPEG,
 | 
			
		||||
                      );
 | 
			
		||||
                    } else {
 | 
			
		||||
                      provider = remoteThumbnailImageProvider(
 | 
			
		||||
                        assetList[index],
 | 
			
		||||
                        api.ThumbnailFormat.WEBP,
 | 
			
		||||
                      );
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                  return PhotoViewGalleryPageOptions(
 | 
			
		||||
                    onDragStart: (_, details, __) =>
 | 
			
		||||
                        localPosition = details.localPosition,
 | 
			
		||||
                    onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
 | 
			
		||||
                    onDragUpdate: (_, details, __) =>
 | 
			
		||||
                        handleSwipeUpDown(details),
 | 
			
		||||
                    onTapDown: (_, __, ___) =>
 | 
			
		||||
                        showAppBar.value = !showAppBar.value,
 | 
			
		||||
                    imageProvider: provider,
 | 
			
		||||
@@ -409,12 +432,17 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
			
		||||
                    filterQuality: FilterQuality.high,
 | 
			
		||||
                    tightMode: true,
 | 
			
		||||
                    minScale: PhotoViewComputedScale.contained,
 | 
			
		||||
                    errorBuilder: (context, error, stackTrace) => ImmichImage(
 | 
			
		||||
                      assetList[indexOfAsset.value],
 | 
			
		||||
                      fit: BoxFit.contain,
 | 
			
		||||
                    ),
 | 
			
		||||
                  );
 | 
			
		||||
                } else {
 | 
			
		||||
                  return PhotoViewGalleryPageOptions.customChild(
 | 
			
		||||
                    onDragStart: (_, details, __) =>
 | 
			
		||||
                        localPosition = details.localPosition,
 | 
			
		||||
                    onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
 | 
			
		||||
                    onDragUpdate: (_, details, __) =>
 | 
			
		||||
                        handleSwipeUpDown(details),
 | 
			
		||||
                    heroAttributes: PhotoViewHeroAttributes(
 | 
			
		||||
                      tag: assetList[index].id,
 | 
			
		||||
                    ),
 | 
			
		||||
 
 | 
			
		||||
@@ -66,6 +66,8 @@ class HomePageAppBar extends ConsumerWidget with PreferredSizeWidget {
 | 
			
		||||
                image:
 | 
			
		||||
                    '$endpoint/user/profile-image/${authState.userId}?d=${dummy++}',
 | 
			
		||||
                fadeInDuration: const Duration(milliseconds: 200),
 | 
			
		||||
                imageErrorBuilder: (context, error, stackTrace) =>
 | 
			
		||||
                    Image.memory(kTransparentImage),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
 
 | 
			
		||||
@@ -40,6 +40,8 @@ class ProfileDrawerHeader extends HookConsumerWidget {
 | 
			
		||||
            image:
 | 
			
		||||
                '$endpoint/user/profile-image/${authState.userId}?d=${dummy++}',
 | 
			
		||||
            fadeInDuration: const Duration(milliseconds: 200),
 | 
			
		||||
            imageErrorBuilder: (context, error, stackTrace) =>
 | 
			
		||||
                Image.memory(kTransparentImage),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
 
 | 
			
		||||
@@ -106,7 +106,9 @@ class ServerInfoBox extends HookConsumerWidget {
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                  Text(
 | 
			
		||||
                    "${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch_}",
 | 
			
		||||
                    serverInfoState.serverVersion.major > 0
 | 
			
		||||
                        ? "${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch_}"
 | 
			
		||||
                        : "?",
 | 
			
		||||
                    style: TextStyle(
 | 
			
		||||
                      fontSize: 11,
 | 
			
		||||
                      color: Colors.grey[500],
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter/services.dart';
 | 
			
		||||
import 'package:hive/hive.dart';
 | 
			
		||||
@@ -145,7 +147,14 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
 | 
			
		||||
    required String serverUrl,
 | 
			
		||||
  }) async {
 | 
			
		||||
    _apiService.setAccessToken(accessToken);
 | 
			
		||||
    var userResponseDto = await _apiService.userApi.getMyUserInfo();
 | 
			
		||||
    UserResponseDto? userResponseDto;
 | 
			
		||||
    try {
 | 
			
		||||
      userResponseDto = await _apiService.userApi.getMyUserInfo();
 | 
			
		||||
    } on ApiException catch (e) {
 | 
			
		||||
      if (e.innerException is SocketException) {
 | 
			
		||||
        state = state.copyWith(isAuthenticated: true);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (userResponseDto != null) {
 | 
			
		||||
      var userInfoHiveBox = await Hive.openBox(userInfoBox);
 | 
			
		||||
@@ -200,7 +209,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
 | 
			
		||||
      state = state.copyWith(deviceInfo: deviceInfo);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      debugPrint("ERROR Register Device Info: $e");
 | 
			
		||||
      return false;
 | 
			
		||||
      return e is ApiException && e.innerException is SocketException;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return true;
 | 
			
		||||
 
 | 
			
		||||
@@ -53,6 +53,8 @@ class ThumbnailWithInfo extends StatelessWidget {
 | 
			
		||||
                          httpHeaders: {
 | 
			
		||||
                            "Authorization": "Bearer ${box.get(accessTokenKey)}"
 | 
			
		||||
                          },
 | 
			
		||||
                          errorWidget: (context, url, error) =>
 | 
			
		||||
                              const Icon(Icons.image_not_supported_outlined),
 | 
			
		||||
                        ),
 | 
			
		||||
                      )
 | 
			
		||||
                    : Center(
 | 
			
		||||
 
 | 
			
		||||
@@ -119,7 +119,10 @@ class SearchResultPage extends HookConsumerWidget {
 | 
			
		||||
          settings.getSetting(AppSettingsEnum.storageIndicator);
 | 
			
		||||
 | 
			
		||||
      if (searchResultPageState.isError) {
 | 
			
		||||
        return const Text("Error");
 | 
			
		||||
        return Padding(
 | 
			
		||||
          padding: const EdgeInsets.all(12),
 | 
			
		||||
          child: const Text("common_server_error").tr(),
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (searchResultPageState.isLoading) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
 | 
			
		||||
import 'package:auto_route/auto_route.dart';
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:hive/hive.dart';
 | 
			
		||||
import 'package:immich_mobile/constants/hive_box.dart';
 | 
			
		||||
import 'package:immich_mobile/routing/router.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/services/api.service.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
 | 
			
		||||
class AuthGuard extends AutoRouteGuard {
 | 
			
		||||
  final ApiService _apiService;
 | 
			
		||||
@@ -11,11 +12,6 @@ class AuthGuard extends AutoRouteGuard {
 | 
			
		||||
  @override
 | 
			
		||||
  void onNavigation(NavigationResolver resolver, StackRouter router) async {
 | 
			
		||||
    try {
 | 
			
		||||
      // temporary fix for race condition that the _apiService
 | 
			
		||||
      // get called before accessToken is set
 | 
			
		||||
      var userInfoHiveBox = await Hive.openBox(userInfoBox);
 | 
			
		||||
      var accessToken = userInfoHiveBox.get(accessTokenKey);
 | 
			
		||||
      _apiService.setAccessToken(accessToken);
 | 
			
		||||
      var res = await _apiService.authenticationApi.validateAccessToken();
 | 
			
		||||
 | 
			
		||||
      if (res != null && res.authStatus) {
 | 
			
		||||
@@ -23,9 +19,15 @@ class AuthGuard extends AutoRouteGuard {
 | 
			
		||||
      } else {
 | 
			
		||||
        router.replaceAll([const LoginRoute()]);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
    } on ApiException catch (e) {
 | 
			
		||||
      if (e.code == HttpStatus.badRequest &&
 | 
			
		||||
          e.innerException is SocketException) {
 | 
			
		||||
        // offline?
 | 
			
		||||
        resolver.next(true);
 | 
			
		||||
      } else {
 | 
			
		||||
        debugPrint("Error [onNavigation] ${e.toString()}");
 | 
			
		||||
        router.replaceAll([const LoginRoute()]);
 | 
			
		||||
      }
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -104,7 +104,10 @@ class AssetNotifier extends StateNotifier<AssetsState> {
 | 
			
		||||
      final bool newLocal = await _albumService.refreshDeviceAlbums();
 | 
			
		||||
      log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms");
 | 
			
		||||
      stopwatch.reset();
 | 
			
		||||
      if (!newRemote && !newLocal) {
 | 
			
		||||
      if (!newRemote &&
 | 
			
		||||
          !newLocal &&
 | 
			
		||||
          state.allAssets.length ==
 | 
			
		||||
              await _db.assets.filter().ownerIdEqualTo(me.isarId).count()) {
 | 
			
		||||
        log.info("state is already up-to-date");
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:immich_mobile/shared/models/server_info_state.model.dart';
 | 
			
		||||
@@ -28,8 +29,7 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
 | 
			
		||||
    if (serverVersion == null) {
 | 
			
		||||
      state = state.copyWith(
 | 
			
		||||
        isVersionMismatch: true,
 | 
			
		||||
        versionMismatchErrorMessage:
 | 
			
		||||
            "Server is out of date. Some functionalities might not working correctly. Download and rebuild server",
 | 
			
		||||
        versionMismatchErrorMessage: "common_server_error".tr(),
 | 
			
		||||
      );
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -11,15 +11,17 @@ import 'package:photo_manager/photo_manager.dart';
 | 
			
		||||
class ImmichImage extends StatelessWidget {
 | 
			
		||||
  const ImmichImage(
 | 
			
		||||
    this.asset, {
 | 
			
		||||
    required this.width,
 | 
			
		||||
    required this.height,
 | 
			
		||||
    this.width,
 | 
			
		||||
    this.height,
 | 
			
		||||
    this.fit = BoxFit.cover,
 | 
			
		||||
    this.useGrayBoxPlaceholder = false,
 | 
			
		||||
    super.key,
 | 
			
		||||
  });
 | 
			
		||||
  final Asset? asset;
 | 
			
		||||
  final bool useGrayBoxPlaceholder;
 | 
			
		||||
  final double width;
 | 
			
		||||
  final double height;
 | 
			
		||||
  final double? width;
 | 
			
		||||
  final double? height;
 | 
			
		||||
  final BoxFit fit;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
@@ -47,7 +49,7 @@ class ImmichImage extends StatelessWidget {
 | 
			
		||||
        ),
 | 
			
		||||
        width: width,
 | 
			
		||||
        height: height,
 | 
			
		||||
        fit: BoxFit.cover,
 | 
			
		||||
        fit: fit,
 | 
			
		||||
        frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
 | 
			
		||||
          if (wasSynchronouslyLoaded || frame != null) {
 | 
			
		||||
            return child;
 | 
			
		||||
@@ -93,7 +95,7 @@ class ImmichImage extends StatelessWidget {
 | 
			
		||||
      // keeping memCacheWidth, memCacheHeight, maxWidthDiskCache and
 | 
			
		||||
      // maxHeightDiskCache = null allows to simply store the webp thumbnail
 | 
			
		||||
      // from the server and use it for all rendered thumbnail sizes
 | 
			
		||||
      fit: BoxFit.cover,
 | 
			
		||||
      fit: fit,
 | 
			
		||||
      fadeInDuration: const Duration(milliseconds: 250),
 | 
			
		||||
      progressIndicatorBuilder: (context, url, downloadProgress) {
 | 
			
		||||
        if (useGrayBoxPlaceholder) {
 | 
			
		||||
 
 | 
			
		||||
@@ -21,21 +21,24 @@ class SplashScreenPage extends HookConsumerWidget {
 | 
			
		||||
        Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).get(savedLoginInfoKey);
 | 
			
		||||
 | 
			
		||||
    void performLoggingIn() async {
 | 
			
		||||
      try {
 | 
			
		||||
      bool isSuccess = false;
 | 
			
		||||
      if (loginInfo != null) {
 | 
			
		||||
        try {
 | 
			
		||||
          // Resolve API server endpoint from user provided serverUrl
 | 
			
		||||
          await apiService.resolveAndSetEndpoint(loginInfo.serverUrl);
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          // okay, try to continue anyway if offline
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
          var isSuccess = await ref
 | 
			
		||||
              .read(authenticationProvider.notifier)
 | 
			
		||||
              .setSuccessLoginInfo(
 | 
			
		||||
        isSuccess =
 | 
			
		||||
            await ref.read(authenticationProvider.notifier).setSuccessLoginInfo(
 | 
			
		||||
                  accessToken: loginInfo.accessToken,
 | 
			
		||||
                  serverUrl: loginInfo.serverUrl,
 | 
			
		||||
                );
 | 
			
		||||
      }
 | 
			
		||||
      if (isSuccess) {
 | 
			
		||||
            final hasPermission = await ref
 | 
			
		||||
                .read(galleryPermissionNotifier.notifier)
 | 
			
		||||
                .hasPermission;
 | 
			
		||||
        final hasPermission =
 | 
			
		||||
            await ref.read(galleryPermissionNotifier.notifier).hasPermission;
 | 
			
		||||
        if (hasPermission) {
 | 
			
		||||
          // Resume backup (if enable) then navigate
 | 
			
		||||
          ref.watch(backupProvider.notifier).resumeBackup();
 | 
			
		||||
@@ -45,10 +48,6 @@ class SplashScreenPage extends HookConsumerWidget {
 | 
			
		||||
        AutoRouter.of(context).replace(const LoginRoute());
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
      } catch (_) {
 | 
			
		||||
        AutoRouter.of(context).replace(const LoginRoute());
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    useEffect(
 | 
			
		||||
      () {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user