diff --git a/assets/images/course1.png b/assets/images/course1.png new file mode 100644 index 00000000..92b7ff50 Binary files /dev/null and b/assets/images/course1.png differ diff --git a/assets/images/course2.png b/assets/images/course2.png new file mode 100644 index 00000000..589c2f02 Binary files /dev/null and b/assets/images/course2.png differ diff --git a/lib/config/config.dart b/lib/config/config.dart index d025d082..3f234978 100644 --- a/lib/config/config.dart +++ b/lib/config/config.dart @@ -22,8 +22,8 @@ var PACKAGES_ORDER_HISTORY = '/api/orders/items'; var PACKAGES_TAMARA_OPT = '/api/orders/paymentoptions/tamara'; // var BASE_URL = 'http://10.50.100.198:2018/'; // var BASE_URL = 'http://10.50.100.198:4422/'; -// var BASE_URL = 'https://uat.hmgwebservices.com/'; -var BASE_URL = 'https://hmgwebservices.com/'; + var BASE_URL = 'https://uat.hmgwebservices.com/'; +// var BASE_URL = 'https://hmgwebservices.com/'; // var BASE_URL = 'http://10.20.200.111:1010/'; // var BASE_URL = 'https://orash.cloudsolutions.com.sa/'; // var BASE_URL = 'https://vidauat.cloudsolutions.com.sa/'; @@ -685,6 +685,11 @@ var applePayMerchantId = "merchant.com.hmgwebservices"; // var payFortEnvironment = FortEnvironment.test; // var applePayMerchantId = "merchant.com.hmgwebservices.uat"; +// Patient Courses +var GET_PATIENT_COURSES_LIST = "Services/Nabed.svc/REST/PatientEducationalJourney"; +var GET_PATIENT_COURSE_BY_ID = "Services/Nabed.svc/REST/PatientEducationalJourneyByID"; +var INSERT_PATIENT_COURSE_VIEW_STATS = "Services/Nabed.svc/REST/InsertJourneyStatistics"; + class AppGlobal { static var context; diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index df9cb4f6..b6dd3b5b 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'package:crypto/crypto.dart'; +import 'package:flutter/material.dart'; extension CapExtension on String { String get toCamelCase => "${this[0].toUpperCase()}${this.substring(1)}"; @@ -17,3 +18,13 @@ extension HashSha on String { return sha256.convert(bytes).toString(); } } + +extension OnTapWidget on Widget { + Widget onTap(VoidCallback onTap, {Key? key}) { + return GestureDetector( + key: key, + onTap: onTap, + child: this, // This refers to the widget on which the extension is called + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index a0a584a5..ed6f9f17 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -23,6 +23,7 @@ import 'core/viewModels/pharmacyModule/OrderPreviewViewModel.dart'; import 'core/viewModels/project_view_model.dart'; import 'locator.dart'; import 'pages/pharmacies/compare-list.dart'; +import 'services/course_service/course_service.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -55,8 +56,6 @@ class _MyApp extends State { //0567184134 mobile //246305493 - - // checkForUpdate() { // // todo need to verify 'imp' // InAppUpdate.checkForUpdate().then((info) { @@ -118,6 +117,9 @@ class _MyApp extends State { ChangeNotifierProvider( create: (context) => SearchProvider(), ), + ChangeNotifierProvider( + create: (context) => CourseServiceProvider(), + ), ChangeNotifierProvider.value( value: SearchProvider(), ), @@ -140,14 +142,12 @@ class _MyApp extends State { ], child: Consumer( builder: (context, projectProvider, child) => MaterialApp( - builder: (_, mchild) { return MediaQuery( - data: MediaQuery.of(context).copyWith( - textScaler: TextScaler.linear(1.0), - ), //set desired text scale factor here - child: mchild! - ); + data: MediaQuery.of(context).copyWith( + textScaler: TextScaler.linear(1.0), + ), //set desired text scale factor here + child: mchild!); // Container( // color: Colors.blue, // )); diff --git a/lib/models/course/education_journey_list_model.dart b/lib/models/course/education_journey_list_model.dart new file mode 100644 index 00000000..7feadf42 --- /dev/null +++ b/lib/models/course/education_journey_list_model.dart @@ -0,0 +1,333 @@ +// To parse this JSON data, do +// +// final patientEducationJourneyListModel = patientEducationJourneyListModelFromJson(jsonString); + +import 'dart:convert'; + +PatientEducationJourneyListModel patientEducationJourneyListModelFromJson(String str) => PatientEducationJourneyListModel.fromJson(json.decode(str)); + +String patientEducationJourneyListModelToJson(PatientEducationJourneyListModel data) => json.encode(data.toJson()); + +class PatientEducationJourneyListModel { + NabedJourneyResponseResult? nabedJourneyResponseResult; + + PatientEducationJourneyListModel({ + this.nabedJourneyResponseResult, + }); + + factory PatientEducationJourneyListModel.fromJson(Map json) => PatientEducationJourneyListModel( + nabedJourneyResponseResult: json["NabedJourneyResponseResult"] == null ? null : NabedJourneyResponseResult.fromJson(json["NabedJourneyResponseResult"]), + ); + + Map toJson() => { + "NabedJourneyResponseResult": nabedJourneyResponseResult?.toJson(), + }; +} + +class NabedJourneyResponseResult { + List? data; + Links? links; + Meta? meta; + + NabedJourneyResponseResult({ + this.data, + this.links, + this.meta, + }); + + factory NabedJourneyResponseResult.fromJson(Map json) => NabedJourneyResponseResult( + data: json["data"] == null ? [] : List.from(json["data"]!.map((x) => Datum.fromJson(x))), + links: json["links"] == null ? null : Links.fromJson(json["links"]), + meta: json["meta"] == null ? null : Meta.fromJson(json["meta"]), + ); + + Map toJson() => { + "data": data == null ? [] : List.from(data!.map((x) => x.toJson())), + "links": links?.toJson(), + "meta": meta?.toJson(), + }; +} + +class Datum { + List? contentClasses; + DateTime? date; + DefaultPocData? defaultPocData; + int? id; + String? integrationApproach; + bool? isNew; + DateTime? openedAt; + String? sourceType; + Stats? stats; + TagValues? tagValues; + + Datum({ + this.contentClasses, + this.date, + this.defaultPocData, + this.id, + this.integrationApproach, + this.isNew, + this.openedAt, + this.sourceType, + this.stats, + this.tagValues, + }); + + factory Datum.fromJson(Map json) => Datum( + contentClasses: json["content_classes"] == null ? [] : List.from(json["content_classes"]!.map((x) => ContentClass.fromJson(x))), + date: json["date"] == null ? null : DateTime.parse(json["date"]), + defaultPocData: json["default_poc_data"] == null ? null : DefaultPocData.fromJson(json["default_poc_data"]), + id: json["id"], + integrationApproach: json["integration_approach"], + isNew: json["is_new"], + openedAt: json["opened_at"] == null ? null : DateTime.parse(json["opened_at"]), + sourceType: json["source_type"], + stats: json["stats"] == null ? null : Stats.fromJson(json["stats"]), + tagValues: json["tag_values"] == null ? null : TagValues.fromJson(json["tag_values"]), + ); + + Map toJson() => { + "content_classes": contentClasses == null ? [] : List.from(contentClasses!.map((x) => x.toJson())), + "date": "${date!.year.toString().padLeft(4, '0')}-${date!.month.toString().padLeft(2, '0')}-${date!.day.toString().padLeft(2, '0')}", + "default_poc_data": defaultPocData?.toJson(), + "id": id, + "integration_approach": integrationApproach, + "is_new": isNew, + "opened_at": openedAt?.toIso8601String(), + "source_type": sourceType, + "stats": stats?.toJson(), + "tag_values": tagValues?.toJson(), + }; +} + +class ContentClass { + int? id; + Image? image; + int? readPercentage; + String? title; + dynamic topics; + String? type; + + ContentClass({ + this.id, + this.image, + this.readPercentage, + this.title, + this.topics, + this.type, + }); + + factory ContentClass.fromJson(Map json) => ContentClass( + id: json["id"], + image: json["image"] == null ? null : Image.fromJson(json["image"]), + readPercentage: json["read_percentage"], + title: json["title"], + topics: json["topics"], + type: json["type"], + ); + + Map toJson() => { + "id": id, + "image": image?.toJson(), + "read_percentage": readPercentage, + "title": title, + "topics": topics, + "type": type, + }; +} + +class Image { + String? thumbUrl; + String? url; + + Image({ + this.thumbUrl, + this.url, + }); + + factory Image.fromJson(Map json) => Image( + thumbUrl: json["thumb_url"], + url: json["url"], + ); + + Map toJson() => { + "thumb_url": thumbUrl, + "url": url, + }; +} + +class DefaultPocData { + Image? image; + String? title; + + DefaultPocData({ + this.image, + this.title, + }); + + factory DefaultPocData.fromJson(Map json) => DefaultPocData( + image: json["image"] == null ? null : Image.fromJson(json["image"]), + title: json["title"], + ); + + Map toJson() => { + "image": image?.toJson(), + "title": title, + }; +} + +class Stats { + int? attachmentsCount; + int? contentCount; + int? contentReadCount; + int? educationalContentCount; + int? educationalContentReadCount; + int? educationalReadPercentage; + bool? isCompleted; + int? readPercentage; + int? readRemainingPercentage; + int? screenshotsCount; + int? topicCount; + int? videosCount; + int? videosReadCount; + + Stats({ + this.attachmentsCount, + this.contentCount, + this.contentReadCount, + this.educationalContentCount, + this.educationalContentReadCount, + this.educationalReadPercentage, + this.isCompleted, + this.readPercentage, + this.readRemainingPercentage, + this.screenshotsCount, + this.topicCount, + this.videosCount, + this.videosReadCount, + }); + + factory Stats.fromJson(Map json) => Stats( + attachmentsCount: json["attachments_count"], + contentCount: json["content_count"], + contentReadCount: json["content_read_count"], + educationalContentCount: json["educational_content_count"], + educationalContentReadCount: json["educational_content_read_count"], + educationalReadPercentage: json["educational_read_percentage"], + isCompleted: json["is_completed"], + readPercentage: json["read_percentage"], + readRemainingPercentage: json["read_remaining_percentage"], + screenshotsCount: json["screenshots_count"], + topicCount: json["topic_count"], + videosCount: json["videos_count"], + videosReadCount: json["videos_read_count"], + ); + + Map toJson() => { + "attachments_count": attachmentsCount, + "content_count": contentCount, + "content_read_count": contentReadCount, + "educational_content_count": educationalContentCount, + "educational_content_read_count": educationalContentReadCount, + "educational_read_percentage": educationalReadPercentage, + "is_completed": isCompleted, + "read_percentage": readPercentage, + "read_remaining_percentage": readRemainingPercentage, + "screenshots_count": screenshotsCount, + "topic_count": topicCount, + "videos_count": videosCount, + "videos_read_count": videosReadCount, + }; +} + +class TagValues { + String? consultationCode; + String? title; + String? titleAr; + + TagValues({ + this.consultationCode, + this.title, + this.titleAr, + }); + + factory TagValues.fromJson(Map json) => TagValues( + consultationCode: json["consultation_code"], + title: json["title"], + titleAr: json["title_ar"], + ); + + Map toJson() => { + "consultation_code": consultationCode, + "title": title, + "title_ar": titleAr, + }; +} + +class Links { + String? first; + String? last; + dynamic next; + dynamic prev; + + Links({ + this.first, + this.last, + this.next, + this.prev, + }); + + factory Links.fromJson(Map json) => Links( + first: json["first"], + last: json["last"], + next: json["next"], + prev: json["prev"], + ); + + Map toJson() => { + "first": first, + "last": last, + "next": next, + "prev": prev, + }; +} + +class Meta { + int? currentPage; + int? from; + int? lastPage; + String? path; + int? perPage; + int? to; + int? total; + + Meta({ + this.currentPage, + this.from, + this.lastPage, + this.path, + this.perPage, + this.to, + this.total, + }); + + factory Meta.fromJson(Map json) => Meta( + currentPage: json["current_page"], + from: json["from"], + lastPage: json["last_page"], + path: json["path"], + perPage: json["per_page"], + to: json["to"], + total: json["total"], + ); + + Map toJson() => { + "current_page": currentPage, + "from": from, + "last_page": lastPage, + "path": path, + "per_page": perPage, + "to": to, + "total": total, + }; +} diff --git a/lib/models/course/education_journey_model.dart b/lib/models/course/education_journey_model.dart new file mode 100644 index 00000000..18b638a4 --- /dev/null +++ b/lib/models/course/education_journey_model.dart @@ -0,0 +1,619 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:video_player/video_player.dart'; + +class PatientEducationJourneyModel { + dynamic date; + int? languageId; + int? serviceName; + dynamic time; + dynamic androidLink; + dynamic authenticationTokenId; + dynamic data; + bool? dataw; + int? dietType; + int? dietTypeId; + dynamic errorCode; + dynamic errorEndUserMessage; + dynamic errorEndUserMessageN; + dynamic errorMessage; + int? errorStatusCode; + int? errorType; + int? foodCategory; + dynamic iosLink; + bool? isAuthenticated; + int? mealOrderStatus; + int? mealType; + int? messageStatus; + int? numberOfResultRecords; + dynamic patientBlodType; + dynamic successMsg; + dynamic successMsgN; + dynamic vidaUpdatedResponse; + NabedJourneyByIdResponseResult? nabedJourneyByIdResponseResult; + dynamic nabedJourneyResponseResult; + dynamic nabedPatientList; + dynamic nabedResponse; + + PatientEducationJourneyModel({ + this.date, + this.languageId, + this.serviceName, + this.time, + this.androidLink, + this.authenticationTokenId, + this.data, + this.dataw, + this.dietType, + this.dietTypeId, + this.errorCode, + this.errorEndUserMessage, + this.errorEndUserMessageN, + this.errorMessage, + this.errorStatusCode, + this.errorType, + this.foodCategory, + this.iosLink, + this.isAuthenticated, + this.mealOrderStatus, + this.mealType, + this.messageStatus, + this.numberOfResultRecords, + this.patientBlodType, + this.successMsg, + this.successMsgN, + this.vidaUpdatedResponse, + this.nabedJourneyByIdResponseResult, + this.nabedJourneyResponseResult, + this.nabedPatientList, + this.nabedResponse, + }); + + factory PatientEducationJourneyModel.fromRawJson(String str) => PatientEducationJourneyModel.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory PatientEducationJourneyModel.fromJson(Map json) => PatientEducationJourneyModel( + date: json["Date"], + languageId: json["LanguageID"], + serviceName: json["ServiceName"], + time: json["Time"], + androidLink: json["AndroidLink"], + authenticationTokenId: json["AuthenticationTokenID"], + data: json["Data"], + dataw: json["Dataw"], + dietType: json["DietType"], + dietTypeId: json["DietTypeID"], + errorCode: json["ErrorCode"], + errorEndUserMessage: json["ErrorEndUserMessage"], + errorEndUserMessageN: json["ErrorEndUserMessageN"], + errorMessage: json["ErrorMessage"], + errorStatusCode: json["ErrorStatusCode"], + errorType: json["ErrorType"], + foodCategory: json["FoodCategory"], + iosLink: json["IOSLink"], + isAuthenticated: json["IsAuthenticated"], + mealOrderStatus: json["MealOrderStatus"], + mealType: json["MealType"], + messageStatus: json["MessageStatus"], + numberOfResultRecords: json["NumberOfResultRecords"], + patientBlodType: json["PatientBlodType"], + successMsg: json["SuccessMsg"], + successMsgN: json["SuccessMsgN"], + vidaUpdatedResponse: json["VidaUpdatedResponse"], + nabedJourneyByIdResponseResult: json["NabedJourneyByIdResponseResult"] == null ? null : NabedJourneyByIdResponseResult.fromJson(json["NabedJourneyByIdResponseResult"]), + nabedJourneyResponseResult: json["NabedJourneyResponseResult"], + nabedPatientList: json["NabedPatientList"], + nabedResponse: json["NabedResponse"], + ); + + Map toJson() => { + "Date": date, + "LanguageID": languageId, + "ServiceName": serviceName, + "Time": time, + "AndroidLink": androidLink, + "AuthenticationTokenID": authenticationTokenId, + "Data": data, + "Dataw": dataw, + "DietType": dietType, + "DietTypeID": dietTypeId, + "ErrorCode": errorCode, + "ErrorEndUserMessage": errorEndUserMessage, + "ErrorEndUserMessageN": errorEndUserMessageN, + "ErrorMessage": errorMessage, + "ErrorStatusCode": errorStatusCode, + "ErrorType": errorType, + "FoodCategory": foodCategory, + "IOSLink": iosLink, + "IsAuthenticated": isAuthenticated, + "MealOrderStatus": mealOrderStatus, + "MealType": mealType, + "MessageStatus": messageStatus, + "NumberOfResultRecords": numberOfResultRecords, + "PatientBlodType": patientBlodType, + "SuccessMsg": successMsg, + "SuccessMsgN": successMsgN, + "VidaUpdatedResponse": vidaUpdatedResponse, + "NabedJourneyByIdResponseResult": nabedJourneyByIdResponseResult?.toJson(), + "NabedJourneyResponseResult": nabedJourneyResponseResult, + "NabedPatientList": nabedPatientList, + "NabedResponse": nabedResponse, + }; +} + +class NabedJourneyByIdResponseResult { + Consultation? consultation; + List? contentClasses; + + NabedJourneyByIdResponseResult({ + this.consultation, + this.contentClasses, + }); + + factory NabedJourneyByIdResponseResult.fromRawJson(String str) => NabedJourneyByIdResponseResult.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory NabedJourneyByIdResponseResult.fromJson(Map json) => NabedJourneyByIdResponseResult( + consultation: json["consultation"] == null ? null : Consultation.fromJson(json["consultation"]), + contentClasses: json["content_classes"] == null ? [] : List.from(json["content_classes"]!.map((x) => ContentClass.fromJson(x))), + ); + + Map toJson() => { + "consultation": consultation?.toJson(), + "content_classes": contentClasses == null ? [] : List.from(contentClasses!.map((x) => x.toJson())), + }; +} + +class Consultation { + DateTime? date; + DefaultPocData? defaultPocData; + int? id; + String? integrationApproach; + bool? isNew; + String? openedAt; + String? sourceType; + Stats? stats; + TagValues? tagValues; + + Consultation({ + this.date, + this.defaultPocData, + this.id, + this.integrationApproach, + this.isNew, + this.openedAt, + this.sourceType, + this.stats, + this.tagValues, + }); + + factory Consultation.fromRawJson(String str) => Consultation.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory Consultation.fromJson(Map json) => Consultation( + date: json["date"] == null ? null : DateTime.parse(json["date"]), + defaultPocData: json["default_poc_data"] == null ? null : DefaultPocData.fromJson(json["default_poc_data"]), + id: json["id"], + integrationApproach: json["integration_approach"], + isNew: json["is_new"], + openedAt: json["opened_at"], + sourceType: json["source_type"], + stats: json["stats"] == null ? null : Stats.fromJson(json["stats"]), + tagValues: json["tag_values"] == null ? null : TagValues.fromJson(json["tag_values"]), + ); + + Map toJson() => { + "date": "${date!.year.toString().padLeft(4, '0')}-${date!.month.toString().padLeft(2, '0')}-${date!.day.toString().padLeft(2, '0')}", + "default_poc_data": defaultPocData?.toJson(), + "id": id, + "integration_approach": integrationApproach, + "is_new": isNew, + "opened_at": openedAt, + "source_type": sourceType, + "stats": stats?.toJson(), + "tag_values": tagValues?.toJson(), + }; +} + +class DefaultPocData { + Image? image; + String? title; + + DefaultPocData({ + this.image, + this.title, + }); + + factory DefaultPocData.fromRawJson(String str) => DefaultPocData.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory DefaultPocData.fromJson(Map json) => DefaultPocData( + image: json["image"] == null ? null : Image.fromJson(json["image"]), + title: json["title"], + ); + + Map toJson() => { + "image": image?.toJson(), + "title": title, + }; +} + +class Image { + String? thumbUrl; + String? url; + + Image({ + this.thumbUrl, + this.url, + }); + + factory Image.fromRawJson(String str) => Image.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory Image.fromJson(Map json) => Image( + thumbUrl: json["thumb_url"], + url: json["url"], + ); + + Map toJson() => { + "thumb_url": thumbUrl, + "url": url, + }; +} + +class Stats { + int? attachmentsCount; + int? contentCount; + int? contentReadCount; + int? educationalContentCount; + int? educationalContentReadCount; + int? educationalReadPercentage; + bool? isCompleted; + int? readPercentage; + int? readRemainingPercentage; + int? screenshotsCount; + int? topicCount; + int? videosCount; + int? videosReadCount; + + Stats({ + this.attachmentsCount, + this.contentCount, + this.contentReadCount, + this.educationalContentCount, + this.educationalContentReadCount, + this.educationalReadPercentage, + this.isCompleted, + this.readPercentage, + this.readRemainingPercentage, + this.screenshotsCount, + this.topicCount, + this.videosCount, + this.videosReadCount, + }); + + factory Stats.fromRawJson(String str) => Stats.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory Stats.fromJson(Map json) => Stats( + attachmentsCount: json["attachments_count"], + contentCount: json["content_count"], + contentReadCount: json["content_read_count"], + educationalContentCount: json["educational_content_count"], + educationalContentReadCount: json["educational_content_read_count"], + educationalReadPercentage: json["educational_read_percentage"], + isCompleted: json["is_completed"], + readPercentage: json["read_percentage"], + readRemainingPercentage: json["read_remaining_percentage"], + screenshotsCount: json["screenshots_count"], + topicCount: json["topic_count"], + videosCount: json["videos_count"], + videosReadCount: json["videos_read_count"], + ); + + Map toJson() => { + "attachments_count": attachmentsCount, + "content_count": contentCount, + "content_read_count": contentReadCount, + "educational_content_count": educationalContentCount, + "educational_content_read_count": educationalContentReadCount, + "educational_read_percentage": educationalReadPercentage, + "is_completed": isCompleted, + "read_percentage": readPercentage, + "read_remaining_percentage": readRemainingPercentage, + "screenshots_count": screenshotsCount, + "topic_count": topicCount, + "videos_count": videosCount, + "videos_read_count": videosReadCount, + }; +} + +class TagValues { + String? consultationCode; + String? title; + String? titleAr; + + TagValues({ + this.consultationCode, + this.title, + this.titleAr, + }); + + factory TagValues.fromRawJson(String str) => TagValues.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory TagValues.fromJson(Map json) => TagValues( + consultationCode: json["consultation_code"], + title: json["title"], + titleAr: json["title_ar"], + ); + + Map toJson() => { + "consultation_code": consultationCode, + "title": title, + "title_ar": titleAr, + }; +} + +class ContentClass { + int? id; + Image? image; + int? readPercentage; + String? title; + List? topics; + String? type; + + ContentClass({ + this.id, + this.image, + this.readPercentage, + this.title, + this.topics, + this.type, + }); + + factory ContentClass.fromRawJson(String str) => ContentClass.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory ContentClass.fromJson(Map json) => ContentClass( + id: json["id"], + image: json["image"] == null ? null : Image.fromJson(json["image"]), + readPercentage: json["read_percentage"], + title: json["title"], + topics: json["topics"] == null ? [] : List.from(json["topics"]!.map((x) => Topic.fromJson(x))), + type: json["type"], + ); + + Map toJson() => { + "id": id, + "image": image?.toJson(), + "read_percentage": readPercentage, + "title": title, + "topics": topics == null ? [] : List.from(topics!.map((x) => x.toJson())), + "type": type, + }; +} + +class Topic { + List? contents; + int? contentsCount; + int? id; + Image? image; + int? readContentsCount; + int? readPercentage; + String? subjectId; + String? title; + + Topic({ + this.contents, + this.contentsCount, + this.id, + this.image, + this.readContentsCount, + this.readPercentage, + this.subjectId, + this.title, + }); + + factory Topic.fromRawJson(String str) => Topic.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory Topic.fromJson(Map json) => Topic( + contents: json["contents"] == null ? [] : List.from(json["contents"]!.map((x) => Content.fromJson(x))), + contentsCount: json["contents_count"], + id: json["id"], + image: json["image"] == null ? null : Image.fromJson(json["image"]), + readContentsCount: json["read_contents_count"], + readPercentage: json["read_percentage"], + subjectId: json["subject_id"], + title: json["title"], + ); + + Map toJson() => { + "contents": contents == null ? [] : List.from(contents!.map((x) => x.toJson())), + "contents_count": contentsCount, + "id": id, + "image": image?.toJson(), + "read_contents_count": readContentsCount, + "read_percentage": readPercentage, + "subject_id": subjectId, + "title": title, + }; +} + +class Content { + String? body; + int? id; + Question? question; + dynamic read; + String? subjectId; + String? title; + Video? video; + VideoPlayerController? controller; + Timer? timer; + + Content({this.body, this.id, this.question, this.read, this.subjectId, this.title, this.video, this.controller, this.timer}); + + factory Content.fromRawJson(String str) => Content.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory Content.fromJson(Map json) => Content( + body: json["body"], + id: json["id"], + question: json["question"] == null ? null : Question.fromJson(json["question"]), + read: json["read"], + subjectId: json["subject_id"], + title: json["title"], + video: json["video"] == null ? null : Video.fromJson(json["video"]), + controller: null, + timer: null); + + Map toJson() => { + "body": body, + "id": id, + "question": question?.toJson(), + "read": read, + "subject_id": subjectId, + "title": title, + "video": video?.toJson(), + }; +} + +class Question { + List? answers; + int? id; + bool? shouldAnswer; + String? subjectId; + String? text; + int? triggerAt; + + Question({ + this.answers, + this.id, + this.shouldAnswer, + this.subjectId, + this.text, + this.triggerAt, + }); + + factory Question.fromRawJson(String str) => Question.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory Question.fromJson(Map json) => Question( + answers: json["answers"] == null ? [] : List.from(json["answers"]!.map((x) => Answer.fromJson(x))), + id: json["id"], + shouldAnswer: json["should_answer"], + subjectId: json["subject_id"], + text: json["text"], + triggerAt: json["trigger_at"], + ); + + Map toJson() => { + "answers": answers == null ? [] : List.from(answers!.map((x) => x.toJson())), + "id": id, + "should_answer": shouldAnswer, + "subject_id": subjectId, + "text": text, + "trigger_at": triggerAt, + }; +} + +class Answer { + String? description; + int? id; + bool? isCorrect; + String? text; + + Answer({ + this.description, + this.id, + this.isCorrect, + this.text, + }); + + factory Answer.fromRawJson(String str) => Answer.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory Answer.fromJson(Map json) => Answer( + description: json["description"], + id: json["id"], + isCorrect: json["is_correct"], + text: json["text"], + ); + + Map toJson() => { + "description": description, + "id": id, + "is_correct": isCorrect, + "text": text, + }; +} + +class Video { + Flavor? flavor; + + Video({ + this.flavor, + }); + + factory Video.fromRawJson(String str) => Video.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory Video.fromJson(Map json) => Video( + flavor: json["flavor"] == null ? null : Flavor.fromJson(json["flavor"]), + ); + + Map toJson() => { + "flavor": flavor?.toJson(), + }; +} + +class Flavor { + String? downloadable; + int? duration; + int? flavorId; + String? hls; + String? picture; + + Flavor({ + this.downloadable, + this.duration, + this.flavorId, + this.hls, + this.picture, + }); + + factory Flavor.fromRawJson(String str) => Flavor.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory Flavor.fromJson(Map json) => Flavor( + downloadable: json["downloadable"], + duration: json["duration"], + flavorId: json["flavor_id"], + hls: json["hls"], + picture: json["picture"], + ); + + Map toJson() => { + "downloadable": downloadable, + "duration": duration, + "flavor_id": flavorId, + "hls": hls, + "picture": picture, + }; +} diff --git a/lib/pages/landing/landing_page.dart b/lib/pages/landing/landing_page.dart index 05d90571..5050f216 100644 --- a/lib/pages/landing/landing_page.dart +++ b/lib/pages/landing/landing_page.dart @@ -22,8 +22,7 @@ import 'package:diplomaticquarterapp/pages/rateAppointment/rate_appointment_doct import 'package:diplomaticquarterapp/pages/videocall-webrtc-rnd/webrtc/start_video_call.dart'; import 'package:diplomaticquarterapp/services/authentication/auth_provider.dart'; import 'package:diplomaticquarterapp/services/clinic_services/get_clinic_service.dart'; -import 'package:diplomaticquarterapp/services/family_files/family_files_provider.dart' - as family; +import 'package:diplomaticquarterapp/services/family_files/family_files_provider.dart' as family; import 'package:diplomaticquarterapp/services/livecare_services/livecare_provider.dart'; import 'package:diplomaticquarterapp/services/payfort_services/payfort_view_model.dart'; import 'package:diplomaticquarterapp/services/robo_search/event_provider.dart'; @@ -44,6 +43,7 @@ import 'package:diplomaticquarterapp/widgets/transitions/fade_page.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; + // import 'package:flutter_app_icon_badge/flutter_app_icon_badge.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -548,12 +548,19 @@ class _LandingPageState extends State with WidgetsBindingObserver { }, ), actions: [ - // IconButton( - // //iconSize: 70, - // icon: Icon( - // projectViewModel.isLogin ? Icons.settings : Icons.login, - // color: Theme.of(context).textTheme.headline1.color, - // ), + IconButton( + //iconSize: 70, + icon: Icon( + Icons.video_call, + size: 30, + color: Colors.red, + ), + onPressed: () { + Navigator.of(context).pushNamed( + COURSES_LIST_PAGE, + ); + }, + ), // onPressed: () { // if (projectViewModel.isLogin) // Navigator.of(context).pushNamed( diff --git a/lib/pages/learning/content_widget.dart b/lib/pages/learning/content_widget.dart new file mode 100644 index 00000000..2ca09353 --- /dev/null +++ b/lib/pages/learning/content_widget.dart @@ -0,0 +1,52 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +class ContentWidget extends StatefulWidget { + final String body; + + ContentWidget({required this.body}); + + @override + _ContentWidgetState createState() => _ContentWidgetState(); +} + +class _ContentWidgetState extends State { + bool isExpanded = false; + + @override + Widget build(BuildContext context) { + final truncatedText = widget.body.length > 70 ? '${widget.body.substring(0, 70)}... ' : widget.body; + + return RichText( + text: TextSpan( + children: [ + TextSpan( + text: isExpanded ? widget.body : truncatedText, + style: TextStyle( + color: Color(0xFFA2A2A2), + fontSize: 13, + fontWeight: FontWeight.w400, + height: 13 / 10, + ), + ), + if (widget.body.length > 100) + TextSpan( + text: isExpanded ? 'See Less' : 'See More', + style: TextStyle( + color: Color(0xFF2B353E), + fontSize: 13, + fontWeight: FontWeight.w500, + height: 13 / 10, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + setState(() { + isExpanded = !isExpanded; + }); + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/learning/course_detailed.dart b/lib/pages/learning/course_detailed.dart new file mode 100644 index 00000000..4feb81de --- /dev/null +++ b/lib/pages/learning/course_detailed.dart @@ -0,0 +1,441 @@ +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/pages/learning/content_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'; + +class CourseDetailedPage extends StatefulWidget { + @override + State createState() => _CourseDetailedPageState(); +} + +class _CourseDetailedPageState extends State { + @override + void initState() { + super.initState(); + context.read().getCourseById(context); + } + + @override + void dispose() { + // _timer?.cancel(); + // _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AppScaffold( + isShowAppBar: false, + isShowDecPage: false, + showNewAppBarTitle: true, + showNewAppBar: true, + appBarTitle: "Course Name", + backgroundColor: Color(0xFFF7F7F7), + onTap: () {}, + body: Consumer( + builder: (context, provider, child) { + if (provider.courseData != null) { + return ListView( + shrinkWrap: true, + padding: EdgeInsets.zero, + children: [ + if (provider.controller != null && provider.controller!.value.isInitialized) ...[ + Column( + children: [ + AspectRatio( + aspectRatio: provider.controller!.value.aspectRatio, + child: VideoPlayer(provider.controller!), + ).onTap(() { + setState(() { + provider.controller!.value.isPlaying ? provider.controller!.pause() : provider.controller!.play(); + }); + }), + VideoProgressIndicator( + provider.controller!, + padding: EdgeInsets.only(bottom: 0), + colors: VideoProgressColors( + backgroundColor: Color(0xFFD9D9D9), + bufferedColor: Colors.black12, + playedColor: Color(0xFFD02127), + ), + allowScrubbing: true, + ), + ], + ) + ] else ...[ + CachedNetworkImage( + width: double.infinity, + height: MediaQuery.of(context).size.height * .265, + imageUrl: provider.courseData!.nabedJourneyByIdResponseResult!.consultation!.defaultPocData!.image!.thumbUrl!, + imageBuilder: (context, imageProvider) => Container( + decoration: BoxDecoration( + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + child: Center( + child: Icon( + Icons.play_circle, + size: 35, + color: Colors.white, + ), + ), + ), + placeholder: (context, url) => Center(child: CircularProgressIndicator()), + errorWidget: (context, url, error) => Icon(Icons.error), + ).onTap(() { + setState(() { + if (provider.controller != null) { + provider.controller!.value.isPlaying ? provider.controller!.pause() : provider.controller!.play(); + } + }); + }), + ], + + if (provider.consultation != null) ...[ + ListView( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.symmetric(horizontal: 20), + children: [ + SizedBox(height: 20.0), + Text( + context.read().isArabic ? provider.consultation!.tagValues!.titleAr! : provider.consultation!.tagValues!.title ?? "Basics of Heart", + style: TextStyle( + fontSize: 16.0, + height: 24 / 16, + color: Color(0xFF2E303A), + fontWeight: FontWeight.w600, + ), + ), + // 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), + 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), + ), + ), + 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( + 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: [ + SizedBox( + height: 10, + ), + 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, + ), + ) + ], + SizedBox( + width: 10, + ), + if (content.controller != null) ...[ + Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: Color(0xFFDFDFDF), + borderRadius: BorderRadius.all( + Radius.circular(30), + ), + ), + child: Icon( + content.controller!.value.isPlaying ? Icons.pause : Icons.play_arrow, + 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), + ), + ), + child: Icon( + Icons.play_arrow, + size: 18, + ).onTap(() { + provider.playVideo(context, content: content); + }), + ) + ] + ], + ), + ], + ), + ), + ], + ), + ); + }).toList(), + ], + ); + }).toList(), + ), + ] + + // 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 { + return SizedBox(); + } + }, + ), + ); + } +} diff --git a/lib/pages/learning/course_list.dart b/lib/pages/learning/course_list.dart new file mode 100644 index 00000000..20116105 --- /dev/null +++ b/lib/pages/learning/course_list.dart @@ -0,0 +1,168 @@ +import 'dart:async'; + +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/routes.dart'; +import 'package:diplomaticquarterapp/services/course_service/course_service.dart'; +import 'package:diplomaticquarterapp/uitl/gif_loader_dialog_utils.dart'; +import 'package:diplomaticquarterapp/widgets/others/app_scaffold_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class CourseList extends StatefulWidget { + @override + State createState() => _CourseListState(); +} + +class _CourseListState extends State { + CourseServiceProvider? _courseServiceProvider; + + @override + void initState() { + super.initState(); + _courseServiceProvider = context.read(); + _courseServiceProvider!.getCourses(context); + } + + @override + void dispose() { + _clearData(); + super.dispose(); + } + + Future _clearData() async { + await _courseServiceProvider!.clearData(); + } + + @override + Widget build(BuildContext context) { + return AppScaffold( + isShowAppBar: false, + isShowDecPage: false, + showNewAppBarTitle: true, + showNewAppBar: true, + appBarTitle: "Learning", + backgroundColor: Color(0xFFF7F7F7), + onTap: () {}, + body: Consumer(builder: (context, provider, child) { + if (provider.nabedJourneyResponse != null) { + return ListView.separated( + itemCount: provider.data!.length, + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + List conClass = provider.data![index].contentClasses!; + model.Datum data = provider.data![index]; + return Card( + margin: EdgeInsets.only(left: 20, right: 20, top: 20), + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: Color(0xFFEFEFEF)), + borderRadius: BorderRadius.circular(10.0), // Set your desired radius here + ), + child: Padding( + padding: EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: CachedNetworkImage( + imageUrl: conClass[index].image!.thumbUrl!, + width: double.infinity, + height: MediaQuery.of(context).size.height * .25, + imageBuilder: (context, imageProvider) => Container( + decoration: BoxDecoration( + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + //colorFilter: ColorFilter.mode(Colors.red, BlendMode.colorBurn), + ), + ), + ), + placeholder: (context, url) => Center(child: CircularProgressIndicator()), + errorWidget: (context, url, error) => Icon(Icons.error), + ), + ), + SizedBox(height: 20.0), + Text( + conClass[index].title ?? "", + // "Basics of Heart", + style: TextStyle( + fontSize: 16.0, + height: 24 / 16, + color: Color(0xFF2E303A), + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 2.0), + Text( + //data.date!.toIso8601String(), + "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( + conClass[index].readPercentage.toString(), + // "2 Hours", + style: TextStyle( + fontSize: 10.0, + height: 15 / 10, + color: Color(0xFF2E303A), + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 8.0), + 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, + ), + ), + ], + ), + Icon( + Icons.arrow_right_alt, + size: 18.0, + color: Color(0xFF2B353E), + ), + ], + ), + ], + ), + ), + ).onTap(() { + provider.navigate(context, data.id!, onBack: (val) { + provider.clear(); + }); + }); + }, + separatorBuilder: (context, index) { + return SizedBox(); + }, + ); + } else { + return SizedBox(); + } + }), + ); + } +} diff --git a/lib/pages/learning/question_sheet.dart b/lib/pages/learning/question_sheet.dart new file mode 100644 index 00000000..38f2e44c --- /dev/null +++ b/lib/pages/learning/question_sheet.dart @@ -0,0 +1,221 @@ +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'; + +enum QuestionSheetAction { skip, rewatch } + +class QuestionSheet extends StatefulWidget { + final Content content; + final Function(QuestionSheetAction) onActionSelected; + + const QuestionSheet({Key? key, required this.content, required this.onActionSelected}) : super(key: key); + + @override + _QuestionSheetState createState() => _QuestionSheetState(); +} + +class _QuestionSheetState extends State { + int? selectedAnswerId; + Answer? selectedAnswer; + + @override + Widget build(BuildContext context) { + return AnimatedPadding( + duration: Duration(milliseconds: 300), + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 10), + _QuestionText(questionText: widget.content.question!.text!), + SizedBox(height: 20), + ...widget.content.question!.answers!.map((answer) => _AnswerOption( + answer: answer, + isSelected: selectedAnswerId == answer.id, + onSelect: () { + if (selectedAnswer == null) { + setState(() { + selectedAnswerId = answer.id; + selectedAnswer = answer; + }); + } + }, + )), + SizedBox(height: 20), + if (selectedAnswer != null) + _AnswerDescription( + description: selectedAnswer!.description!, + isCorrect: selectedAnswer!.isCorrect!, + ), + SizedBox(height: 20), + if (selectedAnswer != null) + _ActionButtons( + onSkip: () => widget.onActionSelected(QuestionSheetAction.skip), + onRewatch: () => widget.onActionSelected(QuestionSheetAction.rewatch), + ), + SizedBox(height: 10), + ], + ), + ), + ); + } +} + +class _QuestionText extends StatelessWidget { + final String questionText; + + const _QuestionText({Key? key, required this.questionText}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Text( + questionText, + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ); + } +} + +class _AnswerOption extends StatelessWidget { + final Answer answer; + final bool isSelected; + final VoidCallback onSelect; + + const _AnswerOption({ + Key? key, + required this.answer, + required this.isSelected, + required this.onSelect, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + width: double.infinity, + duration: Duration(milliseconds: 300), + margin: EdgeInsets.symmetric(vertical: 5), + decoration: BoxDecoration( + border: Border.all( + color: isSelected ? (answer.isCorrect! ? Colors.green : Color(0xFFD1272D)) : Colors.grey, + width: 2, + ), + borderRadius: BorderRadius.circular(10), + ), + child: TextButton( + onPressed: onSelect, + child: Text( + answer.text!, + style: TextStyle(fontSize: 16), + ), + ), + ); + } +} + +class _AnswerDescription extends StatelessWidget { + final String description; + final bool isCorrect; + + const _AnswerDescription({ + Key? key, + required this.description, + required this.isCorrect, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + description, + style: TextStyle( + color: isCorrect ? Colors.green : Color(0xFFD1272D), + ), + ), + ); + } +} + +class _ActionButtons extends StatelessWidget { + final VoidCallback onSkip; + final VoidCallback onRewatch; + + const _ActionButtons({ + Key? key, + required this.onSkip, + required this.onRewatch, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: _ActionButton( + label: "SKIP", + onPressed: onSkip, + ), + ), + SizedBox(width: 20), + Expanded( + child: _ActionButton( + label: "RE-WATCH", + onPressed: onRewatch, + ), + ), + ], + ); + } +} + +class _ActionButton extends StatelessWidget { + final String label; + final VoidCallback onPressed; + + const _ActionButton({ + Key? key, + required this.label, + required this.onPressed, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + width: double.infinity, + duration: Duration(milliseconds: 300), + margin: EdgeInsets.symmetric(vertical: 5), + decoration: BoxDecoration( + border: Border.all(color: Color(0xFFD1272D), width: 2), + borderRadius: BorderRadius.circular(10), + ), + child: TextButton( + onPressed: onPressed, + child: Text( + label, + style: TextStyle(fontSize: 16), + ), + ), + ); + } +} + +// Modified showQuestionSheet function +Future showQuestionSheet(BuildContext con, {required Content content}) { + return showModalBottomSheet( + context: con, + isDismissible: false, + isScrollControlled: true, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20))), + builder: (context) => QuestionSheet( + content: content, + onActionSelected: (action) { + Navigator.of(context).pop(action); + }, + ), + ); +} diff --git a/lib/routes.dart b/lib/routes.dart index 6e8aca9e..8bd95908 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -7,6 +7,8 @@ import 'package:diplomaticquarterapp/pages/TestPage.dart'; import 'package:diplomaticquarterapp/pages/appUpdatePage/app_update_page.dart'; import 'package:diplomaticquarterapp/pages/conference/zoom/call_screen.dart'; import 'package:diplomaticquarterapp/pages/landing/landing_page.dart'; +import 'package:diplomaticquarterapp/pages/learning/course_detailed.dart'; +import 'package:diplomaticquarterapp/pages/learning/course_list.dart'; import 'package:diplomaticquarterapp/pages/livecare/incoming_call.dart'; import 'package:diplomaticquarterapp/pages/livecare/livecare_home.dart'; import 'package:diplomaticquarterapp/pages/login/confirm-login.dart'; @@ -60,6 +62,8 @@ const String HEALTH_WEATHER = 'health-weather'; const APP_UPDATE = 'app-update'; const ZOOM_CALL_PAGE = 'zoom_call_page'; +const COURSES_LIST_PAGE = 'courses_list_page'; +const COURSES_DETAILED_PAGE = 'courses_detailed_page'; var routes = { SPLASH: (_) => SplashScreen(), @@ -91,6 +95,9 @@ var routes = { ZOOM_CALL_PAGE: (_) => CallScreen(), + COURSES_LIST_PAGE: (_) => CourseList(), + COURSES_DETAILED_PAGE: (_) => CourseDetailedPage(), + OPENTOK_CALL_PAGE: (_) => OpenTokConnectCallPage( apiKey: OPENTOK_API_KEY, sessionId: '1_MX40NjIwOTk2Mn5-MTY0NzE3MzcwNjA0MH55RUJoZnd0ZGh2U3BPc01ENVZBelQvT1Z-fg', diff --git a/lib/services/course_service/course_service.dart b/lib/services/course_service/course_service.dart new file mode 100644 index 00000000..9cf195b4 --- /dev/null +++ b/lib/services/course_service/course_service.dart @@ -0,0 +1,302 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; + +import 'package:diplomaticquarterapp/config/config.dart'; +import 'package:diplomaticquarterapp/config/shared_pref_kay.dart'; +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_list_model.dart'; +import 'package:diplomaticquarterapp/models/course/education_journey_model.dart'; +import 'package:diplomaticquarterapp/pages/learning/question_sheet.dart'; +import 'package:diplomaticquarterapp/routes.dart'; +import 'package:diplomaticquarterapp/services/authentication/auth_provider.dart'; +import 'package:diplomaticquarterapp/uitl/app_shared_preferences.dart'; +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'; + +class CourseServiceProvider with ChangeNotifier { + AppSharedPreferences sharedPref = AppSharedPreferences(); + AppGlobal appGlobal = AppGlobal(); + AuthenticatedUser authUser = AuthenticatedUser(); + AuthProvider authProvider = AuthProvider(); + bool isDataLoaded = false; + int? selectedJourney; + List? data; + PatientEducationJourneyListModel? nabedJourneyResponse; + PatientEducationJourneyModel? courseData; + List? courseTopics; + Consultation? consultation; + + //Main Video Controller & Timer + VideoPlayerState _videoState = VideoPlayerState.paused; + + VideoPlayerState get videoState => _videoState; + VideoPlayerController? controller; + Timer? timer; + + // Learning Page + + Future fetchPatientCoursesList() async { + print("====== Api Initiated ========="); + Map request; + if (await this.sharedPref.getObject(USER_PROFILE) != null) { + var data = AuthenticatedUser.fromJson(await this.sharedPref.getObject(USER_PROFILE)); + authUser = data; + } + request = {"Channel": 3, "TokenID": "@dm!n", "PatientID": 22335}; + dynamic localRes; + await BaseAppClient().post(GET_PATIENT_COURSES_LIST, onSuccess: (response, statusCode) async { + print("====== Api Response ========="); + print("${response["NabedJourneyResponseResult"]}"); + localRes = PatientEducationJourneyListModel.fromJson(response); + }, onFailure: (String error, int statusCode) { + 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) { + nabedJourneyResponse = response; + isDataLoaded = true; + data = nabedJourneyResponse!.nabedJourneyResponseResult!.data; + } + notifyListeners(); + } + + // Detailed Page + + Future fetchPatientCourseById() async { + print("====== Api Initiated ========="); + Map request; + if (await this.sharedPref.getObject(USER_PROFILE) != null) { + var data = AuthenticatedUser.fromJson(await this.sharedPref.getObject(USER_PROFILE)); + authUser = data; + } + request = {"Channel": 3, "TokenID": "@dm!n", "JourneyID": selectedJourney}; + dynamic localRes; + await BaseAppClient().post(GET_PATIENT_COURSE_BY_ID, onSuccess: (response, statusCode) async { + print("====== Api Response ========="); + print("${response}"); + localRes = response; + }, onFailure: (String error, int statusCode) { + 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)); + courseTopics = courseData!.nabedJourneyByIdResponseResult!.contentClasses!.first.topics; + consultation = courseData!.nabedJourneyByIdResponseResult!.consultation; + } + notifyListeners(); + } + + Future navigate(BuildContext context, int journey, {required Function(Object? val) onBack}) async { + selectedJourney = journey; + Navigator.of(context).pushNamed(COURSES_DETAILED_PAGE).then((val) { + onBack(val); + }); + } + + String getDurationOfVideo(int duration) { + final durationInSeconds = duration; + final minutes = durationInSeconds ~/ 60; + final seconds = durationInSeconds % 60; + 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 { + try { + final videoUrl = content.video?.flavor?.downloadable ?? ""; + if (videoUrl.isEmpty) { + Utils.showErrorToast("No video URL provided."); + return; + } + + controller = VideoPlayerController.networkUrl( + Uri.parse(videoUrl), + formatHint: VideoFormat.hls, + ); + + await controller!.initialize(); + controller!.play(); + setVideoState(VideoPlayerState.playing); + notifyListeners(); + + content.controller = controller; + + controller!.addListener(() { + if (controller!.value.isPlaying && timer == null) { + _startTimer(context, content: content); + } else if (!controller!.value.isPlaying && timer != null) { + stopTimer(); + } + + if (controller!.value.hasError) { + Utils.showErrorToast("Failed to load video."); + controller = null; + setVideoState(VideoPlayerState.paused); + } + }); + } catch (e) { + Utils.showErrorToast("Error loading video: $e"); + controller = null; + setVideoState(VideoPlayerState.paused); + } + } + } + + 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"); + 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); + } + } + } + } + }); + } + + void stopTimer() { + timer?.cancel(); + timer = null; + } + + // 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); + } + + Future clearData() async { + data = courseData = nabedJourneyResponse = selectedJourney = courseTopics = consultation = timer = controller = null; + } + + Future clear() async { + if (controller != null) controller!.dispose(); + courseData = courseTopics = timer = controller = null; + } +} + +enum VideoPlayerState { + playing, + paused, + completed, +} diff --git a/pubspec.yaml b/pubspec.yaml index 475a72f2..46ffeafa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -155,11 +155,9 @@ dependencies: badges: ^3.1.2 # flutter_app_icon_badge: ^2.0.0 -# dropdown_search: 5.0.6 youtube_player_flutter: ^9.1.0 + video_player: ^2.9.2 -# shimmer: ^3.0.0 -# carousel_slider: ^4.0.0 # flutter_staggered_grid_view: ^0.7.0 huawei_hmsavailability: ^6.11.0+301 huawei_location: ^6.11.0+301 @@ -183,6 +181,7 @@ dependencies: cloudflare_turnstile: ^2.0.1 + dependency_overrides: dev_dependencies: