refactor(mobile): immich loading overlay (#5320)

* refactor: dcm fixes

* refactor: ImmichLoadingOverlay to custom hook

* chore: dart fixes

* pr changes

* fix: process overlay add / remove in postframecallback

---------

Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
shenlong 2023-11-29 04:20:00 +00:00 committed by GitHub
parent 513f252a0c
commit 527d602a9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 146 additions and 128 deletions

View file

@ -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

View file

@ -45,7 +45,7 @@ class ImmichTestHelper {
await tester.pumpWidget(
ProviderScope(
overrides: [dbProvider.overrideWithValue(db)],
child: app.getMainWidget(),
child: const app.MainWidget(),
),
);
// Post run tasks

View file

@ -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);

View file

@ -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<Isar> 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<ImmichApp>
// 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<ImmichApp>
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(),
);
}
}

View file

@ -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<void> 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() {

View file

@ -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) {

View file

@ -33,6 +33,7 @@ class AlbumViewerPage extends HookConsumerWidget {
final userId = ref.watch(authenticationProvider).userId;
final selection = useState<Set<Asset>>({});
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;
}
}

View file

@ -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()),
],
),
);

View file

@ -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(<Asset>{});
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()),
],
),
),

View file

@ -21,7 +21,7 @@ class ImmichLoadingIndicator extends StatelessWidget {
padding: const EdgeInsets.all(15),
child: const CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
strokeWidth: 3,
),
);
}

View file

@ -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<bool> useProcessingOverlay() {
return use(const _LoadingOverlay());
}
class _LoadingOverlay extends Hook<ValueNotifier<bool>> {
const _LoadingOverlay();
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
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<bool> loaderShowingNotifier = ValueNotifier(false);
ValueNotifier<String> loaderTextNotifier = ValueNotifier('error message');
class _LoadingOverlayState
extends HookState<ValueNotifier<bool>, _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<bool> build(BuildContext context) {
return _isProcessing;
}
@override
void dispose() {
_isProcessing.dispose();
super.dispose();
}
@override
Object? get debugValue => _isProcessing.value;
@override
String get debugLabel => 'useProcessingOverlay<>';
}

View file

@ -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,
),
);