Compare commits

...

3 Commits

Author SHA1 Message Date
6dc8f81fa7 Merge branch 'release/0.1.15'
- fixx: Apply UAT comments.
2026-05-14 01:18:48 +03:00
610bd2b2fd fix: Apply UAT comments 2026-05-14 01:17:56 +03:00
64cad421e7 Merge tag '0.1.14' into develop
- fix: Apply UAT comments
2026-05-09 01:07:30 +03:00
73 changed files with 2522 additions and 998 deletions

View File

@ -35,7 +35,6 @@ import 'package:yimaru_app/ui/views/learn_lesson_detail/learn_lesson_detail_view
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
import 'package:yimaru_app/ui/views/course_practice/course_practice_view.dart';
import 'package:yimaru_app/ui/views/course_payment/course_payment_view.dart';
import 'package:yimaru_app/ui/views/course_category/course_category_view.dart';
import 'package:yimaru_app/ui/views/failure/failure_view.dart';
import 'package:yimaru_app/ui/views/course_lesson/course_lesson_view.dart';
import 'package:yimaru_app/ui/views/course_lesson_detail/course_lesson_detail_view.dart';
@ -43,7 +42,6 @@ import 'package:yimaru_app/services/notification_service.dart';
import 'package:yimaru_app/ui/views/duolingo/duolingo_view.dart';
import 'package:yimaru_app/services/smart_auth_service.dart';
import 'package:yimaru_app/services/course_service.dart';
import 'package:yimaru_app/ui/views/course_subcategory/course_subcategory_view.dart';
import 'package:yimaru_app/ui/views/course/course_view.dart';
import 'package:yimaru_app/services/audio_player_service.dart';
import 'package:yimaru_app/services/voice_recorder_service.dart';
@ -55,6 +53,11 @@ import 'package:yimaru_app/ui/views/assessment/assessment_view.dart';
import 'package:yimaru_app/services/vimeo_service.dart';
import 'package:yimaru_app/services/url_launcher_service.dart';
import 'package:yimaru_app/services/phone_caller_service.dart';
import 'package:yimaru_app/ui/views/learn_subscription/learn_subscription_view.dart';
import 'package:yimaru_app/ui/views/arif_pay/arif_pay_view.dart';
import 'package:yimaru_app/services/learn_service.dart';
import 'package:yimaru_app/ui/views/course_catalog/course_catalog_view.dart';
import 'package:yimaru_app/ui/views/course_unit/course_unit_view.dart';
// @stacked-import
@StackedApp(
@ -83,17 +86,19 @@ import 'package:yimaru_app/services/phone_caller_service.dart';
MaterialRoute(page: LearnPracticeView),
MaterialRoute(page: CoursePracticeView),
MaterialRoute(page: CoursePaymentView),
MaterialRoute(page: CourseCategoryView),
MaterialRoute(page: FailureView),
MaterialRoute(page: CourseLessonView),
MaterialRoute(page: CourseLessonDetailView),
MaterialRoute(page: DuolingoView),
MaterialRoute(page: CourseSubcategoryView),
MaterialRoute(page: CourseView),
MaterialRoute(page: CoursePracticeQuestionView),
MaterialRoute(page: LearnProgramView),
MaterialRoute(page: LearnCourseView),
MaterialRoute(page: AssessmentView),
MaterialRoute(page: LearnSubscriptionView),
MaterialRoute(page: ArifPayView),
MaterialRoute(page: CourseCatalogView),
MaterialRoute(page: CourseUnitView),
// @stacked-route
],
dependencies: [
@ -118,6 +123,7 @@ import 'package:yimaru_app/services/phone_caller_service.dart';
LazySingleton(classType: VimeoService),
LazySingleton(classType: UrlLauncherService),
LazySingleton(classType: PhoneCallerService),
LazySingleton(classType: LearnService),
// @stacked-service
],
bottomsheets: [

View File

@ -21,6 +21,7 @@ import '../services/google_auth_service.dart';
import '../services/image_downloader_service.dart';
import '../services/image_picker_service.dart';
import '../services/in_app_update_service.dart';
import '../services/learn_service.dart';
import '../services/notification_service.dart';
import '../services/permission_handler_service.dart';
import '../services/phone_caller_service.dart';
@ -61,4 +62,5 @@ Future<void> setupLocator(
locator.registerLazySingleton(() => VimeoService());
locator.registerLazySingleton(() => UrlLauncherService());
locator.registerLazySingleton(() => PhoneCallerService());
locator.registerLazySingleton(() => LearnService());
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View 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);
}

View 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,
};

View 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);
}

View 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,
};

View 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);
}

View 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,
};

View File

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

View File

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

View File

@ -4,8 +4,7 @@ import 'package:yimaru_app/models/learn_practice.dart';
import 'package:yimaru_app/models/learn_program.dart';
import 'package:yimaru_app/models/level.dart';
import 'package:yimaru_app/models/assessment_question.dart';
import 'package:yimaru_app/models/subcategory.dart';
import 'package:yimaru_app/models/category.dart';
import 'package:yimaru_app/models/course_catalog.dart';
import 'package:yimaru_app/models/course_lesson.dart';
import 'package:yimaru_app/models/course_progress.dart';
import 'package:yimaru_app/models/course.dart';
@ -18,10 +17,12 @@ import '../app/app.locator.dart';
import '../models/learn_course.dart';
import '../models/learn_module.dart';
import '../models/learn_question.dart';
import '../models/learn_subscription.dart';
import '../models/lesson.dart';
import '../models/module.dart';
import '../models/assessment.dart';
import '../models/submodule.dart';
import '../models/learn_subscription_request.dart';
import '../ui/common/enmus.dart';
class ApiService {
@ -432,7 +433,7 @@ class ApiService {
}
// Learn learn courses
Future<List<LearnCourse>> getLearnCourse(int id) async {
Future<List<LearnCourse>> getLearnCourses(int id) async {
try {
List<LearnCourse> learnCourses = [];
@ -552,6 +553,32 @@ class ApiService {
}
}
// Complete lesson
Future<Map<String, dynamic>> completeLearnLesson(int id) async {
try {
Response response = await _service.dio.post(
'$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kLessonsUrl/$id/$kCompleteUrl',
);
if (response.statusCode == 200 && response.data['success']) {
return {
'message': 'Lesson completed',
'status': ResponseStatus.success,
};
} else {
return {
'status': ResponseStatus.failure,
'message': 'Unknown Error Occurred'
};
}
} on DioException catch (e) {
return {
'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
};
}
}
// Learn lesson practices
Future<List<LearnPractice>> getLearnLessonPractices(int id) async {
try {
@ -601,49 +628,48 @@ class ApiService {
}
}
/* TO BE MODIFIED*/
// Get categories
Future<List<Category>> getCategories() async {
// Complete lesson
Future<Map<String, dynamic>> completeLearnPractice(int id) async {
try {
List<Category> categories = [];
final Response response = await _service.dio.get(
'$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kCategoryUrl');
Response response = await _service.dio.post(
'$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kProgressUrl/$kPracticesUrl/$id/$kCompleteUrl',
);
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data']['categories'] as List;
categories = decodedData.map(
(e) {
return Category.fromJson(e);
},
).toList();
return categories;
return {
'status': ResponseStatus.success,
'message': 'Lesson completed'
};
} else {
return {
'status': ResponseStatus.failure,
'message': 'Unknown Error Occurred'
};
}
return [];
} catch (e) {
return [];
} on DioException catch (e) {
return {
'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
};
}
}
// Get course subcategory
Future<List<Subcategory>> getSubcategories(int id) async {
Future<List<LearnSubscription>> getLearnSubscriptions() async {
try {
List<Subcategory> subcategories = [];
List<LearnSubscription> subscriptions = [];
final Response response = await _service.dio
.get('$kBaseUrl/$kCourseBaseUrl/$kCategoryUrl/$id/$kCoursesUrl');
.get('$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kSubscriptionsUrl');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data']['courses'] as List;
subcategories = decodedData.map(
var decodedData = data['data'] as List;
subscriptions = decodedData.map(
(e) {
return Subcategory.fromJson(e);
return LearnSubscription.fromJson(e);
},
).toList();
return subcategories;
return subscriptions;
}
return [];
} catch (e) {
@ -651,6 +677,60 @@ class ApiService {
}
}
// Create subscription
Future<Map<String, dynamic>> createSubscriptionRequest(
Map<String, dynamic> data) async {
try {
Response response = await _service.dio.post(
'$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kPaymentsUrl/$kSubscribeUrl',
data: data);
if (response.statusCode == 200) {
return {
'message': 'Lesson completed',
'status': ResponseStatus.success,
'data': LearnSubscriptionRequest.fromJson(response.data['data']),
};
} else {
return {
'status': ResponseStatus.failure,
'message': 'Unknown Error Occurred'
};
}
} on DioException catch (e) {
return {
'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
};
}
}
// Get course catalogs
Future<List<CourseCatalog>> getCourseCatalogs() async {
try {
List<CourseCatalog> catalogs = [];
final Response response = await _service.dio.get(
'$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kExamPrepUrl/$kCatalogCoursesUrl');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data']['catalog_courses'] as List;
catalogs = decodedData.map(
(e) {
return CourseCatalog.fromJson(e);
},
).toList();
return catalogs;
}
return [];
} catch (e) {
return [];
}
}
/* TO BE MODIFIED*/
// Get courses
// Future<List<Course>> getCourses(int id) async {
// try {
@ -727,7 +807,7 @@ class ApiService {
Future<Map<String, dynamic>> completeLesson(int id) async {
try {
Response response = await _service.dio.post(
'$kBaseUrl/$kLessonProgressUrl/$id/$kCompleteLessonUrl',
'$kBaseUrl/$kLessonProgressUrl/$id/$kCompleteUrl',
);
if (response.statusCode == 200) {
@ -813,9 +893,9 @@ class ApiService {
}
// Get learn subcategories
Future<List<Subcategory>> getLearnSubcategories() async {
Future<List<CourseCatalog>> getLearnSubcategories() async {
try {
List<Subcategory> learnSubcategories = [];
List<CourseCatalog> learnSubcategories = [];
final Response response = await _service.dio.get(
'$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kLearnSubcategoriesUrl');
@ -825,7 +905,7 @@ class ApiService {
var decodedData = data['data']['sub_categories'] as List;
learnSubcategories = decodedData.map(
(e) {
return Subcategory.fromJson(e);
return CourseCatalog.fromJson(e);
},
).toList();
return learnSubcategories;

View 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();
}
}

View File

@ -130,7 +130,7 @@ class NotificationService {
}
Future<void> updateFCMToken() async {
// print('DEVICE TOKEN: ${await _messaging.getToken()}');
print('DEVICE TOKEN: ${await _messaging.getToken()}');
_messaging.onTokenRefresh.listen((newToken) {
// updateTokenOnServer(newToken);
});

View File

@ -17,11 +17,19 @@ String kProgramsUrl = 'programs';
String kRegisterUrl = 'register';
String kProgressUrl = 'progress';
String kCompleteUrl = 'complete';
String kPaymentsUrl = 'payments';
String kSubscribeUrl = 'subscribe';
String kPracticesUrl = 'practices';
String kQuestionsUrl = 'questions';
String kCategoryUrl = 'categories';
String kExamPrepUrl = 'exam-prep';
String kCoursePractice = 'by-owner';
@ -37,8 +45,6 @@ String kSubmodulesUrl = 'sub-modules';
String kSubcoursesUrl = 'sub-courses';
String kCompleteLessonUrl = 'complete';
String kResetPassword = 'resetPassword';
String kQuestionSetsUrl = 'question-sets';
@ -51,8 +57,12 @@ String kPublishedVideos = 'videos/published';
String kCoursePracticeQuestions = 'questions';
String kCatalogCoursesUrl = 'catalog-courses';
String kUpdateProfileImage = 'profile-picture';
String kSubscriptionsUrl = 'subscription-plans';
String kRefreshTokenUrl = 'api/v1/auth/refresh';
String kLoginUrl = 'api/v1/auth/customer-login';
@ -89,3 +99,10 @@ String kServerClientId =
String kPhoneSupport = '+251946396655';
String kTelegramSupport = '@yimaruacademy2026';
String kTelegramSupportLink = 'https://t.me/yimaruacademy2026';
String kErrorUrl = 'https://yimaru.net/api/v1/payments/arifpay/error';
String kSuccessUrl =
'https://api.yimaruacademy.com/api/v1/payments/arifpay/success';

View File

@ -42,23 +42,25 @@ enum StateObjects {
courseLessons,
profileUpdate,
resetPassword,
subcategories,
learnPractice,
courseCatalogs,
loginWithEmail,
coursePractice,
learnPractices,
loginWithGoogle,
loadLessonVideo,
loadCourseVideo,
learnSubmodules,
requestResetCode,
courseCategories,
profileCompletion,
learnSubscription,
learnSubscriptions,
registerWithGoogle,
learnPracticeSample,
learnPracticeAnswer,
loginWithPhoneNumber,
assessmentQuestions,
learnPracticeQuestion,
completeLearnPractice,
coursePracticeQuestion,
coursePracticeQuestions,
recordLearnPracticeAnswer,

View File

@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:math';
import 'dart:ui';

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

View 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'];
// }
// }
// }
}

View File

@ -2,25 +2,19 @@ import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import '../../../models/course_detail.dart';
import '../../../models/subcategory.dart';
import '../../../models/course_catalog.dart';
import '../../common/app_colors.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/course_category_card.dart';
import '../../widgets/course_tile.dart';
import '../../widgets/custom_circular_progress_indicator.dart';
import '../../widgets/profile_app_bar.dart';
import '../../widgets/small_app_bar.dart';
import 'course_viewmodel.dart';
class CourseView extends StackedView<CourseViewModel> {
final Subcategory subcategory;
const CourseView({Key? key, required this.subcategory}) : super(key: key);
@override
void onViewModelReady(CourseViewModel viewModel) async {
await viewModel.getCourseDetails(subcategory.id ?? 0);
super.onViewModelReady(viewModel);
}
const CourseView({Key? key}) : super(key: key);
@override
CourseViewModel viewModelBuilder(BuildContext context) => CourseViewModel();
@ -51,78 +45,63 @@ class CourseView extends StackedView<CourseViewModel> {
verticalSpaceMedium,
_buildAppBar(viewModel),
verticalSpaceMedium,
_buildCoursesColumnWrapper(viewModel),
_buildCategoryColumnWrapper(viewModel)
],
);
Widget _buildAppBar(CourseViewModel viewModel) => SmallAppBar(
onPop: viewModel.pop,
showBackButton: true,
Widget _buildAppBar(CourseViewModel viewModel) => ProfileAppBar(
name: viewModel.user?.firstName,
profileImage: viewModel.user?.profilePicture,
);
Widget _buildCoursesColumnWrapper(CourseViewModel viewModel) =>
Expanded(child: _buildCoursesColumnScrollView(viewModel));
Widget _buildCategoryColumnWrapper(CourseViewModel viewModel) =>
Expanded(child: _buildCourseColumnScrollView(viewModel));
Widget _buildCoursesColumnScrollView(CourseViewModel viewModel) =>
Widget _buildCourseColumnScrollView(CourseViewModel viewModel) =>
SingleChildScrollView(
child: _buildCoursesColumn(viewModel),
child: _buildCourseColumn(viewModel),
);
Widget _buildCoursesColumn(CourseViewModel viewModel) => Column(
Widget _buildCourseColumn(CourseViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildCoursesColumnChildren(viewModel),
children: _buildLevelsColumnChildren(viewModel),
);
List<Widget> _buildCoursesColumnChildren(CourseViewModel viewModel) => [
verticalSpaceMedium,
List<Widget> _buildLevelsColumnChildren(CourseViewModel viewModel) => [
_buildTitle(),
_buildSubtitle(),
verticalSpaceMedium,
_buildListViewBuilder(viewModel)
_buildListView(viewModel)
];
Widget _buildTitle() => Text(
'${subcategory.name ?? ''} courses',
'Courses',
style: style18DG700,
);
Widget _buildSubtitle() => Text(
'Explore variety of courses on ${subcategory.name ?? ''}.',
'Choose a course to improve your professional or exam skills.',
style: style14DG400,
);
Widget _buildListViewBuilder(CourseViewModel viewModel) =>
viewModel.busy(StateObjects.courses)
? _buildProgressIndicator()
: _buildListView(viewModel);
Widget _buildProgressIndicator() => const Center(
child: CustomCircularProgressIndicator(color: kcPrimaryColor),
);
Widget _buildListView(CourseViewModel viewModel) => ListView.separated(
shrinkWrap: true,
itemCount: viewModel.courseDetail.length,
itemCount: viewModel.courses.length,
physics: const NeverScrollableScrollPhysics(),
separatorBuilder: (context, index) => verticalSpaceSmall,
itemBuilder: (context, index) => _buildTile(
courseDetail: viewModel.courseDetail[index],
onCourseTap: () async => await viewModel
.navigateToCoursePayment(viewModel.courseDetail[index].course!),
onPracticeTap: () async => await viewModel.navigateToCoursePractice(
viewModel.courseDetail[index].course?.id ?? 0),
),
course: viewModel.courses[index],
onTap: () async => await viewModel.navigateToCourseCatalog()),
separatorBuilder: (context, index) => verticalSpaceSmall,
);
//
Widget _buildTile({
GestureTapCallback? onCourseTap,
GestureTapCallback? onPracticeTap,
required CourseDetail courseDetail,
required GestureTapCallback onTap,
required Map<String, dynamic> course,
}) =>
CourseTile(
onCourseTap: onCourseTap,
courseDetail: courseDetail,
onPracticeTap: onPracticeTap,
CourseCard(
onTap: onTap,
course: course,
);
}

View File

@ -5,44 +5,47 @@ import '../../../app/app.locator.dart';
import '../../../app/app.router.dart';
import '../../../models/course.dart';
import '../../../models/course_detail.dart';
import '../../../models/user.dart';
import '../../../services/authentication_service.dart';
import '../../../services/course_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
class CourseViewModel extends BaseViewModel {
class CourseViewModel extends ReactiveViewModel {
// Dependency injection
final _courseService = locator<CourseService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
// Subcourse with progress
List<CourseDetail> _courseDetail = [];
final _authenticationService = locator<AuthenticationService>();
List<CourseDetail> get courseDetail => _courseDetail;
@override
List<ListenableServiceMixin> get listenableServices =>
[_authenticationService];
// Current user
User? get _user => _authenticationService.user;
User? get user => _user;
// Course
final List<Map<String, dynamic>> _courses = [
{
'title': 'English Proficiency Exams',
'description':
'Prepare for IELTS, TOEFL, or Duolingo with structured practice.'
},
{
'title': 'Skill-Based Courses',
'description':
'Learn English for the workplace, travel, and real-life communication.'
},
];
List<Map<String, dynamic>> get courses => _courses;
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToCoursePractice(int id) =>
_navigationService.navigateToCoursePracticeView(id: id);
Future<void> navigateToCoursePayment(Course course) async =>
_navigationService.navigateToCoursePaymentView(course: course);
// Remote api call
// Course detail
Future<void> getCourseDetails(int id) async =>
await runBusyFuture(_getCourseDetails(id),
busyObject: StateObjects.courses);
Future<void> _getCourseDetails(int id) async {
if (await _statusChecker.checkConnection()) {
_courseDetail = await _courseService.getCoursesDetail(id);
// _courseDetail.sort((a, b) =>
// (a.course?.displayOrder ?? 0).compareTo(b.course?.displayOrder ?? 0));
}
}
Future<void> navigateToCourseCatalog() async =>
await _navigationService.navigateToCourseCatalogView();
}

View File

@ -1,55 +1,50 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/models/course_catalog.dart';
import '../../../models/category.dart';
import '../../../models/subcategory.dart';
import '../../common/app_colors.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/course_subcategory_tile.dart';
import '../../widgets/course_catalog_tile.dart';
import '../../widgets/custom_circular_progress_indicator.dart';
import '../../widgets/small_app_bar.dart';
import 'course_subcategory_viewmodel.dart';
import 'course_catalog_viewmodel.dart';
class CourseSubcategoryView extends StackedView<CourseSubcategoryViewModel> {
final Category category;
const CourseSubcategoryView({Key? key, required this.category})
: super(key: key);
class CourseCatalogView extends StackedView<CourseCatalogViewModel> {
const CourseCatalogView({Key? key}) : super(key: key);
@override
void onViewModelReady(CourseSubcategoryViewModel viewModel) async {
await viewModel.getSubcategories(category.id ?? 0);
void onViewModelReady(CourseCatalogViewModel viewModel) async {
await viewModel.getCourseCatalogs();
super.onViewModelReady(viewModel);
}
@override
CourseSubcategoryViewModel viewModelBuilder(BuildContext context) =>
CourseSubcategoryViewModel();
CourseCatalogViewModel viewModelBuilder(BuildContext context) =>
CourseCatalogViewModel();
@override
Widget builder(
BuildContext context,
CourseSubcategoryViewModel viewModel,
CourseCatalogViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(CourseSubcategoryViewModel viewModel) =>
Scaffold(
Widget _buildScaffoldWrapper(CourseCatalogViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(CourseSubcategoryViewModel viewModel) =>
Widget _buildScaffold(CourseCatalogViewModel viewModel) =>
SafeArea(child: _buildBody(viewModel));
Widget _buildBody(CourseSubcategoryViewModel viewModel) => Padding(
Widget _buildBody(CourseCatalogViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel),
);
Widget _buildColumn(CourseSubcategoryViewModel viewModel) => Column(
Widget _buildColumn(CourseCatalogViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(viewModel),
@ -58,27 +53,26 @@ class CourseSubcategoryView extends StackedView<CourseSubcategoryViewModel> {
],
);
Widget _buildAppBar(CourseSubcategoryViewModel viewModel) => SmallAppBar(
Widget _buildAppBar(CourseCatalogViewModel viewModel) => SmallAppBar(
onPop: viewModel.pop,
showBackButton: true,
);
Widget _buildCoursesColumnWrapper(CourseSubcategoryViewModel viewModel) =>
Widget _buildCoursesColumnWrapper(CourseCatalogViewModel viewModel) =>
Expanded(child: _buildCoursesColumnScrollView(viewModel));
Widget _buildCoursesColumnScrollView(CourseSubcategoryViewModel viewModel) =>
Widget _buildCoursesColumnScrollView(CourseCatalogViewModel viewModel) =>
SingleChildScrollView(
child: _buildCoursesColumn(viewModel),
);
Widget _buildCoursesColumn(CourseSubcategoryViewModel viewModel) => Column(
Widget _buildCoursesColumn(CourseCatalogViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildCoursesColumnChildren(viewModel),
);
List<Widget> _buildCoursesColumnChildren(
CourseSubcategoryViewModel viewModel) =>
List<Widget> _buildCoursesColumnChildren(CourseCatalogViewModel viewModel) =>
[
_buildTitle(),
_buildSubtitle(),
@ -87,17 +81,17 @@ class CourseSubcategoryView extends StackedView<CourseSubcategoryViewModel> {
];
Widget _buildTitle() => Text(
'${category.name ?? ''} courses',
'English Proficiency Exams',
style: style18DG700,
);
Widget _buildSubtitle() => Text(
'Explore variety of courses on this category.',
'Select your target exam and start preparing',
style: style14DG400,
);
Widget _buildListViewBuilder(CourseSubcategoryViewModel viewModel) =>
viewModel.busy(StateObjects.subcategories)
Widget _buildListViewBuilder(CourseCatalogViewModel viewModel) =>
viewModel.busy(StateObjects.courseCatalogs)
? _buildProgressIndicator()
: _buildListView(viewModel);
@ -105,27 +99,25 @@ class CourseSubcategoryView extends StackedView<CourseSubcategoryViewModel> {
child: CustomCircularProgressIndicator(color: kcPrimaryColor),
);
Widget _buildListView(CourseSubcategoryViewModel viewModel) =>
ListView.separated(
Widget _buildListView(CourseCatalogViewModel viewModel) => ListView.separated(
shrinkWrap: true,
itemCount: viewModel.subcategories.length,
itemCount: viewModel.courseCatalogs.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildTile(
subcategory: viewModel.subcategories[index],
onCourseTap: () async => await viewModel
.navigateToSubcourse(viewModel.subcategories[index]),
),
courseCatalog: viewModel.courseCatalogs[index],
onCourseTap: () {},
onPracticeTap: () {}),
separatorBuilder: (context, index) => verticalSpaceSmall,
);
Widget _buildTile({
GestureTapCallback? onCourseTap,
GestureTapCallback? onPracticeTap,
required Subcategory subcategory,
required CourseCatalog courseCatalog,
required GestureTapCallback onCourseTap,
required GestureTapCallback onPracticeTap,
}) =>
CourseSubcategoryTile(
subcategory: subcategory,
CourseCatalogTile(
onCourseTap: onCourseTap,
onPracticeTap: onPracticeTap,
courseCatalog: courseCatalog,
);
}

View File

@ -1,14 +1,14 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/models/course_catalog.dart';
import '../../../app/app.locator.dart';
import '../../../app/app.router.dart';
import '../../../models/subcategory.dart';
import '../../../services/api_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
class CourseSubcategoryViewModel extends BaseViewModel {
class CourseCatalogViewModel extends BaseViewModel {
// Dependency injection
final _apiService = locator<ApiService>();
@ -16,10 +16,10 @@ class CourseSubcategoryViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>();
// Course subcategories
List<Subcategory> _subcategories = [];
// Course catalogs
List<CourseCatalog> _courseCatalogs = [];
List<Subcategory> get subcategories => _subcategories;
List<CourseCatalog> get courseCatalogs => _courseCatalogs;
// Navigation
void pop() => _navigationService.back();
@ -27,19 +27,19 @@ class CourseSubcategoryViewModel extends BaseViewModel {
Future<void> navigateToCoursePractice(int id) async =>
_navigationService.navigateToCoursePracticeView(id: id);
Future<void> navigateToSubcourse(Subcategory subcategory) async =>
_navigationService.navigateToCourseView(subcategory: subcategory);
// Future<void> navigateToSubcourse(Subcategory subcategory) async =>
// _navigationService.navigateToCourseView(subcategory: subcategory);
// Remote api call
// Course subcategories
Future<void> getSubcategories(int id) async =>
await runBusyFuture(_getSubcategories(id),
busyObject: StateObjects.subcategories);
// Course catalogs
Future<void> getCourseCatalogs() async =>
await runBusyFuture(_getSubcategories(),
busyObject: StateObjects.courseCatalogs);
Future<void> _getSubcategories(int id) async {
Future<void> _getSubcategories() async {
if (await _statusChecker.checkConnection()) {
_subcategories = await _apiService.getSubcategories(id);
_courseCatalogs = await _apiService.getCourseCatalogs();
}
}
}

View File

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

View File

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

View 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();
}

View File

@ -0,0 +1,3 @@
import 'package:stacked/stacked.dart';
class CourseUnitViewModel extends BaseViewModel {}

View File

@ -110,8 +110,6 @@ class ForgetPasswordViewModel extends FormViewModel {
_length = false;
}
if (password == confirmPassword) {
_passwordMatch = true;
} else {

View File

@ -5,6 +5,7 @@ import 'package:yimaru_app/ui/views/learn_program/learn_program_view.dart';
import 'package:yimaru_app/ui/views/profile/profile_view.dart';
import 'package:yimaru_app/ui/widgets/coming_soon.dart';
import '../course/course_view.dart';
import 'home_viewmodel.dart';
class HomeView extends StackedView<HomeViewModel> {
@ -69,7 +70,7 @@ class HomeView extends StackedView<HomeViewModel> {
case 0:
return const LearnProgramView();
case 1:
return const ComingSoon();
return const CourseView();
default:
return const ProfileView();

View File

@ -78,15 +78,15 @@ class LearnCourseView extends StackedView<LearnCourseViewModel> {
Widget _buildListView(LearnCourseViewModel viewModel) => ListView.separated(
shrinkWrap: true,
itemCount: viewModel.learnCourses.length,
itemCount: viewModel.courses.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildTile(
course: viewModel.learnCourses[index],
onViewTap: () async => await viewModel
.navigateToLearnModule(viewModel.learnCourses[index]),
course: viewModel.courses[index],
onViewTap: () async =>
await viewModel.navigateToLearnModule(viewModel.courses[index]),
onPracticeTap: () async => await viewModel.navigateToLearnPractice(
id: viewModel.learnCourses[index].id ?? 0,
level: viewModel.learnCourses[index].name ?? ''),
id: viewModel.courses[index].id ?? 0,
level: viewModel.courses[index].name ?? ''),
),
separatorBuilder: (context, index) => verticalSpaceSmall,
);

View File

@ -5,21 +5,25 @@ import 'package:yimaru_app/app/app.router.dart';
import '../../../app/app.locator.dart';
import '../../../models/learn_course.dart';
import '../../../services/api_service.dart';
import '../../../services/learn_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
class LearnCourseViewModel extends BaseViewModel {
class LearnCourseViewModel extends ReactiveViewModel {
// Dependency injection
final _apiService = locator<ApiService>();
final _learnService = locator<LearnService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
// Learn courses
List<LearnCourse> _learnCourses = [];
@override
List<ListenableServiceMixin> get listenableServices => [_learnService];
List<LearnCourse> get learnCourses => _learnCourses;
// Learn lessons
List<LearnCourse> get _courses => _learnService.courses;
List<LearnCourse> get courses => _courses;
// Navigation
void pop() => _navigationService.back();
@ -46,12 +50,8 @@ class LearnCourseViewModel extends BaseViewModel {
busyObject: StateObjects.learnCourses);
Future<void> _getLearnCourses(int id) async {
if (_learnCourses.isEmpty) {
if (await _statusChecker.checkConnection()) {
_learnCourses = await _apiService.getLearnCourse(id);
_learnCourses
.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
}
await _learnService.getLearnCourses(id);
}
}
}

View File

@ -133,7 +133,11 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
style: style14DG500,
);
Widget _buildModuleProgress() => const ModuleProgress();
Widget _buildModuleProgress() => ModuleProgress(
total: module.access?.totalCount ?? 0,
completed: module.access?.completedCount ?? 0,
progress: module.access?.progressPercent ?? 0,
);
Widget _buildMotivationCard() => const MotivationCard();
@ -159,6 +163,7 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
index: index,
lesson: viewModel.lessons[index],
onLessonTap: () async => await viewModel.navigateToLearnLessonDetail(
module: module,
lesson: viewModel.lessons[index],
hasPractice:
index != viewModel.lessons.length - 1 ? true : false),

View File

@ -5,19 +5,23 @@ import 'package:yimaru_app/models/learn_lesson.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import '../../../app/app.locator.dart';
import '../../../services/api_service.dart';
import '../../../models/learn_module.dart';
import '../../../services/learn_service.dart';
import '../../../services/status_checker_service.dart';
class LearnLessonViewModel extends BaseViewModel {
class LearnLessonViewModel extends ReactiveViewModel {
// Dependency injection
final _apiService = locator<ApiService>();
final _learnService = locator<LearnService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
@override
List<ListenableServiceMixin> get listenableServices => [_learnService];
// Learn lessons
List<LearnLesson> _lessons = [];
List<LearnLesson> get _lessons => _learnService.lessons;
List<LearnLesson> get lessons => _lessons;
@ -35,9 +39,11 @@ class LearnLessonViewModel extends BaseViewModel {
);
Future<void> navigateToLearnLessonDetail(
{required bool hasPractice, required LearnLesson lesson}) async =>
{required bool hasPractice,
required LearnLesson lesson,
required LearnModule module}) async =>
await _navigationService.navigateToLearnLessonDetailView(
lesson: lesson, hasPractice: hasPractice);
lesson: lesson, module: module, hasPractice: hasPractice);
// Remote api call
@ -46,11 +52,8 @@ class LearnLessonViewModel extends BaseViewModel {
busyObject: StateObjects.learnLessons);
Future<void> _getLessons(int id) async {
if (_lessons.isEmpty) {
if (await _statusChecker.checkConnection()) {
_lessons = await _apiService.getLearnLessons(id);
_lessons.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
}
await _learnService.getLearnLessons(id);
}
}
}

View File

@ -1,8 +1,8 @@
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:vimeo_video_player/vimeo_video_player.dart';
import 'package:yimaru_app/models/learn_lesson.dart';
import 'package:yimaru_app/models/learn_module.dart';
import '../../common/app_colors.dart';
import '../../common/enmus.dart';
@ -14,10 +14,14 @@ import 'learn_lesson_detail_viewmodel.dart';
class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
final bool hasPractice;
final LearnModule module;
final LearnLesson lesson;
const LearnLessonDetailView(
{Key? key, required this.lesson, required this.hasPractice})
{Key? key,
required this.lesson,
required this.module,
required this.hasPractice})
: super(key: key);
Future<void> _navigate(LearnLessonDetailViewModel viewModel) async {
@ -27,7 +31,11 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
@override
void onViewModelReady(LearnLessonDetailViewModel viewModel) async {
await viewModel.initializePlayer(lesson.videoUrl ?? '');
await viewModel.initializePlayer(
lessonId: lesson.id ?? 0,
moduleId: module.id ?? 0,
url: lesson.videoUrl ?? '',
);
super.onViewModelReady(viewModel);
}
@ -144,8 +152,9 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
Widget _buildVideoPlayer(LearnLessonDetailViewModel viewModel) =>
_buildChewiePlayer(viewModel);
Widget _buildChewiePlayer(LearnLessonDetailViewModel viewModel) =>
Chewie(controller: viewModel.chewieController!);
Widget _buildChewiePlayer(LearnLessonDetailViewModel viewModel) => Chewie(
controller: viewModel.chewieController!,
);
Widget _buildEmptyVideoPlayer() => const EmptyVideoPlayer();

View File

@ -1,5 +1,4 @@
import 'package:chewie/chewie.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:video_player/video_player.dart';
@ -7,19 +6,37 @@ import 'package:yimaru_app/ui/common/enmus.dart';
import '../../../app/app.locator.dart';
import '../../../app/app.router.dart';
import '../../../models/learn_lesson.dart';
import '../../../services/api_service.dart';
import '../../../services/learn_service.dart';
import '../../../services/status_checker_service.dart';
import '../../../services/vimeo_service.dart';
import '../../common/app_constants.dart';
import '../../common/helper_functions.dart';
import '../../common/ui_helpers.dart';
class LearnLessonDetailViewModel extends BaseViewModel {
class LearnLessonDetailViewModel extends ReactiveViewModel {
// Dependency injection
final _apiService = locator<ApiService>();
final _learnService = locator<LearnService>();
final _vimeoService = locator<VimeoService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
@override
List<ListenableServiceMixin> get listenableServices => [_learnService];
// Learn lessons
List<LearnLesson> get _lessons => _learnService.lessons;
List<LearnLesson> get lessons => _lessons;
// Video player config
bool _lessonCompleted = false;
ChewieController? _chewieController;
ChewieController? get chewieController => _chewieController;
@ -38,11 +55,18 @@ class LearnLessonDetailViewModel extends BaseViewModel {
await _chewieController?.pause();
}
Future<void> initializePlayer(String url) async =>
await runBusyFuture(_initializePlayer(url),
Future<void> initializePlayer(
{required String url,
required int lessonId,
required int moduleId}) async =>
await runBusyFuture(
_initializePlayer(url: url, moduleId: moduleId, lessonId: lessonId),
busyObject: StateObjects.loadLessonVideo);
Future<void> _initializePlayer(String url) async {
Future<void> _initializePlayer(
{required String url,
required int lessonId,
required int moduleId}) async {
final playableUrl = await _vimeoService.getVideoUrl(url);
if (playableUrl == null) {
@ -54,11 +78,41 @@ class LearnLessonDetailViewModel extends BaseViewModel {
await _videoPlayerController?.initialize();
// Listen for video completion
_videoPlayerController?.addListener(() async {
final controller = _videoPlayerController;
if (controller == null || _lessonCompleted) return;
final position = controller.value.position.inSeconds;
final duration = controller.value.duration.inSeconds;
if (duration <= 0) return;
// Calculate watched percentage
final progress = position / duration;
// Mark complete at 95%
if (progress >= 0.95) {
_lessonCompleted = true;
await completeLearnLesson(
lessonId: lessonId,
moduleId: moduleId,
);
}
});
_chewieController = ChewieController(
videoPlayerController: _videoPlayerController!,
looping: false,
autoPlay: true,
looping: true,
showOptions: true,
showControls: true,
aspectRatio: 16 / 9,
autoInitialize: true,
allowedScreenSleep: false,
videoPlayerController: _videoPlayerController!,
materialProgressColors: buildChewieProgressIndicator,
);
notifyListeners();
@ -76,4 +130,24 @@ class LearnLessonDetailViewModel extends BaseViewModel {
subtitle:
'Ill 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);
}
}
}
}

View File

@ -98,6 +98,7 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
Widget _buildOverallProgress() => OverallLearnProgress(
indicatorBackgroundColor: kcWhite,
progress: course.access?.progressPercent ?? 0,
backgroundColor: kcPrimaryColor.withOpacity(0.1),
);
@ -116,6 +117,7 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildTile(
module: viewModel.modules[index],
onLockTap: () async => await viewModel.navigateToLearnSubscription(),
onPracticeTap: () async => await viewModel.navigateToLearnPractice(
id: viewModel.modules[index].id ?? 0,
module: viewModel.modules[index].name ?? ''),
@ -126,11 +128,13 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
Widget _buildTile({
required LearnModule module,
required GestureTapCallback onLockTap,
required GestureTapCallback onModuleTap,
required GestureTapCallback onPracticeTap,
}) =>
LearnModuleTile(
module: module,
onLockTap: onLockTap,
onModuleTap: onModuleTap,
onPracticeTap: onPracticeTap);
}

View File

@ -5,25 +5,32 @@ import 'package:yimaru_app/models/learn_module.dart';
import '../../../app/app.locator.dart';
import '../../../services/api_service.dart';
import '../../../services/learn_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
class LearnModuleViewModel extends BaseViewModel {
class LearnModuleViewModel extends ReactiveViewModel {
// Dependency injection
final _apiService = locator<ApiService>();
final _learnService = locator<LearnService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
@override
List<ListenableServiceMixin> get listenableServices => [_learnService];
// Learn module
List<LearnModule> _modules = [];
List<LearnModule> get _modules => _learnService.modules;
List<LearnModule> get modules => _modules;
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToLearnSubscription() async =>
await _navigationService.navigateToLearnSubscriptionView();
Future<void> navigateToLearnLesson(LearnModule module) async =>
await _navigationService.navigateToLearnLessonView(module: module);
@ -45,11 +52,8 @@ class LearnModuleViewModel extends BaseViewModel {
busyObject: StateObjects.learnModules);
Future<void> _getLearnModules(int id) async {
if (_modules.isEmpty) {
if (await _statusChecker.checkConnection()) {
_modules = await _apiService.getLearnModules(id);
_modules.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
}
await _learnService.getLearnModules(id);
}
}
}

View File

@ -93,7 +93,7 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
);
Widget _buildBodyState(LearnPracticeViewModel viewModel) =>
viewModel.busy(StateObjects.learnPractices)
viewModel.busy(StateObjects.learnPractice)
? const PageLoadingIndicator()
: viewModel.practices.isEmpty || viewModel.questions.isEmpty
? _buildPageLoadingIndicator(viewModel)
@ -101,7 +101,7 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
Widget _buildPageLoadingIndicator(LearnPracticeViewModel viewModel) =>
LearnLoadingScreen(
isLoading: viewModel.busy(StateObjects.learnPractices),
isLoading: viewModel.busy(StateObjects.learnPractice),
onTap: () async =>
await viewModel.getLearnPractices(id: id, practice: practice),
onPop: viewModel.practices.isEmpty || viewModel.questions.isEmpty
@ -142,6 +142,7 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
Widget _buildLearnPracticeResultScreen() =>
LearnPracticeResultScreen(practice: practice);
Widget _buildLearnPracticeCompletionScreen() =>
LearnPracticeCompletionScreen(level: level ?? '');
Widget _buildLearnPracticeCompletionScreen() => LearnPracticeCompletionScreen(
level: level ?? '',
);
}

View File

@ -13,13 +13,17 @@ import '../../../app/app.locator.dart';
import '../../../models/learn_question.dart';
import '../../../services/api_service.dart';
import '../../../services/audio_player_service.dart';
import '../../../services/learn_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/app_colors.dart';
class LearnPracticeViewModel extends ReactiveViewModel {
// Dependency injection
final _apiService = locator<ApiService>();
final _learnService = locator<LearnService>();
final _dialogService = locator<DialogService>();
final _statusChecker = locator<StatusCheckerService>();
@ -37,8 +41,12 @@ class LearnPracticeViewModel extends ReactiveViewModel {
}
@override
List<ListenableServiceMixin> get listenableServices =>
[_audioPlayerService, _voiceRecorderService, _authenticationService];
List<ListenableServiceMixin> get listenableServices => [
_learnService,
_audioPlayerService,
_voiceRecorderService,
_authenticationService
];
// User
User? get _user => _authenticationService.user;
@ -256,7 +264,7 @@ class LearnPracticeViewModel extends ReactiveViewModel {
Future<void> getLearnPractices(
{required int id, required LearnPractices practice}) async =>
await runBusyFuture(_getLearnPractices(id: id, practice: practice),
busyObject: StateObjects.learnPractices);
busyObject: StateObjects.learnPractice);
Future<void> _getLearnPractices(
{required int id, required LearnPractices practice}) async {
@ -279,4 +287,15 @@ class LearnPracticeViewModel extends ReactiveViewModel {
Future<void> _getLearnPracticeQuestions(int id) async {
_questions = await _apiService.getLearnQuestions(id);
}
// Complete practice
Future<void> completeLearnPractices() async =>
await runBusyFuture(_completeLearnPractices(),
busyObject: StateObjects.completeLearnPractice);
Future<void> _completeLearnPractices() async {
if (await _statusChecker.checkConnection()) {
await _apiService.completeLearnPractice(_practices.first.id ?? 0);
}
}
}

View File

@ -3,9 +3,12 @@ import 'package:flutter_svg/svg.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
import '../../../../models/learn_practice.dart';
import '../../../common/app_colors.dart';
import '../../../common/enmus.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/page_loading_indicator.dart';
class LearnPracticeCompletionScreen
extends ViewModelWidget<LearnPracticeViewModel> {

View File

@ -9,7 +9,6 @@ import '../../../common/ui_helpers.dart';
import '../../../widgets/cancel_learn_practice_sheet.dart';
import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/small_app_bar.dart';
import '../../../widgets/speaking_partner_image.dart';
class LearnPracticeDescriptionScreen
extends ViewModelWidget<LearnPracticeViewModel> {
@ -121,6 +120,7 @@ class LearnPracticeDescriptionScreen
_buildSubtitle(viewModel),
verticalSpaceMedium,
_buildImageContainer(viewModel),
verticalSpaceLarge
];
Widget _buildTitle(LearnPracticeViewModel viewModel) => Text.rich(

View File

@ -9,6 +9,7 @@ import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/cancel_learn_practice_sheet.dart';
import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/page_loading_indicator.dart';
import '../../../widgets/small_app_bar.dart';
class LearnPracticeResultScreen
@ -18,6 +19,7 @@ class LearnPracticeResultScreen
const LearnPracticeResultScreen({super.key, required this.practice});
Future<void> _navigate(LearnPracticeViewModel viewModel) async {
await viewModel.completeLearnPractices();
if (practice == LearnPractices.course) {
viewModel.goTo(5);
} else {
@ -53,9 +55,16 @@ class LearnPracticeResultScreen
required LearnPracticeViewModel viewModel}) =>
Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(context: context, viewModel: viewModel),
body: _buildScaffoldState(context: context, viewModel: viewModel),
);
Widget _buildScaffoldState(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
viewModel.busy(StateObjects.completeLearnPractice)
? const PageLoadingIndicator()
: _buildScaffold(context: context, viewModel: viewModel);
Widget _buildScaffold(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>

View File

@ -7,12 +7,14 @@ import '../../../app/app.locator.dart';
import '../../../models/user.dart';
import '../../../services/api_service.dart';
import '../../../services/authentication_service.dart';
import '../../../services/learn_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
class LearnProgramViewModel extends ReactiveViewModel {
// Dependency injection
final _apiService = locator<ApiService>();
final _learnService = locator<LearnService>();
final _statusChecker = locator<StatusCheckerService>();
@ -22,7 +24,7 @@ class LearnProgramViewModel extends ReactiveViewModel {
@override
List<ListenableServiceMixin> get listenableServices =>
[_authenticationService];
[_learnService, _authenticationService];
// Current user
User? get _user => _authenticationService.user;
@ -30,7 +32,7 @@ class LearnProgramViewModel extends ReactiveViewModel {
User? get user => _user;
// Learn programs
List<LearnProgram> _learnPrograms = [];
List<LearnProgram> get _learnPrograms => _learnService.programs;
List<LearnProgram> get learnPrograms => _learnPrograms;
@ -46,12 +48,8 @@ class LearnProgramViewModel extends ReactiveViewModel {
busyObject: StateObjects.learnPrograms);
Future<void> _getLearnPrograms() async {
if (_learnPrograms.isEmpty) {
if (await _statusChecker.checkConnection()) {
_learnPrograms = await _apiService.getLearnPrograms();
_learnPrograms
.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
}
await _learnService.getLearnPrograms();
}
}
}

View 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);
}

View File

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

View File

@ -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;
}
}
}

View File

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

View File

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

View File

@ -7,7 +7,6 @@ import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
import '../../../widgets/custom_dropdown.dart';
import '../onboarding_view.form.dart';
class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
const OccupationFormScreen({super.key});

View File

@ -2,8 +2,6 @@ import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/services/image_picker_service.dart';
import 'package:yimaru_app/services/phone_caller_service.dart';
import 'package:yimaru_app/ui/common/app_constants.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import '../../../app/app.locator.dart';
@ -12,7 +10,6 @@ import '../../../services/api_service.dart';
import '../../../services/authentication_service.dart';
import '../../../services/google_auth_service.dart';
import '../../../services/status_checker_service.dart';
import '../../../services/url_launcher_service.dart';
import '../../common/app_colors.dart';
class ProfileViewModel extends ReactiveViewModel {

View File

@ -13,7 +13,7 @@ class TelegramSupportViewModel extends BaseViewModel {
// Launch telegram
Future<void> launchTelegram() =>
_urlLauncherService.launchUri(kTelegramSupport);
_urlLauncherService.launchUri(kTelegramSupportLink);
// Navigation
void pop() => _navigationService.back();

View File

@ -4,7 +4,6 @@ import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/services/authentication_service.dart';
import '../../../app/app.locator.dart';
import '../../../services/status_checker_service.dart';
class WelcomeViewModel extends BaseViewModel {
// Dependency Injection

View File

@ -1,21 +1,21 @@
import 'package:flutter/material.dart';
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
import '../../models/subcategory.dart';
import '../../models/course_catalog.dart';
import '../common/app_colors.dart';
import '../common/ui_helpers.dart';
import 'custom_elevated_button.dart';
class CourseSubcategoryTile extends StatelessWidget {
final Subcategory subcategory;
class CourseCatalogTile extends StatelessWidget {
final CourseCatalog courseCatalog;
final GestureTapCallback? onCourseTap;
final GestureTapCallback? onPracticeTap;
const CourseSubcategoryTile({
const CourseCatalogTile({
super.key,
this.onCourseTap,
this.onPracticeTap,
required this.subcategory,
required this.courseCatalog,
});
@override
@ -50,16 +50,14 @@ class CourseSubcategoryTile extends StatelessWidget {
);
List<Widget> _buildExpansionTileChildren() => [
// _buildProgressRow(),
// verticalSpaceSmall,
_buildProgressRow(),
verticalSpaceSmall,
_buildActionButtonWrapper(),
verticalSpaceSmall
];
Widget _buildTitle() => Text(
(subcategory.name == null || subcategory.name!.isEmpty)
? 'Course ${subcategory.id}'
: subcategory.name!,
courseCatalog.name ?? '',
style: style16P600,
);
@ -76,12 +74,12 @@ class CourseSubcategoryTile extends StatelessWidget {
);
Widget _buildProgressStatus() => const CustomLinearProgressIndicator(
progress: 0.75,
progress: 0,
activeColor: kcPrimaryColor,
backgroundColor: kcVeryLightGrey);
Widget _buildProgress() => const Text(
'75%',
'0%',
style: TextStyle(color: kcDarkGrey),
);
@ -113,7 +111,7 @@ class CourseSubcategoryTile extends StatelessWidget {
);
Widget _buildExamButtonWrapper() => Expanded(
child: Container(),
child: _buildExamButton(),
);
Widget _buildExamButton() => CustomElevatedButton(

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:yimaru_app/models/category.dart';
import 'package:yimaru_app/ui/common/helper_functions.dart';
import '../common/app_colors.dart';
@ -7,11 +6,11 @@ import '../common/app_strings.dart';
import '../common/ui_helpers.dart';
import 'custom_elevated_button.dart';
class CourseCategoryCard extends StatelessWidget {
final Category category;
class CourseCard extends StatelessWidget {
final GestureTapCallback? onTap;
final Map<String, dynamic> course;
const CourseCategoryCard({super.key, this.onTap, required this.category});
const CourseCard({super.key, this.onTap, required this.course});
@override
Widget build(BuildContext context) => _buildContainer();
@ -42,12 +41,12 @@ class CourseCategoryCard extends StatelessWidget {
];
Widget _buildTitle() => Text(
category.name ?? '',
course['title'],
style: style18DG700,
);
Widget _buildSubtitle() => Text(
ksCategorySubtitle,
course['description'],
maxLines: 3,
style: style16DG400,
);

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

View File

@ -35,7 +35,7 @@ class LargeAppBar extends StatelessWidget {
);
Widget _buildStack() => Stack(
children: [ _buildPattern(),_buildAppBarWrapper()],
children: [_buildPattern(), _buildAppBarWrapper()],
);
Widget _buildAppBarWrapper() => Container(

View File

@ -8,6 +8,7 @@ import '../common/app_colors.dart';
import '../common/helper_functions.dart';
import '../common/ui_helpers.dart';
import 'custom_container_shader.dart';
import 'custom_elevated_button.dart';
import 'custom_linear_progress_indicator.dart';
@ -17,7 +18,8 @@ class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
final GestureTapCallback? onLessonTap;
final GestureTapCallback? onPracticeTap;
const LearnLessonTile({super.key,
const LearnLessonTile(
{super.key,
this.onLessonTap,
this.onPracticeTap,
required this.index,
@ -33,41 +35,44 @@ class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
border: Border.all(
color: kcPrimaryColor.withOpacity(0.1),
// color: ProgressStatuses.pending == status
// ? kcPrimaryColor.withOpacity(0.1)
// : kcGreen.withOpacity(0.1),
color: (lesson.access?.isCompleted ?? false)
? kcGreen.withOpacity(0.1)
: kcPrimaryColor.withOpacity(0.1),
),
),
child: _buildExpansionTile(viewModel),
child: _buildTileStack(viewModel),
);
Widget _buildTileStack(LearnLessonViewModel viewModel) => Stack(
children: [
_buildExpansionTile(viewModel),
_buildContainerShaderState()
],
);
Widget _buildExpansionTile(LearnLessonViewModel viewModel) => ExpansionTile(
enabled: true,
title: _buildTitle(),
textColor: kcDarkGrey,
showTrailingIcon: true,
initiallyExpanded: false,
trailing: _buildPendingIcon(),
trailing: _buildIconState(),
collapsedIconColor: kcDarkGrey,
collapsedTextColor: kcDarkGrey,
leading: _buildLeadingWrapper(),
shape: Border.all(color: kcTransparent),
expandedAlignment: Alignment.centerLeft,
backgroundColor: kcPrimaryColor.withOpacity(0.1),
enabled: (lesson.access?.isAccessible ?? false),
controlAffinity: ListTileControlAffinity.trailing,
expandedCrossAxisAlignment: CrossAxisAlignment.start,
tilePadding: const EdgeInsets.fromLTRB(15, 15, 15, 15),
collapsedBackgroundColor: kcPrimaryColor.withOpacity(0.1),
backgroundColor: (lesson.access?.isCompleted ?? false)
? kcGreen.withOpacity(0.1)
: kcPrimaryColor.withOpacity(0.1),
childrenPadding: const EdgeInsets.fromLTRB(15, 0, 15, 15),
// enabled: (lesson.access?.isAccessible ?? false),
// backgroundColor: ProgressStatuses.pending == status
// ? kcPrimaryColor.withOpacity(0.1)
// : kcGreen.withOpacity(0.1),
// collapsedBackgroundColor: ProgressStatuses.pending == status
// ? kcPrimaryColor.withOpacity(0.1)
// : kcGreen.withOpacity(0.1),
// initiallyExpanded: status != ProgressStatuses.completed ? true : false,
initiallyExpanded: (lesson.access?.isAccessible ?? false) &&
!(lesson.access?.isCompleted ?? false),
collapsedBackgroundColor: (lesson.access?.isCompleted ?? false)
? kcGreen.withOpacity(0.1)
: kcPrimaryColor.withOpacity(0.1),
children: _buildExpansionTileChildren(viewModel),
);
@ -83,9 +88,9 @@ class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
style: style16DG600,
);
// Widget _buildIconState() => ProgressStatuses.pending == status
// ? _buildPendingIcon()
// : _buildCompleteIcon();
Widget _buildIconState() => (lesson.access?.isCompleted ?? false)
? _buildCompleteIcon()
: _buildPendingIcon();
Widget _buildCompleteIcon() => const Icon(
Icons.check,
@ -116,14 +121,14 @@ class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
_buildActionButtonWrapper(viewModel)
];
Widget _buildProgress() => const CustomLinearProgressIndicator(
progress: 0,
Widget _buildProgress() => CustomLinearProgressIndicator(
activeColor: kcPrimaryColor,
backgroundColor: kcVeryLightGrey,
progress: (lesson.access?.progressPercent ?? 0) / 100,
);
Widget _buildProgressText() => Text(
'In Progress',
(lesson.access?.isCompleted ?? false) ? 'Completed' : 'In Progress',
style: style14P600,
);
@ -179,4 +184,10 @@ class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
trailingIcon: Icons.play_arrow,
backgroundColor: kcPrimaryColor,
);
Widget _buildContainerShaderState() => !(lesson.access?.isAccessible ?? false)
? _buildContainerShader()
: Container();
Widget _buildContainerShader() => const CustomContainerShader();
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/models/learn_module.dart';
import 'package:yimaru_app/ui/views/learn_module/learn_module_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_container_shader.dart';
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
import 'package:yimaru_app/ui/widgets/finish_practice_sheet.dart';
@ -11,11 +12,16 @@ import 'custom_elevated_button.dart';
class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
final LearnModule module;
final GestureTapCallback? onLockTap;
final GestureTapCallback? onModuleTap;
final GestureTapCallback? onPracticeTap;
const LearnModuleTile(
{super.key, this.onModuleTap, this.onPracticeTap, required this.module});
{super.key,
this.onLockTap,
this.onModuleTap,
this.onPracticeTap,
required this.module});
Future<void> _showSheet(
{required BuildContext context,
@ -28,8 +34,15 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
@override
Widget build(BuildContext context, LearnModuleViewModel viewModel) =>
_buildExpansionTileCard(context: context, viewModel: viewModel);
_buildExpansionTileWrapper(context: context, viewModel: viewModel);
Widget _buildExpansionTileWrapper(
{required BuildContext context,
required LearnModuleViewModel viewModel}) =>
GestureDetector(
onTap: !(module.access?.isAccessible ?? false) ? onLockTap : null,
child: _buildExpansionTileCard(context: context, viewModel: viewModel),
);
Widget _buildExpansionTileCard(
{required BuildContext context,
required LearnModuleViewModel viewModel}) =>
@ -48,7 +61,7 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
Stack(
children: [
_buildExpansionTile(context: context, viewModel: viewModel),
// _buildContainerShaderState()
_buildContainerShaderState()
],
);
@ -57,8 +70,8 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
required LearnModuleViewModel viewModel}) =>
ExpansionTile(
textColor: kcDarkGrey,
initiallyExpanded: true,
subtitle: _buildContent(),
trailing: _buildLockIcon(),
title: _buildTitleWrapper(),
leading: _buildIconWrapper(),
collapsedIconColor: kcDarkGrey,
@ -72,11 +85,18 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
expandedCrossAxisAlignment: CrossAxisAlignment.start,
tilePadding: const EdgeInsets.symmetric(horizontal: 15),
childrenPadding: const EdgeInsets.fromLTRB(70, 0, 15, 15),
showTrailingIcon: (module.access?.isAccessible ?? false) ? true : false,
initiallyExpanded: (module.access?.isAccessible ?? false),
showTrailingIcon:
!(module.access?.isAccessible ?? false) ? true : false,
children:
_buildExpansionTileChildren(context: context, viewModel: viewModel),
);
Widget _buildLockIcon() => const Icon(
Icons.lock_outline_rounded,
color: kcLightGrey,
);
Widget _buildIconWrapper() => CircleAvatar(
backgroundColor: kcPrimaryColor.withOpacity(0.1),
child: _buildIcon(),
@ -144,14 +164,15 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
child: _buildProgressStatus(),
);
Widget _buildProgressStatus() => const CustomLinearProgressIndicator(
progress: 0,
Widget _buildProgressStatus() => CustomLinearProgressIndicator(
activeColor: kcPrimaryColor,
backgroundColor: kcPrimaryColorLight);
backgroundColor: kcPrimaryColorLight,
progress: (module.access?.progressPercent ?? 0) / 100,
);
Widget _buildProgress() => const Text(
'0/0',
style: TextStyle(color: kcDarkGrey),
Widget _buildProgress() => Text(
'${module.access?.completedCount ?? 0}/${module.access?.totalCount ?? 0}',
style: style14DG500,
);
Widget _buildActionButtonWrapper(
@ -207,17 +228,8 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
);
Widget _buildContainerShaderState() => !(module.access?.isAccessible ?? false)
? _buildContainerShaderWrapper()
? _buildContainerShader()
: Container();
Widget _buildContainerShaderWrapper() => Positioned.fill(
child: _buildContainerShader(),
);
Widget _buildContainerShader() => Container(
decoration: BoxDecoration(
color: kcWhite.withOpacity(0.5),
borderRadius: BorderRadius.circular(5),
),
);
Widget _buildContainerShader() => const CustomContainerShader();
}

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

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

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

View File

@ -5,7 +5,15 @@ import '../common/ui_helpers.dart';
import 'custom_linear_progress_indicator.dart';
class ModuleProgress extends StatelessWidget {
const ModuleProgress({super.key});
final int total;
final int progress;
final int completed;
const ModuleProgress(
{super.key,
required this.total,
required this.progress,
required this.completed});
@override
Widget build(BuildContext context) => _buildContainer();
@ -36,17 +44,17 @@ class ModuleProgress extends StatelessWidget {
[_buildProgressInfo(), _buildProgress()];
Widget _buildProgressInfo() => Text(
'0% Progress',
'$progress% Progress',
style: style16DG400,
);
Widget _buildProgress() => Text(
'0/3',
'$completed/$total',
style: style14P400,
);
Widget _buildProgressIndicator() => const CustomLinearProgressIndicator(
progress: 0,
Widget _buildProgressIndicator() => CustomLinearProgressIndicator(
progress: progress / 100,
activeColor: kcPrimaryColor,
backgroundColor: kcVeryLightGrey,
);

View File

@ -4,10 +4,12 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
class OverallLearnProgress extends StatelessWidget {
final int progress;
final Color backgroundColor;
final Color indicatorBackgroundColor;
const OverallLearnProgress(
{super.key,
required this.progress,
required this.backgroundColor,
required this.indicatorBackgroundColor});
@ -51,12 +53,12 @@ class OverallLearnProgress extends StatelessWidget {
);
Widget _buildProgress() => Text(
'0%',
'$progress%',
style: style14P400,
);
Widget _buildProgressIndicator() => CustomLinearProgressIndicator(
progress: 0.0,
progress: progress / 100,
activeColor: kcPrimaryColor,
backgroundColor: indicatorBackgroundColor,
);

View File

@ -873,7 +873,7 @@ packages:
source: hosted
version: "0.15.6"
http:
dependency: transitive
dependency: "direct main"
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"

View File

@ -1,5 +1,5 @@
name: yimaru_app
version: 0.1.14+16
version: 0.1.15+17
publish_to: 'none'
description: A new Flutter project.
@ -54,6 +54,7 @@ dependencies:
flutter_local_notifications: ^20.1.0
internet_connection_checker_plus: ^2.9.1+2
http: any
dev_dependencies:
flutter_test:
sdk: flutter

View File

@ -20,6 +20,7 @@ import 'package:yimaru_app/services/in_app_update_service.dart';
import 'package:yimaru_app/services/vimeo_service.dart';
import 'package:yimaru_app/services/url_launcher_service.dart';
import 'package:yimaru_app/services/phone_caller_service.dart';
import 'package:yimaru_app/services/learn_service.dart';
// @stacked-import
import 'test_helpers.mocks.dart';
@ -52,6 +53,9 @@ import 'test_helpers.mocks.dart';
MockSpec<UrlLauncherService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<UrlLauncherService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<PhoneCallerService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<LearnLessonService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<LearnService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<LearnService>(onMissingStub: OnMissingStub.returnDefault),
// @stacked-mock-spec
],
)
@ -79,6 +83,9 @@ void registerServices() {
getAndRegisterUrlLauncherService();
getAndRegisterUrlLauncherService();
getAndRegisterPhoneCallerService();
getAndRegisterLearnLessonService();
getAndRegisterLearnService();
getAndRegisterLearnService();
// @stacked-mock-register
}
@ -261,6 +268,13 @@ MockPhoneCallerService getAndRegisterPhoneCallerService() {
locator.registerSingleton<PhoneCallerService>(service);
return service;
}
MockLearnService getAndRegisterLearnService() {
_removeRegistrationIfExists<LearnService>();
final service = MockLearnService();
locator.registerSingleton<LearnService>(service);
return service;
}
// @stacked-mock-create
void _removeRegistrationIfExists<T extends Object>() {

View File

@ -4,7 +4,7 @@ import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('CourseCategoryViewModel Tests -', () {
group('LearnServiceTest -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});

View File

@ -4,7 +4,7 @@ import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('CourseSubcategoryViewModel Tests -', () {
group('ArifPayViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});

View 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());
});
}

View 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());
});
}

View 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());
});
}