// ignore_for_file: annotate_overrides import 'dart:async'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:mtcore/markettakers.dart'; import 'package:percent_indicator/circular_percent_indicator.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'bootstrapper.g.dart'; part 'bootstrapper.freezed.dart'; /// A single bootstrap stage. /// Implementations should report progress/title updates via [StageController]. abstract class StageFactory { String get title; /// Optional: if your stage can be skipped when already completed. Future get isAlreadyCompleted async => false; Future start(StageController controller); void dispose() {} } /// Bootstrap progress: either definite (0..1) or indefinite. @freezed sealed class Progress with _$Progress { const factory Progress.definite(double value) = PercentageProgress; const factory Progress.indefinite() = IndefiniteProgress; } @freezed class StageState with _$StageState { String title; Progress progress; StageState({required this.title, required this.progress}); } typedef Stages = List; @riverpod class CurrentStage extends _$CurrentStage { @override StageState? build() => null; void set(StageState newState) => state = newState; void setProgress(Progress progress) { final s = state; if (s == null) return; state = s.copyWith(progress: progress); } void setTitle(String title) { final s = state; if (s == null) return; state = s.copyWith(title: title); } } class StageController { final Ref _ref; StageController(this._ref); void setDefiniteProgress(double value) { if (!_ref.mounted) return; _ref .read(currentStageProvider.notifier) .setProgress(Progress.definite(value)); } void setIndefiniteProgress() { if (!_ref.mounted) return; _ref .read(currentStageProvider.notifier) .setProgress(const Progress.indefinite()); } void updateTitle(String title) { if (!_ref.mounted) return; _ref.read(currentStageProvider.notifier).setTitle(title); } } @riverpod class StagesWorker extends _$StagesWorker { bool _hasStarted = false; @override Future build() async => false; Future start(Stages stages) async { if (_hasStarted) return; _hasStarted = true; state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { for (final stage in stages) { talker.info("${stage.title} starting"); // Optional skip. final alreadyDone = await stage.isAlreadyCompleted; if (alreadyDone) { talker.info("${stage.title} skipped (already completed)"); continue; } ref .read(currentStageProvider.notifier) .set( StageState( title: stage.title, progress: const Progress.indefinite(), ), ); await stage.start(StageController(ref)); talker.info("${stage.title} completed"); } return true; }); } } class _DefiniteIndicator extends StatelessWidget { final double progress; // 0.0 to 1.0 const _DefiniteIndicator({required this.progress}); @override Widget build(BuildContext context) { // Make it adaptive and square to prevent overflow in flexible layouts. return AspectRatio( aspectRatio: 1, child: LayoutBuilder( builder: (context, c) { final side = c.maxWidth.isFinite ? c.maxWidth : 120.0; final radius = side * 0.42; // padding around ring final stroke = (side * 0.08).clamp(4.0, 10.0); final value = progress.clamp(0.0, 1.0); final formattedProgress = "${(value * 100).toStringAsFixed(0)}%"; return Center( child: CircularPercentIndicator( radius: radius, lineWidth: stroke, percent: value, center: Text( formattedProgress, style: Theme.of( context, ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), ), circularStrokeCap: CircularStrokeCap.round, backgroundColor: Theme.of( context, ).colorScheme.onSurface.withValues(alpha: 0.12), progressColor: Theme.of(context).colorScheme.secondary, ), ); }, ), ); } } class _IndefiniteIndicator extends StatelessWidget { const _IndefiniteIndicator(); @override Widget build(BuildContext context) { return AspectRatio( aspectRatio: 1, child: LayoutBuilder( builder: (context, c) { final side = c.maxWidth.isFinite ? c.maxWidth : 120.0; final size = side * 0.55; return Center( child: SizedBox( width: size, height: size, child: SpinKitRing( color: Theme.of(context).colorScheme.secondary, lineWidth: (side * 0.06).clamp(3.0, 8.0), ), ), ); }, ), ); } } class _FrostCard extends StatelessWidget { const _FrostCard({required this.child}); final Widget child; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return ClipRRect( borderRadius: BorderRadius.circular(22), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 14, sigmaY: 14), child: DecoratedBox( decoration: BoxDecoration( color: cs.surface.withValues(alpha: 0.10), borderRadius: BorderRadius.circular(22), border: Border.all( color: cs.onSurface.withValues(alpha: 0.10), width: 1, ), ), child: child, ), ), ); } } class _BackgroundGlow extends StatelessWidget { const _BackgroundGlow(); @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return DecoratedBox( decoration: BoxDecoration( gradient: RadialGradient( center: const Alignment(0, -0.55), radius: 1.25, colors: [ cs.primary.withValues(alpha: 0.18), cs.secondary.withValues(alpha: 0.10), Colors.black, ], stops: const [0.0, 0.45, 1.0], ), ), ); } } class _BootstrapFooter extends ConsumerWidget { const _BootstrapFooter(); @override Widget build(BuildContext context, WidgetRef ref) { final stage = ref.watch(currentStageProvider); final title = stage?.title ?? "Preparing…"; final progress = stage?.progress ?? const Progress.indefinite(); final indicator = progress.when( definite: (definite) => _DefiniteIndicator(progress: definite), indefinite: () => const _IndefiniteIndicator(), ); return ConstrainedBox( constraints: const BoxConstraints(maxWidth: 520), child: _FrostCard( child: Padding( padding: const EdgeInsets.all(22), child: Column( mainAxisSize: MainAxisSize.min, children: [ AnimatedSwitcher( duration: const Duration(milliseconds: 250), child: Text( title, key: ValueKey(title), textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w700, letterSpacing: 0.2, ), ), ), const SizedBox(height: 12), Text( "Please don’t close the app", style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of( context, ).colorScheme.onSurface.withValues(alpha: 0.72), ), ), const SizedBox(height: 18), SizedBox(height: 180, child: indicator), ], ), ), ), ); } } /// Bootstrapper UI + lifecycle glue. /// /// - Runs [StagesWorker.start] exactly once (using hooks). /// - Uses `ref.listen` to complete [onCompleted] without rebuilding the logo. /// - Watches only [CurrentStage] for UI updates. class Bootstrapper extends HookConsumerWidget { final Stages stages; final Completer onCompleted; const Bootstrapper({ super.key, required this.stages, required this.onCompleted, }); @override Widget build(BuildContext context, WidgetRef ref) { useEffect(() { Future.microtask( () => ref.read(stagesWorkerProvider.notifier).start(stages), ); return null; }, const []); ref.listen>(stagesWorkerProvider, (_, next) { if (onCompleted.isCompleted) return; next.whenOrNull( data: (ok) { if (ok) onCompleted.complete(); }, error: (e, st) => onCompleted.completeError(e, st), ); }); return Stack( children: [ const _BackgroundGlow(), SafeArea( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 24), child: Column( children: const [ // Keep this as a const child so it doesn't restart animations // when the footer rebuilds. _LogoHeader(), SizedBox(height: 24), Expanded(child: Center(child: _BootstrapFooter())), SizedBox(height: 16), _BottomCaption(), ], ), ), ), ], ); } } class _LogoHeader extends StatelessWidget { const _LogoHeader(); @override Widget build(BuildContext context) { return SizedBox( height: 160, child: Center(child: Loader.playing(flavour: LoaderFlavour.big)), ); } } class _BottomCaption extends StatelessWidget { const _BottomCaption(); @override Widget build(BuildContext context) { return Text( "Initializing secure components…", style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.55), letterSpacing: 0.2, ), ); } }