nabed implementation

dev_3.24.3_nabed
Aamir 1 year ago
parent 7984aedcd8
commit 4be0fc1cc9

@ -451,6 +451,8 @@ class Topic {
}; };
} }
enum VideoPlayerState { playing, paused, completed, loading }
class Content { class Content {
String? body; String? body;
int? id; int? id;
@ -460,24 +462,27 @@ class Content {
String? title; String? title;
Video? video; Video? video;
VideoPlayerController? controller; 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)); factory Content.fromRawJson(String str) => Content.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson()); String toRawJson() => json.encode(toJson());
factory Content.fromJson(Map<String, dynamic> json) => Content( factory Content.fromJson(Map<String, dynamic> json) => Content(
body: json["body"], body: json["body"],
id: json["id"], id: json["id"],
question: json["question"] == null ? null : Question.fromJson(json["question"]), question: json["question"] == null ? null : Question.fromJson(json["question"]),
read: json["read"], read: json["read"],
subjectId: json["subject_id"], subjectId: json["subject_id"],
title: json["title"], title: json["title"],
video: json["video"] == null ? null : Video.fromJson(json["video"]), video: json["video"] == null ? null : Video.fromJson(json["video"]),
controller: null, controller: null,
timer: null); videoState: VideoPlayerState.paused,
viewedPercentage: 0.0,
);
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"body": body, "body": body,

@ -576,7 +576,7 @@ class _ToDoState extends State<ToDo> with SingleTickerProviderStateMixin {
Permission.bluetoothScan, Permission.bluetoothScan,
Permission.activityRecognition, Permission.activityRecognition,
].request().whenComplete(() { ].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);
}); });
} }
} }

