fix(learn): Integrate learn lessons according to the new hierarchy

This commit is contained in:
BisratHailu 2026-04-20 15:59:42 +03:00
parent 2050bd332c
commit 9323f73bb4
36 changed files with 1235 additions and 548 deletions

File diff suppressed because one or more lines are too long

View File

@ -52,6 +52,7 @@ import 'package:yimaru_app/services/audio_player_service.dart';
import 'package:yimaru_app/services/voice_recorder_service.dart';
import 'package:yimaru_app/ui/views/course_practice_question/course_practice_question_view.dart';
import 'package:yimaru_app/ui/views/learn_subcategory/learn_subcategory_view.dart';
import 'package:yimaru_app/ui/views/learn_submodule/learn_submodule_view.dart';
// @stacked-import
@StackedApp(
@ -92,6 +93,7 @@ import 'package:yimaru_app/ui/views/learn_subcategory/learn_subcategory_view.dar
MaterialRoute(page: CourseView),
MaterialRoute(page: CoursePracticeQuestionView),
MaterialRoute(page: LearnSubcategoryView),
MaterialRoute(page: LearnSubmoduleView),
// @stacked-route
],
dependencies: [

File diff suppressed because it is too large Load Diff

52
lib/models/lesson.dart Normal file
View File

@ -0,0 +1,52 @@
import 'package:json_annotation/json_annotation.dart';
part 'lesson.g.dart';
@JsonSerializable()
class Lesson {
final int? id;
final String? title;
final String? thumbnail;
final String? description;
@JsonKey(name: 'is_active')
final bool? isActive;
@JsonKey(name: 'sub_module_id')
final int? subModuleId;
@JsonKey(name: 'teaching_text')
final String? teachingText;
@JsonKey(name: 'display_order')
final int? displayOrder;
@JsonKey(name: 'teaching_video_url')
final String? teachingVideoUrl;
@JsonKey(name: 'teaching_image_url')
final String? teachingImageUrl;
@JsonKey(name: 'teaching_audio_url')
final String? teachingAudioUrl;
const Lesson(
{this.id,
this.title,
this.isActive,
this.thumbnail,
this.subModuleId,
this.description,
this.teachingText,
this.displayOrder,
this.teachingAudioUrl,
this.teachingImageUrl,
this.teachingVideoUrl});
factory Lesson.fromJson(Map<String, dynamic> json) => _$LessonFromJson(json);
Map<String, dynamic> toJson() => _$LessonToJson(this);
}

35
lib/models/lesson.g.dart Normal file
View File

@ -0,0 +1,35 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'lesson.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Lesson _$LessonFromJson(Map<String, dynamic> json) => Lesson(
id: (json['id'] as num?)?.toInt(),
title: json['title'] as String?,
isActive: json['is_active'] as bool?,
thumbnail: json['thumbnail'] as String?,
subModuleId: (json['sub_module_id'] as num?)?.toInt(),
description: json['description'] as String?,
teachingText: json['teaching_text'] as String?,
displayOrder: (json['display_order'] as num?)?.toInt(),
teachingAudioUrl: json['teaching_audio_url'] as String?,
teachingImageUrl: json['teaching_image_url'] as String?,
teachingVideoUrl: json['teaching_video_url'] as String?,
);
Map<String, dynamic> _$LessonToJson(Lesson instance) => <String, dynamic>{
'id': instance.id,
'title': instance.title,
'thumbnail': instance.thumbnail,
'description': instance.description,
'is_active': instance.isActive,
'sub_module_id': instance.subModuleId,
'teaching_text': instance.teachingText,
'display_order': instance.displayOrder,
'teaching_video_url': instance.teachingVideoUrl,
'teaching_image_url': instance.teachingImageUrl,
'teaching_audio_url': instance.teachingAudioUrl,
};

44
lib/models/submodule.dart Normal file
View File

@ -0,0 +1,44 @@
import 'package:json_annotation/json_annotation.dart';
part 'submodule.g.dart';
@JsonSerializable()
class Submodule {
final int? id;
final String? tips;
final String? title;
final String? thumbnail;
final String? description;
@JsonKey(name: 'module_id')
final int? moduleId;
@JsonKey(name: 'is_active')
final bool? isActive;
@JsonKey(name: 'display_order')
final int? displayOrder;
@JsonKey(name: 'legacy_sub_course_id')
final int? legacySubCourseId;
const Submodule(
{this.id,
this.title,
this.tips,
this.moduleId,
this.isActive,
this.thumbnail,
this.description,
this.displayOrder,
this.legacySubCourseId});
factory Submodule.fromJson(Map<String, dynamic> json) =>
_$SubmoduleFromJson(json);
Map<String, dynamic> toJson() => _$SubmoduleToJson(this);
}

View File

@ -0,0 +1,31 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'submodule.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Submodule _$SubmoduleFromJson(Map<String, dynamic> json) => Submodule(
id: (json['id'] as num?)?.toInt(),
title: json['title'] as String?,
tips: json['tips'] as String?,
moduleId: (json['module_id'] as num?)?.toInt(),
isActive: json['is_active'] as bool?,
thumbnail: json['thumbnail'] as String?,
description: json['description'] as String?,
displayOrder: (json['display_order'] as num?)?.toInt(),
legacySubCourseId: (json['legacy_sub_course_id'] as num?)?.toInt(),
);
Map<String, dynamic> _$SubmoduleToJson(Submodule instance) => <String, dynamic>{
'id': instance.id,
'tips': instance.tips,
'title': instance.title,
'thumbnail': instance.thumbnail,
'description': instance.description,
'module_id': instance.moduleId,
'is_active': instance.isActive,
'display_order': instance.displayOrder,
'legacy_sub_course_id': instance.legacySubCourseId,
};

View File

@ -13,7 +13,9 @@ import 'package:yimaru_app/services/dio_service.dart';
import 'package:yimaru_app/ui/common/app_constants.dart';
import '../app/app.locator.dart';
import '../models/lesson.dart';
import '../models/module.dart';
import '../models/submodule.dart';
import '../ui/common/enmus.dart';
class ApiService {
@ -674,4 +676,52 @@ class ApiService {
return [];
}
}
// Get submodules
Future<List<Submodule>> getSubmodules(int id) async {
try {
List<Submodule> submodules = [];
final Response response = await _service.dio.get(
'$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kModulesUrl/$id/$kSubmodulesUrl');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data']['sub_modules'] as List;
submodules = decodedData.map(
(e) {
return Submodule.fromJson(e);
},
).toList();
return submodules;
}
return [];
} catch (e) {
return [];
}
}
// Get lessons
Future<List<Lesson>> getLessons(int id) async {
try {
List<Lesson> lessons = [];
final Response response = await _service.dio.get(
'$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kSubmodulesUrl/$id/$kLessonsUrl');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data'] as List;
lessons = decodedData.map(
(e) {
return Lesson.fromJson(e);
},
).toList();
return lessons;
}
return [];
} catch (e) {
return [];
}
}
}

View File

@ -10,6 +10,8 @@ String kCoursesUrl = 'courses';
String kModulesUrl = 'modules';
String kLessonsUrl = 'lessons';
String kRegisterUrl = 'register';
String kCategoryUrl = 'categories';
@ -24,6 +26,8 @@ String kResendOtpUrl = 'resend-otp';
String kGetUserUrl = 'user-profile';
String kSubmodulesUrl = 'sub-modules';
String kSubcoursesUrl = 'sub-courses';
String kCompleteLessonUrl = 'complete';

View File

