Compare commits
3 Commits
a2a26c456d
...
6dc8f81fa7
| Author | SHA1 | Date | |
|---|---|---|---|
| 6dc8f81fa7 | |||
| 610bd2b2fd | |||
| 64cad421e7 |
|
|
@ -35,7 +35,6 @@ import 'package:yimaru_app/ui/views/learn_lesson_detail/learn_lesson_detail_view
|
|||
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
|
||||
import 'package:yimaru_app/ui/views/course_practice/course_practice_view.dart';
|
||||
import 'package:yimaru_app/ui/views/course_payment/course_payment_view.dart';
|
||||
import 'package:yimaru_app/ui/views/course_category/course_category_view.dart';
|
||||
import 'package:yimaru_app/ui/views/failure/failure_view.dart';
|
||||
import 'package:yimaru_app/ui/views/course_lesson/course_lesson_view.dart';
|
||||
import 'package:yimaru_app/ui/views/course_lesson_detail/course_lesson_detail_view.dart';
|
||||
|
|
@ -43,7 +42,6 @@ import 'package:yimaru_app/services/notification_service.dart';
|
|||
import 'package:yimaru_app/ui/views/duolingo/duolingo_view.dart';
|
||||
import 'package:yimaru_app/services/smart_auth_service.dart';
|
||||
import 'package:yimaru_app/services/course_service.dart';
|
||||
import 'package:yimaru_app/ui/views/course_subcategory/course_subcategory_view.dart';
|
||||
import 'package:yimaru_app/ui/views/course/course_view.dart';
|
||||
import 'package:yimaru_app/services/audio_player_service.dart';
|
||||
import 'package:yimaru_app/services/voice_recorder_service.dart';
|
||||
|
|
@ -55,6 +53,11 @@ import 'package:yimaru_app/ui/views/assessment/assessment_view.dart';
|
|||
import 'package:yimaru_app/services/vimeo_service.dart';
|
||||
import 'package:yimaru_app/services/url_launcher_service.dart';
|
||||
import 'package:yimaru_app/services/phone_caller_service.dart';
|
||||
import 'package:yimaru_app/ui/views/learn_subscription/learn_subscription_view.dart';
|
||||
import 'package:yimaru_app/ui/views/arif_pay/arif_pay_view.dart';
|
||||
import 'package:yimaru_app/services/learn_service.dart';
|
||||
import 'package:yimaru_app/ui/views/course_catalog/course_catalog_view.dart';
|
||||
import 'package:yimaru_app/ui/views/course_unit/course_unit_view.dart';
|
||||
// @stacked-import
|
||||
|
||||
@StackedApp(
|
||||
|
|
@ -83,17 +86,19 @@ import 'package:yimaru_app/services/phone_caller_service.dart';
|
|||
MaterialRoute(page: LearnPracticeView),
|
||||
MaterialRoute(page: CoursePracticeView),
|
||||
MaterialRoute(page: CoursePaymentView),
|
||||
MaterialRoute(page: CourseCategoryView),
|
||||
MaterialRoute(page: FailureView),
|
||||
MaterialRoute(page: CourseLessonView),
|
||||
MaterialRoute(page: CourseLessonDetailView),
|
||||
MaterialRoute(page: DuolingoView),
|
||||
MaterialRoute(page: CourseSubcategoryView),
|
||||
MaterialRoute(page: CourseView),
|
||||
MaterialRoute(page: CoursePracticeQuestionView),
|
||||
MaterialRoute(page: LearnProgramView),
|
||||
MaterialRoute(page: LearnCourseView),
|
||||
MaterialRoute(page: AssessmentView),
|
||||
MaterialRoute(page: LearnSubscriptionView),
|
||||
MaterialRoute(page: ArifPayView),
|
||||
MaterialRoute(page: CourseCatalogView),
|
||||
MaterialRoute(page: CourseUnitView),
|
||||
// @stacked-route
|
||||
],
|
||||
dependencies: [
|
||||
|
|
@ -118,6 +123,7 @@ import 'package:yimaru_app/services/phone_caller_service.dart';
|
|||
LazySingleton(classType: VimeoService),
|
||||
LazySingleton(classType: UrlLauncherService),
|
||||
LazySingleton(classType: PhoneCallerService),
|
||||
LazySingleton(classType: LearnService),
|
||||
// @stacked-service
|
||||
],
|
||||
bottomsheets: [
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import '../services/google_auth_service.dart';
|
|||
import '../services/image_downloader_service.dart';
|
||||
import '../services/image_picker_service.dart';
|
||||
import '../services/in_app_update_service.dart';
|
||||
import '../services/learn_service.dart';
|
||||
import '../services/notification_service.dart';
|
||||
import '../services/permission_handler_service.dart';
|
||||
import '../services/phone_caller_service.dart';
|
||||
|
|
@ -61,4 +62,5 @@ Future<void> setupLocator(
|
|||
locator.registerLazySingleton(() => VimeoService());
|
||||
locator.registerLazySingleton(() => UrlLauncherService());
|
||||
locator.registerLazySingleton(() => PhoneCallerService());
|
||||
locator.registerLazySingleton(() => LearnService());
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,23 +0,0 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'category.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class Category {
|
||||
final int? id;
|
||||
|
||||
final String? name;
|
||||
|
||||
@JsonKey(name: 'is_active')
|
||||
final bool? isActive;
|
||||
|
||||
@JsonKey(name: 'total_count')
|
||||
final int? totalCount;
|
||||
|
||||
const Category({this.id, this.name, this.isActive, this.totalCount});
|
||||
|
||||
factory Category.fromJson(Map<String, dynamic> json) =>
|
||||
_$CategoryFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$CategoryToJson(this);
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'category.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Category _$CategoryFromJson(Map<String, dynamic> json) => Category(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
name: json['name'] as String?,
|
||||
isActive: json['is_active'] as bool?,
|
||||
totalCount: (json['total_count'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CategoryToJson(Category instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'is_active': instance.isActive,
|
||||
'total_count': instance.totalCount,
|
||||
};
|
||||
45
lib/models/course_catalog.dart
Normal file
45
lib/models/course_catalog.dart
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'course_catalog.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class CourseCatalog {
|
||||
final int? id;
|
||||
|
||||
final String? name;
|
||||
|
||||
final String? thumbnail;
|
||||
|
||||
final String? description;
|
||||
|
||||
@JsonKey(name: 'sort_order')
|
||||
final int? sortOrder;
|
||||
|
||||
@JsonKey(name: 'units_count')
|
||||
final int? unitsCount;
|
||||
|
||||
@JsonKey(name: 'modules_count')
|
||||
final int? modulesCount;
|
||||
|
||||
@JsonKey(name: 'lessons_count')
|
||||
final int? lessonsCount;
|
||||
|
||||
@JsonKey(name: 'has_practice')
|
||||
final bool? hasPractice;
|
||||
|
||||
const CourseCatalog(
|
||||
{this.id,
|
||||
this.name,
|
||||
this.thumbnail,
|
||||
this.lessonsCount,
|
||||
this.sortOrder,
|
||||
this.unitsCount,
|
||||
this.hasPractice,
|
||||
this.description,
|
||||
this.modulesCount});
|
||||
|
||||
factory CourseCatalog.fromJson(Map<String, dynamic> json) =>
|
||||
_$CourseCatalogFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$CourseCatalogToJson(this);
|
||||
}
|
||||
33
lib/models/course_catalog.g.dart
Normal file
33
lib/models/course_catalog.g.dart
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'course_catalog.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
CourseCatalog _$CourseCatalogFromJson(Map<String, dynamic> json) =>
|
||||
CourseCatalog(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
name: json['name'] as String?,
|
||||
thumbnail: json['thumbnail'] as String?,
|
||||
lessonsCount: (json['lessons_count'] as num?)?.toInt(),
|
||||
sortOrder: (json['sort_order'] as num?)?.toInt(),
|
||||
unitsCount: (json['units_count'] as num?)?.toInt(),
|
||||
hasPractice: json['has_practice'] as bool?,
|
||||
description: json['description'] as String?,
|
||||
modulesCount: (json['modules_count'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CourseCatalogToJson(CourseCatalog instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'thumbnail': instance.thumbnail,
|
||||
'description': instance.description,
|
||||
'sort_order': instance.sortOrder,
|
||||
'units_count': instance.unitsCount,
|
||||
'modules_count': instance.modulesCount,
|
||||
'lessons_count': instance.lessonsCount,
|
||||
'has_practice': instance.hasPractice,
|
||||
};
|
||||
40
lib/models/learn_subscription.dart
Normal file
40
lib/models/learn_subscription.dart
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'learn_subscription.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class LearnSubscription {
|
||||
final int? id;
|
||||
|
||||
final String? name;
|
||||
|
||||
final double? price;
|
||||
|
||||
final String? currency;
|
||||
|
||||
final String? description;
|
||||
|
||||
@JsonKey(name: 'is_active')
|
||||
final bool? isActive;
|
||||
|
||||
@JsonKey(name: 'duration_unit')
|
||||
final String? durationUnit;
|
||||
|
||||
@JsonKey(name: 'duration_value')
|
||||
final int? durationValue;
|
||||
|
||||
const LearnSubscription(
|
||||
{this.id,
|
||||
this.name,
|
||||
this.price,
|
||||
this.isActive,
|
||||
this.currency,
|
||||
this.description,
|
||||
this.durationUnit,
|
||||
this.durationValue});
|
||||
|
||||
factory LearnSubscription.fromJson(Map<String, dynamic> json) =>
|
||||
_$LearnSubscriptionFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$LearnSubscriptionToJson(this);
|
||||
}
|
||||
31
lib/models/learn_subscription.g.dart
Normal file
31
lib/models/learn_subscription.g.dart
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'learn_subscription.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
LearnSubscription _$LearnSubscriptionFromJson(Map<String, dynamic> json) =>
|
||||
LearnSubscription(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
name: json['name'] as String?,
|
||||
price: (json['price'] as num?)?.toDouble(),
|
||||
isActive: json['is_active'] as bool?,
|
||||
currency: json['currency'] as String?,
|
||||
description: json['description'] as String?,
|
||||
durationUnit: json['duration_unit'] as String?,
|
||||
durationValue: (json['duration_value'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$LearnSubscriptionToJson(LearnSubscription instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'price': instance.price,
|
||||
'currency': instance.currency,
|
||||
'description': instance.description,
|
||||
'is_active': instance.isActive,
|
||||
'duration_unit': instance.durationUnit,
|
||||
'duration_value': instance.durationValue,
|
||||
};
|
||||
31
lib/models/learn_subscription_request.dart
Normal file
31
lib/models/learn_subscription_request.dart
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'learn_subscription_request.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class LearnSubscriptionRequest {
|
||||
final double? amount;
|
||||
|
||||
final String? currency;
|
||||
|
||||
@JsonKey(name: 'payment_id')
|
||||
final int? paymentId;
|
||||
|
||||
@JsonKey(name: 'session_id')
|
||||
final String? sessionId;
|
||||
|
||||
@JsonKey(name: 'payment_url')
|
||||
final String? paymentUrl;
|
||||
|
||||
const LearnSubscriptionRequest(
|
||||
{this.amount,
|
||||
this.currency,
|
||||
this.sessionId,
|
||||
this.paymentId,
|
||||
this.paymentUrl});
|
||||
|
||||
factory LearnSubscriptionRequest.fromJson(Map<String, dynamic> json) =>
|
||||
_$LearnSubscriptionRequestFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$LearnSubscriptionRequestToJson(this);
|
||||
}
|
||||
27
lib/models/learn_subscription_request.g.dart
Normal file
27
lib/models/learn_subscription_request.g.dart
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'learn_subscription_request.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
LearnSubscriptionRequest _$LearnSubscriptionRequestFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
LearnSubscriptionRequest(
|
||||
amount: (json['amount'] as num?)?.toDouble(),
|
||||
currency: json['currency'] as String?,
|
||||
sessionId: json['session_id'] as String?,
|
||||
paymentId: (json['payment_id'] as num?)?.toInt(),
|
||||
paymentUrl: json['payment_url'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$LearnSubscriptionRequestToJson(
|
||||
LearnSubscriptionRequest instance) =>
|
||||
<String, dynamic>{
|
||||
'amount': instance.amount,
|
||||
'currency': instance.currency,
|
||||
'payment_id': instance.paymentId,
|
||||
'session_id': instance.sessionId,
|
||||
'payment_url': instance.paymentUrl,
|
||||
};
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'subcategory.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class Subcategory {
|
||||
final int? id;
|
||||
|
||||
final String? name;
|
||||
|
||||
final String? description;
|
||||
|
||||
@JsonKey(name: 'is_active')
|
||||
final bool? isActive;
|
||||
|
||||
@JsonKey(name: 'total_count')
|
||||
final int? totalCount;
|
||||
|
||||
@JsonKey(name: 'category_id')
|
||||
final int? categoryId;
|
||||
|
||||
@JsonKey(name: 'category_name')
|
||||
final String? categoryName;
|
||||
|
||||
@JsonKey(name: 'display_order')
|
||||
final int? displayOrder;
|
||||
|
||||
const Subcategory(
|
||||
{this.id,
|
||||
this.name,
|
||||
this.isActive,
|
||||
this.totalCount,
|
||||
this.categoryId,
|
||||
this.description,
|
||||
this.categoryName,
|
||||
this.displayOrder});
|
||||
|
||||
factory Subcategory.fromJson(Map<String, dynamic> json) =>
|
||||
_$SubcategoryFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$SubcategoryToJson(this);
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'subcategory.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Subcategory _$SubcategoryFromJson(Map<String, dynamic> json) => Subcategory(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
name: json['name'] as String?,
|
||||
isActive: json['is_active'] as bool?,
|
||||
totalCount: (json['total_count'] as num?)?.toInt(),
|
||||
categoryId: (json['category_id'] as num?)?.toInt(),
|
||||
description: json['description'] as String?,
|
||||
categoryName: json['category_name'] as String?,
|
||||
displayOrder: (json['display_order'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SubcategoryToJson(Subcategory instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'is_active': instance.isActive,
|
||||
'total_count': instance.totalCount,
|
||||
'category_id': instance.categoryId,
|
||||
'category_name': instance.categoryName,
|
||||
'display_order': instance.displayOrder,
|
||||
};
|
||||
|
|
@ -4,8 +4,7 @@ import 'package:yimaru_app/models/learn_practice.dart';
|
|||
import 'package:yimaru_app/models/learn_program.dart';
|
||||
import 'package:yimaru_app/models/level.dart';
|
||||
import 'package:yimaru_app/models/assessment_question.dart';
|
||||
import 'package:yimaru_app/models/subcategory.dart';
|
||||
import 'package:yimaru_app/models/category.dart';
|
||||
import 'package:yimaru_app/models/course_catalog.dart';
|
||||
import 'package:yimaru_app/models/course_lesson.dart';
|
||||
import 'package:yimaru_app/models/course_progress.dart';
|
||||
import 'package:yimaru_app/models/course.dart';
|
||||
|
|
@ -18,10 +17,12 @@ import '../app/app.locator.dart';
|
|||
import '../models/learn_course.dart';
|
||||
import '../models/learn_module.dart';
|
||||
import '../models/learn_question.dart';
|
||||
import '../models/learn_subscription.dart';
|
||||
import '../models/lesson.dart';
|
||||
import '../models/module.dart';
|
||||
import '../models/assessment.dart';
|
||||
import '../models/submodule.dart';
|
||||
import '../models/learn_subscription_request.dart';
|
||||
import '../ui/common/enmus.dart';
|
||||
|
||||
class ApiService {
|
||||
|
|
@ -432,7 +433,7 @@ class ApiService {
|
|||
}
|
||||
|
||||
// Learn learn courses
|
||||
Future<List<LearnCourse>> getLearnCourse(int id) async {
|
||||
Future<List<LearnCourse>> getLearnCourses(int id) async {
|
||||
try {
|
||||
List<LearnCourse> learnCourses = [];
|
||||
|
||||
|
|
@ -552,6 +553,32 @@ class ApiService {
|
|||
}
|
||||
}
|
||||
|
||||
// Complete lesson
|
||||
Future<Map<String, dynamic>> completeLearnLesson(int id) async {
|
||||
try {
|
||||
Response response = await _service.dio.post(
|
||||
'$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kLessonsUrl/$id/$kCompleteUrl',
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 && response.data['success']) {
|
||||
return {
|
||||
'message': 'Lesson completed',
|
||||
'status': ResponseStatus.success,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'status': ResponseStatus.failure,
|
||||
'message': 'Unknown Error Occurred'
|
||||
};
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
return {
|
||||
'status': ResponseStatus.failure,
|
||||
'message': e.response?.data.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Learn lesson practices
|
||||
Future<List<LearnPractice>> getLearnLessonPractices(int id) async {
|
||||
try {
|
||||
|
|
@ -601,49 +628,48 @@ class ApiService {
|
|||
}
|
||||
}
|
||||
|
||||
/* TO BE MODIFIED*/
|
||||
|
||||
// Get categories
|
||||
Future<List<Category>> getCategories() async {
|
||||
// Complete lesson
|
||||
Future<Map<String, dynamic>> completeLearnPractice(int id) async {
|
||||
try {
|
||||
List<Category> categories = [];
|
||||
|
||||
final Response response = await _service.dio.get(
|
||||
'$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kCategoryUrl');
|
||||
Response response = await _service.dio.post(
|
||||
'$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kProgressUrl/$kPracticesUrl/$id/$kCompleteUrl',
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
var data = response.data;
|
||||
var decodedData = data['data']['categories'] as List;
|
||||
categories = decodedData.map(
|
||||
(e) {
|
||||
return Category.fromJson(e);
|
||||
},
|
||||
).toList();
|
||||
return categories;
|
||||
return {
|
||||
'status': ResponseStatus.success,
|
||||
'message': 'Lesson completed'
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'status': ResponseStatus.failure,
|
||||
'message': 'Unknown Error Occurred'
|
||||
};
|
||||
}
|
||||
return [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
} on DioException catch (e) {
|
||||
return {
|
||||
'status': ResponseStatus.failure,
|
||||
'message': e.response?.data.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Get course subcategory
|
||||
Future<List<Subcategory>> getSubcategories(int id) async {
|
||||
Future<List<LearnSubscription>> getLearnSubscriptions() async {
|
||||
try {
|
||||
List<Subcategory> subcategories = [];
|
||||
List<LearnSubscription> subscriptions = [];
|
||||
|
||||
final Response response = await _service.dio
|
||||
.get('$kBaseUrl/$kCourseBaseUrl/$kCategoryUrl/$id/$kCoursesUrl');
|
||||
.get('$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kSubscriptionsUrl');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
var data = response.data;
|
||||
var decodedData = data['data']['courses'] as List;
|
||||
subcategories = decodedData.map(
|
||||
var decodedData = data['data'] as List;
|
||||
subscriptions = decodedData.map(
|
||||
(e) {
|
||||
return Subcategory.fromJson(e);
|
||||
return LearnSubscription.fromJson(e);
|
||||
},
|
||||
).toList();
|
||||
return subcategories;
|
||||
return subscriptions;
|
||||
}
|
||||
return [];
|
||||
} catch (e) {
|
||||
|
|
@ -651,6 +677,60 @@ class ApiService {
|
|||
}
|
||||
}
|
||||
|
||||
// Create subscription
|
||||
Future<Map<String, dynamic>> createSubscriptionRequest(
|
||||
Map<String, dynamic> data) async {
|
||||
try {
|
||||
Response response = await _service.dio.post(
|
||||
'$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kPaymentsUrl/$kSubscribeUrl',
|
||||
data: data);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return {
|
||||
'message': 'Lesson completed',
|
||||
'status': ResponseStatus.success,
|
||||
'data': LearnSubscriptionRequest.fromJson(response.data['data']),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'status': ResponseStatus.failure,
|
||||
'message': 'Unknown Error Occurred'
|
||||
};
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
return {
|
||||
'status': ResponseStatus.failure,
|
||||
'message': e.response?.data.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Get course catalogs
|
||||
Future<List<CourseCatalog>> getCourseCatalogs() async {
|
||||
try {
|
||||
List<CourseCatalog> catalogs = [];
|
||||
|
||||
final Response response = await _service.dio.get(
|
||||
'$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kExamPrepUrl/$kCatalogCoursesUrl');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
var data = response.data;
|
||||
var decodedData = data['data']['catalog_courses'] as List;
|
||||
catalogs = decodedData.map(
|
||||
(e) {
|
||||
return CourseCatalog.fromJson(e);
|
||||
},
|
||||
).toList();
|
||||
return catalogs;
|
||||
}
|
||||
return [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/* TO BE MODIFIED*/
|
||||
|
||||
// Get courses
|
||||
// Future<List<Course>> getCourses(int id) async {
|
||||
// try {
|
||||
|
|
@ -727,7 +807,7 @@ class ApiService {
|
|||
Future<Map<String, dynamic>> completeLesson(int id) async {
|
||||
try {
|
||||
Response response = await _service.dio.post(
|
||||
'$kBaseUrl/$kLessonProgressUrl/$id/$kCompleteLessonUrl',
|
||||
'$kBaseUrl/$kLessonProgressUrl/$id/$kCompleteUrl',
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
|
|
@ -813,9 +893,9 @@ class ApiService {
|
|||
}
|
||||
|
||||
// Get learn subcategories
|
||||
Future<List<Subcategory>> getLearnSubcategories() async {
|
||||
Future<List<CourseCatalog>> getLearnSubcategories() async {
|
||||
try {
|
||||
List<Subcategory> learnSubcategories = [];
|
||||
List<CourseCatalog> learnSubcategories = [];
|
||||
|
||||
final Response response = await _service.dio.get(
|
||||
'$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kLearnSubcategoriesUrl');
|
||||
|
|
@ -825,7 +905,7 @@ class ApiService {
|
|||
var decodedData = data['data']['sub_categories'] as List;
|
||||
learnSubcategories = decodedData.map(
|
||||
(e) {
|
||||
return Subcategory.fromJson(e);
|
||||
return CourseCatalog.fromJson(e);
|
||||
},
|
||||
).toList();
|
||||
return learnSubcategories;
|
||||
|
|
|
|||
66
lib/services/learn_service.dart
Normal file
66
lib/services/learn_service.dart
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import 'package:stacked/stacked.dart';
|
||||
|
||||
import '../app/app.locator.dart';
|
||||
import '../models/learn_course.dart';
|
||||
import '../models/learn_lesson.dart';
|
||||
import '../models/learn_module.dart';
|
||||
import '../models/learn_program.dart';
|
||||
import 'api_service.dart';
|
||||
|
||||
class LearnService with ListenableServiceMixin {
|
||||
// Dependency injection
|
||||
final _apiService = locator<ApiService>();
|
||||
|
||||
// Initialization
|
||||
LearnLessonService() {
|
||||
listenToReactiveValues([_programs, _lessons]);
|
||||
}
|
||||
|
||||
// Learn program
|
||||
List<LearnProgram> _programs = [];
|
||||
|
||||
List<LearnProgram> get programs => _programs;
|
||||
|
||||
// Learn course
|
||||
List<LearnCourse> _courses = [];
|
||||
|
||||
List<LearnCourse> get courses => _courses;
|
||||
|
||||
// Learn module
|
||||
List<LearnModule> _modules = [];
|
||||
|
||||
List<LearnModule> get modules => _modules;
|
||||
|
||||
// Learn lesson
|
||||
List<LearnLesson> _lessons = [];
|
||||
|
||||
List<LearnLesson> get lessons => _lessons;
|
||||
|
||||
// Learn programs
|
||||
Future<void> getLearnPrograms() async {
|
||||
_programs = await _apiService.getLearnPrograms();
|
||||
_programs.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Learn modules
|
||||
Future<void> getLearnCourses(int id) async {
|
||||
_courses = await _apiService.getLearnCourses(id);
|
||||
_courses.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Learn modules
|
||||
Future<void> getLearnModules(int id) async {
|
||||
_modules = await _apiService.getLearnModules(id);
|
||||
_modules.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Learn lessons
|
||||
Future<void> getLearnLessons(int id) async {
|
||||
_lessons = await _apiService.getLearnLessons(id);
|
||||
_lessons.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
|
@ -130,7 +130,7 @@ class NotificationService {
|
|||
}
|
||||
|
||||
Future<void> updateFCMToken() async {
|
||||
// print('DEVICE TOKEN: ${await _messaging.getToken()}');
|
||||
print('DEVICE TOKEN: ${await _messaging.getToken()}');
|
||||
_messaging.onTokenRefresh.listen((newToken) {
|
||||
// updateTokenOnServer(newToken);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,11 +17,19 @@ String kProgramsUrl = 'programs';
|
|||
|
||||
String kRegisterUrl = 'register';
|
||||
|
||||
String kProgressUrl = 'progress';
|
||||
|
||||
String kCompleteUrl = 'complete';
|
||||
|
||||
String kPaymentsUrl = 'payments';
|
||||
|
||||
String kSubscribeUrl = 'subscribe';
|
||||
|
||||
String kPracticesUrl = 'practices';
|
||||
|
||||
String kQuestionsUrl = 'questions';
|
||||
|
||||
String kCategoryUrl = 'categories';
|
||||
String kExamPrepUrl = 'exam-prep';
|
||||
|
||||
String kCoursePractice = 'by-owner';
|
||||
|
||||
|
|
@ -37,8 +45,6 @@ String kSubmodulesUrl = 'sub-modules';
|
|||
|
||||
String kSubcoursesUrl = 'sub-courses';
|
||||
|
||||
String kCompleteLessonUrl = 'complete';
|
||||
|
||||
String kResetPassword = 'resetPassword';
|
||||
|
||||
String kQuestionSetsUrl = 'question-sets';
|
||||
|
|
@ -51,8 +57,12 @@ String kPublishedVideos = 'videos/published';
|
|||
|
||||
String kCoursePracticeQuestions = 'questions';
|
||||
|
||||
String kCatalogCoursesUrl = 'catalog-courses';
|
||||
|
||||
String kUpdateProfileImage = 'profile-picture';
|
||||
|
||||
String kSubscriptionsUrl = 'subscription-plans';
|
||||
|
||||
String kRefreshTokenUrl = 'api/v1/auth/refresh';
|
||||
|
||||
String kLoginUrl = 'api/v1/auth/customer-login';
|
||||
|
|
@ -89,3 +99,10 @@ String kServerClientId =
|
|||
String kPhoneSupport = '+251946396655';
|
||||
|
||||
String kTelegramSupport = '@yimaruacademy2026';
|
||||
|
||||
String kTelegramSupportLink = 'https://t.me/yimaruacademy2026';
|
||||
|
||||
String kErrorUrl = 'https://yimaru.net/api/v1/payments/arifpay/error';
|
||||
|
||||
String kSuccessUrl =
|
||||
'https://api.yimaruacademy.com/api/v1/payments/arifpay/success';
|
||||
|
|
|
|||
|
|
@ -42,23 +42,25 @@ enum StateObjects {
|
|||
courseLessons,
|
||||
profileUpdate,
|
||||
resetPassword,
|
||||
subcategories,
|
||||
learnPractice,
|
||||
courseCatalogs,
|
||||
loginWithEmail,
|
||||
coursePractice,
|
||||
learnPractices,
|
||||
loginWithGoogle,
|
||||
loadLessonVideo,
|
||||
loadCourseVideo,
|
||||
learnSubmodules,
|
||||
requestResetCode,
|
||||
courseCategories,
|
||||
profileCompletion,
|
||||
learnSubscription,
|
||||
learnSubscriptions,
|
||||
registerWithGoogle,
|
||||
learnPracticeSample,
|
||||
learnPracticeAnswer,
|
||||
loginWithPhoneNumber,
|
||||
assessmentQuestions,
|
||||
learnPracticeQuestion,
|
||||
completeLearnPractice,
|
||||
coursePracticeQuestion,
|
||||
coursePracticeQuestions,
|
||||
recordLearnPracticeAnswer,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
|
|
|
|||
81
lib/ui/views/arif_pay/arif_pay_view.dart
Normal file
81
lib/ui/views/arif_pay/arif_pay_view.dart
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:yimaru_app/models/learn_subscription.dart';
|
||||
import 'package:yimaru_app/ui/common/app_constants.dart';
|
||||
import 'package:yimaru_app/ui/common/enmus.dart';
|
||||
import 'package:yimaru_app/ui/common/ui_helpers.dart';
|
||||
import 'package:yimaru_app/ui/widgets/page_loading_indicator.dart';
|
||||
|
||||
import '../../../models/learn_subscription_request.dart';
|
||||
import 'arif_pay_viewmodel.dart';
|
||||
|
||||
class ArifPayView extends StackedView<ArifPayViewModel> {
|
||||
final String phone;
|
||||
|
||||
const ArifPayView({Key? key, required this.phone}) : super(key: key);
|
||||
|
||||
void _pop(ArifPayViewModel viewModel) => viewModel.pop;
|
||||
|
||||
Future<void> _error() async {
|
||||
// await Navigator.pushNamed(context, AppRoutes.subscriptionErrorPage);
|
||||
// Navigation.pop();
|
||||
}
|
||||
|
||||
void _success() {
|
||||
// Navigation.navigateTo(
|
||||
// AppRoutes.subscriptionSuccessPage,
|
||||
// arguments: widget.body,
|
||||
// );
|
||||
}
|
||||
|
||||
@override
|
||||
void onViewModelReady(ArifPayViewModel viewModel) async {
|
||||
await viewModel.createLearnSubscriptionRequest(phone);
|
||||
super.onViewModelReady(viewModel);
|
||||
}
|
||||
|
||||
@override
|
||||
ArifPayViewModel viewModelBuilder(BuildContext context) => ArifPayViewModel();
|
||||
|
||||
@override
|
||||
Widget builder(
|
||||
BuildContext context,
|
||||
ArifPayViewModel viewModel,
|
||||
Widget? child,
|
||||
) =>
|
||||
_buildScaffoldWrapper(viewModel);
|
||||
|
||||
Widget _buildScaffoldWrapper(ArifPayViewModel viewModel) =>
|
||||
Scaffold(body: _buildScaffoldState(viewModel));
|
||||
|
||||
Widget _buildScaffoldState(ArifPayViewModel viewModel) =>
|
||||
viewModel.busy(StateObjects.learnSubscription)
|
||||
? const PageLoadingIndicator()
|
||||
: _buildScaffold(viewModel);
|
||||
|
||||
Widget _buildScaffold(ArifPayViewModel viewModel) =>
|
||||
SafeArea(child: _buildBody(viewModel));
|
||||
|
||||
Widget _buildBody(ArifPayViewModel viewModel) => InAppWebView(
|
||||
initialUrlRequest:
|
||||
URLRequest(url: WebUri(viewModel.request?.paymentUrl ?? '')),
|
||||
onUpdateVisitedHistory: (controller, url, androidIsReload) {
|
||||
if (url
|
||||
.toString()
|
||||
.contains("https://checkout.arifpay.net/canceled")) {
|
||||
showErrorToast('Operation was cancelled');
|
||||
// _pop();
|
||||
} else if (url.toString().contains(kSuccessUrl)) {
|
||||
_success();
|
||||
} else if (url.toString().contains(kErrorUrl)) {
|
||||
showErrorToast('Operation was cancelled');
|
||||
// _pop();
|
||||
} else if (url.toString().contains("http://x.com/elonmusk/status/")) {
|
||||
_error();
|
||||
} else if (url.toString().contains(kErrorUrl)) {
|
||||
_error();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
71
lib/ui/views/arif_pay/arif_pay_viewmodel.dart
Normal file
71
lib/ui/views/arif_pay/arif_pay_viewmodel.dart
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
import 'package:yimaru_app/ui/common/enmus.dart';
|
||||
|
||||
import '../../../app/app.locator.dart';
|
||||
import '../../../models/learn_subscription_request.dart';
|
||||
import '../../../services/api_service.dart';
|
||||
import '../../../services/status_checker_service.dart';
|
||||
|
||||
class ArifPayViewModel extends BaseViewModel {
|
||||
// Dependency injection
|
||||
|
||||
final _apiService = locator<ApiService>();
|
||||
|
||||
final _statusChecker = locator<StatusCheckerService>();
|
||||
|
||||
final _navigationService = locator<NavigationService>();
|
||||
|
||||
// Learn subscription request
|
||||
LearnSubscriptionRequest? _request;
|
||||
|
||||
LearnSubscriptionRequest? get request => _request;
|
||||
|
||||
// Navigation
|
||||
void pop() => _navigationService.back();
|
||||
|
||||
// Remote api call
|
||||
|
||||
// Learn subscription
|
||||
Future<void> createLearnSubscriptionRequest(String phone) async =>
|
||||
await runBusyFuture(_createLearnSubscriptionRequest(phone),
|
||||
busyObject: StateObjects.learnSubscription);
|
||||
|
||||
Future<void> _createLearnSubscriptionRequest(String phone) async {
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
Map<String, dynamic> data = {
|
||||
'plan_id': 1,
|
||||
'phone': '251$phone',
|
||||
'email': 'test@gmail.com'
|
||||
};
|
||||
|
||||
Map<String, dynamic> response =
|
||||
await _apiService.createSubscriptionRequest(data);
|
||||
|
||||
if (response['status'] == ResponseStatus.success) {
|
||||
_request = response['data'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Future<void> verifyLearnSubscription(String id) async => await runBusyFuture(_verifyLearnSubscription(phone),
|
||||
// busyObject: StateObjects.learnSubscription);
|
||||
//
|
||||
// Future<void> _verifyLearnSubscription(String id) async {
|
||||
// if (await _statusChecker.checkConnection()) {
|
||||
// Map<String,dynamic> data = {
|
||||
// 'plan_id':1,
|
||||
// 'phone': '251$phone',
|
||||
// 'email':'test@gmail.com'
|
||||
// };
|
||||
//
|
||||
// Map<String, dynamic> response =
|
||||
// await _apiService.createSubscriptionRequest(data);
|
||||
//
|
||||
// if (response['status'] == ResponseStatus.success) {
|
||||
// _request = response['data'];
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
|
@ -2,25 +2,19 @@ import 'package:flutter/material.dart';
|
|||
import 'package:stacked/stacked.dart';
|
||||
|
||||
import '../../../models/course_detail.dart';
|
||||
import '../../../models/subcategory.dart';
|
||||
import '../../../models/course_catalog.dart';
|
||||
import '../../common/app_colors.dart';
|
||||
import '../../common/enmus.dart';
|
||||
import '../../common/ui_helpers.dart';
|
||||
import '../../widgets/course_category_card.dart';
|
||||
import '../../widgets/course_tile.dart';
|
||||
import '../../widgets/custom_circular_progress_indicator.dart';
|
||||
import '../../widgets/profile_app_bar.dart';
|
||||
import '../../widgets/small_app_bar.dart';
|
||||
import 'course_viewmodel.dart';
|
||||
|
||||
class CourseView extends StackedView<CourseViewModel> {
|
||||
final Subcategory subcategory;
|
||||
|
||||
const CourseView({Key? key, required this.subcategory}) : super(key: key);
|
||||
|
||||
@override
|
||||
void onViewModelReady(CourseViewModel viewModel) async {
|
||||
await viewModel.getCourseDetails(subcategory.id ?? 0);
|
||||
super.onViewModelReady(viewModel);
|
||||
}
|
||||
const CourseView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
CourseViewModel viewModelBuilder(BuildContext context) => CourseViewModel();
|
||||
|
|
@ -51,78 +45,63 @@ class CourseView extends StackedView<CourseViewModel> {
|
|||
verticalSpaceMedium,
|
||||
_buildAppBar(viewModel),
|
||||
verticalSpaceMedium,
|
||||
_buildCoursesColumnWrapper(viewModel),
|
||||
_buildCategoryColumnWrapper(viewModel)
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildAppBar(CourseViewModel viewModel) => SmallAppBar(
|
||||
onPop: viewModel.pop,
|
||||
showBackButton: true,
|
||||
Widget _buildAppBar(CourseViewModel viewModel) => ProfileAppBar(
|
||||
name: viewModel.user?.firstName,
|
||||
profileImage: viewModel.user?.profilePicture,
|
||||
);
|
||||
|
||||
Widget _buildCoursesColumnWrapper(CourseViewModel viewModel) =>
|
||||
Expanded(child: _buildCoursesColumnScrollView(viewModel));
|
||||
Widget _buildCategoryColumnWrapper(CourseViewModel viewModel) =>
|
||||
Expanded(child: _buildCourseColumnScrollView(viewModel));
|
||||
|
||||
Widget _buildCoursesColumnScrollView(CourseViewModel viewModel) =>
|
||||
Widget _buildCourseColumnScrollView(CourseViewModel viewModel) =>
|
||||
SingleChildScrollView(
|
||||
child: _buildCoursesColumn(viewModel),
|
||||
child: _buildCourseColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildCoursesColumn(CourseViewModel viewModel) => Column(
|
||||
Widget _buildCourseColumn(CourseViewModel viewModel) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildCoursesColumnChildren(viewModel),
|
||||
children: _buildLevelsColumnChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildCoursesColumnChildren(CourseViewModel viewModel) => [
|
||||
verticalSpaceMedium,
|
||||
List<Widget> _buildLevelsColumnChildren(CourseViewModel viewModel) => [
|
||||
_buildTitle(),
|
||||
_buildSubtitle(),
|
||||
verticalSpaceMedium,
|
||||
_buildListViewBuilder(viewModel)
|
||||
_buildListView(viewModel)
|
||||
];
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
'${subcategory.name ?? ''} courses',
|
||||
'Courses',
|
||||
style: style18DG700,
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
'Explore variety of courses on ${subcategory.name ?? ''}.',
|
||||
'Choose a course to improve your professional or exam skills.',
|
||||
style: style14DG400,
|
||||
);
|
||||
|
||||
Widget _buildListViewBuilder(CourseViewModel viewModel) =>
|
||||
viewModel.busy(StateObjects.courses)
|
||||
? _buildProgressIndicator()
|
||||
: _buildListView(viewModel);
|
||||
|
||||
Widget _buildProgressIndicator() => const Center(
|
||||
child: CustomCircularProgressIndicator(color: kcPrimaryColor),
|
||||
);
|
||||
|
||||
Widget _buildListView(CourseViewModel viewModel) => ListView.separated(
|
||||
shrinkWrap: true,
|
||||
itemCount: viewModel.courseDetail.length,
|
||||
itemCount: viewModel.courses.length,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
separatorBuilder: (context, index) => verticalSpaceSmall,
|
||||
itemBuilder: (context, index) => _buildTile(
|
||||
courseDetail: viewModel.courseDetail[index],
|
||||
onCourseTap: () async => await viewModel
|
||||
.navigateToCoursePayment(viewModel.courseDetail[index].course!),
|
||||
onPracticeTap: () async => await viewModel.navigateToCoursePractice(
|
||||
viewModel.courseDetail[index].course?.id ?? 0),
|
||||
),
|
||||
course: viewModel.courses[index],
|
||||
onTap: () async => await viewModel.navigateToCourseCatalog()),
|
||||
separatorBuilder: (context, index) => verticalSpaceSmall,
|
||||
);
|
||||
|
||||
//
|
||||
Widget _buildTile({
|
||||
GestureTapCallback? onCourseTap,
|
||||
GestureTapCallback? onPracticeTap,
|
||||
required CourseDetail courseDetail,
|
||||
required GestureTapCallback onTap,
|
||||
required Map<String, dynamic> course,
|
||||
}) =>
|
||||
CourseTile(
|
||||
onCourseTap: onCourseTap,
|
||||
courseDetail: courseDetail,
|
||||
onPracticeTap: onPracticeTap,
|
||||
CourseCard(
|
||||
onTap: onTap,
|
||||
course: course,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,44 +5,47 @@ import '../../../app/app.locator.dart';
|
|||
import '../../../app/app.router.dart';
|
||||
import '../../../models/course.dart';
|
||||
import '../../../models/course_detail.dart';
|
||||
import '../../../models/user.dart';
|
||||
import '../../../services/authentication_service.dart';
|
||||
import '../../../services/course_service.dart';
|
||||
import '../../../services/status_checker_service.dart';
|
||||
import '../../common/enmus.dart';
|
||||
|
||||
class CourseViewModel extends BaseViewModel {
|
||||
class CourseViewModel extends ReactiveViewModel {
|
||||
// Dependency injection
|
||||
final _courseService = locator<CourseService>();
|
||||
|
||||
final _statusChecker = locator<StatusCheckerService>();
|
||||
|
||||
final _navigationService = locator<NavigationService>();
|
||||
|
||||
// Subcourse with progress
|
||||
List<CourseDetail> _courseDetail = [];
|
||||
final _authenticationService = locator<AuthenticationService>();
|
||||
|
||||
List<CourseDetail> get courseDetail => _courseDetail;
|
||||
@override
|
||||
List<ListenableServiceMixin> get listenableServices =>
|
||||
[_authenticationService];
|
||||
|
||||
// Current user
|
||||
User? get _user => _authenticationService.user;
|
||||
|
||||
User? get user => _user;
|
||||
|
||||
// Course
|
||||
final List<Map<String, dynamic>> _courses = [
|
||||
{
|
||||
'title': 'English Proficiency Exams',
|
||||
'description':
|
||||
'Prepare for IELTS, TOEFL, or Duolingo with structured practice.'
|
||||
},
|
||||
{
|
||||
'title': 'Skill-Based Courses',
|
||||
'description':
|
||||
'Learn English for the workplace, travel, and real-life communication.'
|
||||
},
|
||||
];
|
||||
|
||||
List<Map<String, dynamic>> get courses => _courses;
|
||||
|
||||
// Navigation
|
||||
void pop() => _navigationService.back();
|
||||
|
||||
Future<void> navigateToCoursePractice(int id) =>
|
||||
_navigationService.navigateToCoursePracticeView(id: id);
|
||||
|
||||
Future<void> navigateToCoursePayment(Course course) async =>
|
||||
_navigationService.navigateToCoursePaymentView(course: course);
|
||||
|
||||
// Remote api call
|
||||
|
||||
// Course detail
|
||||
Future<void> getCourseDetails(int id) async =>
|
||||
await runBusyFuture(_getCourseDetails(id),
|
||||
busyObject: StateObjects.courses);
|
||||
|
||||
Future<void> _getCourseDetails(int id) async {
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
_courseDetail = await _courseService.getCoursesDetail(id);
|
||||
// _courseDetail.sort((a, b) =>
|
||||
// (a.course?.displayOrder ?? 0).compareTo(b.course?.displayOrder ?? 0));
|
||||
}
|
||||
}
|
||||
Future<void> navigateToCourseCatalog() async =>
|
||||
await _navigationService.navigateToCourseCatalogView();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,55 +1,50 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:yimaru_app/models/course_catalog.dart';
|
||||
|
||||
import '../../../models/category.dart';
|
||||
import '../../../models/subcategory.dart';
|
||||
import '../../common/app_colors.dart';
|
||||
import '../../common/enmus.dart';
|
||||
import '../../common/ui_helpers.dart';
|
||||
import '../../widgets/course_subcategory_tile.dart';
|
||||
import '../../widgets/course_catalog_tile.dart';
|
||||
import '../../widgets/custom_circular_progress_indicator.dart';
|
||||
import '../../widgets/small_app_bar.dart';
|
||||
import 'course_subcategory_viewmodel.dart';
|
||||
import 'course_catalog_viewmodel.dart';
|
||||
|
||||
class CourseSubcategoryView extends StackedView<CourseSubcategoryViewModel> {
|
||||
final Category category;
|
||||
|
||||
const CourseSubcategoryView({Key? key, required this.category})
|
||||
: super(key: key);
|
||||
class CourseCatalogView extends StackedView<CourseCatalogViewModel> {
|
||||
const CourseCatalogView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
void onViewModelReady(CourseSubcategoryViewModel viewModel) async {
|
||||
await viewModel.getSubcategories(category.id ?? 0);
|
||||
void onViewModelReady(CourseCatalogViewModel viewModel) async {
|
||||
await viewModel.getCourseCatalogs();
|
||||
super.onViewModelReady(viewModel);
|
||||
}
|
||||
|
||||
@override
|
||||
CourseSubcategoryViewModel viewModelBuilder(BuildContext context) =>
|
||||
CourseSubcategoryViewModel();
|
||||
CourseCatalogViewModel viewModelBuilder(BuildContext context) =>
|
||||
CourseCatalogViewModel();
|
||||
|
||||
@override
|
||||
Widget builder(
|
||||
BuildContext context,
|
||||
CourseSubcategoryViewModel viewModel,
|
||||
CourseCatalogViewModel viewModel,
|
||||
Widget? child,
|
||||
) =>
|
||||
_buildScaffoldWrapper(viewModel);
|
||||
|
||||
Widget _buildScaffoldWrapper(CourseSubcategoryViewModel viewModel) =>
|
||||
Scaffold(
|
||||
Widget _buildScaffoldWrapper(CourseCatalogViewModel viewModel) => Scaffold(
|
||||
backgroundColor: kcBackgroundColor,
|
||||
body: _buildScaffold(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildScaffold(CourseSubcategoryViewModel viewModel) =>
|
||||
Widget _buildScaffold(CourseCatalogViewModel viewModel) =>
|
||||
SafeArea(child: _buildBody(viewModel));
|
||||
|
||||
Widget _buildBody(CourseSubcategoryViewModel viewModel) => Padding(
|
||||
Widget _buildBody(CourseCatalogViewModel viewModel) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: _buildColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildColumn(CourseSubcategoryViewModel viewModel) => Column(
|
||||
Widget _buildColumn(CourseCatalogViewModel viewModel) => Column(
|
||||
children: [
|
||||
verticalSpaceMedium,
|
||||
_buildAppBar(viewModel),
|
||||
|
|
@ -58,27 +53,26 @@ class CourseSubcategoryView extends StackedView<CourseSubcategoryViewModel> {
|
|||
],
|
||||
);
|
||||
|
||||
Widget _buildAppBar(CourseSubcategoryViewModel viewModel) => SmallAppBar(
|
||||
Widget _buildAppBar(CourseCatalogViewModel viewModel) => SmallAppBar(
|
||||
onPop: viewModel.pop,
|
||||
showBackButton: true,
|
||||
);
|
||||
|
||||
Widget _buildCoursesColumnWrapper(CourseSubcategoryViewModel viewModel) =>
|
||||
Widget _buildCoursesColumnWrapper(CourseCatalogViewModel viewModel) =>
|
||||
Expanded(child: _buildCoursesColumnScrollView(viewModel));
|
||||
|
||||
Widget _buildCoursesColumnScrollView(CourseSubcategoryViewModel viewModel) =>
|
||||
Widget _buildCoursesColumnScrollView(CourseCatalogViewModel viewModel) =>
|
||||
SingleChildScrollView(
|
||||
child: _buildCoursesColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildCoursesColumn(CourseSubcategoryViewModel viewModel) => Column(
|
||||
Widget _buildCoursesColumn(CourseCatalogViewModel viewModel) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildCoursesColumnChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildCoursesColumnChildren(
|
||||
CourseSubcategoryViewModel viewModel) =>
|
||||
List<Widget> _buildCoursesColumnChildren(CourseCatalogViewModel viewModel) =>
|
||||
[
|
||||
_buildTitle(),
|
||||
_buildSubtitle(),
|
||||
|
|
@ -87,17 +81,17 @@ class CourseSubcategoryView extends StackedView<CourseSubcategoryViewModel> {
|
|||
];
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
'${category.name ?? ''} courses',
|
||||
'English Proficiency Exams',
|
||||
style: style18DG700,
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
'Explore variety of courses on this category.',
|
||||
'Select your target exam and start preparing',
|
||||
style: style14DG400,
|
||||
);
|
||||
|
||||
Widget _buildListViewBuilder(CourseSubcategoryViewModel viewModel) =>
|
||||
viewModel.busy(StateObjects.subcategories)
|
||||
Widget _buildListViewBuilder(CourseCatalogViewModel viewModel) =>
|
||||
viewModel.busy(StateObjects.courseCatalogs)
|
||||
? _buildProgressIndicator()
|
||||
: _buildListView(viewModel);
|
||||
|
||||
|
|
@ -105,27 +99,25 @@ class CourseSubcategoryView extends StackedView<CourseSubcategoryViewModel> {
|
|||
child: CustomCircularProgressIndicator(color: kcPrimaryColor),
|
||||
);
|
||||
|
||||
Widget _buildListView(CourseSubcategoryViewModel viewModel) =>
|
||||
ListView.separated(
|
||||
Widget _buildListView(CourseCatalogViewModel viewModel) => ListView.separated(
|
||||
shrinkWrap: true,
|
||||
itemCount: viewModel.subcategories.length,
|
||||
itemCount: viewModel.courseCatalogs.length,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) => _buildTile(
|
||||
subcategory: viewModel.subcategories[index],
|
||||
onCourseTap: () async => await viewModel
|
||||
.navigateToSubcourse(viewModel.subcategories[index]),
|
||||
),
|
||||
courseCatalog: viewModel.courseCatalogs[index],
|
||||
onCourseTap: () {},
|
||||
onPracticeTap: () {}),
|
||||
separatorBuilder: (context, index) => verticalSpaceSmall,
|
||||
);
|
||||
|
||||
Widget _buildTile({
|
||||
GestureTapCallback? onCourseTap,
|
||||
GestureTapCallback? onPracticeTap,
|
||||
required Subcategory subcategory,
|
||||
required CourseCatalog courseCatalog,
|
||||
required GestureTapCallback onCourseTap,
|
||||
required GestureTapCallback onPracticeTap,
|
||||
}) =>
|
||||
CourseSubcategoryTile(
|
||||
subcategory: subcategory,
|
||||
CourseCatalogTile(
|
||||
onCourseTap: onCourseTap,
|
||||
onPracticeTap: onPracticeTap,
|
||||
courseCatalog: courseCatalog,
|
||||
);
|
||||
}
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
import 'package:yimaru_app/models/course_catalog.dart';
|
||||
|
||||
import '../../../app/app.locator.dart';
|
||||
import '../../../app/app.router.dart';
|
||||
import '../../../models/subcategory.dart';
|
||||
import '../../../services/api_service.dart';
|
||||
import '../../../services/status_checker_service.dart';
|
||||
import '../../common/enmus.dart';
|
||||
|
||||
class CourseSubcategoryViewModel extends BaseViewModel {
|
||||
class CourseCatalogViewModel extends BaseViewModel {
|
||||
// Dependency injection
|
||||
final _apiService = locator<ApiService>();
|
||||
|
||||
|
|
@ -16,10 +16,10 @@ class CourseSubcategoryViewModel extends BaseViewModel {
|
|||
|
||||
final _navigationService = locator<NavigationService>();
|
||||
|
||||
// Course subcategories
|
||||
List<Subcategory> _subcategories = [];
|
||||
// Course catalogs
|
||||
List<CourseCatalog> _courseCatalogs = [];
|
||||
|
||||
List<Subcategory> get subcategories => _subcategories;
|
||||
List<CourseCatalog> get courseCatalogs => _courseCatalogs;
|
||||
|
||||
// Navigation
|
||||
void pop() => _navigationService.back();
|
||||
|
|
@ -27,19 +27,19 @@ class CourseSubcategoryViewModel extends BaseViewModel {
|
|||
Future<void> navigateToCoursePractice(int id) async =>
|
||||
_navigationService.navigateToCoursePracticeView(id: id);
|
||||
|
||||
Future<void> navigateToSubcourse(Subcategory subcategory) async =>
|
||||
_navigationService.navigateToCourseView(subcategory: subcategory);
|
||||
// Future<void> navigateToSubcourse(Subcategory subcategory) async =>
|
||||
// _navigationService.navigateToCourseView(subcategory: subcategory);
|
||||
|
||||
// Remote api call
|
||||
|
||||
// Course subcategories
|
||||
Future<void> getSubcategories(int id) async =>
|
||||
await runBusyFuture(_getSubcategories(id),
|
||||
busyObject: StateObjects.subcategories);
|
||||
// Course catalogs
|
||||
Future<void> getCourseCatalogs() async =>
|
||||
await runBusyFuture(_getSubcategories(),
|
||||
busyObject: StateObjects.courseCatalogs);
|
||||
|
||||
Future<void> _getSubcategories(int id) async {
|
||||
Future<void> _getSubcategories() async {
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
_subcategories = await _apiService.getSubcategories(id);
|
||||
_courseCatalogs = await _apiService.getCourseCatalogs();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
import '../../../models/category.dart';
|
||||
import '../../common/app_colors.dart';
|
||||
import '../../common/enmus.dart';
|
||||
import '../../common/ui_helpers.dart';
|
||||
import '../../widgets/course_category_card.dart';
|
||||
import '../../widgets/custom_circular_progress_indicator.dart';
|
||||
import '../../widgets/profile_app_bar.dart';
|
||||
import 'course_category_viewmodel.dart';
|
||||
|
||||
class CourseCategoryView extends StackedView<CourseCategoryViewModel> {
|
||||
const CourseCategoryView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
void onViewModelReady(CourseCategoryViewModel viewModel) async {
|
||||
await viewModel.getCategories();
|
||||
super.onViewModelReady(viewModel);
|
||||
}
|
||||
|
||||
@override
|
||||
CourseCategoryViewModel viewModelBuilder(BuildContext context) =>
|
||||
CourseCategoryViewModel();
|
||||
|
||||
@override
|
||||
Widget builder(
|
||||
BuildContext context,
|
||||
CourseCategoryViewModel viewModel,
|
||||
Widget? child,
|
||||
) =>
|
||||
_buildScaffoldWrapper(viewModel);
|
||||
|
||||
Widget _buildScaffoldWrapper(CourseCategoryViewModel viewModel) => Scaffold(
|
||||
backgroundColor: kcBackgroundColor,
|
||||
body: _buildScaffold(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildScaffold(CourseCategoryViewModel viewModel) =>
|
||||
SafeArea(child: _buildBody(viewModel));
|
||||
|
||||
Widget _buildBody(CourseCategoryViewModel viewModel) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: _buildColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildColumn(CourseCategoryViewModel viewModel) => Column(
|
||||
children: [
|
||||
verticalSpaceMedium,
|
||||
_buildAppBar(viewModel),
|
||||
verticalSpaceMedium,
|
||||
_buildCategoryColumnWrapper(viewModel)
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildAppBar(CourseCategoryViewModel viewModel) => ProfileAppBar(
|
||||
name: viewModel.user?.firstName,
|
||||
profileImage: viewModel.user?.profilePicture,
|
||||
);
|
||||
|
||||
Widget _buildCategoryColumnWrapper(CourseCategoryViewModel viewModel) =>
|
||||
Expanded(child: _buildCourseColumnScrollView(viewModel));
|
||||
|
||||
Widget _buildCourseColumnScrollView(CourseCategoryViewModel viewModel) =>
|
||||
SingleChildScrollView(
|
||||
child: _buildCourseColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildCourseColumn(CourseCategoryViewModel viewModel) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildLevelsColumnChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildLevelsColumnChildren(CourseCategoryViewModel viewModel) =>
|
||||
[
|
||||
_buildTitle(),
|
||||
_buildSubtitle(),
|
||||
verticalSpaceMedium,
|
||||
_buildListViewBuilder(viewModel)
|
||||
];
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
'Courses',
|
||||
style: style18DG700,
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
'Choose a course to improve your professional or exam skills.',
|
||||
style: style14DG400,
|
||||
);
|
||||
|
||||
Widget _buildListViewBuilder(CourseCategoryViewModel viewModel) =>
|
||||
viewModel.busy(StateObjects.courseCategories)
|
||||
? _buildProgressIndicator()
|
||||
: _buildListView(viewModel);
|
||||
|
||||
Widget _buildProgressIndicator() => const Center(
|
||||
child: CustomCircularProgressIndicator(color: kcPrimaryColor),
|
||||
);
|
||||
|
||||
Widget _buildListView(CourseCategoryViewModel viewModel) =>
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
itemCount: viewModel.categories.length,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) => _buildTile(
|
||||
category: viewModel.categories[index],
|
||||
onTap: () async => await viewModel
|
||||
.navigateToCourseCategory(viewModel.categories[index]),
|
||||
),
|
||||
separatorBuilder: (context, index) => verticalSpaceSmall,
|
||||
);
|
||||
|
||||
//
|
||||
Widget _buildTile({
|
||||
required Category category,
|
||||
required GestureTapCallback onTap,
|
||||
}) =>
|
||||
CourseCategoryCard(
|
||||
onTap: onTap,
|
||||
category: category,
|
||||
);
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
|
||||
import '../../../app/app.locator.dart';
|
||||
import '../../../app/app.router.dart';
|
||||
import '../../../models/category.dart';
|
||||
import '../../../models/user.dart';
|
||||
import '../../../services/api_service.dart';
|
||||
import '../../../services/authentication_service.dart';
|
||||
import '../../../services/status_checker_service.dart';
|
||||
import '../../common/enmus.dart';
|
||||
|
||||
class CourseCategoryViewModel extends ReactiveViewModel {
|
||||
// Dependency injection
|
||||
final _apiService = locator<ApiService>();
|
||||
|
||||
final _statusChecker = locator<StatusCheckerService>();
|
||||
|
||||
final _navigationService = locator<NavigationService>();
|
||||
|
||||
final _authenticationService = locator<AuthenticationService>();
|
||||
|
||||
@override
|
||||
List<ListenableServiceMixin> get listenableServices =>
|
||||
[_authenticationService];
|
||||
|
||||
// Current user
|
||||
User? get _user => _authenticationService.user;
|
||||
|
||||
User? get user => _user;
|
||||
|
||||
// Course categories
|
||||
List<Category> _categories = [];
|
||||
|
||||
List<Category> get categories => _categories;
|
||||
|
||||
// Navigation
|
||||
Future<void> navigateToCourseCategory(Category category) async =>
|
||||
_navigationService.navigateToCourseSubcategoryView(category: category);
|
||||
|
||||
// Remote api call
|
||||
|
||||
// Course categories
|
||||
Future<void> getCategories() async => await runBusyFuture(_getCategories(),
|
||||
busyObject: StateObjects.courseCategories);
|
||||
|
||||
Future<void> _getCategories() async {
|
||||
if (categories.isEmpty) {
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
_categories = await _apiService.getCategories();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
lib/ui/views/course_unit/course_unit_view.dart
Normal file
29
lib/ui/views/course_unit/course_unit_view.dart
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
import 'course_unit_viewmodel.dart';
|
||||
|
||||
class CourseUnitView extends StackedView<CourseUnitViewModel> {
|
||||
const CourseUnitView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget builder(
|
||||
BuildContext context,
|
||||
CourseUnitViewModel viewModel,
|
||||
Widget? child,
|
||||
) {
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
body: Container(
|
||||
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
|
||||
child: const Center(child: Text("CourseUnitView")),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
CourseUnitViewModel viewModelBuilder(
|
||||
BuildContext context,
|
||||
) =>
|
||||
CourseUnitViewModel();
|
||||
}
|
||||
3
lib/ui/views/course_unit/course_unit_viewmodel.dart
Normal file
3
lib/ui/views/course_unit/course_unit_viewmodel.dart
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class CourseUnitViewModel extends BaseViewModel {}
|
||||
|
|
@ -110,8 +110,6 @@ class ForgetPasswordViewModel extends FormViewModel {
|
|||
_length = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (password == confirmPassword) {
|
||||
_passwordMatch = true;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:yimaru_app/ui/views/learn_program/learn_program_view.dart';
|
|||
import 'package:yimaru_app/ui/views/profile/profile_view.dart';
|
||||
import 'package:yimaru_app/ui/widgets/coming_soon.dart';
|
||||
|
||||
import '../course/course_view.dart';
|
||||
import 'home_viewmodel.dart';
|
||||
|
||||
class HomeView extends StackedView<HomeViewModel> {
|
||||
|
|
@ -69,7 +70,7 @@ class HomeView extends StackedView<HomeViewModel> {
|
|||
case 0:
|
||||
return const LearnProgramView();
|
||||
case 1:
|
||||
return const ComingSoon();
|
||||
return const CourseView();
|
||||
|
||||
default:
|
||||
return const ProfileView();
|
||||
|
|
|
|||
|
|
@ -78,15 +78,15 @@ class LearnCourseView extends StackedView<LearnCourseViewModel> {
|
|||
|
||||
Widget _buildListView(LearnCourseViewModel viewModel) => ListView.separated(
|
||||
shrinkWrap: true,
|
||||
itemCount: viewModel.learnCourses.length,
|
||||
itemCount: viewModel.courses.length,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) => _buildTile(
|
||||
course: viewModel.learnCourses[index],
|
||||
onViewTap: () async => await viewModel
|
||||
.navigateToLearnModule(viewModel.learnCourses[index]),
|
||||
course: viewModel.courses[index],
|
||||
onViewTap: () async =>
|
||||
await viewModel.navigateToLearnModule(viewModel.courses[index]),
|
||||
onPracticeTap: () async => await viewModel.navigateToLearnPractice(
|
||||
id: viewModel.learnCourses[index].id ?? 0,
|
||||
level: viewModel.learnCourses[index].name ?? ''),
|
||||
id: viewModel.courses[index].id ?? 0,
|
||||
level: viewModel.courses[index].name ?? ''),
|
||||
),
|
||||
separatorBuilder: (context, index) => verticalSpaceSmall,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,21 +5,25 @@ import 'package:yimaru_app/app/app.router.dart';
|
|||
import '../../../app/app.locator.dart';
|
||||
import '../../../models/learn_course.dart';
|
||||
import '../../../services/api_service.dart';
|
||||
import '../../../services/learn_service.dart';
|
||||
import '../../../services/status_checker_service.dart';
|
||||
import '../../common/enmus.dart';
|
||||
|
||||
class LearnCourseViewModel extends BaseViewModel {
|
||||
class LearnCourseViewModel extends ReactiveViewModel {
|
||||
// Dependency injection
|
||||
final _apiService = locator<ApiService>();
|
||||
final _learnService = locator<LearnService>();
|
||||
|
||||
final _statusChecker = locator<StatusCheckerService>();
|
||||
|
||||
final _navigationService = locator<NavigationService>();
|
||||
|
||||
// Learn courses
|
||||
List<LearnCourse> _learnCourses = [];
|
||||
@override
|
||||
List<ListenableServiceMixin> get listenableServices => [_learnService];
|
||||
|
||||
List<LearnCourse> get learnCourses => _learnCourses;
|
||||
// Learn lessons
|
||||
List<LearnCourse> get _courses => _learnService.courses;
|
||||
|
||||
List<LearnCourse> get courses => _courses;
|
||||
|
||||
// Navigation
|
||||
void pop() => _navigationService.back();
|
||||
|
|
@ -46,12 +50,8 @@ class LearnCourseViewModel extends BaseViewModel {
|
|||
busyObject: StateObjects.learnCourses);
|
||||
|
||||
Future<void> _getLearnCourses(int id) async {
|
||||
if (_learnCourses.isEmpty) {
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
_learnCourses = await _apiService.getLearnCourse(id);
|
||||
_learnCourses
|
||||
.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
|
||||
}
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
await _learnService.getLearnCourses(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,7 +133,11 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
|
|||
style: style14DG500,
|
||||
);
|
||||
|
||||
Widget _buildModuleProgress() => const ModuleProgress();
|
||||
Widget _buildModuleProgress() => ModuleProgress(
|
||||
total: module.access?.totalCount ?? 0,
|
||||
completed: module.access?.completedCount ?? 0,
|
||||
progress: module.access?.progressPercent ?? 0,
|
||||
);
|
||||
|
||||
Widget _buildMotivationCard() => const MotivationCard();
|
||||
|
||||
|
|
@ -159,6 +163,7 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
|
|||
index: index,
|
||||
lesson: viewModel.lessons[index],
|
||||
onLessonTap: () async => await viewModel.navigateToLearnLessonDetail(
|
||||
module: module,
|
||||
lesson: viewModel.lessons[index],
|
||||
hasPractice:
|
||||
index != viewModel.lessons.length - 1 ? true : false),
|
||||
|
|
|
|||
|
|
@ -5,19 +5,23 @@ import 'package:yimaru_app/models/learn_lesson.dart';
|
|||
import 'package:yimaru_app/ui/common/enmus.dart';
|
||||
|
||||
import '../../../app/app.locator.dart';
|
||||
import '../../../services/api_service.dart';
|
||||
import '../../../models/learn_module.dart';
|
||||
import '../../../services/learn_service.dart';
|
||||
import '../../../services/status_checker_service.dart';
|
||||
|
||||
class LearnLessonViewModel extends BaseViewModel {
|
||||
class LearnLessonViewModel extends ReactiveViewModel {
|
||||
// Dependency injection
|
||||
final _apiService = locator<ApiService>();
|
||||
final _learnService = locator<LearnService>();
|
||||
|
||||
final _statusChecker = locator<StatusCheckerService>();
|
||||
|
||||
final _navigationService = locator<NavigationService>();
|
||||
|
||||
@override
|
||||
List<ListenableServiceMixin> get listenableServices => [_learnService];
|
||||
|
||||
// Learn lessons
|
||||
List<LearnLesson> _lessons = [];
|
||||
List<LearnLesson> get _lessons => _learnService.lessons;
|
||||
|
||||
List<LearnLesson> get lessons => _lessons;
|
||||
|
||||
|
|
@ -35,9 +39,11 @@ class LearnLessonViewModel extends BaseViewModel {
|
|||
);
|
||||
|
||||
Future<void> navigateToLearnLessonDetail(
|
||||
{required bool hasPractice, required LearnLesson lesson}) async =>
|
||||
{required bool hasPractice,
|
||||
required LearnLesson lesson,
|
||||
required LearnModule module}) async =>
|
||||
await _navigationService.navigateToLearnLessonDetailView(
|
||||
lesson: lesson, hasPractice: hasPractice);
|
||||
lesson: lesson, module: module, hasPractice: hasPractice);
|
||||
|
||||
// Remote api call
|
||||
|
||||
|
|
@ -46,11 +52,8 @@ class LearnLessonViewModel extends BaseViewModel {
|
|||
busyObject: StateObjects.learnLessons);
|
||||
|
||||
Future<void> _getLessons(int id) async {
|
||||
if (_lessons.isEmpty) {
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
_lessons = await _apiService.getLearnLessons(id);
|
||||
_lessons.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
|
||||
}
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
await _learnService.getLearnLessons(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:chewie/chewie.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:vimeo_video_player/vimeo_video_player.dart';
|
||||
import 'package:yimaru_app/models/learn_lesson.dart';
|
||||
import 'package:yimaru_app/models/learn_module.dart';
|
||||
|
||||
import '../../common/app_colors.dart';
|
||||
import '../../common/enmus.dart';
|
||||
|
|
@ -14,10 +14,14 @@ import 'learn_lesson_detail_viewmodel.dart';
|
|||
|
||||
class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
|
||||
final bool hasPractice;
|
||||
final LearnModule module;
|
||||
final LearnLesson lesson;
|
||||
|
||||
const LearnLessonDetailView(
|
||||
{Key? key, required this.lesson, required this.hasPractice})
|
||||
{Key? key,
|
||||
required this.lesson,
|
||||
required this.module,
|
||||
required this.hasPractice})
|
||||
: super(key: key);
|
||||
|
||||
Future<void> _navigate(LearnLessonDetailViewModel viewModel) async {
|
||||
|
|
@ -27,7 +31,11 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
|
|||
|
||||
@override
|
||||
void onViewModelReady(LearnLessonDetailViewModel viewModel) async {
|
||||
await viewModel.initializePlayer(lesson.videoUrl ?? '');
|
||||
await viewModel.initializePlayer(
|
||||
lessonId: lesson.id ?? 0,
|
||||
moduleId: module.id ?? 0,
|
||||
url: lesson.videoUrl ?? '',
|
||||
);
|
||||
super.onViewModelReady(viewModel);
|
||||
}
|
||||
|
||||
|
|
@ -144,8 +152,9 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
|
|||
Widget _buildVideoPlayer(LearnLessonDetailViewModel viewModel) =>
|
||||
_buildChewiePlayer(viewModel);
|
||||
|
||||
Widget _buildChewiePlayer(LearnLessonDetailViewModel viewModel) =>
|
||||
Chewie(controller: viewModel.chewieController!);
|
||||
Widget _buildChewiePlayer(LearnLessonDetailViewModel viewModel) => Chewie(
|
||||
controller: viewModel.chewieController!,
|
||||
);
|
||||
|
||||
Widget _buildEmptyVideoPlayer() => const EmptyVideoPlayer();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:chewie/chewie.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
|
@ -7,19 +6,37 @@ import 'package:yimaru_app/ui/common/enmus.dart';
|
|||
|
||||
import '../../../app/app.locator.dart';
|
||||
import '../../../app/app.router.dart';
|
||||
import '../../../models/learn_lesson.dart';
|
||||
import '../../../services/api_service.dart';
|
||||
import '../../../services/learn_service.dart';
|
||||
import '../../../services/status_checker_service.dart';
|
||||
import '../../../services/vimeo_service.dart';
|
||||
import '../../common/app_constants.dart';
|
||||
import '../../common/helper_functions.dart';
|
||||
import '../../common/ui_helpers.dart';
|
||||
|
||||
class LearnLessonDetailViewModel extends BaseViewModel {
|
||||
class LearnLessonDetailViewModel extends ReactiveViewModel {
|
||||
// Dependency injection
|
||||
|
||||
final _apiService = locator<ApiService>();
|
||||
|
||||
final _learnService = locator<LearnService>();
|
||||
|
||||
final _vimeoService = locator<VimeoService>();
|
||||
|
||||
final _statusChecker = locator<StatusCheckerService>();
|
||||
|
||||
final _navigationService = locator<NavigationService>();
|
||||
|
||||
@override
|
||||
List<ListenableServiceMixin> get listenableServices => [_learnService];
|
||||
|
||||
// Learn lessons
|
||||
List<LearnLesson> get _lessons => _learnService.lessons;
|
||||
|
||||
List<LearnLesson> get lessons => _lessons;
|
||||
|
||||
// Video player config
|
||||
bool _lessonCompleted = false;
|
||||
|
||||
ChewieController? _chewieController;
|
||||
|
||||
ChewieController? get chewieController => _chewieController;
|
||||
|
|
@ -38,11 +55,18 @@ class LearnLessonDetailViewModel extends BaseViewModel {
|
|||
await _chewieController?.pause();
|
||||
}
|
||||
|
||||
Future<void> initializePlayer(String url) async =>
|
||||
await runBusyFuture(_initializePlayer(url),
|
||||
Future<void> initializePlayer(
|
||||
{required String url,
|
||||
required int lessonId,
|
||||
required int moduleId}) async =>
|
||||
await runBusyFuture(
|
||||
_initializePlayer(url: url, moduleId: moduleId, lessonId: lessonId),
|
||||
busyObject: StateObjects.loadLessonVideo);
|
||||
|
||||
Future<void> _initializePlayer(String url) async {
|
||||
Future<void> _initializePlayer(
|
||||
{required String url,
|
||||
required int lessonId,
|
||||
required int moduleId}) async {
|
||||
final playableUrl = await _vimeoService.getVideoUrl(url);
|
||||
|
||||
if (playableUrl == null) {
|
||||
|
|
@ -54,11 +78,41 @@ class LearnLessonDetailViewModel extends BaseViewModel {
|
|||
|
||||
await _videoPlayerController?.initialize();
|
||||
|
||||
// Listen for video completion
|
||||
_videoPlayerController?.addListener(() async {
|
||||
final controller = _videoPlayerController;
|
||||
|
||||
if (controller == null || _lessonCompleted) return;
|
||||
|
||||
final position = controller.value.position.inSeconds;
|
||||
final duration = controller.value.duration.inSeconds;
|
||||
|
||||
if (duration <= 0) return;
|
||||
|
||||
// Calculate watched percentage
|
||||
final progress = position / duration;
|
||||
|
||||
// Mark complete at 95%
|
||||
if (progress >= 0.95) {
|
||||
_lessonCompleted = true;
|
||||
|
||||
await completeLearnLesson(
|
||||
lessonId: lessonId,
|
||||
moduleId: moduleId,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
_chewieController = ChewieController(
|
||||
videoPlayerController: _videoPlayerController!,
|
||||
looping: false,
|
||||
autoPlay: true,
|
||||
looping: true,
|
||||
showOptions: true,
|
||||
showControls: true,
|
||||
aspectRatio: 16 / 9,
|
||||
autoInitialize: true,
|
||||
allowedScreenSleep: false,
|
||||
videoPlayerController: _videoPlayerController!,
|
||||
materialProgressColors: buildChewieProgressIndicator,
|
||||
);
|
||||
|
||||
notifyListeners();
|
||||
|
|
@ -76,4 +130,24 @@ class LearnLessonDetailViewModel extends BaseViewModel {
|
|||
subtitle:
|
||||
'I’ll ask you a few questions, and you can respond naturally.',
|
||||
);
|
||||
|
||||
// Remote api call
|
||||
|
||||
// Mark lesson
|
||||
Future<void> completeLearnLesson(
|
||||
{required int lessonId, required int moduleId}) async =>
|
||||
await runBusyFuture(
|
||||
_completeLearnLesson(lessonId: lessonId, moduleId: moduleId),
|
||||
busyObject: StateObjects.learnLessons);
|
||||
|
||||
Future<void> _completeLearnLesson(
|
||||
{required int lessonId, required int moduleId}) async {
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
Map<String, dynamic> response =
|
||||
await _apiService.completeLearnLesson(lessonId);
|
||||
if (response['status'] == ResponseStatus.success) {
|
||||
await _learnService.getLearnLessons(moduleId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
|
|||
|
||||
Widget _buildOverallProgress() => OverallLearnProgress(
|
||||
indicatorBackgroundColor: kcWhite,
|
||||
progress: course.access?.progressPercent ?? 0,
|
||||
backgroundColor: kcPrimaryColor.withOpacity(0.1),
|
||||
);
|
||||
|
||||
|
|
@ -116,6 +117,7 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
|
|||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) => _buildTile(
|
||||
module: viewModel.modules[index],
|
||||
onLockTap: () async => await viewModel.navigateToLearnSubscription(),
|
||||
onPracticeTap: () async => await viewModel.navigateToLearnPractice(
|
||||
id: viewModel.modules[index].id ?? 0,
|
||||
module: viewModel.modules[index].name ?? ''),
|
||||
|
|
@ -126,11 +128,13 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
|
|||
|
||||
Widget _buildTile({
|
||||
required LearnModule module,
|
||||
required GestureTapCallback onLockTap,
|
||||
required GestureTapCallback onModuleTap,
|
||||
required GestureTapCallback onPracticeTap,
|
||||
}) =>
|
||||
LearnModuleTile(
|
||||
module: module,
|
||||
onLockTap: onLockTap,
|
||||
onModuleTap: onModuleTap,
|
||||
onPracticeTap: onPracticeTap);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,25 +5,32 @@ import 'package:yimaru_app/models/learn_module.dart';
|
|||
|
||||
import '../../../app/app.locator.dart';
|
||||
import '../../../services/api_service.dart';
|
||||
import '../../../services/learn_service.dart';
|
||||
import '../../../services/status_checker_service.dart';
|
||||
import '../../common/enmus.dart';
|
||||
|
||||
class LearnModuleViewModel extends BaseViewModel {
|
||||
class LearnModuleViewModel extends ReactiveViewModel {
|
||||
// Dependency injection
|
||||
final _apiService = locator<ApiService>();
|
||||
final _learnService = locator<LearnService>();
|
||||
|
||||
final _statusChecker = locator<StatusCheckerService>();
|
||||
|
||||
final _navigationService = locator<NavigationService>();
|
||||
|
||||
@override
|
||||
List<ListenableServiceMixin> get listenableServices => [_learnService];
|
||||
|
||||
// Learn module
|
||||
List<LearnModule> _modules = [];
|
||||
List<LearnModule> get _modules => _learnService.modules;
|
||||
|
||||
List<LearnModule> get modules => _modules;
|
||||
|
||||
// Navigation
|
||||
void pop() => _navigationService.back();
|
||||
|
||||
Future<void> navigateToLearnSubscription() async =>
|
||||
await _navigationService.navigateToLearnSubscriptionView();
|
||||
|
||||
Future<void> navigateToLearnLesson(LearnModule module) async =>
|
||||
await _navigationService.navigateToLearnLessonView(module: module);
|
||||
|
||||
|
|
@ -45,11 +52,8 @@ class LearnModuleViewModel extends BaseViewModel {
|
|||
busyObject: StateObjects.learnModules);
|
||||
|
||||
Future<void> _getLearnModules(int id) async {
|
||||
if (_modules.isEmpty) {
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
_modules = await _apiService.getLearnModules(id);
|
||||
_modules.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
|
||||
}
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
await _learnService.getLearnModules(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
|
|||
);
|
||||
|
||||
Widget _buildBodyState(LearnPracticeViewModel viewModel) =>
|
||||
viewModel.busy(StateObjects.learnPractices)
|
||||
viewModel.busy(StateObjects.learnPractice)
|
||||
? const PageLoadingIndicator()
|
||||
: viewModel.practices.isEmpty || viewModel.questions.isEmpty
|
||||
? _buildPageLoadingIndicator(viewModel)
|
||||
|
|
@ -101,7 +101,7 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
|
|||
|
||||
Widget _buildPageLoadingIndicator(LearnPracticeViewModel viewModel) =>
|
||||
LearnLoadingScreen(
|
||||
isLoading: viewModel.busy(StateObjects.learnPractices),
|
||||
isLoading: viewModel.busy(StateObjects.learnPractice),
|
||||
onTap: () async =>
|
||||
await viewModel.getLearnPractices(id: id, practice: practice),
|
||||
onPop: viewModel.practices.isEmpty || viewModel.questions.isEmpty
|
||||
|
|
@ -142,6 +142,7 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
|
|||
Widget _buildLearnPracticeResultScreen() =>
|
||||
LearnPracticeResultScreen(practice: practice);
|
||||
|
||||
Widget _buildLearnPracticeCompletionScreen() =>
|
||||
LearnPracticeCompletionScreen(level: level ?? '');
|
||||
Widget _buildLearnPracticeCompletionScreen() => LearnPracticeCompletionScreen(
|
||||
level: level ?? '',
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,13 +13,17 @@ import '../../../app/app.locator.dart';
|
|||
import '../../../models/learn_question.dart';
|
||||
import '../../../services/api_service.dart';
|
||||
import '../../../services/audio_player_service.dart';
|
||||
import '../../../services/learn_service.dart';
|
||||
import '../../../services/status_checker_service.dart';
|
||||
import '../../common/app_colors.dart';
|
||||
|
||||
class LearnPracticeViewModel extends ReactiveViewModel {
|
||||
// Dependency injection
|
||||
|
||||
final _apiService = locator<ApiService>();
|
||||
|
||||
final _learnService = locator<LearnService>();
|
||||
|
||||
final _dialogService = locator<DialogService>();
|
||||
|
||||
final _statusChecker = locator<StatusCheckerService>();
|
||||
|
|
@ -37,8 +41,12 @@ class LearnPracticeViewModel extends ReactiveViewModel {
|
|||
}
|
||||
|
||||
@override
|
||||
List<ListenableServiceMixin> get listenableServices =>
|
||||
[_audioPlayerService, _voiceRecorderService, _authenticationService];
|
||||
List<ListenableServiceMixin> get listenableServices => [
|
||||
_learnService,
|
||||
_audioPlayerService,
|
||||
_voiceRecorderService,
|
||||
_authenticationService
|
||||
];
|
||||
|
||||
// User
|
||||
User? get _user => _authenticationService.user;
|
||||
|
|
@ -256,7 +264,7 @@ class LearnPracticeViewModel extends ReactiveViewModel {
|
|||
Future<void> getLearnPractices(
|
||||
{required int id, required LearnPractices practice}) async =>
|
||||
await runBusyFuture(_getLearnPractices(id: id, practice: practice),
|
||||
busyObject: StateObjects.learnPractices);
|
||||
busyObject: StateObjects.learnPractice);
|
||||
|
||||
Future<void> _getLearnPractices(
|
||||
{required int id, required LearnPractices practice}) async {
|
||||
|
|
@ -279,4 +287,15 @@ class LearnPracticeViewModel extends ReactiveViewModel {
|
|||
Future<void> _getLearnPracticeQuestions(int id) async {
|
||||
_questions = await _apiService.getLearnQuestions(id);
|
||||
}
|
||||
|
||||
// Complete practice
|
||||
Future<void> completeLearnPractices() async =>
|
||||
await runBusyFuture(_completeLearnPractices(),
|
||||
busyObject: StateObjects.completeLearnPractice);
|
||||
|
||||
Future<void> _completeLearnPractices() async {
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
await _apiService.completeLearnPractice(_practices.first.id ?? 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ import 'package:flutter_svg/svg.dart';
|
|||
import 'package:stacked/stacked.dart';
|
||||
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
|
||||
|
||||
import '../../../../models/learn_practice.dart';
|
||||
import '../../../common/app_colors.dart';
|
||||
import '../../../common/enmus.dart';
|
||||
import '../../../common/ui_helpers.dart';
|
||||
import '../../../widgets/custom_elevated_button.dart';
|
||||
import '../../../widgets/page_loading_indicator.dart';
|
||||
|
||||
class LearnPracticeCompletionScreen
|
||||
extends ViewModelWidget<LearnPracticeViewModel> {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import '../../../common/ui_helpers.dart';
|
|||
import '../../../widgets/cancel_learn_practice_sheet.dart';
|
||||
import '../../../widgets/custom_elevated_button.dart';
|
||||
import '../../../widgets/small_app_bar.dart';
|
||||
import '../../../widgets/speaking_partner_image.dart';
|
||||
|
||||
class LearnPracticeDescriptionScreen
|
||||
extends ViewModelWidget<LearnPracticeViewModel> {
|
||||
|
|
@ -121,6 +120,7 @@ class LearnPracticeDescriptionScreen
|
|||
_buildSubtitle(viewModel),
|
||||
verticalSpaceMedium,
|
||||
_buildImageContainer(viewModel),
|
||||
verticalSpaceLarge
|
||||
];
|
||||
|
||||
Widget _buildTitle(LearnPracticeViewModel viewModel) => Text.rich(
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import '../../../common/app_colors.dart';
|
|||
import '../../../common/ui_helpers.dart';
|
||||
import '../../../widgets/cancel_learn_practice_sheet.dart';
|
||||
import '../../../widgets/custom_elevated_button.dart';
|
||||
import '../../../widgets/page_loading_indicator.dart';
|
||||
import '../../../widgets/small_app_bar.dart';
|
||||
|
||||
class LearnPracticeResultScreen
|
||||
|
|
@ -18,6 +19,7 @@ class LearnPracticeResultScreen
|
|||
const LearnPracticeResultScreen({super.key, required this.practice});
|
||||
|
||||
Future<void> _navigate(LearnPracticeViewModel viewModel) async {
|
||||
await viewModel.completeLearnPractices();
|
||||
if (practice == LearnPractices.course) {
|
||||
viewModel.goTo(5);
|
||||
} else {
|
||||
|
|
@ -53,9 +55,16 @@ class LearnPracticeResultScreen
|
|||
required LearnPracticeViewModel viewModel}) =>
|
||||
Scaffold(
|
||||
backgroundColor: kcBackgroundColor,
|
||||
body: _buildScaffold(context: context, viewModel: viewModel),
|
||||
body: _buildScaffoldState(context: context, viewModel: viewModel),
|
||||
);
|
||||
|
||||
Widget _buildScaffoldState(
|
||||
{required BuildContext context,
|
||||
required LearnPracticeViewModel viewModel}) =>
|
||||
viewModel.busy(StateObjects.completeLearnPractice)
|
||||
? const PageLoadingIndicator()
|
||||
: _buildScaffold(context: context, viewModel: viewModel);
|
||||
|
||||
Widget _buildScaffold(
|
||||
{required BuildContext context,
|
||||
required LearnPracticeViewModel viewModel}) =>
|
||||
|
|
|
|||
|
|
@ -7,12 +7,14 @@ import '../../../app/app.locator.dart';
|
|||
import '../../../models/user.dart';
|
||||
import '../../../services/api_service.dart';
|
||||
import '../../../services/authentication_service.dart';
|
||||
import '../../../services/learn_service.dart';
|
||||
import '../../../services/status_checker_service.dart';
|
||||
import '../../common/enmus.dart';
|
||||
|
||||
class LearnProgramViewModel extends ReactiveViewModel {
|
||||
// Dependency injection
|
||||
final _apiService = locator<ApiService>();
|
||||
|
||||
final _learnService = locator<LearnService>();
|
||||
|
||||
final _statusChecker = locator<StatusCheckerService>();
|
||||
|
||||
|
|
@ -22,7 +24,7 @@ class LearnProgramViewModel extends ReactiveViewModel {
|
|||
|
||||
@override
|
||||
List<ListenableServiceMixin> get listenableServices =>
|
||||
[_authenticationService];
|
||||
[_learnService, _authenticationService];
|
||||
|
||||
// Current user
|
||||
User? get _user => _authenticationService.user;
|
||||
|
|
@ -30,7 +32,7 @@ class LearnProgramViewModel extends ReactiveViewModel {
|
|||
User? get user => _user;
|
||||
|
||||
// Learn programs
|
||||
List<LearnProgram> _learnPrograms = [];
|
||||
List<LearnProgram> get _learnPrograms => _learnService.programs;
|
||||
|
||||
List<LearnProgram> get learnPrograms => _learnPrograms;
|
||||
|
||||
|
|
@ -46,12 +48,8 @@ class LearnProgramViewModel extends ReactiveViewModel {
|
|||
busyObject: StateObjects.learnPrograms);
|
||||
|
||||
Future<void> _getLearnPrograms() async {
|
||||
if (_learnPrograms.isEmpty) {
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
_learnPrograms = await _apiService.getLearnPrograms();
|
||||
_learnPrograms
|
||||
.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
|
||||
}
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
await _learnService.getLearnPrograms();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
70
lib/ui/views/learn_subscription/learn_subscription_view.dart
Normal file
70
lib/ui/views/learn_subscription/learn_subscription_view.dart
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked/stacked_annotations.dart';
|
||||
import 'package:yimaru_app/ui/views/learn_subscription/learn_subscription_view.form.dart';
|
||||
import 'package:yimaru_app/ui/views/learn_subscription/screens/learn_subscription_form_screen.dart';
|
||||
import 'package:yimaru_app/ui/views/learn_subscription/screens/learn_subscription_package_screen.dart';
|
||||
import 'package:yimaru_app/ui/widgets/learn_subscription_card.dart';
|
||||
import 'package:yimaru_app/ui/widgets/learn_subscription_pricing_section.dart';
|
||||
|
||||
import '../../common/app_colors.dart';
|
||||
import '../../common/ui_helpers.dart';
|
||||
import '../../common/validators/form_validator.dart';
|
||||
import '../../widgets/custom_elevated_button.dart';
|
||||
import '../../widgets/small_app_bar.dart';
|
||||
import 'learn_subscription_viewmodel.dart';
|
||||
|
||||
@FormView(fields: [
|
||||
FormTextField(name: 'phoneNumber', validator: FormValidator.validateForm)
|
||||
])
|
||||
class LearnSubscriptionView extends StackedView<LearnSubscriptionViewModel>
|
||||
with $LearnSubscriptionView {
|
||||
const LearnSubscriptionView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
void onViewModelReady(LearnSubscriptionViewModel viewModel) async {
|
||||
await viewModel.getLearnSubscriptions();
|
||||
_clearData();
|
||||
syncFormWithViewModel(viewModel);
|
||||
super.onViewModelReady(viewModel);
|
||||
}
|
||||
|
||||
void _clearData() {
|
||||
phoneNumberController.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
LearnSubscriptionViewModel viewModelBuilder(BuildContext context) =>
|
||||
LearnSubscriptionViewModel();
|
||||
|
||||
@override
|
||||
Widget builder(
|
||||
BuildContext context,
|
||||
LearnSubscriptionViewModel viewModel,
|
||||
Widget? child,
|
||||
) =>
|
||||
_buildBodyWrapper(viewModel);
|
||||
|
||||
Widget _buildBodyWrapper(LearnSubscriptionViewModel viewModel) => PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, data) {
|
||||
if (!didPop) {
|
||||
Future.microtask(() => viewModel.goBack());
|
||||
}
|
||||
},
|
||||
child: _buildBody(viewModel));
|
||||
|
||||
Widget _buildBody(LearnSubscriptionViewModel viewModel) => IndexedStack(
|
||||
index: viewModel.currentPage, children: _buildScreens(viewModel));
|
||||
|
||||
List<Widget> _buildScreens(LearnSubscriptionViewModel viewModel) => [
|
||||
_buildLearnSubscriptionPackageScreen(),
|
||||
_buildLearnSubscriptionFormScreen()
|
||||
];
|
||||
|
||||
Widget _buildLearnSubscriptionPackageScreen() =>
|
||||
const LearnSubscriptionPackageScreen();
|
||||
|
||||
Widget _buildLearnSubscriptionFormScreen() =>
|
||||
LearnSubscriptionFormScreen(phoneNumberController: phoneNumberController);
|
||||
}
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format width=80
|
||||
|
||||
// **************************************************************************
|
||||
// StackedFormGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// ignore_for_file: public_member_api_docs, constant_identifier_names, non_constant_identifier_names,unnecessary_this
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:yimaru_app/ui/common/validators/form_validator.dart';
|
||||
|
||||
const bool _autoTextFieldValidation = true;
|
||||
|
||||
const String PhoneNumberValueKey = 'phoneNumber';
|
||||
|
||||
final Map<String, TextEditingController>
|
||||
_LearnSubscriptionViewTextEditingControllers = {};
|
||||
|
||||
final Map<String, FocusNode> _LearnSubscriptionViewFocusNodes = {};
|
||||
|
||||
final Map<String, String? Function(String?)?>
|
||||
_LearnSubscriptionViewTextValidations = {
|
||||
PhoneNumberValueKey: FormValidator.validateForm,
|
||||
};
|
||||
|
||||
mixin $LearnSubscriptionView {
|
||||
TextEditingController get phoneNumberController =>
|
||||
_getFormTextEditingController(PhoneNumberValueKey);
|
||||
|
||||
FocusNode get phoneNumberFocusNode => _getFormFocusNode(PhoneNumberValueKey);
|
||||
|
||||
TextEditingController _getFormTextEditingController(
|
||||
String key, {
|
||||
String? initialValue,
|
||||
}) {
|
||||
if (_LearnSubscriptionViewTextEditingControllers.containsKey(key)) {
|
||||
return _LearnSubscriptionViewTextEditingControllers[key]!;
|
||||
}
|
||||
|
||||
_LearnSubscriptionViewTextEditingControllers[key] =
|
||||
TextEditingController(text: initialValue);
|
||||
return _LearnSubscriptionViewTextEditingControllers[key]!;
|
||||
}
|
||||
|
||||
FocusNode _getFormFocusNode(String key) {
|
||||
if (_LearnSubscriptionViewFocusNodes.containsKey(key)) {
|
||||
return _LearnSubscriptionViewFocusNodes[key]!;
|
||||
}
|
||||
_LearnSubscriptionViewFocusNodes[key] = FocusNode();
|
||||
return _LearnSubscriptionViewFocusNodes[key]!;
|
||||
}
|
||||
|
||||
/// Registers a listener on every generated controller that calls [model.setData()]
|
||||
/// with the latest textController values
|
||||
void syncFormWithViewModel(FormStateHelper model) {
|
||||
phoneNumberController.addListener(() => _updateFormData(model));
|
||||
|
||||
_updateFormData(model, forceValidate: _autoTextFieldValidation);
|
||||
}
|
||||
|
||||
/// Registers a listener on every generated controller that calls [model.setData()]
|
||||
/// with the latest textController values
|
||||
@Deprecated(
|
||||
'Use syncFormWithViewModel instead.'
|
||||
'This feature was deprecated after 3.1.0.',
|
||||
)
|
||||
void listenToFormUpdated(FormViewModel model) {
|
||||
phoneNumberController.addListener(() => _updateFormData(model));
|
||||
|
||||
_updateFormData(model, forceValidate: _autoTextFieldValidation);
|
||||
}
|
||||
|
||||
/// Updates the formData on the FormViewModel
|
||||
void _updateFormData(FormStateHelper model, {bool forceValidate = false}) {
|
||||
model.setData(
|
||||
model.formValueMap
|
||||
..addAll({
|
||||
PhoneNumberValueKey: phoneNumberController.text,
|
||||
}),
|
||||
);
|
||||
|
||||
if (_autoTextFieldValidation || forceValidate) {
|
||||
updateValidationData(model);
|
||||
}
|
||||
}
|
||||
|
||||
bool validateFormFields(FormViewModel model) {
|
||||
_updateFormData(model, forceValidate: true);
|
||||
return model.isFormValid;
|
||||
}
|
||||
|
||||
/// Calls dispose on all the generated controllers and focus nodes
|
||||
void disposeForm() {
|
||||
// The dispose function for a TextEditingController sets all listeners to null
|
||||
|
||||
for (var controller
|
||||
in _LearnSubscriptionViewTextEditingControllers.values) {
|
||||
controller.dispose();
|
||||
}
|
||||
for (var focusNode in _LearnSubscriptionViewFocusNodes.values) {
|
||||
focusNode.dispose();
|
||||
}
|
||||
|
||||
_LearnSubscriptionViewTextEditingControllers.clear();
|
||||
_LearnSubscriptionViewFocusNodes.clear();
|
||||
}
|
||||
}
|
||||
|
||||
extension ValueProperties on FormStateHelper {
|
||||
bool get hasAnyValidationMessage => this
|
||||
.fieldsValidationMessages
|
||||
.values
|
||||
.any((validation) => validation != null);
|
||||
|
||||
bool get isFormValid {
|
||||
if (!_autoTextFieldValidation) this.validateForm();
|
||||
|
||||
return !hasAnyValidationMessage;
|
||||
}
|
||||
|
||||
String? get phoneNumberValue =>
|
||||
this.formValueMap[PhoneNumberValueKey] as String?;
|
||||
|
||||
set phoneNumberValue(String? value) {
|
||||
this.setData(
|
||||
this.formValueMap..addAll({PhoneNumberValueKey: value}),
|
||||
);
|
||||
|
||||
if (_LearnSubscriptionViewTextEditingControllers.containsKey(
|
||||
PhoneNumberValueKey)) {
|
||||
_LearnSubscriptionViewTextEditingControllers[PhoneNumberValueKey]?.text =
|
||||
value ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
bool get hasPhoneNumber =>
|
||||
this.formValueMap.containsKey(PhoneNumberValueKey) &&
|
||||
(phoneNumberValue?.isNotEmpty ?? false);
|
||||
|
||||
bool get hasPhoneNumberValidationMessage =>
|
||||
this.fieldsValidationMessages[PhoneNumberValueKey]?.isNotEmpty ?? false;
|
||||
|
||||
String? get phoneNumberValidationMessage =>
|
||||
this.fieldsValidationMessages[PhoneNumberValueKey];
|
||||
}
|
||||
|
||||
extension Methods on FormStateHelper {
|
||||
void setPhoneNumberValidationMessage(String? validationMessage) =>
|
||||
this.fieldsValidationMessages[PhoneNumberValueKey] = validationMessage;
|
||||
|
||||
/// Clears text input fields on the Form
|
||||
void clearForm() {
|
||||
phoneNumberValue = '';
|
||||
}
|
||||
|
||||
/// Validates text input fields on the Form
|
||||
void validateForm() {
|
||||
this.setValidationMessages({
|
||||
PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the validation message for the given key
|
||||
String? getValidationMessage(String key) {
|
||||
final validatorForKey = _LearnSubscriptionViewTextValidations[key];
|
||||
if (validatorForKey == null) return null;
|
||||
|
||||
String? validationMessageForKey = validatorForKey(
|
||||
_LearnSubscriptionViewTextEditingControllers[key]?.text,
|
||||
);
|
||||
|
||||
return validationMessageForKey;
|
||||
}
|
||||
|
||||
/// Updates the fieldsValidationMessages on the FormViewModel
|
||||
void updateValidationData(FormStateHelper model) =>
|
||||
model.setValidationMessages({
|
||||
PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey),
|
||||
});
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
import 'package:yimaru_app/app/app.router.dart';
|
||||
import 'package:yimaru_app/models/learn_subscription.dart';
|
||||
import 'package:yimaru_app/models/learn_subscription_request.dart';
|
||||
|
||||
import '../../../app/app.locator.dart';
|
||||
import '../../../services/api_service.dart';
|
||||
import '../../../services/status_checker_service.dart';
|
||||
import '../../common/enmus.dart';
|
||||
|
||||
class LearnSubscriptionViewModel extends FormViewModel {
|
||||
// Dependency injection
|
||||
final _apiService = locator<ApiService>();
|
||||
|
||||
final _statusChecker = locator<StatusCheckerService>();
|
||||
|
||||
final _navigationService = locator<NavigationService>();
|
||||
|
||||
// In-app navigation
|
||||
int _currentPage = 0;
|
||||
|
||||
int get currentPage => _currentPage;
|
||||
|
||||
// Phone number
|
||||
bool _focusPhoneNumber = false;
|
||||
|
||||
bool get focusPhoneNumber => _focusPhoneNumber;
|
||||
|
||||
// Learn subscriptions
|
||||
int _selectedIndex = 0;
|
||||
|
||||
int get selectedIndex => _selectedIndex;
|
||||
|
||||
List<LearnSubscription> _subscriptions = [];
|
||||
|
||||
List<LearnSubscription> get subscriptions => _subscriptions;
|
||||
|
||||
// Phone number
|
||||
void setPhoneNumberFocus() {
|
||||
_focusPhoneNumber = true;
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
// In-app navigation
|
||||
|
||||
void goBack() {
|
||||
if (_currentPage == 0) {
|
||||
_navigationService.back();
|
||||
} else {
|
||||
_currentPage--;
|
||||
rebuildUi();
|
||||
}
|
||||
}
|
||||
|
||||
void next() async {
|
||||
_currentPage++;
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
// Navigation
|
||||
void pop() => _navigationService.back();
|
||||
|
||||
Future<void> navigateToArifPay(String phone) async {
|
||||
pop();
|
||||
await _navigationService.navigateToArifPayView(phone: phone);
|
||||
}
|
||||
|
||||
//Learn subscriptions
|
||||
|
||||
void setSelectedPricing(int index) {
|
||||
_selectedIndex = index;
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
// Remote api call
|
||||
|
||||
// Learn subscriptions
|
||||
Future<void> getLearnSubscriptions() async =>
|
||||
await runBusyFuture(_getLearnSubscriptions(),
|
||||
busyObject: StateObjects.learnSubscriptions);
|
||||
|
||||
Future<void> _getLearnSubscriptions() async {
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
_subscriptions = await _apiService.getLearnSubscriptions();
|
||||
_subscriptions = _subscriptions + _subscriptions + _subscriptions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:yimaru_app/ui/views/learn_subscription/learn_subscription_viewmodel.dart';
|
||||
import 'package:yimaru_app/ui/widgets/phone_number_prefix.dart';
|
||||
import 'package:yimaru_app/ui/widgets/speaking_partner_image.dart';
|
||||
|
||||
import '../../../common/app_colors.dart';
|
||||
import '../../../common/ui_helpers.dart';
|
||||
import '../../../widgets/small_app_bar.dart';
|
||||
import '../../course_practice_question/course_practice_question_view.form.dart';
|
||||
import '../../../widgets/custom_bottom_sheet.dart';
|
||||
import '../../../widgets/custom_elevated_button.dart';
|
||||
import '../learn_subscription_view.form.dart';
|
||||
|
||||
class LearnSubscriptionFormScreen
|
||||
extends ViewModelWidget<LearnSubscriptionViewModel> {
|
||||
final TextEditingController phoneNumberController;
|
||||
|
||||
const LearnSubscriptionFormScreen({
|
||||
super.key,
|
||||
required this.phoneNumberController,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, LearnSubscriptionViewModel viewModel) =>
|
||||
_buildScaffoldWrapper(viewModel);
|
||||
|
||||
Widget _buildScaffoldWrapper(LearnSubscriptionViewModel viewModel) =>
|
||||
Scaffold(
|
||||
backgroundColor: kcBackgroundColor,
|
||||
body: _buildScaffold(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildScaffold(LearnSubscriptionViewModel viewModel) =>
|
||||
SafeArea(child: _buildBody(viewModel));
|
||||
|
||||
Widget _buildBody(LearnSubscriptionViewModel viewModel) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: _buildColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildColumn(LearnSubscriptionViewModel viewModel) => Column(
|
||||
children: [
|
||||
verticalSpaceMedium,
|
||||
_buildAppBar(viewModel),
|
||||
_buildSubscriptionColumnWrapper(viewModel),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildAppBar(LearnSubscriptionViewModel viewModel) => SmallAppBar(
|
||||
showBackButton: true,
|
||||
onPop: viewModel.goBack,
|
||||
);
|
||||
|
||||
Widget _buildSubscriptionColumnWrapper(
|
||||
LearnSubscriptionViewModel viewModel) =>
|
||||
Expanded(child: _buildSubscriptionColumnScrollView(viewModel));
|
||||
|
||||
Widget _buildSubscriptionColumnScrollView(
|
||||
LearnSubscriptionViewModel viewModel) =>
|
||||
SingleChildScrollView(
|
||||
child: _buildSubscriptionColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildSubscriptionColumn(LearnSubscriptionViewModel viewModel) =>
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: _buildSheetChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildSheetChildren(LearnSubscriptionViewModel viewModel) => [
|
||||
verticalSpaceMedium,
|
||||
_buildTitleWrapper(),
|
||||
verticalSpaceTiny,
|
||||
_buildSubtitle(),
|
||||
verticalSpaceMassive,
|
||||
_buildPhoneNumberWrapper(viewModel),
|
||||
if (viewModel.hasPhoneNumberValidationMessage &&
|
||||
viewModel.focusPhoneNumber)
|
||||
verticalSpaceTiny,
|
||||
if (viewModel.hasPhoneNumberValidationMessage &&
|
||||
viewModel.focusPhoneNumber)
|
||||
_buildPhoneNumberValidatorWrapper(viewModel),
|
||||
verticalSpaceLarge,
|
||||
_buildContinueButton(viewModel),
|
||||
verticalSpaceMassive,
|
||||
_buildSecurePaymentWrapper()
|
||||
];
|
||||
|
||||
Widget _buildTitleWrapper() => Align(
|
||||
alignment: Alignment.center,
|
||||
child: _buildTitle(),
|
||||
);
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
'Unlock Next Module',
|
||||
style: style18DG700,
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
'Enter payment phone number, this will be used to process the payment.',
|
||||
style: style14MG400,
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
|
||||
Widget _buildPhoneNumberWrapper(LearnSubscriptionViewModel viewModel) => Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildPhoneNumberChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildPhoneNumberChildren(
|
||||
LearnSubscriptionViewModel viewModel) =>
|
||||
[
|
||||
_buildPhoneNumberPrefix(viewModel),
|
||||
horizontalSpaceSmall,
|
||||
_buildPhoneNumberFormFieldWrapper(viewModel),
|
||||
];
|
||||
|
||||
Widget _buildPhoneNumberPrefix(LearnSubscriptionViewModel viewModel) =>
|
||||
PhoneNumberPrefix(selected: viewModel.focusPhoneNumber);
|
||||
|
||||
Widget _buildPhoneNumberFormFieldWrapper(
|
||||
LearnSubscriptionViewModel viewModel) =>
|
||||
Expanded(child: _buildPhoneNumberFormField(viewModel));
|
||||
|
||||
Widget _buildPhoneNumberFormField(LearnSubscriptionViewModel viewModel) =>
|
||||
TextFormField(
|
||||
maxLength: 9,
|
||||
keyboardType: TextInputType.phone,
|
||||
controller: phoneNumberController,
|
||||
onTap: viewModel.setPhoneNumberFocus,
|
||||
decoration: inputDecoration(
|
||||
focus: viewModel.focusPhoneNumber,
|
||||
filled: phoneNumberController.text.isNotEmpty),
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
);
|
||||
|
||||
Widget _buildPhoneNumberValidatorWrapper(
|
||||
LearnSubscriptionViewModel viewModel) =>
|
||||
viewModel.hasPhoneNumberValidationMessage
|
||||
? _buildPhoneNumberValidator(viewModel)
|
||||
: Container();
|
||||
|
||||
Widget _buildPhoneNumberValidator(LearnSubscriptionViewModel viewModel) =>
|
||||
Text(
|
||||
viewModel.phoneNumberValidationMessage!,
|
||||
style: style12R700,
|
||||
);
|
||||
|
||||
Widget _buildContinueButton(LearnSubscriptionViewModel viewModel) =>
|
||||
CustomElevatedButton(
|
||||
height: 55,
|
||||
borderRadius: 12,
|
||||
foregroundColor: kcWhite,
|
||||
text: 'Proceed to Payment',
|
||||
backgroundColor: phoneNumberController.text.isNotEmpty
|
||||
? kcPrimaryColor
|
||||
: kcPrimaryColor.withOpacity(0.1),
|
||||
onTap: phoneNumberController.text.isNotEmpty
|
||||
? () async =>
|
||||
await viewModel.navigateToArifPay(phoneNumberController.text)
|
||||
: null,
|
||||
);
|
||||
|
||||
Widget _buildSecurePaymentWrapper() => Align(
|
||||
alignment: Alignment.center,
|
||||
child: _buildSecurePayment(),
|
||||
);
|
||||
|
||||
Widget _buildSecurePayment() => Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: _buildSecurePaymentChildren(),
|
||||
);
|
||||
|
||||
List<Widget> _buildSecurePaymentChildren() =>
|
||||
[_buildTileLeading(), horizontalSpaceTiny, _buildTileTitle()];
|
||||
|
||||
Widget _buildTileLeading() => const Icon(
|
||||
Icons.lock_outline_rounded,
|
||||
size: 16,
|
||||
color: kcMediumGrey,
|
||||
);
|
||||
|
||||
Widget _buildTileTitle() => Text(
|
||||
'Unlock All Lessons & Practices',
|
||||
style: style14MG400,
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:yimaru_app/ui/common/enmus.dart';
|
||||
import 'package:yimaru_app/ui/views/learn_subscription/learn_subscription_viewmodel.dart';
|
||||
import 'package:yimaru_app/ui/widgets/page_loading_indicator.dart';
|
||||
|
||||
import '../../../common/app_colors.dart';
|
||||
import '../../../common/ui_helpers.dart';
|
||||
import '../../../widgets/custom_elevated_button.dart';
|
||||
import '../../../widgets/learn_subscription_card.dart';
|
||||
import '../../../widgets/learn_subscription_pricing_section.dart';
|
||||
import '../../../widgets/small_app_bar.dart';
|
||||
|
||||
class LearnSubscriptionPackageScreen
|
||||
extends ViewModelWidget<LearnSubscriptionViewModel> {
|
||||
const LearnSubscriptionPackageScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, LearnSubscriptionViewModel viewModel) =>
|
||||
_buildScaffoldWrapper(viewModel);
|
||||
|
||||
Widget _buildScaffoldWrapper(LearnSubscriptionViewModel viewModel) =>
|
||||
Scaffold(
|
||||
backgroundColor: kcBackgroundColor,
|
||||
body: _buildScaffoldState(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildScaffoldState(LearnSubscriptionViewModel viewModel) =>
|
||||
viewModel.busy(StateObjects.learnSubscriptions)
|
||||
? const PageLoadingIndicator()
|
||||
: _buildScaffold(viewModel);
|
||||
Widget _buildScaffold(LearnSubscriptionViewModel viewModel) =>
|
||||
SafeArea(child: _buildBody(viewModel));
|
||||
|
||||
Widget _buildBody(LearnSubscriptionViewModel viewModel) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: _buildColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildColumn(LearnSubscriptionViewModel viewModel) => Column(
|
||||
children: [
|
||||
verticalSpaceMedium,
|
||||
_buildAppBar(viewModel),
|
||||
_buildSubscriptionColumnWrapper(viewModel),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildAppBar(LearnSubscriptionViewModel viewModel) => SmallAppBar(
|
||||
showBackButton: true,
|
||||
onPop: viewModel.goBack,
|
||||
);
|
||||
|
||||
Widget _buildSubscriptionColumnWrapper(
|
||||
LearnSubscriptionViewModel viewModel) =>
|
||||
Expanded(child: _buildSubscriptionColumnScrollView(viewModel));
|
||||
|
||||
Widget _buildSubscriptionColumnScrollView(
|
||||
LearnSubscriptionViewModel viewModel) =>
|
||||
SingleChildScrollView(
|
||||
child: _buildSubscriptionColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildSubscriptionColumn(LearnSubscriptionViewModel viewModel) =>
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: _buildSubscriptionColumnChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildSubscriptionColumnChildren(
|
||||
LearnSubscriptionViewModel viewModel) =>
|
||||
[
|
||||
verticalSpaceSmall,
|
||||
_buildTitleWrapper(),
|
||||
verticalSpaceTiny,
|
||||
_buildSubtitle(),
|
||||
verticalSpaceMedium,
|
||||
_buildFirstCard(),
|
||||
verticalSpaceMedium,
|
||||
_buildSecondCard(),
|
||||
verticalSpaceLarge,
|
||||
_buildHeadingWrapper(),
|
||||
verticalSpaceSmall,
|
||||
_buildSubscriptionPricingSection(),
|
||||
verticalSpaceMedium,
|
||||
_buildSubscribeButton(viewModel),
|
||||
verticalSpaceMedium,
|
||||
_buildSecurePaymentWrapper()
|
||||
];
|
||||
|
||||
Widget _buildTitleWrapper() => Align(
|
||||
alignment: Alignment.center,
|
||||
child: _buildTitle(),
|
||||
);
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
'Unlock Next Module',
|
||||
style: style18DG700,
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
'Unlock the next level to keep growing your English skills.',
|
||||
style: style14MG400,
|
||||
);
|
||||
|
||||
Widget _buildFirstCard() => const LearnSubscriptionCard(
|
||||
icon: Icons.school,
|
||||
title: '180+ New Lessons',
|
||||
subtitle: 'Access fresh, advanced content',
|
||||
);
|
||||
|
||||
Widget _buildSecondCard() => const LearnSubscriptionCard(
|
||||
icon: Icons.developer_board,
|
||||
title: 'Mastery Through Practice',
|
||||
subtitle: 'Practice All Lessons, Modules & Levels',
|
||||
);
|
||||
|
||||
Widget _buildHeadingWrapper() => Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: _buildHeading(),
|
||||
);
|
||||
|
||||
Widget _buildHeading() => Text(
|
||||
'Choose Your Learning Plan',
|
||||
style: style16DG600,
|
||||
);
|
||||
|
||||
Widget _buildSubscriptionPricingSection() =>
|
||||
const LearnSubscriptionPricingSection();
|
||||
|
||||
Widget _buildSubscribeButton(LearnSubscriptionViewModel viewModel) =>
|
||||
CustomElevatedButton(
|
||||
height: 55,
|
||||
borderRadius: 12,
|
||||
text: 'Subscribe Now',
|
||||
foregroundColor: kcWhite,
|
||||
onTap: () => viewModel.next(),
|
||||
backgroundColor: kcPrimaryColor,
|
||||
);
|
||||
|
||||
Widget _buildSecurePaymentWrapper() => Align(
|
||||
alignment: Alignment.center,
|
||||
child: _buildSecurePayment(),
|
||||
);
|
||||
|
||||
Widget _buildSecurePayment() => Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: _buildSecurePaymentChildren(),
|
||||
);
|
||||
|
||||
List<Widget> _buildSecurePaymentChildren() =>
|
||||
[_buildTileLeading(), horizontalSpaceTiny, _buildTileTitle()];
|
||||
|
||||
Widget _buildTileLeading() => const Icon(
|
||||
Icons.lock_outline_rounded,
|
||||
size: 16,
|
||||
color: kcMediumGrey,
|
||||
);
|
||||
|
||||
Widget _buildTileTitle() => Text(
|
||||
'Unlock All Lessons & Practices',
|
||||
style: style14MG400,
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
|
|||
import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
|
||||
|
||||
import '../../../widgets/custom_dropdown.dart';
|
||||
import '../onboarding_view.form.dart';
|
||||
|
||||
class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||
const OccupationFormScreen({super.key});
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ import 'package:stacked/stacked.dart';
|
|||
import 'package:stacked_services/stacked_services.dart';
|
||||
import 'package:yimaru_app/app/app.router.dart';
|
||||
import 'package:yimaru_app/services/image_picker_service.dart';
|
||||
import 'package:yimaru_app/services/phone_caller_service.dart';
|
||||
import 'package:yimaru_app/ui/common/app_constants.dart';
|
||||
import 'package:yimaru_app/ui/common/enmus.dart';
|
||||
|
||||
import '../../../app/app.locator.dart';
|
||||
|
|
@ -12,7 +10,6 @@ import '../../../services/api_service.dart';
|
|||
import '../../../services/authentication_service.dart';
|
||||
import '../../../services/google_auth_service.dart';
|
||||
import '../../../services/status_checker_service.dart';
|
||||
import '../../../services/url_launcher_service.dart';
|
||||
import '../../common/app_colors.dart';
|
||||
|
||||
class ProfileViewModel extends ReactiveViewModel {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class TelegramSupportViewModel extends BaseViewModel {
|
|||
|
||||
// Launch telegram
|
||||
Future<void> launchTelegram() =>
|
||||
_urlLauncherService.launchUri(kTelegramSupport);
|
||||
_urlLauncherService.launchUri(kTelegramSupportLink);
|
||||
|
||||
// Navigation
|
||||
void pop() => _navigationService.back();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import 'package:yimaru_app/app/app.router.dart';
|
|||
import 'package:yimaru_app/services/authentication_service.dart';
|
||||
|
||||
import '../../../app/app.locator.dart';
|
||||
import '../../../services/status_checker_service.dart';
|
||||
|
||||
class WelcomeViewModel extends BaseViewModel {
|
||||
// Dependency Injection
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
|
||||
|
||||
import '../../models/subcategory.dart';
|
||||
import '../../models/course_catalog.dart';
|
||||
import '../common/app_colors.dart';
|
||||
import '../common/ui_helpers.dart';
|
||||
import 'custom_elevated_button.dart';
|
||||
|
||||
class CourseSubcategoryTile extends StatelessWidget {
|
||||
final Subcategory subcategory;
|
||||
class CourseCatalogTile extends StatelessWidget {
|
||||
final CourseCatalog courseCatalog;
|
||||
final GestureTapCallback? onCourseTap;
|
||||
final GestureTapCallback? onPracticeTap;
|
||||
|
||||
const CourseSubcategoryTile({
|
||||
const CourseCatalogTile({
|
||||
super.key,
|
||||
this.onCourseTap,
|
||||
this.onPracticeTap,
|
||||
required this.subcategory,
|
||||
required this.courseCatalog,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -50,16 +50,14 @@ class CourseSubcategoryTile extends StatelessWidget {
|
|||
);
|
||||
|
||||
List<Widget> _buildExpansionTileChildren() => [
|
||||
// _buildProgressRow(),
|
||||
// verticalSpaceSmall,
|
||||
_buildProgressRow(),
|
||||
verticalSpaceSmall,
|
||||
_buildActionButtonWrapper(),
|
||||
verticalSpaceSmall
|
||||
];
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
(subcategory.name == null || subcategory.name!.isEmpty)
|
||||
? 'Course ${subcategory.id}'
|
||||
: subcategory.name!,
|
||||
courseCatalog.name ?? '',
|
||||
style: style16P600,
|
||||
);
|
||||
|
||||
|
|
@ -76,12 +74,12 @@ class CourseSubcategoryTile extends StatelessWidget {
|
|||
);
|
||||
|
||||
Widget _buildProgressStatus() => const CustomLinearProgressIndicator(
|
||||
progress: 0.75,
|
||||
progress: 0,
|
||||
activeColor: kcPrimaryColor,
|
||||
backgroundColor: kcVeryLightGrey);
|
||||
|
||||
Widget _buildProgress() => const Text(
|
||||
'75%',
|
||||
'0%',
|
||||
style: TextStyle(color: kcDarkGrey),
|
||||
);
|
||||
|
||||
|
|
@ -113,7 +111,7 @@ class CourseSubcategoryTile extends StatelessWidget {
|
|||
);
|
||||
|
||||
Widget _buildExamButtonWrapper() => Expanded(
|
||||
child: Container(),
|
||||
child: _buildExamButton(),
|
||||
);
|
||||
|
||||
Widget _buildExamButton() => CustomElevatedButton(
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:yimaru_app/models/category.dart';
|
||||
import 'package:yimaru_app/ui/common/helper_functions.dart';
|
||||
|
||||
import '../common/app_colors.dart';
|
||||
|
|
@ -7,11 +6,11 @@ import '../common/app_strings.dart';
|
|||
import '../common/ui_helpers.dart';
|
||||
import 'custom_elevated_button.dart';
|
||||
|
||||
class CourseCategoryCard extends StatelessWidget {
|
||||
final Category category;
|
||||
class CourseCard extends StatelessWidget {
|
||||
final GestureTapCallback? onTap;
|
||||
final Map<String, dynamic> course;
|
||||
|
||||
const CourseCategoryCard({super.key, this.onTap, required this.category});
|
||||
const CourseCard({super.key, this.onTap, required this.course});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _buildContainer();
|
||||
|
|
@ -42,12 +41,12 @@ class CourseCategoryCard extends StatelessWidget {
|
|||
];
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
category.name ?? '',
|
||||
course['title'],
|
||||
style: style18DG700,
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
ksCategorySubtitle,
|
||||
course['description'],
|
||||
maxLines: 3,
|
||||
style: style16DG400,
|
||||
);
|
||||
|
|
|
|||
21
lib/ui/widgets/custom_container_shader.dart
Normal file
21
lib/ui/widgets/custom_container_shader.dart
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../common/app_colors.dart';
|
||||
|
||||
class CustomContainerShader extends StatelessWidget {
|
||||
const CustomContainerShader({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _buildContainerShaderWrapper();
|
||||
|
||||
Widget _buildContainerShaderWrapper() => Positioned.fill(
|
||||
child: _buildContainerShader(),
|
||||
);
|
||||
|
||||
Widget _buildContainerShader() => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: kcWhite.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -35,7 +35,7 @@ class LargeAppBar extends StatelessWidget {
|
|||
);
|
||||
|
||||
Widget _buildStack() => Stack(
|
||||
children: [ _buildPattern(),_buildAppBarWrapper()],
|
||||
children: [_buildPattern(), _buildAppBarWrapper()],
|
||||
);
|
||||
|
||||
Widget _buildAppBarWrapper() => Container(
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import '../common/app_colors.dart';
|
|||
import '../common/helper_functions.dart';
|
||||
|
||||
import '../common/ui_helpers.dart';
|
||||
import 'custom_container_shader.dart';
|
||||
import 'custom_elevated_button.dart';
|
||||
import 'custom_linear_progress_indicator.dart';
|
||||
|
||||
|
|
@ -17,11 +18,12 @@ class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
|
|||
final GestureTapCallback? onLessonTap;
|
||||
final GestureTapCallback? onPracticeTap;
|
||||
|
||||
const LearnLessonTile({super.key,
|
||||
this.onLessonTap,
|
||||
this.onPracticeTap,
|
||||
required this.index,
|
||||
required this.lesson});
|
||||
const LearnLessonTile(
|
||||
{super.key,
|
||||
this.onLessonTap,
|
||||
this.onPracticeTap,
|
||||
required this.index,
|
||||
required this.lesson});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, LearnLessonViewModel viewModel) =>
|
||||
|
|
@ -33,41 +35,44 @@ class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
|
|||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
border: Border.all(
|
||||
color: kcPrimaryColor.withOpacity(0.1),
|
||||
// color: ProgressStatuses.pending == status
|
||||
// ? kcPrimaryColor.withOpacity(0.1)
|
||||
// : kcGreen.withOpacity(0.1),
|
||||
color: (lesson.access?.isCompleted ?? false)
|
||||
? kcGreen.withOpacity(0.1)
|
||||
: kcPrimaryColor.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
child: _buildExpansionTile(viewModel),
|
||||
child: _buildTileStack(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildTileStack(LearnLessonViewModel viewModel) => Stack(
|
||||
children: [
|
||||
_buildExpansionTile(viewModel),
|
||||
_buildContainerShaderState()
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildExpansionTile(LearnLessonViewModel viewModel) => ExpansionTile(
|
||||
enabled: true,
|
||||
title: _buildTitle(),
|
||||
textColor: kcDarkGrey,
|
||||
showTrailingIcon: true,
|
||||
initiallyExpanded: false,
|
||||
trailing: _buildPendingIcon(),
|
||||
trailing: _buildIconState(),
|
||||
collapsedIconColor: kcDarkGrey,
|
||||
collapsedTextColor: kcDarkGrey,
|
||||
leading: _buildLeadingWrapper(),
|
||||
shape: Border.all(color: kcTransparent),
|
||||
expandedAlignment: Alignment.centerLeft,
|
||||
backgroundColor: kcPrimaryColor.withOpacity(0.1),
|
||||
enabled: (lesson.access?.isAccessible ?? false),
|
||||
controlAffinity: ListTileControlAffinity.trailing,
|
||||
expandedCrossAxisAlignment: CrossAxisAlignment.start,
|
||||
tilePadding: const EdgeInsets.fromLTRB(15, 15, 15, 15),
|
||||
collapsedBackgroundColor: kcPrimaryColor.withOpacity(0.1),
|
||||
backgroundColor: (lesson.access?.isCompleted ?? false)
|
||||
? kcGreen.withOpacity(0.1)
|
||||
: kcPrimaryColor.withOpacity(0.1),
|
||||
childrenPadding: const EdgeInsets.fromLTRB(15, 0, 15, 15),
|
||||
// enabled: (lesson.access?.isAccessible ?? false),
|
||||
// backgroundColor: ProgressStatuses.pending == status
|
||||
// ? kcPrimaryColor.withOpacity(0.1)
|
||||
// : kcGreen.withOpacity(0.1),
|
||||
// collapsedBackgroundColor: ProgressStatuses.pending == status
|
||||
// ? kcPrimaryColor.withOpacity(0.1)
|
||||
// : kcGreen.withOpacity(0.1),
|
||||
// initiallyExpanded: status != ProgressStatuses.completed ? true : false,
|
||||
initiallyExpanded: (lesson.access?.isAccessible ?? false) &&
|
||||
!(lesson.access?.isCompleted ?? false),
|
||||
collapsedBackgroundColor: (lesson.access?.isCompleted ?? false)
|
||||
? kcGreen.withOpacity(0.1)
|
||||
: kcPrimaryColor.withOpacity(0.1),
|
||||
children: _buildExpansionTileChildren(viewModel),
|
||||
);
|
||||
|
||||
|
|
@ -83,9 +88,9 @@ class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
|
|||
style: style16DG600,
|
||||
);
|
||||
|
||||
// Widget _buildIconState() => ProgressStatuses.pending == status
|
||||
// ? _buildPendingIcon()
|
||||
// : _buildCompleteIcon();
|
||||
Widget _buildIconState() => (lesson.access?.isCompleted ?? false)
|
||||
? _buildCompleteIcon()
|
||||
: _buildPendingIcon();
|
||||
|
||||
Widget _buildCompleteIcon() => const Icon(
|
||||
Icons.check,
|
||||
|
|
@ -116,14 +121,14 @@ class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
|
|||
_buildActionButtonWrapper(viewModel)
|
||||
];
|
||||
|
||||
Widget _buildProgress() => const CustomLinearProgressIndicator(
|
||||
progress: 0,
|
||||
Widget _buildProgress() => CustomLinearProgressIndicator(
|
||||
activeColor: kcPrimaryColor,
|
||||
backgroundColor: kcVeryLightGrey,
|
||||
progress: (lesson.access?.progressPercent ?? 0) / 100,
|
||||
);
|
||||
|
||||
Widget _buildProgressText() => Text(
|
||||
'In Progress',
|
||||
(lesson.access?.isCompleted ?? false) ? 'Completed' : 'In Progress',
|
||||
style: style14P600,
|
||||
);
|
||||
|
||||
|
|
@ -179,4 +184,10 @@ class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
|
|||
trailingIcon: Icons.play_arrow,
|
||||
backgroundColor: kcPrimaryColor,
|
||||
);
|
||||
|
||||
Widget _buildContainerShaderState() => !(lesson.access?.isAccessible ?? false)
|
||||
? _buildContainerShader()
|
||||
: Container();
|
||||
|
||||
Widget _buildContainerShader() => const CustomContainerShader();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:stacked/stacked.dart';
|
||||
import 'package:yimaru_app/models/learn_module.dart';
|
||||
import 'package:yimaru_app/ui/views/learn_module/learn_module_viewmodel.dart';
|
||||
import 'package:yimaru_app/ui/widgets/custom_container_shader.dart';
|
||||
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
|
||||
import 'package:yimaru_app/ui/widgets/finish_practice_sheet.dart';
|
||||
|
||||
|
|
@ -11,11 +12,16 @@ import 'custom_elevated_button.dart';
|
|||
|
||||
class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
|
||||
final LearnModule module;
|
||||
final GestureTapCallback? onLockTap;
|
||||
final GestureTapCallback? onModuleTap;
|
||||
final GestureTapCallback? onPracticeTap;
|
||||
|
||||
const LearnModuleTile(
|
||||
{super.key, this.onModuleTap, this.onPracticeTap, required this.module});
|
||||
{super.key,
|
||||
this.onLockTap,
|
||||
this.onModuleTap,
|
||||
this.onPracticeTap,
|
||||
required this.module});
|
||||
|
||||
Future<void> _showSheet(
|
||||
{required BuildContext context,
|
||||
|
|
@ -28,8 +34,15 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, LearnModuleViewModel viewModel) =>
|
||||
_buildExpansionTileCard(context: context, viewModel: viewModel);
|
||||
_buildExpansionTileWrapper(context: context, viewModel: viewModel);
|
||||
|
||||
Widget _buildExpansionTileWrapper(
|
||||
{required BuildContext context,
|
||||
required LearnModuleViewModel viewModel}) =>
|
||||
GestureDetector(
|
||||
onTap: !(module.access?.isAccessible ?? false) ? onLockTap : null,
|
||||
child: _buildExpansionTileCard(context: context, viewModel: viewModel),
|
||||
);
|
||||
Widget _buildExpansionTileCard(
|
||||
{required BuildContext context,
|
||||
required LearnModuleViewModel viewModel}) =>
|
||||
|
|
@ -48,7 +61,7 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
|
|||
Stack(
|
||||
children: [
|
||||
_buildExpansionTile(context: context, viewModel: viewModel),
|
||||
// _buildContainerShaderState()
|
||||
_buildContainerShaderState()
|
||||
],
|
||||
);
|
||||
|
||||
|
|
@ -57,8 +70,8 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
|
|||
required LearnModuleViewModel viewModel}) =>
|
||||
ExpansionTile(
|
||||
textColor: kcDarkGrey,
|
||||
initiallyExpanded: true,
|
||||
subtitle: _buildContent(),
|
||||
trailing: _buildLockIcon(),
|
||||
title: _buildTitleWrapper(),
|
||||
leading: _buildIconWrapper(),
|
||||
collapsedIconColor: kcDarkGrey,
|
||||
|
|
@ -72,11 +85,18 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
|
|||
expandedCrossAxisAlignment: CrossAxisAlignment.start,
|
||||
tilePadding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
childrenPadding: const EdgeInsets.fromLTRB(70, 0, 15, 15),
|
||||
showTrailingIcon: (module.access?.isAccessible ?? false) ? true : false,
|
||||
initiallyExpanded: (module.access?.isAccessible ?? false),
|
||||
showTrailingIcon:
|
||||
!(module.access?.isAccessible ?? false) ? true : false,
|
||||
children:
|
||||
_buildExpansionTileChildren(context: context, viewModel: viewModel),
|
||||
);
|
||||
|
||||
Widget _buildLockIcon() => const Icon(
|
||||
Icons.lock_outline_rounded,
|
||||
color: kcLightGrey,
|
||||
);
|
||||
|
||||
Widget _buildIconWrapper() => CircleAvatar(
|
||||
backgroundColor: kcPrimaryColor.withOpacity(0.1),
|
||||
child: _buildIcon(),
|
||||
|
|
@ -144,14 +164,15 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
|
|||
child: _buildProgressStatus(),
|
||||
);
|
||||
|
||||
Widget _buildProgressStatus() => const CustomLinearProgressIndicator(
|
||||
progress: 0,
|
||||
activeColor: kcPrimaryColor,
|
||||
backgroundColor: kcPrimaryColorLight);
|
||||
Widget _buildProgressStatus() => CustomLinearProgressIndicator(
|
||||
activeColor: kcPrimaryColor,
|
||||
backgroundColor: kcPrimaryColorLight,
|
||||
progress: (module.access?.progressPercent ?? 0) / 100,
|
||||
);
|
||||
|
||||
Widget _buildProgress() => const Text(
|
||||
'0/0',
|
||||
style: TextStyle(color: kcDarkGrey),
|
||||
Widget _buildProgress() => Text(
|
||||
'${module.access?.completedCount ?? 0}/${module.access?.totalCount ?? 0}',
|
||||
style: style14DG500,
|
||||
);
|
||||
|
||||
Widget _buildActionButtonWrapper(
|
||||
|
|
@ -207,17 +228,8 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
|
|||
);
|
||||
|
||||
Widget _buildContainerShaderState() => !(module.access?.isAccessible ?? false)
|
||||
? _buildContainerShaderWrapper()
|
||||
? _buildContainerShader()
|
||||
: Container();
|
||||
|
||||
Widget _buildContainerShaderWrapper() => Positioned.fill(
|
||||
child: _buildContainerShader(),
|
||||
);
|
||||
|
||||
Widget _buildContainerShader() => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: kcWhite.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
);
|
||||
Widget _buildContainerShader() => const CustomContainerShader();
|
||||
}
|
||||
|
|
|
|||
52
lib/ui/widgets/learn_subscription_card.dart
Normal file
52
lib/ui/widgets/learn_subscription_card.dart
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../common/app_colors.dart';
|
||||
import '../common/ui_helpers.dart';
|
||||
|
||||
class LearnSubscriptionCard extends StatelessWidget {
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final String subtitle;
|
||||
|
||||
const LearnSubscriptionCard(
|
||||
{super.key,
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.subtitle});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _buildListTile();
|
||||
|
||||
Widget _buildListTile() => ListTile(
|
||||
title: _buildTitle(),
|
||||
leading: _buildLeading(),
|
||||
subtitle: _buildSubtitle(),
|
||||
tileColor: kcPrimaryColor.withValues(alpha: 0.1),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(color: kcPrimaryColor.withValues(alpha: 0.25)),
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
title,
|
||||
style: style16DG600,
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
subtitle,
|
||||
style: style14DG400,
|
||||
);
|
||||
|
||||
Widget _buildLeading() => CircleAvatar(
|
||||
radius: 25,
|
||||
backgroundColor: kcPrimaryColor.withValues(alpha: 0.25),
|
||||
child: _buildIcon(),
|
||||
);
|
||||
|
||||
Widget _buildIcon() => Icon(
|
||||
icon,
|
||||
size: 25,
|
||||
color: kcPrimaryColor,
|
||||
);
|
||||
}
|
||||
66
lib/ui/widgets/learn_subscription_pricing_card.dart
Normal file
66
lib/ui/widgets/learn_subscription_pricing_card.dart
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../common/app_colors.dart';
|
||||
import '../common/ui_helpers.dart';
|
||||
|
||||
class LearnSubscriptionPricingCard extends StatelessWidget {
|
||||
final int index;
|
||||
final String type;
|
||||
final String price;
|
||||
final String currency;
|
||||
final int selectedIndex;
|
||||
final GestureTapCallback? onTap;
|
||||
|
||||
const LearnSubscriptionPricingCard({
|
||||
super.key,
|
||||
this.onTap,
|
||||
required this.type,
|
||||
required this.price,
|
||||
required this.index,
|
||||
required this.currency,
|
||||
required this.selectedIndex,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _buildContainerWrapper();
|
||||
|
||||
Widget _buildContainerWrapper() => GestureDetector(
|
||||
onTap: onTap,
|
||||
child: _buildContainer(),
|
||||
);
|
||||
|
||||
Widget _buildContainer() => Container(
|
||||
alignment: Alignment.center,
|
||||
margin: const EdgeInsets.only(right: 10),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
color: selectedIndex == index
|
||||
? kcPrimaryColor.withValues(alpha: 0.25)
|
||||
: kcPrimaryColor.withValues(alpha: 0.1),
|
||||
border: Border.all(
|
||||
width: selectedIndex == index ? 2 : 1,
|
||||
color: selectedIndex == index
|
||||
? kcPrimaryColor
|
||||
: kcPrimaryColor.withValues(alpha: 0.25)),
|
||||
),
|
||||
child: _buildColumn(),
|
||||
);
|
||||
|
||||
Widget _buildColumn() => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: _buildColumnChildren(),
|
||||
);
|
||||
|
||||
List<Widget> _buildColumnChildren() =>
|
||||
[_buildPriceCardTitle(), _buildPriceCardSubtitle()];
|
||||
|
||||
Widget _buildPriceCardTitle() => Text(
|
||||
'$price $currency',
|
||||
style: style16DG600,
|
||||
);
|
||||
|
||||
Widget _buildPriceCardSubtitle() => Text(
|
||||
type,
|
||||
style: style14DG400,
|
||||
);
|
||||
}
|
||||
99
lib/ui/widgets/learn_subscription_pricing_section.dart
Normal file
99
lib/ui/widgets/learn_subscription_pricing_section.dart
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:yimaru_app/ui/common/app_colors.dart';
|
||||
import 'package:yimaru_app/ui/views/learn_subscription/learn_subscription_viewmodel.dart';
|
||||
import 'package:yimaru_app/ui/widgets/learn_subscription_pricing_card.dart';
|
||||
|
||||
import '../common/ui_helpers.dart';
|
||||
|
||||
class LearnSubscriptionPricingSection
|
||||
extends ViewModelWidget<LearnSubscriptionViewModel> {
|
||||
const LearnSubscriptionPricingSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, LearnSubscriptionViewModel viewModel) =>
|
||||
_buildContainer(viewModel);
|
||||
|
||||
Widget _buildContainer(LearnSubscriptionViewModel viewModel) => Container(
|
||||
height: 200,
|
||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||
decoration: BoxDecoration(
|
||||
color: kcBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
border: Border.all(color: kcPrimaryColor.withValues(alpha: 0.25)),
|
||||
),
|
||||
child: _buildColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildColumn(LearnSubscriptionViewModel viewModel) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: _buildColumnChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildColumnChildren(LearnSubscriptionViewModel viewModel) => [
|
||||
_buildTileWrapper(),
|
||||
verticalSpaceSmall,
|
||||
_buildLearnPriceWrapper(viewModel)
|
||||
];
|
||||
|
||||
Widget _buildTileWrapper() => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: _buildTile(),
|
||||
);
|
||||
|
||||
Widget _buildTile() => ListTile(
|
||||
title: _buildTileTitle(),
|
||||
leading: _buildTileLeading(),
|
||||
subtitle: _buildTileSubtitle(),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
);
|
||||
|
||||
Widget _buildTileTitle() => Text(
|
||||
'Subscription Plans',
|
||||
style: style16DG600,
|
||||
);
|
||||
|
||||
Widget _buildTileSubtitle() =>
|
||||
Text('This includes Monthly and 3-Month packages', style: style14MG400);
|
||||
|
||||
Widget _buildTileLeading() => const Icon(
|
||||
Icons.key,
|
||||
size: 35,
|
||||
color: kcPrimaryColor,
|
||||
);
|
||||
|
||||
Widget _buildLearnPriceWrapper(LearnSubscriptionViewModel viewModel) =>
|
||||
Expanded(
|
||||
child: _buildLearnPricing(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildLearnPricing(LearnSubscriptionViewModel viewModel) =>
|
||||
PageView.builder(
|
||||
itemCount: viewModel.subscriptions.length,
|
||||
controller: PageController(viewportFraction: 0.9),
|
||||
itemBuilder: (context, index) => _buildPriceCard(
|
||||
index: index,
|
||||
selectedIndex: viewModel.selectedIndex,
|
||||
type: viewModel.subscriptions[index].name ?? '',
|
||||
onTap: () => viewModel.setSelectedPricing(index),
|
||||
currency: viewModel.subscriptions[index].currency ?? '',
|
||||
price: viewModel.subscriptions[index].price.toString() ?? '',
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildPriceCard(
|
||||
{required int index,
|
||||
required String type,
|
||||
required String price,
|
||||
required String currency,
|
||||
required int selectedIndex,
|
||||
required GestureTapCallback onTap}) =>
|
||||
LearnSubscriptionPricingCard(
|
||||
type: type,
|
||||
price: price,
|
||||
onTap: onTap,
|
||||
index: index,
|
||||
currency: currency,
|
||||
selectedIndex: selectedIndex,
|
||||
);
|
||||
}
|
||||
|
|
@ -5,7 +5,15 @@ import '../common/ui_helpers.dart';
|
|||
import 'custom_linear_progress_indicator.dart';
|
||||
|
||||
class ModuleProgress extends StatelessWidget {
|
||||
const ModuleProgress({super.key});
|
||||
final int total;
|
||||
final int progress;
|
||||
final int completed;
|
||||
|
||||
const ModuleProgress(
|
||||
{super.key,
|
||||
required this.total,
|
||||
required this.progress,
|
||||
required this.completed});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _buildContainer();
|
||||
|
|
@ -36,17 +44,17 @@ class ModuleProgress extends StatelessWidget {
|
|||
[_buildProgressInfo(), _buildProgress()];
|
||||
|
||||
Widget _buildProgressInfo() => Text(
|
||||
'0% Progress',
|
||||
'$progress% Progress',
|
||||
style: style16DG400,
|
||||
);
|
||||
|
||||
Widget _buildProgress() => Text(
|
||||
'0/3',
|
||||
'$completed/$total',
|
||||
style: style14P400,
|
||||
);
|
||||
|
||||
Widget _buildProgressIndicator() => const CustomLinearProgressIndicator(
|
||||
progress: 0,
|
||||
Widget _buildProgressIndicator() => CustomLinearProgressIndicator(
|
||||
progress: progress / 100,
|
||||
activeColor: kcPrimaryColor,
|
||||
backgroundColor: kcVeryLightGrey,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart';
|
|||
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
|
||||
|
||||
class OverallLearnProgress extends StatelessWidget {
|
||||
final int progress;
|
||||
final Color backgroundColor;
|
||||
final Color indicatorBackgroundColor;
|
||||
const OverallLearnProgress(
|
||||
{super.key,
|
||||
required this.progress,
|
||||
required this.backgroundColor,
|
||||
required this.indicatorBackgroundColor});
|
||||
|
||||
|
|
@ -51,12 +53,12 @@ class OverallLearnProgress extends StatelessWidget {
|
|||
);
|
||||
|
||||
Widget _buildProgress() => Text(
|
||||
'0%',
|
||||
'$progress%',
|
||||
style: style14P400,
|
||||
);
|
||||
|
||||
Widget _buildProgressIndicator() => CustomLinearProgressIndicator(
|
||||
progress: 0.0,
|
||||
progress: progress / 100,
|
||||
activeColor: kcPrimaryColor,
|
||||
backgroundColor: indicatorBackgroundColor,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -873,7 +873,7 @@ packages:
|
|||
source: hosted
|
||||
version: "0.15.6"
|
||||
http:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
name: yimaru_app
|
||||
version: 0.1.14+16
|
||||
version: 0.1.15+17
|
||||
publish_to: 'none'
|
||||
description: A new Flutter project.
|
||||
|
||||
|
|
@ -54,6 +54,7 @@ dependencies:
|
|||
flutter_local_notifications: ^20.1.0
|
||||
internet_connection_checker_plus: ^2.9.1+2
|
||||
|
||||
http: any
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import 'package:yimaru_app/services/in_app_update_service.dart';
|
|||
import 'package:yimaru_app/services/vimeo_service.dart';
|
||||
import 'package:yimaru_app/services/url_launcher_service.dart';
|
||||
import 'package:yimaru_app/services/phone_caller_service.dart';
|
||||
import 'package:yimaru_app/services/learn_service.dart';
|
||||
// @stacked-import
|
||||
|
||||
import 'test_helpers.mocks.dart';
|
||||
|
|
@ -52,6 +53,9 @@ import 'test_helpers.mocks.dart';
|
|||
MockSpec<UrlLauncherService>(onMissingStub: OnMissingStub.returnDefault),
|
||||
MockSpec<UrlLauncherService>(onMissingStub: OnMissingStub.returnDefault),
|
||||
MockSpec<PhoneCallerService>(onMissingStub: OnMissingStub.returnDefault),
|
||||
MockSpec<LearnLessonService>(onMissingStub: OnMissingStub.returnDefault),
|
||||
MockSpec<LearnService>(onMissingStub: OnMissingStub.returnDefault),
|
||||
MockSpec<LearnService>(onMissingStub: OnMissingStub.returnDefault),
|
||||
// @stacked-mock-spec
|
||||
],
|
||||
)
|
||||
|
|
@ -79,6 +83,9 @@ void registerServices() {
|
|||
getAndRegisterUrlLauncherService();
|
||||
getAndRegisterUrlLauncherService();
|
||||
getAndRegisterPhoneCallerService();
|
||||
getAndRegisterLearnLessonService();
|
||||
getAndRegisterLearnService();
|
||||
getAndRegisterLearnService();
|
||||
// @stacked-mock-register
|
||||
}
|
||||
|
||||
|
|
@ -261,6 +268,13 @@ MockPhoneCallerService getAndRegisterPhoneCallerService() {
|
|||
locator.registerSingleton<PhoneCallerService>(service);
|
||||
return service;
|
||||
}
|
||||
|
||||
MockLearnService getAndRegisterLearnService() {
|
||||
_removeRegistrationIfExists<LearnService>();
|
||||
final service = MockLearnService();
|
||||
locator.registerSingleton<LearnService>(service);
|
||||
return service;
|
||||
}
|
||||
// @stacked-mock-create
|
||||
|
||||
void _removeRegistrationIfExists<T extends Object>() {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import 'package:yimaru_app/app/app.locator.dart';
|
|||
import '../helpers/test_helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('CourseCategoryViewModel Tests -', () {
|
||||
group('LearnServiceTest -', () {
|
||||
setUp(() => registerServices());
|
||||
tearDown(() => locator.reset());
|
||||
});
|
||||
|
|
@ -4,7 +4,7 @@ import 'package:yimaru_app/app/app.locator.dart';
|
|||
import '../helpers/test_helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('CourseSubcategoryViewModel Tests -', () {
|
||||
group('ArifPayViewModel Tests -', () {
|
||||
setUp(() => registerServices());
|
||||
tearDown(() => locator.reset());
|
||||
});
|
||||
11
test/viewmodels/course_catalog_viewmodel_test.dart
Normal file
11
test/viewmodels/course_catalog_viewmodel_test.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:yimaru_app/app/app.locator.dart';
|
||||
|
||||
import '../helpers/test_helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('CourseCatalogViewModel Tests -', () {
|
||||
setUp(() => registerServices());
|
||||
tearDown(() => locator.reset());
|
||||
});
|
||||
}
|
||||
11
test/viewmodels/course_unit_viewmodel_test.dart
Normal file
11
test/viewmodels/course_unit_viewmodel_test.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:yimaru_app/app/app.locator.dart';
|
||||
|
||||
import '../helpers/test_helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('CourseUnitViewModel Tests -', () {
|
||||
setUp(() => registerServices());
|
||||
tearDown(() => locator.reset());
|
||||
});
|
||||
}
|
||||
11
test/viewmodels/learn_subscription_viewmodel_test.dart
Normal file
11
test/viewmodels/learn_subscription_viewmodel_test.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:yimaru_app/app/app.locator.dart';
|
||||
|
||||
import '../helpers/test_helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('LearnSubscriptionViewModel Tests -', () {
|
||||
setUp(() => registerServices());
|
||||
tearDown(() => locator.reset());
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user