@ -3,14 +3,21 @@ import 'dart:async';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:diplomaticquarterapp/core/viewModels/project_view_model.dart'; import 'package:diplomaticquarterapp/core/viewModels/project_view_model.dart';
import 'package:diplomaticquarterapp/extensions/string_extensions.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/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/services/course_service/course_service.dart';
import 'package:diplomaticquarterapp/widgets/others/app_scaffold_widget.dart'; import 'package:diplomaticquarterapp/widgets/others/app_scaffold_widget.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
import 'package:visibility_detector/visibility_detector.dart';
import 'widgets/player_controlls.dart';
class CourseDetailedPage extends StatefulWidget { class CourseDetailedPage extends StatefulWidget {
@override @override
@ -26,8 +33,6 @@ class _CourseDetailedPageState extends State<CourseDetailedPage> {
@override @override
void dispose() { void dispose() {
// _timer?.cancel();
// _controller.dispose();
super.dispose(); super.dispose();
} }
@ -38,37 +43,48 @@ class _CourseDetailedPageState extends State<CourseDetailedPage> {
isShowDecPage: false, isShowDecPage: false,
showNewAppBarTitle: true, showNewAppBarTitle: true,
showNewAppBar: true, showNewAppBar: true,
appBarTitle: "Course Name", appBarTitle: context.read<CourseServiceProvider>().getPageTitle ?? "",
backgroundColor: Color(0xFFF7F7F7), backgroundColor: Color(0xFFF7F7F7),
onTap: () {}, onTap: () {},
body: Consumer<CourseServiceProvider>( body: Consumer<CourseServiceProvider>(
builder: (context, provider, child) { builder: (context, provider, child) {
if (provider.courseData != null) { if (provider.courseData != null) {
return ListView( return Column(
shrinkWrap: true, crossAxisAlignment: CrossAxisAlignment.start,
padding: EdgeInsets.zero,
children: [ children: [
if (provider.controller != null && provider.controller!.value.isInitialized) ...[ if (provider.controller != null && provider.controller!.value.isInitialized) ...[
Column( Column(
children: [ children: [
AspectRatio( AspectRatio(
aspectRatio: provider.controller!.value.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(() { ).onTap(() {
setState(() { 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 ...[ ] else ...[
@ -83,15 +99,25 @@ class _CourseDetailedPageState extends State<CourseDetailedPage> {
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
), ),
child: Center( child: provider.controller != null && !provider.controller!.value.isLooping
child: Icon( ? Center(
Icons.play_circle, child: CircularProgressIndicator(
size: 35, color: Color(0xFFD02127),
color: Colors.white, ),
), )
), : 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), errorWidget: (context, url, error) => Icon(Icons.error),
).onTap(() { ).onTap(() {
setState(() { setState(() {
@ -101,334 +127,184 @@ class _CourseDetailedPageState extends State<CourseDetailedPage> {
}); });
}), }),
], ],
if (provider.consultation != null) ...[ if (provider.consultation != null) ...[
ListView( Padding(
shrinkWrap: true, padding: const EdgeInsets.symmetric(horizontal: 20),
physics: NeverScrollableScrollPhysics(), child: Column(
padding: EdgeInsets.symmetric(horizontal: 20), crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox(height: 20.0), SizedBox(height: 20.0),
Text( Text(
context.read<ProjectViewModel>().isArabic ? provider.consultation!.tagValues!.titleAr! : provider.consultation!.tagValues!.title ?? "Basics of Heart", context.read<ProjectViewModel>().isArabic ? provider.consultation!.tagValues!.titleAr! : provider.consultation!.tagValues!.title ?? "Basics of Heart",
style: TextStyle( style: TextStyle(
fontSize: 16.0, fontSize: 16.0,
height: 24 / 16, height: 24 / 16,
color: Color(0xFF2E303A), color: Color(0xFF2E303A),
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
),
), ),
), // SizedBox(height: 2.0),
// SizedBox(height: 2.0), // Text(
// Text( // "Heart diseases include conditions like coronary and heart failure, often caused by factors ",
// "Heart diseases include conditions like coronary and heart failure, often caused by factors ", // style: TextStyle(fontSize: 10.0, height: 15 / 10, color: Color(0xFF575757)),
// style: TextStyle(fontSize: 10.0, height: 15 / 10, color: Color(0xFF575757)), // ),
// ), // SizedBox(height: 6.0),
// SizedBox(height: 6.0), Text(
Text( provider.consultation!.stats!.videosCount.toString() + " Videos",
provider.consultation!.stats!.videosCount.toString() + " Videos", style: TextStyle(
style: TextStyle( fontSize: 10.0,
fontSize: 10.0, height: 15 / 10,
height: 15 / 10, color: Color(0xFF2E303A),
color: Color(0xFF2E303A), fontWeight: FontWeight.w600,
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) ...[ if (provider.courseTopics != null) ...[
ListView( Expanded(
shrinkWrap: true, child: ListViewSeparatedExtension.separatedWithScrollListener(
physics: NeverScrollableScrollPhysics(), itemCount: provider.courseTopics!.length,
padding: EdgeInsets.symmetric(horizontal: 20), shrinkWrap: true,
children: provider.courseTopics!.map((topic) { physics: AlwaysScrollableScrollPhysics(),
return Column( padding: EdgeInsets.symmetric(horizontal: 20),
crossAxisAlignment: CrossAxisAlignment.start, itemBuilder: (context, ind) {
children: [ Topic topic = provider.courseTopics![ind];
// Topic title return Column(
Text( crossAxisAlignment: CrossAxisAlignment.start,
(topic.title.toString() + "(${topic.contentsCount.toString()})") ?? "Topic", mainAxisSize: MainAxisSize.max,
style: TextStyle( children: [
fontSize: 14, // Topic title
fontWeight: FontWeight.bold, Text(
color: Color(0xFF2B353E), (topic.title.toString() + "(${topic.contentsCount.toString()})") ?? "Topic",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Color(0xFF2B353E),
),
), ),
), SizedBox(height: 8),
SizedBox(height: 8), ListViewSeparatedExtension.separatedWithScrollListener(
...topic.contents!.asMap().entries.map((entry) { itemCount: topic.contents!.length,
final index = entry.key + 1; shrinkWrap: true,
final content = entry.value; physics: NeverScrollableScrollPhysics(),
return Padding( itemBuilder: (context, index) {
padding: const EdgeInsets.symmetric(vertical: 8), final content = topic.contents![index];
child: Row( var percentage = content.viewedPercentage != 0.0 ? content.viewedPercentage : provider.convertToDoublePercentage(content.read ?? 0);
crossAxisAlignment: CrossAxisAlignment.start, return VisibilityDetector(
children: [ key: Key(content.id.toString()),
Text(index.toString()), onVisibilityChanged: (visibilityInfo) => provider.onVisibilityChange(visibilityInfo, content, topic.id!),
SizedBox( child: Padding(
width: 9, padding: const EdgeInsets.symmetric(vertical: 8),
), child: Row(
Expanded(
flex: 10,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(index.toString()),
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: [
SizedBox( SizedBox(
height: 10, width: 9,
), ),
Row( Expanded(
crossAxisAlignment: CrossAxisAlignment.center, flex: 10,
mainAxisAlignment: MainAxisAlignment.end, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
if (content.controller != null) ...[ children: [
Container( Text(
width: 55, content.title ?? "Course overview",
child: VideoProgressIndicator(content.controller!, style: TextStyle(
padding: EdgeInsets.only(bottom: 1), fontSize: 13,
colors: VideoProgressColors( fontWeight: FontWeight.w600,
backgroundColor: Color(0xFFD9D9D9), color: Color(0xFF2B353E),
bufferedColor: Colors.black12, ),
playedColor: Color(0xFF359846), ),
), Text(
allowScrubbing: true), "Video - ${provider.getDurationOfVideo(content.video!.flavor!.duration!)}",
) style: TextStyle(
] else ...[ fontSize: 12,
Container( fontWeight: FontWeight.w500,
width: 55, color: Color(0xFF575757),
child: Container(
height: 4,
color: Colors.black12,
width: double.infinity,
), ),
),
ContentWidget(
body: content.body!,
) )
], ],
SizedBox( ),
width: 10, ),
), Expanded(
if (content.controller != null) ...[ flex: 4,
Container( child: Column(
width: 20, children: [
height: 20, SizedBox(
decoration: BoxDecoration( height: 10,
color: Color(0xFFDFDFDF), ),
borderRadius: BorderRadius.all( Row(
Radius.circular(30), crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.end,
children: [
CustomProgressBar(
playedPercentage: percentage!,
bufferedPercentage: 0.0,
backgroundColor: Color(0xFFD9D9D9),
bufferedColor: Colors.black12,
playedColor: Color(0xFF359846),
), ),
), SizedBox(
child: Icon( width: 10,
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),
), ),
), Container(
child: Icon( width: 20,
Icons.play_arrow, height: 20,
size: 18, decoration: BoxDecoration(
).onTap(() { color: Color(0xFFDFDFDF),
provider.playVideo(context, content: content); 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);
});
}),
)
],
) )
] ],
], ),
), ),
], ],
), ),
), ),
], );
), },
); separatorBuilder: (BuildContext context, int index) {
}).toList(), return SizedBox();
], },
); // onScroll: onScroll,
}).toList(), ),
],
);
},
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 { } else {

@ -4,6 +4,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:diplomaticquarterapp/core/enum/viewstate.dart'; import 'package:diplomaticquarterapp/core/enum/viewstate.dart';
import 'package:diplomaticquarterapp/extensions/string_extensions.dart'; import 'package:diplomaticquarterapp/extensions/string_extensions.dart';
import 'package:diplomaticquarterapp/models/course/education_journey_list_model.dart' as model; 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/routes.dart';
import 'package:diplomaticquarterapp/services/course_service/course_service.dart'; import 'package:diplomaticquarterapp/services/course_service/course_service.dart';
import 'package:diplomaticquarterapp/uitl/gif_loader_dialog_utils.dart'; import 'package:diplomaticquarterapp/uitl/gif_loader_dialog_utils.dart';
@ -18,6 +19,7 @@ class CourseList extends StatefulWidget {
class _CourseListState extends State<CourseList> { class _CourseListState extends State<CourseList> {
CourseServiceProvider? _courseServiceProvider; CourseServiceProvider? _courseServiceProvider;
GlobalKey _cardKey = GlobalKey();
@override @override
void initState() { void initState() {
@ -33,6 +35,7 @@ class _CourseListState extends State<CourseList> {
} }
Future<void> _clearData() async { Future<void> _clearData() async {
await _courseServiceProvider!.postNabedJourneyData();
await _courseServiceProvider!.clearData(); await _courseServiceProvider!.clearData();
} }
@ -48,13 +51,13 @@ class _CourseListState extends State<CourseList> {
onTap: () {}, onTap: () {},
body: Consumer<CourseServiceProvider>(builder: (context, provider, child) { body: Consumer<CourseServiceProvider>(builder: (context, provider, child) {
if (provider.nabedJourneyResponse != null) { if (provider.nabedJourneyResponse != null) {
return ListView.separated( return ListViewSeparatedExtension.separatedWithScrollListener(
itemCount: provider.data!.length, itemCount: provider.data!.length,
padding: EdgeInsets.zero,
itemBuilder: (context, index) { itemBuilder: (context, index) {
List<model.ContentClass> conClass = provider.data![index].contentClasses!; List<model.ContentClass> conClass = provider.data![index].contentClasses!;
model.Datum data = provider.data![index]; model.Datum data = provider.data![index];
return Card( return Card(
key: index == 0 ? _cardKey : null,
margin: EdgeInsets.only(left: 20, right: 20, top: 20), margin: EdgeInsets.only(left: 20, right: 20, top: 20),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
side: BorderSide(width: 1, color: Color(0xFFEFEFEF)), side: BorderSide(width: 1, color: Color(0xFFEFEFEF)),
@ -103,7 +106,8 @@ class _CourseListState extends State<CourseList> {
), ),
SizedBox(height: 6.0), SizedBox(height: 6.0),
Text( Text(
conClass.first.readPercentage.toString() + " Hour Watched", "${data.stats!.videosReadCount} of ${data.stats!.videosCount} Watched",
// conClass.first.readPercentage.toString() + "% Watched",
// "2 Hours", // "2 Hours",
style: TextStyle( style: TextStyle(
fontSize: 10.0, fontSize: 10.0,
@ -112,33 +116,31 @@ class _CourseListState extends State<CourseList> {
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
SizedBox(height: 8.0),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row( Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
Row( Icon(
children: List.generate(5, (starIndex) { Icons.play_arrow,
return Icon(
Icons.star,
color: Color(0xFFD1272D),
size: 15.0,
);
}),
), ),
SizedBox(width: 8.0),
Text( Text(
'540 reviews', "${data.stats!.videosCount} Videos",
// conClass.first.readPercentage.toString() + "% Watched",
// "2 Hours",
style: TextStyle( style: TextStyle(
fontSize: 10.0, fontSize: 10.0,
height: 15 / 10, height: 15 / 10,
color: Color(0xFF2B353E), color: Color(0xFF2E303A),
fontWeight: FontWeight.w700, fontWeight: FontWeight.w600,
), ),
), ),
], ],
), ),
SizedBox(
width: 8,
),
Icon( Icon(
Icons.arrow_right_alt, Icons.arrow_right_alt,
size: 18.0, size: 18.0,
@ -150,8 +152,9 @@ class _CourseListState extends State<CourseList> {
), ),
), ),
).onTap(() { ).onTap(() {
provider.navigate(context, data.id!, onBack: (val) { provider.insertJourneyListData(index, true);
provider.uploadStats(); provider.navigate(context, data.id!, conClass.first.title!, onBack: (val) {
// provider.uploadStats();
provider.clear(); provider.clear();
}); });
}); });
@ -159,6 +162,7 @@ class _CourseListState extends State<CourseList> {
separatorBuilder: (context, index) { separatorBuilder: (context, index) {
return SizedBox(); return SizedBox();
}, },
onScroll: onScroll,
); );
} else { } else {
return SizedBox(); return SizedBox();
@ -166,4 +170,37 @@ class _CourseListState extends State<CourseList> {
}), }),
); );
} }
final Set<int> _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<int> 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;
}
} }

@ -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<MeasureSize> {
@override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
final size = context.size;
if (size != null) widget.onChange(size);
});
return widget.child;
}
}

@ -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),
),
),
],
),
);
}
}

@ -206,6 +206,7 @@ class _ActionButton extends StatelessWidget {
// Modified showQuestionSheet function // Modified showQuestionSheet function
Future<QuestionSheetAction?> showQuestionSheet(BuildContext con, {required Content content}) { Future<QuestionSheetAction?> showQuestionSheet(BuildContext con, {required Content content}) {
return showModalBottomSheet<QuestionSheetAction>( return showModalBottomSheet<QuestionSheetAction>(
context: con, context: con,
isDismissible: false, isDismissible: false,

@ -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<ScrollNotification>(
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,
),
);
}
}

@ -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<PlayerControlsOverlay> createState() => _PlayerControlsOverlayState();
}
class _PlayerControlsOverlayState extends State<PlayerControlsOverlay> {
@override
Widget build(BuildContext context) {
return Consumer<CourseServiceProvider>(builder: (context, provider, child) {
return Stack(
children: <Widget>[
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',
),
),
),
),
],
);
});
}
}

@ -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/Authentication/authenticated_user.dart';
import 'package:diplomaticquarterapp/models/course/education_journey_insert_model.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_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/pages/learning/question_sheet.dart';
import 'package:diplomaticquarterapp/routes.dart'; import 'package:diplomaticquarterapp/routes.dart';
import 'package:diplomaticquarterapp/services/authentication/auth_provider.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:diplomaticquarterapp/uitl/utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
import 'package:visibility_detector/visibility_detector.dart';
class CourseServiceProvider with ChangeNotifier { class CourseServiceProvider with ChangeNotifier {
AppSharedPreferences sharedPref = AppSharedPreferences(); AppSharedPreferences sharedPref = AppSharedPreferences();
@ -22,48 +23,56 @@ class CourseServiceProvider with ChangeNotifier {
AuthenticatedUser authUser = AuthenticatedUser(); AuthenticatedUser authUser = AuthenticatedUser();
AuthProvider authProvider = AuthProvider(); AuthProvider authProvider = AuthProvider();
bool isDataLoaded = false; 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<Datum>? data; List<Datum>? data;
PatientEducationJourneyListModel? nabedJourneyResponse; PatientEducationJourneyListModel? nabedJourneyResponse;
PatientEducationJourneyModel? courseData; ejm.PatientEducationJourneyModel? courseData;
List<Topic>? courseTopics; List<ejm.Topic>? courseTopics;
Consultation? consultation; ejm.Consultation? consultation;
List<ejm.ContentClass>? contentClasses;
List<Data>? playedData = [];
//Main Video Controller & Timer List<Data> nabedInsertDataPayload = [];
VideoPlayerState _videoState = VideoPlayerState.paused; ejm.Content? playedContent;
VideoPlayerState get videoState => _videoState;
VideoPlayerController? controller; VideoPlayerController? controller;
Timer? timer; Timer? timer;
// Learning Page // Learning Page
void getCourses(BuildContext context) async {
Future<dynamic> fetchPatientCoursesList() async { GifLoaderDialogUtils.showMyDialog(context);
print("====== Api Initiated =========");
Map<String, dynamic> request; Map<String, dynamic> request;
if (await this.sharedPref.getObject(USER_PROFILE) != null) { if (await this.sharedPref.getObject(USER_PROFILE) != null) {
var data = AuthenticatedUser.fromJson(await this.sharedPref.getObject(USER_PROFILE)); var data = AuthenticatedUser.fromJson(await this.sharedPref.getObject(USER_PROFILE));
authUser = data; authUser = data;
} }
request = {"Channel": 3, "TokenID": "@dm!n", "PatientID": 22335}; dynamic response;
dynamic localRes; //{"Channel": 3, "TokenID": "@dm!n", "PatientID": 22335};
await BaseAppClient().post(GET_PATIENT_COURSES_LIST, onSuccess: (response, statusCode) async { request = {"Channel": 3};
print("====== Api Response ========="); await BaseAppClient().post(GET_PATIENT_COURSES_LIST, onSuccess: (res, statusCode) async {
print("${response["NabedJourneyResponseResult"]}"); response = PatientEducationJourneyListModel.fromJson(res);
localRes = PatientEducationJourneyListModel.fromJson(response);
}, onFailure: (String error, int statusCode) { }, onFailure: (String error, int statusCode) {
GifLoaderDialogUtils.hideDialog(context);
Utils.showErrorToast(error);
throw error; throw error;
}, body: request); }, body: request);
return localRes;
}
void getCourses(BuildContext context) async {
GifLoaderDialogUtils.showMyDialog(context);
dynamic response = await fetchPatientCoursesList();
GifLoaderDialogUtils.hideDialog(context);
if (response.nabedJourneyResponseResult != null) { if (response.nabedJourneyResponseResult != null) {
GifLoaderDialogUtils.hideDialog(context);
nabedJourneyResponse = response; nabedJourneyResponse = response;
isDataLoaded = true; isDataLoaded = true;
data = nabedJourneyResponse!.nabedJourneyResponseResult!.data; data = nabedJourneyResponse!.nabedJourneyResponseResult!.data;
@ -72,40 +81,64 @@ class CourseServiceProvider with ChangeNotifier {
} }
// Detailed Page // Detailed Page
void getCourseById(BuildContext context) async {
Future<dynamic> fetchPatientCourseById() async { GifLoaderDialogUtils.showMyDialog(context);
print("====== Api Initiated =========");
Map<String, dynamic> request; Map<String, dynamic> request;
if (await this.sharedPref.getObject(USER_PROFILE) != null) { if (await this.sharedPref.getObject(USER_PROFILE) != null) {
var data = AuthenticatedUser.fromJson(await this.sharedPref.getObject(USER_PROFILE)); var data = AuthenticatedUser.fromJson(await this.sharedPref.getObject(USER_PROFILE));
authUser = data; authUser = data;
} }
request = {"Channel": 3, "TokenID": "@dm!n", "JourneyID": selectedJourney}; dynamic response;
dynamic localRes; // "TokenID": "@dm!n",
await BaseAppClient().post(GET_PATIENT_COURSE_BY_ID, onSuccess: (response, statusCode) async { request = {"Channel": 3, "JourneyID": getSelectedJourney};
print("====== Api Response ========="); await BaseAppClient().post(GET_PATIENT_COURSE_BY_ID, onSuccess: (res, statusCode) async {
print("${response}"); response = res;
localRes = response;
}, onFailure: (String error, int statusCode) { }, onFailure: (String error, int statusCode) {
GifLoaderDialogUtils.hideDialog(context);
Utils.showErrorToast(error);
throw error; throw error;
}, body: request); }, body: request);
return localRes;
}
void getCourseById(BuildContext context) async {
GifLoaderDialogUtils.showMyDialog(context);
dynamic response = await fetchPatientCourseById();
GifLoaderDialogUtils.hideDialog(context); GifLoaderDialogUtils.hideDialog(context);
if (response != null) { if (response != null) {
courseData = PatientEducationJourneyModel.fromRawJson(jsonEncode(response)); courseData = ejm.PatientEducationJourneyModel.fromRawJson(jsonEncode(response));
courseTopics = courseData!.nabedJourneyByIdResponseResult!.contentClasses!.first.topics; courseTopics = courseData!.nabedJourneyByIdResponseResult!.contentClasses!.first.topics;
contentClasses = courseData!.nabedJourneyByIdResponseResult!.contentClasses;
consultation = courseData!.nabedJourneyByIdResponseResult!.consultation; consultation = courseData!.nabedJourneyByIdResponseResult!.consultation;
// Future.delayed(Duration(seconds: 1), () {
// insertDetailedJourneyListData();
// });
}
notifyListeners();
}
// Learning Page
Future<void> postNabedJourneyData() async {
// GifLoaderDialogUtils.showMyDialog(context);
Map<String, dynamic> 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(); notifyListeners();
} }
Future navigate(BuildContext context, int journey, {required Function(Object? val) onBack}) async { Future navigate(BuildContext context, int journey, String title, {required Function(Object? val) onBack}) async {
selectedJourney = journey; setSelectedJourney = journey;
setPageTitle = title;
Navigator.of(context).pushNamed(COURSES_DETAILED_PAGE).then((val) { Navigator.of(context).pushNamed(COURSES_DETAILED_PAGE).then((val) {
onBack(val); onBack(val);
}); });
@ -118,84 +151,116 @@ class CourseServiceProvider with ChangeNotifier {
return "$minutes:${seconds.toString().padLeft(2, '0')} mins"; return "$minutes:${seconds.toString().padLeft(2, '0')} mins";
} }
void playVideo(BuildContext context, {required Content content}) async { void play(BuildContext context, {required ejm.Content content}) async {
print("OnTap"); switch (content.videoState) {
if (_videoState == VideoPlayerState.playing) { case ejm.VideoPlayerState.loading:
controller?.pause(); notifyListeners();
setVideoState(VideoPlayerState.paused); break;
} else {
setVideoState(VideoPlayerState.loading);
notifyListeners();
try { case ejm.VideoPlayerState.playing:
final videoUrl = content.video?.flavor?.downloadable ?? ""; controller?.pause();
if (videoUrl.isEmpty) { content.videoState = ejm.VideoPlayerState.paused;
Utils.showErrorToast("No video URL provided."); notifyListeners();
setVideoState(VideoPlayerState.paused); break;
return;
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( void playVideo(BuildContext context, {required ejm.Content content}) async {
Uri.parse(videoUrl), if (controller != null && controller!.value.isPlaying) {
//formatHint: VideoFormat.hls, controller!.pause();
); if (playedContent != null) {
playedContent!.videoState = ejm.VideoPlayerState.paused;
}
removeVideoListener(context);
controller = null;
}
await controller!.initialize(); content.videoState = ejm.VideoPlayerState.loading;
controller!.play(); notifyListeners();
setVideoState(VideoPlayerState.playing); final videoUrl = content.video?.flavor?.downloadable ?? "";
notifyListeners(); 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(() { void removeVideoListener(BuildContext context) {
if (controller!.value.isPlaying && timer == null) { controller?.removeListener(() => videoListener(context));
_startTimer(context, content: content); }
} else if (!controller!.value.isPlaying && timer != null) {
stopTimer();
}
if (controller!.value.hasError) { int? lastProcessedSecond;
Utils.showErrorToast("Failed to load video.");
controller = null; void videoListener(BuildContext context) async {
setVideoState(VideoPlayerState.paused); final currentSecond = controller!.value.position.inSeconds;
notifyListeners(); if (lastProcessedSecond != currentSecond) {
} lastProcessedSecond = currentSecond;
}); Duration currentWatchedTime = controller!.value.position;
} catch (e) { Duration totalVideoTime = controller!.value.duration;
Utils.showErrorToast("Error loading video: $e"); if (totalVideoTime != null) {
controller = null; playedContent!.viewedPercentage = await calculateWatchedPercentage(currentWatchedTime, totalVideoTime);
setVideoState(VideoPlayerState.paused); 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(); notifyListeners();
} }
} }
} }
void setVideoState(VideoPlayerState state) { double calculateWatchedPercentage(Duration currentWatchedTime, Duration totalVideoTime) {
_videoState = state; if (totalVideoTime.inSeconds == 0) {
notifyListeners(); 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}) { int convertToIntegerPercentage(double percentage) {
timer = Timer.periodic(Duration(seconds: 1), (timer) async { return (percentage.clamp(0.0, 1.0) * 99 + 1).toInt();
if (controller != null && _videoState == VideoPlayerState.playing) { }
final position = await controller!.position;
if (position != null) { double convertToDoublePercentage(int value) {
print("Current position: ${position.inSeconds} seconds"); int clampedValue = value.clamp(1, 100);
notifyListeners(); return (clampedValue - 1) / 99.0;
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);
}
}
}
}
});
} }
void stopTimer() { void stopTimer() {
@ -204,148 +269,163 @@ class CourseServiceProvider with ChangeNotifier {
} }
Future<void> uploadStats() async { Future<void> uploadStats() async {
// if(courseTopics != null) {
// Future.forEach(courseTopics!, (topic) {
// Future.forEach(topic.contents!, (content){
// content.controller
// });
// });
// }
if (courseTopics != null) { if (courseTopics != null) {
await Future.forEach(courseTopics!, (topic) async { nabedInsertDataPayload = [];
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",
));
});
});
} }
print("==============="); print("===============");
print(playedData.toString()); print(nabedInsertDataPayload.toString());
print("==============="); print("===============");
} }
// void playVideo(BuildContext context, {required Content content}) async { void insertJourneyListData(int index, bool isClicked) {
// print("OnTap"); if (index >= 0 && index < data!.length) {
// if (_videoState == VideoPlayerState.playing) { var datum = data![index];
// controller?.pause(); bool journeyDisplayed = nabedInsertDataPayload.any((dataItem) => dataItem.type == UserAction.journeyDisplayed.toJson() && dataItem.consultationId == datum.id);
// setVideoState(VideoPlayerState.paused); if (!journeyDisplayed) {
// } else { nabedInsertDataPayload.add(
// try { Data(
// controller = VideoPlayerController.networkUrl( type: UserAction.journeyDisplayed.toJson(),
// Uri.parse( consultationId: datum.id,
// Platform.isIOS ? content.video!.flavor!.downloadable! : content.video!.flavor!.downloadable!, srcType: "educate-external",
// ), screenType: "journey_listing_screen",
// formatHint: VideoFormat.hls) ),
// ..initialize() );
// ..play(); }
// notifyListeners(); if (isClicked) {
// content.controller = controller; bool journeyClick = nabedInsertDataPayload.any((dataItem) => dataItem.type == UserAction.journeyClick.toJson() && dataItem.consultationId == datum.id);
// notifyListeners(); if (!journeyClick) {
// controller!.addListener(() { nabedInsertDataPayload.add(
// if (controller!.value.isPlaying && timer == null) { Data(
// startTimer(context, content: content); type: UserAction.journeyClick.toJson(),
// notifyListeners(); consultationId: datum.id,
// } else if (!controller!.value.isPlaying && timer != null) { srcType: "educate-external",
// stopTimer(); screenType: "journey_listing_screen",
// notifyListeners(); ),
// } );
// }); }
// }
// controller!.addListener(() { print(jsonEncode(nabedInsertDataPayload));
// if (controller!.value.hasError) { } else {
// Utils.showErrorToast("Failed to load video."); print("Index $index is out of bounds. Please provide a valid index.");
// controller = null; }
// setVideoState(VideoPlayerState.paused); }
// notifyListeners();
// } void onVisibilityChange(VisibilityInfo? visibilityInfo, ejm.Content content, int topicId, {bool isClicked = false}) {
// }); int? contentClassID;
// notifyListeners(); for (var data in contentClasses!) {
// } catch (e) { for (var topic in data.topics!) {
// Utils.showErrorToast("Error loading video: $e"); for (var con in topic.contents!) {
// controller = null; if (con.id == content.id) {
// setVideoState(VideoPlayerState.paused); contentClassID = data.id;
// notifyListeners(); }
// } }
// controller?.play(); }
// setVideoState(VideoPlayerState.playing); }
// }
// } if (!isClicked) {
// var visiblePercentage = visibilityInfo!.visibleFraction * 100;
// if (visiblePercentage == 100) {
// void setVideoState(VideoPlayerState state) { bool isAddedBefore = nabedInsertDataPayload.any((dataItem) => dataItem.type == UserAction.contentDisplayed.toJson() && dataItem.contentId == content.id);
// _videoState = state; if (!isAddedBefore) {
// notifyListeners(); nabedInsertDataPayload.add(
// } Data(
// type: UserAction.contentDisplayed.toJson(),
// void startTimer(BuildContext context, {required Content content}) { consultationId: consultation!.id,
// timer = Timer.periodic(Duration(seconds: 1), (timer) async { srcType: "educate-external",
// if (controller != null && _videoState == VideoPlayerState.playing) { screenType: "journey_details_screen",
// final position = await controller!.position; flavorId: content.video!.flavor!.flavorId,
// if (position != null) { topicId: topicId,
// print("Current position: ${position.inSeconds} seconds"); contentId: content.id,
// if (position.inSeconds == content.question!.triggerAt) { contentClassId: contentClassID ?? null,
// 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"); if (isClicked) {
// controller!.play(); bool journeyClick = nabedInsertDataPayload.any((item) => item.type == UserAction.contentClick.toJson() && item.contentId == content.id);
// } else if (action == QuestionSheetAction.rewatch) { if (!journeyClick) {
// print("Re-watch"); double percentage = content.viewedPercentage ?? 0.0;
// playVideo(context, content: content); nabedInsertDataPayload.add(
// } Data(
// } type: UserAction.contentClick.toJson(),
// notifyListeners(); consultationId: consultation!.id,
// } srcType: "educate-external",
// } screenType: "journey_details_screen",
// }); flavorId: content.video!.flavor!.flavorId,
// } topicId: topicId,
// contentId: content.id,
// void stopTimer() { percentage: convertToIntegerPercentage(percentage),
// timer?.cancel(); contentClassId: contentClassID ?? null,
// timer = null; ),
// } );
}
// void onComplete() { bool isWatchBefore = nabedInsertDataPayload.any((item) => item.type == UserAction.contentWatch.toJson() && item.contentId == content.id);
// stopTimer(); if (!isWatchBefore) {
// setVideoState(VideoPlayerState.completed); 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<void> clearData() async { Future<void> 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<void> clear() async { Future<void> clear() async {
print("==== Clear ======");
if (controller != null) controller!.dispose(); if (controller != null) controller!.dispose();
courseData = courseTopics = timer = controller = null; courseData = courseTopics = timer = controller = null;
} }
} }
enum VideoPlayerState { playing, paused, completed, loading } enum UserAction {
contentDisplayed,
IconData getVideoIcon(VideoPlayerState state) { contentClick,
switch (state) { contentWatch,
case VideoPlayerState.loading: journeyClick,
return Icons.hourglass_top; journeyDisplayed,
case VideoPlayerState.playing: }
return Icons.pause;
case VideoPlayerState.paused: extension UserActionExtension on UserAction {
return Icons.play_arrow; String toJson() {
case VideoPlayerState.completed: switch (this) {
return Icons.replay; case UserAction.contentDisplayed:
default: return "content_displayed";
return Icons.play_arrow; case UserAction.contentClick:
return "content_click";
case UserAction.contentWatch:
return "content_watch";
case UserAction.journeyClick:
return "journey_click";
case UserAction.journeyDisplayed:
return "journey_displayed";
}
} }
} }

@ -180,6 +180,7 @@ dependencies:
win32: ^5.5.4 win32: ^5.5.4
cloudflare_turnstile: ^2.0.1 cloudflare_turnstile: ^2.0.1
visibility_detector: ^0.4.0+2
dependency_overrides: dependency_overrides:

Loading…
Cancel
Save