import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'app.dart'; import 'data/species_repository.dart'; import 'data/variety_repository.dart'; import 'di/injector.dart'; import 'i18n/strings.g.dart'; import 'services/auto_backup_service.dart'; import 'services/coarse_location.dart'; import 'services/inbox_service.dart'; import 'services/locale_store.dart'; import 'services/message_store.dart'; import 'services/notification_service.dart'; import 'services/offer_outbox.dart'; import 'services/onboarding_store.dart'; import 'services/profile_cache.dart'; import 'services/profile_store.dart'; import 'services/social_account_store.dart'; import 'services/social_connection.dart'; import 'services/social_service.dart'; import 'services/social_settings.dart'; import 'services/trust_referents.dart'; import 'services/wot_settings.dart'; import 'ui/theme.dart'; /// Boots the app WITHOUT blocking the first frame: paints a splash immediately, /// then wires the encrypted DB + services and swaps in [TaneApp] once ready. /// /// Before this, `main` awaited the whole of `configureDependencies()` (open the /// encrypted DB, derive the social key, seed/scan the species catalog…) before /// the first `runApp`, so nothing painted until init finished — the startup jank /// visible as skipped frames. Now `main` runs `runApp` immediately with this /// widget; the heavy work runs while the splash is already on screen. class Bootstrap extends StatefulWidget { const Bootstrap({super.key}); @override State createState() => _BootstrapState(); } class _BootstrapState extends State { late Future _app = _boot(); Future _boot() async { await configureDependencies(); // The saved language lives in the keystore, so it can only be applied once // DI is up. Until now the splash showed in the device language (it has no // text), so there is no visible flip. final savedLocale = await getIt().saved(); if (savedLocale != null) LocaleSettings.setLocaleSync(savedLocale); final onboarding = getIt(); final connection = getIt.isRegistered() ? getIt() : null; final inbox = getIt.isRegistered() ? getIt() : null; final notifications = getIt.isRegistered() ? getIt() : null; // Ask for notification permission and set up the OS channel (no-op on // unsupported platforms). Done before the inbox starts listening. if (notifications != null) await notifications.initialize(); // Subscribe the inbox listener BEFORE the shared connection starts // connecting, so the first session is caught; then bring the connection up. inbox?.start(); connection?.start(); return TaneApp( repository: getIt(), species: getIt(), onboarding: onboarding, social: getIt.isRegistered() ? getIt() : null, socialSettings: getIt(), connection: connection, location: const GeolocatorCoarseLocation(), outbox: getIt(), messageStore: getIt(), profileStore: getIt(), profileCache: getIt(), socialAccounts: getIt(), trustReferents: getIt(), wotSettings: getIt(), inbox: inbox, notifications: notifications, showIntro: !await onboarding.introSeen(), autoBackup: getIt.isRegistered() ? getIt() : null, ); } // `configureDependencies` is idempotent (and self-heals a half-wired // container), so retrying after a failed boot is safe. void _retry() => setState(() => _app = _boot()); @override Widget build(BuildContext context) { return FutureBuilder( future: _app, builder: (context, snapshot) { if (snapshot.hasData) return snapshot.data!; return BootstrapSplash( error: snapshot.hasError, onRetry: snapshot.hasError ? _retry : null, ); }, ); } } /// The pre-init frame. A minimal [MaterialApp] so its text has a /// `Directionality`, theme and localizations. Its background matches the native /// splash colour ([seedGreen]) for a seamless handoff — one continuous green /// screen from launch until the app is ready. Shows a spinner while booting and, /// on [error], the failure message with a [onRetry] action. class BootstrapSplash extends StatelessWidget { const BootstrapSplash({required this.error, this.onRetry, super.key}); final bool error; final VoidCallback? onRetry; @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, theme: buildTaneTheme(), // Asturian (`ast`) has no bundled Material/Cupertino localizations, so map // it to Spanish for the framework chrome — same as the main app (app.dart). locale: materialLocaleFor(TranslationProvider.of(context).flutterLocale), supportedLocales: AppLocaleUtils.supportedLocales, localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], home: Scaffold( backgroundColor: seedGreen, body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Image.asset('assets/logo.png', width: 120, height: 120), const SizedBox(height: 32), if (error) ...[ Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Text( context.t.bootstrap.failed, textAlign: TextAlign.center, style: const TextStyle(color: Colors.white, fontSize: 16), ), ), const SizedBox(height: 16), if (onRetry != null) FilledButton( onPressed: onRetry, style: FilledButton.styleFrom( backgroundColor: Colors.white, foregroundColor: seedGreen, ), child: Text(context.t.bootstrap.retry), ), ] else const SizedBox( width: 28, height: 28, child: CircularProgressIndicator( strokeWidth: 3, valueColor: AlwaysStoppedAnimation(Colors.white), ), ), ], ), ), ), ); } }