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/dialog_html.dart'; import 'package:aitrainer_app/widgets/menu_image.dart'; import 'package:aitrainer_app/widgets/victory_widget.dart'; import 'package:badges/badges.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 TrainingPlanDayFinished) { showDialog( context: context, barrierDismissible: true, builder: (BuildContext context) { return Victory( victory: true, ); }); bloc!.celebrating = false; } else if (state is TrainingPlanDayReadyToRestart) { if (!bloc!.celebrating) { showCupertinoDialog( useRootNavigator: true, context: context, builder: (_) => CupertinoAlertDialog( title: Text(t("The training is finished")), content: Column(children: [Divider(), Text(t("Do you want to restart, or select a new Training Plan?"))]), actions: [ TextButton( child: Text(t("New Training Plan"), textAlign: TextAlign.center), onPressed: () => { Navigator.pop(context), Navigator.of(context).popAndPushNamed('myTrainingPlans'), bloc!.restarting = false, }), TextButton( child: Text(t("Restart")), onPressed: () { bloc!.restart(); Navigator.pop(context); Navigator.of(context).popAndPushNamed('home'); }, ) ], )); } } }, 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(); print("init TAB ${widget.bloc.dayNames.length} index ${widget.bloc.activeDayIndex}"); tabController = TabController(length: widget.bloc.dayNames.length, vsync: this); tabController.animateTo(widget.bloc.activeDayIndex, 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( indicator: BoxDecoration( color: Colors.black87, border: Border( bottom: BorderSide(width: 4.0, color: Colors.blue), top: BorderSide(width: 4.0, color: Colors.blue), )), labelPadding: EdgeInsets.only(left: 5, right: 5), tabs: getTabNames(), controller: tabController, onTap: (index) => bloc.activeDayIndex = index, ), 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 = Container( //height: 50, padding: EdgeInsets.only(top: 2, left: 5, right: 5, bottom: 2), color: Colors.white24, child: RichText( textScaleFactor: 0.8, 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! : ""; } 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) { 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; } 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); print("detail ${widget.detail}"); 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 != null ? widget.detail.weight!.toStringAsFixed(1) : "-"; bool isDrop = false; if (widget.detail.weight == -3) { weight = t("DROP"); isDrop = true; } String restingTime = widget.detail.restingTime == null ? "" : widget.detail.restingTime!.toStringAsFixed(0); bool isTest = false; if (widget.detail.weight != null && widget.detail.weight! == -1) { weight = t("TEST"); isTest = true; } String repeats = widget.detail.repeats!.toString(); if (widget.detail.repeats! == -1) { repeats = t("MAX"); } final bool extraExercise = widget.detail.exerciseType!.name == "Warming Up" || widget.detail.exerciseType!.name == "Stretching"; bool buddyWarning = widget.detail.exerciseType == null ? false : widget.detail.exerciseType!.buddyWarning; 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: Badge( elevation: 0, padding: EdgeInsets.all(0), position: BadgePosition.bottomStart(start: -5), animationDuration: Duration(milliseconds: 500), animationType: BadgeAnimationType.slide, badgeColor: Colors.transparent, showBadge: true, badgeContent: IconButton( onPressed: () => showDialog( context: context, builder: (BuildContext context) { return DialogHTML( title: widget.detail.exerciseType!.nameTranslation, htmlData: '

' + widget.detail.exerciseType!.descriptionTranslation + '

'); }), icon: Icon( Icons.info_outline, color: Colors.yellow[200], )), child: Badge( elevation: 0, padding: EdgeInsets.all(0), position: BadgePosition.topEnd(end: -8), animationDuration: Duration(milliseconds: 500), animationType: BadgeAnimationType.slide, badgeColor: Colors.transparent, showBadge: buddyWarning, badgeContent: IconButton( onPressed: () => showDialog( context: context, builder: (BuildContext context) { return DialogCommon( warning: true, text: "Warning", descriptions: t("Attention!"), description2: t("The safe and exact execution of this exercise you need a training buddy or a trainer"), description3: t("Execution at your own risk!"), onTap: () => Navigator.of(context).pop(), onCancel: () => Navigator.of(context).pop(), title: t('Training Buddy'), ); }), icon: Icon( CustomIcon.exclamation_circle, color: Colors.red[800], )), child: MenuImage( imageName: widget.bloc.getActualImageName(widget.detail.exerciseType!.exerciseTypeId), workoutTreeId: widget.bloc.getActualWorkoutTreeId(widget.detail.exerciseType!.exerciseTypeId)!, radius: 12, ))), ), 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 && !extraExercise ? TextSpan( text: "\n", ) : TextSpan(), widget.detail.exerciseType!.unitQuantityUnit != null && !extraExercise ? 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 && !extraExercise ? TextSpan( text: weight, style: GoogleFonts.inter( fontSize: 12, )) : TextSpan(), TextSpan( text: "\n", ), !extraExercise ? TextSpan( text: t(widget.detail.exerciseType!.unit) + ": ", style: GoogleFonts.inter( fontSize: 12, color: done ? Colors.grey[100] : Colors.yellow[400], fontWeight: FontWeight.bold)) : TextSpan(), !extraExercise ? TextSpan( text: repeats, style: GoogleFonts.inter( fontSize: 12, )) : TextSpan(), TextSpan( text: "\n", ), !extraExercise ? TextSpan( text: t("Set") + ": ", style: GoogleFonts.inter( fontSize: 12, color: done ? Colors.grey[100] : Colors.yellow[400], fontWeight: FontWeight.bold)) : TextSpan(), !extraExercise ? TextSpan( text: step + "/" + countSerie, style: GoogleFonts.inter( fontSize: 12, )) : TextSpan(), TextSpan( text: "\n", ), !extraExercise ? TextSpan( text: t("Resting time") + ": ", style: GoogleFonts.inter( fontSize: 12, color: done ? Colors.grey[100] : Colors.yellow[400], fontWeight: FontWeight.bold)) : TextSpan(), !extraExercise ? TextSpan( text: restingTime + " " + t("min(s)"), style: GoogleFonts.inter(fontSize: 12, color: done ? Colors.grey[100] : Colors.white, fontWeight: FontWeight.bold)) : TextSpan(), ]), )), 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, )), ]); }) : isDrop ? 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("Drop set"), descriptions: t("Drop set"), description2: t("Recommended method:"), text: "OK", onTap: () => Navigator.of(context).pop(), onCancel: () => { Navigator.of(context).pop(), }, ); }), child: Icon( CustomIcon.question_circle, color: Colors.orange[200], 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)); }, ) ], )); } }