nabed implementation

dev_3.24.3_nabed
Aamir 11 months ago
parent 7984aedcd8
commit 4be0fc1cc9

@ -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<String, dynamic> 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<String, dynamic> toJson() => {
"body": body,

@ -576,7 +576,7 @@ class _ToDoState extends State<ToDo> 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);
});
}
}

@ -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<CourseDetailedPage> {
@override
void dispose() {
// _timer?.cancel();
// _controller.dispose();
super.dispose();
}
@ -38,37 +43,48 @@ class _CourseDetailedPageState extends State<CourseDetailedPage> {
isShowDecPage: false,
showNewAppBarTitle: true,
showNewAppBar: true,
appBarTitle: "Course Name",
appBarTitle: context.read<CourseServiceProvider>().getPageTitle ?? "",
backgroundColor: Color(0xFFF7F7F7),
onTap: () {},
body: Consumer<CourseServiceProvider>(
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<CourseDetailedPage> {
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<CourseDetailedPage> {
});
}),
],
if (provider.consultation != null) ...[
ListView(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.symmetric(horizontal: 20),
children: [
SizedBox(height: 20.0),
Text(
context.read<ProjectViewModel>().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<ProjectViewModel>().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 {

@ -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<CourseList> {
CourseServiceProvider? _courseServiceProvider;
GlobalKey _cardKey = GlobalKey();
@override
void initState() {
@ -33,6 +35,7 @@ class _CourseListState extends State<CourseList> {
}
Future<void> _clearData() async {
await _courseServiceProvider!.postNabedJourneyData();
await _courseServiceProvider!.clearData();
}
@ -48,13 +51,13 @@ class _CourseListState extends State<CourseList> {
onTap: () {},
body: Consumer<CourseServiceProvider>(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<model.ContentClass> 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<CourseList> {
),
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<CourseList> {
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<CourseList> {
),
),
).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<CourseList> {
separatorBuilder: (context, index) {
return SizedBox();
},
onScroll: onScroll,
);
} else {
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
Future<QuestionSheetAction?> showQuestionSheet(BuildContext con, {required Content content}) {
return showModalBottomSheet<QuestionSheetAction>(
context: con,
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/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<Datum>? data;
PatientEducationJourneyListModel? nabedJourneyResponse;
PatientEducationJourneyModel? courseData;
List<Topic>? courseTopics;
Consultation? consultation;
List<Data>? playedData = [];
ejm.PatientEducationJourneyModel? courseData;
List<ejm.Topic>? courseTopics;
ejm.Consultation? consultation;
List<ejm.ContentClass>? contentClasses;
//Main Video Controller & Timer
VideoPlayerState _videoState = VideoPlayerState.paused;
List<Data> nabedInsertDataPayload = [];
ejm.Content? playedContent;
VideoPlayerState get videoState => _videoState;
VideoPlayerController? controller;
Timer? timer;
// Learning Page
Future<dynamic> fetchPatientCoursesList() async {
print("====== Api Initiated =========");
void getCourses(BuildContext context) 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;
}
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<dynamic> fetchPatientCourseById() async {
print("====== Api Initiated =========");
void getCourseById(BuildContext context) 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;
}
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<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();
}
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<void> 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<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 {
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";
}
}
}

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

Loading…
Cancel
Save