mirror of
https://github.com/immich-app/immich.git
synced 2026-03-22 09:00:58 +03:00
feat: splash screen error page (#26460)
* feat: splash screen error page * Update mobile/lib/pages/common/splash_screen.page.dart Co-authored-by: Alex <alex.tran1502@gmail.com> * add clear data action --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
@@ -1883,7 +1883,10 @@
|
|||||||
"reset_pin_code_success": "Successfully reset PIN code",
|
"reset_pin_code_success": "Successfully reset PIN code",
|
||||||
"reset_pin_code_with_password": "You can always reset your PIN code with your password",
|
"reset_pin_code_with_password": "You can always reset your PIN code with your password",
|
||||||
"reset_sqlite": "Reset SQLite Database",
|
"reset_sqlite": "Reset SQLite Database",
|
||||||
"reset_sqlite_confirmation": "Are you sure you want to reset the SQLite database? You will need to log out and log in again to resync the data",
|
"reset_sqlite_clear_app_data": "Clear Data",
|
||||||
|
"reset_sqlite_confirmation": "Are you sure you want to clear the app data? This will remove all settings and sign you out.",
|
||||||
|
"reset_sqlite_confirmation_note": "Note: You will need to restart the app after clearing.",
|
||||||
|
"reset_sqlite_done": "App data has been cleared. Please restart Immich and log in again.",
|
||||||
"reset_sqlite_success": "Successfully reset the SQLite database",
|
"reset_sqlite_success": "Successfully reset the SQLite database",
|
||||||
"reset_to_default": "Reset to default",
|
"reset_to_default": "Reset to default",
|
||||||
"resolution": "Resolution",
|
"resolution": "Resolution",
|
||||||
@@ -1911,6 +1914,7 @@
|
|||||||
"saved_settings": "Saved settings",
|
"saved_settings": "Saved settings",
|
||||||
"say_something": "Say something",
|
"say_something": "Say something",
|
||||||
"scaffold_body_error_occurred": "Error occurred",
|
"scaffold_body_error_occurred": "Error occurred",
|
||||||
|
"scaffold_body_error_unrecoverable": "An unrecoverable error has occurred. Please share the error and stack trace on Discord or GitHub so we can help. If advised, you can clear the app data below.",
|
||||||
"scan": "Scan",
|
"scan": "Scan",
|
||||||
"scan_all_libraries": "Scan All Libraries",
|
"scan_all_libraries": "Scan All Libraries",
|
||||||
"scan_library": "Scan",
|
"scan_library": "Scan",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import 'package:immich_mobile/extensions/translate_extensions.dart';
|
|||||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||||
import 'package:immich_mobile/generated/translations.g.dart';
|
import 'package:immich_mobile/generated/translations.g.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||||
|
import 'package:immich_mobile/pages/common/splash_screen.page.dart';
|
||||||
import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
|
import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
|
||||||
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
||||||
@@ -49,30 +50,34 @@ import 'package:logging/logging.dart';
|
|||||||
import 'package:timezone/data/latest.dart';
|
import 'package:timezone/data/latest.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
ImmichWidgetsBinding();
|
try {
|
||||||
unawaited(BackgroundWorkerLockService(BackgroundWorkerLockApi()).lock());
|
ImmichWidgetsBinding();
|
||||||
final (isar, drift, logDb) = await Bootstrap.initDB();
|
unawaited(BackgroundWorkerLockService(BackgroundWorkerLockApi()).lock());
|
||||||
await Bootstrap.initDomain(isar, drift, logDb);
|
await EasyLocalization.ensureInitialized();
|
||||||
await initApp();
|
final (isar, drift, logDb) = await Bootstrap.initDB();
|
||||||
// Warm-up isolate pool for worker manager
|
await Bootstrap.initDomain(isar, drift, logDb);
|
||||||
await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: max(Platform.numberOfProcessors - 1, 5));
|
await initApp();
|
||||||
await migrateDatabaseIfNeeded(isar, drift);
|
// Warm-up isolate pool for worker manager
|
||||||
HttpSSLOptions.apply();
|
await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: max(Platform.numberOfProcessors - 1, 5));
|
||||||
|
await migrateDatabaseIfNeeded(isar, drift);
|
||||||
|
HttpSSLOptions.apply();
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
ProviderScope(
|
ProviderScope(
|
||||||
overrides: [
|
overrides: [
|
||||||
dbProvider.overrideWithValue(isar),
|
dbProvider.overrideWithValue(isar),
|
||||||
isarProvider.overrideWithValue(isar),
|
isarProvider.overrideWithValue(isar),
|
||||||
driftProvider.overrideWith(driftOverride(drift)),
|
driftProvider.overrideWith(driftOverride(drift)),
|
||||||
],
|
],
|
||||||
child: const MainWidget(),
|
child: const MainWidget(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
} catch (error, stack) {
|
||||||
|
runApp(BootstrapErrorWidget(error: error.toString(), stack: stack.toString()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initApp() async {
|
Future<void> initApp() async {
|
||||||
await EasyLocalization.ensureInitialized();
|
|
||||||
await initializeDateFormatting();
|
await initializeDateFormatting();
|
||||||
|
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/colors.dart';
|
||||||
|
import 'package:immich_mobile/constants/locales.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||||
|
import 'package:immich_mobile/generated/translations.g.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||||
@@ -13,7 +20,254 @@ import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/theme/color_scheme.dart';
|
||||||
|
import 'package:immich_mobile/theme/theme_data.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_logo.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_title_text.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart' show launchUrl, LaunchMode;
|
||||||
|
|
||||||
|
class BootstrapErrorWidget extends StatelessWidget {
|
||||||
|
final String error;
|
||||||
|
final String stack;
|
||||||
|
|
||||||
|
const BootstrapErrorWidget({super.key, required this.error, required this.stack});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext _) {
|
||||||
|
final immichTheme = defaultColorPreset.themeOfPreset;
|
||||||
|
|
||||||
|
return EasyLocalization(
|
||||||
|
supportedLocales: locales.values.toList(),
|
||||||
|
path: translationsPath,
|
||||||
|
useFallbackTranslations: true,
|
||||||
|
fallbackLocale: locales.values.first,
|
||||||
|
assetLoader: const CodegenLoader(),
|
||||||
|
child: Builder(
|
||||||
|
builder: (lCtx) => MaterialApp(
|
||||||
|
title: 'Immich',
|
||||||
|
debugShowCheckedModeBanner: true,
|
||||||
|
localizationsDelegates: lCtx.localizationDelegates,
|
||||||
|
supportedLocales: lCtx.supportedLocales,
|
||||||
|
locale: lCtx.locale,
|
||||||
|
themeMode: ThemeMode.system,
|
||||||
|
darkTheme: getThemeData(colorScheme: immichTheme.dark, locale: lCtx.locale),
|
||||||
|
theme: getThemeData(colorScheme: immichTheme.light, locale: lCtx.locale),
|
||||||
|
home: Builder(
|
||||||
|
builder: (ctx) => Scaffold(
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
const SafeArea(
|
||||||
|
bottom: false,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [ImmichLogo(size: 48), SizedBox(width: 12), ImmichTitleText(fontSize: 24)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: _ErrorCard(error: error, stack: stack),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
const SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: Padding(padding: EdgeInsets.fromLTRB(24, 16, 24, 16), child: _BottomPanel()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BottomPanel extends StatefulWidget {
|
||||||
|
const _BottomPanel();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_BottomPanel> createState() => _BottomPanelState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BottomPanelState extends State<_BottomPanel> {
|
||||||
|
bool _cleared = false;
|
||||||
|
|
||||||
|
Future<void> _clearDatabase() async {
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogCtx) => AlertDialog(
|
||||||
|
title: Text(context.t.reset_sqlite_clear_app_data),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(context.t.reset_sqlite_confirmation),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
context.t.reset_sqlite_confirmation_note,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => Navigator.of(dialogCtx).pop(false), child: Text(context.t.cancel)),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(dialogCtx).pop(true),
|
||||||
|
child: Text(context.t.confirm, style: TextStyle(color: Theme.of(context).colorScheme.error)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmed != true || !mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final db = Drift();
|
||||||
|
try {
|
||||||
|
await db.reset();
|
||||||
|
} finally {
|
||||||
|
await db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _cleared = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_cleared ? context.t.reset_sqlite_done : context.t.scaffold_body_error_unrecoverable,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_ActionLink(
|
||||||
|
icon: Icons.chat_bubble_outline,
|
||||||
|
label: context.t.discord,
|
||||||
|
onTap: () => launchUrl(Uri.parse('https://discord.immich.app/'), mode: LaunchMode.externalApplication),
|
||||||
|
),
|
||||||
|
_ActionLink(
|
||||||
|
icon: Icons.bug_report_outlined,
|
||||||
|
label: context.t.profile_drawer_github,
|
||||||
|
onTap: () => launchUrl(
|
||||||
|
Uri.parse('https://github.com/immich-app/immich/issues'),
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!_cleared)
|
||||||
|
_ActionLink(
|
||||||
|
icon: Icons.delete_outline,
|
||||||
|
label: context.t.reset_sqlite_clear_app_data,
|
||||||
|
onTap: _clearDatabase,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ActionLink extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final String label;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const _ActionLink({required this.icon, required this.label, required this.onTap});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 24),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(label, style: const TextStyle(fontSize: 12)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ErrorCard extends StatelessWidget {
|
||||||
|
final String error;
|
||||||
|
final String stack;
|
||||||
|
|
||||||
|
const _ErrorCard({required this.error, required this.stack});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final scheme = Theme.of(context).colorScheme;
|
||||||
|
final textTheme = Theme.of(context).textTheme;
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ColoredBox(
|
||||||
|
color: scheme.error,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 8, 8, 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
context.t.scaffold_body_error_occurred,
|
||||||
|
style: textTheme.titleSmall?.copyWith(color: scheme.onError),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: context.t.copy_error,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
icon: Icon(Icons.copy_outlined, size: 16, color: scheme.onError),
|
||||||
|
onPressed: () => Clipboard.setData(ClipboardData(text: '$error\n\n$stack')),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Text(error, style: textTheme.bodyMedium),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(context.t.stacktrace, style: textTheme.labelMedium),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
SelectableText(stack, style: textTheme.bodySmall?.copyWith(fontFamily: 'GoogleSansCode')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class SplashScreenPage extends StatefulHookConsumerWidget {
|
class SplashScreenPage extends StatefulHookConsumerWidget {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -5,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/generated/translations.g.dart';
|
||||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
@@ -87,25 +89,27 @@ class SyncStatusAndActions extends HookConsumerWidget {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text("reset_sqlite".t(context: context)),
|
title: Text(context.t.reset_sqlite),
|
||||||
content: Text("reset_sqlite_confirmation".t(context: context)),
|
content: Text(context.t.reset_sqlite_confirmation),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(onPressed: () => context.pop(), child: Text(context.t.cancel)),
|
||||||
onPressed: () => context.pop(),
|
|
||||||
child: Text("cancel".t(context: context)),
|
|
||||||
),
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await ref.read(driftProvider).reset();
|
await ref.read(driftProvider).reset();
|
||||||
context.pop();
|
context.pop();
|
||||||
context.scaffoldMessenger.showSnackBar(
|
unawaited(
|
||||||
SnackBar(content: Text("reset_sqlite_success".t(context: context))),
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: Text(context.t.reset_sqlite_success),
|
||||||
|
content: Text(context.t.reset_sqlite_done),
|
||||||
|
actions: [TextButton(onPressed: () => ctx.pop(), child: Text(context.t.ok))],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(context.t.confirm, style: TextStyle(color: context.colorScheme.error)),
|
||||||
"confirm".t(context: context),
|
|
||||||
style: TextStyle(color: context.colorScheme.error),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user