From bfcd9fe60668ceb5a472658616e57fc8b5116671 Mon Sep 17 00:00:00 2001 From: hdbg Date: Fri, 10 Oct 2025 21:31:06 +0200 Subject: [PATCH] feat(bootstrapper): added definitie/indefinite progress and circular indicator --- example/bootstraper.dart | 12 +- lib/src/bootstrapper.dart | 130 ++++++++---- lib/src/bootstrapper.freezed.dart | 338 ++++++++++++++++++++++++------ pubspec.yaml | 1 + 4 files changed, 377 insertions(+), 104 deletions(-) diff --git a/example/bootstraper.dart b/example/bootstraper.dart index 427ee16..af4e01d 100644 --- a/example/bootstraper.dart +++ b/example/bootstraper.dart @@ -8,18 +8,18 @@ class SimpleStage extends StageFactory { final String title = "Test"; @override - Future get isCompleted async { + Future get isAlreadyCompleted async { return false; } @override Future start(StageController controller) async { await Future.delayed(Duration(seconds: 2)); - controller.updateProgress(0.3); + controller.setDefiniteProgress(0.3); controller.updateTitle("test 2"); await Future.delayed(Duration(seconds: 2)); - controller.updateProgress(0.6); + controller.setDefiniteProgress(0.6); } } @@ -28,18 +28,18 @@ class SimpleStage2 extends StageFactory { final String title = "Test 2"; @override - Future get isCompleted async { + Future get isAlreadyCompleted async { return false; } @override Future start(StageController controller) async { await Future.delayed(Duration(seconds: 2)); - controller.updateProgress(0.3); + controller.setDefiniteProgress(0.3); controller.updateTitle("test 5"); await Future.delayed(Duration(seconds: 2)); - controller.updateProgress(0.6); + controller.setDefiniteProgress(0.6); } } diff --git a/lib/src/bootstrapper.dart b/lib/src/bootstrapper.dart index c6760dd..a02ff0d 100644 --- a/lib/src/bootstrapper.dart +++ b/lib/src/bootstrapper.dart @@ -1,35 +1,48 @@ +// 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 isCompleted async => false; + 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 double progress; + final Progress progress; StageState({required this.title, required this.progress}); } class StageController extends Cubit { StageController({required String title}) - : super(StageState(title: title, progress: 0.0)); + : super(StageState(title: title, progress: const Progress.indefinite())); - void updateProgress(double progress) { - emit(state.copyWith(progress: progress)); + void setDefiniteProgress(double value) { + emit(state.copyWith(progress: Progress.definite(value))); + } + + void setIndefiniteProgress() { + emit(state.copyWith(progress: const Progress.indefinite())); } void updateTitle(String title) { @@ -46,20 +59,20 @@ class BootstrapState with _$BootstrapState { final StageController? controller; StageFactory get currentStage => stages[currentStageIndex]; - bool get isCompleted => currentStageIndex >= stages.length; + 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; +sealed class _BootstrapEvent with _$BootstrapEvent { + const factory _BootstrapEvent.start() = StartEvent; + const factory _BootstrapEvent.stageCompleted() = StageCompletedEvent; } -class BootstrapController extends Bloc { +class _BootstrapController extends Bloc<_BootstrapEvent, BootstrapState> { final Completer completer; - BootstrapController(super.initialState, this.completer) { + _BootstrapController(super.initialState, this.completer) { assert(state.stages.isNotEmpty, "Stages list cannot be empty"); on((event, emit) { @@ -77,14 +90,19 @@ class BootstrapController extends Bloc { final nextIndex = state.currentStageIndex + 1; final newState = state.copyWith(currentStageIndex: nextIndex); - if (newState.isCompleted) { + // all stages completed + if (newState.areStagesCompleted) { talker.info("BootstrapController: All stages completed"); completer.complete(); - } else if (await newState.currentStage.isCompleted) { + + // skip already completed stages + } else if (await newState.currentStage.isAlreadyCompleted) { talker.info( "BootstrapController: Stage ${newState.currentStage.title} already completed, skipping", ); - add(const BootstrapEvent.stageCompleted()); + add(const _BootstrapEvent.stageCompleted()); + + // move to next stage } else { final nextStage = newState.currentStage; talker.info("BootstrapController: Starting stage ${nextStage.title}"); @@ -92,9 +110,6 @@ class BootstrapController extends Bloc { emit(newState); } }); - on((event, emit) { - talker.info("BootstrapController: Bootstrap process finished"); - }); } StageController launchCurrentStage(Stages stages, int index) { @@ -104,7 +119,7 @@ class BootstrapController extends Bloc { currentStage .start(controller) .then((_) { - add(BootstrapEvent.stageCompleted()); + add(_BootstrapEvent.stageCompleted()); }) .catchError((error) { talker.handle( @@ -119,27 +134,64 @@ class BootstrapController extends Bloc { } } -class BootstrapFooter extends StatelessWidget { - const BootstrapFooter({super.key}); +class _DefiniteIndicator extends StatelessWidget { + final double progress; // 0.0 to 1.0 + + const _DefiniteIndicator({required this.progress}); @override Widget build(BuildContext context) { - return BlocBuilder( + 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) { - final controller = state.controller; - if (controller == null) { + 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(); } - return Column( - children: [ - Text( - "${state.currentStage.title} ${(controller.state.progress * 100).toStringAsFixed(2)}%", - ), - CircularProgressIndicator.adaptive( - value: controller.state.progress, - ), - ], - ); }, ); } @@ -159,30 +211,30 @@ class Bootstrapper extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) { - final controller = BootstrapController( + final controller = _BootstrapController( BootstrapState(stages), onCompleted, ); - controller.add(const BootstrapEvent.start()); + controller.add(const _BootstrapEvent.start()); return controller; }, - child: BlocListener( + child: BlocListener<_BootstrapController, BootstrapState>( listener: (context, state) { - if (state.isCompleted) { + if (state.areStagesCompleted) { onCompleted.complete(); } }, child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.max, 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: _BootstrapFooter()), + Flexible(flex: 1, child: Container()), ], ), ), diff --git a/lib/src/bootstrapper.freezed.dart b/lib/src/bootstrapper.freezed.dart index 2c65038..fefcae4 100644 --- a/lib/src/bootstrapper.freezed.dart +++ b/lib/src/bootstrapper.freezed.dart @@ -11,10 +11,268 @@ part of 'bootstrapper.dart'; // dart format off T _$identity(T value) => value; +/// @nodoc +mixin _$Progress { + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is Progress); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'Progress()'; +} + + +} + +/// @nodoc +class $ProgressCopyWith<$Res> { +$ProgressCopyWith(Progress _, $Res Function(Progress) __); +} + + +/// Adds pattern-matching-related methods to [Progress]. +extension ProgressPatterns on Progress { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap({TResult Function( PercentageProgress value)? definite,TResult Function( IndefiniteProgress value)? indefinite,required TResult orElse(),}){ +final _that = this; +switch (_that) { +case PercentageProgress() when definite != null: +return definite(_that);case IndefiniteProgress() when indefinite != null: +return indefinite(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map({required TResult Function( PercentageProgress value) definite,required TResult Function( IndefiniteProgress value) indefinite,}){ +final _that = this; +switch (_that) { +case PercentageProgress(): +return definite(_that);case IndefiniteProgress(): +return indefinite(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull({TResult? Function( PercentageProgress value)? definite,TResult? Function( IndefiniteProgress value)? indefinite,}){ +final _that = this; +switch (_that) { +case PercentageProgress() when definite != null: +return definite(_that);case IndefiniteProgress() when indefinite != null: +return indefinite(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen({TResult Function( double value)? definite,TResult Function()? indefinite,required TResult orElse(),}) {final _that = this; +switch (_that) { +case PercentageProgress() when definite != null: +return definite(_that.value);case IndefiniteProgress() when indefinite != null: +return indefinite();case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when({required TResult Function( double value) definite,required TResult Function() indefinite,}) {final _that = this; +switch (_that) { +case PercentageProgress(): +return definite(_that.value);case IndefiniteProgress(): +return indefinite();} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull({TResult? Function( double value)? definite,TResult? Function()? indefinite,}) {final _that = this; +switch (_that) { +case PercentageProgress() when definite != null: +return definite(_that.value);case IndefiniteProgress() when indefinite != null: +return indefinite();case _: + return null; + +} +} + +} + +/// @nodoc + + +class PercentageProgress implements Progress { + const PercentageProgress(this.value); + + + final double value; + +/// Create a copy of Progress +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$PercentageProgressCopyWith get copyWith => _$PercentageProgressCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is PercentageProgress&&(identical(other.value, value) || other.value == value)); +} + + +@override +int get hashCode => Object.hash(runtimeType,value); + +@override +String toString() { + return 'Progress.definite(value: $value)'; +} + + +} + +/// @nodoc +abstract mixin class $PercentageProgressCopyWith<$Res> implements $ProgressCopyWith<$Res> { + factory $PercentageProgressCopyWith(PercentageProgress value, $Res Function(PercentageProgress) _then) = _$PercentageProgressCopyWithImpl; +@useResult +$Res call({ + double value +}); + + + + +} +/// @nodoc +class _$PercentageProgressCopyWithImpl<$Res> + implements $PercentageProgressCopyWith<$Res> { + _$PercentageProgressCopyWithImpl(this._self, this._then); + + final PercentageProgress _self; + final $Res Function(PercentageProgress) _then; + +/// Create a copy of Progress +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? value = null,}) { + return _then(PercentageProgress( +null == value ? _self.value : value // ignore: cast_nullable_to_non_nullable +as double, + )); +} + + +} + +/// @nodoc + + +class IndefiniteProgress implements Progress { + const IndefiniteProgress(); + + + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is IndefiniteProgress); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'Progress.indefinite()'; +} + + +} + + + + /// @nodoc mixin _$StageState { - String get title; double get progress; + String get title; Progress get progress; /// Create a copy of StageState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -45,7 +303,7 @@ abstract mixin class $StageStateCopyWith<$Res> { factory $StageStateCopyWith(StageState value, $Res Function(StageState) _then) = _$StageStateCopyWithImpl; @useResult $Res call({ - String title, double progress + String title, Progress progress }); @@ -66,7 +324,7 @@ class _$StageStateCopyWithImpl<$Res> return _then(StageState( title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable as String,progress: null == progress ? _self.progress : progress // ignore: cast_nullable_to_non_nullable -as double, +as Progress, )); } @@ -393,7 +651,7 @@ mixin _$BootstrapEvent { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is BootstrapEvent); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _BootstrapEvent); } @@ -410,12 +668,12 @@ String toString() { /// @nodoc class $BootstrapEventCopyWith<$Res> { -$BootstrapEventCopyWith(BootstrapEvent _, $Res Function(BootstrapEvent) __); +$BootstrapEventCopyWith(_BootstrapEvent _, $Res Function(_BootstrapEvent) __); } -/// Adds pattern-matching-related methods to [BootstrapEvent]. -extension BootstrapEventPatterns on BootstrapEvent { +/// Adds pattern-matching-related methods to [_BootstrapEvent]. +extension BootstrapEventPatterns on _BootstrapEvent { /// A variant of `map` that fallback to returning `orElse`. /// /// It is equivalent to doing: @@ -428,13 +686,12 @@ extension BootstrapEventPatterns on BootstrapEvent { /// } /// ``` -@optionalTypeArgs TResult maybeMap({TResult Function( StartEvent value)? start,TResult Function( StageCompletedEvent value)? stageCompleted,TResult Function( FinishedEvent value)? finished,required TResult orElse(),}){ +@optionalTypeArgs TResult maybeMap({TResult Function( StartEvent value)? start,TResult Function( StageCompletedEvent value)? stageCompleted,required TResult orElse(),}){ final _that = this; switch (_that) { case StartEvent() when start != null: return start(_that);case StageCompletedEvent() when stageCompleted != null: -return stageCompleted(_that);case FinishedEvent() when finished != null: -return finished(_that);case _: +return stageCompleted(_that);case _: return orElse(); } @@ -452,13 +709,12 @@ return finished(_that);case _: /// } /// ``` -@optionalTypeArgs TResult map({required TResult Function( StartEvent value) start,required TResult Function( StageCompletedEvent value) stageCompleted,required TResult Function( FinishedEvent value) finished,}){ +@optionalTypeArgs TResult map({required TResult Function( StartEvent value) start,required TResult Function( StageCompletedEvent value) stageCompleted,}){ final _that = this; switch (_that) { case StartEvent(): return start(_that);case StageCompletedEvent(): -return stageCompleted(_that);case FinishedEvent(): -return finished(_that);} +return stageCompleted(_that);} } /// A variant of `map` that fallback to returning `null`. /// @@ -472,13 +728,12 @@ return finished(_that);} /// } /// ``` -@optionalTypeArgs TResult? mapOrNull({TResult? Function( StartEvent value)? start,TResult? Function( StageCompletedEvent value)? stageCompleted,TResult? Function( FinishedEvent value)? finished,}){ +@optionalTypeArgs TResult? mapOrNull({TResult? Function( StartEvent value)? start,TResult? Function( StageCompletedEvent value)? stageCompleted,}){ final _that = this; switch (_that) { case StartEvent() when start != null: return start(_that);case StageCompletedEvent() when stageCompleted != null: -return stageCompleted(_that);case FinishedEvent() when finished != null: -return finished(_that);case _: +return stageCompleted(_that);case _: return null; } @@ -495,12 +750,11 @@ return finished(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen({TResult Function()? start,TResult Function()? stageCompleted,TResult Function()? finished,required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen({TResult Function()? start,TResult Function()? stageCompleted,required TResult orElse(),}) {final _that = this; switch (_that) { case StartEvent() when start != null: return start();case StageCompletedEvent() when stageCompleted != null: -return stageCompleted();case FinishedEvent() when finished != null: -return finished();case _: +return stageCompleted();case _: return orElse(); } @@ -518,12 +772,11 @@ return finished();case _: /// } /// ``` -@optionalTypeArgs TResult when({required TResult Function() start,required TResult Function() stageCompleted,required TResult Function() finished,}) {final _that = this; +@optionalTypeArgs TResult when({required TResult Function() start,required TResult Function() stageCompleted,}) {final _that = this; switch (_that) { case StartEvent(): return start();case StageCompletedEvent(): -return stageCompleted();case FinishedEvent(): -return finished();} +return stageCompleted();} } /// A variant of `when` that fallback to returning `null` /// @@ -537,12 +790,11 @@ return finished();} /// } /// ``` -@optionalTypeArgs TResult? whenOrNull({TResult? Function()? start,TResult? Function()? stageCompleted,TResult? Function()? finished,}) {final _that = this; +@optionalTypeArgs TResult? whenOrNull({TResult? Function()? start,TResult? Function()? stageCompleted,}) {final _that = this; switch (_that) { case StartEvent() when start != null: return start();case StageCompletedEvent() when stageCompleted != null: -return stageCompleted();case FinishedEvent() when finished != null: -return finished();case _: +return stageCompleted();case _: return null; } @@ -553,7 +805,7 @@ return finished();case _: /// @nodoc -class StartEvent implements BootstrapEvent { +class StartEvent implements _BootstrapEvent { const StartEvent(); @@ -585,7 +837,7 @@ String toString() { /// @nodoc -class StageCompletedEvent implements BootstrapEvent { +class StageCompletedEvent implements _BootstrapEvent { const StageCompletedEvent(); @@ -614,36 +866,4 @@ String toString() { -/// @nodoc - - -class FinishedEvent implements BootstrapEvent { - const FinishedEvent(); - - - - - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is FinishedEvent); -} - - -@override -int get hashCode => runtimeType.hashCode; - -@override -String toString() { - return 'BootstrapEvent.finished()'; -} - - -} - - - - // dart format on diff --git a/pubspec.yaml b/pubspec.yaml index e22e80d..ad71793 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: flutter_hooks: ^0.21.3+1 freezed_annotation: ^3.1.0 hooks_riverpod: ^3.0.2 + percent_indicator: ^4.2.5 rive: ^0.14.0-dev.9 rive_native: ^0.0.12 talker_flutter: ^5.0.2