@ -28,6 +28,7 @@ enum StateObjects {
verifyOtp,
resendOtp,
learnLevels,
learnLessons,
learnModules,
learnCourses,
profileImage,
@ -40,6 +41,7 @@ enum StateObjects {
loginWithGoogle,
loadLessonVideo,
loadCourseVideo,
learnSubmodules,
requestResetCode,
courseCategories,
profileCompletion,

View File

@ -41,9 +41,8 @@ class CourseCategoryViewModel extends ReactiveViewModel {
// Remote api call
// Course categories
Future<void> getCategories() async =>
await runBusyFuture(_getCategories(),
busyObject: StateObjects.courseCategories);
Future<void> getCategories() async => await runBusyFuture(_getCategories(),
busyObject: StateObjects.courseCategories);
Future<void> _getCategories() async {
if (categories.isEmpty) {

View File

@ -66,6 +66,7 @@ class CourseLessonDetailViewModel extends BaseViewModel {
busyObject: StateObjects.loadCourseVideo);
Future<void> _initializePlayer(CourseLesson lesson) async {
print('URL: $kSampleVideoUrl');
_videoPlayerController =
VideoPlayerController.networkUrl(Uri.parse(kSampleVideoUrl));

View File

@ -1,31 +1,30 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/models/submodule.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/widgets/learn_lesson_tile.dart';
import 'package:yimaru_app/ui/widgets/module_progress.dart';
import 'package:yimaru_app/ui/widgets/motivation_card.dart';
import '../../../models/lesson.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/custom_elevated_button.dart';
import '../../widgets/custom_circular_progress_indicator.dart';
import '../../widgets/small_app_bar.dart';
import 'learn_lesson_viewmodel.dart';
class LearnLessonView extends StackedView<LearnLessonViewModel> {
final String title;
final String topics;
final String subtitle;
final String description;
final List<Map<String, dynamic>> practices;
final Submodule submodule;
const LearnLessonView({Key? key, required this.submodule}) : super(key: key);
@override
void onViewModelReady(LearnLessonViewModel viewModel) async {
await viewModel.getLessons(submodule.id ?? 0);
super.onViewModelReady(viewModel);
}
const LearnLessonView(
{Key? key,
required this.title,
required this.topics,
required this.subtitle,
required this.practices,
required this.description})
: super(key: key);
Widget getPadding(context) {
double half = screenHeight(context) / 2;
@ -117,95 +116,63 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
verticalSpaceTiny,
_buildSubtitle(),
verticalSpaceSmall,
_buildTopics(),
verticalSpaceSmall,
// _buildModuleProgress(),
// verticalSpaceMedium,
// _buildContinueButton(),
// verticalSpaceMedium,
// _buildMotivationCard(),
// verticalSpaceMedium,
//_buildHeader(),
//verticalSpaceMedium,
// _buildListView(viewModel),
getPadding(context),
_buildStartButton(viewModel),
verticalSpaceSmall,
_buildPracticeButton(viewModel)
_buildModuleProgress(),
verticalSpaceMedium,
verticalSpaceMedium,
_buildMotivationCard(),
verticalSpaceMedium,
_buildHeader(),
verticalSpaceMedium,
_buildListViewBuilder(viewModel),
];
Widget _buildTitle() => Text(
title,
submodule.title ?? '',
style: style16DG600,
);
Widget _buildSubtitle() => Text(
subtitle,
submodule.description ?? '',
style: style14DG600,
);
Widget _buildTopics() => Text(
topics,
style: style14DG500,
);
Widget _buildModuleProgress() => const ModuleProgress();
Widget _buildStartButton(LearnLessonViewModel viewModel) =>
CustomElevatedButton(
height: 55,
borderRadius: 12,
text: 'Start $title',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
onTap: () async => await viewModel.navigateToLearnLessonDetail(
title: title, practices: practices, description: description),
);
Widget _buildPracticeButton(LearnLessonViewModel viewModel) =>
CustomElevatedButton(
height: 55,
borderRadius: 12,
text: 'Practice',
backgroundColor: kcWhite,
borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor,
onTap: () async =>
await viewModel.navigateToLearnPractice(practices));
Widget _buildMotivationCard() => const MotivationCard();
Widget _buildHeader() => Text(
title,
'Lessons in this module',
style: style18DG700,
);
Widget _buildListViewBuilder(LearnLessonViewModel viewModel) =>
viewModel.busy(StateObjects.learnLessons)
? _buildProgressIndicator()
: _buildListView(viewModel);
Widget _buildProgressIndicator() => const Center(
child: CustomCircularProgressIndicator(color: kcPrimaryColor),
);
Widget _buildListView(LearnLessonViewModel viewModel) => ListView.builder(
shrinkWrap: true,
itemCount: viewModel.lessons.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildTile(
title: viewModel.lessons[index]['title'],
status: viewModel.lessons[index]['status'],
thumbnail: viewModel.lessons[index]['thumbnail'],
onLessonTap: () async => await viewModel.navigateToLearnLessonDetail(
title: title, practices: practices, description: description),
// onPracticeTap: () async => await viewModel.navigateToLearnPractice(),
lesson: viewModel.lessons[index],
onLessonTap: () async => await viewModel.navigateToLearnLessonDetail(viewModel.lessons[index]),
),
);
Widget _buildTile({
required String title,
required String thumbnail,
GestureTapCallback? onLessonTap,
required ProgressStatuses status,
GestureTapCallback? onPracticeTap,
required Lesson lesson,
required GestureTapCallback? onLessonTap,
}) =>
LearnLessonTile(
title: title,
status: status,
thumbnail: thumbnail,
lesson: lesson,
onLessonTap: onLessonTap,
onPracticeTap: onPracticeTap,
);
}

View File

@ -4,47 +4,42 @@ import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import '../../../app/app.locator.dart';
import '../../../models/lesson.dart';
import '../../../services/api_service.dart';
import '../../../services/status_checker_service.dart';
class LearnLessonViewModel extends BaseViewModel {
// Dependency injection
final _apiService = locator<ApiService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
// Lessons
final List<Map<String, dynamic>> _lessons = [
{
'title': '1.1 Introducing Yourself',
'status': ProgressStatuses.completed,
'thumbnail': 'assets/images/image_1.png',
},
{
'status': ProgressStatuses.completed,
'thumbnail': 'assets/images/image_1.png',
'title': '1.2 Talking About Your Surroundings',
},
{
'status': ProgressStatuses.pending,
'title': '1.1 Introducing Yourself',
'thumbnail': 'assets/images/image_1.png',
},
];
// Learn lessons
List<Lesson> _lessons = [];
List<Map<String, dynamic>> get lessons => _lessons;
List<Lesson> get lessons => _lessons;
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToLearnLessonDetail(
{required String title,
required List<Map<String, dynamic>> practices,
required String description}) async =>
await _navigationService.navigateToLearnLessonDetailView(
title: title, practices: practices, description: description);
Future<void> navigateToLearnLessonDetail(Lesson lesson) async =>
await _navigationService.navigateToLearnLessonDetailView(lesson: lesson);
Future<void> navigateToLearnPractice(
List<Map<String, dynamic>> practices) async =>
await _navigationService.navigateToLearnPracticeView(
practices: practices,
title: 'Lets Practice',
buttonLabel: 'Begin Lesson Practice',
subtitle: 'Lets quickly review what youve learned in this lesson!',
);
// Remote api call
// Learn modules
Future<void> getLessons(int id) async => await runBusyFuture(_getLessons(id),
busyObject: StateObjects.learnLessons);
Future<void> _getLessons(int id) async {
if (_lessons.isEmpty) {
if (await _statusChecker.checkConnection()) {
_lessons = await _apiService.getLessons(id);
_lessons.sort(
(a, b) => (a.displayOrder ?? 0).compareTo(b.displayOrder ?? 0));
}
}
}
}

View File

@ -1,8 +1,9 @@
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/ui/widgets/empty_video_player.dart';
import '../../../models/lesson.dart';
import '../../common/app_colors.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
@ -11,20 +12,14 @@ import '../../widgets/small_app_bar.dart';
import 'learn_lesson_detail_viewmodel.dart';
class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
final String title;
final String description;
final List<Map<String, dynamic>> practices;
final Lesson lesson;
const LearnLessonDetailView(
{Key? key,
required this.title,
required this.practices,
required this.description})
const LearnLessonDetailView({Key? key, required this.lesson})
: super(key: key);
Future<void> _navigate(LearnLessonDetailViewModel viewModel) async {
await viewModel.pause();
await viewModel.navigateToLearnPractice(practices);
// await viewModel.navigateToLearnPractice(practices);
}
@override
@ -34,12 +29,6 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
super.onDispose(viewModel);
}
@override
void onViewModelReady(LearnLessonDetailViewModel viewModel) async {
await viewModel.initializePlayer();
super.onViewModelReady(viewModel);
}
@override
LearnLessonDetailViewModel viewModelBuilder(BuildContext context) =>
LearnLessonDetailViewModel();
@ -125,7 +114,7 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
);
Widget _buildTitle() => Text(
title,
lesson.title ?? '',
style: style16DG600,
);
@ -134,21 +123,21 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
height: 200,
color: kcBlack,
width: double.maxFinite,
child: _buildVideoPlayerState(viewModel),
child: _buildVideoPlayer(viewModel),
);
Widget _buildVideoPlayerState(LearnLessonDetailViewModel viewModel) =>
viewModel.chewieController != null &&
viewModel.videoPlayerController != null &&
!viewModel.busy(StateObjects.loadLessonVideo)
? _buildVideoPlayer(viewModel)
: _buildEmptyVideoPlayer();
Widget _buildVideoPlayer(LearnLessonDetailViewModel viewModel) =>
_buildChewiePlayer(viewModel);
_buildVimeoPlayer(viewModel);
Widget _buildChewiePlayer(LearnLessonDetailViewModel viewModel) =>
Chewie(controller: viewModel.chewieController!);
Widget _buildVimeoPlayer(LearnLessonDetailViewModel viewModel) =>
VimeoVideoPlayer(
isAutoPlay: true,
onInAppWebViewCreated: (controller) =>
viewModel.initializePlayer(controller),
videoId: lesson.teachingVideoUrl?.split('/').last ?? '',
);
Widget _buildEmptyVideoPlayer() => const EmptyVideoPlayer();
@ -158,7 +147,7 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
);
Widget _buildDescription() => Text(
description,
lesson.description ?? '',
style: style14DG600,
);
@ -175,7 +164,7 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
Widget _buildContinueButton(LearnLessonDetailViewModel viewModel) =>
CustomElevatedButton(
height: 55,
text: 'Practice',
text: 'Lessons',
borderRadius: 12,
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,

View File

@ -1,11 +1,10 @@
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';
import 'package:yimaru_app/app/app.router.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 '../../../app/app.locator.dart';
import '../../../services/status_checker_service.dart';
@ -20,6 +19,10 @@ class LearnLessonDetailViewModel extends BaseViewModel {
ChewieController? get chewieController => _chewieController;
InAppWebViewController? _webViewController;
InAppWebViewController? get webViewController => _webViewController;
VideoPlayerController? _videoPlayerController;
VideoPlayerController? get videoPlayerController => _videoPlayerController;
@ -34,32 +37,22 @@ class LearnLessonDetailViewModel extends BaseViewModel {
await _chewieController?.pause();
}
Future<void> initializePlayer() async =>
await runBusyFuture(_initializePlayer(),
busyObject: StateObjects.loadLessonVideo);
Future<void> _initializePlayer() async {
_videoPlayerController =
VideoPlayerController.networkUrl(Uri.parse(kSampleVideoUrl));
await _videoPlayerController?.initialize();
if (_videoPlayerController != null) {
_chewieController = ChewieController(
looping: true,
autoPlay: true,
showOptions: true,
showControls: true,
aspectRatio: 16 / 9,
autoInitialize: true,
allowedScreenSleep: false,
videoPlayerController: _videoPlayerController!,
materialProgressColors: buildChewieProgressIndicator);
}
// rebuildUi();
void initializePlayer(InAppWebViewController controller){
_webViewController = controller;
rebuildUi();
}
void onLoadVideoStart() {
setBusyForObject(StateObjects.loadLessonVideo, true);
rebuildUi();
}
void onLoadVideoComplete() {
setBusyForObject(StateObjects.loadLessonVideo, false);
rebuildUi();
}
// Navigation
void pop() => _navigationService.back();

View File

@ -55,6 +55,7 @@ class LearnLevelView extends StackedView<LearnLevelViewModel> {
);
Widget _buildAppBar(LearnLevelViewModel viewModel) => SmallAppBar(
title: 'Levels',
onTap: viewModel.pop,
showBackButton: true,
);
@ -82,7 +83,8 @@ class LearnLevelView extends StackedView<LearnLevelViewModel> {
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildTile(
level: viewModel.levels[index],
onTap: () async => await viewModel.navigateToModule( viewModel.levels[index]),
onTap: () async =>
await viewModel.navigateToModule(viewModel.levels[index]),
),
separatorBuilder: (context, index) => verticalSpaceSmall,
);

View File

@ -1,13 +1,14 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/models/level.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/widgets/learn_module_tile.dart';
import 'package:yimaru_app/ui/widgets/overall_learn_progress.dart';
import '../../../models/module.dart';
import '../../common/app_colors.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/custom_circular_progress_indicator.dart';
import '../../widgets/small_app_bar.dart';
import 'learn_module_viewmodel.dart';
@ -57,6 +58,7 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
);
Widget _buildAppBar(LearnModuleViewModel viewModel) => SmallAppBar(
title: 'Modules',
onTap: viewModel.pop,
showBackButton: true,
);
@ -79,10 +81,10 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
verticalSpaceMedium,
_buildTitle(),
_buildSubtitle(),
verticalSpaceMedium,
verticalSpaceLarge,
_buildOverallProgress(),
verticalSpaceMedium,
_buildListView(viewModel)
_buildListViewBuilder(viewModel)
];
Widget _buildTitle() => Text(
@ -95,26 +97,36 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
style: style14DG400,
);
Widget _buildOverallProgress() => const OverallLearnProgress();
Widget _buildOverallProgress() => OverallLearnProgress(
color: kcPrimaryColor.withOpacity(0.1),
);
Widget _buildListViewBuilder(LearnModuleViewModel viewModel) =>
viewModel.busy(StateObjects.learnModules)
? _buildProgressIndicator()
: _buildListView(viewModel);
Widget _buildProgressIndicator() => const Center(
child: CustomCircularProgressIndicator(color: kcPrimaryColor),
);
Widget _buildListView(LearnModuleViewModel viewModel) => ListView.builder(
shrinkWrap: true,
itemCount: viewModel.modules.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildTile(
module: viewModel.modules[index],
onLessonTap: () {},
onPracticeTap: () {}),
module: viewModel.modules[index],
onModuleTap: () async => await viewModel
.navigateToLearnSubmodule(viewModel.modules[index]),
),
);
Widget _buildTile({
required Module module,
required GestureTapCallback onLessonTap,
required GestureTapCallback onPracticeTap,
required GestureTapCallback onModuleTap,
}) =>
LearnModuleTile(
module: module,
onLessonTap: onLessonTap,
onPracticeTap: onPracticeTap,
onModuleTap: onModuleTap,
);
}

View File

@ -24,27 +24,8 @@ class LearnModuleViewModel extends BaseViewModel {
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToLearnLesson(
{required String title,
required String topics,
required String subtitle,
required String description,
required List<Map<String, dynamic>> practices}) async =>
await _navigationService.navigateToLearnLessonView(
title: title,
topics: topics,
subtitle: subtitle,
practices: practices,
description: description);
Future<void> navigateToLearnPractice(
List<Map<String, dynamic>> practices) async =>
await _navigationService.navigateToLearnPracticeView(
practices: practices,
title: 'Lets Practice',
buttonLabel: 'Begin Lesson Practice',
subtitle: 'Lets quickly review what youve learned in this lesson!',
);
Future<void> navigateToLearnSubmodule(Module module) async =>
await _navigationService.navigateToLearnSubmoduleView(module: module);
// Remote api call

View File

@ -0,0 +1,146 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/models/module.dart';
import 'package:yimaru_app/models/submodule.dart';
import 'package:yimaru_app/ui/widgets/course_module_banner.dart';
import 'package:yimaru_app/ui/widgets/learn_submodule_tile.dart';
import '../../common/app_colors.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/custom_circular_progress_indicator.dart';
import '../../widgets/custom_elevated_button.dart';
import '../../widgets/overall_learn_progress.dart';
import '../../widgets/small_app_bar.dart';
import 'learn_submodule_viewmodel.dart';
class LearnSubmoduleView extends StackedView<LearnSubmoduleViewModel> {
final Module module;
@override
void onViewModelReady(LearnSubmoduleViewModel viewModel) async {
await viewModel.getSubmodules(module.id ?? 0);
super.onViewModelReady(viewModel);
}
const LearnSubmoduleView({Key? key, required this.module}) : super(key: key);
@override
LearnSubmoduleViewModel viewModelBuilder(BuildContext context) =>
LearnSubmoduleViewModel();
@override
Widget builder(
BuildContext context,
LearnSubmoduleViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(LearnSubmoduleViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(LearnSubmoduleViewModel viewModel) =>
SafeArea(child: _buildBody(viewModel));
Widget _buildBody(LearnSubmoduleViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel),
);
Widget _buildColumn(LearnSubmoduleViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(viewModel),
verticalSpaceMedium,
_buildModulesColumnWrapper(viewModel),
],
);
Widget _buildAppBar(LearnSubmoduleViewModel viewModel) => SmallAppBar(
title: 'Submodules',
onTap: viewModel.pop,
showBackButton: true,
);
Widget _buildModulesColumnWrapper(LearnSubmoduleViewModel viewModel) =>
Expanded(child: _buildLevelsColumnScrollView(viewModel));
Widget _buildLevelsColumnScrollView(LearnSubmoduleViewModel viewModel) =>
SingleChildScrollView(
child: _buildLevelsColumn(viewModel),
);
Widget _buildLevelsColumn(LearnSubmoduleViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildLevelsColumnChildren(viewModel),
);
List<Widget> _buildLevelsColumnChildren(LearnSubmoduleViewModel viewModel) =>
[
verticalSpaceMedium,
_buildTitle(),
verticalSpaceMedium,
_buildCourseModuleBanner(),
verticalSpaceMedium,
_buildOverallProgress(),
verticalSpaceTiny,
_buildContinueButton(viewModel),
verticalSpaceMedium,
_buildListViewBuilder(viewModel)
];
Widget _buildTitle() => Text(
module.title ?? '',
style: style18P600,
);
Widget _buildCourseModuleBanner() => const CourseModuleBanner();
Widget _buildOverallProgress() => const OverallLearnProgress(
color: Colors.transparent,
);
Widget _buildContinueButton(LearnSubmoduleViewModel viewModel) =>
const CustomElevatedButton(
height: 55,
borderRadius: 12,
foregroundColor: kcWhite,
text: 'Continue Submodule',
backgroundColor: kcPrimaryColor);
Widget _buildListViewBuilder(LearnSubmoduleViewModel viewModel) =>
viewModel.busy(StateObjects.learnSubmodules)
? _buildProgressIndicator()
: _buildListView(viewModel);
Widget _buildProgressIndicator() => const Center(
child: CustomCircularProgressIndicator(color: kcPrimaryColor),
);
Widget _buildListView(LearnSubmoduleViewModel viewModel) => ListView.builder(
shrinkWrap: true,
itemCount: viewModel.submodules.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildTile(
submodule: viewModel.submodules[index],
onPracticeTap: () {},
onLessonTap: () async => await viewModel
.navigateToLearnLessons(viewModel.submodules[index]),
),
);
Widget _buildTile({
required Submodule submodule,
required GestureTapCallback onLessonTap,
required GestureTapCallback onPracticeTap,
}) =>
LearnSubmoduleTile(
submodule: submodule,
onLessonTap: onLessonTap,
onPracticeTap: onPracticeTap,
);
}

View File

@ -0,0 +1,46 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import '../../../app/app.locator.dart';
import '../../../models/submodule.dart';
import '../../../services/api_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
class LearnSubmoduleViewModel extends BaseViewModel {
// Dependency injection
final _apiService = locator<ApiService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
// Learn submodule
List<Submodule> _submodules = [];
List<Submodule> get submodules => _submodules;
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToLearnLessons(Submodule submodule) async =>
await _navigationService.navigateToLearnLessonView(submodule: submodule);
// Remote api call
// Learn modules
Future<void> getSubmodules(int id) async =>
await runBusyFuture(_getSubmodules(id),
busyObject: StateObjects.learnSubmodules);
Future<void> _getSubmodules(int id) async {
if (_submodules.isEmpty) {
if (await _statusChecker.checkConnection()) {
_submodules = await _apiService.getSubmodules(id);
_submodules.sort(
(a, b) => (a.displayOrder ?? 0).compareTo(b.displayOrder ?? 0));
}
}
}
}

View File

@ -8,7 +8,7 @@ class CourseModuleBanner extends StatelessWidget {
Widget build(BuildContext context) => _buildContainer();
Widget _buildContainer() => Container(
height: 150,
height: 125,
width: double.maxFinite,
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(

View File

@ -1,27 +1,17 @@
import 'package:flutter/material.dart';
import 'package:yimaru_app/models/lesson.dart';
import 'package:yimaru_app/ui/widgets/mini_thumbnail.dart';
import '../common/app_colors.dart';
import '../common/enmus.dart';
import '../common/ui_helpers.dart';
import 'custom_elevated_button.dart';
import 'custom_linear_progress_indicator.dart';
class LearnLessonTile extends StatelessWidget {
final String title;
final String thumbnail;
final ProgressStatuses status;
final Lesson lesson;
final GestureTapCallback? onLessonTap;
final GestureTapCallback? onPracticeTap;
const LearnLessonTile({
super.key,
this.onLessonTap,
this.onPracticeTap,
required this.title,
required this.status,
required this.thumbnail,
});
const LearnLessonTile({super.key, this.onLessonTap, required this.lesson});
@override
Widget build(BuildContext context) => _buildContainer();
@ -32,50 +22,55 @@ class LearnLessonTile extends StatelessWidget {
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
border: Border.all(
color: ProgressStatuses.pending == status
? kcPrimaryColor.withOpacity(0.1)
: kcGreen.withOpacity(0.1),
color: kcPrimaryColor.withOpacity(0.1),
// color: ProgressStatuses.pending == status
// ? kcPrimaryColor.withOpacity(0.1)
// : kcGreen.withOpacity(0.1),
),
),
child: _buildExpansionTile(),
);
Widget _buildExpansionTile() => ExpansionTile(
enabled: true,
title: _buildTitle(),
textColor: kcDarkGrey,
showTrailingIcon: true,
trailing: _buildIconState(),
// subtitle: _buildContent(),
initiallyExpanded: true,
trailing: _buildPendingIcon(),
collapsedIconColor: kcDarkGrey,
collapsedTextColor: kcDarkGrey,
leading: _buildLeadingWrapper(),
shape: Border.all(color: kcTransparent),
expandedAlignment: Alignment.centerLeft,
enabled: status != ProgressStatuses.pending,
backgroundColor: kcGreen.withOpacity(0.1),
controlAffinity: ListTileControlAffinity.trailing,
backgroundColor: ProgressStatuses.pending == status
? kcPrimaryColor.withOpacity(0.1)
: kcGreen.withOpacity(0.1),
childrenPadding: const EdgeInsets.fromLTRB(15, 15, 15, 15),
expandedCrossAxisAlignment: CrossAxisAlignment.start,
collapsedBackgroundColor: ProgressStatuses.pending == status
? kcPrimaryColor.withOpacity(0.1)
: kcGreen.withOpacity(0.1),
collapsedBackgroundColor: kcPrimaryColor.withOpacity(0.1),
childrenPadding: const EdgeInsets.fromLTRB(15, 15, 15, 15),
tilePadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 15),
initiallyExpanded: status != ProgressStatuses.completed ? true : false,
// enabled: status != ProgressStatuses.pending,
// 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,
children: _buildExpansionTileChildren(),
);
Widget _buildLeadingWrapper() => MiniThumbnail(thumbnail: thumbnail);
Widget _buildLeadingWrapper() =>
MiniThumbnail(thumbnail: lesson.thumbnail ?? 'assets/images/image_1.png');
Widget _buildTitle() => Text(
title,
lesson.title ?? '',
style: style16DG600,
);
Widget _buildIconState() => ProgressStatuses.pending == status
? _buildPendingIcon()
: _buildCompleteIcon();
// Widget _buildIconState() => ProgressStatuses.pending == status
// ? _buildPendingIcon()
// : _buildCompleteIcon();
Widget _buildCompleteIcon() => const Icon(
Icons.check,
@ -98,58 +93,35 @@ class LearnLessonTile extends StatelessWidget {
List<Widget> _buildExpansionTileItemChildren() => [
_buildProgress(),
horizontalSpaceSmall,
_buildProgressText(),
verticalSpaceSmall,
// _buildProgressText(),
// verticalSpaceSmall,
_buildActionButtonWrapper()
];
Widget _buildProgress() => CustomLinearProgressIndicator(
Widget _buildProgress() => const CustomLinearProgressIndicator(
progress: 0,
activeColor: kcPrimaryColor,
backgroundColor: kcVeryLightGrey,
progress: ProgressStatuses.completed == status ? 1 : 0.75,
);
Widget _buildProgressText() => Text(
ProgressStatuses.completed == status ? 'Completed' : 'In Progress',
style: style14P400,
);
// Widget _buildProgressText() => Text(
// ProgressStatuses.completed == status ? 'Completed' : 'In Progress',
// style: style14P400,
// );
Widget _buildActionButtonWrapper() => SizedBox(
height: 40,
child: _buildActionButtons(),
);
Widget _buildActionButtons() => Row(
mainAxisAlignment: MainAxisAlignment.end,
children: _buildActionButtonChildren(),
);
List<Widget> _buildActionButtonChildren() => [
_buildPracticeButton(),
horizontalSpaceSmall,
_buildLessonButton(),
];
Widget _buildPracticeButton() => CustomElevatedButton(
height: 15,
width: 135,
text: 'Practice',
borderRadius: 12,
onTap: onPracticeTap,
trailingIcon: Icons.mic,
backgroundColor: kcWhite,
borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor,
height: 50,
child: _buildLessonButton(),
);
Widget _buildLessonButton() => CustomElevatedButton(
height: 15,
width: 135,
text: 'Start',
borderRadius: 12,
onTap: onLessonTap,
width: double.maxFinite,
foregroundColor: kcWhite,
trailingIcon: Icons.play_arrow,
backgroundColor: kcPrimaryColor,
text: ProgressStatuses.completed == status ? 'View' : 'Continue',
);
}

View File

@ -6,17 +6,14 @@ import 'package:yimaru_app/ui/widgets/finish_practice_sheet.dart';
import '../../models/module.dart';
import '../common/app_colors.dart';
import '../common/enmus.dart';
import '../common/ui_helpers.dart';
import 'custom_elevated_button.dart';
class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
final Module module;
final GestureTapCallback? onLessonTap;
final GestureTapCallback? onPracticeTap;
final GestureTapCallback? onModuleTap;
const LearnModuleTile(
{super.key, this.onLessonTap, this.onPracticeTap, required this.module});
const LearnModuleTile({super.key, this.onModuleTap, required this.module});
Future<void> _showSheet(
{required BuildContext context,
@ -158,7 +155,7 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
required LearnModuleViewModel viewModel}) =>
SizedBox(
height: 40,
child: _buildActionButtons(context: context, viewModel: viewModel),
child: _buildModuleButton(viewModel),
);
Widget _buildActionButtons(
@ -166,46 +163,39 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
required LearnModuleViewModel viewModel}) =>
Row(
children: [
_buildLessonButtonWrapper(viewModel),
_buildModuleButtonWrapper(viewModel),
horizontalSpaceSmall,
_buildPracticeButtonWrapper(context: context, viewModel: viewModel)
],
);
Widget _buildLessonButtonWrapper(LearnModuleViewModel viewModel) => Expanded(
child: _buildLessonButton(viewModel),
Widget _buildModuleButtonWrapper(LearnModuleViewModel viewModel) => Expanded(
child: _buildModuleButton(viewModel),
);
Widget _buildLessonButton(LearnModuleViewModel viewModel) =>
Widget _buildModuleButton(LearnModuleViewModel viewModel) =>
CustomElevatedButton(
height: 15,
borderRadius: 12,
onTap: onLessonTap,
onTap: onModuleTap,
text: 'View Module',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
// onTap: () async => await viewModel.navigateToLearnLesson(
// title: title,
// topics: topics,
// subtitle: subtitle,
// practices: practices,
// description: description),
);
Widget _buildPracticeButtonWrapper(
{required BuildContext context,
required LearnModuleViewModel viewModel}) =>
Expanded(
child: _buildPracticeButton(context: context, viewModel: viewModel),
child: Container(),
);
Widget _buildPracticeButton(
{required BuildContext context,
required LearnModuleViewModel viewModel}) =>
CustomElevatedButton(
const CustomElevatedButton(
height: 15,
borderRadius: 12,
onTap: onPracticeTap,
text: 'View Practices',
backgroundColor: kcWhite,
borderColor: kcPrimaryColor,

View File

@ -0,0 +1,237 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/models/submodule.dart';
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
import 'package:yimaru_app/ui/widgets/finish_practice_sheet.dart';
import '../common/app_colors.dart';
import '../common/ui_helpers.dart';
import '../views/learn_submodule/learn_submodule_viewmodel.dart';
import 'custom_elevated_button.dart';
class LearnSubmoduleTile extends ViewModelWidget<LearnSubmoduleViewModel> {
final Submodule submodule;
final GestureTapCallback? onLessonTap;
final GestureTapCallback? onPracticeTap;
const LearnSubmoduleTile(
{super.key,
this.onLessonTap,
this.onPracticeTap,
required this.submodule});
Future<void> _showSheet(
{required BuildContext context,
required LearnSubmoduleViewModel viewModel}) async =>
await showModalBottomSheet(
context: context,
backgroundColor: kcTransparent,
builder: (_) => _buildSheet(viewModel),
);
@override
Widget build(BuildContext context, LearnSubmoduleViewModel viewModel) =>
_buildExpansionTileCard(context: context, viewModel: viewModel);
Widget _buildExpansionTileCard(
{required BuildContext context,
required LearnSubmoduleViewModel viewModel}) =>
Container(
margin: const EdgeInsets.only(bottom: 15),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
border: Border.all(color: kcVeryLightGrey),
),
child: _buildTileStack(context: context, viewModel: viewModel),
);
Widget _buildTileStack(
{required BuildContext context,
required LearnSubmoduleViewModel viewModel}) =>
Stack(
children: [
_buildExpansionTile(context: context, viewModel: viewModel),
// _buildContainerShaderState()
],
);
Widget _buildExpansionTile(
{required BuildContext context,
required LearnSubmoduleViewModel viewModel}) =>
ExpansionTile(
enabled: true,
title: _buildTitle(),
textColor: kcDarkGrey,
showTrailingIcon: true,
initiallyExpanded: true,
subtitle: _buildContent(),
leading: _buildIconWrapper(),
collapsedIconColor: kcDarkGrey,
collapsedTextColor: kcDarkGrey,
backgroundColor: kcBackgroundColor,
shape: Border.all(color: kcTransparent),
expandedAlignment: Alignment.centerLeft,
collapsedBackgroundColor: kcBackgroundColor,
controlAffinity: ListTileControlAffinity.trailing,
expandedCrossAxisAlignment: CrossAxisAlignment.start,
tilePadding: const EdgeInsets.symmetric(horizontal: 15),
childrenPadding: const EdgeInsets.fromLTRB(70, 15, 15, 15),
// enabled: status != ProgressStatuses.pending,
// showTrailingIcon: status != ProgressStatuses.pending ? true : false,
//initiallyExpanded: status == ProgressStatuses.started ? true : false,
children:
_buildExpansionTileChildren(context: context, viewModel: viewModel),
);
Widget _buildIconWrapper() => CircleAvatar(
backgroundColor: kcPrimaryColor.withOpacity(0.1),
child: _buildIcon(),
);
Widget _buildIcon() => const Icon(
Icons.lightbulb_outline,
color: kcPrimaryColor,
);
Widget _buildTitle() => Text(
submodule.title ?? '',
maxLines: 1,
softWrap: false,
style: style16P600,
overflow: TextOverflow.ellipsis,
);
Widget _buildContent() => Text(
submodule.description ?? '',
maxLines: 1,
softWrap: false,
style: style14DG400,
overflow: TextOverflow.ellipsis,
);
List<Widget> _buildExpansionTileChildren(
{required BuildContext context,
required LearnSubmoduleViewModel viewModel}) =>
[_buildExpansionTileItem(context: context, viewModel: viewModel)];
Widget _buildExpansionTileItem(
{required BuildContext context,
required LearnSubmoduleViewModel viewModel}) =>
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildExpansionTileItemChildren(
context: context, viewModel: viewModel),
);
List<Widget> _buildExpansionTileItemChildren(
{required BuildContext context,
required LearnSubmoduleViewModel viewModel}) =>
[
// _buildProgressRow(),
// verticalSpaceSmall,
_buildActionButtonWrapper(context: context, viewModel: viewModel)
];
Widget _buildProgressRow() => Row(
mainAxisSize: MainAxisSize.min,
children: _buildProgressChildren(),
);
List<Widget> _buildProgressChildren() =>
[_buildProgressStatusWrapper(), horizontalSpaceSmall, _buildProgress()];
Widget _buildProgressStatusWrapper() => Expanded(
child: _buildProgressStatus(),
);
Widget _buildProgressStatus() => const CustomLinearProgressIndicator(
progress: 0.75,
activeColor: kcPrimaryColor,
backgroundColor: kcVeryLightGrey);
Widget _buildProgress() => const Text(
'2/3',
style: TextStyle(color: kcDarkGrey),
);
Widget _buildActionButtonWrapper(
{required BuildContext context,
required LearnSubmoduleViewModel viewModel}) =>
SizedBox(
height: 40,
child: _buildActionButtons(context: context, viewModel: viewModel),
);
Widget _buildActionButtons(
{required BuildContext context,
required LearnSubmoduleViewModel viewModel}) =>
Row(
children: [
_buildLessonButtonWrapper(viewModel),
horizontalSpaceSmall,
_buildPracticeButtonWrapper(context: context, viewModel: viewModel)
],
);
Widget _buildLessonButtonWrapper(LearnSubmoduleViewModel viewModel) =>
Expanded(
child: _buildLessonButton(viewModel),
);
Widget _buildLessonButton(LearnSubmoduleViewModel viewModel) =>
CustomElevatedButton(
height: 15,
borderRadius: 12,
onTap: onLessonTap,
text: 'View Module',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
// onTap: () async => await viewModel.navigateToLearnLesson(
// title: title,
// topics: topics,
// subtitle: subtitle,
// practices: practices,
// description: description),
);
Widget _buildPracticeButtonWrapper(
{required BuildContext context,
required LearnSubmoduleViewModel viewModel}) =>
Expanded(
child: _buildPracticeButton(context: context, viewModel: viewModel),
);
Widget _buildPracticeButton(
{required BuildContext context,
required LearnSubmoduleViewModel viewModel}) =>
CustomElevatedButton(
height: 15,
borderRadius: 12,
onTap: onPracticeTap,
text: 'View Practices',
backgroundColor: kcWhite,
borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor,
// onTap: () async => await viewModel.navigateToLearnPractice(practices),
);
Widget _buildSheet(LearnSubmoduleViewModel viewModel) => FinishPracticeSheet(
onTap: viewModel.pop,
);
// Widget _buildContainerShaderState() => status == ProgressStatuses.pending
// ? _buildContainerShaderWrapper()
// : Container();
Widget _buildContainerShaderWrapper() => Positioned.fill(
child: _buildContainerShader(),
);
Widget _buildContainerShader() => Container(
decoration: BoxDecoration(
color: kcWhite.withOpacity(0.5),
borderRadius: BorderRadius.circular(5),
),
);
}

View File

@ -36,17 +36,17 @@ class ModuleProgress extends StatelessWidget {
[_buildProgressInfo(), _buildProgress()];
Widget _buildProgressInfo() => Text(
'60% Progress',
'0% Progress',
style: style16DG400,
);
Widget _buildProgress() => Text(
'2/3',
'0/3',
style: style14P400,
);
Widget _buildProgressIndicator() => const CustomLinearProgressIndicator(
progress: 0.75,
progress: 0,
activeColor: kcPrimaryColor,
backgroundColor: kcVeryLightGrey,
);

View File

@ -4,7 +4,8 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
class OverallLearnProgress extends StatelessWidget {
const OverallLearnProgress({super.key});
final Color color;
const OverallLearnProgress({super.key, required this.color});
@override
Widget build(BuildContext context) => _buildContainer();
@ -12,8 +13,8 @@ class OverallLearnProgress extends StatelessWidget {
Widget _buildContainer() => Container(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 25),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(4),
color: kcPrimaryColor.withOpacity(0.1),
),
child: _buildProgressSection(),
);

View File

@ -8,6 +8,7 @@
#include <audioplayers_linux/audioplayers_linux_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_inappwebview_linux/flutter_inappwebview_linux_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <record_linux/record_linux_plugin.h>
@ -18,6 +19,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) flutter_inappwebview_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterInappwebviewLinuxPlugin");
flutter_inappwebview_linux_plugin_register_with_registrar(flutter_inappwebview_linux_registrar);
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);

View File

@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_linux
file_selector_linux
flutter_inappwebview_linux
flutter_secure_storage_linux
record_linux
)

View File

@ -11,6 +11,7 @@ import connectivity_plus
import file_selector_macos
import firebase_core
import firebase_messaging
import flutter_inappwebview_macos
import flutter_local_notifications
import flutter_secure_storage_darwin
import google_sign_in_ios
@ -27,6 +28,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))

View File

@ -558,6 +558,78 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.0"
flutter_inappwebview:
dependency: "direct main"
description:
name: flutter_inappwebview
sha256: "3952d116ee93bad2946401377e7ade87b5ef200e95ecb5ba1affa1b6329a6867"
url: "https://pub.dev"
source: hosted
version: "6.2.0-beta.3"
flutter_inappwebview_android:
dependency: transitive
description:
name: flutter_inappwebview_android
sha256: "8dfb76bd4e507112c3942c2272eeb01fab2e42be11374e5eb226f58698e7a04b"
url: "https://pub.dev"
source: hosted
version: "1.2.0-beta.3"
flutter_inappwebview_internal_annotations:
dependency: transitive
description:
name: flutter_inappwebview_internal_annotations
sha256: e30fba942e3debea7b7e6cdd4f0f59ce89dd403a9865193e3221293b6d1544c6
url: "https://pub.dev"
source: hosted
version: "1.3.0"
flutter_inappwebview_ios:
dependency: transitive
description:
name: flutter_inappwebview_ios
sha256: ae8a78829398771be863aa3c8804a9d40728e1815e66c9c966f86d2cc3ae4fd9
url: "https://pub.dev"
source: hosted
version: "1.2.0-beta.3"
flutter_inappwebview_linux:
dependency: transitive
description:
name: flutter_inappwebview_linux
sha256: "2e1a3b09bb911fb5a8bb155cb7f1eb1428a19b6e20363b9db48beef428b8cef5"
url: "https://pub.dev"
source: hosted
version: "0.1.0-beta.1"
flutter_inappwebview_macos:
dependency: transitive
description:
name: flutter_inappwebview_macos
sha256: "545148cb5c46475ce669ab21621e9f2ad66e05f8e80b2cf49d4018879ab52393"
url: "https://pub.dev"
source: hosted
version: "1.2.0-beta.3"
flutter_inappwebview_platform_interface:
dependency: transitive
description:
name: flutter_inappwebview_platform_interface
sha256: e3522c76e6760d1c0a9ff690e30e1503f226783d3277fa4d26675911977e9766
url: "https://pub.dev"
source: hosted
version: "1.4.0-beta.3"
flutter_inappwebview_web:
dependency: transitive
description:
name: flutter_inappwebview_web
sha256: e98b8875ccb6a3fd255873318db45c18ab135ed0ed22d20169abad9f5c810eb9
url: "https://pub.dev"
source: hosted
version: "1.2.0-beta.3"
flutter_inappwebview_windows:
dependency: transitive
description:
name: flutter_inappwebview_windows
sha256: "902edd6f6326952af822e21aa928f7426d723d45c94c15e6ce3c2d5640d28ad7"
url: "https://pub.dev"
source: hosted
version: "0.7.0-beta.3"
flutter_lints:
dependency: "direct dev"
description:
@ -1741,6 +1813,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.4.0"
vimeo_video_player:
dependency: "direct main"
description:
name: vimeo_video_player
sha256: b5dc8ad763489c94136e6080ba3ee89830742a48f5e7b2e28968f54d8c3734ad
url: "https://pub.dev"
source: hosted
version: "1.0.3"
vm_service:
dependency: transitive
description:

View File

@ -1,7 +1,7 @@
name: yimaru_app
description: A new Flutter project.
publish_to: 'none'
version: 0.1.3+4
version: 0.1.3+5
environment:
sdk: '>=3.0.3 <4.0.0'
@ -38,6 +38,7 @@ dependencies:
omni_datetime_picker: any
json_serializable: ^6.8.0
waveform_recorder: ^1.8.0
vimeo_video_player: ^1.0.3
permission_handler: ^12.0.1
firebase_messaging: ^16.1.1
cached_network_image: ^3.4.1
@ -46,6 +47,7 @@ dependencies:
flutter_secure_storage: ^10.0.0
flutter_timer_countdown: ^1.0.7
flutter_carousel_widget: ^3.1.0
flutter_inappwebview: ^6.2.0-beta.3
flutter_local_notifications: ^20.1.0
internet_connection_checker_plus: ^2.9.1+2

View File

@ -8,40 +8,42 @@ import 'dart:ui' as _i10;
import 'package:audioplayers/audioplayers.dart' as _i4;
import 'package:dio/dio.dart' as _i2;
import 'package:firebase_messaging/firebase_messaging.dart' as _i32;
import 'package:firebase_messaging/firebase_messaging.dart' as _i34;
import 'package:flutter/material.dart' as _i8;
import 'package:mockito/mockito.dart' as _i1;
import 'package:mockito/src/dummies.dart' as _i7;
import 'package:permission_handler/permission_handler.dart' as _i27;
import 'package:permission_handler/permission_handler.dart' as _i29;
import 'package:stacked_services/stacked_services.dart' as _i6;
import 'package:waveform_recorder/waveform_recorder.dart' as _i5;
import 'package:yimaru_app/models/category.dart' as _i15;
import 'package:yimaru_app/models/course.dart' as _i21;
import 'package:yimaru_app/models/course_detail.dart' as _i35;
import 'package:yimaru_app/models/course_detail.dart' as _i37;
import 'package:yimaru_app/models/course_lesson.dart' as _i18;
import 'package:yimaru_app/models/course_progress.dart' as _i17;
import 'package:yimaru_app/models/lesson.dart' as _i25;
import 'package:yimaru_app/models/level.dart' as _i22;
import 'package:yimaru_app/models/module.dart' as _i23;
import 'package:yimaru_app/models/practice.dart' as _i19;
import 'package:yimaru_app/models/practice_question.dart' as _i20;
import 'package:yimaru_app/models/question.dart' as _i14;
import 'package:yimaru_app/models/subcategory.dart' as _i16;
import 'package:yimaru_app/models/submodule.dart' as _i24;
import 'package:yimaru_app/models/user.dart' as _i12;
import 'package:yimaru_app/services/api_service.dart' as _i13;
import 'package:yimaru_app/services/audio_player_service.dart' as _i36;
import 'package:yimaru_app/services/audio_player_service.dart' as _i38;
import 'package:yimaru_app/services/authentication_service.dart' as _i11;
import 'package:yimaru_app/services/course_service.dart' as _i34;
import 'package:yimaru_app/services/dio_service.dart' as _i24;
import 'package:yimaru_app/services/google_auth_service.dart' as _i29;
import 'package:yimaru_app/services/image_downloader_service.dart' as _i30;
import 'package:yimaru_app/services/image_picker_service.dart' as _i28;
import 'package:yimaru_app/services/notification_service.dart' as _i31;
import 'package:yimaru_app/services/permission_handler_service.dart' as _i26;
import 'package:yimaru_app/services/course_service.dart' as _i36;
import 'package:yimaru_app/services/dio_service.dart' as _i26;
import 'package:yimaru_app/services/google_auth_service.dart' as _i31;
import 'package:yimaru_app/services/image_downloader_service.dart' as _i32;
import 'package:yimaru_app/services/image_picker_service.dart' as _i30;
import 'package:yimaru_app/services/notification_service.dart' as _i33;
import 'package:yimaru_app/services/permission_handler_service.dart' as _i28;
import 'package:yimaru_app/services/secure_storage_service.dart' as _i3;
import 'package:yimaru_app/services/smart_auth_service.dart' as _i33;
import 'package:yimaru_app/services/status_checker_service.dart' as _i25;
import 'package:yimaru_app/services/voice_recorder_service.dart' as _i37;
import 'package:yimaru_app/ui/common/enmus.dart' as _i38;
import 'package:yimaru_app/services/smart_auth_service.dart' as _i35;
import 'package:yimaru_app/services/status_checker_service.dart' as _i27;
import 'package:yimaru_app/services/voice_recorder_service.dart' as _i39;
import 'package:yimaru_app/ui/common/enmus.dart' as _i40;
// ignore_for_file: type=lint
// ignore_for_file: avoid_redundant_argument_values
@ -1128,7 +1130,7 @@ class MockApiService extends _i1.Mock implements _i13.ApiService {
@override
_i9.Future<List<_i15.Category>> getCategories() => (super.noSuchMethod(
Invocation.method(
#getCourseCategories,
#getCategories,
[],
),
returnValue: _i9.Future<List<_i15.Category>>.value(<_i15.Category>[]),
@ -1140,7 +1142,7 @@ class MockApiService extends _i1.Mock implements _i13.ApiService {
_i9.Future<List<_i16.Subcategory>> getSubcategories(int? id) =>
(super.noSuchMethod(
Invocation.method(
#getCourseSubcategories,
#getSubcategories,
[id],
),
returnValue:
@ -1270,6 +1272,29 @@ class MockApiService extends _i1.Mock implements _i13.ApiService {
returnValueForMissingStub:
_i9.Future<List<_i23.Module>>.value(<_i23.Module>[]),
) as _i9.Future<List<_i23.Module>>);
@override
_i9.Future<List<_i24.Submodule>> getSubmodules(int? id) =>
(super.noSuchMethod(
Invocation.method(
#getSubmodules,
[id],
),
returnValue: _i9.Future<List<_i24.Submodule>>.value(<_i24.Submodule>[]),
returnValueForMissingStub:
_i9.Future<List<_i24.Submodule>>.value(<_i24.Submodule>[]),
) as _i9.Future<List<_i24.Submodule>>);
@override
_i9.Future<List<_i25.Lesson>> getLessons(int? id) => (super.noSuchMethod(
Invocation.method(
#getLessons,
[id],
),
returnValue: _i9.Future<List<_i25.Lesson>>.value(<_i25.Lesson>[]),
returnValueForMissingStub:
_i9.Future<List<_i25.Lesson>>.value(<_i25.Lesson>[]),
) as _i9.Future<List<_i25.Lesson>>);
}
/// A class which mocks [SecureStorageService].
@ -1372,7 +1397,7 @@ class MockSecureStorageService extends _i1.Mock
/// A class which mocks [DioService].
///
/// See the documentation for Mockito's code generation for more information.
class MockDioService extends _i1.Mock implements _i24.DioService {
class MockDioService extends _i1.Mock implements _i26.DioService {
@override
_i2.Dio get dio => (super.noSuchMethod(
Invocation.getter(#dio),
@ -1391,7 +1416,7 @@ class MockDioService extends _i1.Mock implements _i24.DioService {
///
/// See the documentation for Mockito's code generation for more information.
class MockStatusCheckerService extends _i1.Mock
implements _i25.StatusCheckerService {
implements _i27.StatusCheckerService {
@override
_i3.SecureStorageService get storage => (super.noSuchMethod(
Invocation.getter(#storage),
@ -1457,40 +1482,40 @@ class MockStatusCheckerService extends _i1.Mock
///
/// See the documentation for Mockito's code generation for more information.
class MockPermissionHandlerService extends _i1.Mock
implements _i26.PermissionHandlerService {
implements _i28.PermissionHandlerService {
@override
_i9.Future<_i27.PermissionStatus> requestPermission(
_i27.Permission? requestedPermission) =>
_i9.Future<_i29.PermissionStatus> requestPermission(
_i29.Permission? requestedPermission) =>
(super.noSuchMethod(
Invocation.method(
#requestPermission,
[requestedPermission],
),
returnValue: _i9.Future<_i27.PermissionStatus>.value(
_i27.PermissionStatus.denied),
returnValueForMissingStub: _i9.Future<_i27.PermissionStatus>.value(
_i27.PermissionStatus.denied),
) as _i9.Future<_i27.PermissionStatus>);
returnValue: _i9.Future<_i29.PermissionStatus>.value(
_i29.PermissionStatus.denied),
returnValueForMissingStub: _i9.Future<_i29.PermissionStatus>.value(
_i29.PermissionStatus.denied),
) as _i9.Future<_i29.PermissionStatus>);
@override
_i9.Future<_i27.PermissionStatus> request(_i27.Permission? permission) =>
_i9.Future<_i29.PermissionStatus> request(_i29.Permission? permission) =>
(super.noSuchMethod(
Invocation.method(
#request,
[permission],
),
returnValue: _i9.Future<_i27.PermissionStatus>.value(
_i27.PermissionStatus.denied),
returnValueForMissingStub: _i9.Future<_i27.PermissionStatus>.value(
_i27.PermissionStatus.denied),
) as _i9.Future<_i27.PermissionStatus>);
returnValue: _i9.Future<_i29.PermissionStatus>.value(
_i29.PermissionStatus.denied),
returnValueForMissingStub: _i9.Future<_i29.PermissionStatus>.value(
_i29.PermissionStatus.denied),
) as _i9.Future<_i29.PermissionStatus>);
}
/// A class which mocks [ImagePickerService].
///
/// See the documentation for Mockito's code generation for more information.
class MockImagePickerService extends _i1.Mock
implements _i28.ImagePickerService {
implements _i30.ImagePickerService {
@override
_i9.Future<String?> gallery() => (super.noSuchMethod(
Invocation.method(
@ -1515,7 +1540,7 @@ class MockImagePickerService extends _i1.Mock
/// A class which mocks [GoogleAuthService].
///
/// See the documentation for Mockito's code generation for more information.
class MockGoogleAuthService extends _i1.Mock implements _i29.GoogleAuthService {
class MockGoogleAuthService extends _i1.Mock implements _i31.GoogleAuthService {
@override
int get listenersCount => (super.noSuchMethod(
Invocation.getter(#listenersCount),
@ -1585,7 +1610,7 @@ class MockGoogleAuthService extends _i1.Mock implements _i29.GoogleAuthService {
///
/// See the documentation for Mockito's code generation for more information.
class MockImageDownloaderService extends _i1.Mock
implements _i30.ImageDownloaderService {
implements _i32.ImageDownloaderService {
@override
_i9.Future<String> downloader(String? networkImage) => (super.noSuchMethod(
Invocation.method(
@ -1614,7 +1639,7 @@ class MockImageDownloaderService extends _i1.Mock
///
/// See the documentation for Mockito's code generation for more information.
class MockNotificationService extends _i1.Mock
implements _i31.NotificationService {
implements _i33.NotificationService {
@override
_i9.Future<void> initialize() => (super.noSuchMethod(
Invocation.method(
@ -1636,7 +1661,7 @@ class MockNotificationService extends _i1.Mock
) as _i9.Future<void>);
@override
_i9.Future<void> showNotification(_i32.RemoteMessage? message) =>
_i9.Future<void> showNotification(_i34.RemoteMessage? message) =>
(super.noSuchMethod(
Invocation.method(
#showNotification,
@ -1670,7 +1695,7 @@ class MockNotificationService extends _i1.Mock
/// A class which mocks [SmartAuthService].
///
/// See the documentation for Mockito's code generation for more information.
class MockSmartAuthService extends _i1.Mock implements _i33.SmartAuthService {
class MockSmartAuthService extends _i1.Mock implements _i35.SmartAuthService {
@override
bool get listenForMultipleSms => (super.noSuchMethod(
Invocation.getter(#listenForMultipleSms),
@ -1702,26 +1727,26 @@ class MockSmartAuthService extends _i1.Mock implements _i33.SmartAuthService {
/// A class which mocks [CourseService].
///
/// See the documentation for Mockito's code generation for more information.
class MockCourseService extends _i1.Mock implements _i34.CourseService {
class MockCourseService extends _i1.Mock implements _i36.CourseService {
@override
_i9.Future<List<_i35.CourseDetail>> getCoursesDetail(int? id) =>
_i9.Future<List<_i37.CourseDetail>> getCoursesDetail(int? id) =>
(super.noSuchMethod(
Invocation.method(
#getCoursesDetail,
[id],
),
returnValue:
_i9.Future<List<_i35.CourseDetail>>.value(<_i35.CourseDetail>[]),
_i9.Future<List<_i37.CourseDetail>>.value(<_i37.CourseDetail>[]),
returnValueForMissingStub:
_i9.Future<List<_i35.CourseDetail>>.value(<_i35.CourseDetail>[]),
) as _i9.Future<List<_i35.CourseDetail>>);
_i9.Future<List<_i37.CourseDetail>>.value(<_i37.CourseDetail>[]),
) as _i9.Future<List<_i37.CourseDetail>>);
}
/// A class which mocks [AudioPlayerService].
///
/// See the documentation for Mockito's code generation for more information.
class MockAudioPlayerService extends _i1.Mock
implements _i36.AudioPlayerService {
implements _i38.AudioPlayerService {
@override
_i4.AudioPlayer get player => (super.noSuchMethod(
Invocation.getter(#player),
@ -1845,13 +1870,13 @@ class MockAudioPlayerService extends _i1.Mock
///
/// See the documentation for Mockito's code generation for more information.
class MockVoiceRecorderService extends _i1.Mock
implements _i37.VoiceRecorderService {
implements _i39.VoiceRecorderService {
@override
_i38.VoiceRecordingState get recordingState => (super.noSuchMethod(
_i40.VoiceRecordingState get recordingState => (super.noSuchMethod(
Invocation.getter(#recordingState),
returnValue: _i38.VoiceRecordingState.pending,
returnValueForMissingStub: _i38.VoiceRecordingState.pending,
) as _i38.VoiceRecordingState);
returnValue: _i40.VoiceRecordingState.pending,
returnValueForMissingStub: _i40.VoiceRecordingState.pending,
) as _i40.VoiceRecordingState);
@override
_i5.WaveformRecorderController get waveController => (super.noSuchMethod(

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('LearnSubmoduleViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -11,6 +11,7 @@
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <file_selector_windows/file_selector_windows.h>
#include <firebase_core/firebase_core_plugin_c_api.h>
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <record_windows/record_windows_plugin_c_api.h>
@ -26,6 +27,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FirebaseCorePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(

View File

@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
connectivity_plus
file_selector_windows
firebase_core
flutter_inappwebview_windows
flutter_secure_storage_windows
permission_handler_windows
record_windows