import 'dart:collection'; import 'package:aitrainer_app/bloc/training_plan/training_plan_bloc.dart'; import 'package:aitrainer_app/library/custom_icon_icons.dart'; import 'package:aitrainer_app/model/customer_training_plan_details.dart'; import 'package:aitrainer_app/model/exercise_plan_detail.dart'; import 'package:aitrainer_app/util/app_localization.dart'; import 'package:aitrainer_app/util/trans.dart'; import 'package:aitrainer_app/widgets/app_bar.dart'; import 'package:aitrainer_app/widgets/dialog_common.dart'; import 'package:aitrainer_app/widgets/menu_image.dart'; import 'package:extended_tabs/extended_tabs.dart'; import 'package:ezanimation/ezanimation.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:modal_progress_hud_nsn/modal_progress_hud_nsn.dart'; import 'package:timeline_tile/timeline_tile.dart'; // ignore: must_be_immutable class TrainingPlanExecutePage extends StatefulWidget { @override _TrainingPlanExecutePageState createState() => _TrainingPlanExecutePageState(); } class _TrainingPlanExecutePageState extends State with Trans { final scrollController = ScrollController(); TrainingPlanBloc? bloc; @override Widget build(BuildContext context) { bloc = BlocProvider.of(context); bloc!.activateDays(); setContext(context); return Scaffold( appBar: AppBarNav(depth: 0), body: Container( padding: EdgeInsets.all(20), decoration: BoxDecoration( image: DecorationImage( image: AssetImage('asset/image/WT_black_background.jpg'), fit: BoxFit.cover, alignment: Alignment.center, ), ), child: BlocConsumer(listener: (context, state) { if (state is TrainingPlanError) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(backgroundColor: Colors.orange, content: Text(state.message, style: TextStyle(color: Colors.white)))); } else if (state is TrainingPlanFinished) {} }, builder: (context, state) { return ModalProgressHUD( child: ExerciseTabs(bloc: bloc!), inAsyncCall: state is TrainingPlanLoading, opacity: 0.5, color: Colors.black54, progressIndicator: CircularProgressIndicator(), ); }), ), floatingActionButton: FloatingActionButton.extended( onPressed: () => bloc!.getNext() != null ? _ExerciseListState.executeExercise(bloc!, bloc!.getNext()!, context) : Navigator.of(context).pushNamed('home'), backgroundColor: Colors.orange[800], icon: Icon(CustomIcon.weight_hanging), label: Text( t("Training!"), style: GoogleFonts.inter(fontWeight: FontWeight.bold, fontSize: 16), ), ), ); } } class ExerciseTabs extends StatefulWidget { final TrainingPlanBloc bloc; ExerciseTabs({required this.bloc}); @override _ExerciseTabs createState() => _ExerciseTabs(); } class _ExerciseTabs extends State with TickerProviderStateMixin { late TabController tabController; @override void initState() { super.initState(); tabController = TabController(length: widget.bloc.dayNames.length, vsync: this); tabController.animateTo(0, duration: Duration(milliseconds: 300)); } @override void dispose() { tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return getTabs(widget.bloc); } Widget getTabs(TrainingPlanBloc bloc) { return Column(children: [ ExtendedTabBar( tabs: getTabNames(), controller: tabController, ), Expanded( child: ExtendedTabBarView( children: getExerciseLists(), controller: tabController, /// if link is true and current tabbarview over scroll, /// it will check and scroll ancestor or child tabbarView. link: true, /// cache page count /// default is 0. /// if cacheExtent is 1, it has two pages in cache /// null is infinity, it will cache all pages cacheExtent: 0, )), ]); } List getTabNames() { List tabs = []; widget.bloc.dayNames.forEach((element) { final Widget widget = RichText( text: TextSpan( style: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.white, ), children: [ TextSpan( text: AppLocalizations.of(context)!.translate("Training Day") + ": \n", style: GoogleFonts.inter( fontSize: 14, color: Colors.white, shadows: [ Shadow( offset: Offset(5.0, 5.0), blurRadius: 12.0, color: Colors.black54, ), Shadow( offset: Offset(-3.0, 3.0), blurRadius: 12.0, color: Colors.black54, ), ], )), TextSpan( text: element, style: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.yellow[400], shadows: [ Shadow( offset: Offset(5.0, 5.0), blurRadius: 12.0, color: Colors.black54, ), Shadow( offset: Offset(-3.0, 3.0), blurRadius: 12.0, color: Colors.black54, ), ], )), ])); tabs.add(Tab(child: widget)); }); return tabs; } List getExerciseLists() { List list = []; widget.bloc.dayNames.forEach((element) { list.add(ExerciseList(bloc: widget.bloc, dayName: element)); }); return list; } } class ExerciseList extends StatefulWidget { final TrainingPlanBloc bloc; final String dayName; ExerciseList({required this.bloc, required this.dayName}); @override _ExerciseListState createState() => _ExerciseListState(); } class _ExerciseListState extends State with Trans { final scrollController = ScrollController(); double offset = 5; @override void initState() { WidgetsBinding.instance!.addPostFrameCallback((_) { animate(); }); super.initState(); } @override void didUpdateWidget(ExerciseList page) { super.didUpdateWidget(page); WidgetsBinding.instance!.addPostFrameCallback((_) { animate(); }); } void animate() { offset = widget.bloc.getOffset(); if (scrollController.hasClients) { scrollController.animateTo(offset, duration: Duration(milliseconds: 300), curve: Curves.easeIn); } } @override void dispose() { scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { setContext(context); return CustomScrollView(controller: scrollController, slivers: [ SliverList(delegate: SliverChildListDelegate(getTiles(widget.bloc))), ]); } List getTiles(TrainingPlanBloc bloc) { List tiles = []; tiles.add(getStartTile(bloc)); tiles.addAll(getExerciseTiles(bloc, context)); if (bloc.getMyPlan() != null) tiles.add(getEndTile()); return tiles; } Widget getStartTile(TrainingPlanBloc bloc) { String startText = ""; String explainingText = ""; if (null == bloc.getMyPlan()) { startText = t("No Active Training Plan"); explainingText = t("Please select one in the Training menu, or create your custom plan"); } else { startText = bloc.isStarted() ? t("Continue your training") : t("Start your training"); explainingText = bloc.getMyPlan()!.name != null ? bloc.getMyPlan()!.name! : ""; print(" *** Plan NAME ${bloc.getMyPlan()!.name}"); } return TimelineTile( alignment: TimelineAlign.manual, lineXY: 0.1, isFirst: true, afterLineStyle: const LineStyle( color: Colors.orange, thickness: 6, ), indicatorStyle: IndicatorStyle( width: 40, color: Colors.orange, padding: const EdgeInsets.all(8), iconStyle: IconStyle( color: Colors.white, iconData: Icons.emoji_flags_rounded, ), ), endChild: Container( padding: EdgeInsets.only(top: 30), constraints: const BoxConstraints( minHeight: 120, ), color: Colors.transparent, child: RichText( text: TextSpan( style: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.white, ), children: [ TextSpan( text: startText, style: GoogleFonts.inter( fontSize: 20, fontWeight: FontWeight.bold, color: Colors.yellow[400], shadows: [ Shadow( offset: Offset(5.0, 5.0), blurRadius: 12.0, color: Colors.black54, ), Shadow( offset: Offset(-3.0, 3.0), blurRadius: 12.0, color: Colors.black54, ), ], )), TextSpan( text: "\n", style: GoogleFonts.inter( fontSize: 16, color: Colors.white, )), TextSpan( text: explainingText, style: GoogleFonts.inter( fontSize: 16, color: Colors.white, )), ])), ), ); } Widget getEndTile() { return Container( color: Colors.transparent, child: TimelineTile( alignment: TimelineAlign.manual, lineXY: 0.1, isLast: true, beforeLineStyle: const LineStyle( color: Colors.orange, thickness: 6, ), indicatorStyle: IndicatorStyle( width: 40, color: Colors.orange, padding: const EdgeInsets.all(8), iconStyle: IconStyle( color: Colors.white, iconData: Icons.thumb_up, ), ), endChild: Container( padding: EdgeInsets.only(top: 50), constraints: const BoxConstraints( minHeight: 120, ), color: Colors.transparent, child: RichText( text: TextSpan( style: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.white, ), children: [ TextSpan( text: "Finish!", style: GoogleFonts.inter( fontSize: 20, fontWeight: FontWeight.bold, color: Colors.yellow[400], shadows: [ Shadow( offset: Offset(5.0, 5.0), blurRadius: 12.0, color: Colors.black54, ), Shadow( offset: Offset(-3.0, 3.0), blurRadius: 12.0, color: Colors.black54, ), ], )), ])), ), ), ); } List getExerciseTiles(TrainingPlanBloc bloc, BuildContext context) { List tiles = []; if (bloc.getMyPlan() != null && bloc.getMyPlan()!.details.isNotEmpty && bloc.getMyPlan()!.days[widget.dayName] != null && bloc.getMyPlan()!.days[widget.dayName]!.isNotEmpty) { bloc.getMyPlan()!.days[widget.dayName]!.forEach((element) { //bloc.getMyPlan()!.details.forEach((element) { tiles.add(GestureDetector( onTap: () => bloc.getNext() != null ? executeExercise(bloc, bloc.getNext()!, context) : Navigator.of(context).pushNamed('home'), child: ExerciseTile( bloc: bloc, detail: element, ))); }); } return tiles; } static void executeExercise(TrainingPlanBloc bloc, CustomerTrainingPlanDetails detail, BuildContext context) { CustomerTrainingPlanDetails? next = bloc.getNext(); if (next != null) { String title = ""; String description = ""; String description2 = ""; if (next.exerciseTypeId != detail.exerciseTypeId) { title = AppLocalizations.of(context)!.translate("Stop!"); description = AppLocalizations.of(context)!.translate("Please continue with the next exercise in the queue:") + next.exerciseType!.nameTranslation; description2 = AppLocalizations.of(context)!.translate("Or, you can redifine this exercise queue in the Compact Test menu"); } else { final HashMap args = HashMap(); args['exerciseType'] = next.exerciseType; args['customerTrainingPlanDetails'] = detail; Navigator.of(context).pushNamed('myTrainingPlanExercise', arguments: args); return; } showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return DialogCommon( title: title, descriptions: description, description2: description2, text: "OK", onTap: () => {Navigator.of(context).pop()}, onCancel: () => {Navigator.of(context).pop()}, ); }); } else { Navigator.of(context).pushNamed('home'); } } } class ExerciseTile extends StatefulWidget { final TrainingPlanBloc bloc; final CustomerTrainingPlanDetails detail; ExerciseTile({required this.bloc, required this.detail}); @override _ExerciseTileState createState() => _ExerciseTileState(); } class _ExerciseTileState extends State with Trans { final EzAnimation animation = EzAnimation(1.0, 30.0, Duration(seconds: 3), reverseCurve: Curves.easeIn); @override void initState() { animation.start(); animation.addStatusListener((status) { if (status == AnimationStatus.completed) {} }); super.initState(); } @override bool didUpdateWidget(ExerciseTile oldWidget) { super.didUpdateWidget(oldWidget); Future.delayed(Duration(milliseconds: 400)).then((value) => animation.start()); return true; } Widget getIndicator(ExercisePlanDetailState state) { CustomerTrainingPlanDetails? next = widget.bloc.getNext(); bool actual = false; if (next != null) { if (next.exerciseTypeId == widget.detail.exerciseTypeId) { actual = true; } } if (state.equalsTo(ExercisePlanDetailState.inProgress)) { return ClipRRect( borderRadius: BorderRadius.circular(24.0), child: Container( color: actual ? Colors.green : Colors.orange, child: Icon( CustomIcon.calendar_2, size: 28, color: Colors.white, ))); } else if (state.equalsTo(ExercisePlanDetailState.finished)) { return ClipRRect( borderRadius: BorderRadius.circular(24.0), child: Container( color: Colors.white, child: Icon( CustomIcon.ok_circled, size: 40, color: Colors.green, ))); } else if (state.equalsTo(ExercisePlanDetailState.skipped)) { return ClipRRect( borderRadius: BorderRadius.circular(24.0), child: Container( color: Colors.white, child: Icon( CustomIcon.stop_1, size: 40, color: Colors.grey, ))); } else { return Image.asset( "asset/image/pict_reps_volumen_db.png", ); } } @override Widget build(BuildContext context) { setContext(context); final ExercisePlanDetailState state = widget.detail.state; final bool done = state.equalsTo(ExercisePlanDetailState.finished) || state.equalsTo(ExercisePlanDetailState.skipped); final String countSerie = widget.detail.set.toString(); final String step = (widget.detail.exercises.length).toString(); String weight = widget.detail.weight!.toStringAsFixed(1); String restingTime = widget.detail.restingTime == null ? "" : widget.detail.restingTime!.toStringAsFixed(0); bool isTest = false; if (widget.detail.weight! == -1) { weight = t("TEST"); isTest = true; } setContext(context); return Container( color: Colors.transparent, child: TimelineTile( alignment: TimelineAlign.manual, lineXY: 0.1, beforeLineStyle: const LineStyle( color: Color(0xffb4f500), thickness: 6, ), afterLineStyle: const LineStyle( color: Color(0xffb4f500), thickness: 6, ), indicatorStyle: IndicatorStyle( width: 40, height: 40, indicator: getIndicator(state), ), startChild: Container( child: Column(children: [ SizedBox( height: 1, ), SizedBox( height: 55, ), done ? Offstage() : IconButton( padding: EdgeInsets.zero, alignment: Alignment.centerLeft, icon: Icon( Icons.skip_next_sharp, size: 30, color: Colors.orange[300], ), onPressed: () => skip()), ]), ), endChild: Container( padding: EdgeInsets.only(left: 10), child: Row(children: [ Container( width: 120, height: 80, child: MenuImage( imageName: widget.bloc.getActualImageName(widget.detail.exerciseType!.exerciseTypeId), workoutTreeId: widget.bloc.getActualWorkoutTreeId(widget.detail.exerciseType!.exerciseTypeId)!, ), ), SizedBox( width: 10, ), Expanded( child: RichText( text: TextSpan( style: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.bold, color: done ? Colors.grey[400] : Colors.white, ), children: [ TextSpan( text: widget.detail.exerciseType!.nameTranslation, style: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.bold, color: done ? Colors.grey[400] : Colors.orange[500], shadows: [ Shadow( offset: Offset(5.0, 5.0), blurRadius: 12.0, color: Colors.black54, ), Shadow( offset: Offset(-3.0, 3.0), blurRadius: 12.0, color: Colors.black54, ), ], )), widget.detail.exerciseType!.unitQuantityUnit != null ? TextSpan( text: "\n", ) : TextSpan(), widget.detail.exerciseType!.unitQuantityUnit != null ? TextSpan( text: t(widget.detail.exerciseType!.unitQuantityUnit!) + ": ", style: GoogleFonts.inter( fontSize: 12, color: done ? Colors.grey[100] : Colors.yellow[400], fontWeight: FontWeight.bold)) : TextSpan(), widget.detail.exerciseType!.unitQuantityUnit != null ? TextSpan( text: weight, style: GoogleFonts.inter( fontSize: 12, )) : TextSpan(), TextSpan( text: "\n", ), TextSpan( text: t(widget.detail.exerciseType!.unit) + ": ", style: GoogleFonts.inter( fontSize: 12, color: done ? Colors.grey[100] : Colors.yellow[400], fontWeight: FontWeight.bold)), TextSpan( text: widget.detail.repeats!.toString(), style: GoogleFonts.inter( fontSize: 12, )), TextSpan( text: "\n", ), TextSpan( text: t("Set") + ": ", style: GoogleFonts.inter( fontSize: 12, color: done ? Colors.grey[100] : Colors.yellow[400], fontWeight: FontWeight.bold)), TextSpan( text: step + "/" + countSerie, style: GoogleFonts.inter( fontSize: 12, )), TextSpan( text: "\n", ), TextSpan( text: t("Resting time") + ": ", style: GoogleFonts.inter( fontSize: 12, color: done ? Colors.grey[100] : Colors.yellow[400], fontWeight: FontWeight.bold)), TextSpan( text: restingTime + " " + t("min(s)"), style: GoogleFonts.inter(fontSize: 12, color: done ? Colors.grey[100] : Colors.white, fontWeight: FontWeight.bold)), ]), )), done ? AnimatedBuilder( animation: animation, builder: (context, snapshot) { return Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset( "asset/image/kupa.png", width: animation.value, ), Text("Result", style: GoogleFonts.inter(fontSize: 10, color: Colors.white)), ]); }) : Offstage(), isTest ? AnimatedBuilder( animation: animation, builder: (context, snapshot) { return Column(mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( onTap: () => showDialog( context: context, builder: (BuildContext context) { return DialogCommon( warning: false, title: t("Why Test?"), descriptions: t("This is your first exercise after at least 3 weeks."), description2: t("The first exercise will be a test. The following sets will be recalculated base on your test."), description3: t("This is the most optimal way for your development"), text: "OK", onTap: () => Navigator.of(context).pop(), onCancel: () => { Navigator.of(context).pop(), }, ); }), child: Icon( CustomIcon.question_circle, color: Colors.yellowAccent[700], size: 16, )), ]); }) : Offstage() ]), ), ), ); } void skip() { showCupertinoDialog( useRootNavigator: true, context: context, builder: (_) => CupertinoAlertDialog( title: Text(t("You want to skip really this exercise?")), content: Column(children: [ Divider(), ]), actions: [ TextButton( child: Text(t("No")), onPressed: () => { Navigator.pop(context), }), TextButton( child: Text(t("Yes")), onPressed: () { Navigator.pop(context); widget.bloc.add(TrainingPlanSkipExercise(detail: widget.detail)); }, ) ], )); } }