diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index d026b42fe2..230e2a0d77 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -49,7 +49,6 @@ dart_code_metrics: # Common - avoid-accessing-collections-by-constant-index - avoid-accessing-other-classes-private-members - - avoid-async-call-in-sync-function - avoid-cascade-after-if-null - avoid-collapsible-if - avoid-collection-methods-with-unrelated-types diff --git a/mobile/integration_test/test_utils/general_helper.dart b/mobile/integration_test/test_utils/general_helper.dart index e3b28e1f3d..8daa08d70d 100644 --- a/mobile/integration_test/test_utils/general_helper.dart +++ b/mobile/integration_test/test_utils/general_helper.dart @@ -45,7 +45,7 @@ class ImmichTestHelper { await tester.pumpWidget( ProviderScope( overrides: [dbProvider.overrideWithValue(db)], - child: app.getMainWidget(), + child: const app.MainWidget(), ), ); // Post run tasks diff --git a/mobile/lib/constants/immich_colors.dart b/mobile/lib/constants/immich_colors.dart index e3f6013581..598f956619 100644 --- a/mobile/lib/constants/immich_colors.dart +++ b/mobile/lib/constants/immich_colors.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -Color immichBackgroundColor = const Color(0xFFf6f8fe); -Color immichDarkBackgroundColor = const Color.fromARGB(255, 0, 0, 0); -Color immichDarkThemePrimaryColor = const Color.fromARGB(255, 173, 203, 250); +const Color immichBackgroundColor = Color(0xFFf6f8fe); +const Color immichDarkBackgroundColor = Color.fromARGB(255, 0, 0, 0); +const Color immichDarkThemePrimaryColor = Color.fromARGB(255, 173, 203, 250); diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 6d4a812b61..a12c43b6ca 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; @@ -7,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:timezone/data/latest.dart'; import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/modules/backup/background_service/background.service.dart'; @@ -28,7 +30,6 @@ import 'package:immich_mobile/shared/providers/app_state.provider.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; import 'package:immich_mobile/shared/services/immich_logger.service.dart'; import 'package:immich_mobile/shared/services/local_notification.service.dart'; -import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; import 'package:immich_mobile/utils/immich_app_theme.dart'; import 'package:immich_mobile/utils/migration.dart'; @@ -43,10 +44,11 @@ void main() async { await initApp(); await migrateDatabaseIfNeeded(db); HttpOverrides.global = HttpSSLCertOverride(); + runApp( ProviderScope( overrides: [dbProvider.overrideWithValue(db)], - child: getMainWidget(), + child: const MainWidget(), ), ); } @@ -108,16 +110,6 @@ Future loadDb() async { return db; } -Widget getMainWidget() { - return EasyLocalization( - supportedLocales: locales, - path: translationsPath, - useFallbackTranslations: true, - fallbackLocale: locales.first, - child: const ImmichApp(), - ); -} - class ImmichApp extends ConsumerStatefulWidget { const ImmichApp({super.key}); @@ -167,10 +159,9 @@ class ImmichAppState extends ConsumerState // 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 - ? SystemUiOverlayStyle.light - : SystemUiOverlayStyle.dark; + overlayStyle = context.isDarkTheme + ? SystemUiOverlayStyle.dark + : SystemUiOverlayStyle.light; } } SystemChrome.setSystemUIOverlayStyle(overlayStyle); @@ -202,22 +193,33 @@ class ImmichAppState extends ConsumerState supportedLocales: context.supportedLocales, locale: context.locale, debugShowCheckedModeBanner: false, - home: Stack( - children: [ - MaterialApp.router( - title: 'Immich', - debugShowCheckedModeBanner: false, - themeMode: ref.watch(immichThemeProvider), - darkTheme: immichDarkTheme, - theme: immichLightTheme, - routeInformationParser: router.defaultRouteParser(), - routerDelegate: router.delegate( - navigatorObservers: () => [TabNavigationObserver(ref: ref)], - ), - ), - const ImmichLoadingOverlay(), - ], + home: MaterialApp.router( + title: 'Immich', + debugShowCheckedModeBanner: false, + themeMode: ref.watch(immichThemeProvider), + darkTheme: immichDarkTheme, + theme: immichLightTheme, + routeInformationParser: router.defaultRouteParser(), + routerDelegate: router.delegate( + navigatorObservers: () => [TabNavigationObserver(ref: ref)], + ), ), ); } } + +// ignore: prefer-single-widget-per-file +class MainWidget extends StatelessWidget { + const MainWidget({super.key}); + + @override + Widget build(BuildContext context) { + return EasyLocalization( + supportedLocales: locales, + path: translationsPath, + useFallbackTranslations: true, + fallbackLocale: locales.first, + child: const ImmichApp(), + ); + } +} diff --git a/mobile/lib/modules/album/ui/album_viewer_appbar.dart b/mobile/lib/modules/album/ui/album_viewer_appbar.dart index 766b55f9b3..0e2fc74fb3 100644 --- a/mobile/lib/modules/album/ui/album_viewer_appbar.dart +++ b/mobile/lib/modules/album/ui/album_viewer_appbar.dart @@ -43,6 +43,7 @@ class AlbumViewerAppbar extends HookConsumerWidget Widget build(BuildContext context, WidgetRef ref) { final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText; final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum; + final isProcessing = useProcessingOverlay(); final comments = album.shared ? ref.watch( activityStatisticsStateProvider( @@ -52,7 +53,7 @@ class AlbumViewerAppbar extends HookConsumerWidget : 0; deleteAlbum() async { - ImmichLoadingOverlayController.appLoader.show(); + isProcessing.value = true; final bool success; if (album.shared) { @@ -74,7 +75,7 @@ class AlbumViewerAppbar extends HookConsumerWidget ); } - ImmichLoadingOverlayController.appLoader.hide(); + isProcessing.value = false; } Future showConfirmationDialog() async { @@ -122,7 +123,7 @@ class AlbumViewerAppbar extends HookConsumerWidget } void onLeaveAlbumPressed() async { - ImmichLoadingOverlayController.appLoader.show(); + isProcessing.value = true; bool isSuccess = await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(album); @@ -140,11 +141,11 @@ class AlbumViewerAppbar extends HookConsumerWidget ); } - ImmichLoadingOverlayController.appLoader.hide(); + isProcessing.value = false; } void onRemoveFromAlbumPressed() async { - ImmichLoadingOverlayController.appLoader.show(); + isProcessing.value = true; bool isSuccess = await ref.watch(sharedAlbumProvider.notifier).removeAssetFromAlbum( @@ -167,7 +168,7 @@ class AlbumViewerAppbar extends HookConsumerWidget ); } - ImmichLoadingOverlayController.appLoader.hide(); + isProcessing.value = false; } void handleShareAssets( @@ -198,9 +199,9 @@ class AlbumViewerAppbar extends HookConsumerWidget } void onShareAssetsTo() async { - ImmichLoadingOverlayController.appLoader.show(); + isProcessing.value = true; handleShareAssets(ref, context, selected); - ImmichLoadingOverlayController.appLoader.hide(); + isProcessing.value = false; } buildBottomSheetActions() { diff --git a/mobile/lib/modules/album/views/album_options_part.dart b/mobile/lib/modules/album/views/album_options_part.dart index 492cede6a7..6ef7733392 100644 --- a/mobile/lib/modules/album/views/album_options_part.dart +++ b/mobile/lib/modules/album/views/album_options_part.dart @@ -24,6 +24,7 @@ class AlbumOptionsPage extends HookConsumerWidget { final owner = album.owner.value; final userId = ref.watch(authenticationProvider).userId; final activityEnabled = useState(album.activityEnabled); + final isProcessing = useProcessingOverlay(); final isOwner = owner?.id == userId; void showErrorMessage() { @@ -37,7 +38,7 @@ class AlbumOptionsPage extends HookConsumerWidget { } void leaveAlbum() async { - ImmichLoadingOverlayController.appLoader.show(); + isProcessing.value = true; try { final isSuccess = @@ -54,11 +55,11 @@ class AlbumOptionsPage extends HookConsumerWidget { showErrorMessage(); } - ImmichLoadingOverlayController.appLoader.hide(); + isProcessing.value = false; } void removeUserFromAlbum(User user) async { - ImmichLoadingOverlayController.appLoader.show(); + isProcessing.value = true; try { await ref @@ -71,7 +72,7 @@ class AlbumOptionsPage extends HookConsumerWidget { } context.pop(); - ImmichLoadingOverlayController.appLoader.hide(); + isProcessing.value = false; } void handleUserClick(User user) { diff --git a/mobile/lib/modules/album/views/album_viewer_page.dart b/mobile/lib/modules/album/views/album_viewer_page.dart index a75be70fd7..6d07c3b66a 100644 --- a/mobile/lib/modules/album/views/album_viewer_page.dart +++ b/mobile/lib/modules/album/views/album_viewer_page.dart @@ -33,6 +33,7 @@ class AlbumViewerPage extends HookConsumerWidget { final userId = ref.watch(authenticationProvider).userId; final selection = useState>({}); final multiSelectEnabled = useState(false); + final isProcessing = useProcessingOverlay(); useEffect( () { @@ -75,24 +76,21 @@ class AlbumViewerPage extends HookConsumerWidget { ), ); - if (returnPayload != null) { + if (returnPayload != null && returnPayload.selectedAssets.isNotEmpty) { // Check if there is new assets add - if (returnPayload.selectedAssets.isNotEmpty) { - ImmichLoadingOverlayController.appLoader.show(); + isProcessing.value = true; - var addAssetsResult = - await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum( - returnPayload.selectedAssets, - albumInfo, - ); + var addAssetsResult = + await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum( + returnPayload.selectedAssets, + albumInfo, + ); - if (addAssetsResult != null && - addAssetsResult.successfullyAdded > 0) { - ref.invalidate(albumDetailProvider(albumId)); - } - - ImmichLoadingOverlayController.appLoader.hide(); + if (addAssetsResult != null && addAssetsResult.successfullyAdded > 0) { + ref.invalidate(albumDetailProvider(albumId)); } + + isProcessing.value = false; } } @@ -102,7 +100,7 @@ class AlbumViewerPage extends HookConsumerWidget { ); if (sharedUserIds != null) { - ImmichLoadingOverlayController.appLoader.show(); + isProcessing.value = true; var isSuccess = await ref .watch(albumServiceProvider) @@ -112,7 +110,7 @@ class AlbumViewerPage extends HookConsumerWidget { ref.invalidate(albumDetailProvider(album.id)); } - ImmichLoadingOverlayController.appLoader.hide(); + isProcessing.value = false; } } diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart index 4383ed21b0..58770ed5ca 100644 --- a/mobile/lib/modules/home/views/home_page.dart +++ b/mobile/lib/modules/home/views/home_page.dart @@ -28,6 +28,7 @@ import 'package:immich_mobile/shared/providers/websocket.provider.dart'; import 'package:immich_mobile/shared/ui/immich_app_bar.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/immich_toast.dart'; +import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; import 'package:immich_mobile/utils/selection_handlers.dart'; class HomePage extends HookConsumerWidget { @@ -50,7 +51,7 @@ class HomePage extends HookConsumerWidget { final tipOneOpacity = useState(0.0); final refreshCount = useState(0); - final processing = useState(false); + final processing = useProcessingOverlay(); useEffect( () { @@ -212,10 +213,10 @@ class HomePage extends HookConsumerWidget { processing.value = true; selectionEnabledHook.value = false; try { - ref.read(manualUploadProvider.notifier).uploadAssets( - context, - selection.value.where((a) => a.storage == AssetState.local), - ); + ref.read(manualUploadProvider.notifier).uploadAssets( + context, + selection.value.where((a) => a.storage == AssetState.local), + ); } finally { processing.value = false; } @@ -323,16 +324,12 @@ class HomePage extends HookConsumerWidget { } else { refreshCount.value++; // set counter back to 0 if user does not request refresh again - Timer(const Duration(seconds: 4), () { - refreshCount.value = 0; - }); + Timer(const Duration(seconds: 4), () => refreshCount.value = 0); } } buildLoadingIndicator() { - Timer(const Duration(seconds: 2), () { - tipOneOpacity.value = 1; - }); + Timer(const Duration(seconds: 2), () => tipOneOpacity.value = 1); return Center( child: Column( @@ -415,7 +412,6 @@ class HomePage extends HookConsumerWidget { selectionAssetState: selectionAssetState.value, onStack: onStack, ), - if (processing.value) const Center(child: ImmichLoadingIndicator()), ], ), ); diff --git a/mobile/lib/modules/trash/views/trash_page.dart b/mobile/lib/modules/trash/views/trash_page.dart index 63a18c5a1a..88fd32d013 100644 --- a/mobile/lib/modules/trash/views/trash_page.dart +++ b/mobile/lib/modules/trash/views/trash_page.dart @@ -12,8 +12,8 @@ import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart'; import 'package:immich_mobile/shared/providers/server_info.provider.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/immich_toast.dart'; +import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; class TrashPage extends HookConsumerWidget { const TrashPage({super.key}); @@ -25,7 +25,7 @@ class TrashPage extends HookConsumerWidget { ref.watch(serverInfoProvider.select((v) => v.serverConfig.trashDays)); final selectionEnabledHook = useState(false); final selection = useState({}); - final processing = useState(false); + final processing = useProcessingOverlay(); void selectionListener( bool multiselect, @@ -261,8 +261,6 @@ class TrashPage extends HookConsumerWidget { ), ), if (selectionEnabledHook.value) buildBottomBar(), - if (processing.value) - const Center(child: ImmichLoadingIndicator()), ], ), ), diff --git a/mobile/lib/shared/ui/immich_loading_indicator.dart b/mobile/lib/shared/ui/immich_loading_indicator.dart index db5dd3c199..24eedcd47e 100644 --- a/mobile/lib/shared/ui/immich_loading_indicator.dart +++ b/mobile/lib/shared/ui/immich_loading_indicator.dart @@ -21,7 +21,7 @@ class ImmichLoadingIndicator extends StatelessWidget { padding: const EdgeInsets.all(15), child: const CircularProgressIndicator( color: Colors.white, - strokeWidth: 2, + strokeWidth: 3, ), ); } diff --git a/mobile/lib/shared/views/immich_loading_overlay.dart b/mobile/lib/shared/views/immich_loading_overlay.dart index 6e4ef166bc..85f0123ed9 100644 --- a/mobile/lib/shared/views/immich_loading_overlay.dart +++ b/mobile/lib/shared/views/immich_loading_overlay.dart @@ -1,41 +1,64 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; -class ImmichLoadingOverlay extends StatelessWidget { - const ImmichLoadingOverlay({ - Key? key, - }) : super(key: key); +final _loadingEntry = OverlayEntry( + builder: (context) => SizedBox.square( + dimension: double.infinity, + child: DecoratedBox( + decoration: + BoxDecoration(color: context.colorScheme.surface.withAlpha(200)), + child: const Center(child: ImmichLoadingIndicator()), + ), + ), +); + +ValueNotifier useProcessingOverlay() { + return use(const _LoadingOverlay()); +} + +class _LoadingOverlay extends Hook> { + const _LoadingOverlay(); @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: - ImmichLoadingOverlayController.appLoader.loaderShowingNotifier, - builder: (context, shouldShow, child) { - return shouldShow - ? const Scaffold( - backgroundColor: Colors.black54, - body: Center( - child: ImmichLoadingIndicator(), - ), - ) - : const SizedBox(); - }, - ); - } + _LoadingOverlayState createState() => _LoadingOverlayState(); } -class ImmichLoadingOverlayController { - static final ImmichLoadingOverlayController appLoader = - ImmichLoadingOverlayController(); - ValueNotifier loaderShowingNotifier = ValueNotifier(false); - ValueNotifier loaderTextNotifier = ValueNotifier('error message'); +class _LoadingOverlayState + extends HookState, _LoadingOverlay> { + late final _isProcessing = ValueNotifier(false)..addListener(_listener); + OverlayEntry? overlayEntry; - void show() { - loaderShowingNotifier.value = true; + void _listener() { + setState(() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_isProcessing.value) { + overlayEntry?.remove(); + overlayEntry = _loadingEntry; + Overlay.of(context).insert(_loadingEntry); + } else { + overlayEntry?.remove(); + overlayEntry = null; + } + }); + }); } - void hide() { - loaderShowingNotifier.value = false; + @override + ValueNotifier build(BuildContext context) { + return _isProcessing; } + + @override + void dispose() { + _isProcessing.dispose(); + super.dispose(); + } + + @override + Object? get debugValue => _isProcessing.value; + + @override + String get debugLabel => 'useProcessingOverlay<>'; } diff --git a/mobile/lib/utils/immich_app_theme.dart b/mobile/lib/utils/immich_app_theme.dart index 76977b4461..4313da60f9 100644 --- a/mobile/lib/utils/immich_app_theme.dart +++ b/mobile/lib/utils/immich_app_theme.dart @@ -48,8 +48,8 @@ ThemeData immichLightTheme = ThemeData( ), backgroundColor: Colors.white, ), - appBarTheme: AppBarTheme( - titleTextStyle: const TextStyle( + appBarTheme: const AppBarTheme( + titleTextStyle: TextStyle( fontFamily: 'Overpass', color: Colors.indigo, fontWeight: FontWeight.bold, @@ -61,7 +61,7 @@ ThemeData immichLightTheme = ThemeData( scrolledUnderElevation: 0, centerTitle: true, ), - bottomNavigationBarTheme: BottomNavigationBarThemeData( + bottomNavigationBarTheme: const BottomNavigationBarThemeData( type: BottomNavigationBarType.fixed, backgroundColor: immichBackgroundColor, selectedItemColor: Colors.indigo, @@ -69,7 +69,7 @@ ThemeData immichLightTheme = ThemeData( cardTheme: const CardTheme( surfaceTintColor: Colors.transparent, ), - drawerTheme: DrawerThemeData( + drawerTheme: const DrawerThemeData( backgroundColor: immichBackgroundColor, ), textTheme: const TextTheme( @@ -162,7 +162,7 @@ ThemeData immichDarkTheme = ThemeData( hintColor: Colors.grey[600], fontFamily: 'Overpass', snackBarTheme: SnackBarThemeData( - contentTextStyle: TextStyle( + contentTextStyle: const TextStyle( fontFamily: 'Overpass', color: immichDarkThemePrimaryColor, fontWeight: FontWeight.bold, @@ -174,35 +174,35 @@ ThemeData immichDarkTheme = ThemeData( foregroundColor: immichDarkThemePrimaryColor, ), ), - appBarTheme: AppBarTheme( + appBarTheme: const AppBarTheme( titleTextStyle: TextStyle( fontFamily: 'Overpass', color: immichDarkThemePrimaryColor, fontWeight: FontWeight.bold, fontSize: 18, ), - backgroundColor: const Color.fromARGB(255, 32, 33, 35), + backgroundColor: Color.fromARGB(255, 32, 33, 35), foregroundColor: immichDarkThemePrimaryColor, elevation: 0, scrolledUnderElevation: 0, centerTitle: true, ), - bottomNavigationBarTheme: BottomNavigationBarThemeData( + bottomNavigationBarTheme: const BottomNavigationBarThemeData( type: BottomNavigationBarType.fixed, - backgroundColor: const Color.fromARGB(255, 35, 36, 37), + backgroundColor: Color.fromARGB(255, 35, 36, 37), selectedItemColor: immichDarkThemePrimaryColor, ), drawerTheme: DrawerThemeData( backgroundColor: immichDarkBackgroundColor, scrimColor: Colors.white.withOpacity(0.1), ), - textTheme: TextTheme( - displayLarge: const TextStyle( + textTheme: const TextTheme( + displayLarge: TextStyle( fontSize: 26, fontWeight: FontWeight.bold, color: Color.fromARGB(255, 255, 255, 255), ), - displayMedium: const TextStyle( + displayMedium: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Color.fromARGB(255, 255, 255, 255), @@ -212,15 +212,15 @@ ThemeData immichDarkTheme = ThemeData( fontWeight: FontWeight.bold, color: immichDarkThemePrimaryColor, ), - titleSmall: const TextStyle( + titleSmall: TextStyle( fontSize: 16.0, fontWeight: FontWeight.bold, ), - titleMedium: const TextStyle( + titleMedium: TextStyle( fontSize: 18.0, fontWeight: FontWeight.bold, ), - titleLarge: const TextStyle( + titleLarge: TextStyle( fontSize: 26.0, fontWeight: FontWeight.bold, ), @@ -258,7 +258,7 @@ ThemeData immichDarkTheme = ThemeData( dialogTheme: const DialogTheme( surfaceTintColor: Colors.transparent, ), - inputDecorationTheme: InputDecorationTheme( + inputDecorationTheme: const InputDecorationTheme( focusedBorder: OutlineInputBorder( borderSide: BorderSide( color: immichDarkThemePrimaryColor, @@ -267,12 +267,12 @@ ThemeData immichDarkTheme = ThemeData( labelStyle: TextStyle( color: immichDarkThemePrimaryColor, ), - hintStyle: const TextStyle( + hintStyle: TextStyle( fontSize: 14.0, fontWeight: FontWeight.normal, ), ), - textSelectionTheme: TextSelectionThemeData( + textSelectionTheme: const TextSelectionThemeData( cursorColor: immichDarkThemePrimaryColor, ), );