diff --git a/lib/models/course/education_journey_model.dart b/lib/models/course/education_journey_model.dart index 18b638a4..199dac28 100644 --- a/lib/models/course/education_journey_model.dart +++ b/lib/models/course/education_journey_model.dart @@ -451,6 +451,8 @@ class Topic { }; } +enum VideoPlayerState { playing, paused, completed, loading } + class Content { String? body; int? id; @@ -460,24 +462,27 @@ class Content { String? title; Video? video; VideoPlayerController? controller; - Timer? timer; + VideoPlayerState? videoState; + double? viewedPercentage; - Content({this.body, this.id, this.question, this.read, this.subjectId, this.title, this.video, this.controller, this.timer}); + Content({this.body, this.id, this.question, this.read, this.subjectId, this.title, this.video, this.controller, this.videoState, this.viewedPercentage}); factory Content.fromRawJson(String str) => Content.fromJson(json.decode(str)); String toRawJson() => json.encode(toJson()); factory Content.fromJson(Map json) => Content( - body: json["body"], - id: json["id"], - question: json["question"] == null ? null : Question.fromJson(json["question"]), - read: json["read"], - subjectId: json["subject_id"], - title: json["title"], - video: json["video"] == null ? null : Video.fromJson(json["video"]), - controller: null, - timer: null); + body: json["body"], + id: json["id"], + question: json["question"] == null ? null : Question.fromJson(json["question"]), + read: json["read"], + subjectId: json["subject_id"], + title: json["title"], + video: json["video"] == null ? null : Video.fromJson(json["video"]), + controller: null, + videoState: VideoPlayerState.paused, + viewedPercentage: 0.0, + ); Map toJson() => { "body": body, diff --git a/lib/pages/ToDoList/ToDo.dart b/lib/pages/ToDoList/ToDo.dart index 6ab67f12..c225987c 100644 --- a/lib/pages/ToDoList/ToDo.dart +++ b/lib/pages/ToDoList/ToDo.dart @@ -576,7 +576,7 @@ class _ToDoState extends State with SingleTickerProviderStateMixin { Permission.bluetoothScan, Permission.activityRecognition, ].request().whenComplete(() { - PenguinMethodChannel.launch("penguin", projectViewModel.isArabic ? "ar" : "en", projectViewModel.authenticatedUserObject.user.patientID.toString(), details: data); + PenguinMethodChannel().launch("penguin", projectViewModel.isArabic ? "ar" : "en", projectViewModel.authenticatedUserObject.user.patientID.toString(), details: data); }); } } diff --git a/lib/pages/learning/course_detailed.dart b/lib/pages/learning/course_detailed.dart index 8ba6ae1a..68c3b7d1 100644 --- a/lib/pages/learning/course_detailed.dart +++ b/lib/pages/learning/course_detailed.dart @@ -3,14 +3,21 @@ import 'dart:async'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:diplomaticquarterapp/core/viewModels/project_view_model.dart'; import 'package:diplomaticquarterapp/extensions/string_extensions.dart'; +import 'package:diplomaticquarterapp/models/course/education_journey_model.dart'; import 'package:diplomaticquarterapp/pages/learning/content_widget.dart'; +import 'package:diplomaticquarterapp/pages/learning/measureWidget.dart'; +import 'package:diplomaticquarterapp/pages/learning/progress_bar_widget.dart'; +import 'package:diplomaticquarterapp/pages/learning/question_sheet.dart'; +import 'package:diplomaticquarterapp/pages/learning/scroll_widget.dart'; import 'package:diplomaticquarterapp/services/course_service/course_service.dart'; import 'package:diplomaticquarterapp/widgets/others/app_scaffold_widget.dart'; import 'package:flutter/cupertino.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:video_player/video_player.dart'; +import 'package:visibility_detector/visibility_detector.dart'; + +import 'widgets/player_controlls.dart'; class CourseDetailedPage extends StatefulWidget { @override @@ -26,8 +33,6 @@ class _CourseDetailedPageState extends State { @override void dispose() { - // _timer?.cancel(); - // _controller.dispose(); super.dispose(); } @@ -38,37 +43,48 @@ class _CourseDetailedPageState extends State { isShowDecPage: false, showNewAppBarTitle: true, showNewAppBar: true, - appBarTitle: "Course Name", + appBarTitle: context.read().getPageTitle ?? "", backgroundColor: Color(0xFFF7F7F7), onTap: () {}, body: Consumer( builder: (context, provider, child) { if (provider.courseData != null) { - return ListView( - shrinkWrap: true, - padding: EdgeInsets.zero, + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ if (provider.controller != null && provider.controller!.value.isInitialized) ...[ Column( children: [ AspectRatio( aspectRatio: provider.controller!.value.aspectRatio, - child: VideoPlayer(provider.controller!), + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + VideoPlayer(provider.controller!), + PlayerControlsOverlay(), + VideoProgressIndicator( + provider.controller!, + padding: EdgeInsets.only(bottom: 0), + colors: VideoProgressColors( + backgroundColor: Color(0xFFD9D9D9), + bufferedColor: Colors.black12, + playedColor: Color(0xFFD02127), + ), + allowScrubbing: true, + ), + ], + ), ).onTap(() { setState(() { - provider.controller!.value.isPlaying ? provider.controller!.pause() : provider.controller!.play(); + if (provider.controller!.value.isPlaying) { + provider.controller!.pause(); + provider.playedContent!.videoState = VideoPlayerState.paused; + } else { + provider.controller!.play(); + provider.playedContent!.videoState = VideoPlayerState.playing; + } }); }), - VideoProgressIndicator( - provider.controller!, - padding: EdgeInsets.only(bottom: 0), - colors: VideoProgressColors( - backgroundColor: Color(0xFFD9D9D9), - bufferedColor: Colors.black12, - playedColor: Color(0xFFD02127), - ), - allowScrubbing: true, - ), ], ) ] else ...[ @@ -83,15 +99,25 @@ class _CourseDetailedPageState extends State { fit: BoxFit.cover, ), ), - child: Center( - child: Icon( - Icons.play_circle, - size: 35, - color: Colors.white, - ), - ), + child: provider.controller != null && !provider.controller!.value.isLooping + ? Center( + child: CircularProgressIndicator( + color: Color(0xFFD02127), + ), + ) + : provider.controller != null + ? Center( + child: Icon( + Icons.play_circle, + size: 35, + color: Colors.white, + ), + ) + : SizedBox(), + ), + placeholder: (context, url) => Center( + child: CircularProgressIndicator(), ), - placeholder: (context, url) => Center(child: CircularProgressIndicator()), errorWidget: (context, url, error) => Icon(Icons.error), ).onTap(() { setState(() { @@ -101,334 +127,184 @@ class _CourseDetailedPageState extends State { }); }), ], - if (provider.consultation != null) ...[ - ListView( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - padding: EdgeInsets.symmetric(horizontal: 20), - children: [ - SizedBox(height: 20.0), - Text( - context.read().isArabic ? provider.consultation!.tagValues!.titleAr! : provider.consultation!.tagValues!.title ?? "Basics of Heart", - style: TextStyle( - fontSize: 16.0, - height: 24 / 16, - color: Color(0xFF2E303A), - fontWeight: FontWeight.w600, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 20.0), + Text( + context.read().isArabic ? provider.consultation!.tagValues!.titleAr! : provider.consultation!.tagValues!.title ?? "Basics of Heart", + style: TextStyle( + fontSize: 16.0, + height: 24 / 16, + color: Color(0xFF2E303A), + fontWeight: FontWeight.w600, + ), ), - ), - // SizedBox(height: 2.0), - // Text( - // "Heart diseases include conditions like coronary and heart failure, often caused by factors ", - // style: TextStyle(fontSize: 10.0, height: 15 / 10, color: Color(0xFF575757)), - // ), - // SizedBox(height: 6.0), - Text( - provider.consultation!.stats!.videosCount.toString() + " Videos", - style: TextStyle( - fontSize: 10.0, - height: 15 / 10, - color: Color(0xFF2E303A), - fontWeight: FontWeight.w600, + // SizedBox(height: 2.0), + // Text( + // "Heart diseases include conditions like coronary and heart failure, often caused by factors ", + // style: TextStyle(fontSize: 10.0, height: 15 / 10, color: Color(0xFF575757)), + // ), + // SizedBox(height: 6.0), + Text( + provider.consultation!.stats!.videosCount.toString() + " Videos", + style: TextStyle( + fontSize: 10.0, + height: 15 / 10, + color: Color(0xFF2E303A), + fontWeight: FontWeight.w600, + ), ), - ), - - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // Row( - // children: [ - // Row( - // children: List.generate(5, (starIndex) { - // return Icon( - // Icons.star, - // color: Color(0xFFD1272D), - // size: 15.0, - // ); - // }), - // ), - // SizedBox(width: 8.0), - // Text( - // '540 reviews', - // style: TextStyle( - // fontSize: 10.0, - // height: 15 / 10, - // color: Color(0xFF2B353E), - // fontWeight: FontWeight.w700, - // ), - // ), - // ], - // ), - // ], - // ), - ], + ], + ), ), ], - SizedBox(height: 30.0), + SizedBox(height: 20.0), if (provider.courseTopics != null) ...[ - ListView( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - padding: EdgeInsets.symmetric(horizontal: 20), - children: provider.courseTopics!.map((topic) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Topic title - Text( - (topic.title.toString() + "(${topic.contentsCount.toString()})") ?? "Topic", - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Color(0xFF2B353E), + Expanded( + child: ListViewSeparatedExtension.separatedWithScrollListener( + itemCount: provider.courseTopics!.length, + shrinkWrap: true, + physics: AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.symmetric(horizontal: 20), + itemBuilder: (context, ind) { + Topic topic = provider.courseTopics![ind]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + // Topic title + Text( + (topic.title.toString() + "(${topic.contentsCount.toString()})") ?? "Topic", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Color(0xFF2B353E), + ), ), - ), - SizedBox(height: 8), - ...topic.contents!.asMap().entries.map((entry) { - final index = entry.key + 1; - final content = entry.value; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(index.toString()), - SizedBox( - width: 9, - ), - Expanded( - flex: 10, - child: Column( + SizedBox(height: 8), + ListViewSeparatedExtension.separatedWithScrollListener( + itemCount: topic.contents!.length, + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + final content = topic.contents![index]; + var percentage = content.viewedPercentage != 0.0 ? content.viewedPercentage : provider.convertToDoublePercentage(content.read ?? 0); + return VisibilityDetector( + key: Key(content.id.toString()), + onVisibilityChanged: (visibilityInfo) => provider.onVisibilityChange(visibilityInfo, content, topic.id!), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - content.title ?? "Course overview", - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: Color(0xFF2B353E), - ), - ), - Text( - "Video - ${provider.getDurationOfVideo(content.video!.flavor!.duration!)}", - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: Color(0xFF575757), - ), - ), - ContentWidget( - body: content.body!, - ) - ], - ), - ), - Expanded( - flex: 4, - child: Column( - children: [ + Text(index.toString()), SizedBox( - height: 10, + width: 9, ), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (content.controller != null) ...[ - Container( - width: 55, - child: VideoProgressIndicator(content.controller!, - padding: EdgeInsets.only(bottom: 1), - colors: VideoProgressColors( - backgroundColor: Color(0xFFD9D9D9), - bufferedColor: Colors.black12, - playedColor: Color(0xFF359846), - ), - allowScrubbing: true), - ) - ] else ...[ - Container( - width: 55, - child: Container( - height: 4, - color: Colors.black12, - width: double.infinity, + Expanded( + flex: 10, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + content.title ?? "Course overview", + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Color(0xFF2B353E), + ), + ), + Text( + "Video - ${provider.getDurationOfVideo(content.video!.flavor!.duration!)}", + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Color(0xFF575757), ), + ), + ContentWidget( + body: content.body!, ) ], - SizedBox( - width: 10, - ), - if (content.controller != null) ...[ - Container( - width: 20, - height: 20, - decoration: BoxDecoration( - color: Color(0xFFDFDFDF), - borderRadius: BorderRadius.all( - Radius.circular(30), + ), + ), + Expanded( + flex: 4, + child: Column( + children: [ + SizedBox( + height: 10, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + CustomProgressBar( + playedPercentage: percentage!, + bufferedPercentage: 0.0, + backgroundColor: Color(0xFFD9D9D9), + bufferedColor: Colors.black12, + playedColor: Color(0xFF359846), ), - ), - child: Icon( - getVideoIcon(provider.videoState), - size: 18, - ).onTap(() { - setState(() { - content.controller!.value.isPlaying ? content.controller!.pause() : content.controller!.play(); - }); - }), - ) - ] else ...[ - Container( - width: 20, - height: 20, - decoration: BoxDecoration( - color: Color(0xFFDFDFDF), - borderRadius: BorderRadius.all( - Radius.circular(30), + SizedBox( + width: 10, ), - ), - child: Icon( - Icons.play_arrow, - size: 18, - ).onTap(() { - provider.playVideo(context, content: content); - }), + Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: Color(0xFFDFDFDF), + borderRadius: BorderRadius.all( + Radius.circular(30), + ), + ), + child: Icon( + content.videoState == VideoPlayerState.loading + ? Icons.hourglass_top + : content.videoState == VideoPlayerState.playing + ? Icons.pause + : content.videoState == VideoPlayerState.paused + ? Icons.play_arrow + : content.videoState == VideoPlayerState.completed + ? Icons.replay + : Icons.play_arrow, + // Use specific content's state + size: 18, + ).onTap(() { + setState(() { + provider.onVisibilityChange(null, content, topic.id!, isClicked: true); + provider.play(context, content: content); + }); + }), + ) + ], ) - ] - ], + ], + ), ), ], ), ), - ], - ), - ); - }).toList(), - ], - ); - }).toList(), + ); + }, + separatorBuilder: (BuildContext context, int index) { + return SizedBox(); + }, + // onScroll: onScroll, + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return SizedBox(); + }, + ), ), ] - - // if(provider.courseTopics != null)...[ - // ListView( - // shrinkWrap: true, - // physics: NeverScrollableScrollPhysics(), - // padding: EdgeInsets.symmetric(horizontal: 20), - // children: [ - // Row( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Text("1"), - // SizedBox( - // width: 9, - // ), - // Expanded( - // flex: 10, - // child: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Text( - // "Course overview", - // style: TextStyle( - // fontSize: 14, - // fontWeight: FontWeight.w600, - // color: Color(0xFF2B353E), - // ), - // ), - // Text( - // "Video - 02:56 mins", - // style: TextStyle( - // fontSize: 12, - // fontWeight: FontWeight.w500, - // color: Color(0xFF575757), - // ), - // ), - // RichText( - // text: TextSpan( - // children: [ - // TextSpan( - // text: 'Heart diseases include conditions like coronary and heart failure... ', - // style: TextStyle( - // color: Color( - // 0xFFA2A2A2, - // ), - // fontSize: 13, - // fontWeight: FontWeight.w400, - // height: 13 / 10, - // ), - // ), - // TextSpan( - // text: 'See More', - // style: TextStyle( - // color: Color(0xFF2B353E), - // fontSize: 13, - // fontWeight: FontWeight.w500, - // height: 13 / 10, - // ), - // recognizer: TapGestureRecognizer()..onTap = () {}, - // ), - // ], - // ), - // ), - // ], - // ), - // ), - // Expanded( - // flex: 3, - // child: Column( - // children: [ - // SizedBox( - // height: 10, - // ), - // Row( - // crossAxisAlignment: CrossAxisAlignment.center, - // mainAxisAlignment: MainAxisAlignment.center, - // children: [ - // Container( - // width: 55, - // child: VideoProgressIndicator(_controller, - // padding: EdgeInsets.only(bottom: 1), - // colors: VideoProgressColors( - // backgroundColor: Color(0xFFD9D9D9), - // bufferedColor: Colors.black12, - // playedColor: Color(0xFF359846), - // ), - // allowScrubbing: true), - // ), - // SizedBox( - // width: 10, - // ), - // Container( - // width: 18, - // height: 18, - // decoration: BoxDecoration( - // color: Color(0xFFDFDFDF), - // borderRadius: BorderRadius.all( - // Radius.circular(30), - // ), - // ), - // child: Icon( - // _controller.value.isPlaying ? Icons.pause : Icons.play_arrow, - // size: 18, - // ).onTap(() { - // setState(() { - // _controller.value.isPlaying ? _controller.pause() : _controller.play(); - // }); - // }), - // ) - // ], - // ), - // ], - // ), - // ), - // ], - // ) - // ], - // ) - // ] ], ); } else { diff --git a/lib/pages/learning/course_list.dart b/lib/pages/learning/course_list.dart index 9332c7ce..50abcb09 100644 --- a/lib/pages/learning/course_list.dart +++ b/lib/pages/learning/course_list.dart @@ -4,6 +4,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:diplomaticquarterapp/core/enum/viewstate.dart'; import 'package:diplomaticquarterapp/extensions/string_extensions.dart'; import 'package:diplomaticquarterapp/models/course/education_journey_list_model.dart' as model; +import 'package:diplomaticquarterapp/pages/learning/scroll_widget.dart'; import 'package:diplomaticquarterapp/routes.dart'; import 'package:diplomaticquarterapp/services/course_service/course_service.dart'; import 'package:diplomaticquarterapp/uitl/gif_loader_dialog_utils.dart'; @@ -18,6 +19,7 @@ class CourseList extends StatefulWidget { class _CourseListState extends State { CourseServiceProvider? _courseServiceProvider; + GlobalKey _cardKey = GlobalKey(); @override void initState() { @@ -33,6 +35,7 @@ class _CourseListState extends State { } Future _clearData() async { + await _courseServiceProvider!.postNabedJourneyData(); await _courseServiceProvider!.clearData(); } @@ -48,13 +51,13 @@ class _CourseListState extends State { onTap: () {}, body: Consumer(builder: (context, provider, child) { if (provider.nabedJourneyResponse != null) { - return ListView.separated( + return ListViewSeparatedExtension.separatedWithScrollListener( itemCount: provider.data!.length, - padding: EdgeInsets.zero, itemBuilder: (context, index) { List conClass = provider.data![index].contentClasses!; model.Datum data = provider.data![index]; return Card( + key: index == 0 ? _cardKey : null, margin: EdgeInsets.only(left: 20, right: 20, top: 20), shape: RoundedRectangleBorder( side: BorderSide(width: 1, color: Color(0xFFEFEFEF)), @@ -103,7 +106,8 @@ class _CourseListState extends State { ), SizedBox(height: 6.0), Text( - conClass.first.readPercentage.toString() + " Hour Watched", + "${data.stats!.videosReadCount} of ${data.stats!.videosCount} Watched", + // conClass.first.readPercentage.toString() + "% Watched", // "2 Hours", style: TextStyle( fontSize: 10.0, @@ -112,33 +116,31 @@ class _CourseListState extends State { fontWeight: FontWeight.w600, ), ), - SizedBox(height: 8.0), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( + mainAxisAlignment: MainAxisAlignment.start, children: [ - Row( - children: List.generate(5, (starIndex) { - return Icon( - Icons.star, - color: Color(0xFFD1272D), - size: 15.0, - ); - }), + Icon( + Icons.play_arrow, ), - SizedBox(width: 8.0), Text( - '540 reviews', + "${data.stats!.videosCount} Videos", + // conClass.first.readPercentage.toString() + "% Watched", + // "2 Hours", style: TextStyle( fontSize: 10.0, height: 15 / 10, - color: Color(0xFF2B353E), - fontWeight: FontWeight.w700, + color: Color(0xFF2E303A), + fontWeight: FontWeight.w600, ), ), ], ), + SizedBox( + width: 8, + ), Icon( Icons.arrow_right_alt, size: 18.0, @@ -150,8 +152,9 @@ class _CourseListState extends State { ), ), ).onTap(() { - provider.navigate(context, data.id!, onBack: (val) { - provider.uploadStats(); + provider.insertJourneyListData(index, true); + provider.navigate(context, data.id!, conClass.first.title!, onBack: (val) { + // provider.uploadStats(); provider.clear(); }); }); @@ -159,6 +162,7 @@ class _CourseListState extends State { separatorBuilder: (context, index) { return SizedBox(); }, + onScroll: onScroll, ); } else { return SizedBox(); @@ -166,4 +170,37 @@ class _CourseListState extends State { }), ); } + + final Set _visibleCardIds = {}; + + void onScroll(ScrollNotification notification, ScrollController controller, int itemCount) { + double _cardHeight = _getCardSize(); + + if (_cardHeight > 0) { + final double offset = controller.offset; + final double screenHeight = MediaQuery.of(context).size.height; + int firstVisibleIndex = (offset / _cardHeight).floor(); + int lastVisibleIndex = ((offset + screenHeight) / _cardHeight).ceil(); + + Set newlyVisibleCards = {}; + + for (int index = firstVisibleIndex; index <= lastVisibleIndex && index < itemCount; index++) { + final double cardStartOffset = index * _cardHeight; + final double cardEndOffset = cardStartOffset + _cardHeight; + if (cardStartOffset >= offset && cardEndOffset <= offset + screenHeight) { + if (!_visibleCardIds.contains(index)) { + newlyVisibleCards.add(index); + _courseServiceProvider!.insertJourneyListData(index, false); + } + } + } + _visibleCardIds.addAll(newlyVisibleCards); + } + } + + double _getCardSize() { + final RenderBox renderBox = _cardKey.currentContext?.findRenderObject() as RenderBox; + final cardHeight = renderBox.size.height; + return cardHeight; + } } diff --git a/lib/pages/learning/measureWidget.dart b/lib/pages/learning/measureWidget.dart new file mode 100644 index 00000000..8aa1f4d6 --- /dev/null +++ b/lib/pages/learning/measureWidget.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +typedef OnWidgetSizeChange = void Function(Size size); + +class MeasureSize extends StatefulWidget { + final Widget child; + final OnWidgetSizeChange onChange; + + const MeasureSize({ + Key? key, + required this.onChange, + required this.child, + }) : super(key: key); + + @override + _MeasureSizeState createState() => _MeasureSizeState(); +} + +class _MeasureSizeState extends State { + @override + Widget build(BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback((_) { + final size = context.size; + if (size != null) widget.onChange(size); + }); + return widget.child; + } +} diff --git a/lib/pages/learning/progress_bar_widget.dart b/lib/pages/learning/progress_bar_widget.dart new file mode 100644 index 00000000..fa78e1f0 --- /dev/null +++ b/lib/pages/learning/progress_bar_widget.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; + +class CustomProgressBar extends StatelessWidget { + final double playedPercentage; // value between 0.0 and 1.0 + final double bufferedPercentage; // value between 0.0 and 1.0 + final Color backgroundColor; + final Color bufferedColor; + final Color playedColor; + + const CustomProgressBar({ + Key? key, + required this.playedPercentage, + required this.bufferedPercentage, + this.backgroundColor = const Color(0xFFD9D9D9), + this.bufferedColor = Colors.black12, + this.playedColor = const Color(0xFF359846), + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + width: 55, + height: 4, + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(2), + ), + child: Stack( + children: [ + // Buffered bar + Container( + width: 55 * bufferedPercentage.clamp(0.0, 1.0), // Buffered width as a percentage of 55 + decoration: BoxDecoration( + color: bufferedColor, + borderRadius: BorderRadius.circular(2), + ), + ), + // Played bar + Container( + width: 55 * playedPercentage.clamp(0.0, 1.0), // Played width as a percentage of 55 + decoration: BoxDecoration( + color: playedColor, + borderRadius: BorderRadius.circular(2), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/learning/question_sheet.dart b/lib/pages/learning/question_sheet.dart index 38f2e44c..b5ea381c 100644 --- a/lib/pages/learning/question_sheet.dart +++ b/lib/pages/learning/question_sheet.dart @@ -206,6 +206,7 @@ class _ActionButton extends StatelessWidget { // Modified showQuestionSheet function Future showQuestionSheet(BuildContext con, {required Content content}) { + return showModalBottomSheet( context: con, isDismissible: false, diff --git a/lib/pages/learning/scroll_widget.dart b/lib/pages/learning/scroll_widget.dart new file mode 100644 index 00000000..8264a877 --- /dev/null +++ b/lib/pages/learning/scroll_widget.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +typedef ScrollCallback = void Function(ScrollNotification notification, ScrollController controller, int itemCount); + +extension ListViewSeparatedExtension on ListView { + static Widget separatedWithScrollListener({ + required IndexedWidgetBuilder itemBuilder, + required IndexedWidgetBuilder separatorBuilder, + required int itemCount, + bool shrinkWrap = false, + EdgeInsetsGeometry padding = EdgeInsets.zero, + ScrollCallback? onScroll, + ScrollController? controller, + ScrollPhysics? physics, + }) { + final ScrollController _scrollController = controller ?? ScrollController(); + + return NotificationListener( + onNotification: (ScrollNotification notification) { + if (onScroll != null) { + onScroll(notification, _scrollController, itemCount); + } + return false; + }, + child: ListView.separated( + controller: _scrollController, + itemBuilder: itemBuilder, + padding: padding, + separatorBuilder: separatorBuilder, + physics: physics, + shrinkWrap: shrinkWrap, + itemCount: itemCount, + ), + ); + } +} diff --git a/lib/pages/learning/widgets/player_controlls.dart b/lib/pages/learning/widgets/player_controlls.dart new file mode 100644 index 00000000..95855f4b --- /dev/null +++ b/lib/pages/learning/widgets/player_controlls.dart @@ -0,0 +1,39 @@ +import 'package:diplomaticquarterapp/models/course/education_journey_model.dart'; +import 'package:diplomaticquarterapp/services/course_service/course_service.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class PlayerControlsOverlay extends StatefulWidget { + @override + State createState() => _PlayerControlsOverlayState(); +} + +class _PlayerControlsOverlayState extends State { + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, provider, child) { + return Stack( + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 50), + reverseDuration: const Duration(milliseconds: 200), + child: provider.controller!.value.isPlaying + ? const SizedBox.shrink() + : const ColoredBox( + color: Colors.black26, + child: Center( + child: Icon( + Icons.play_arrow, + color: Colors.white, + size: 50.0, + semanticLabel: 'Play', + ), + ), + ), + ), + + ], + ); + }); + } +} diff --git a/lib/services/course_service/course_service.dart b/lib/services/course_service/course_service.dart index 778b4dae..c5583191 100644 --- a/lib/services/course_service/course_service.dart +++ b/lib/services/course_service/course_service.dart @@ -6,7 +6,7 @@ import 'package:diplomaticquarterapp/core/service/client/base_app_client.dart'; import 'package:diplomaticquarterapp/models/Authentication/authenticated_user.dart'; import 'package:diplomaticquarterapp/models/course/education_journey_insert_model.dart'; import 'package:diplomaticquarterapp/models/course/education_journey_list_model.dart'; -import 'package:diplomaticquarterapp/models/course/education_journey_model.dart'; +import 'package:diplomaticquarterapp/models/course/education_journey_model.dart' as ejm; import 'package:diplomaticquarterapp/pages/learning/question_sheet.dart'; import 'package:diplomaticquarterapp/routes.dart'; import 'package:diplomaticquarterapp/services/authentication/auth_provider.dart'; @@ -15,6 +15,7 @@ import 'package:diplomaticquarterapp/uitl/gif_loader_dialog_utils.dart'; import 'package:diplomaticquarterapp/uitl/utils.dart'; import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; +import 'package:visibility_detector/visibility_detector.dart'; class CourseServiceProvider with ChangeNotifier { AppSharedPreferences sharedPref = AppSharedPreferences(); @@ -22,48 +23,56 @@ class CourseServiceProvider with ChangeNotifier { AuthenticatedUser authUser = AuthenticatedUser(); AuthProvider authProvider = AuthProvider(); bool isDataLoaded = false; - int? selectedJourney; + int? _selectedJourney; + + int get getSelectedJourney => _selectedJourney!; + + set setSelectedJourney(int value) { + _selectedJourney = value; + } + + String? _pageTitle; + + String? get getPageTitle => _pageTitle; + + set setPageTitle(String? value) { + _pageTitle = value; + } + List? data; PatientEducationJourneyListModel? nabedJourneyResponse; - PatientEducationJourneyModel? courseData; - List? courseTopics; - Consultation? consultation; - - List? playedData = []; + ejm.PatientEducationJourneyModel? courseData; + List? courseTopics; + ejm.Consultation? consultation; + List? contentClasses; - //Main Video Controller & Timer - VideoPlayerState _videoState = VideoPlayerState.paused; + List nabedInsertDataPayload = []; + ejm.Content? playedContent; - VideoPlayerState get videoState => _videoState; VideoPlayerController? controller; + Timer? timer; // Learning Page - - Future fetchPatientCoursesList() async { - print("====== Api Initiated ========="); + void getCourses(BuildContext context) async { + GifLoaderDialogUtils.showMyDialog(context); Map request; if (await this.sharedPref.getObject(USER_PROFILE) != null) { var data = AuthenticatedUser.fromJson(await this.sharedPref.getObject(USER_PROFILE)); authUser = data; } - request = {"Channel": 3, "TokenID": "@dm!n", "PatientID": 22335}; - dynamic localRes; - await BaseAppClient().post(GET_PATIENT_COURSES_LIST, onSuccess: (response, statusCode) async { - print("====== Api Response ========="); - print("${response["NabedJourneyResponseResult"]}"); - localRes = PatientEducationJourneyListModel.fromJson(response); + dynamic response; + //{"Channel": 3, "TokenID": "@dm!n", "PatientID": 22335}; + request = {"Channel": 3}; + await BaseAppClient().post(GET_PATIENT_COURSES_LIST, onSuccess: (res, statusCode) async { + response = PatientEducationJourneyListModel.fromJson(res); }, onFailure: (String error, int statusCode) { + GifLoaderDialogUtils.hideDialog(context); + Utils.showErrorToast(error); throw error; }, body: request); - return localRes; - } - - void getCourses(BuildContext context) async { - GifLoaderDialogUtils.showMyDialog(context); - dynamic response = await fetchPatientCoursesList(); - GifLoaderDialogUtils.hideDialog(context); if (response.nabedJourneyResponseResult != null) { + GifLoaderDialogUtils.hideDialog(context); nabedJourneyResponse = response; isDataLoaded = true; data = nabedJourneyResponse!.nabedJourneyResponseResult!.data; @@ -72,40 +81,64 @@ class CourseServiceProvider with ChangeNotifier { } // Detailed Page - - Future fetchPatientCourseById() async { - print("====== Api Initiated ========="); + void getCourseById(BuildContext context) async { + GifLoaderDialogUtils.showMyDialog(context); Map request; if (await this.sharedPref.getObject(USER_PROFILE) != null) { var data = AuthenticatedUser.fromJson(await this.sharedPref.getObject(USER_PROFILE)); authUser = data; } - request = {"Channel": 3, "TokenID": "@dm!n", "JourneyID": selectedJourney}; - dynamic localRes; - await BaseAppClient().post(GET_PATIENT_COURSE_BY_ID, onSuccess: (response, statusCode) async { - print("====== Api Response ========="); - print("${response}"); - localRes = response; + dynamic response; + // "TokenID": "@dm!n", + request = {"Channel": 3, "JourneyID": getSelectedJourney}; + await BaseAppClient().post(GET_PATIENT_COURSE_BY_ID, onSuccess: (res, statusCode) async { + response = res; }, onFailure: (String error, int statusCode) { + GifLoaderDialogUtils.hideDialog(context); + Utils.showErrorToast(error); throw error; }, body: request); - return localRes; - } - - void getCourseById(BuildContext context) async { - GifLoaderDialogUtils.showMyDialog(context); - dynamic response = await fetchPatientCourseById(); GifLoaderDialogUtils.hideDialog(context); if (response != null) { - courseData = PatientEducationJourneyModel.fromRawJson(jsonEncode(response)); + courseData = ejm.PatientEducationJourneyModel.fromRawJson(jsonEncode(response)); courseTopics = courseData!.nabedJourneyByIdResponseResult!.contentClasses!.first.topics; + contentClasses = courseData!.nabedJourneyByIdResponseResult!.contentClasses; consultation = courseData!.nabedJourneyByIdResponseResult!.consultation; + + // Future.delayed(Duration(seconds: 1), () { + // insertDetailedJourneyListData(); + // }); + } + notifyListeners(); + } + + // Learning Page + Future postNabedJourneyData() async { + // GifLoaderDialogUtils.showMyDialog(context); + Map request; + if (await this.sharedPref.getObject(USER_PROFILE) != null) { + var data = AuthenticatedUser.fromJson(await this.sharedPref.getObject(USER_PROFILE)); + authUser = data; } + dynamic response; + print(jsonEncode(nabedInsertDataPayload)); + //"PatientID": 22335, + //"TokenID": "@dm!n", + request = {"LanguageID": 1, "data": nabedInsertDataPayload}; + await BaseAppClient().post(INSERT_PATIENT_COURSE_VIEW_STATS, onSuccess: (res, statusCode) async { + print(res); + //response = PatientEducationJourneyListModel.fromJson(res); + }, onFailure: (String error, int statusCode) { + // GifLoaderDialogUtils.hideDialog(context); + Utils.showErrorToast(error); + throw error; + }, body: request); notifyListeners(); } - Future navigate(BuildContext context, int journey, {required Function(Object? val) onBack}) async { - selectedJourney = journey; + Future navigate(BuildContext context, int journey, String title, {required Function(Object? val) onBack}) async { + setSelectedJourney = journey; + setPageTitle = title; Navigator.of(context).pushNamed(COURSES_DETAILED_PAGE).then((val) { onBack(val); }); @@ -118,84 +151,116 @@ class CourseServiceProvider with ChangeNotifier { return "$minutes:${seconds.toString().padLeft(2, '0')} mins"; } - void playVideo(BuildContext context, {required Content content}) async { - print("OnTap"); - if (_videoState == VideoPlayerState.playing) { - controller?.pause(); - setVideoState(VideoPlayerState.paused); - } else { - setVideoState(VideoPlayerState.loading); - notifyListeners(); + void play(BuildContext context, {required ejm.Content content}) async { + switch (content.videoState) { + case ejm.VideoPlayerState.loading: + notifyListeners(); + break; - try { - final videoUrl = content.video?.flavor?.downloadable ?? ""; - if (videoUrl.isEmpty) { - Utils.showErrorToast("No video URL provided."); - setVideoState(VideoPlayerState.paused); - return; + case ejm.VideoPlayerState.playing: + controller?.pause(); + content.videoState = ejm.VideoPlayerState.paused; + notifyListeners(); + break; + + case ejm.VideoPlayerState.paused: + if (controller != null && content.id == playedContent!.id) { + controller!.play(); + playedContent!.videoState = ejm.VideoPlayerState.playing; + notifyListeners(); + } else { + playVideo(context, content: content); } + default: + playVideo(context, content: content); + break; + } + } - controller = VideoPlayerController.networkUrl( - Uri.parse(videoUrl), - //formatHint: VideoFormat.hls, - ); + void playVideo(BuildContext context, {required ejm.Content content}) async { + if (controller != null && controller!.value.isPlaying) { + controller!.pause(); + if (playedContent != null) { + playedContent!.videoState = ejm.VideoPlayerState.paused; + } + removeVideoListener(context); + controller = null; + } - await controller!.initialize(); - controller!.play(); - setVideoState(VideoPlayerState.playing); - notifyListeners(); + content.videoState = ejm.VideoPlayerState.loading; + notifyListeners(); + final videoUrl = content.video?.flavor?.downloadable ?? ""; + if (videoUrl.isEmpty) { + Utils.showErrorToast("Something went wrong."); + content.videoState = ejm.VideoPlayerState.paused; + notifyListeners(); + return; + } + + controller = await VideoPlayerController.networkUrl(Uri.parse(videoUrl)); + controller!.initialize()..then((value) => notifyListeners()); + print("Controller Value = "+controller!.value.toString()); + controller!.play(); + addVideoListener(context); + content.videoState = ejm.VideoPlayerState.playing; + playedContent = content; + notifyListeners(); + } - content.controller = controller; + void addVideoListener(BuildContext context) { + controller?.removeListener(() => videoListener(context)); + controller?.addListener(() => videoListener(context)); + } - controller!.addListener(() { - if (controller!.value.isPlaying && timer == null) { - _startTimer(context, content: content); - } else if (!controller!.value.isPlaying && timer != null) { - stopTimer(); - } + void removeVideoListener(BuildContext context) { + controller?.removeListener(() => videoListener(context)); + } - if (controller!.value.hasError) { - Utils.showErrorToast("Failed to load video."); - controller = null; - setVideoState(VideoPlayerState.paused); - notifyListeners(); - } - }); - } catch (e) { - Utils.showErrorToast("Error loading video: $e"); - controller = null; - setVideoState(VideoPlayerState.paused); + int? lastProcessedSecond; + + void videoListener(BuildContext context) async { + final currentSecond = controller!.value.position.inSeconds; + if (lastProcessedSecond != currentSecond) { + lastProcessedSecond = currentSecond; + Duration currentWatchedTime = controller!.value.position; + Duration totalVideoTime = controller!.value.duration; + if (totalVideoTime != null) { + playedContent!.viewedPercentage = await calculateWatchedPercentage(currentWatchedTime, totalVideoTime); + notifyListeners(); + } + print(playedContent!.viewedPercentage); + if (currentSecond == playedContent!.question!.triggerAt) { + controller!.pause(); + playedContent!.videoState = ejm.VideoPlayerState.paused; + notifyListeners(); + + QuestionSheetAction? action = await showQuestionSheet(context, content: playedContent!); + if (action == QuestionSheetAction.skip) { + controller!.play(); + playedContent!.videoState = ejm.VideoPlayerState.playing; + } else if (action == QuestionSheetAction.rewatch) { + playVideo(context, content: playedContent!); + } notifyListeners(); } } } - void setVideoState(VideoPlayerState state) { - _videoState = state; - notifyListeners(); + double calculateWatchedPercentage(Duration currentWatchedTime, Duration totalVideoTime) { + if (totalVideoTime.inSeconds == 0) { + return 0.0; + } + double percentage = (currentWatchedTime.inSeconds / totalVideoTime.inSeconds); + return percentage.clamp(0.0, 1.0); // Clamp the value between 0.0 and 1.0 } - void _startTimer(BuildContext context, {required Content content}) { - timer = Timer.periodic(Duration(seconds: 1), (timer) async { - if (controller != null && _videoState == VideoPlayerState.playing) { - final position = await controller!.position; - if (position != null) { - print("Current position: ${position.inSeconds} seconds"); - notifyListeners(); - if (position.inSeconds == content.question!.triggerAt) { - controller!.pause(); - setVideoState(VideoPlayerState.paused); - QuestionSheetAction? action = await showQuestionSheet(context, content: content); - if (action == QuestionSheetAction.skip) { - controller!.play(); - setVideoState(VideoPlayerState.playing); - } else if (action == QuestionSheetAction.rewatch) { - playVideo(context, content: content); - } - } - } - } - }); + int convertToIntegerPercentage(double percentage) { + return (percentage.clamp(0.0, 1.0) * 99 + 1).toInt(); + } + + double convertToDoublePercentage(int value) { + int clampedValue = value.clamp(1, 100); + return (clampedValue - 1) / 99.0; } void stopTimer() { @@ -204,148 +269,163 @@ class CourseServiceProvider with ChangeNotifier { } Future uploadStats() async { - // if(courseTopics != null) { - // Future.forEach(courseTopics!, (topic) { - // Future.forEach(topic.contents!, (content){ - // content.controller - // }); - // }); - // } - if (courseTopics != null) { - await Future.forEach(courseTopics!, (topic) async { - await Future.forEach(topic.contents!, (content) { - print(content.controller?.value); - playedData!.add(new Data( - type: "content_watch", - consultationId: 2660, - contentClassId: 46, - contentId: 322, - topicId: 6, - percentage: content.controller?.value.position.inSeconds ?? 0, - flavorId: 1, - srcType: "educate-external", - screenType: "journey_screen", - )); - }); - }); + nabedInsertDataPayload = []; } print("==============="); - print(playedData.toString()); + print(nabedInsertDataPayload.toString()); print("==============="); } - // void playVideo(BuildContext context, {required Content content}) async { - // print("OnTap"); - // if (_videoState == VideoPlayerState.playing) { - // controller?.pause(); - // setVideoState(VideoPlayerState.paused); - // } else { - // try { - // controller = VideoPlayerController.networkUrl( - // Uri.parse( - // Platform.isIOS ? content.video!.flavor!.downloadable! : content.video!.flavor!.downloadable!, - // ), - // formatHint: VideoFormat.hls) - // ..initialize() - // ..play(); - // notifyListeners(); - // content.controller = controller; - // notifyListeners(); - // controller!.addListener(() { - // if (controller!.value.isPlaying && timer == null) { - // startTimer(context, content: content); - // notifyListeners(); - // } else if (!controller!.value.isPlaying && timer != null) { - // stopTimer(); - // notifyListeners(); - // } - // }); - // - // controller!.addListener(() { - // if (controller!.value.hasError) { - // Utils.showErrorToast("Failed to load video."); - // controller = null; - // setVideoState(VideoPlayerState.paused); - // notifyListeners(); - // } - // }); - // notifyListeners(); - // } catch (e) { - // Utils.showErrorToast("Error loading video: $e"); - // controller = null; - // setVideoState(VideoPlayerState.paused); - // notifyListeners(); - // } - // controller?.play(); - // setVideoState(VideoPlayerState.playing); - // } - // } - // - // - // void setVideoState(VideoPlayerState state) { - // _videoState = state; - // notifyListeners(); - // } - // - // void startTimer(BuildContext context, {required Content content}) { - // timer = Timer.periodic(Duration(seconds: 1), (timer) async { - // if (controller != null && _videoState == VideoPlayerState.playing) { - // final position = await controller!.position; - // if (position != null) { - // print("Current position: ${position.inSeconds} seconds"); - // if (position.inSeconds == content.question!.triggerAt) { - // print("position: ${position.inSeconds} - ${content.question!.triggerAt} seconds"); - // controller!.pause(); - // setVideoState(VideoPlayerState.paused); - // QuestionSheetAction? action = await showQuestionSheet(context, content: content); - // if (action == QuestionSheetAction.skip) { - // print("Skip"); - // controller!.play(); - // } else if (action == QuestionSheetAction.rewatch) { - // print("Re-watch"); - // playVideo(context, content: content); - // } - // } - // notifyListeners(); - // } - // } - // }); - // } - // - // void stopTimer() { - // timer?.cancel(); - // timer = null; - // } - - // void onComplete() { - // stopTimer(); - // setVideoState(VideoPlayerState.completed); - // } + void insertJourneyListData(int index, bool isClicked) { + if (index >= 0 && index < data!.length) { + var datum = data![index]; + bool journeyDisplayed = nabedInsertDataPayload.any((dataItem) => dataItem.type == UserAction.journeyDisplayed.toJson() && dataItem.consultationId == datum.id); + if (!journeyDisplayed) { + nabedInsertDataPayload.add( + Data( + type: UserAction.journeyDisplayed.toJson(), + consultationId: datum.id, + srcType: "educate-external", + screenType: "journey_listing_screen", + ), + ); + } + if (isClicked) { + bool journeyClick = nabedInsertDataPayload.any((dataItem) => dataItem.type == UserAction.journeyClick.toJson() && dataItem.consultationId == datum.id); + if (!journeyClick) { + nabedInsertDataPayload.add( + Data( + type: UserAction.journeyClick.toJson(), + consultationId: datum.id, + srcType: "educate-external", + screenType: "journey_listing_screen", + ), + ); + } + } + print(jsonEncode(nabedInsertDataPayload)); + } else { + print("Index $index is out of bounds. Please provide a valid index."); + } + } + + void onVisibilityChange(VisibilityInfo? visibilityInfo, ejm.Content content, int topicId, {bool isClicked = false}) { + int? contentClassID; + for (var data in contentClasses!) { + for (var topic in data.topics!) { + for (var con in topic.contents!) { + if (con.id == content.id) { + contentClassID = data.id; + } + } + } + } + + if (!isClicked) { + var visiblePercentage = visibilityInfo!.visibleFraction * 100; + if (visiblePercentage == 100) { + bool isAddedBefore = nabedInsertDataPayload.any((dataItem) => dataItem.type == UserAction.contentDisplayed.toJson() && dataItem.contentId == content.id); + if (!isAddedBefore) { + nabedInsertDataPayload.add( + Data( + type: UserAction.contentDisplayed.toJson(), + consultationId: consultation!.id, + srcType: "educate-external", + screenType: "journey_details_screen", + flavorId: content.video!.flavor!.flavorId, + topicId: topicId, + contentId: content.id, + contentClassId: contentClassID ?? null, + ), + ); + } + } + } + if (isClicked) { + bool journeyClick = nabedInsertDataPayload.any((item) => item.type == UserAction.contentClick.toJson() && item.contentId == content.id); + if (!journeyClick) { + double percentage = content.viewedPercentage ?? 0.0; + nabedInsertDataPayload.add( + Data( + type: UserAction.contentClick.toJson(), + consultationId: consultation!.id, + srcType: "educate-external", + screenType: "journey_details_screen", + flavorId: content.video!.flavor!.flavorId, + topicId: topicId, + contentId: content.id, + percentage: convertToIntegerPercentage(percentage), + contentClassId: contentClassID ?? null, + ), + ); + } + bool isWatchBefore = nabedInsertDataPayload.any((item) => item.type == UserAction.contentWatch.toJson() && item.contentId == content.id); + if (!isWatchBefore) { + nabedInsertDataPayload.add( + Data( + type: UserAction.contentWatch.toJson(), + consultationId: consultation!.id, + srcType: "educate-external", + screenType: "journey_details_screen", + flavorId: content.video!.flavor!.flavorId, + topicId: topicId, + contentId: content.id, + contentClassId: contentClassID ?? null, + percentage: convertToIntegerPercentage(content.viewedPercentage!), + ), + ); + } else { + for (var dataItem in nabedInsertDataPayload) { + if (dataItem.contentId == content.id && dataItem.type == UserAction.contentWatch.toJson()) { + dataItem.percentage = convertToIntegerPercentage(content.viewedPercentage!); + break; + } + } + } + } + + print("======= Updated Data ============="); + print(jsonEncode(nabedInsertDataPayload)); + } + + void getContentClassID() {} Future clearData() async { - data = courseData = nabedJourneyResponse = selectedJourney = courseTopics = consultation = timer = controller = null; + print("======== Clear Data ======"); + data = courseData = nabedJourneyResponse = courseTopics = consultation = timer = controller = null; + setSelectedJourney = 0; + nabedInsertDataPayload = []; } Future clear() async { + print("==== Clear ======"); if (controller != null) controller!.dispose(); courseData = courseTopics = timer = controller = null; } } -enum VideoPlayerState { playing, paused, completed, loading } - -IconData getVideoIcon(VideoPlayerState state) { - switch (state) { - case VideoPlayerState.loading: - return Icons.hourglass_top; - case VideoPlayerState.playing: - return Icons.pause; - case VideoPlayerState.paused: - return Icons.play_arrow; - case VideoPlayerState.completed: - return Icons.replay; - default: - return Icons.play_arrow; +enum UserAction { + contentDisplayed, + contentClick, + contentWatch, + journeyClick, + journeyDisplayed, +} + +extension UserActionExtension on UserAction { + String toJson() { + switch (this) { + case UserAction.contentDisplayed: + return "content_displayed"; + case UserAction.contentClick: + return "content_click"; + case UserAction.contentWatch: + return "content_watch"; + case UserAction.journeyClick: + return "journey_click"; + case UserAction.journeyDisplayed: + return "journey_displayed"; + } } } diff --git a/pubspec.yaml b/pubspec.yaml index 46ffeafa..a13655b4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -180,6 +180,7 @@ dependencies: win32: ^5.5.4 cloudflare_turnstile: ^2.0.1 + visibility_detector: ^0.4.0+2 dependency_overrides: