fix: Apply UAT comments
This commit is contained in:
parent
64cad421e7
commit
610bd2b2fd
|
|
@ -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/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_practice/course_practice_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/course_payment/course_payment_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/failure/failure_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/course_lesson/course_lesson_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';
|
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/ui/views/duolingo/duolingo_view.dart';
|
||||||
import 'package:yimaru_app/services/smart_auth_service.dart';
|
import 'package:yimaru_app/services/smart_auth_service.dart';
|
||||||
import 'package:yimaru_app/services/course_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/ui/views/course/course_view.dart';
|
||||||
import 'package:yimaru_app/services/audio_player_service.dart';
|
import 'package:yimaru_app/services/audio_player_service.dart';
|
||||||
import 'package:yimaru_app/services/voice_recorder_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/vimeo_service.dart';
|
||||||
import 'package:yimaru_app/services/url_launcher_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/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
|
// @stacked-import
|
||||||
|
|
||||||
@StackedApp(
|
@StackedApp(
|
||||||
|
|
@ -83,17 +86,19 @@ import 'package:yimaru_app/services/phone_caller_service.dart';
|
||||||
MaterialRoute(page: LearnPracticeView),
|
MaterialRoute(page: LearnPracticeView),
|
||||||
MaterialRoute(page: CoursePracticeView),
|
MaterialRoute(page: CoursePracticeView),
|
||||||
MaterialRoute(page: CoursePaymentView),
|
MaterialRoute(page: CoursePaymentView),
|
||||||
MaterialRoute(page: CourseCategoryView),
|
|
||||||
MaterialRoute(page: FailureView),
|
MaterialRoute(page: FailureView),
|
||||||
MaterialRoute(page: CourseLessonView),
|
MaterialRoute(page: CourseLessonView),
|
||||||
MaterialRoute(page: CourseLessonDetailView),
|
MaterialRoute(page: CourseLessonDetailView),
|
||||||
MaterialRoute(page: DuolingoView),
|
MaterialRoute(page: DuolingoView),
|
||||||
MaterialRoute(page: CourseSubcategoryView),
|
|
||||||
MaterialRoute(page: CourseView),
|
MaterialRoute(page: CourseView),
|
||||||
MaterialRoute(page: CoursePracticeQuestionView),
|
MaterialRoute(page: CoursePracticeQuestionView),
|
||||||
MaterialRoute(page: LearnProgramView),
|
MaterialRoute(page: LearnProgramView),
|
||||||
MaterialRoute(page: LearnCourseView),
|
MaterialRoute(page: LearnCourseView),
|
||||||
MaterialRoute(page: AssessmentView),
|
MaterialRoute(page: AssessmentView),
|
||||||
|
MaterialRoute(page: LearnSubscriptionView),
|
||||||
|
MaterialRoute(page: ArifPayView),
|
||||||
|
MaterialRoute(page: CourseCatalogView),
|
||||||
|
MaterialRoute(page: CourseUnitView),
|
||||||
// @stacked-route
|
// @stacked-route
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
|
@ -118,6 +123,7 @@ import 'package:yimaru_app/services/phone_caller_service.dart';
|
||||||
LazySingleton(classType: VimeoService),
|
LazySingleton(classType: VimeoService),
|
||||||
LazySingleton(classType: UrlLauncherService),
|
LazySingleton(classType: UrlLauncherService),
|
||||||
LazySingleton(classType: PhoneCallerService),
|
LazySingleton(classType: PhoneCallerService),
|
||||||
|
LazySingleton(classType: LearnService),
|
||||||
// @stacked-service
|
// @stacked-service
|
||||||
],
|
],
|
||||||
bottomsheets: [
|
bottomsheets: [
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import '../services/google_auth_service.dart';
|
||||||
import '../services/image_downloader_service.dart';
|
import '../services/image_downloader_service.dart';
|
||||||
import '../services/image_picker_service.dart';
|
import '../services/image_picker_service.dart';
|
||||||
import '../services/in_app_update_service.dart';
|
import '../services/in_app_update_service.dart';
|
||||||
|
import '../services/learn_service.dart';
|
||||||
import '../services/notification_service.dart';
|
import '../services/notification_service.dart';
|
||||||
import '../services/permission_handler_service.dart';
|
import '../services/permission_handler_service.dart';
|
||||||
import '../services/phone_caller_service.dart';
|
import '../services/phone_caller_service.dart';
|
||||||
|
|
@ -61,4 +62,5 @@ Future<void> setupLocator(
|
||||||
locator.registerLazySingleton(() => VimeoService());
|
locator.registerLazySingleton(() => VimeoService());
|
||||||
locator.registerLazySingleton(() => UrlLauncherService());
|
locator.registerLazySingleton(() => UrlLauncherService());
|
||||||
locator.registerLazySingleton(() => PhoneCallerService());
|
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/learn_program.dart';
|
||||||
import 'package:yimaru_app/models/level.dart';
|
import 'package:yimaru_app/models/level.dart';
|
||||||
import 'package:yimaru_app/models/assessment_question.dart';
|
import 'package:yimaru_app/models/assessment_question.dart';
|
||||||
import 'package:yimaru_app/models/subcategory.dart';
|
import 'package:yimaru_app/models/course_catalog.dart';
|
||||||
import 'package:yimaru_app/models/category.dart';
|
|
||||||
import 'package:yimaru_app/models/course_lesson.dart';
|
import 'package:yimaru_app/models/course_lesson.dart';
|
||||||
import 'package:yimaru_app/models/course_progress.dart';
|
import 'package:yimaru_app/models/course_progress.dart';
|
||||||
import 'package:yimaru_app/models/course.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_course.dart';
|
||||||
import '../models/learn_module.dart';
|
import '../models/learn_module.dart';
|
||||||
import '../models/learn_question.dart';
|
import '../models/learn_question.dart';
|
||||||
|
import '../models/learn_subscription.dart';
|
||||||
import '../models/lesson.dart';
|
import '../models/lesson.dart';
|
||||||
import '../models/module.dart';
|
import '../models/module.dart';
|
||||||
import '../models/assessment.dart';
|
import '../models/assessment.dart';
|
||||||
import '../models/submodule.dart';
|
import '../models/submodule.dart';
|
||||||
|
import '../models/learn_subscription_request.dart';
|
||||||
import '../ui/common/enmus.dart';
|
import '../ui/common/enmus.dart';
|
||||||
|
|
||||||
class ApiService {
|
class ApiService {
|
||||||
|
|
@ -432,7 +433,7 @@ class ApiService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Learn learn courses
|
// Learn learn courses
|
||||||
Future<List<LearnCourse>> getLearnCourse(int id) async {
|
Future<List<LearnCourse>> getLearnCourses(int id) async {
|
||||||
try {
|
try {
|
||||||
List<LearnCourse> learnCourses = [];
|
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
|
// Learn lesson practices
|
||||||
Future<List<LearnPractice>> getLearnLessonPractices(int id) async {
|
Future<List<LearnPractice>> getLearnLessonPractices(int id) async {
|
||||||
try {
|
try {
|
||||||
|
|
@ -601,49 +628,48 @@ class ApiService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TO BE MODIFIED*/
|
// Complete lesson
|
||||||
|
Future<Map<String, dynamic>> completeLearnPractice(int id) async {
|
||||||
// Get categories
|
|
||||||
Future<List<Category>> getCategories() async {
|
|
||||||
try {
|
try {
|
||||||
List<Category> categories = [];
|
Response response = await _service.dio.post(
|
||||||
|
'$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kProgressUrl/$kPracticesUrl/$id/$kCompleteUrl',
|
||||||
final Response response = await _service.dio.get(
|
);
|
||||||
'$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kCategoryUrl');
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
var data = response.data;
|
return {
|
||||||
var decodedData = data['data']['categories'] as List;
|
'status': ResponseStatus.success,
|
||||||
categories = decodedData.map(
|
'message': 'Lesson completed'
|
||||||
(e) {
|
};
|
||||||
return Category.fromJson(e);
|
} else {
|
||||||
},
|
return {
|
||||||
).toList();
|
'status': ResponseStatus.failure,
|
||||||
return categories;
|
'message': 'Unknown Error Occurred'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return [];
|
} on DioException catch (e) {
|
||||||
} catch (e) {
|
return {
|
||||||
return [];
|
'status': ResponseStatus.failure,
|
||||||
|
'message': e.response?.data.toString(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get course subcategory
|
Future<List<LearnSubscription>> getLearnSubscriptions() async {
|
||||||
Future<List<Subcategory>> getSubcategories(int id) async {
|
|
||||||
try {
|
try {
|
||||||
List<Subcategory> subcategories = [];
|
List<LearnSubscription> subscriptions = [];
|
||||||
|
|
||||||
final Response response = await _service.dio
|
final Response response = await _service.dio
|
||||||
.get('$kBaseUrl/$kCourseBaseUrl/$kCategoryUrl/$id/$kCoursesUrl');
|
.get('$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kSubscriptionsUrl');
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
var data = response.data;
|
var data = response.data;
|
||||||
var decodedData = data['data']['courses'] as List;
|
var decodedData = data['data'] as List;
|
||||||
subcategories = decodedData.map(
|
subscriptions = decodedData.map(
|
||||||
(e) {
|
(e) {
|
||||||
return Subcategory.fromJson(e);
|
return LearnSubscription.fromJson(e);
|
||||||
},
|
},
|
||||||
).toList();
|
).toList();
|
||||||
return subcategories;
|
return subscriptions;
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
} catch (e) {
|
} 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
|
// Get courses
|
||||||
// Future<List<Course>> getCourses(int id) async {
|
// Future<List<Course>> getCourses(int id) async {
|
||||||
// try {
|
// try {
|
||||||
|
|
@ -727,7 +807,7 @@ class ApiService {
|
||||||
Future<Map<String, dynamic>> completeLesson(int id) async {
|
Future<Map<String, dynamic>> completeLesson(int id) async {
|
||||||
try {
|
try {
|
||||||
Response response = await _service.dio.post(
|
Response response = await _service.dio.post(
|
||||||
'$kBaseUrl/$kLessonProgressUrl/$id/$kCompleteLessonUrl',
|
'$kBaseUrl/$kLessonProgressUrl/$id/$kCompleteUrl',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
|
|
@ -813,9 +893,9 @@ class ApiService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get learn subcategories
|
// Get learn subcategories
|
||||||
Future<List<Subcategory>> getLearnSubcategories() async {
|
Future<List<CourseCatalog>> getLearnSubcategories() async {
|
||||||
try {
|
try {
|
||||||
List<Subcategory> learnSubcategories = [];
|
List<CourseCatalog> learnSubcategories = [];
|
||||||
|
|
||||||
final Response response = await _service.dio.get(
|
final Response response = await _service.dio.get(
|
||||||
'$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kLearnSubcategoriesUrl');
|
'$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kLearnSubcategoriesUrl');
|
||||||
|
|
@ -825,7 +905,7 @@ class ApiService {
|
||||||
var decodedData = data['data']['sub_categories'] as List;
|
var decodedData = data['data']['sub_categories'] as List;
|
||||||
learnSubcategories = decodedData.map(
|
learnSubcategories = decodedData.map(
|
||||||
(e) {
|
(e) {
|
||||||
return Subcategory.fromJson(e);
|
return CourseCatalog.fromJson(e);
|
||||||
},
|
},
|
||||||
).toList();
|
).toList();
|
||||||
return learnSubcategories;
|
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 {
|
Future<void> updateFCMToken() async {
|
||||||
// print('DEVICE TOKEN: ${await _messaging.getToken()}');
|
print('DEVICE TOKEN: ${await _messaging.getToken()}');
|
||||||
_messaging.onTokenRefresh.listen((newToken) {
|
_messaging.onTokenRefresh.listen((newToken) {
|
||||||
// updateTokenOnServer(newToken);
|
// updateTokenOnServer(newToken);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,19 @@ String kProgramsUrl = 'programs';
|
||||||
|
|
||||||
String kRegisterUrl = 'register';
|
String kRegisterUrl = 'register';
|
||||||
|
|
||||||
|
String kProgressUrl = 'progress';
|
||||||
|
|
||||||
|
String kCompleteUrl = 'complete';
|
||||||
|
|
||||||
|
String kPaymentsUrl = 'payments';
|
||||||
|
|
||||||
|
String kSubscribeUrl = 'subscribe';
|
||||||
|
|
||||||
String kPracticesUrl = 'practices';
|
String kPracticesUrl = 'practices';
|
||||||
|
|
||||||
String kQuestionsUrl = 'questions';
|
String kQuestionsUrl = 'questions';
|
||||||
|
|
||||||
String kCategoryUrl = 'categories';
|
String kExamPrepUrl = 'exam-prep';
|
||||||
|
|
||||||
String kCoursePractice = 'by-owner';
|
String kCoursePractice = 'by-owner';
|
||||||
|
|
||||||
|
|
@ -37,8 +45,6 @@ String kSubmodulesUrl = 'sub-modules';
|
||||||
|
|
||||||
String kSubcoursesUrl = 'sub-courses';
|
String kSubcoursesUrl = 'sub-courses';
|
||||||
|
|
||||||
String kCompleteLessonUrl = 'complete';
|
|
||||||
|
|
||||||
String kResetPassword = 'resetPassword';
|
String kResetPassword = 'resetPassword';
|
||||||
|
|
||||||
String kQuestionSetsUrl = 'question-sets';
|
String kQuestionSetsUrl = 'question-sets';
|
||||||
|
|
@ -51,8 +57,12 @@ String kPublishedVideos = 'videos/published';
|
||||||
|
|
||||||
String kCoursePracticeQuestions = 'questions';
|
String kCoursePracticeQuestions = 'questions';
|
||||||
|
|
||||||
|
String kCatalogCoursesUrl = 'catalog-courses';
|
||||||
|
|
||||||
String kUpdateProfileImage = 'profile-picture';
|
String kUpdateProfileImage = 'profile-picture';
|
||||||
|
|
||||||
|
String kSubscriptionsUrl = 'subscription-plans';
|
||||||
|
|
||||||
String kRefreshTokenUrl = 'api/v1/auth/refresh';
|
String kRefreshTokenUrl = 'api/v1/auth/refresh';
|
||||||
|
|
||||||
String kLoginUrl = 'api/v1/auth/customer-login';
|
String kLoginUrl = 'api/v1/auth/customer-login';
|
||||||
|
|
@ -89,3 +99,10 @@ String kServerClientId =
|
||||||
String kPhoneSupport = '+251946396655';
|
String kPhoneSupport = '+251946396655';
|
||||||
|
|
||||||
String kTelegramSupport = '@yimaruacademy2026';
|
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,
|
courseLessons,
|
||||||
profileUpdate,
|
profileUpdate,
|
||||||
resetPassword,
|
resetPassword,
|
||||||
subcategories,
|
learnPractice,
|
||||||
|
courseCatalogs,
|
||||||
loginWithEmail,
|
loginWithEmail,
|
||||||
coursePractice,
|
coursePractice,
|
||||||
learnPractices,
|
|
||||||
loginWithGoogle,
|
loginWithGoogle,
|
||||||
loadLessonVideo,
|
loadLessonVideo,
|
||||||
loadCourseVideo,
|
loadCourseVideo,
|
||||||
learnSubmodules,
|
learnSubmodules,
|
||||||
requestResetCode,
|
requestResetCode,
|
||||||
courseCategories,
|
|
||||||
profileCompletion,
|
profileCompletion,
|
||||||
|
learnSubscription,
|
||||||
|
learnSubscriptions,
|
||||||
registerWithGoogle,
|
registerWithGoogle,
|
||||||
learnPracticeSample,
|
learnPracticeSample,
|
||||||
learnPracticeAnswer,
|
learnPracticeAnswer,
|
||||||
loginWithPhoneNumber,
|
loginWithPhoneNumber,
|
||||||
assessmentQuestions,
|
assessmentQuestions,
|
||||||
learnPracticeQuestion,
|
learnPracticeQuestion,
|
||||||
|
completeLearnPractice,
|
||||||
coursePracticeQuestion,
|
coursePracticeQuestion,
|
||||||
coursePracticeQuestions,
|
coursePracticeQuestions,
|
||||||
recordLearnPracticeAnswer,
|
recordLearnPracticeAnswer,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:ui';
|
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 'package:stacked/stacked.dart';
|
||||||
|
|
||||||
import '../../../models/course_detail.dart';
|
import '../../../models/course_detail.dart';
|
||||||
import '../../../models/subcategory.dart';
|
import '../../../models/course_catalog.dart';
|
||||||
import '../../common/app_colors.dart';
|
import '../../common/app_colors.dart';
|
||||||
import '../../common/enmus.dart';
|
import '../../common/enmus.dart';
|
||||||
import '../../common/ui_helpers.dart';
|
import '../../common/ui_helpers.dart';
|
||||||
|
import '../../widgets/course_category_card.dart';
|
||||||
import '../../widgets/course_tile.dart';
|
import '../../widgets/course_tile.dart';
|
||||||
import '../../widgets/custom_circular_progress_indicator.dart';
|
import '../../widgets/custom_circular_progress_indicator.dart';
|
||||||
|
import '../../widgets/profile_app_bar.dart';
|
||||||
import '../../widgets/small_app_bar.dart';
|
import '../../widgets/small_app_bar.dart';
|
||||||
import 'course_viewmodel.dart';
|
import 'course_viewmodel.dart';
|
||||||
|
|
||||||
class CourseView extends StackedView<CourseViewModel> {
|
class CourseView extends StackedView<CourseViewModel> {
|
||||||
final Subcategory subcategory;
|
const CourseView({Key? key}) : super(key: key);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
CourseViewModel viewModelBuilder(BuildContext context) => CourseViewModel();
|
CourseViewModel viewModelBuilder(BuildContext context) => CourseViewModel();
|
||||||
|
|
@ -51,78 +45,63 @@ class CourseView extends StackedView<CourseViewModel> {
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildAppBar(viewModel),
|
_buildAppBar(viewModel),
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildCoursesColumnWrapper(viewModel),
|
_buildCategoryColumnWrapper(viewModel)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildAppBar(CourseViewModel viewModel) => SmallAppBar(
|
Widget _buildAppBar(CourseViewModel viewModel) => ProfileAppBar(
|
||||||
onPop: viewModel.pop,
|
name: viewModel.user?.firstName,
|
||||||
showBackButton: true,
|
profileImage: viewModel.user?.profilePicture,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildCoursesColumnWrapper(CourseViewModel viewModel) =>
|
Widget _buildCategoryColumnWrapper(CourseViewModel viewModel) =>
|
||||||
Expanded(child: _buildCoursesColumnScrollView(viewModel));
|
Expanded(child: _buildCourseColumnScrollView(viewModel));
|
||||||
|
|
||||||
Widget _buildCoursesColumnScrollView(CourseViewModel viewModel) =>
|
Widget _buildCourseColumnScrollView(CourseViewModel viewModel) =>
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
child: _buildCoursesColumn(viewModel),
|
child: _buildCourseColumn(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildCoursesColumn(CourseViewModel viewModel) => Column(
|
Widget _buildCourseColumn(CourseViewModel viewModel) => Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: _buildCoursesColumnChildren(viewModel),
|
children: _buildLevelsColumnChildren(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
List<Widget> _buildCoursesColumnChildren(CourseViewModel viewModel) => [
|
List<Widget> _buildLevelsColumnChildren(CourseViewModel viewModel) => [
|
||||||
verticalSpaceMedium,
|
|
||||||
_buildTitle(),
|
_buildTitle(),
|
||||||
_buildSubtitle(),
|
_buildSubtitle(),
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildListViewBuilder(viewModel)
|
_buildListView(viewModel)
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildTitle() => Text(
|
Widget _buildTitle() => Text(
|
||||||
'${subcategory.name ?? ''} courses',
|
'Courses',
|
||||||
style: style18DG700,
|
style: style18DG700,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubtitle() => Text(
|
Widget _buildSubtitle() => Text(
|
||||||
'Explore variety of courses on ${subcategory.name ?? ''}.',
|
'Choose a course to improve your professional or exam skills.',
|
||||||
style: style14DG400,
|
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(
|
Widget _buildListView(CourseViewModel viewModel) => ListView.separated(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
itemCount: viewModel.courseDetail.length,
|
itemCount: viewModel.courses.length,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
separatorBuilder: (context, index) => verticalSpaceSmall,
|
|
||||||
itemBuilder: (context, index) => _buildTile(
|
itemBuilder: (context, index) => _buildTile(
|
||||||
courseDetail: viewModel.courseDetail[index],
|
course: viewModel.courses[index],
|
||||||
onCourseTap: () async => await viewModel
|
onTap: () async => await viewModel.navigateToCourseCatalog()),
|
||||||
.navigateToCoursePayment(viewModel.courseDetail[index].course!),
|
separatorBuilder: (context, index) => verticalSpaceSmall,
|
||||||
onPracticeTap: () async => await viewModel.navigateToCoursePractice(
|
|
||||||
viewModel.courseDetail[index].course?.id ?? 0),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
//
|
||||||
Widget _buildTile({
|
Widget _buildTile({
|
||||||
GestureTapCallback? onCourseTap,
|
required GestureTapCallback onTap,
|
||||||
GestureTapCallback? onPracticeTap,
|
required Map<String, dynamic> course,
|
||||||
required CourseDetail courseDetail,
|
|
||||||
}) =>
|
}) =>
|
||||||
CourseTile(
|
CourseCard(
|
||||||
onCourseTap: onCourseTap,
|
onTap: onTap,
|
||||||
courseDetail: courseDetail,
|
course: course,
|
||||||
onPracticeTap: onPracticeTap,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,44 +5,47 @@ import '../../../app/app.locator.dart';
|
||||||
import '../../../app/app.router.dart';
|
import '../../../app/app.router.dart';
|
||||||
import '../../../models/course.dart';
|
import '../../../models/course.dart';
|
||||||
import '../../../models/course_detail.dart';
|
import '../../../models/course_detail.dart';
|
||||||
|
import '../../../models/user.dart';
|
||||||
|
import '../../../services/authentication_service.dart';
|
||||||
import '../../../services/course_service.dart';
|
import '../../../services/course_service.dart';
|
||||||
import '../../../services/status_checker_service.dart';
|
import '../../../services/status_checker_service.dart';
|
||||||
import '../../common/enmus.dart';
|
import '../../common/enmus.dart';
|
||||||
|
|
||||||
class CourseViewModel extends BaseViewModel {
|
class CourseViewModel extends ReactiveViewModel {
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
final _courseService = locator<CourseService>();
|
|
||||||
|
|
||||||
final _statusChecker = locator<StatusCheckerService>();
|
|
||||||
|
|
||||||
final _navigationService = locator<NavigationService>();
|
final _navigationService = locator<NavigationService>();
|
||||||
|
|
||||||
// Subcourse with progress
|
final _authenticationService = locator<AuthenticationService>();
|
||||||
List<CourseDetail> _courseDetail = [];
|
|
||||||
|
|
||||||
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
|
// Navigation
|
||||||
void pop() => _navigationService.back();
|
void pop() => _navigationService.back();
|
||||||
|
|
||||||
Future<void> navigateToCoursePractice(int id) =>
|
Future<void> navigateToCourseCatalog() async =>
|
||||||
_navigationService.navigateToCoursePracticeView(id: id);
|
await _navigationService.navigateToCourseCatalogView();
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,55 +1,50 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stacked/stacked.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/app_colors.dart';
|
||||||
import '../../common/enmus.dart';
|
import '../../common/enmus.dart';
|
||||||
import '../../common/ui_helpers.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/custom_circular_progress_indicator.dart';
|
||||||
import '../../widgets/small_app_bar.dart';
|
import '../../widgets/small_app_bar.dart';
|
||||||
import 'course_subcategory_viewmodel.dart';
|
import 'course_catalog_viewmodel.dart';
|
||||||
|
|
||||||
class CourseSubcategoryView extends StackedView<CourseSubcategoryViewModel> {
|
class CourseCatalogView extends StackedView<CourseCatalogViewModel> {
|
||||||
final Category category;
|
const CourseCatalogView({Key? key}) : super(key: key);
|
||||||
|
|
||||||
const CourseSubcategoryView({Key? key, required this.category})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onViewModelReady(CourseSubcategoryViewModel viewModel) async {
|
void onViewModelReady(CourseCatalogViewModel viewModel) async {
|
||||||
await viewModel.getSubcategories(category.id ?? 0);
|
await viewModel.getCourseCatalogs();
|
||||||
super.onViewModelReady(viewModel);
|
super.onViewModelReady(viewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
CourseSubcategoryViewModel viewModelBuilder(BuildContext context) =>
|
CourseCatalogViewModel viewModelBuilder(BuildContext context) =>
|
||||||
CourseSubcategoryViewModel();
|
CourseCatalogViewModel();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget builder(
|
Widget builder(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
CourseSubcategoryViewModel viewModel,
|
CourseCatalogViewModel viewModel,
|
||||||
Widget? child,
|
Widget? child,
|
||||||
) =>
|
) =>
|
||||||
_buildScaffoldWrapper(viewModel);
|
_buildScaffoldWrapper(viewModel);
|
||||||
|
|
||||||
Widget _buildScaffoldWrapper(CourseSubcategoryViewModel viewModel) =>
|
Widget _buildScaffoldWrapper(CourseCatalogViewModel viewModel) => Scaffold(
|
||||||
Scaffold(
|
|
||||||
backgroundColor: kcBackgroundColor,
|
backgroundColor: kcBackgroundColor,
|
||||||
body: _buildScaffold(viewModel),
|
body: _buildScaffold(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildScaffold(CourseSubcategoryViewModel viewModel) =>
|
Widget _buildScaffold(CourseCatalogViewModel viewModel) =>
|
||||||
SafeArea(child: _buildBody(viewModel));
|
SafeArea(child: _buildBody(viewModel));
|
||||||
|
|
||||||
Widget _buildBody(CourseSubcategoryViewModel viewModel) => Padding(
|
Widget _buildBody(CourseCatalogViewModel viewModel) => Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
child: _buildColumn(viewModel),
|
child: _buildColumn(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildColumn(CourseSubcategoryViewModel viewModel) => Column(
|
Widget _buildColumn(CourseCatalogViewModel viewModel) => Column(
|
||||||
children: [
|
children: [
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildAppBar(viewModel),
|
_buildAppBar(viewModel),
|
||||||
|
|
@ -58,27 +53,26 @@ class CourseSubcategoryView extends StackedView<CourseSubcategoryViewModel> {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildAppBar(CourseSubcategoryViewModel viewModel) => SmallAppBar(
|
Widget _buildAppBar(CourseCatalogViewModel viewModel) => SmallAppBar(
|
||||||
onPop: viewModel.pop,
|
onPop: viewModel.pop,
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildCoursesColumnWrapper(CourseSubcategoryViewModel viewModel) =>
|
Widget _buildCoursesColumnWrapper(CourseCatalogViewModel viewModel) =>
|
||||||
Expanded(child: _buildCoursesColumnScrollView(viewModel));
|
Expanded(child: _buildCoursesColumnScrollView(viewModel));
|
||||||
|
|
||||||
Widget _buildCoursesColumnScrollView(CourseSubcategoryViewModel viewModel) =>
|
Widget _buildCoursesColumnScrollView(CourseCatalogViewModel viewModel) =>
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
child: _buildCoursesColumn(viewModel),
|
child: _buildCoursesColumn(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildCoursesColumn(CourseSubcategoryViewModel viewModel) => Column(
|
Widget _buildCoursesColumn(CourseCatalogViewModel viewModel) => Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: _buildCoursesColumnChildren(viewModel),
|
children: _buildCoursesColumnChildren(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
List<Widget> _buildCoursesColumnChildren(
|
List<Widget> _buildCoursesColumnChildren(CourseCatalogViewModel viewModel) =>
|
||||||
CourseSubcategoryViewModel viewModel) =>
|
|
||||||
[
|
[
|
||||||
_buildTitle(),
|
_buildTitle(),
|
||||||
_buildSubtitle(),
|
_buildSubtitle(),
|
||||||
|
|
@ -87,17 +81,17 @@ class CourseSubcategoryView extends StackedView<CourseSubcategoryViewModel> {
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildTitle() => Text(
|
Widget _buildTitle() => Text(
|
||||||
'${category.name ?? ''} courses',
|
'English Proficiency Exams',
|
||||||
style: style18DG700,
|
style: style18DG700,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubtitle() => Text(
|
Widget _buildSubtitle() => Text(
|
||||||
'Explore variety of courses on this category.',
|
'Select your target exam and start preparing',
|
||||||
style: style14DG400,
|
style: style14DG400,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildListViewBuilder(CourseSubcategoryViewModel viewModel) =>
|
Widget _buildListViewBuilder(CourseCatalogViewModel viewModel) =>
|
||||||
viewModel.busy(StateObjects.subcategories)
|
viewModel.busy(StateObjects.courseCatalogs)
|
||||||
? _buildProgressIndicator()
|
? _buildProgressIndicator()
|
||||||
: _buildListView(viewModel);
|
: _buildListView(viewModel);
|
||||||
|
|
||||||
|
|
@ -105,27 +99,25 @@ class CourseSubcategoryView extends StackedView<CourseSubcategoryViewModel> {
|
||||||
child: CustomCircularProgressIndicator(color: kcPrimaryColor),
|
child: CustomCircularProgressIndicator(color: kcPrimaryColor),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildListView(CourseSubcategoryViewModel viewModel) =>
|
Widget _buildListView(CourseCatalogViewModel viewModel) => ListView.separated(
|
||||||
ListView.separated(
|
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
itemCount: viewModel.subcategories.length,
|
itemCount: viewModel.courseCatalogs.length,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
itemBuilder: (context, index) => _buildTile(
|
itemBuilder: (context, index) => _buildTile(
|
||||||
subcategory: viewModel.subcategories[index],
|
courseCatalog: viewModel.courseCatalogs[index],
|
||||||
onCourseTap: () async => await viewModel
|
onCourseTap: () {},
|
||||||
.navigateToSubcourse(viewModel.subcategories[index]),
|
onPracticeTap: () {}),
|
||||||
),
|
|
||||||
separatorBuilder: (context, index) => verticalSpaceSmall,
|
separatorBuilder: (context, index) => verticalSpaceSmall,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildTile({
|
Widget _buildTile({
|
||||||
GestureTapCallback? onCourseTap,
|
required CourseCatalog courseCatalog,
|
||||||
GestureTapCallback? onPracticeTap,
|
required GestureTapCallback onCourseTap,
|
||||||
required Subcategory subcategory,
|
required GestureTapCallback onPracticeTap,
|
||||||
}) =>
|
}) =>
|
||||||
CourseSubcategoryTile(
|
CourseCatalogTile(
|
||||||
subcategory: subcategory,
|
|
||||||
onCourseTap: onCourseTap,
|
onCourseTap: onCourseTap,
|
||||||
onPracticeTap: onPracticeTap,
|
onPracticeTap: onPracticeTap,
|
||||||
|
courseCatalog: courseCatalog,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:stacked_services/stacked_services.dart';
|
import 'package:stacked_services/stacked_services.dart';
|
||||||
|
import 'package:yimaru_app/models/course_catalog.dart';
|
||||||
|
|
||||||
import '../../../app/app.locator.dart';
|
import '../../../app/app.locator.dart';
|
||||||
import '../../../app/app.router.dart';
|
import '../../../app/app.router.dart';
|
||||||
import '../../../models/subcategory.dart';
|
|
||||||
import '../../../services/api_service.dart';
|
import '../../../services/api_service.dart';
|
||||||
import '../../../services/status_checker_service.dart';
|
import '../../../services/status_checker_service.dart';
|
||||||
import '../../common/enmus.dart';
|
import '../../common/enmus.dart';
|
||||||
|
|
||||||
class CourseSubcategoryViewModel extends BaseViewModel {
|
class CourseCatalogViewModel extends BaseViewModel {
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
final _apiService = locator<ApiService>();
|
final _apiService = locator<ApiService>();
|
||||||
|
|
||||||
|
|
@ -16,10 +16,10 @@ class CourseSubcategoryViewModel extends BaseViewModel {
|
||||||
|
|
||||||
final _navigationService = locator<NavigationService>();
|
final _navigationService = locator<NavigationService>();
|
||||||
|
|
||||||
// Course subcategories
|
// Course catalogs
|
||||||
List<Subcategory> _subcategories = [];
|
List<CourseCatalog> _courseCatalogs = [];
|
||||||
|
|
||||||
List<Subcategory> get subcategories => _subcategories;
|
List<CourseCatalog> get courseCatalogs => _courseCatalogs;
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
void pop() => _navigationService.back();
|
void pop() => _navigationService.back();
|
||||||
|
|
@ -27,19 +27,19 @@ class CourseSubcategoryViewModel extends BaseViewModel {
|
||||||
Future<void> navigateToCoursePractice(int id) async =>
|
Future<void> navigateToCoursePractice(int id) async =>
|
||||||
_navigationService.navigateToCoursePracticeView(id: id);
|
_navigationService.navigateToCoursePracticeView(id: id);
|
||||||
|
|
||||||
Future<void> navigateToSubcourse(Subcategory subcategory) async =>
|
// Future<void> navigateToSubcourse(Subcategory subcategory) async =>
|
||||||
_navigationService.navigateToCourseView(subcategory: subcategory);
|
// _navigationService.navigateToCourseView(subcategory: subcategory);
|
||||||
|
|
||||||
// Remote api call
|
// Remote api call
|
||||||
|
|
||||||
// Course subcategories
|
// Course catalogs
|
||||||
Future<void> getSubcategories(int id) async =>
|
Future<void> getCourseCatalogs() async =>
|
||||||
await runBusyFuture(_getSubcategories(id),
|
await runBusyFuture(_getSubcategories(),
|
||||||
busyObject: StateObjects.subcategories);
|
busyObject: StateObjects.courseCatalogs);
|
||||||
|
|
||||||
Future<void> _getSubcategories(int id) async {
|
Future<void> _getSubcategories() async {
|
||||||
if (await _statusChecker.checkConnection()) {
|
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;
|
_length = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (password == confirmPassword) {
|
if (password == confirmPassword) {
|
||||||
_passwordMatch = true;
|
_passwordMatch = true;
|
||||||
} else {
|
} 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/views/profile/profile_view.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/coming_soon.dart';
|
import 'package:yimaru_app/ui/widgets/coming_soon.dart';
|
||||||
|
|
||||||
|
import '../course/course_view.dart';
|
||||||
import 'home_viewmodel.dart';
|
import 'home_viewmodel.dart';
|
||||||
|
|
||||||
class HomeView extends StackedView<HomeViewModel> {
|
class HomeView extends StackedView<HomeViewModel> {
|
||||||
|
|
@ -69,7 +70,7 @@ class HomeView extends StackedView<HomeViewModel> {
|
||||||
case 0:
|
case 0:
|
||||||
return const LearnProgramView();
|
return const LearnProgramView();
|
||||||
case 1:
|
case 1:
|
||||||
return const ComingSoon();
|
return const CourseView();
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return const ProfileView();
|
return const ProfileView();
|
||||||
|
|
|
||||||
|
|
@ -78,15 +78,15 @@ class LearnCourseView extends StackedView<LearnCourseViewModel> {
|
||||||
|
|
||||||
Widget _buildListView(LearnCourseViewModel viewModel) => ListView.separated(
|
Widget _buildListView(LearnCourseViewModel viewModel) => ListView.separated(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
itemCount: viewModel.learnCourses.length,
|
itemCount: viewModel.courses.length,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
itemBuilder: (context, index) => _buildTile(
|
itemBuilder: (context, index) => _buildTile(
|
||||||
course: viewModel.learnCourses[index],
|
course: viewModel.courses[index],
|
||||||
onViewTap: () async => await viewModel
|
onViewTap: () async =>
|
||||||
.navigateToLearnModule(viewModel.learnCourses[index]),
|
await viewModel.navigateToLearnModule(viewModel.courses[index]),
|
||||||
onPracticeTap: () async => await viewModel.navigateToLearnPractice(
|
onPracticeTap: () async => await viewModel.navigateToLearnPractice(
|
||||||
id: viewModel.learnCourses[index].id ?? 0,
|
id: viewModel.courses[index].id ?? 0,
|
||||||
level: viewModel.learnCourses[index].name ?? ''),
|
level: viewModel.courses[index].name ?? ''),
|
||||||
),
|
),
|
||||||
separatorBuilder: (context, index) => verticalSpaceSmall,
|
separatorBuilder: (context, index) => verticalSpaceSmall,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,21 +5,25 @@ import 'package:yimaru_app/app/app.router.dart';
|
||||||
import '../../../app/app.locator.dart';
|
import '../../../app/app.locator.dart';
|
||||||
import '../../../models/learn_course.dart';
|
import '../../../models/learn_course.dart';
|
||||||
import '../../../services/api_service.dart';
|
import '../../../services/api_service.dart';
|
||||||
|
import '../../../services/learn_service.dart';
|
||||||
import '../../../services/status_checker_service.dart';
|
import '../../../services/status_checker_service.dart';
|
||||||
import '../../common/enmus.dart';
|
import '../../common/enmus.dart';
|
||||||
|
|
||||||
class LearnCourseViewModel extends BaseViewModel {
|
class LearnCourseViewModel extends ReactiveViewModel {
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
final _apiService = locator<ApiService>();
|
final _learnService = locator<LearnService>();
|
||||||
|
|
||||||
final _statusChecker = locator<StatusCheckerService>();
|
final _statusChecker = locator<StatusCheckerService>();
|
||||||
|
|
||||||
final _navigationService = locator<NavigationService>();
|
final _navigationService = locator<NavigationService>();
|
||||||
|
|
||||||
// Learn courses
|
@override
|
||||||
List<LearnCourse> _learnCourses = [];
|
List<ListenableServiceMixin> get listenableServices => [_learnService];
|
||||||
|
|
||||||
List<LearnCourse> get learnCourses => _learnCourses;
|
// Learn lessons
|
||||||
|
List<LearnCourse> get _courses => _learnService.courses;
|
||||||
|
|
||||||
|
List<LearnCourse> get courses => _courses;
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
void pop() => _navigationService.back();
|
void pop() => _navigationService.back();
|
||||||
|
|
@ -46,12 +50,8 @@ class LearnCourseViewModel extends BaseViewModel {
|
||||||
busyObject: StateObjects.learnCourses);
|
busyObject: StateObjects.learnCourses);
|
||||||
|
|
||||||
Future<void> _getLearnCourses(int id) async {
|
Future<void> _getLearnCourses(int id) async {
|
||||||
if (_learnCourses.isEmpty) {
|
|
||||||
if (await _statusChecker.checkConnection()) {
|
if (await _statusChecker.checkConnection()) {
|
||||||
_learnCourses = await _apiService.getLearnCourse(id);
|
await _learnService.getLearnCourses(id);
|
||||||
_learnCourses
|
|
||||||
.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,11 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
|
||||||
style: style14DG500,
|
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();
|
Widget _buildMotivationCard() => const MotivationCard();
|
||||||
|
|
||||||
|
|
@ -159,6 +163,7 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
|
||||||
index: index,
|
index: index,
|
||||||
lesson: viewModel.lessons[index],
|
lesson: viewModel.lessons[index],
|
||||||
onLessonTap: () async => await viewModel.navigateToLearnLessonDetail(
|
onLessonTap: () async => await viewModel.navigateToLearnLessonDetail(
|
||||||
|
module: module,
|
||||||
lesson: viewModel.lessons[index],
|
lesson: viewModel.lessons[index],
|
||||||
hasPractice:
|
hasPractice:
|
||||||
index != viewModel.lessons.length - 1 ? true : false),
|
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 'package:yimaru_app/ui/common/enmus.dart';
|
||||||
|
|
||||||
import '../../../app/app.locator.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';
|
import '../../../services/status_checker_service.dart';
|
||||||
|
|
||||||
class LearnLessonViewModel extends BaseViewModel {
|
class LearnLessonViewModel extends ReactiveViewModel {
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
final _apiService = locator<ApiService>();
|
final _learnService = locator<LearnService>();
|
||||||
|
|
||||||
final _statusChecker = locator<StatusCheckerService>();
|
final _statusChecker = locator<StatusCheckerService>();
|
||||||
|
|
||||||
final _navigationService = locator<NavigationService>();
|
final _navigationService = locator<NavigationService>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<ListenableServiceMixin> get listenableServices => [_learnService];
|
||||||
|
|
||||||
// Learn lessons
|
// Learn lessons
|
||||||
List<LearnLesson> _lessons = [];
|
List<LearnLesson> get _lessons => _learnService.lessons;
|
||||||
|
|
||||||
List<LearnLesson> get lessons => _lessons;
|
List<LearnLesson> get lessons => _lessons;
|
||||||
|
|
||||||
|
|
@ -35,9 +39,11 @@ class LearnLessonViewModel extends BaseViewModel {
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<void> navigateToLearnLessonDetail(
|
Future<void> navigateToLearnLessonDetail(
|
||||||
{required bool hasPractice, required LearnLesson lesson}) async =>
|
{required bool hasPractice,
|
||||||
|
required LearnLesson lesson,
|
||||||
|
required LearnModule module}) async =>
|
||||||
await _navigationService.navigateToLearnLessonDetailView(
|
await _navigationService.navigateToLearnLessonDetailView(
|
||||||
lesson: lesson, hasPractice: hasPractice);
|
lesson: lesson, module: module, hasPractice: hasPractice);
|
||||||
|
|
||||||
// Remote api call
|
// Remote api call
|
||||||
|
|
||||||
|
|
@ -46,11 +52,8 @@ class LearnLessonViewModel extends BaseViewModel {
|
||||||
busyObject: StateObjects.learnLessons);
|
busyObject: StateObjects.learnLessons);
|
||||||
|
|
||||||
Future<void> _getLessons(int id) async {
|
Future<void> _getLessons(int id) async {
|
||||||
if (_lessons.isEmpty) {
|
|
||||||
if (await _statusChecker.checkConnection()) {
|
if (await _statusChecker.checkConnection()) {
|
||||||
_lessons = await _apiService.getLearnLessons(id);
|
await _learnService.getLearnLessons(id);
|
||||||
_lessons.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import 'package:chewie/chewie.dart';
|
import 'package:chewie/chewie.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stacked/stacked.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_lesson.dart';
|
||||||
|
import 'package:yimaru_app/models/learn_module.dart';
|
||||||
|
|
||||||
import '../../common/app_colors.dart';
|
import '../../common/app_colors.dart';
|
||||||
import '../../common/enmus.dart';
|
import '../../common/enmus.dart';
|
||||||
|
|
@ -14,10 +14,14 @@ import 'learn_lesson_detail_viewmodel.dart';
|
||||||
|
|
||||||
class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
|
class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
|
||||||
final bool hasPractice;
|
final bool hasPractice;
|
||||||
|
final LearnModule module;
|
||||||
final LearnLesson lesson;
|
final LearnLesson lesson;
|
||||||
|
|
||||||
const LearnLessonDetailView(
|
const LearnLessonDetailView(
|
||||||
{Key? key, required this.lesson, required this.hasPractice})
|
{Key? key,
|
||||||
|
required this.lesson,
|
||||||
|
required this.module,
|
||||||
|
required this.hasPractice})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
Future<void> _navigate(LearnLessonDetailViewModel viewModel) async {
|
Future<void> _navigate(LearnLessonDetailViewModel viewModel) async {
|
||||||
|
|
@ -27,7 +31,11 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onViewModelReady(LearnLessonDetailViewModel viewModel) async {
|
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);
|
super.onViewModelReady(viewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,8 +152,9 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
|
||||||
Widget _buildVideoPlayer(LearnLessonDetailViewModel viewModel) =>
|
Widget _buildVideoPlayer(LearnLessonDetailViewModel viewModel) =>
|
||||||
_buildChewiePlayer(viewModel);
|
_buildChewiePlayer(viewModel);
|
||||||
|
|
||||||
Widget _buildChewiePlayer(LearnLessonDetailViewModel viewModel) =>
|
Widget _buildChewiePlayer(LearnLessonDetailViewModel viewModel) => Chewie(
|
||||||
Chewie(controller: viewModel.chewieController!);
|
controller: viewModel.chewieController!,
|
||||||
|
);
|
||||||
|
|
||||||
Widget _buildEmptyVideoPlayer() => const EmptyVideoPlayer();
|
Widget _buildEmptyVideoPlayer() => const EmptyVideoPlayer();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:chewie/chewie.dart';
|
import 'package:chewie/chewie.dart';
|
||||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:stacked_services/stacked_services.dart';
|
import 'package:stacked_services/stacked_services.dart';
|
||||||
import 'package:video_player/video_player.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.locator.dart';
|
||||||
import '../../../app/app.router.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/status_checker_service.dart';
|
||||||
import '../../../services/vimeo_service.dart';
|
import '../../../services/vimeo_service.dart';
|
||||||
import '../../common/app_constants.dart';
|
|
||||||
import '../../common/helper_functions.dart';
|
|
||||||
import '../../common/ui_helpers.dart';
|
import '../../common/ui_helpers.dart';
|
||||||
|
|
||||||
class LearnLessonDetailViewModel extends BaseViewModel {
|
class LearnLessonDetailViewModel extends ReactiveViewModel {
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
|
|
||||||
|
final _apiService = locator<ApiService>();
|
||||||
|
|
||||||
|
final _learnService = locator<LearnService>();
|
||||||
|
|
||||||
final _vimeoService = locator<VimeoService>();
|
final _vimeoService = locator<VimeoService>();
|
||||||
|
|
||||||
|
final _statusChecker = locator<StatusCheckerService>();
|
||||||
|
|
||||||
final _navigationService = locator<NavigationService>();
|
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
|
// Video player config
|
||||||
|
bool _lessonCompleted = false;
|
||||||
|
|
||||||
ChewieController? _chewieController;
|
ChewieController? _chewieController;
|
||||||
|
|
||||||
ChewieController? get chewieController => _chewieController;
|
ChewieController? get chewieController => _chewieController;
|
||||||
|
|
@ -38,11 +55,18 @@ class LearnLessonDetailViewModel extends BaseViewModel {
|
||||||
await _chewieController?.pause();
|
await _chewieController?.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initializePlayer(String url) async =>
|
Future<void> initializePlayer(
|
||||||
await runBusyFuture(_initializePlayer(url),
|
{required String url,
|
||||||
|
required int lessonId,
|
||||||
|
required int moduleId}) async =>
|
||||||
|
await runBusyFuture(
|
||||||
|
_initializePlayer(url: url, moduleId: moduleId, lessonId: lessonId),
|
||||||
busyObject: StateObjects.loadLessonVideo);
|
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);
|
final playableUrl = await _vimeoService.getVideoUrl(url);
|
||||||
|
|
||||||
if (playableUrl == null) {
|
if (playableUrl == null) {
|
||||||
|
|
@ -54,11 +78,41 @@ class LearnLessonDetailViewModel extends BaseViewModel {
|
||||||
|
|
||||||
await _videoPlayerController?.initialize();
|
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(
|
_chewieController = ChewieController(
|
||||||
videoPlayerController: _videoPlayerController!,
|
looping: false,
|
||||||
autoPlay: true,
|
autoPlay: true,
|
||||||
looping: true,
|
showOptions: true,
|
||||||
|
showControls: true,
|
||||||
aspectRatio: 16 / 9,
|
aspectRatio: 16 / 9,
|
||||||
|
autoInitialize: true,
|
||||||
|
allowedScreenSleep: false,
|
||||||
|
videoPlayerController: _videoPlayerController!,
|
||||||
|
materialProgressColors: buildChewieProgressIndicator,
|
||||||
);
|
);
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
@ -76,4 +130,24 @@ class LearnLessonDetailViewModel extends BaseViewModel {
|
||||||
subtitle:
|
subtitle:
|
||||||
'I’ll ask you a few questions, and you can respond naturally.',
|
'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(
|
Widget _buildOverallProgress() => OverallLearnProgress(
|
||||||
indicatorBackgroundColor: kcWhite,
|
indicatorBackgroundColor: kcWhite,
|
||||||
|
progress: course.access?.progressPercent ?? 0,
|
||||||
backgroundColor: kcPrimaryColor.withOpacity(0.1),
|
backgroundColor: kcPrimaryColor.withOpacity(0.1),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -116,6 +117,7 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
itemBuilder: (context, index) => _buildTile(
|
itemBuilder: (context, index) => _buildTile(
|
||||||
module: viewModel.modules[index],
|
module: viewModel.modules[index],
|
||||||
|
onLockTap: () async => await viewModel.navigateToLearnSubscription(),
|
||||||
onPracticeTap: () async => await viewModel.navigateToLearnPractice(
|
onPracticeTap: () async => await viewModel.navigateToLearnPractice(
|
||||||
id: viewModel.modules[index].id ?? 0,
|
id: viewModel.modules[index].id ?? 0,
|
||||||
module: viewModel.modules[index].name ?? ''),
|
module: viewModel.modules[index].name ?? ''),
|
||||||
|
|
@ -126,11 +128,13 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
|
||||||
|
|
||||||
Widget _buildTile({
|
Widget _buildTile({
|
||||||
required LearnModule module,
|
required LearnModule module,
|
||||||
|
required GestureTapCallback onLockTap,
|
||||||
required GestureTapCallback onModuleTap,
|
required GestureTapCallback onModuleTap,
|
||||||
required GestureTapCallback onPracticeTap,
|
required GestureTapCallback onPracticeTap,
|
||||||
}) =>
|
}) =>
|
||||||
LearnModuleTile(
|
LearnModuleTile(
|
||||||
module: module,
|
module: module,
|
||||||
|
onLockTap: onLockTap,
|
||||||
onModuleTap: onModuleTap,
|
onModuleTap: onModuleTap,
|
||||||
onPracticeTap: onPracticeTap);
|
onPracticeTap: onPracticeTap);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,25 +5,32 @@ import 'package:yimaru_app/models/learn_module.dart';
|
||||||
|
|
||||||
import '../../../app/app.locator.dart';
|
import '../../../app/app.locator.dart';
|
||||||
import '../../../services/api_service.dart';
|
import '../../../services/api_service.dart';
|
||||||
|
import '../../../services/learn_service.dart';
|
||||||
import '../../../services/status_checker_service.dart';
|
import '../../../services/status_checker_service.dart';
|
||||||
import '../../common/enmus.dart';
|
import '../../common/enmus.dart';
|
||||||
|
|
||||||
class LearnModuleViewModel extends BaseViewModel {
|
class LearnModuleViewModel extends ReactiveViewModel {
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
final _apiService = locator<ApiService>();
|
final _learnService = locator<LearnService>();
|
||||||
|
|
||||||
final _statusChecker = locator<StatusCheckerService>();
|
final _statusChecker = locator<StatusCheckerService>();
|
||||||
|
|
||||||
final _navigationService = locator<NavigationService>();
|
final _navigationService = locator<NavigationService>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<ListenableServiceMixin> get listenableServices => [_learnService];
|
||||||
|
|
||||||
// Learn module
|
// Learn module
|
||||||
List<LearnModule> _modules = [];
|
List<LearnModule> get _modules => _learnService.modules;
|
||||||
|
|
||||||
List<LearnModule> get modules => _modules;
|
List<LearnModule> get modules => _modules;
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
void pop() => _navigationService.back();
|
void pop() => _navigationService.back();
|
||||||
|
|
||||||
|
Future<void> navigateToLearnSubscription() async =>
|
||||||
|
await _navigationService.navigateToLearnSubscriptionView();
|
||||||
|
|
||||||
Future<void> navigateToLearnLesson(LearnModule module) async =>
|
Future<void> navigateToLearnLesson(LearnModule module) async =>
|
||||||
await _navigationService.navigateToLearnLessonView(module: module);
|
await _navigationService.navigateToLearnLessonView(module: module);
|
||||||
|
|
||||||
|
|
@ -45,11 +52,8 @@ class LearnModuleViewModel extends BaseViewModel {
|
||||||
busyObject: StateObjects.learnModules);
|
busyObject: StateObjects.learnModules);
|
||||||
|
|
||||||
Future<void> _getLearnModules(int id) async {
|
Future<void> _getLearnModules(int id) async {
|
||||||
if (_modules.isEmpty) {
|
|
||||||
if (await _statusChecker.checkConnection()) {
|
if (await _statusChecker.checkConnection()) {
|
||||||
_modules = await _apiService.getLearnModules(id);
|
await _learnService.getLearnModules(id);
|
||||||
_modules.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildBodyState(LearnPracticeViewModel viewModel) =>
|
Widget _buildBodyState(LearnPracticeViewModel viewModel) =>
|
||||||
viewModel.busy(StateObjects.learnPractices)
|
viewModel.busy(StateObjects.learnPractice)
|
||||||
? const PageLoadingIndicator()
|
? const PageLoadingIndicator()
|
||||||
: viewModel.practices.isEmpty || viewModel.questions.isEmpty
|
: viewModel.practices.isEmpty || viewModel.questions.isEmpty
|
||||||
? _buildPageLoadingIndicator(viewModel)
|
? _buildPageLoadingIndicator(viewModel)
|
||||||
|
|
@ -101,7 +101,7 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
|
||||||
|
|
||||||
Widget _buildPageLoadingIndicator(LearnPracticeViewModel viewModel) =>
|
Widget _buildPageLoadingIndicator(LearnPracticeViewModel viewModel) =>
|
||||||
LearnLoadingScreen(
|
LearnLoadingScreen(
|
||||||
isLoading: viewModel.busy(StateObjects.learnPractices),
|
isLoading: viewModel.busy(StateObjects.learnPractice),
|
||||||
onTap: () async =>
|
onTap: () async =>
|
||||||
await viewModel.getLearnPractices(id: id, practice: practice),
|
await viewModel.getLearnPractices(id: id, practice: practice),
|
||||||
onPop: viewModel.practices.isEmpty || viewModel.questions.isEmpty
|
onPop: viewModel.practices.isEmpty || viewModel.questions.isEmpty
|
||||||
|
|
@ -142,6 +142,7 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
|
||||||
Widget _buildLearnPracticeResultScreen() =>
|
Widget _buildLearnPracticeResultScreen() =>
|
||||||
LearnPracticeResultScreen(practice: practice);
|
LearnPracticeResultScreen(practice: practice);
|
||||||
|
|
||||||
Widget _buildLearnPracticeCompletionScreen() =>
|
Widget _buildLearnPracticeCompletionScreen() => LearnPracticeCompletionScreen(
|
||||||
LearnPracticeCompletionScreen(level: level ?? '');
|
level: level ?? '',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,17 @@ import '../../../app/app.locator.dart';
|
||||||
import '../../../models/learn_question.dart';
|
import '../../../models/learn_question.dart';
|
||||||
import '../../../services/api_service.dart';
|
import '../../../services/api_service.dart';
|
||||||
import '../../../services/audio_player_service.dart';
|
import '../../../services/audio_player_service.dart';
|
||||||
|
import '../../../services/learn_service.dart';
|
||||||
import '../../../services/status_checker_service.dart';
|
import '../../../services/status_checker_service.dart';
|
||||||
import '../../common/app_colors.dart';
|
import '../../common/app_colors.dart';
|
||||||
|
|
||||||
class LearnPracticeViewModel extends ReactiveViewModel {
|
class LearnPracticeViewModel extends ReactiveViewModel {
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
|
|
||||||
final _apiService = locator<ApiService>();
|
final _apiService = locator<ApiService>();
|
||||||
|
|
||||||
|
final _learnService = locator<LearnService>();
|
||||||
|
|
||||||
final _dialogService = locator<DialogService>();
|
final _dialogService = locator<DialogService>();
|
||||||
|
|
||||||
final _statusChecker = locator<StatusCheckerService>();
|
final _statusChecker = locator<StatusCheckerService>();
|
||||||
|
|
@ -37,8 +41,12 @@ class LearnPracticeViewModel extends ReactiveViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<ListenableServiceMixin> get listenableServices =>
|
List<ListenableServiceMixin> get listenableServices => [
|
||||||
[_audioPlayerService, _voiceRecorderService, _authenticationService];
|
_learnService,
|
||||||
|
_audioPlayerService,
|
||||||
|
_voiceRecorderService,
|
||||||
|
_authenticationService
|
||||||
|
];
|
||||||
|
|
||||||
// User
|
// User
|
||||||
User? get _user => _authenticationService.user;
|
User? get _user => _authenticationService.user;
|
||||||
|
|
@ -256,7 +264,7 @@ class LearnPracticeViewModel extends ReactiveViewModel {
|
||||||
Future<void> getLearnPractices(
|
Future<void> getLearnPractices(
|
||||||
{required int id, required LearnPractices practice}) async =>
|
{required int id, required LearnPractices practice}) async =>
|
||||||
await runBusyFuture(_getLearnPractices(id: id, practice: practice),
|
await runBusyFuture(_getLearnPractices(id: id, practice: practice),
|
||||||
busyObject: StateObjects.learnPractices);
|
busyObject: StateObjects.learnPractice);
|
||||||
|
|
||||||
Future<void> _getLearnPractices(
|
Future<void> _getLearnPractices(
|
||||||
{required int id, required LearnPractices practice}) async {
|
{required int id, required LearnPractices practice}) async {
|
||||||
|
|
@ -279,4 +287,15 @@ class LearnPracticeViewModel extends ReactiveViewModel {
|
||||||
Future<void> _getLearnPracticeQuestions(int id) async {
|
Future<void> _getLearnPracticeQuestions(int id) async {
|
||||||
_questions = await _apiService.getLearnQuestions(id);
|
_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:stacked/stacked.dart';
|
||||||
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.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/app_colors.dart';
|
||||||
|
import '../../../common/enmus.dart';
|
||||||
import '../../../common/ui_helpers.dart';
|
import '../../../common/ui_helpers.dart';
|
||||||
import '../../../widgets/custom_elevated_button.dart';
|
import '../../../widgets/custom_elevated_button.dart';
|
||||||
|
import '../../../widgets/page_loading_indicator.dart';
|
||||||
|
|
||||||
class LearnPracticeCompletionScreen
|
class LearnPracticeCompletionScreen
|
||||||
extends ViewModelWidget<LearnPracticeViewModel> {
|
extends ViewModelWidget<LearnPracticeViewModel> {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import '../../../common/ui_helpers.dart';
|
||||||
import '../../../widgets/cancel_learn_practice_sheet.dart';
|
import '../../../widgets/cancel_learn_practice_sheet.dart';
|
||||||
import '../../../widgets/custom_elevated_button.dart';
|
import '../../../widgets/custom_elevated_button.dart';
|
||||||
import '../../../widgets/small_app_bar.dart';
|
import '../../../widgets/small_app_bar.dart';
|
||||||
import '../../../widgets/speaking_partner_image.dart';
|
|
||||||
|
|
||||||
class LearnPracticeDescriptionScreen
|
class LearnPracticeDescriptionScreen
|
||||||
extends ViewModelWidget<LearnPracticeViewModel> {
|
extends ViewModelWidget<LearnPracticeViewModel> {
|
||||||
|
|
@ -121,6 +120,7 @@ class LearnPracticeDescriptionScreen
|
||||||
_buildSubtitle(viewModel),
|
_buildSubtitle(viewModel),
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildImageContainer(viewModel),
|
_buildImageContainer(viewModel),
|
||||||
|
verticalSpaceLarge
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildTitle(LearnPracticeViewModel viewModel) => Text.rich(
|
Widget _buildTitle(LearnPracticeViewModel viewModel) => Text.rich(
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import '../../../common/app_colors.dart';
|
||||||
import '../../../common/ui_helpers.dart';
|
import '../../../common/ui_helpers.dart';
|
||||||
import '../../../widgets/cancel_learn_practice_sheet.dart';
|
import '../../../widgets/cancel_learn_practice_sheet.dart';
|
||||||
import '../../../widgets/custom_elevated_button.dart';
|
import '../../../widgets/custom_elevated_button.dart';
|
||||||
|
import '../../../widgets/page_loading_indicator.dart';
|
||||||
import '../../../widgets/small_app_bar.dart';
|
import '../../../widgets/small_app_bar.dart';
|
||||||
|
|
||||||
class LearnPracticeResultScreen
|
class LearnPracticeResultScreen
|
||||||
|
|
@ -18,6 +19,7 @@ class LearnPracticeResultScreen
|
||||||
const LearnPracticeResultScreen({super.key, required this.practice});
|
const LearnPracticeResultScreen({super.key, required this.practice});
|
||||||
|
|
||||||
Future<void> _navigate(LearnPracticeViewModel viewModel) async {
|
Future<void> _navigate(LearnPracticeViewModel viewModel) async {
|
||||||
|
await viewModel.completeLearnPractices();
|
||||||
if (practice == LearnPractices.course) {
|
if (practice == LearnPractices.course) {
|
||||||
viewModel.goTo(5);
|
viewModel.goTo(5);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -53,9 +55,16 @@ class LearnPracticeResultScreen
|
||||||
required LearnPracticeViewModel viewModel}) =>
|
required LearnPracticeViewModel viewModel}) =>
|
||||||
Scaffold(
|
Scaffold(
|
||||||
backgroundColor: kcBackgroundColor,
|
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(
|
Widget _buildScaffold(
|
||||||
{required BuildContext context,
|
{required BuildContext context,
|
||||||
required LearnPracticeViewModel viewModel}) =>
|
required LearnPracticeViewModel viewModel}) =>
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,14 @@ import '../../../app/app.locator.dart';
|
||||||
import '../../../models/user.dart';
|
import '../../../models/user.dart';
|
||||||
import '../../../services/api_service.dart';
|
import '../../../services/api_service.dart';
|
||||||
import '../../../services/authentication_service.dart';
|
import '../../../services/authentication_service.dart';
|
||||||
|
import '../../../services/learn_service.dart';
|
||||||
import '../../../services/status_checker_service.dart';
|
import '../../../services/status_checker_service.dart';
|
||||||
import '../../common/enmus.dart';
|
import '../../common/enmus.dart';
|
||||||
|
|
||||||
class LearnProgramViewModel extends ReactiveViewModel {
|
class LearnProgramViewModel extends ReactiveViewModel {
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
final _apiService = locator<ApiService>();
|
|
||||||
|
final _learnService = locator<LearnService>();
|
||||||
|
|
||||||
final _statusChecker = locator<StatusCheckerService>();
|
final _statusChecker = locator<StatusCheckerService>();
|
||||||
|
|
||||||
|
|
@ -22,7 +24,7 @@ class LearnProgramViewModel extends ReactiveViewModel {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<ListenableServiceMixin> get listenableServices =>
|
List<ListenableServiceMixin> get listenableServices =>
|
||||||
[_authenticationService];
|
[_learnService, _authenticationService];
|
||||||
|
|
||||||
// Current user
|
// Current user
|
||||||
User? get _user => _authenticationService.user;
|
User? get _user => _authenticationService.user;
|
||||||
|
|
@ -30,7 +32,7 @@ class LearnProgramViewModel extends ReactiveViewModel {
|
||||||
User? get user => _user;
|
User? get user => _user;
|
||||||
|
|
||||||
// Learn programs
|
// Learn programs
|
||||||
List<LearnProgram> _learnPrograms = [];
|
List<LearnProgram> get _learnPrograms => _learnService.programs;
|
||||||
|
|
||||||
List<LearnProgram> get learnPrograms => _learnPrograms;
|
List<LearnProgram> get learnPrograms => _learnPrograms;
|
||||||
|
|
||||||
|
|
@ -46,12 +48,8 @@ class LearnProgramViewModel extends ReactiveViewModel {
|
||||||
busyObject: StateObjects.learnPrograms);
|
busyObject: StateObjects.learnPrograms);
|
||||||
|
|
||||||
Future<void> _getLearnPrograms() async {
|
Future<void> _getLearnPrograms() async {
|
||||||
if (_learnPrograms.isEmpty) {
|
|
||||||
if (await _statusChecker.checkConnection()) {
|
if (await _statusChecker.checkConnection()) {
|
||||||
_learnPrograms = await _apiService.getLearnPrograms();
|
await _learnService.getLearnPrograms();
|
||||||
_learnPrograms
|
|
||||||
.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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 'package:yimaru_app/ui/widgets/large_app_bar.dart';
|
||||||
|
|
||||||
import '../../../widgets/custom_dropdown.dart';
|
import '../../../widgets/custom_dropdown.dart';
|
||||||
import '../onboarding_view.form.dart';
|
|
||||||
|
|
||||||
class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
const OccupationFormScreen({super.key});
|
const OccupationFormScreen({super.key});
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@ import 'package:stacked/stacked.dart';
|
||||||
import 'package:stacked_services/stacked_services.dart';
|
import 'package:stacked_services/stacked_services.dart';
|
||||||
import 'package:yimaru_app/app/app.router.dart';
|
import 'package:yimaru_app/app/app.router.dart';
|
||||||
import 'package:yimaru_app/services/image_picker_service.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 'package:yimaru_app/ui/common/enmus.dart';
|
||||||
|
|
||||||
import '../../../app/app.locator.dart';
|
import '../../../app/app.locator.dart';
|
||||||
|
|
@ -12,7 +10,6 @@ import '../../../services/api_service.dart';
|
||||||
import '../../../services/authentication_service.dart';
|
import '../../../services/authentication_service.dart';
|
||||||
import '../../../services/google_auth_service.dart';
|
import '../../../services/google_auth_service.dart';
|
||||||
import '../../../services/status_checker_service.dart';
|
import '../../../services/status_checker_service.dart';
|
||||||
import '../../../services/url_launcher_service.dart';
|
|
||||||
import '../../common/app_colors.dart';
|
import '../../common/app_colors.dart';
|
||||||
|
|
||||||
class ProfileViewModel extends ReactiveViewModel {
|
class ProfileViewModel extends ReactiveViewModel {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ class TelegramSupportViewModel extends BaseViewModel {
|
||||||
|
|
||||||
// Launch telegram
|
// Launch telegram
|
||||||
Future<void> launchTelegram() =>
|
Future<void> launchTelegram() =>
|
||||||
_urlLauncherService.launchUri(kTelegramSupport);
|
_urlLauncherService.launchUri(kTelegramSupportLink);
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
void pop() => _navigationService.back();
|
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 'package:yimaru_app/services/authentication_service.dart';
|
||||||
|
|
||||||
import '../../../app/app.locator.dart';
|
import '../../../app/app.locator.dart';
|
||||||
import '../../../services/status_checker_service.dart';
|
|
||||||
|
|
||||||
class WelcomeViewModel extends BaseViewModel {
|
class WelcomeViewModel extends BaseViewModel {
|
||||||
// Dependency Injection
|
// Dependency Injection
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,21 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.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/app_colors.dart';
|
||||||
import '../common/ui_helpers.dart';
|
import '../common/ui_helpers.dart';
|
||||||
import 'custom_elevated_button.dart';
|
import 'custom_elevated_button.dart';
|
||||||
|
|
||||||
class CourseSubcategoryTile extends StatelessWidget {
|
class CourseCatalogTile extends StatelessWidget {
|
||||||
final Subcategory subcategory;
|
final CourseCatalog courseCatalog;
|
||||||
final GestureTapCallback? onCourseTap;
|
final GestureTapCallback? onCourseTap;
|
||||||
final GestureTapCallback? onPracticeTap;
|
final GestureTapCallback? onPracticeTap;
|
||||||
|
|
||||||
const CourseSubcategoryTile({
|
const CourseCatalogTile({
|
||||||
super.key,
|
super.key,
|
||||||
this.onCourseTap,
|
this.onCourseTap,
|
||||||
this.onPracticeTap,
|
this.onPracticeTap,
|
||||||
required this.subcategory,
|
required this.courseCatalog,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -50,16 +50,14 @@ class CourseSubcategoryTile extends StatelessWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
List<Widget> _buildExpansionTileChildren() => [
|
List<Widget> _buildExpansionTileChildren() => [
|
||||||
// _buildProgressRow(),
|
_buildProgressRow(),
|
||||||
// verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
_buildActionButtonWrapper(),
|
_buildActionButtonWrapper(),
|
||||||
verticalSpaceSmall
|
verticalSpaceSmall
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildTitle() => Text(
|
Widget _buildTitle() => Text(
|
||||||
(subcategory.name == null || subcategory.name!.isEmpty)
|
courseCatalog.name ?? '',
|
||||||
? 'Course ${subcategory.id}'
|
|
||||||
: subcategory.name!,
|
|
||||||
style: style16P600,
|
style: style16P600,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -76,12 +74,12 @@ class CourseSubcategoryTile extends StatelessWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildProgressStatus() => const CustomLinearProgressIndicator(
|
Widget _buildProgressStatus() => const CustomLinearProgressIndicator(
|
||||||
progress: 0.75,
|
progress: 0,
|
||||||
activeColor: kcPrimaryColor,
|
activeColor: kcPrimaryColor,
|
||||||
backgroundColor: kcVeryLightGrey);
|
backgroundColor: kcVeryLightGrey);
|
||||||
|
|
||||||
Widget _buildProgress() => const Text(
|
Widget _buildProgress() => const Text(
|
||||||
'75%',
|
'0%',
|
||||||
style: TextStyle(color: kcDarkGrey),
|
style: TextStyle(color: kcDarkGrey),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -113,7 +111,7 @@ class CourseSubcategoryTile extends StatelessWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildExamButtonWrapper() => Expanded(
|
Widget _buildExamButtonWrapper() => Expanded(
|
||||||
child: Container(),
|
child: _buildExamButton(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildExamButton() => CustomElevatedButton(
|
Widget _buildExamButton() => CustomElevatedButton(
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:yimaru_app/models/category.dart';
|
|
||||||
import 'package:yimaru_app/ui/common/helper_functions.dart';
|
import 'package:yimaru_app/ui/common/helper_functions.dart';
|
||||||
|
|
||||||
import '../common/app_colors.dart';
|
import '../common/app_colors.dart';
|
||||||
|
|
@ -7,11 +6,11 @@ import '../common/app_strings.dart';
|
||||||
import '../common/ui_helpers.dart';
|
import '../common/ui_helpers.dart';
|
||||||
import 'custom_elevated_button.dart';
|
import 'custom_elevated_button.dart';
|
||||||
|
|
||||||
class CourseCategoryCard extends StatelessWidget {
|
class CourseCard extends StatelessWidget {
|
||||||
final Category category;
|
|
||||||
final GestureTapCallback? onTap;
|
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
|
@override
|
||||||
Widget build(BuildContext context) => _buildContainer();
|
Widget build(BuildContext context) => _buildContainer();
|
||||||
|
|
@ -42,12 +41,12 @@ class CourseCategoryCard extends StatelessWidget {
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildTitle() => Text(
|
Widget _buildTitle() => Text(
|
||||||
category.name ?? '',
|
course['title'],
|
||||||
style: style18DG700,
|
style: style18DG700,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubtitle() => Text(
|
Widget _buildSubtitle() => Text(
|
||||||
ksCategorySubtitle,
|
course['description'],
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
style: style16DG400,
|
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(
|
Widget _buildStack() => Stack(
|
||||||
children: [ _buildPattern(),_buildAppBarWrapper()],
|
children: [_buildPattern(), _buildAppBarWrapper()],
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildAppBarWrapper() => Container(
|
Widget _buildAppBarWrapper() => Container(
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import '../common/app_colors.dart';
|
||||||
import '../common/helper_functions.dart';
|
import '../common/helper_functions.dart';
|
||||||
|
|
||||||
import '../common/ui_helpers.dart';
|
import '../common/ui_helpers.dart';
|
||||||
|
import 'custom_container_shader.dart';
|
||||||
import 'custom_elevated_button.dart';
|
import 'custom_elevated_button.dart';
|
||||||
import 'custom_linear_progress_indicator.dart';
|
import 'custom_linear_progress_indicator.dart';
|
||||||
|
|
||||||
|
|
@ -17,7 +18,8 @@ class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
|
||||||
final GestureTapCallback? onLessonTap;
|
final GestureTapCallback? onLessonTap;
|
||||||
final GestureTapCallback? onPracticeTap;
|
final GestureTapCallback? onPracticeTap;
|
||||||
|
|
||||||
const LearnLessonTile({super.key,
|
const LearnLessonTile(
|
||||||
|
{super.key,
|
||||||
this.onLessonTap,
|
this.onLessonTap,
|
||||||
this.onPracticeTap,
|
this.onPracticeTap,
|
||||||
required this.index,
|
required this.index,
|
||||||
|
|
@ -33,41 +35,44 @@ class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(5),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: kcPrimaryColor.withOpacity(0.1),
|
color: (lesson.access?.isCompleted ?? false)
|
||||||
// color: ProgressStatuses.pending == status
|
? kcGreen.withOpacity(0.1)
|
||||||
// ? kcPrimaryColor.withOpacity(0.1)
|
: kcPrimaryColor.withOpacity(0.1),
|
||||||
// : kcGreen.withOpacity(0.1),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: _buildExpansionTile(viewModel),
|
child: _buildTileStack(viewModel),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildTileStack(LearnLessonViewModel viewModel) => Stack(
|
||||||
|
children: [
|
||||||
|
_buildExpansionTile(viewModel),
|
||||||
|
_buildContainerShaderState()
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildExpansionTile(LearnLessonViewModel viewModel) => ExpansionTile(
|
Widget _buildExpansionTile(LearnLessonViewModel viewModel) => ExpansionTile(
|
||||||
enabled: true,
|
|
||||||
title: _buildTitle(),
|
title: _buildTitle(),
|
||||||
textColor: kcDarkGrey,
|
textColor: kcDarkGrey,
|
||||||
showTrailingIcon: true,
|
showTrailingIcon: true,
|
||||||
initiallyExpanded: false,
|
trailing: _buildIconState(),
|
||||||
trailing: _buildPendingIcon(),
|
|
||||||
collapsedIconColor: kcDarkGrey,
|
collapsedIconColor: kcDarkGrey,
|
||||||
collapsedTextColor: kcDarkGrey,
|
collapsedTextColor: kcDarkGrey,
|
||||||
leading: _buildLeadingWrapper(),
|
leading: _buildLeadingWrapper(),
|
||||||
shape: Border.all(color: kcTransparent),
|
shape: Border.all(color: kcTransparent),
|
||||||
expandedAlignment: Alignment.centerLeft,
|
expandedAlignment: Alignment.centerLeft,
|
||||||
backgroundColor: kcPrimaryColor.withOpacity(0.1),
|
enabled: (lesson.access?.isAccessible ?? false),
|
||||||
controlAffinity: ListTileControlAffinity.trailing,
|
controlAffinity: ListTileControlAffinity.trailing,
|
||||||
expandedCrossAxisAlignment: CrossAxisAlignment.start,
|
expandedCrossAxisAlignment: CrossAxisAlignment.start,
|
||||||
tilePadding: const EdgeInsets.fromLTRB(15, 15, 15, 15),
|
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),
|
childrenPadding: const EdgeInsets.fromLTRB(15, 0, 15, 15),
|
||||||
// enabled: (lesson.access?.isAccessible ?? false),
|
initiallyExpanded: (lesson.access?.isAccessible ?? false) &&
|
||||||
// backgroundColor: ProgressStatuses.pending == status
|
!(lesson.access?.isCompleted ?? false),
|
||||||
// ? kcPrimaryColor.withOpacity(0.1)
|
collapsedBackgroundColor: (lesson.access?.isCompleted ?? false)
|
||||||
// : kcGreen.withOpacity(0.1),
|
? kcGreen.withOpacity(0.1)
|
||||||
// collapsedBackgroundColor: ProgressStatuses.pending == status
|
: kcPrimaryColor.withOpacity(0.1),
|
||||||
// ? kcPrimaryColor.withOpacity(0.1)
|
|
||||||
// : kcGreen.withOpacity(0.1),
|
|
||||||
// initiallyExpanded: status != ProgressStatuses.completed ? true : false,
|
|
||||||
children: _buildExpansionTileChildren(viewModel),
|
children: _buildExpansionTileChildren(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -83,9 +88,9 @@ class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
|
||||||
style: style16DG600,
|
style: style16DG600,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Widget _buildIconState() => ProgressStatuses.pending == status
|
Widget _buildIconState() => (lesson.access?.isCompleted ?? false)
|
||||||
// ? _buildPendingIcon()
|
? _buildCompleteIcon()
|
||||||
// : _buildCompleteIcon();
|
: _buildPendingIcon();
|
||||||
|
|
||||||
Widget _buildCompleteIcon() => const Icon(
|
Widget _buildCompleteIcon() => const Icon(
|
||||||
Icons.check,
|
Icons.check,
|
||||||
|
|
@ -116,14 +121,14 @@ class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
|
||||||
_buildActionButtonWrapper(viewModel)
|
_buildActionButtonWrapper(viewModel)
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildProgress() => const CustomLinearProgressIndicator(
|
Widget _buildProgress() => CustomLinearProgressIndicator(
|
||||||
progress: 0,
|
|
||||||
activeColor: kcPrimaryColor,
|
activeColor: kcPrimaryColor,
|
||||||
backgroundColor: kcVeryLightGrey,
|
backgroundColor: kcVeryLightGrey,
|
||||||
|
progress: (lesson.access?.progressPercent ?? 0) / 100,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildProgressText() => Text(
|
Widget _buildProgressText() => Text(
|
||||||
'In Progress',
|
(lesson.access?.isCompleted ?? false) ? 'Completed' : 'In Progress',
|
||||||
style: style14P600,
|
style: style14P600,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -179,4 +184,10 @@ class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
|
||||||
trailingIcon: Icons.play_arrow,
|
trailingIcon: Icons.play_arrow,
|
||||||
backgroundColor: kcPrimaryColor,
|
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:stacked/stacked.dart';
|
||||||
import 'package:yimaru_app/models/learn_module.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/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/custom_linear_progress_indicator.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/finish_practice_sheet.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> {
|
class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
|
||||||
final LearnModule module;
|
final LearnModule module;
|
||||||
|
final GestureTapCallback? onLockTap;
|
||||||
final GestureTapCallback? onModuleTap;
|
final GestureTapCallback? onModuleTap;
|
||||||
final GestureTapCallback? onPracticeTap;
|
final GestureTapCallback? onPracticeTap;
|
||||||
|
|
||||||
const LearnModuleTile(
|
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(
|
Future<void> _showSheet(
|
||||||
{required BuildContext context,
|
{required BuildContext context,
|
||||||
|
|
@ -28,8 +34,15 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, LearnModuleViewModel viewModel) =>
|
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(
|
Widget _buildExpansionTileCard(
|
||||||
{required BuildContext context,
|
{required BuildContext context,
|
||||||
required LearnModuleViewModel viewModel}) =>
|
required LearnModuleViewModel viewModel}) =>
|
||||||
|
|
@ -48,7 +61,7 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
|
||||||
Stack(
|
Stack(
|
||||||
children: [
|
children: [
|
||||||
_buildExpansionTile(context: context, viewModel: viewModel),
|
_buildExpansionTile(context: context, viewModel: viewModel),
|
||||||
// _buildContainerShaderState()
|
_buildContainerShaderState()
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -57,8 +70,8 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
|
||||||
required LearnModuleViewModel viewModel}) =>
|
required LearnModuleViewModel viewModel}) =>
|
||||||
ExpansionTile(
|
ExpansionTile(
|
||||||
textColor: kcDarkGrey,
|
textColor: kcDarkGrey,
|
||||||
initiallyExpanded: true,
|
|
||||||
subtitle: _buildContent(),
|
subtitle: _buildContent(),
|
||||||
|
trailing: _buildLockIcon(),
|
||||||
title: _buildTitleWrapper(),
|
title: _buildTitleWrapper(),
|
||||||
leading: _buildIconWrapper(),
|
leading: _buildIconWrapper(),
|
||||||
collapsedIconColor: kcDarkGrey,
|
collapsedIconColor: kcDarkGrey,
|
||||||
|
|
@ -72,11 +85,18 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
|
||||||
expandedCrossAxisAlignment: CrossAxisAlignment.start,
|
expandedCrossAxisAlignment: CrossAxisAlignment.start,
|
||||||
tilePadding: const EdgeInsets.symmetric(horizontal: 15),
|
tilePadding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
childrenPadding: const EdgeInsets.fromLTRB(70, 0, 15, 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:
|
children:
|
||||||
_buildExpansionTileChildren(context: context, viewModel: viewModel),
|
_buildExpansionTileChildren(context: context, viewModel: viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Widget _buildLockIcon() => const Icon(
|
||||||
|
Icons.lock_outline_rounded,
|
||||||
|
color: kcLightGrey,
|
||||||
|
);
|
||||||
|
|
||||||
Widget _buildIconWrapper() => CircleAvatar(
|
Widget _buildIconWrapper() => CircleAvatar(
|
||||||
backgroundColor: kcPrimaryColor.withOpacity(0.1),
|
backgroundColor: kcPrimaryColor.withOpacity(0.1),
|
||||||
child: _buildIcon(),
|
child: _buildIcon(),
|
||||||
|
|
@ -144,14 +164,15 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
|
||||||
child: _buildProgressStatus(),
|
child: _buildProgressStatus(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildProgressStatus() => const CustomLinearProgressIndicator(
|
Widget _buildProgressStatus() => CustomLinearProgressIndicator(
|
||||||
progress: 0,
|
|
||||||
activeColor: kcPrimaryColor,
|
activeColor: kcPrimaryColor,
|
||||||
backgroundColor: kcPrimaryColorLight);
|
backgroundColor: kcPrimaryColorLight,
|
||||||
|
progress: (module.access?.progressPercent ?? 0) / 100,
|
||||||
|
);
|
||||||
|
|
||||||
Widget _buildProgress() => const Text(
|
Widget _buildProgress() => Text(
|
||||||
'0/0',
|
'${module.access?.completedCount ?? 0}/${module.access?.totalCount ?? 0}',
|
||||||
style: TextStyle(color: kcDarkGrey),
|
style: style14DG500,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildActionButtonWrapper(
|
Widget _buildActionButtonWrapper(
|
||||||
|
|
@ -207,17 +228,8 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildContainerShaderState() => !(module.access?.isAccessible ?? false)
|
Widget _buildContainerShaderState() => !(module.access?.isAccessible ?? false)
|
||||||
? _buildContainerShaderWrapper()
|
? _buildContainerShader()
|
||||||
: Container();
|
: Container();
|
||||||
|
|
||||||
Widget _buildContainerShaderWrapper() => Positioned.fill(
|
Widget _buildContainerShader() => const CustomContainerShader();
|
||||||
child: _buildContainerShader(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildContainerShader() => Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: kcWhite.withOpacity(0.5),
|
|
||||||
borderRadius: BorderRadius.circular(5),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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';
|
import 'custom_linear_progress_indicator.dart';
|
||||||
|
|
||||||
class ModuleProgress extends StatelessWidget {
|
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
|
@override
|
||||||
Widget build(BuildContext context) => _buildContainer();
|
Widget build(BuildContext context) => _buildContainer();
|
||||||
|
|
@ -36,17 +44,17 @@ class ModuleProgress extends StatelessWidget {
|
||||||
[_buildProgressInfo(), _buildProgress()];
|
[_buildProgressInfo(), _buildProgress()];
|
||||||
|
|
||||||
Widget _buildProgressInfo() => Text(
|
Widget _buildProgressInfo() => Text(
|
||||||
'0% Progress',
|
'$progress% Progress',
|
||||||
style: style16DG400,
|
style: style16DG400,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildProgress() => Text(
|
Widget _buildProgress() => Text(
|
||||||
'0/3',
|
'$completed/$total',
|
||||||
style: style14P400,
|
style: style14P400,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildProgressIndicator() => const CustomLinearProgressIndicator(
|
Widget _buildProgressIndicator() => CustomLinearProgressIndicator(
|
||||||
progress: 0,
|
progress: progress / 100,
|
||||||
activeColor: kcPrimaryColor,
|
activeColor: kcPrimaryColor,
|
||||||
backgroundColor: kcVeryLightGrey,
|
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';
|
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
|
||||||
|
|
||||||
class OverallLearnProgress extends StatelessWidget {
|
class OverallLearnProgress extends StatelessWidget {
|
||||||
|
final int progress;
|
||||||
final Color backgroundColor;
|
final Color backgroundColor;
|
||||||
final Color indicatorBackgroundColor;
|
final Color indicatorBackgroundColor;
|
||||||
const OverallLearnProgress(
|
const OverallLearnProgress(
|
||||||
{super.key,
|
{super.key,
|
||||||
|
required this.progress,
|
||||||
required this.backgroundColor,
|
required this.backgroundColor,
|
||||||
required this.indicatorBackgroundColor});
|
required this.indicatorBackgroundColor});
|
||||||
|
|
||||||
|
|
@ -51,12 +53,12 @@ class OverallLearnProgress extends StatelessWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildProgress() => Text(
|
Widget _buildProgress() => Text(
|
||||||
'0%',
|
'$progress%',
|
||||||
style: style14P400,
|
style: style14P400,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildProgressIndicator() => CustomLinearProgressIndicator(
|
Widget _buildProgressIndicator() => CustomLinearProgressIndicator(
|
||||||
progress: 0.0,
|
progress: progress / 100,
|
||||||
activeColor: kcPrimaryColor,
|
activeColor: kcPrimaryColor,
|
||||||
backgroundColor: indicatorBackgroundColor,
|
backgroundColor: indicatorBackgroundColor,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -873,7 +873,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.15.6"
|
version: "0.15.6"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
name: yimaru_app
|
name: yimaru_app
|
||||||
version: 0.1.14+16
|
version: 0.1.15+17
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
description: A new Flutter project.
|
description: A new Flutter project.
|
||||||
|
|
||||||
|
|
@ -54,6 +54,7 @@ dependencies:
|
||||||
flutter_local_notifications: ^20.1.0
|
flutter_local_notifications: ^20.1.0
|
||||||
internet_connection_checker_plus: ^2.9.1+2
|
internet_connection_checker_plus: ^2.9.1+2
|
||||||
|
|
||||||
|
http: any
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
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/vimeo_service.dart';
|
||||||
import 'package:yimaru_app/services/url_launcher_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/phone_caller_service.dart';
|
||||||
|
import 'package:yimaru_app/services/learn_service.dart';
|
||||||
// @stacked-import
|
// @stacked-import
|
||||||
|
|
||||||
import 'test_helpers.mocks.dart';
|
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<UrlLauncherService>(onMissingStub: OnMissingStub.returnDefault),
|
MockSpec<UrlLauncherService>(onMissingStub: OnMissingStub.returnDefault),
|
||||||
MockSpec<PhoneCallerService>(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
|
// @stacked-mock-spec
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
@ -79,6 +83,9 @@ void registerServices() {
|
||||||
getAndRegisterUrlLauncherService();
|
getAndRegisterUrlLauncherService();
|
||||||
getAndRegisterUrlLauncherService();
|
getAndRegisterUrlLauncherService();
|
||||||
getAndRegisterPhoneCallerService();
|
getAndRegisterPhoneCallerService();
|
||||||
|
getAndRegisterLearnLessonService();
|
||||||
|
getAndRegisterLearnService();
|
||||||
|
getAndRegisterLearnService();
|
||||||
// @stacked-mock-register
|
// @stacked-mock-register
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -261,6 +268,13 @@ MockPhoneCallerService getAndRegisterPhoneCallerService() {
|
||||||
locator.registerSingleton<PhoneCallerService>(service);
|
locator.registerSingleton<PhoneCallerService>(service);
|
||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MockLearnService getAndRegisterLearnService() {
|
||||||
|
_removeRegistrationIfExists<LearnService>();
|
||||||
|
final service = MockLearnService();
|
||||||
|
locator.registerSingleton<LearnService>(service);
|
||||||
|
return service;
|
||||||
|
}
|
||||||
// @stacked-mock-create
|
// @stacked-mock-create
|
||||||
|
|
||||||
void _removeRegistrationIfExists<T extends Object>() {
|
void _removeRegistrationIfExists<T extends Object>() {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import 'package:yimaru_app/app/app.locator.dart';
|
||||||
import '../helpers/test_helpers.dart';
|
import '../helpers/test_helpers.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('CourseCategoryViewModel Tests -', () {
|
group('LearnServiceTest -', () {
|
||||||
setUp(() => registerServices());
|
setUp(() => registerServices());
|
||||||
tearDown(() => locator.reset());
|
tearDown(() => locator.reset());
|
||||||
});
|
});
|
||||||
|
|
@ -4,7 +4,7 @@ import 'package:yimaru_app/app/app.locator.dart';
|
||||||
import '../helpers/test_helpers.dart';
|
import '../helpers/test_helpers.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('CourseSubcategoryViewModel Tests -', () {
|
group('ArifPayViewModel Tests -', () {
|
||||||
setUp(() => registerServices());
|
setUp(() => registerServices());
|
||||||
tearDown(() => locator.reset());
|
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