// ignore_for_file: annotate_overrides import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:markettakers/markettakers.dart'; import 'package:markettakers/src/loaders/loader.dart'; import 'package:percent_indicator/circular_percent_indicator.dart'; part 'bootstrapper.freezed.dart'; abstract class StageFactory { String get title; Future get isAlreadyCompleted async => false; Future start(StageController controller); void dispose() {} } @freezed sealed class Progress with _$Progress { const factory Progress.definite(double value) = PercentageProgress; const factory Progress.indefinite() = IndefiniteProgress; } @freezed class StageState with _$StageState { final String title; final Progress progress; StageState({required this.title, required this.progress}); } class StageController extends Cubit { StageController({required String title}) : super(StageState(title: title, progress: const Progress.indefinite())); void setDefiniteProgress(double value) { emit(state.copyWith(progress: Progress.definite(value))); } void setIndefiniteProgress() { emit(state.copyWith(progress: const Progress.indefinite())); } void updateTitle(String title) { emit(state.copyWith(title: title)); } } typedef Stages = List; @freezed class BootstrapState with _$BootstrapState { final Stages stages; final int currentStageIndex; final StageController? controller; StageFactory get currentStage => stages[currentStageIndex]; bool get areStagesCompleted => currentStageIndex >= stages.length; BootstrapState(this.stages, {this.currentStageIndex = 0, this.controller}); } @freezed sealed class _BootstrapEvent with _$BootstrapEvent { const factory _BootstrapEvent.start() = StartEvent; const factory _BootstrapEvent.stageCompleted() = StageCompletedEvent; } class _BootstrapController extends Bloc<_BootstrapEvent, BootstrapState> { final Completer completer; _BootstrapController(super.initialState, this.completer) { assert(state.stages.isNotEmpty, "Stages list cannot be empty"); on((event, emit) { talker.info("BootstrapController: Starting bootstrap process"); final controller = launchCurrentStage( state.stages, state.currentStageIndex, ); emit(state.copyWith(controller: controller)); }); on((event, emit) async { talker.info("BootstrapController: ${state.currentStage.title} completed"); state.currentStage.dispose(); final nextIndex = state.currentStageIndex + 1; final newState = state.copyWith(currentStageIndex: nextIndex); // all stages completed if (newState.areStagesCompleted) { talker.info("BootstrapController: All stages completed"); completer.complete(); // skip already completed stages } else if (await newState.currentStage.isAlreadyCompleted) { talker.info( "BootstrapController: Stage ${newState.currentStage.title} already completed, skipping", ); add(const _BootstrapEvent.stageCompleted()); // move to next stage } else { final nextStage = newState.currentStage; talker.info("BootstrapController: Starting stage ${nextStage.title}"); launchCurrentStage(state.stages, nextIndex); emit(newState); } }); } StageController launchCurrentStage(Stages stages, int index) { final currentStage = stages[index]; final controller = StageController(title: currentStage.title); currentStage .start(controller) .then((_) { add(_BootstrapEvent.stageCompleted()); }) .catchError((error) { talker.handle( error, null, "BootstrapController: Error in ${currentStage.title}", ); addError(error); }); return controller; } } class _DefiniteIndicator extends StatelessWidget { final double progress; // 0.0 to 1.0 const _DefiniteIndicator({required this.progress}); @override Widget build(BuildContext context) { final formattedProgress = "${(progress * 100).toStringAsFixed(0)}%"; return CircularPercentIndicator( radius: 40.0, lineWidth: 5.0, percent: progress.clamp(0.0, 1.0), center: Text(formattedProgress), progressColor: Theme.of(context).colorScheme.secondary, ); } } class _BootstrapFooter extends StatelessWidget { const _BootstrapFooter(); @override Widget build(BuildContext context) { return BlocBuilder<_BootstrapController, BootstrapState>( builder: (context, state) { if (state.areStagesCompleted) { return Text( "All stages completed", style: Theme.of(context).textTheme.titleMedium, ); } else if (state.controller != null) { return BlocBuilder( bloc: state.controller, builder: (context, state) { final progressIndicator = state.progress.when( definite: (definite) => _DefiniteIndicator(progress: definite), indefinite: () => const CircularProgressIndicator(), ); return Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.max, children: [ Flexible( flex: 1, child: Text( state.title, style: Theme.of(context).textTheme.titleLarge, ), ), Flexible(flex: 1, child: progressIndicator), ], ); }, ); } else { return const SizedBox.shrink(); } }, ); } } class Bootstrapper extends StatelessWidget { final Stages stages; final Completer onCompleted; const Bootstrapper({ super.key, required this.stages, required this.onCompleted, }); @override Widget build(BuildContext context) { return BlocProvider( create: (context) { final controller = _BootstrapController( BootstrapState(stages), onCompleted, ); controller.add(const _BootstrapEvent.start()); return controller; }, child: BlocListener<_BootstrapController, BootstrapState>( listener: (context, state) { if (state.areStagesCompleted) { onCompleted.complete(); } }, child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.center, children: [ Flexible( flex: 2, child: Center(child: Loader.playing(flavour: LoaderFlavour.big)), ), Flexible(flex: 1, child: Container()), Flexible(flex: 1, child: _BootstrapFooter()), Flexible(flex: 1, child: Container()), ], ), ), ); } }