From aacecd3b1cdc2b01e491dfa2a08c0b59132433c9 Mon Sep 17 00:00:00 2001 From: Denys Vitali Date: Wed, 8 Oct 2025 09:14:13 +0200 Subject: [PATCH 1/3] feat(mobile): display error on app init fail --- mobile/lib/main.dart | 63 ++++-- .../pages/common/error_display_screen.dart | 205 ++++++++++++++++++ 2 files changed, 247 insertions(+), 21 deletions(-) create mode 100644 mobile/lib/pages/common/error_display_screen.dart diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 263a5ef7695cb..5a857a9689927 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -37,35 +37,56 @@ import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:immich_mobile/utils/cache/widgets_binding.dart'; import 'package:immich_mobile/utils/debug_print.dart'; -import 'package:immich_mobile/utils/http_ssl_options.dart'; import 'package:immich_mobile/utils/licenses.dart'; import 'package:immich_mobile/utils/migration.dart'; +import 'package:immich_mobile/pages/common/error_display_screen.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:logging/logging.dart'; import 'package:timezone/data/latest.dart'; import 'package:worker_manager/worker_manager.dart'; void main() async { - ImmichWidgetsBinding(); - unawaited(BackgroundWorkerLockService(BackgroundWorkerLockApi()).lock()); - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb); - await initApp(); - // Warm-up isolate pool for worker manager - await workerManager.init(dynamicSpawning: true); - await migrateDatabaseIfNeeded(isar, drift); - HttpSSLOptions.apply(); - - runApp( - ProviderScope( - overrides: [ - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), - driftProvider.overrideWith(driftOverride(drift)), - ], - child: const MainWidget(), - ), - ); + try { + ImmichWidgetsBinding(); + unawaited(BackgroundWorkerLockService(BackgroundWorkerLockApi()).lock()); + final (isar, drift, logDb) = await Bootstrap.initDB(); + await Bootstrap.initDomain(isar, drift, logDb); + await initApp(); + // Warm-up isolate pool for worker manager + await workerManager.init(dynamicSpawning: true); + await migrateDatabaseIfNeeded(isar, drift); + + runApp( + ProviderScope( + overrides: [ + dbProvider.overrideWithValue(isar), + isarProvider.overrideWithValue(isar), + driftProvider.overrideWith(driftOverride(drift)), + ], + child: const MainWidget(), + ), + ); + } catch (e, stackTrace) { + // Log the error for debugging + debugPrint('Fatal initialization error: $e'); + debugPrint('Stack trace: $stackTrace'); + + // Display a prominent error message to the user + runApp( + MaterialApp( + title: 'Immich - Initialization Error', + home: ErrorDisplayScreen( + error: e.toString(), + stackTrace: stackTrace.toString(), + ), + debugShowCheckedModeBanner: false, + theme: ThemeData( + brightness: Brightness.dark, + primarySwatch: Colors.red, + ), + ), + ); + } } Future initApp() async { diff --git a/mobile/lib/pages/common/error_display_screen.dart b/mobile/lib/pages/common/error_display_screen.dart new file mode 100644 index 0000000000000..b369daffc9d2a --- /dev/null +++ b/mobile/lib/pages/common/error_display_screen.dart @@ -0,0 +1,205 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +class ErrorDisplayScreen extends StatelessWidget { + final String error; + final String stackTrace; + + const ErrorDisplayScreen({ + super.key, + required this.error, + required this.stackTrace, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // App logo with error indicator + Stack( + children: [ + Image.asset( + 'assets/immich-logo.png', + width: 80, + height: 80, + ), + Positioned( + bottom: 0, + right: 0, + child: Container( + width: 24, + height: 24, + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.error, + color: Colors.white, + size: 16, + ), + ), + ), + ], + ), + const SizedBox(height: 24), + + // Error title + const Text( + 'Initialization Failed', + style: TextStyle( + color: Colors.white, + fontSize: 28, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + + // Error message + const Text( + 'Failed to start due to an error during initialization.', + style: TextStyle( + color: Colors.grey, + fontSize: 16, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + + // Expandable error details + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey[900], + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.red.withValues(alpha: 0.3)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Error Details:', + style: TextStyle( + color: Colors.red, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + IconButton( + onPressed: () async { + try { + final packageInfo = await PackageInfo.fromPlatform(); + final appVersion = '${packageInfo.version} build.${packageInfo.buildNumber}'; + final errorDetails = 'App Version: $appVersion\n\nError: $error\n\nStack Trace:\n$stackTrace'; + Clipboard.setData(ClipboardData(text: errorDetails)).then((_) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Error details copied to clipboard'), + backgroundColor: Colors.green, + ), + ); + }); + } catch (e) { + // Fallback if package info fails + final errorDetails = 'Error: $error\n\nStack Trace:\n$stackTrace'; + Clipboard.setData(ClipboardData(text: errorDetails)).then((_) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Error details copied to clipboard'), + backgroundColor: Colors.green, + ), + ); + }); + } + }, + icon: const Icon( + Icons.copy, + color: Colors.grey, + size: 18, + ), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + const SizedBox(height: 8), + Text( + error, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontFamily: 'monospace', + ), + ), + const SizedBox(height: 16), + ExpansionTile( + title: const Text( + 'Stack Trace', + style: TextStyle( + color: Colors.orange, + fontSize: 12, + ), + ), + tilePadding: EdgeInsets.zero, + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(8), + ), + child: SingleChildScrollView( + child: Text( + stackTrace, + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontFamily: 'monospace', + ), + ), + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 24), + + // Restart button + ElevatedButton.icon( + onPressed: () { + // Attempt to restart the app + if (Platform.isAndroid || Platform.isIOS) { + exit(0); + } + }, + icon: const Icon(Icons.refresh), + label: const Text('Restart App'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + ), + ), + ], + ), + ), + ), + ); + } +} From 339daab18209eb108c1fd7f51e34dfec0cd1c9fb Mon Sep 17 00:00:00 2001 From: Denys Vitali Date: Wed, 8 Oct 2025 09:30:05 +0200 Subject: [PATCH 2/3] fix(mobile): use colorScheme colors - reduce code repetitions --- mobile/lib/main.dart | 4 - .../pages/common/error_display_screen.dart | 84 +++++++++---------- 2 files changed, 41 insertions(+), 47 deletions(-) diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 5a857a9689927..b02828450aa42 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -80,10 +80,6 @@ void main() async { stackTrace: stackTrace.toString(), ), debugShowCheckedModeBanner: false, - theme: ThemeData( - brightness: Brightness.dark, - primarySwatch: Colors.red, - ), ), ); } diff --git a/mobile/lib/pages/common/error_display_screen.dart b/mobile/lib/pages/common/error_display_screen.dart index b369daffc9d2a..9e5dc0266252d 100644 --- a/mobile/lib/pages/common/error_display_screen.dart +++ b/mobile/lib/pages/common/error_display_screen.dart @@ -16,6 +16,8 @@ class ErrorDisplayScreen extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); + return Scaffold( body: Center( child: Padding( @@ -37,13 +39,13 @@ class ErrorDisplayScreen extends StatelessWidget { child: Container( width: 24, height: 24, - decoration: const BoxDecoration( - color: Colors.red, + decoration: BoxDecoration( + color: theme.colorScheme.error, shape: BoxShape.circle, ), - child: const Icon( + child: Icon( Icons.error, - color: Colors.white, + color: theme.colorScheme.onError, size: 16, ), ), @@ -53,22 +55,22 @@ class ErrorDisplayScreen extends StatelessWidget { const SizedBox(height: 24), // Error title - const Text( + Text( 'Initialization Failed', style: TextStyle( - color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold, + color: theme.colorScheme.onSurface, ), textAlign: TextAlign.center, ), const SizedBox(height: 16), // Error message - const Text( + Text( 'Failed to start due to an error during initialization.', style: TextStyle( - color: Colors.grey, + color: theme.colorScheme.onSurfaceVariant, fontSize: 16, ), textAlign: TextAlign.center, @@ -80,9 +82,9 @@ class ErrorDisplayScreen extends StatelessWidget { width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.grey[900], + color: theme.colorScheme.surfaceContainer, borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.red.withValues(alpha: 0.3)), + border: Border.all(color: theme.colorScheme.error), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -90,44 +92,38 @@ class ErrorDisplayScreen extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( + Text( 'Error Details:', style: TextStyle( - color: Colors.red, + color: theme.colorScheme.error, fontSize: 14, fontWeight: FontWeight.bold, ), ), IconButton( onPressed: () async { + String errorDetails; try { final packageInfo = await PackageInfo.fromPlatform(); final appVersion = '${packageInfo.version} build.${packageInfo.buildNumber}'; - final errorDetails = 'App Version: $appVersion\n\nError: $error\n\nStack Trace:\n$stackTrace'; - Clipboard.setData(ClipboardData(text: errorDetails)).then((_) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Error details copied to clipboard'), - backgroundColor: Colors.green, - ), - ); - }); + errorDetails = 'App Version: $appVersion\n\nError: $error\n\nStack Trace:\n$stackTrace'; } catch (e) { // Fallback if package info fails - final errorDetails = 'Error: $error\n\nStack Trace:\n$stackTrace'; - Clipboard.setData(ClipboardData(text: errorDetails)).then((_) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Error details copied to clipboard'), - backgroundColor: Colors.green, - ), - ); - }); + errorDetails = 'Error: $error\n\nStack Trace:\n$stackTrace'; } + + Clipboard.setData(ClipboardData(text: errorDetails)).then((_) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Error details copied to clipboard'), + backgroundColor: theme.colorScheme.primary, + ), + ); + }); }, - icon: const Icon( + icon: Icon( Icons.copy, - color: Colors.grey, + color: theme.colorScheme.onSurfaceVariant, size: 18, ), padding: EdgeInsets.zero, @@ -138,35 +134,37 @@ class ErrorDisplayScreen extends StatelessWidget { const SizedBox(height: 8), Text( error, - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: theme.colorScheme.onSurface, fontSize: 12, fontFamily: 'monospace', ), ), const SizedBox(height: 16), ExpansionTile( - title: const Text( + title: Text( 'Stack Trace', style: TextStyle( - color: Colors.orange, + color: theme.colorScheme.tertiary, fontSize: 12, ), ), tilePadding: EdgeInsets.zero, + iconColor: theme.colorScheme.onSurfaceVariant, + collapsedIconColor: theme.colorScheme.onSurfaceVariant, children: [ Container( width: double.infinity, padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.5), + color: theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), ), child: SingleChildScrollView( child: Text( stackTrace, - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: theme.colorScheme.onSurface, fontSize: 10, fontFamily: 'monospace', ), @@ -188,11 +186,11 @@ class ErrorDisplayScreen extends StatelessWidget { exit(0); } }, - icon: const Icon(Icons.refresh), - label: const Text('Restart App'), + icon: const Icon(Icons.close), + label: const Text('Close App'), style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, + backgroundColor: theme.colorScheme.error, + foregroundColor: theme.colorScheme.onError, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), ), ), From 22c5e49811a3b086916c34c1cc665158adee8975 Mon Sep 17 00:00:00 2001 From: Denys Vitali Date: Wed, 8 Oct 2025 09:32:03 +0200 Subject: [PATCH 3/3] fix: re-add removed HttpSSLOptions --- mobile/lib/main.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index b02828450aa42..f5409618aaaaa 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -37,6 +37,7 @@ import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:immich_mobile/utils/cache/widgets_binding.dart'; import 'package:immich_mobile/utils/debug_print.dart'; +import 'package:immich_mobile/utils/http_ssl_options.dart'; import 'package:immich_mobile/utils/licenses.dart'; import 'package:immich_mobile/utils/migration.dart'; import 'package:immich_mobile/pages/common/error_display_screen.dart'; @@ -55,6 +56,7 @@ void main() async { // Warm-up isolate pool for worker manager await workerManager.init(dynamicSpawning: true); await migrateDatabaseIfNeeded(isar, drift); + HttpSSLOptions.apply(); runApp( ProviderScope(