feat(learn): Integrate learn practice with local data

This commit is contained in:
BisratHailu 2026-03-30 17:21:17 +03:00
parent c368404a83
commit 413502db11
67 changed files with 2383 additions and 991 deletions

View File

@ -14,11 +14,11 @@
}, },
"oauth_client": [ "oauth_client": [
{ {
"client_id": "574860813475-glgnkruic7dflaomb59el8994b7hhfga.apps.googleusercontent.com", "client_id": "574860813475-3p3k63lkrfd113sn6jscgvdj0aigsg5s.apps.googleusercontent.com",
"client_type": 1, "client_type": 1,
"android_info": { "android_info": {
"package_name": "com.yimaru.lms.app", "package_name": "com.yimaru.lms.app",
"certificate_hash": "378836a3aa9f36958b6c6c69bc67e3195352f68d" "certificate_hash": "928ead08b5e39d6a861a55ae7cceb8c402d1ee7a"
} }
}, },
{ {

File diff suppressed because one or more lines are too long

View File

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-all.zip

View File

@ -19,9 +19,10 @@ pluginManagement {
plugins { plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "9.0.1" apply false id("com.android.application") version "9.1.0" apply false
id("org.jetbrains.kotlin.android") version "2.3.0" apply false id("org.jetbrains.kotlin.android") version "2.3.0" apply false
id("com.google.gms.google-services") version("4.4.4") apply false id("com.google.gms.google-services") version("4.4.4") apply false
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
} }

View File

@ -48,6 +48,8 @@ import 'package:yimaru_app/services/smart_auth_service.dart';
import 'package:yimaru_app/services/course_service.dart'; import 'package:yimaru_app/services/course_service.dart';
import 'package:yimaru_app/ui/views/course_subcategory/course_subcategory_view.dart'; import 'package:yimaru_app/ui/views/course_subcategory/course_subcategory_view.dart';
import 'package:yimaru_app/ui/views/course/course_view.dart'; import 'package:yimaru_app/ui/views/course/course_view.dart';
import 'package:yimaru_app/services/audio_player_service.dart';
import 'package:yimaru_app/services/voice_recorder_service.dart';
// @stacked-import // @stacked-import
@StackedApp( @StackedApp(
@ -104,6 +106,8 @@ import 'package:yimaru_app/ui/views/course/course_view.dart';
LazySingleton(classType: NotificationService), LazySingleton(classType: NotificationService),
LazySingleton(classType: SmartAuthService), LazySingleton(classType: SmartAuthService),
LazySingleton(classType: CourseService), LazySingleton(classType: CourseService),
LazySingleton(classType: AudioPlayerService),
LazySingleton(classType: VoiceRecorderService),
// @stacked-service // @stacked-service
], ],
bottomsheets: [ bottomsheets: [

View File

@ -12,6 +12,7 @@ import 'package:stacked_services/src/navigation/navigation_service.dart';
import 'package:stacked_shared/stacked_shared.dart'; import 'package:stacked_shared/stacked_shared.dart';
import '../services/api_service.dart'; import '../services/api_service.dart';
import '../services/audio_player_service.dart';
import '../services/authentication_service.dart'; import '../services/authentication_service.dart';
import '../services/course_service.dart'; import '../services/course_service.dart';
import '../services/dio_service.dart'; import '../services/dio_service.dart';
@ -23,6 +24,7 @@ import '../services/permission_handler_service.dart';
import '../services/secure_storage_service.dart'; import '../services/secure_storage_service.dart';
import '../services/smart_auth_service.dart'; import '../services/smart_auth_service.dart';
import '../services/status_checker_service.dart'; import '../services/status_checker_service.dart';
import '../services/voice_recorder_service.dart';
final locator = StackedLocator.instance; final locator = StackedLocator.instance;
@ -50,4 +52,6 @@ Future<void> setupLocator({
locator.registerLazySingleton(() => NotificationService()); locator.registerLazySingleton(() => NotificationService());
locator.registerLazySingleton(() => SmartAuthService()); locator.registerLazySingleton(() => SmartAuthService());
locator.registerLazySingleton(() => CourseService()); locator.registerLazySingleton(() => CourseService());
locator.registerLazySingleton(() => AudioPlayerService());
locator.registerLazySingleton(() => VoiceRecorderService());
} }

View File

@ -445,8 +445,15 @@ class StackedRouter extends _i1.RouterBase {
); );
}, },
_i23.LearnLessonView: (data) { _i23.LearnLessonView: (data) {
final args = data.getArgs<LearnLessonViewArguments>(nullOk: false);
return _i36.MaterialPageRoute<dynamic>( return _i36.MaterialPageRoute<dynamic>(
builder: (context) => const _i23.LearnLessonView(), builder: (context) => _i23.LearnLessonView(
key: args.key,
title: args.title,
topics: args.topics,
subtitle: args.subtitle,
practices: args.practices,
description: args.description),
settings: data, settings: data,
); );
}, },
@ -457,8 +464,13 @@ class StackedRouter extends _i1.RouterBase {
); );
}, },
_i25.LearnLessonDetailView: (data) { _i25.LearnLessonDetailView: (data) {
final args = data.getArgs<LearnLessonDetailViewArguments>(nullOk: false);
return _i36.MaterialPageRoute<dynamic>( return _i36.MaterialPageRoute<dynamic>(
builder: (context) => const _i25.LearnLessonDetailView(), builder: (context) => _i25.LearnLessonDetailView(
key: args.key,
title: args.title,
practices: args.practices,
description: args.description),
settings: data, settings: data,
); );
}, },
@ -469,6 +481,7 @@ class StackedRouter extends _i1.RouterBase {
key: args.key, key: args.key,
title: args.title, title: args.title,
subtitle: args.subtitle, subtitle: args.subtitle,
practices: args.practices,
buttonLabel: args.buttonLabel), buttonLabel: args.buttonLabel),
settings: data, settings: data,
); );
@ -604,11 +617,100 @@ class AssessmentViewArguments {
} }
} }
class LearnLessonViewArguments {
const LearnLessonViewArguments({
this.key,
required this.title,
required this.topics,
required this.subtitle,
required this.practices,
required this.description,
});
final _i36.Key? key;
final String title;
final String topics;
final String subtitle;
final List<Map<String, dynamic>> practices;
final String description;
@override
String toString() {
return '{"key": "$key", "title": "$title", "topics": "$topics", "subtitle": "$subtitle", "practices": "$practices", "description": "$description"}';
}
@override
bool operator ==(covariant LearnLessonViewArguments other) {
if (identical(this, other)) return true;
return other.key == key &&
other.title == title &&
other.topics == topics &&
other.subtitle == subtitle &&
other.practices == practices &&
other.description == description;
}
@override
int get hashCode {
return key.hashCode ^
title.hashCode ^
topics.hashCode ^
subtitle.hashCode ^
practices.hashCode ^
description.hashCode;
}
}
class LearnLessonDetailViewArguments {
const LearnLessonDetailViewArguments({
this.key,
required this.title,
required this.practices,
required this.description,
});
final _i36.Key? key;
final String title;
final List<Map<String, dynamic>> practices;
final String description;
@override
String toString() {
return '{"key": "$key", "title": "$title", "practices": "$practices", "description": "$description"}';
}
@override
bool operator ==(covariant LearnLessonDetailViewArguments other) {
if (identical(this, other)) return true;
return other.key == key &&
other.title == title &&
other.practices == practices &&
other.description == description;
}
@override
int get hashCode {
return key.hashCode ^
title.hashCode ^
practices.hashCode ^
description.hashCode;
}
}
class LearnPracticeViewArguments { class LearnPracticeViewArguments {
const LearnPracticeViewArguments({ const LearnPracticeViewArguments({
this.key, this.key,
required this.title, required this.title,
required this.subtitle, required this.subtitle,
required this.practices,
required this.buttonLabel, required this.buttonLabel,
}); });
@ -618,11 +720,13 @@ class LearnPracticeViewArguments {
final String subtitle; final String subtitle;
final List<Map<String, dynamic>> practices;
final String buttonLabel; final String buttonLabel;
@override @override
String toString() { String toString() {
return '{"key": "$key", "title": "$title", "subtitle": "$subtitle", "buttonLabel": "$buttonLabel"}'; return '{"key": "$key", "title": "$title", "subtitle": "$subtitle", "practices": "$practices", "buttonLabel": "$buttonLabel"}';
} }
@override @override
@ -631,6 +735,7 @@ class LearnPracticeViewArguments {
return other.key == key && return other.key == key &&
other.title == title && other.title == title &&
other.subtitle == subtitle && other.subtitle == subtitle &&
other.practices == practices &&
other.buttonLabel == buttonLabel; other.buttonLabel == buttonLabel;
} }
@ -639,6 +744,7 @@ class LearnPracticeViewArguments {
return key.hashCode ^ return key.hashCode ^
title.hashCode ^ title.hashCode ^
subtitle.hashCode ^ subtitle.hashCode ^
practices.hashCode ^
buttonLabel.hashCode; buttonLabel.hashCode;
} }
} }
@ -1133,14 +1239,27 @@ extension NavigatorStateExtension on _i41.NavigationService {
transition: transition); transition: transition);
} }
Future<dynamic> navigateToLearnLessonView([ Future<dynamic> navigateToLearnLessonView({
_i36.Key? key,
required String title,
required String topics,
required String subtitle,
required List<Map<String, dynamic>> practices,
required String description,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
Map<String, String>? parameters, Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)? Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition, transition,
]) async { }) async {
return navigateTo<dynamic>(Routes.learnLessonView, return navigateTo<dynamic>(Routes.learnLessonView,
arguments: LearnLessonViewArguments(
key: key,
title: title,
topics: topics,
subtitle: subtitle,
practices: practices,
description: description),
id: routerId, id: routerId,
preventDuplicates: preventDuplicates, preventDuplicates: preventDuplicates,
parameters: parameters, parameters: parameters,
@ -1161,14 +1280,23 @@ extension NavigatorStateExtension on _i41.NavigationService {
transition: transition); transition: transition);
} }
Future<dynamic> navigateToLearnLessonDetailView([ Future<dynamic> navigateToLearnLessonDetailView({
_i36.Key? key,
required String title,
required List<Map<String, dynamic>> practices,
required String description,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
Map<String, String>? parameters, Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)? Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition, transition,
]) async { }) async {
return navigateTo<dynamic>(Routes.learnLessonDetailView, return navigateTo<dynamic>(Routes.learnLessonDetailView,
arguments: LearnLessonDetailViewArguments(
key: key,
title: title,
practices: practices,
description: description),
id: routerId, id: routerId,
preventDuplicates: preventDuplicates, preventDuplicates: preventDuplicates,
parameters: parameters, parameters: parameters,
@ -1179,6 +1307,7 @@ extension NavigatorStateExtension on _i41.NavigationService {
_i36.Key? key, _i36.Key? key,
required String title, required String title,
required String subtitle, required String subtitle,
required List<Map<String, dynamic>> practices,
required String buttonLabel, required String buttonLabel,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -1191,6 +1320,7 @@ extension NavigatorStateExtension on _i41.NavigationService {
key: key, key: key,
title: title, title: title,
subtitle: subtitle, subtitle: subtitle,
practices: practices,
buttonLabel: buttonLabel), buttonLabel: buttonLabel),
id: routerId, id: routerId,
preventDuplicates: preventDuplicates, preventDuplicates: preventDuplicates,
@ -1645,14 +1775,27 @@ extension NavigatorStateExtension on _i41.NavigationService {
transition: transition); transition: transition);
} }
Future<dynamic> replaceWithLearnLessonView([ Future<dynamic> replaceWithLearnLessonView({
_i36.Key? key,
required String title,
required String topics,
required String subtitle,
required List<Map<String, dynamic>> practices,
required String description,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
Map<String, String>? parameters, Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)? Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition, transition,
]) async { }) async {
return replaceWith<dynamic>(Routes.learnLessonView, return replaceWith<dynamic>(Routes.learnLessonView,
arguments: LearnLessonViewArguments(
key: key,
title: title,
topics: topics,
subtitle: subtitle,
practices: practices,
description: description),
id: routerId, id: routerId,
preventDuplicates: preventDuplicates, preventDuplicates: preventDuplicates,
parameters: parameters, parameters: parameters,
@ -1673,14 +1816,23 @@ extension NavigatorStateExtension on _i41.NavigationService {
transition: transition); transition: transition);
} }
Future<dynamic> replaceWithLearnLessonDetailView([ Future<dynamic> replaceWithLearnLessonDetailView({
_i36.Key? key,
required String title,
required List<Map<String, dynamic>> practices,
required String description,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
Map<String, String>? parameters, Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)? Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition, transition,
]) async { }) async {
return replaceWith<dynamic>(Routes.learnLessonDetailView, return replaceWith<dynamic>(Routes.learnLessonDetailView,
arguments: LearnLessonDetailViewArguments(
key: key,
title: title,
practices: practices,
description: description),
id: routerId, id: routerId,
preventDuplicates: preventDuplicates, preventDuplicates: preventDuplicates,
parameters: parameters, parameters: parameters,
@ -1691,6 +1843,7 @@ extension NavigatorStateExtension on _i41.NavigationService {
_i36.Key? key, _i36.Key? key,
required String title, required String title,
required String subtitle, required String subtitle,
required List<Map<String, dynamic>> practices,
required String buttonLabel, required String buttonLabel,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -1703,6 +1856,7 @@ extension NavigatorStateExtension on _i41.NavigationService {
key: key, key: key,
title: title, title: title,
subtitle: subtitle, subtitle: subtitle,
practices: practices,
buttonLabel: buttonLabel), buttonLabel: buttonLabel),
id: routerId, id: routerId,
preventDuplicates: preventDuplicates, preventDuplicates: preventDuplicates,

View File

@ -64,7 +64,7 @@ class DefaultFirebaseOptions {
projectId: 'yimaru-lms-e834e', projectId: 'yimaru-lms-e834e',
storageBucket: 'yimaru-lms-e834e.firebasestorage.app', storageBucket: 'yimaru-lms-e834e.firebasestorage.app',
androidClientId: androidClientId:
'574860813475-glgnkruic7dflaomb59el8994b7hhfga.apps.googleusercontent.com', '574860813475-3p3k63lkrfd113sn6jscgvdj0aigsg5s.apps.googleusercontent.com',
iosBundleId: 'com.yimaru.lms.app', iosBundleId: 'com.yimaru.lms.app',
); );
} }

View File

@ -25,12 +25,16 @@ class Practice {
@JsonKey(name: 'shuffle_questions') @JsonKey(name: 'shuffle_questions')
final bool? shuffleQuestions; final bool? shuffleQuestions;
const Practice(
{this.id,
const Practice({ this.title,
this.id, this.status,
this.title,this.status,this.setType,this.persona,this.ownerId,this.ownerType,this.description,this.shuffleQuestions this.setType,
}); this.persona,
this.ownerId,
this.ownerType,
this.description,
this.shuffleQuestions});
factory Practice.fromJson(Map<String, dynamic> json) => factory Practice.fromJson(Map<String, dynamic> json) =>
_$PracticeFromJson(json); _$PracticeFromJson(json);

View File

@ -512,14 +512,13 @@ class ApiService {
} }
} }
// Course practices // Course practices
Future<List<Practice>> getCoursePractices(Map<String, dynamic> data) async { Future<List<Practice>> getCoursePractices(Map<String, dynamic> data) async {
try { try {
List<Practice> coursePractices = []; List<Practice> coursePractices = [];
final Response response = await _service.dio.get( final Response response = await _service.dio
'$kBaseUrl/$kPracticeBaseUrl/$kCoursePractice',data: data); .get('$kBaseUrl/$kPracticeBaseUrl/$kCoursePractice', data: data);
if (response.statusCode == 200) { if (response.statusCode == 200) {
var data = response.data; var data = response.data;
@ -537,14 +536,13 @@ class ApiService {
} }
} }
// Course practic questions // Course practic questions
Future<List<PracticeQuestion>> getCoursePracticeQuestions(int id) async { Future<List<PracticeQuestion>> getCoursePracticeQuestions(int id) async {
try { try {
List<PracticeQuestion> coursePracticeQuestions = []; List<PracticeQuestion> coursePracticeQuestions = [];
final Response response = await _service.dio.get( final Response response = await _service.dio
'$kBaseUrl/$kPracticeBaseUrl/$id/$kCoursePracticeQuestions'); .get('$kBaseUrl/$kPracticeBaseUrl/$id/$kCoursePracticeQuestions');
if (response.statusCode == 200) { if (response.statusCode == 200) {
var data = response.data; var data = response.data;

View File

@ -0,0 +1,41 @@
import 'package:audioplayers/audioplayers.dart';
import 'package:stacked/stacked.dart';
import '../ui/common/helper_functions.dart';
class AudioPlayerService with ListenableServiceMixin {
final AudioPlayer _player = AudioPlayer();
AudioPlayer get player => _player;
AudioPlayerService() {
_player.setReleaseMode(ReleaseMode.stop);
}
// Streams
Stream<Duration> get positionStream => _player.onPositionChanged;
Stream<Duration> get durationStream => _player.onDurationChanged;
// Optional: player state
Stream<PlayerState> get stateStream => _player.onPlayerStateChanged;
Future<void> playUrl(String url) async {
final playableUrl = getPlayableUrl(url);
if (playableUrl == null) {
throw Exception("Invalid audio URL");
}
await _player.play(UrlSource(playableUrl));
}
Future<void> playLocal(String url) async {
await _player.play(UrlSource(url));
}
Future<void> pause() async => await _player.pause();
Future<void> seek(Duration position) async => await _player.seek(position);
}

View File

@ -0,0 +1,35 @@
import 'package:stacked/stacked.dart';
import 'package:waveform_recorder/waveform_recorder.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
class VoiceRecorderService with ListenableServiceMixin {
VoiceRecordingState _recordingState = VoiceRecordingState.pending;
VoiceRecordingState get recordingState => _recordingState;
final WaveformRecorderController _waveController =
WaveformRecorderController();
WaveformRecorderController get waveController => _waveController;
Future<void> startRecording() async {
await _waveController.startRecording();
_recordingState = VoiceRecordingState.recording;
notifyListeners();
}
Future<void> stopRecording() async {
await _waveController.stopRecording();
_recordingState = VoiceRecordingState.pending;
notifyListeners();
}
Future<String?> getRecordedAudio() async {
final file = _waveController.file;
print('RECORDED $file');
if (file == null) return null;
return file.path;
}
}

View File

@ -19,7 +19,6 @@ String kSubcoursesUrl = 'sub-courses';
String kCompleteLessonUrl = 'complete'; String kCompleteLessonUrl = 'complete';
String kResetPassword = 'resetPassword'; String kResetPassword = 'resetPassword';
String kCourseCategoryUrl = 'categories'; String kCourseCategoryUrl = 'categories';

View File

@ -1,11 +1,14 @@
// Login method // Login method
enum LoginMethod { phone, email, google } enum LoginMethod { phone, email, google }
// Response status
enum ResponseStatus { success, failure }
// Sign-up method // Sign-up method
enum SignUpMethod { phone, email, google } enum SignUpMethod { phone, email, google }
// Response status // Voice recording state
enum ResponseStatus { success, failure } enum VoiceRecordingState { pending, recording }
// Levels // Levels
enum ProficiencyLevels { a1, a2, b1, b2, none } enum ProficiencyLevels { a1, a2, b1, b2, none }
@ -18,6 +21,7 @@ enum DuolingoAssessmentType { speaking, reading, writing, listening }
// State object // State object
enum StateObjects { enum StateObjects {
none,
courses, courses,
homeView, homeView,
register, register,
@ -37,5 +41,10 @@ enum StateObjects {
courseCategories, courseCategories,
profileCompletion, profileCompletion,
registerWithGoogle, registerWithGoogle,
learnPracticeSample,
learnPracticeAnswer,
loginWithPhoneNumber, loginWithPhoneNumber,
learnPracticeQuestion,
recordLearnPracticeAnswer,
} }

View File

@ -43,3 +43,28 @@ Color getColor() {
return kcAquamarine.withValues(alpha: 0.2); return kcAquamarine.withValues(alpha: 0.2);
} }
} }
String? getPlayableUrl(String url) {
try {
// Case 1: /file/d/FILE_ID/view
final fileIdRegex = RegExp(r'/file/d/([a-zA-Z0-9_-]+)');
final match1 = fileIdRegex.firstMatch(url);
if (match1 != null) {
final fileId = match1.group(1);
return "https://drive.google.com/uc?export=download&id=$fileId";
}
// Case 2: open?id=FILE_ID
final uri = Uri.parse(url);
if (uri.queryParameters.containsKey('id')) {
final fileId = uri.queryParameters['id'];
return "https://drive.google.com/uc?export=download&id=$fileId";
}
// Already converted or normal URL
return url;
} catch (e) {
return null;
}
}

View File

@ -237,6 +237,8 @@ class AssessmentViewModel extends BaseViewModel {
} }
// Navigation // Navigation
void pop() => _navigationService.back();
Future<void> navigateToLanguage() async => Future<void> navigateToLanguage() async =>
await _navigationService.navigateToLanguageView(); await _navigationService.navigateToLanguageView();

View File

@ -26,6 +26,7 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
isLoading: viewModel.isBusy, isLoading: viewModel.isBusy,
isEmpty: viewModel.assessments.isEmpty, isEmpty: viewModel.assessments.isEmpty,
onTap: () async => await viewModel.getAssessments(), onTap: () async => await viewModel.getAssessments(),
onPop: viewModel.assessments.isEmpty ? viewModel.pop : null,
); );
Widget _buildAssessmentScreensWrapper(AssessmentViewModel viewModel) => Widget _buildAssessmentScreensWrapper(AssessmentViewModel viewModel) =>

View File

@ -8,9 +8,14 @@ import '../../../widgets/refresh_button.dart';
class AssessmentLoadingScreen extends StatelessWidget { class AssessmentLoadingScreen extends StatelessWidget {
final bool isEmpty; final bool isEmpty;
final bool isLoading; final bool isLoading;
final GestureTapCallback? onPop;
final GestureTapCallback? onTap; final GestureTapCallback? onTap;
const AssessmentLoadingScreen( const AssessmentLoadingScreen(
{super.key, this.onTap, required this.isEmpty, required this.isLoading}); {super.key,
this.onTap,
this.onPop,
required this.isEmpty,
required this.isLoading});
@override @override
Widget build(BuildContext context) => _buildScaffoldWrapper(); Widget build(BuildContext context) => _buildScaffoldWrapper();
@ -35,7 +40,8 @@ class AssessmentLoadingScreen extends StatelessWidget {
List<Widget> _buildColumnChildren() => [_buildAppBar(), _buildBody()]; List<Widget> _buildColumnChildren() => [_buildAppBar(), _buildBody()];
Widget _buildAppBar() => const LargeAppBar( Widget _buildAppBar() => LargeAppBar(
onPop: onPop,
showBackButton: true, showBackButton: true,
showLanguageSelection: true, showLanguageSelection: true,
); );

View File

@ -51,7 +51,6 @@ class CourseCategoryViewModel extends ReactiveViewModel {
_categories = await _apiService.getCourseCategories(); _categories = await _apiService.getCourseCategories();
rebuildUi(); rebuildUi();
} }
} }
} }

View File

@ -32,14 +32,10 @@ class CoursePracticeViewModel extends BaseViewModel {
Future<void> _getCoursePractice(int id) async { Future<void> _getCoursePractice(int id) async {
if (await _statusChecker.checkConnection()) { if (await _statusChecker.checkConnection()) {
Map<String,dynamic> data = { Map<String, dynamic> data = {'owner_id': id, 'owner_type': 'SUB_COURSE'};
'owner_id':id,
'owner_type':'SUB_COURSE'
};
_coursePractices = await _apiService.getCoursePractices(data); _coursePractices = await _apiService.getCoursePractices(data);
rebuildUi(); rebuildUi();
} }
} }
} }

View File

@ -42,7 +42,6 @@ class CourseSubcategoryViewModel extends BaseViewModel {
_subcategories = await _apiService.getCourseSubcategories(id); _subcategories = await _apiService.getCourseSubcategories(id);
rebuildUi(); rebuildUi();
} }
} }
} }

View File

@ -195,7 +195,7 @@ class DuolingoView extends StackedView<DuolingoViewModel> with $DuolingoView {
); );
Widget _buildBody(DuolingoViewModel viewModel) => IndexedStack( Widget _buildBody(DuolingoViewModel viewModel) => IndexedStack(
index: viewModel.currentIndex, children: _buildScreens(viewModel)); index: viewModel.currentPage, children: _buildScreens(viewModel));
List<Widget> _buildScreens(DuolingoViewModel viewModel) => [ List<Widget> _buildScreens(DuolingoViewModel viewModel) => [
_buildDuolingoAssessmentsScreen(), _buildDuolingoAssessmentsScreen(),

View File

@ -18,9 +18,9 @@ class DuolingoViewModel extends FormViewModel {
bool get isSpeaking => _isSpeaking; bool get isSpeaking => _isSpeaking;
// In-app navigation // In-app navigation
int _currentIndex = 0; int _currentPage = 0;
int get currentIndex => _currentIndex; int get currentPage => _currentPage;
// Assessments // Assessments
Map<String, dynamic> _selectedAssessment = { Map<String, dynamic> _selectedAssessment = {
@ -200,15 +200,15 @@ class DuolingoViewModel extends FormViewModel {
// In-app navigation // In-app navigation
void goTo(int page) { void goTo(int page) {
_currentIndex = page; _currentPage = page;
rebuildUi(); rebuildUi();
} }
void goBack() { void goBack() {
if (_currentIndex == 0) { if (_currentPage == 0) {
pop(); pop();
} else { } else {
_currentIndex--; _currentPage--;
rebuildUi(); rebuildUi();
} }
} }

View File

@ -17,6 +17,7 @@ class HomeView extends StackedView<HomeViewModel> {
@override @override
void onViewModelReady(HomeViewModel viewModel) async { void onViewModelReady(HomeViewModel viewModel) async {
// Removable
await _init(viewModel); await _init(viewModel);
super.onViewModelReady(viewModel); super.onViewModelReady(viewModel);
} }
@ -37,7 +38,7 @@ class HomeView extends StackedView<HomeViewModel> {
Widget _buildStartUpView() => const StartupView(label: 'Checking user info'); Widget _buildStartUpView() => const StartupView(label: 'Checking user info');
Widget _buildScaffold(HomeViewModel viewModel) => Scaffold( Widget _buildScaffold(HomeViewModel viewModel) => Scaffold(
body: getViewForIndex(viewModel.currentIndex), body: getViewForIndex(viewModel.currentPage),
bottomNavigationBar: _buildBottomNav(viewModel), bottomNavigationBar: _buildBottomNav(viewModel),
); );
@ -47,7 +48,7 @@ class HomeView extends StackedView<HomeViewModel> {
selectedItemColor: kcPrimaryColor, selectedItemColor: kcPrimaryColor,
backgroundColor: kcBackgroundColor, backgroundColor: kcBackgroundColor,
type: BottomNavigationBarType.fixed, type: BottomNavigationBarType.fixed,
currentIndex: viewModel.currentIndex, currentIndex: viewModel.currentPage,
); );
List<BottomNavigationBarItem> _buildNavBarItems() => [ List<BottomNavigationBarItem> _buildNavBarItems() => [

View File

@ -33,13 +33,13 @@ class HomeViewModel extends ReactiveViewModel {
UserModel? get user => _user; UserModel? get user => _user;
// Bottom navigation // Bottom navigation
int _currentIndex = 0; int _currentPage = 0;
int get currentIndex => _currentIndex; int get currentPage => _currentPage;
// Bottom navigation // Bottom navigation
void setCurrentIndex(int index) { void setCurrentIndex(int index) {
_currentIndex = index; _currentPage = index;
rebuildUi(); rebuildUi();
} }

View File

@ -23,19 +23,19 @@ class LearnViewModel extends ReactiveViewModel {
final List<Map<String, dynamic>> _learnLevels = [ final List<Map<String, dynamic>> _learnLevels = [
{ {
'title': 'Beginner', 'title': 'Beginner',
'status': ProgressStatuses.completed, 'status': ProgressStatuses.started,
'subtitle': 'Start your journey with the basics of English.', 'subtitle': 'Start your journey with the basics of English.',
}, },
{ // {
'title': 'Intermediate', // 'title': 'Intermediate',
'status': ProgressStatuses.started, // 'status': ProgressStatuses.started,
'subtitle': 'Practice real conversations and expand vocabulary.', // 'subtitle': 'Practice real conversations and expand vocabulary.',
}, // },
{ // {
'title': 'Advanced', // 'title': 'Advanced',
'status': ProgressStatuses.pending, // 'status': ProgressStatuses.pending,
'subtitle': 'Achieve fluency and master complex topics.', // 'subtitle': 'Achieve fluency and master complex topics.',
}, // },
]; ];
List<Map<String, dynamic>> get learnLevels => _learnLevels; List<Map<String, dynamic>> get learnLevels => _learnLevels;

View File

@ -12,12 +12,30 @@ import '../../widgets/small_app_bar.dart';
import 'learn_lesson_viewmodel.dart'; import 'learn_lesson_viewmodel.dart';
class LearnLessonView extends StackedView<LearnLessonViewModel> { class LearnLessonView extends StackedView<LearnLessonViewModel> {
const LearnLessonView({Key? key}) : super(key: key); final String title;
final String topics;
final String subtitle;
final String description;
final List<Map<String, dynamic>> practices;
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;
return SizedBox(
height: half + 275 - half,
);
}
@override @override
LearnLessonViewModel viewModelBuilder( LearnLessonViewModel viewModelBuilder(BuildContext context) =>
BuildContext context,
) =>
LearnLessonViewModel(); LearnLessonViewModel();
@override @override
@ -26,27 +44,38 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
LearnLessonViewModel viewModel, LearnLessonViewModel viewModel,
Widget? child, Widget? child,
) => ) =>
_buildScaffoldWrapper(viewModel); _buildScaffoldWrapper(context: context, viewModel: viewModel);
Widget _buildScaffoldWrapper(LearnLessonViewModel viewModel) => Scaffold( Widget _buildScaffoldWrapper(
{required BuildContext context,
required LearnLessonViewModel viewModel}) =>
Scaffold(
backgroundColor: kcBackgroundColor, backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel), body: _buildScaffold(context: context, viewModel: viewModel),
); );
Widget _buildScaffold(LearnLessonViewModel viewModel) => Widget _buildScaffold(
SafeArea(child: _buildBody(viewModel)); {required BuildContext context,
required LearnLessonViewModel viewModel}) =>
SafeArea(child: _buildBody(context: context, viewModel: viewModel));
Widget _buildBody(LearnLessonViewModel viewModel) => Padding( Widget _buildBody(
{required BuildContext context,
required LearnLessonViewModel viewModel}) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel), child: _buildColumn(context: context, viewModel: viewModel),
); );
Widget _buildColumn(LearnLessonViewModel viewModel) => Column( Widget _buildColumn(
{required BuildContext context,
required LearnLessonViewModel viewModel}) =>
Column(
children: [ children: [
verticalSpaceMedium, verticalSpaceMedium,
_buildAppBar(viewModel), _buildAppBar(viewModel),
verticalSpaceMedium, verticalSpaceMedium,
_buildLevelsColumnWrapper(viewModel), _buildLevelsColumnWrapper(context: context, viewModel: viewModel),
], ],
); );
@ -55,62 +84,99 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
showBackButton: true, showBackButton: true,
); );
Widget _buildLevelsColumnWrapper(LearnLessonViewModel viewModel) => Widget _buildLevelsColumnWrapper(
Expanded(child: _buildLevelsColumnScrollView(viewModel)); {required BuildContext context,
required LearnLessonViewModel viewModel}) =>
Expanded(
child: _buildLevelsColumnScrollView(
context: context, viewModel: viewModel));
Widget _buildLevelsColumnScrollView(LearnLessonViewModel viewModel) => Widget _buildLevelsColumnScrollView(
{required BuildContext context,
required LearnLessonViewModel viewModel}) =>
SingleChildScrollView( SingleChildScrollView(
child: _buildLevelsColumn(viewModel), child: _buildLevelsColumn(context: context, viewModel: viewModel),
); );
Widget _buildLevelsColumn(LearnLessonViewModel viewModel) => Column( Widget _buildLevelsColumn(
{required BuildContext context,
required LearnLessonViewModel viewModel}) =>
Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: _buildLevelsColumnChildren(viewModel), children:
_buildLevelsColumnChildren(context: context, viewModel: viewModel),
); );
List<Widget> _buildLevelsColumnChildren(LearnLessonViewModel viewModel) => [ List<Widget> _buildLevelsColumnChildren(
{required BuildContext context,
required LearnLessonViewModel viewModel}) =>
[
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceTiny, verticalSpaceTiny,
_buildSubtitle(), _buildSubtitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildModuleProgress(), _buildTopics(),
verticalSpaceMedium, verticalSpaceSmall,
_buildContinueButton(), // _buildModuleProgress(),
verticalSpaceMedium, // verticalSpaceMedium,
_buildMotivationCard(), // _buildContinueButton(),
verticalSpaceMedium, // verticalSpaceMedium,
_buildHeader(), // _buildMotivationCard(),
verticalSpaceMedium, // verticalSpaceMedium,
_buildListView(viewModel), //_buildHeader(),
verticalSpaceMedium //verticalSpaceMedium,
// _buildListView(viewModel),
getPadding(context),
_buildStartButton(viewModel),
verticalSpaceSmall,
_buildPracticeButton(viewModel)
]; ];
Widget _buildTitle() => Text( Widget _buildTitle() => Text(
'Module 1: Greetings & Introductions', title,
style: style16DG600, style: style16DG600,
); );
Widget _buildSubtitle() => Text( Widget _buildSubtitle() => Text(
'Learn how to introduce yourself, talk about your surroundings, and start simple conversations.', subtitle,
style: style14DG400, style: style14DG600,
);
Widget _buildTopics() => Text(
topics,
style: style14DG500,
); );
Widget _buildModuleProgress() => const ModuleProgress(); Widget _buildModuleProgress() => const ModuleProgress();
Widget _buildContinueButton() => const CustomElevatedButton( Widget _buildStartButton(LearnLessonViewModel viewModel) =>
CustomElevatedButton(
height: 55, height: 55,
borderRadius: 12, borderRadius: 12,
text: 'Start $title',
foregroundColor: kcWhite, foregroundColor: kcWhite,
text: 'Continue Lesson 1.3',
backgroundColor: kcPrimaryColor, 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 _buildMotivationCard() => const MotivationCard();
Widget _buildHeader() => Text( Widget _buildHeader() => Text(
'Module 1: Greetings & Introductions', title,
style: style18DG700, style: style18DG700,
); );
@ -122,9 +188,9 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
title: viewModel.lessons[index]['title'], title: viewModel.lessons[index]['title'],
status: viewModel.lessons[index]['status'], status: viewModel.lessons[index]['status'],
thumbnail: viewModel.lessons[index]['thumbnail'], thumbnail: viewModel.lessons[index]['thumbnail'],
onLessonTap: () async => onLessonTap: () async => await viewModel.navigateToLearnLessonDetail(
await viewModel.navigateToLearnLessonDetail(), title: title, practices: practices, description: description),
onPracticeTap: () async => await viewModel.navigateToLearnPractice(), // onPracticeTap: () async => await viewModel.navigateToLearnPractice(),
), ),
); );

View File

@ -32,14 +32,19 @@ class LearnLessonViewModel extends BaseViewModel {
// Navigation // Navigation
void pop() => _navigationService.back(); void pop() => _navigationService.back();
Future<void> navigateToLearnLessonDetail() async => Future<void> navigateToLearnLessonDetail(
await _navigationService.navigateToLearnLessonDetailView(); {required String title,
required List<Map<String, dynamic>> practices,
required String description}) async =>
await _navigationService.navigateToLearnLessonDetailView(
title: title, practices: practices, description: description);
Future<void> navigateToLearnPractice() async => Future<void> navigateToLearnPractice(
List<Map<String, dynamic>> practices) async =>
await _navigationService.navigateToLearnPracticeView( await _navigationService.navigateToLearnPracticeView(
buttonLabel: 'Start Practice', practices: practices,
title: 'Let \'s practice what you just learnt!', title: 'Lets Practice',
subtitle: buttonLabel: 'Begin Lesson Practice',
'Ill ask you a few questions, and you can respond naturally.', subtitle: 'Lets quickly review what youve learned in this lesson!',
); );
} }

View File

@ -11,19 +11,28 @@ import '../../widgets/small_app_bar.dart';
import 'learn_lesson_detail_viewmodel.dart'; import 'learn_lesson_detail_viewmodel.dart';
class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> { class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
const LearnLessonDetailView({Key? key}) : super(key: key); final String title;
final String description;
final List<Map<String, dynamic>> practices;
const LearnLessonDetailView(
{Key? key,
required this.title,
required this.practices,
required this.description})
: super(key: key);
Future<void> _navigate(LearnLessonDetailViewModel viewModel) async { Future<void> _navigate(LearnLessonDetailViewModel viewModel) async {
await viewModel.pause(); await viewModel.pause();
await viewModel.navigateToLearnPractice(); await viewModel.navigateToLearnPractice(practices);
} }
// @override @override
// void onDispose(LearnLessonDetailViewModel viewModel) { void onDispose(LearnLessonDetailViewModel viewModel) {
// print('DISPOSED'); print('DISPOSED');
// viewModel.dispose(); viewModel.close();
// super.onDispose(viewModel); super.onDispose(viewModel);
// } }
@override @override
void onViewModelReady(LearnLessonDetailViewModel viewModel) async { void onViewModelReady(LearnLessonDetailViewModel viewModel) async {
@ -116,7 +125,7 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
); );
Widget _buildTitle() => Text( Widget _buildTitle() => Text(
'1.3 Common Greetings', title,
style: style16DG600, style: style16DG600,
); );
@ -149,7 +158,7 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
); );
Widget _buildDescription() => Text( Widget _buildDescription() => Text(
'In this lesson, youll explore how to start simple conversations by greeting others in polite and friendly ways. Youll practice different greetings for morning, afternoon, and evening, as well as casual and formal situations. By the end, youll know how to confidently say hello, ask how someone is, and respond naturally.', description,
style: style14DG600, style: style14DG600,
); );

View File

@ -55,18 +55,18 @@ class LearnLessonDetailViewModel extends BaseViewModel {
await _chewieController?.pause(); await _chewieController?.pause();
} }
@override void close() {
void dispose() {
_videoPlayerController?.dispose(); _videoPlayerController?.dispose();
_chewieController?.dispose(); _chewieController?.dispose();
super.dispose();
} }
// Navigation // Navigation
void pop() => _navigationService.back(); void pop() => _navigationService.back();
Future<void> navigateToLearnPractice() async => Future<void> navigateToLearnPractice(
List<Map<String, dynamic>> practices) async =>
await _navigationService.navigateToLearnPracticeView( await _navigationService.navigateToLearnPracticeView(
practices: practices,
buttonLabel: 'Start Practice', buttonLabel: 'Start Practice',
title: 'Let \'s practice what you just learnt!', title: 'Let \'s practice what you just learnt!',
subtitle: subtitle:

View File

@ -13,11 +13,11 @@ class LearnLevelViewModel extends BaseViewModel {
'current': true, 'current': true,
'subtitle': 'Start your journey with the basics of English.', 'subtitle': 'Start your journey with the basics of English.',
}, },
{ // {
'title': 'A2', // 'title': 'A2',
'current': false, // 'current': false,
'subtitle': 'Build upon your foundational knowledge.', // 'subtitle': 'Build upon your foundational knowledge.',
}, // },
]; ];
List<Map<String, dynamic>> get learnSubLevels => _learnSubLevels; List<Map<String, dynamic>> get learnSubLevels => _learnSubLevels;
@ -27,11 +27,4 @@ class LearnLevelViewModel extends BaseViewModel {
Future<void> navigateToLearnModule() async => Future<void> navigateToLearnModule() async =>
_navigationService.navigateToLearnModuleView(); _navigationService.navigateToLearnModuleView();
Future<void> navigateToLearnPractice() async =>
await _navigationService.navigateToLearnPracticeView(
title: 'Lets Practice Level 1',
buttonLabel: 'Begin Level Practice',
subtitle: 'Lets quickly review what youve learned in this level! ',
);
} }

View File

@ -94,17 +94,25 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
itemBuilder: (context, index) => _buildTile( itemBuilder: (context, index) => _buildTile(
title: viewModel.modules[index]['title'], title: viewModel.modules[index]['title'],
status: viewModel.modules[index]['status'], status: viewModel.modules[index]['status'],
subtitle: viewModel.modules[index]['subtitle']), topics: viewModel.modules[index]['topics'],
subtitle: viewModel.modules[index]['subtitle'],
practices: viewModel.modules[index]['practices'],
description: viewModel.modules[index]['description']),
); );
Widget _buildTile({ Widget _buildTile(
required String title, {required String title,
required String topics,
required String subtitle, required String subtitle,
required String description,
required ProgressStatuses status, required ProgressStatuses status,
}) => required List<Map<String, dynamic>> practices}) =>
LearnModuleTile( LearnModuleTile(
title: title, title: title,
status: status, status: status,
topics: topics,
subtitle: subtitle, subtitle: subtitle,
practices: practices,
description: description,
); );
} }

View File

@ -11,26 +11,337 @@ class LearnModuleViewModel extends BaseViewModel {
// Modules // Modules
final List<Map<String, dynamic>> _modules = [ final List<Map<String, dynamic>> _modules = [
{ {
'status': ProgressStatuses.completed, 'status': ProgressStatuses.started,
'title': 'Module 1: Greetings & Introductions', 'title': 'Lesson 1.1',
'subtitle': 'subtitle': 'Start Speaking English Today! Greetings & Introductions',
'Learn how to introduce yourself, talk about your surroundings, and start simple conversations.', 'topics':
"""👉 How to use "Good Morning," "Afternoon," and "Evening" at the right time.
👉 Why saying "I'm" is often better than "I am" in spoken English.
👉 Master "My," "Your," and the verb "To Be" without the headache.
👉 How to perfectly say the "Long I" sound in "Hi" and "My."
""",
'practices': [
{
'question_text': 'Good morning! How are you?',
'question_audio_url':
'https://drive.google.com/file/d/1El9hhmZvLnrTYtVreHR0EDXNrZyGphT9/view?usp=sharing',
'sample_answer':
'https://drive.google.com/file/d/1dUSahuj_VdunV293gEZr0XL9d4WV7_8G/view?usp=sharing'
},
{
'question_text': 'What\'s your name?',
'sample_answer':
'https://drive.google.com/file/d/14oAqcMRltXeQhQ-RTGizO2DZ4CkHmKdu/view?usp=drive_link',
'question_audio_url':
'https://drive.google.com/file/d/1iAsOIXD4NcsctKuubnvWlnwXodTMlou5/view?usp=drive_link'
},
{
'question_text': 'Nice to meet you!',
'question_audio_url':
'https://drive.google.com/file/d/10bOaNCcpNFzxpJ4d2-5KYahMCCcWt668/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/1h8V6lFuOiOf0zRRN-NvYjtLrmMdx0SeX/view?usp=drive_link'
},
{
'question_text':
'You are doing great! Tell me, are you happy to be here?',
'sample_answer':
'https://drive.google.com/file/d/1sjNFofDRr9KB8fQptdM12LDJ5DIaHujs/view?usp=drive_link',
'question_audio_url':
'https://drive.google.com/file/d/1kcNIL5NCt_lfTzAbmCkM3yojD_UzjSx7/view?usp=drive_link',
},
],
'description':
"""Stop feeling nervous when you meet someone new! In this lesson, we break down the most common English greetings and show you exactly how to introduce yourself with confidence.
Whether you are at the market, in a classroom, or meeting someone online, the first 10 seconds of a conversation are the most important. Our teacher will guide you through the exact phrases you need to sound natural and friendly from the very first moment.
""",
}, },
{ {
'status': ProgressStatuses.started, 'status': ProgressStatuses.started,
'title': 'Module 2: Everyday Basics', 'title': 'Lesson 1.2',
'subtitle': 'Learn numbers, colors, and common objects.', 'subtitle': 'Talk About Your Home! "Where Are You From?" & Locations',
'topics':
"""👉 The difference between "Where are you from?" and "Where do you live?"
👉 How to use "Am," "Is," and "Are" to talk about your home.
👉 Learn to say "Where-are-you" as one smooth sound (word linking).
👉 Practice a full conversation with Miss Alem.
👉 A fast-paced game to test your speed and memory.
""",
'practices': [
{
'question_text':
'Hey, It is a pleasure to meet you. Where are you from?',
'sample_answer':
'https://drive.google.com/file/d/1F98PdPqeDhkF5KMzFjgmUoy4HanIDHVs/view?usp=drive_link',
'question_audio_url':
'https://drive.google.com/file/d/1vipFTk-DYgOCYhyDyWVaORec7g0JeidD/view?usp=drive_link',
}, },
{ {
'title': 'Module 3: At the Cafe', 'question_text': 'Ethiopia is beautiful! What city are you from?',
'status': ProgressStatuses.pending, 'sample_answer':
'subtitle': 'Practice ordering food and drinks confidently.', 'https://drive.google.com/file/d/1nZJLV9lOgFGqYr-W3vlpt8rbay_bwARJ/view?usp=drive_link',
'question_audio_url':
'https://drive.google.com/file/d/1T8JIYQ6T9Mq_7TazGr85ag4PCyQgHhzi/view?usp=drive_link'
}, },
{ {
'progress': 0, 'question_text': 'I see. And where does your family live now?',
'status': ProgressStatuses.pending, 'question_audio_url':
'title': 'Module 4: Asking for Directions', 'https://drive.google.com/file/d/19XAbHL3HqTpPcolvOQUVPSGef-Ythusu/view?usp=drive_link',
'subtitle': 'Learn numbers, colors, and common objects.', 'sample_answer':
'https://drive.google.com/file/d/1O8o11DNKYZBcm1-f8pjMFJjNIvGmawvg/view?usp=drive_link'
},
],
'description':
"""Are you ready to talk about your city, town, or country with pride? In Lesson 1.2, we move past basic greetings and learn how to share where you come from and where you live now.
Whether you are meeting a tourist, a new classmate, or a colleague, being able to say "Im from Ethiopia" or "I live in Addis Ababa" is the best way to build a connection. Miss Alem will show you how to link your words together so you sound like a natural English speaker!
""",
},
{
'title': 'Lesson 1.3',
'status': ProgressStatuses.started,
'subtitle': 'Talk About Your Family! Master "Have" vs. "Has"',
'topics':
"""👉 Master the names for parents, grandparents, and the secret word for brothers and sisters (Siblings).
👉 Never mix up "I have" and "She has" ever again.
👉 Why the letter "S" is the most important sound when talking about your brothers.
👉 A close-up look at how to pronounce "Mother" and "Father" like a native speaker.
👉 Test your eyes and ears by finding the hidden mistakes in our family game.
""",
'practices': [
{
'question_text':
'Hello! I want to know about your family. Do you have a brother or sister?',
'sample_answer':
'https://drive.google.com/file/d/1fred7Y5ocdD4codZuK7Pr349O38UkB0Z/view?usp=drive_link',
'question_audio_url':
'https://drive.google.com/file/d/1tMoTA4D6joz0bkWS5Nfn-neUvbej6XDI/view?usp=drive_link',
},
{
'question_text': 'That is nice! How many people are in your family?',
'sample_answer':
'https://drive.google.com/file/d/1dhYhiycWwdtW0ndJUMdMXxxVyrXLXPCM/view?usp=drive_link',
'question_audio_url':
'https://drive.google.com/file/d/1FwRxWvrxzKbhEIYImy2ONzd410pabkJa/view?usp=drive_link'
},
{
'question_text': 'Do you have grandparents?',
'question_audio_url':
'https://drive.google.com/file/d/1UBaQYiJINgOmZWDLFKeXZQ8Zfb7cOKNZ/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/14cnEJTO7GtGZcO8ag50JdupV2EkvDHes/view?usp=drive_link'
},
],
'description':
"""Do you have brothers and sisters? In Lesson 1.3, we dive into the most personal and important topic for building friendships: Your Family. Many beginners make mistakes with the words "have" and "has," but today, we will fix that! Our teacher will teach you the exact formulas to describe your parents, siblings, and grandparents with perfect grammar. By the end of this video, you will be able to introduce your entire family in English with confidence.""",
},
{
'status': ProgressStatuses.started,
'title': 'Lesson 1.4',
'subtitle': '"What Do You Do?" Talk About Your Job & Studies',
'topics':
"""👉 The clear difference between talking about work and academic life.
👉 Simple rules to decide between "Im a teacher" and "Im an accountant."
👉 How to use the "-ing" form if you are still training for your dream job.
👉 Perfecting the final "T" in "Student" and the "SH" sound in "Cashier."
👉 A high-energy game to test your grammar reflexes!""",
'practices': [
{
'question_text': 'It is good to see you again! What do you do?',
'question_audio_url':
'https://drive.google.com/file/d/171fh_iN0aXS0t95_5RzcxFiVmLCB7caP/view?usp=sharing',
'sample_answer':
'https://drive.google.com/file/d/1wDkbp23F2PsLZM9mWlRd-JjnwZa97T-8/view?usp=drive_link',
},
{
'question_text':
'That is great. Are you studying to be a professional? What are you studying?',
'sample_answer':
'https://drive.google.com/file/d/1egHUMIqGt9VAw5HMVMdjWR3zmamdSB6F/view?usp=drive_link',
'question_audio_url':
'https://drive.google.com/file/d/1T_qrH_-KHgQFw5kX4Ti-UwXvnBFyJsWT/view?usp=drive_link'
},
{
'question_text':
'Your sister works in a restaurant. Is she a waitress?',
'question_audio_url':
'https://drive.google.com/file/d/1Okd-W2zRZCukQfnbLEp9A4r_5rt99U_6/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/1t-_JHXel9dtSHRwYU_CO1Mc8-Pa1pOa0/view?usp=drive_link'
},
{
'question_text': 'My friend works in a bank. Is he an accountant?',
'question_audio_url':
'https://drive.google.com/file/d/1K4ahmIzuBdrsUoO-gVRp525ZFyP7b1Cq/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/1ecpWSQTvU9pu8BYq3qRkYUjblX5wG4RZ/view?usp=drive_link'
},
],
'description':
"""Youve shared your name and your home—now its time to talk about your daily life! In Lesson 1.4, we master the most common question in English: "What do you do?" Whether you are working a full-time job, searching for a new career, or currently studying, this lesson gives you the exact phrases to describe your profession with perfect grammar.
Miss [Name] breaks down the tricky "A vs. An" rule so you never have to second-guess yourself again. Plus, learn how to sound more natural by "linking" your words like a native speaker!
"""
},
{
'title': 'Lesson 1.5',
'status': ProgressStatuses.started,
'subtitle': 'Talk About Your Day! Daily Routines & Time Words',
'topics':
"""👉 Master the 6 most common actions, from "Wake up" to "Go to bed."
👉 Learn why we say "I eat" but "She eats" (and how to never forget it!).
👉 Use "First," "Then," and "Finally" to tell a complete story about your day.
👉 How to perfectly say the tricky "TH" sound in the word "Teeth."
👉 A fast-paced game to test if your brain can handle the "S" rule under pressure!""",
'practices': [
{
'question_text':
'I want to hear about your day! What is the first thing you do in the morning?',
'question_audio_url':
'https://drive.google.com/file/d/1B_g45crvD0X2xfJJuCBdBhe7LNRJtKpa/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/13nV3uQ6Vftc0DznBJ8GQMi26dHxmMUwH/view?usp=drive_link',
},
{
'question_text':
'That is a great start. What do you do for your hygiene? I brush...',
'sample_answer':
'https://drive.google.com/file/d/1ijbKHO9A3PEOUb712ycCw-P3ZvWX-NTp/view?usp=drive_link',
'question_audio_url':
'https://drive.google.com/file/d/1r92ixsWSD58TrV9LIArPRaKk1bVE5m44/view?usp=drive_link'
},
{
'question_text':
'Perfect. And then, what do you do before you leave the house?',
'question_audio_url':
'https://drive.google.com/file/d/1xEHuliUwyqFV35o_C2varAHcoI3QrOsS/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/1RJgS3tp0ZKQ1YIt9fLRch-IqFialZR6Q/view?usp=drive_link'
},
{
'question_text':
'And finally, what is the last thing you do at night?',
'question_audio_url':
'https://drive.google.com/file/d/1njWyQExmgAPWgDTrcpqitEoNjiPfCbEw/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/140O0xAkYpVo5FLZQmbc1BaY_6agYWeki/view?usp=drive_link'
},
],
'description':
"""What do you do from the moment you wake up until you go to bed? In Lesson 1.5, we master the art of describing your Daily Routine at Home. This is the best way to practice the "Simple Present Tense" so you can talk about your habits, chores, and schedules like a pro.
Our teacher will show you the "S" secretthe most important grammar rule for talking about other peopleand teach you the "Time Order Words" that make your English flow naturally from one activity to the next.
"""
},
{
'title': 'Lesson 1.6',
'status': ProgressStatuses.started,
'subtitle': 'Express Your Feelings! Likes, Dislikes & Hobbies',
'topics':
"""👉 How to build perfect positive and negative sentences (I like vs. I don't like).
👉 Master the "Do you like...?" structure and the correct short answers.
👉 Learn why we switch from "Don't" to "Doesn't" for He and She.
👉 How to master the "L" sound and make your "Don't" sound natural and fast.
👉 A high-speed game to test if you can handle the grammar of likes and dislikes!""",
'practices': [
{
'question_text':
'I want to know about your hobbies. Do you like football?',
'question_audio_url':
'https://drive.google.com/file/d/1ombhL58UTSSMlVr4OkjEN2m1TXeIs3Jw/view?usp=sharing',
'sample_answer':
'https://drive.google.com/file/d/1E-NilYZkazwTyAFwwNunDQmWIxBHH_GL/view?usp=drive_link',
},
{
'question_text':
'Thats fun! Some people prefer quiet things. What do you like to do at home',
'sample_answer':
'https://drive.google.com/file/d/1HJqwHQId9MUhaQvjoyhuhk-ZQNCz5-RP/view?usp=drive_link',
'question_audio_url':
'https://drive.google.com/file/d/1HJqwHQId9MUhaQvjoyhuhk-ZQNCz5-RP/view?usp=drive_link'
},
{
'question_text':
'Interesting. Many people like music. Do you like slow music?',
'question_audio_url':
'https://drive.google.com/file/d/13edPllK_dZYmmFIsue_A-sGmDe9fh89K/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/1lA18zdiFbKKXaGwUvJv4JwBMW_Vxxkip/view?usp=drive_link'
},
{
'question_text': 'Do you like running?',
'question_audio_url':
'https://drive.google.com/file/d/1MSVceVXL-mAELGvv0_SN68t1-pCM-V6R/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/1XkwoWINsoXDSe63kjJXQFczjtPKbBTY4/view?usp=drive_link'
},
],
'description':
"""What makes you happy? What do you avoid? In Lesson 1.6, we learn how to make your English conversations personal and fun by sharing your Likes and Dislikes. This is the final topic of Module 1, and it is the key to building real friendships by finding common interests!
We will see how to use the power of "Do" and "Don't," and show you how the "S" rule changes when we talk about what our friends like. Get ready to talk about football, coffee, music, and more!
"""
},
{
'title': 'Lesson 1.7',
'status': ProgressStatuses.started,
'subtitle': 'Module 1 Final Test! Speaking Review & Graduation',
'topics': """👉 Fast-paced practice for names, origins, and jobs.
Mastering "Have," "Has," and the plural "S" under pressure.
👉 Using the Simple Present Tense to describe your day and your likes.
👉 A bonus challenge to find and correct common beginner mistakes.
👉 Two speaking scripts to help you link your words and sound like a native speaker.""",
'practices': [
{
'question_text': 'Hello! What\'s your name?',
'question_audio_url':
'https://drive.google.com/file/d/11xsnZpqOXQREc2nku77u_qOXucUdncDq/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/1OgvhumoPNdw33WfxEZfS1xtYZHshAM-k/view?usp=drive_link',
},
{
'question_text': 'It was a pleasure meeting you. Nice to meet you!',
'sample_answer':
'https://drive.google.com/file/d/1NmKeyx0mKmbFyZdmQ4sWGM6UMpwAIrHN/view?usp=drive_link',
'question_audio_url':
'https://drive.google.com/file/d/1nu1fiK4dLCjW9jjENvThshU48vFj-T3h/view?usp=drive_link'
},
{
'question_text': 'Where are you from?',
'question_audio_url':
'https://drive.google.com/file/d/1uM2L6-u0H-LHyDQL_Y0txjoO64n5lHkp/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/1VXhHRG2CJR3lx07cAuiZrfCxzxdOcCIz/view?usp=drive_link'
},
{
'question_text': 'And where do you live now?',
'question_audio_url':
'https://drive.google.com/file/d/1sGJpaRQ1wArA2iDq7BZM-56abMZwiISO/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/10Cj8r2KeYUttBJ7W2wETP8XSdK2ZItom/view?usp=drive_link'
},
{
'question_text': 'Do you have a brother or sister?',
'question_audio_url':
'https://drive.google.com/file/d/1cA4wD7zY7WRgorWladvf1f8QRu54oBBw/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/1IeiGyN1S5XaREfXgmzfl-zESap8A_k3H/view?usp=drive_link'
},
{
'question_text': 'I am a teacher. Are you a student?',
'question_audio_url':
'https://drive.google.com/file/d/1n5w3skHBvQ2hE_OA2HdeUnr0YeN4zL2_/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/1j86TVmeNzZvL8RueZeIueCQO_40i9aMh/view?usp=drive_link'
},
{
'question_text': 'What is the first thing you do in the morning?',
'question_audio_url':
'https://drive.google.com/file/d/1oCrDDtYF4-1S2UYu6shilqmQSqyRG3v_/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/1shpCr2XvV12cXaxuJ-AjW-0XWvBdtT8Q/view?usp=drive_link'
},
],
'description':
"""Congratulations! You have reached the end of Module 1. You started with simple greetings, and now you are ready to hold a full conversation in English! This is our Final Speaking Review, where we test everything youve learned so far.
From your name and family to your daily habits and hobbies, this lesson is designed to push your speed, accuracy, and confidence. We will lead you through three high-intensity speaking drills and a bonus Grammar Check to make sure you are 100% ready for Module 2!
"""
}, },
]; ];
@ -39,13 +350,25 @@ class LearnModuleViewModel extends BaseViewModel {
// Navigation // Navigation
void pop() => _navigationService.back(); void pop() => _navigationService.back();
Future<void> navigateToLearnLesson() async => Future<void> navigateToLearnLesson(
await _navigationService.navigateToLearnLessonView(); {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() async => Future<void> navigateToLearnPractice(
List<Map<String, dynamic>> practices) async =>
await _navigationService.navigateToLearnPracticeView( await _navigationService.navigateToLearnPracticeView(
title: 'Lets Practice Module 1', practices: practices,
buttonLabel: 'Begin Module Practice', title: 'Lets Practice',
subtitle: 'Lets quickly review what youve learned in this module! ', buttonLabel: 'Begin Lesson Practice',
subtitle: 'Lets quickly review what youve learned in this lesson!',
); );
} }

View File

@ -3,26 +3,57 @@ import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_practice/screens/finish_learn_practice_screen.dart'; import 'package:yimaru_app/ui/views/learn_practice/screens/finish_learn_practice_screen.dart';
import 'package:yimaru_app/ui/views/learn_practice/screens/learn_practice_completion_screen.dart'; import 'package:yimaru_app/ui/views/learn_practice/screens/learn_practice_completion_screen.dart';
import 'package:yimaru_app/ui/views/learn_practice/screens/learn_practice_result_screen.dart'; import 'package:yimaru_app/ui/views/learn_practice/screens/learn_practice_result_screen.dart';
import 'package:yimaru_app/ui/views/learn_practice/screens/listen_learn_practice_speaker_screen.dart'; import 'package:yimaru_app/ui/views/learn_practice/screens/learn_practices_screen.dart';
import 'package:yimaru_app/ui/views/learn_practice/screens/interact_learn_practice_screen.dart';
import 'package:yimaru_app/ui/views/learn_practice/screens/learn_practice_intro_screen.dart'; import 'package:yimaru_app/ui/views/learn_practice/screens/learn_practice_intro_screen.dart';
import 'package:yimaru_app/ui/views/learn_practice/screens/speak_to_learn_practice_listener_screen.dart';
import 'package:yimaru_app/ui/views/learn_practice/screens/start_learn_practice_screen.dart'; import 'package:yimaru_app/ui/views/learn_practice/screens/start_learn_practice_screen.dart';
import '../../common/app_colors.dart'; import '../../common/app_colors.dart';
import '../../widgets/cancel_learn_practice_sheet.dart';
import 'learn_practice_viewmodel.dart'; import 'learn_practice_viewmodel.dart';
class LearnPracticeView extends StackedView<LearnPracticeViewModel> { class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
final String title; final String title;
final String subtitle; final String subtitle;
final String buttonLabel; final String buttonLabel;
final List<Map<String, dynamic>> practices;
const LearnPracticeView( const LearnPracticeView(
{Key? key, {Key? key,
required this.title, required this.title,
required this.subtitle, required this.subtitle,
required this.practices,
required this.buttonLabel}) required this.buttonLabel})
: super(key: key); : super(key: key);
Future<void> _cancel(LearnPracticeViewModel viewModel) async {
viewModel.pop();
viewModel.goTo(0);
viewModel.stopRecording();
}
Future<void> _pop(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) async {
{
if (viewModel.currentPage == 3) {
await _showSheet(context: context, viewModel: viewModel);
} else {
viewModel.goBack();
}
}
}
Future<void> _showSheet(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) async =>
await showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: kcTransparent,
builder: (_) => _buildSheet(viewModel),
);
@override @override
LearnPracticeViewModel viewModelBuilder(BuildContext context) => LearnPracticeViewModel viewModelBuilder(BuildContext context) =>
LearnPracticeViewModel(); LearnPracticeViewModel();
@ -33,36 +64,46 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
LearnPracticeViewModel viewModel, LearnPracticeViewModel viewModel,
Widget? child, Widget? child,
) => ) =>
_buildPracticeScreensWrapper(viewModel); _buildPracticeScreensWrapper(context: context, viewModel: viewModel);
Widget _buildPracticeScreensWrapper(LearnPracticeViewModel viewModel) => Widget _buildPracticeScreensWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
PopScope( PopScope(
canPop: true, canPop: viewModel.currentPage == 0 ? true : false,
onPopInvokedWithResult: (value, data) { onPopInvokedWithResult: (value, data) async =>
if (!value) return; await _pop(context: context, viewModel: viewModel),
WidgetsBinding.instance
.addPostFrameCallback((_) => viewModel.goBack());
},
child: _buildScaffoldWrapper(viewModel)); child: _buildScaffoldWrapper(viewModel));
Widget _buildSheet(LearnPracticeViewModel viewModel) =>
CancelLearnPracticeSheet(
onClose: viewModel.pop,
onContinue: viewModel.pop,
onCancel: () async => await _cancel(viewModel),
);
Widget _buildScaffoldWrapper(LearnPracticeViewModel viewModel) => Scaffold( Widget _buildScaffoldWrapper(LearnPracticeViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor, backgroundColor: kcBackgroundColor,
body: _buildBody(viewModel), body: _buildBody(viewModel),
); );
Widget _buildBody(LearnPracticeViewModel viewModel) => Widget _buildBody(LearnPracticeViewModel viewModel) =>
IndexedStack(index: viewModel.currentIndex, children: _buildScreens()); IndexedStack(index: viewModel.currentPage, children: _buildScreens());
List<Widget> _buildScreens() => [ List<Widget> _buildScreens() => [
_buildLearnPracticesScreen(),
_buildLearnPracticeIntroScreen(), _buildLearnPracticeIntroScreen(),
_buildStartLearnPracticeScreen(), _buildStartLearnPracticeScreen(),
_buildListenLearnPracticeSpeakerScreen(), _buildInteractLearnPracticeScreen(),
_buildSpeakToLearnPracticeListenerScreen(),
_buildFinishLearnPracticeScreen(), _buildFinishLearnPracticeScreen(),
_buildLearnPracticeResultScreen(), _buildLearnPracticeResultScreen(),
_buildLearnPracticeCompletionScreen() _buildLearnPracticeCompletionScreen()
]; ];
Widget _buildLearnPracticesScreen() => LearnPracticesScreen(
practices: practices,
);
Widget _buildLearnPracticeIntroScreen() => LearnPracticeIntroScreen( Widget _buildLearnPracticeIntroScreen() => LearnPracticeIntroScreen(
title: title, title: title,
subtitle: subtitle, subtitle: subtitle,
@ -71,11 +112,8 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
Widget _buildStartLearnPracticeScreen() => const StartLearnPracticeScreen(); Widget _buildStartLearnPracticeScreen() => const StartLearnPracticeScreen();
Widget _buildListenLearnPracticeSpeakerScreen() => Widget _buildInteractLearnPracticeScreen() =>
const ListenLearnPracticeSpeakerScreen(); const InteractLearnPracticeScreen();
Widget _buildSpeakToLearnPracticeListenerScreen() =>
const SpeakToLearnPracticeListenerScreen();
Widget _buildFinishLearnPracticeScreen() => const FinishLearnPracticeScreen(); Widget _buildFinishLearnPracticeScreen() => const FinishLearnPracticeScreen();

View File

@ -1,36 +1,200 @@
import 'package:audioplayers/audioplayers.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import 'package:waveform_recorder/waveform_recorder.dart';
import 'package:yimaru_app/models/user_model.dart';
import 'package:yimaru_app/services/authentication_service.dart';
import 'package:yimaru_app/services/voice_recorder_service.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import '../../../app/app.locator.dart'; import '../../../app/app.locator.dart';
import '../../../services/audio_player_service.dart';
import '../../common/app_colors.dart';
class LearnPracticeViewModel extends ReactiveViewModel {
// Dependency injection
final _dialogService = locator<DialogService>();
class LearnPracticeViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>(); final _navigationService = locator<NavigationService>();
final _audioPlayerService = locator<AudioPlayerService>();
final _voiceRecorderService = locator<VoiceRecorderService>();
final _authenticationService = locator<AuthenticationService>();
LearnPracticeViewModel() {
_listenToAudio();
}
@override
List<ListenableServiceMixin> get listenableServices =>
[_audioPlayerService, _voiceRecorderService,_authenticationService];
// User
UserModel? get _user => _authenticationService.user;
UserModel? get user => _user;
// AudioPlayer
AudioPlayer get _player => _audioPlayerService.player;
AudioPlayer get player => _player;
Duration _duration = Duration.zero;
Duration _position = Duration.zero;
Duration get position => _position;
Duration get duration => _duration;
double get progress {
if (_duration.inMilliseconds == 0) return 0;
return _position.inMilliseconds / _duration.inMilliseconds;
}
// Voice recorder
WaveformRecorderController get _waveController =>
_voiceRecorderService.waveController;
WaveformRecorderController get waveController => _waveController;
// Voice recorder state
VoiceRecordingState get _recordingState =>
_voiceRecorderService.recordingState;
VoiceRecordingState get recordingState => _recordingState;
// Busy object
StateObjects _busyObject = StateObjects.none;
StateObjects get busyObject => _busyObject;
// In-app navigation // In-app navigation
int _currentIndex = 0; int _currentPage = 0;
int get currentIndex => _currentIndex; int get currentPage => _currentPage;
// Practice results // Practice
final List<Map<String, dynamic>> _practiceResults = [ Map<String, dynamic> _selectedPractice = {};
{'question': 'What is your name?'},
{'question': 'Where are you from?'},
{'question': 'Where are you from?'}
];
List<Map<String, dynamic>> get practiceResults => _practiceResults; Map<String, dynamic> get selectedPractice => _selectedPractice;
// Practice
void setPractice(Map<String, dynamic> practice) {
_selectedPractice = practice;
goTo(1);
}
// Play practice audio
Future<void> playQuestionAudio() async => await runBusyFuture(_playQuestionAudio(),
busyObject: StateObjects.learnPracticeQuestion);
Future<void> _playQuestionAudio() async {
goTo(3);
await _audioPlayerService.playUrl(_selectedPractice['question_audio_url']);
}
void _listenToAudio() {
_audioPlayerService.durationStream.listen((dur) {
_duration = dur;
print('DURATION: $_duration');
rebuildUi();
});
_audioPlayerService.positionStream.listen((pos) {
_position = pos;
print('POSITION: $_position');
rebuildUi();
});
}
// Set busy object
void setBusyObject(StateObjects object){
_busyObject = object;
notifyListeners();
}
// Sample audio
Future<void> playSampleAudio() async => await runBusyFuture(_playSampleAudio(),
busyObject: StateObjects.learnPracticeSample);
Future<void> _playSampleAudio() async {
setBusyObject(StateObjects.learnPracticeSample);
await _audioPlayerService.playUrl(_selectedPractice['sample_answer']);
}
Future<void> pauseSampleAudio()async=> await runBusyFuture(_pauseSampleAudio(),
busyObject: StateObjects.learnPracticeSample);
Future<void> _pauseSampleAudio() async {
setBusyObject(StateObjects.learnPracticeSample);
await _audioPlayerService.pause();
}
// Recorded audio
Future<void> playRecordedAudio() async => await runBusyFuture(_playRecordedAudio(),
busyObject: StateObjects.learnPracticeAnswer);
Future<void> _playRecordedAudio() async {
setBusyObject(StateObjects.learnPracticeAnswer);
await _audioPlayerService.playLocal(await _voiceRecorderService.getRecordedAudio() ?? '');
}
Future<void> pauseRecordedAudio()async=> await runBusyFuture(_pauseRecordedAudio(),
busyObject: StateObjects.learnPracticeAnswer);
Future<void> _pauseRecordedAudio() async {
setBusyObject(StateObjects.learnPracticeAnswer);
await _audioPlayerService.pause();
}
// Voice recorder
Future<void> startRecording() async => await runBusyFuture(_startRecording(),busyObject: StateObjects.recordLearnPracticeAnswer );
Future<void> _startRecording() async => await _voiceRecorderService.startRecording();
Future<void> stopRecording() async =>
await _voiceRecorderService.stopRecording();
// Dialogue
Future<bool?> showAbortDialog() async {
DialogResponse? response = await _dialogService.showDialog(
cancelTitle: 'No',
title: 'Recording',
buttonTitle: 'Yes',
barrierDismissible: true,
cancelTitleColor: kcDarkGrey,
buttonTitleColor: kcPrimaryColor,
description: 'Are you sure you want to stop recording?',
);
return response?.confirmed;
}
// In-app navigation // In-app navigation
void goTo(int page) { void goTo(int page) {
_currentIndex = page; _currentPage = page;
rebuildUi(); rebuildUi();
} }
void goBack() { void goBack() {
if (_currentIndex == 0) { if (_currentPage == 0) {
pop(); pop();
} else { } else {
_currentIndex--; _currentPage--;
rebuildUi(); rebuildUi();
} }
} }

View File

@ -49,7 +49,7 @@ class FinishLearnPracticeScreen
); );
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar( Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
showBackButton: false, showBackButton: true,
onTap: viewModel.goBack, onTap: viewModel.goBack,
title: 'Practice Speaking', title: 'Practice Speaking',
); );

View File

@ -1,19 +1,38 @@
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:waveform_recorder/waveform_recorder.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart'; import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/cancel_learn_practice_sheet.dart'; import 'package:yimaru_app/ui/widgets/cancel_learn_practice_sheet.dart';
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart'; import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
import 'package:yimaru_app/ui/widgets/page_loading_indicator.dart';
import 'package:yimaru_app/ui/widgets/wave_wrapper.dart'; import 'package:yimaru_app/ui/widgets/wave_wrapper.dart';
import '../../../common/app_colors.dart'; import '../../../common/app_colors.dart';
import '../../../common/enmus.dart';
import '../../../common/ui_helpers.dart'; import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_column_button.dart'; import '../../../widgets/custom_column_button.dart';
import '../../../widgets/small_app_bar.dart'; import '../../../widgets/small_app_bar.dart';
class ListenLearnPracticeSpeakerScreen class InteractLearnPracticeScreen
extends ViewModelWidget<LearnPracticeViewModel> { extends ViewModelWidget<LearnPracticeViewModel> {
const ListenLearnPracticeSpeakerScreen({super.key}); const InteractLearnPracticeScreen({super.key});
Future<void> _cancel(LearnPracticeViewModel viewModel) async {
viewModel.pop();
viewModel.goTo(0);
viewModel.stopRecording();
}
void _start(LearnPracticeViewModel viewModel) {
viewModel.playQuestionAudio();
}
Future<void> _stop(LearnPracticeViewModel viewModel) async {
await viewModel.stopRecording();
viewModel.goTo(4);
}
Future<void> _showSheet( Future<void> _showSheet(
{required BuildContext context, {required BuildContext context,
@ -40,28 +59,34 @@ class ListenLearnPracticeSpeakerScreen
Widget _buildScaffold( Widget _buildScaffold(
{required BuildContext context, {required BuildContext context,
required LearnPracticeViewModel viewModel}) => required LearnPracticeViewModel viewModel}) =>
SafeArea( SafeArea(child: _buildBodyStack(context: context, viewModel: viewModel));
child:
_buildBodyColumnWrapper(context: context, viewModel: viewModel));
Widget _buildBodyColumnWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBodyStack(context: context, viewModel: viewModel),
);
Widget _buildBodyStack( Widget _buildBodyStack(
{required BuildContext context, {required BuildContext context,
required LearnPracticeViewModel viewModel}) => required LearnPracticeViewModel viewModel}) =>
Stack( Stack(
children: [ children: [
_buildBodyColumn(context: context, viewModel: viewModel), _buildBodyColumnWrapper(context: context, viewModel: viewModel),
_buildProgressIndicatorWrapper() _buildProgressIndicatorState(viewModel),
_buildSpeakerState(context: context, viewModel: viewModel)
], ],
); );
Widget _buildSpeakerState(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
viewModel.busy(StateObjects.learnPracticeQuestion)
? const PageLoadingIndicator()
: Container();
Widget _buildBodyColumnWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBodyColumn(context: context, viewModel: viewModel),
);
Widget _buildBodyColumn( Widget _buildBodyColumn(
{required BuildContext context, {required BuildContext context,
required LearnPracticeViewModel viewModel}) => required LearnPracticeViewModel viewModel}) =>
@ -90,28 +115,79 @@ class ListenLearnPracticeSpeakerScreen
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar( Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
showBackButton: true, showBackButton: true,
onTap: viewModel.goBack,
title: 'Practice Speaking', title: 'Practice Speaking',
onTap: () async => await _cancel(viewModel),
); );
Widget _buildSpeakingIndicatorWrapper(LearnPracticeViewModel viewModel) => Widget _buildSpeakingIndicatorWrapper(LearnPracticeViewModel viewModel) =>
Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: _buildSpeakingIndicatorChildren(), children: _buildSpeakingIndicatorChildren(viewModel),
); );
List<Widget> _buildSpeakingIndicatorChildren() => List<Widget> _buildSpeakingIndicatorChildren(
[_buildSpeakerLabel(), verticalSpaceMedium, _buildSpeakingIndicator()]; LearnPracticeViewModel viewModel) =>
[
_buildSpeakerLabelState(viewModel),
verticalSpaceMedium,
_buildSpeakingIndicator(viewModel)
];
Widget _buildSpeakerLabel() => Text( Widget _buildSpeakerLabelState(LearnPracticeViewModel viewModel) =>
viewModel.player.state == PlayerState.playing
? _buildListeningLabel()
: VoiceRecordingState.recording == viewModel.recordingState
? _buildSpeakingLabel()
: const SizedBox(height: 20);
Widget _buildListeningLabel() => Text(
'Daniel is speaking...', 'Daniel is speaking...',
style: style14P400, style: style14P400,
textAlign: TextAlign.center, textAlign: TextAlign.center,
); );
Widget _buildSpeakingIndicator() => Widget _buildSpeakingLabel() => Text(
WaveWrapper(height: 200, child: _buildSpinner()); 'You\'re is speaking...',
style: style14P400,
textAlign: TextAlign.center,
);
Widget _buildSpeakingIndicator(LearnPracticeViewModel viewModel) =>
WaveWrapper(height: 200, child: _buildSpinnerState(viewModel));
Widget _buildSpinnerState(LearnPracticeViewModel viewModel) =>
viewModel.player.state == PlayerState.playing
? _buildSpinner()
: VoiceRecordingState.recording == viewModel.recordingState &&
viewModel.waveController.isRecording
? _buildSpeakingSpinnerColumn(viewModel)
: Container();
Widget _buildSpeakingSpinnerColumn(LearnPracticeViewModel viewModel) =>
Column(
mainAxisSize: MainAxisSize.min,
children: _buildSpeakingSpinnerColumnChildren(viewModel),
);
List<Widget> _buildSpeakingSpinnerColumnChildren(
LearnPracticeViewModel viewModel) =>
[_buildSpinner(), _buildSpeakingSpinnerContainer(viewModel)];
Widget _buildSpeakingSpinnerContainer(LearnPracticeViewModel viewModel) =>
SizedBox(
width: 65,
child: _buildSpeakingSpinner(viewModel),
);
Widget _buildSpeakingSpinner(LearnPracticeViewModel viewModel) =>
WaveformRecorder(
height: 35,
onRecordingStopped: () {},
waveColor: kcPrimaryColor,
durationTextStyle: style14P600,
controller: viewModel.waveController,
);
Widget _buildSpinner() => const SpinKitWave( Widget _buildSpinner() => const SpinKitWave(
size: 20, size: 20,
@ -168,28 +244,61 @@ class ListenLearnPracticeSpeakerScreen
{required BuildContext context, {required BuildContext context,
required LearnPracticeViewModel viewModel}) => required LearnPracticeViewModel viewModel}) =>
[ [
_buildReplyButtonWrapper(), _buildReplyButtonWrapper(viewModel),
_buildMicButtonWrapper(viewModel), _buildMicButtonWrapper(viewModel),
_buildCancelButtonWrapper(context: context, viewModel: viewModel) _buildCancelButtonWrapper(context: context, viewModel: viewModel)
]; ];
Widget _buildReplyButtonWrapper() => Expanded(child: _buildReplyButton()); Widget _buildReplyButtonWrapper(LearnPracticeViewModel viewModel) =>
Expanded(child: _buildReplyButton(viewModel));
Widget _buildReplyButton() => const CustomColumnButton( Widget _buildReplyButton(LearnPracticeViewModel viewModel) =>
icon: Icons.replay, label: 'Reply', color: kcPrimaryColor); CustomColumnButton(
icon: Icons.replay,
label: 'Reply',
color: viewModel.recordingState == VoiceRecordingState.pending &&
viewModel.player.state != PlayerState.playing
? kcPrimaryColor
: kcLightGrey,
onTap: viewModel.recordingState == VoiceRecordingState.pending &&
viewModel.player.state != PlayerState.playing
? () => _start(viewModel)
: null,
);
Widget _buildMicButtonWrapper(LearnPracticeViewModel viewModel) => Widget _buildMicButtonWrapper(LearnPracticeViewModel viewModel) =>
Expanded(child: _buildMicButton(viewModel)); Expanded(child: _buildMicButton(viewModel));
Widget _buildMicButton(LearnPracticeViewModel viewModel) => ElevatedButton( Widget _buildMicButton(LearnPracticeViewModel viewModel) => ElevatedButton(
onPressed: () => viewModel.goTo(3), style: ButtonStyle(
style: const ButtonStyle( backgroundColor: WidgetStatePropertyAll(
shape: WidgetStatePropertyAll(CircleBorder()), viewModel.player.state == PlayerState.playing ||
padding: WidgetStatePropertyAll(EdgeInsets.all(15)), viewModel.busy(StateObjects.recordLearnPracticeAnswer)
shadowColor: WidgetStatePropertyAll(kcPrimaryColor), ? kcVeryLightGrey
backgroundColor: WidgetStatePropertyAll(kcPrimaryColor), : kcPrimaryColor,
), ),
child: _buildMicIcon(), shadowColor: const WidgetStatePropertyAll(kcWhite),
shape: const WidgetStatePropertyAll(CircleBorder()),
padding: const WidgetStatePropertyAll(EdgeInsets.all(15)),
),
onPressed: () async => viewModel.player.state == PlayerState.playing ||
viewModel.busy(StateObjects.recordLearnPracticeAnswer)
? null
: viewModel.recordingState == VoiceRecordingState.pending
? await viewModel.startRecording()
: await _stop(viewModel),
child: _buildMicState(viewModel),
);
Widget _buildMicState(LearnPracticeViewModel viewModel) =>
viewModel.recordingState == VoiceRecordingState.pending
? _buildMicIcon()
: _buildCheckIcon();
Widget _buildCheckIcon() => const Icon(
Icons.check,
size: 35,
color: kcWhite,
); );
Widget _buildMicIcon() => const Icon( Widget _buildMicIcon() => const Icon(
@ -217,18 +326,33 @@ class ListenLearnPracticeSpeakerScreen
Widget _buildSheet(LearnPracticeViewModel viewModel) => Widget _buildSheet(LearnPracticeViewModel viewModel) =>
CancelLearnPracticeSheet( CancelLearnPracticeSheet(
onTap: viewModel.pop, onClose: viewModel.pop,
onContinue: viewModel.pop,
onCancel: () async => await _cancel(viewModel),
); );
Widget _buildProgressIndicatorWrapper() => Positioned( Widget _buildProgressIndicatorState(LearnPracticeViewModel viewModel) =>
viewModel.recordingState == VoiceRecordingState.pending
? _buildProgressIndicatorWrapper(viewModel)
: Container();
Widget _buildProgressIndicatorWrapper(LearnPracticeViewModel viewModel) =>
Positioned(
top: 75, top: 75,
left: 0, left: 0,
right: 0, right: 0,
child: _buildProgressIndicator(), child: _buildProgressIndicatorSpacer(viewModel),
); );
Widget _buildProgressIndicator() => const CustomLinearProgressIndicator( Widget _buildProgressIndicatorSpacer(LearnPracticeViewModel viewModel) =>
progress: 0.7, Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildProgressIndicator(viewModel),
);
Widget _buildProgressIndicator(LearnPracticeViewModel viewModel) =>
CustomLinearProgressIndicator(
activeColor: kcPrimaryColor, activeColor: kcPrimaryColor,
progress: viewModel.progress,
backgroundColor: kcVeryLightGrey); backgroundColor: kcVeryLightGrey);
} }

View File

@ -77,7 +77,7 @@ class LearnPracticeCompletionScreen
borderRadius: 12, borderRadius: 12,
text: 'Continue', text: 'Continue',
foregroundColor: kcWhite, foregroundColor: kcWhite,
onTap: () => viewModel.pop(), onTap: () => viewModel.goTo(0),
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
); );
} }

View File

@ -128,7 +128,7 @@ class LearnPracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
borderRadius: 12, borderRadius: 12,
text: buttonLabel, text: buttonLabel,
foregroundColor: kcWhite, foregroundColor: kcWhite,
onTap: () => viewModel.goTo(1), onTap: () => viewModel.goTo(2),
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
); );
} }

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart'; import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/learn_practice_tip_section.dart'; import 'package:yimaru_app/ui/widgets/learn_practice_tip_section.dart';
import 'package:yimaru_app/ui/widgets/practice_results_wrapper.dart'; import 'package:yimaru_app/ui/widgets/learn_practice_results_wrapper.dart';
import '../../../common/app_colors.dart'; import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart'; import '../../../common/ui_helpers.dart';
@ -71,7 +71,7 @@ class LearnPracticeResultScreen
]; ];
Widget _buildResultsSection(LearnPracticeViewModel viewModel) => Widget _buildResultsSection(LearnPracticeViewModel viewModel) =>
PracticeResultsWrapper(data: viewModel.practiceResults); LearnPracticeResultsWrapper(data: viewModel.selectedPractice);
Widget _buildLearnPracticeTipSection() => const LearnPracticeTipSection(); Widget _buildLearnPracticeTipSection() => const LearnPracticeTipSection();

View File

@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/widgets/learn_practice_card.dart';
import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/small_app_bar.dart';
import '../learn_practice_viewmodel.dart';
class LearnPracticesScreen extends ViewModelWidget<LearnPracticeViewModel> {
final List<Map<String, dynamic>> practices;
const LearnPracticesScreen({Key? key, required this.practices})
: super(key: key);
@override
Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(LearnPracticeViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(LearnPracticeViewModel viewModel) =>
SafeArea(child: _buildBody(viewModel));
Widget _buildBody(LearnPracticeViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel),
);
Widget _buildColumn(LearnPracticeViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(viewModel),
verticalSpaceMedium,
_buildPracticeColumnWrapper(viewModel),
],
);
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
onTap: viewModel.pop,
showBackButton: true,
);
Widget _buildPracticeColumnWrapper(LearnPracticeViewModel viewModel) =>
Expanded(child: _buildPracticeColumnScrollView(viewModel));
Widget _buildPracticeColumnScrollView(LearnPracticeViewModel viewModel) =>
SingleChildScrollView(
child: _buildPracticeColumn(viewModel),
);
Widget _buildPracticeColumn(LearnPracticeViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildPracticeColumnChildren(viewModel),
);
List<Widget> _buildPracticeColumnChildren(LearnPracticeViewModel viewModel) =>
[
verticalSpaceMedium,
_buildTitle(),
_buildSubtitle(),
verticalSpaceMedium,
_buildListView(viewModel)
];
Widget _buildTitle() => Text(
'Learn Practices',
style: style18DG700,
);
Widget _buildSubtitle() => Text(
'Select a practice test your progress',
style: style14DG400,
);
Widget _buildListView(LearnPracticeViewModel viewModel) => GridView.builder(
shrinkWrap: true,
itemCount: practices.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) =>
_buildCard(index: index, practice: practices[index]),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 15,
crossAxisSpacing: 15,
childAspectRatio: 1.45,
),
);
Widget _buildCard(
{required int index, required Map<String, dynamic> practice}) =>
LearnPracticeCard(
index: index + 1,
practice: practice,
);
}

View File

@ -1,233 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/cancel_learn_practice_sheet.dart';
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_column_button.dart';
import '../../../widgets/small_app_bar.dart';
class SpeakToLearnPracticeListenerScreen
extends ViewModelWidget<LearnPracticeViewModel> {
const SpeakToLearnPracticeListenerScreen({super.key});
Future<void> _showSheet(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) async =>
await showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: kcTransparent,
builder: (_) => _buildSheet(viewModel),
);
@override
Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
_buildScaffoldWrapper(context: context, viewModel: viewModel);
Widget _buildScaffoldWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(context: context, viewModel: viewModel),
);
Widget _buildScaffold(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
SafeArea(
child:
_buildBodyColumnWrapper(context: context, viewModel: viewModel));
Widget _buildBodyColumnWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBodyStack(context: context, viewModel: viewModel),
);
Widget _buildBodyStack(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Stack(
children: [
_buildBodyColumn(context: context, viewModel: viewModel),
_buildProgressIndicatorWrapper()
],
);
Widget _buildBodyColumn(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children:
_buildBodyColumnChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildBodyColumnChildren(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
[
_buildAppBarWrapper(viewModel),
_buildSpeakingIndicatorWrapper(viewModel),
_buildLowerButtonsSectionWrapper(context: context, viewModel: viewModel)
];
Widget _buildAppBarWrapper(LearnPracticeViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(viewModel),
verticalSpaceMedium,
],
);
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
onTap: viewModel.goBack,
showBackButton: true,
title: 'Practice Speaking',
);
Widget _buildSpeakingIndicatorWrapper(LearnPracticeViewModel viewModel) =>
Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: _buildSpeakingIndicatorChildren(),
);
List<Widget> _buildSpeakingIndicatorChildren() =>
[_buildSpeakerLabel(), verticalSpaceMedium, _buildSpeakingIndicator()];
Widget _buildSpeakerLabel() => Text(
'You are speaking...',
style: style14P400,
textAlign: TextAlign.center,
);
Widget _buildSpeakingIndicator() => Container(
height: 100,
alignment: Alignment.center,
child: _buildSpinner(),
);
Widget _buildSpinner() => const SpinKitWave(
size: 75,
color: kcPrimaryColor,
type: SpinKitWaveType.center,
);
Widget _buildLowerButtonsSectionWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child:
_buildLowerButtonsSection(context: context, viewModel: viewModel),
);
Widget _buildLowerButtonsSection(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildLowerButtonsSectionChildren(
context: context, viewModel: viewModel),
);
List<Widget> _buildLowerButtonsSectionChildren(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
[
_buildActionLabel(),
verticalSpaceMedium,
_buildButtonsRowWrapper(context: context, viewModel: viewModel),
verticalSpaceMedium,
];
Widget _buildActionLabel() => Text(
'Tap the microphone to speak',
style: style14DG400,
textAlign: TextAlign.center,
);
Widget _buildButtonsRowWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children:
_buildButtonsRowChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildButtonsRowChildren(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
[
_buildEmptySpace(),
_buildCheckButtonWrapper(viewModel),
_buildCancelButtonWrapper(context: context, viewModel: viewModel)
];
Widget _buildEmptySpace() => Expanded(child: Container());
Widget _buildCheckButtonWrapper(LearnPracticeViewModel viewModel) =>
Expanded(child: _buildCheckButton(viewModel));
Widget _buildCheckButton(LearnPracticeViewModel viewModel) => ElevatedButton(
onPressed: () => viewModel.goTo(4),
style: const ButtonStyle(
shape: WidgetStatePropertyAll(CircleBorder()),
padding: WidgetStatePropertyAll(EdgeInsets.all(15)),
shadowColor: WidgetStatePropertyAll(kcPrimaryColor),
backgroundColor: WidgetStatePropertyAll(kcPrimaryColor),
),
child: _buildCheckIcon(),
);
Widget _buildCheckIcon() => const Icon(
Icons.check,
size: 35,
color: kcWhite,
);
Widget _buildCancelButtonWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Expanded(
child: _buildCancelButton(context: context, viewModel: viewModel));
Widget _buildCancelButton(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
CustomColumnButton(
color: kcRed,
label: 'Cancel',
icon: Icons.close,
onTap: () async =>
await _showSheet(context: context, viewModel: viewModel),
);
Widget _buildSheet(LearnPracticeViewModel viewModel) =>
CancelLearnPracticeSheet(
onTap: viewModel.pop,
);
Widget _buildProgressIndicatorWrapper() => Positioned(
top: 75,
left: 0,
right: 0,
child: _buildProgressIndicator(),
);
Widget _buildProgressIndicator() => const CustomLinearProgressIndicator(
progress: 0.7,
activeColor: kcPrimaryColor,
backgroundColor: kcVeryLightGrey);
}

View File

@ -10,6 +10,10 @@ import '../../../widgets/small_app_bar.dart';
class StartLearnPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> { class StartLearnPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
const StartLearnPracticeScreen({super.key}); const StartLearnPracticeScreen({super.key});
void _start(LearnPracticeViewModel viewModel) {
viewModel.playQuestionAudio();
}
@override @override
Widget build(BuildContext context, LearnPracticeViewModel viewModel) => Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
_buildScaffoldWrapper(viewModel); _buildScaffoldWrapper(viewModel);
@ -47,8 +51,8 @@ class StartLearnPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
); );
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar( Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
onTap: viewModel.goBack,
showBackButton: true, showBackButton: true,
onTap: viewModel.goBack,
title: 'Practice Speaking', title: 'Practice Speaking',
); );
@ -56,9 +60,10 @@ class StartLearnPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
child: _buildStartButtonContainer(viewModel), child: _buildStartButtonContainer(viewModel),
); );
Widget _buildStartButtonContainer(LearnPracticeViewModel viewModel) => Widget _buildStartButtonContainer(LearnPracticeViewModel viewModel) =>
GestureDetector( GestureDetector(
onTap: () => viewModel.goTo(2), onTap: () => _start(viewModel),
child: _buildStartButton(), child: _buildStartButton(),
); );
@ -129,7 +134,7 @@ class StartLearnPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
]; ];
Widget _buildActionLabel() => Text( Widget _buildActionLabel() => Text(
'Tap the microphone to speak', 'Tap the start button to listen',
style: style14DG400, style: style14DG400,
textAlign: TextAlign.center, textAlign: TextAlign.center,
); );
@ -149,22 +154,23 @@ class StartLearnPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
Widget _buildReplyButtonWrapper() => Expanded(child: _buildReplyButton()); Widget _buildReplyButtonWrapper() => Expanded(child: _buildReplyButton());
Widget _buildReplyButton() => const CustomColumnButton( Widget _buildReplyButton() => const CustomColumnButton(
icon: Icons.replay, label: 'Reply', color: kcPrimaryColor); icon: Icons.replay, label: 'Reply', color: kcLightGrey);
Widget _buildMicButtonWrapper(LearnPracticeViewModel viewModel) => Widget _buildMicButtonWrapper(LearnPracticeViewModel viewModel) =>
Expanded(child: _buildMicButton(viewModel)); Expanded(child: _buildMicButton(viewModel));
Widget _buildMicButton(LearnPracticeViewModel viewModel) => ElevatedButton( Widget _buildMicButton(LearnPracticeViewModel viewModel) => ElevatedButton(
onPressed: () => viewModel.goTo(2), onPressed: null,
style: const ButtonStyle( style: const ButtonStyle(
shape: WidgetStatePropertyAll(CircleBorder()), shape: WidgetStatePropertyAll(CircleBorder()),
padding: WidgetStatePropertyAll(EdgeInsets.all(15)), padding: WidgetStatePropertyAll(EdgeInsets.all(15)),
shadowColor: WidgetStatePropertyAll(kcPrimaryColor), shadowColor: WidgetStatePropertyAll(kcPrimaryColor),
backgroundColor: WidgetStatePropertyAll(kcPrimaryColor), backgroundColor: WidgetStatePropertyAll(kcVeryLightGrey),
), ),
child: _buildMicIcon(), child: _buildMicIcon(),
); );
Widget _buildMicIcon() => const Icon( Widget _buildMicIcon() => const Icon(
Icons.mic, Icons.mic,
size: 35, size: 35,

View File

@ -44,13 +44,13 @@ class LoginView extends StackedView<LoginViewModel> with $LoginView {
_buildLoginScreensWrapper(viewModel); _buildLoginScreensWrapper(viewModel);
Widget _buildLoginScreensWrapper(LoginViewModel viewModel) => PopScope( Widget _buildLoginScreensWrapper(LoginViewModel viewModel) => PopScope(
canPop: viewModel.currentIndex == 0 ? true : false, canPop: viewModel.currentPage == 0 ? true : false,
onPopInvokedWithResult: (value, data) => WidgetsBinding.instance onPopInvokedWithResult: (value, data) => WidgetsBinding.instance
.addPostFrameCallback((_) => viewModel.goBack()), .addPostFrameCallback((_) => viewModel.goBack()),
child: _buildBody(viewModel)); child: _buildBody(viewModel));
Widget _buildBody(LoginViewModel viewModel) => Widget _buildBody(LoginViewModel viewModel) =>
IndexedStack(index: viewModel.currentIndex, children: _buildScreens()); IndexedStack(index: viewModel.currentPage, children: _buildScreens());
List<Widget> _buildScreens() => [ List<Widget> _buildScreens() => [
_buildLoginWithEmailScreen(), _buildLoginWithEmailScreen(),

View File

@ -37,9 +37,9 @@ class LoginViewModel extends ReactiveViewModel
GoogleSignInAccount? get googleUser => _googleUser; GoogleSignInAccount? get googleUser => _googleUser;
// In-app navigation // In-app navigation
int _currentIndex = 0; int _currentPage = 0;
int get currentIndex => _currentIndex; int get currentPage => _currentPage;
// Email // Email
bool _focusEmail = false; bool _focusEmail = false;
@ -139,16 +139,16 @@ class LoginViewModel extends ReactiveViewModel
// In app navigation // In app navigation
void goTo(int page) { void goTo(int page) {
_currentIndex = page; _currentPage = page;
rebuildUi(); rebuildUi();
} }
void goBack() { void goBack() {
if (_currentIndex == 1) { if (_currentPage == 1) {
_currentIndex = 0; _currentPage = 0;
rebuildUi(); rebuildUi();
} else if (_currentIndex == 2) { } else if (_currentPage == 2) {
_currentIndex = 1; _currentPage = 1;
rebuildUi(); rebuildUi();
} else { } else {
_navigationService.back(); _navigationService.back();

View File

@ -21,6 +21,7 @@ class StartupViewModel extends BaseViewModel {
await _authenticationService.getUser(); await _authenticationService.getUser();
await _navigationService.replaceWithHomeView(); await _navigationService.replaceWithHomeView();
} else { } else {
// Removable
await _navigationService.replaceWithLoginView(); await _navigationService.replaceWithLoginView();
} }
} }

View File

@ -1,4 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/speaking_partner_image.dart'; import 'package:yimaru_app/ui/widgets/speaking_partner_image.dart';
import '../common/app_colors.dart'; import '../common/app_colors.dart';
@ -6,32 +8,37 @@ import '../common/ui_helpers.dart';
import 'custom_bottom_sheet.dart'; import 'custom_bottom_sheet.dart';
import 'custom_elevated_button.dart'; import 'custom_elevated_button.dart';
class CancelLearnPracticeSheet extends StatelessWidget { class CancelLearnPracticeSheet extends ViewModelWidget<LearnPracticeViewModel> {
final GestureTapCallback? onTap; final GestureTapCallback? onClose;
final GestureTapCallback? onCancel;
final GestureTapCallback? onContinue;
const CancelLearnPracticeSheet({super.key, this.onTap}); const CancelLearnPracticeSheet(
{super.key, this.onClose, this.onCancel, this.onContinue});
@override @override
Widget build(BuildContext context) => _buildSheetWrapper(); Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
_buildSheetWrapper(viewModel);
Widget _buildSheetWrapper() => CustomBottomSheet( Widget _buildSheetWrapper(LearnPracticeViewModel viewModel) =>
height: 500, onTap: onTap, child: _buildColumnWrapper()); CustomBottomSheet(
height: 500, onTap: onClose, child: _buildColumnWrapper(viewModel));
Widget _buildColumnWrapper() => Padding( Widget _buildColumnWrapper(LearnPracticeViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(), child: _buildColumn(viewModel),
); );
Widget _buildColumn() => Column( Widget _buildColumn(LearnPracticeViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: _buildSheetChildren(), children: _buildSheetChildren(viewModel),
); );
List<Widget> _buildSheetChildren() => [ List<Widget> _buildSheetChildren(LearnPracticeViewModel viewModel) => [
verticalSpaceLarge, verticalSpaceLarge,
_buildImage(), _buildImage(),
verticalSpaceMedium, verticalSpaceMedium,
_buildMessage(), _buildMessage(viewModel),
_buildSubtitle(), _buildSubtitle(),
verticalSpaceLarge, verticalSpaceLarge,
_buildContinueButton(), _buildContinueButton(),
@ -42,10 +49,10 @@ class CancelLearnPracticeSheet extends StatelessWidget {
radius: 45, radius: 45,
); );
Widget _buildMessage() => Text.rich( Widget _buildMessage(LearnPracticeViewModel viewModel) => Text.rich(
TextSpan(text: 'Youre almost there,', style: style18DG700, children: [ TextSpan(text: 'Youre almost there,', style: style18DG700, children: [
TextSpan( TextSpan(
text: ' Johnny!', text: ' ${viewModel.user?.firstName ?? ''}!',
style: style18P600, style: style18P600,
) )
]), ]),
@ -59,7 +66,7 @@ class CancelLearnPracticeSheet extends StatelessWidget {
Widget _buildContinueButton() => CustomElevatedButton( Widget _buildContinueButton() => CustomElevatedButton(
height: 55, height: 55,
onTap: onTap, onTap: onContinue,
borderRadius: 12, borderRadius: 12,
foregroundColor: kcWhite, foregroundColor: kcWhite,
text: 'Continue Practice', text: 'Continue Practice',
@ -68,7 +75,7 @@ class CancelLearnPracticeSheet extends StatelessWidget {
Widget _buildEndButton() => CustomElevatedButton( Widget _buildEndButton() => CustomElevatedButton(
height: 55, height: 55,
onTap: onTap, onTap: onCancel,
borderRadius: 12, borderRadius: 12,
text: 'End Session', text: 'End Session',
backgroundColor: kcWhite, backgroundColor: kcWhite,

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:iconsax/iconsax.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_module/learn_module_viewmodel.dart'; import 'package:yimaru_app/ui/views/learn_module/learn_module_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart'; import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
@ -12,29 +11,20 @@ import 'custom_elevated_button.dart';
class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> { class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
final String title; final String title;
final String topics;
final String subtitle; final String subtitle;
final String description;
final ProgressStatuses status; final ProgressStatuses status;
final List<Map<String, dynamic>> practices;
const LearnModuleTile({ const LearnModuleTile(
super.key, {super.key,
required this.title, required this.title,
required this.topics,
required this.status, required this.status,
required this.subtitle, required this.subtitle,
}); required this.practices,
required this.description});
IconData _getIcon() {
if (title.contains('Module 1')) {
return Iconsax.cake;
} else if (title.contains('Module 2')) {
return Icons.all_inbox;
} else if (title.contains('Module 3')) {
return Icons.lightbulb_outline;
} else if (title.contains('Module 4')) {
return Icons.search;
} else {
return Iconsax.pen_add;
}
}
Future<void> _showSheet( Future<void> _showSheet(
{required BuildContext context, {required BuildContext context,
@ -101,25 +91,19 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
child: _buildIcon(), child: _buildIcon(),
); );
Widget _buildIcon() => Icon( Widget _buildIcon() => const Icon(
_getIcon(), Icons.lightbulb_outline,
color: kcPrimaryColor, color: kcPrimaryColor,
); );
Widget _buildTitle() => Text( Widget _buildTitle() => Text(
title, title,
style: const TextStyle( style: style16P600,
fontSize: 16,
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
),
); );
Widget _buildContent() => Text( Widget _buildContent() => Text(
subtitle, subtitle,
style: const TextStyle( style: style14DG400,
color: kcDarkGrey,
),
); );
List<Widget> _buildExpansionTileChildren( List<Widget> _buildExpansionTileChildren(
@ -141,8 +125,8 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
{required BuildContext context, {required BuildContext context,
required LearnModuleViewModel viewModel}) => required LearnModuleViewModel viewModel}) =>
[ [
_buildProgressRow(), // _buildProgressRow(),
verticalSpaceSmall, // verticalSpaceSmall,
_buildActionButtonWrapper(context: context, viewModel: viewModel) _buildActionButtonWrapper(context: context, viewModel: viewModel)
]; ];
@ -198,7 +182,12 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
text: 'View Lessons', text: 'View Lessons',
foregroundColor: kcWhite, foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
onTap: () async => await viewModel.navigateToLearnLesson(), onTap: () async => await viewModel.navigateToLearnLesson(
title: title,
topics: topics,
subtitle: subtitle,
practices: practices,
description: description),
); );
Widget _buildPracticeButtonWrapper( Widget _buildPracticeButtonWrapper(
@ -218,9 +207,7 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
backgroundColor: kcWhite, backgroundColor: kcWhite,
borderColor: kcPrimaryColor, borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor, foregroundColor: kcPrimaryColor,
onTap: () async => status == ProgressStatuses.completed onTap: () async => await viewModel.navigateToLearnPractice(practices),
? await viewModel.navigateToLearnPractice()
: await _showSheet(context: context, viewModel: viewModel),
); );
Widget _buildSheet(LearnModuleViewModel viewModel) => FinishPracticeSheet( Widget _buildSheet(LearnModuleViewModel viewModel) => FinishPracticeSheet(

View File

@ -0,0 +1,81 @@
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import '../common/app_colors.dart';
import '../common/enmus.dart';
import '../common/ui_helpers.dart';
import '../views/learn_practice/learn_practice_viewmodel.dart';
class LearnPracticeAnswerCard extends ViewModelWidget<LearnPracticeViewModel> {
final String title;
final StateObjects object;
const LearnPracticeAnswerCard(
{super.key, required this.title, required this.object});
@override
Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
_buildContainer(viewModel);
Widget _buildContainer(LearnPracticeViewModel viewModel) => Container(
decoration: BoxDecoration(
color: kcWhite,
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
child: _buildRow(viewModel),
);
Widget _buildRow(LearnPracticeViewModel viewModel) => Row(
children: [_buildPlayButton(viewModel), _buildColumnWrapper()],
);
Widget _buildPlayButton(LearnPracticeViewModel viewModel) => ElevatedButton(
style: const ButtonStyle(
shape: WidgetStatePropertyAll(CircleBorder()),
padding: WidgetStatePropertyAll(EdgeInsets.all(5)),
shadowColor: WidgetStatePropertyAll(kcPrimaryColor),
backgroundColor: WidgetStatePropertyAll(kcPrimaryColor),
),
onPressed: () async => viewModel.player.state == PlayerState.playing
? viewModel.busyObject == StateObjects.learnPracticeSample
? await viewModel.pauseSampleAudio()
: await viewModel.pauseRecordedAudio()
: object == StateObjects.learnPracticeSample
? await viewModel.playSampleAudio()
: await viewModel.playRecordedAudio(),
child: _buildButtonState(viewModel),
);
Widget _buildButtonState(LearnPracticeViewModel viewModel) =>
viewModel.busy(object)
? viewModel.busyObject == object
? _buildPauseIcon()
: _buildPlayIcon()
: viewModel.busyObject == object &&
viewModel.player.state == PlayerState.playing
? _buildPauseIcon()
: _buildPlayIcon();
Widget _buildPlayIcon() => const Icon(
Icons.play_arrow_rounded,
size: 25,
color: kcWhite,
);
Widget _buildPauseIcon() => const Icon(
Icons.pause,
size: 25,
color: kcWhite,
);
Widget _buildColumnWrapper() => Expanded(child: _buildTitle());
Widget _buildTitle() => Text(
title,
maxLines: 1,
softWrap: false,
style: style12RP600,
);
}

View File

@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
import '../common/app_colors.dart';
import '../common/ui_helpers.dart';
import 'custom_elevated_button.dart';
class LearnPracticeCard extends ViewModelWidget<LearnPracticeViewModel> {
final int index;
final GestureTapCallback? onTap;
final Map<String, dynamic> practice;
const LearnPracticeCard(
{super.key, this.onTap, required this.index, required this.practice});
@override
Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
_buildContainer(viewModel);
Widget _buildContainer(LearnPracticeViewModel viewModel) => Container(
height: 200,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
color: kcPrimaryColor.withValues(alpha: 0.25),
),
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 12),
child: _buildColumn(viewModel),
);
Widget _buildColumn(LearnPracticeViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
children: _buildColumnChildren(viewModel));
List<Widget> _buildColumnChildren(LearnPracticeViewModel viewModel) => [
verticalSpaceTiny,
_buildTitle(),
verticalSpaceSmall,
_buildStartButtonWrapper(viewModel)
];
Widget _buildTitle() => Text(
'Practice $index',
style: style18DG700,
);
Widget _buildStartButtonWrapper(LearnPracticeViewModel viewModel) => SizedBox(
height: 40,
child: _buildStartButton(viewModel),
);
Widget _buildStartButton(LearnPracticeViewModel viewModel) =>
CustomElevatedButton(
height: 50,
width: 200,
borderRadius: 8,
text: 'Practice',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
onTap: () => viewModel.setPractice(practice),
);
}

View File

@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/learn_practice_answer_card.dart';
class LearnPracticeResultCard extends ViewModelWidget<LearnPracticeViewModel> {
final Map<String, dynamic> data;
const LearnPracticeResultCard(
{super.key, required this.data});
@override
Widget build(BuildContext context,LearnPracticeViewModel viewModel) => _buildColumnWrapper();
Widget _buildColumnWrapper() => SizedBox(
height: 100,
width: double.maxFinite,
child: _buildColumn(),
);
Widget _buildColumn() => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildColumnChildren(),
);
List<Widget> _buildColumnChildren() =>
[_buildQuestion(), verticalSpaceSmall, _buildRow()];
Widget _buildQuestion() => Text(
data['question_text'],
style: style14DG400,
);
Widget _buildRow() => Row(
children: _buildRowChildren(),
);
List<Widget> _buildRowChildren() => [
_buildSampleResponseWrapper(),
horizontalSpaceSmall,
_buildActualResponseWrapper()
];
Widget _buildSampleResponseWrapper() =>
Expanded(child: _buildSampleResponse());
Widget _buildSampleResponse() =>
const LearnPracticeAnswerCard(title: 'Sample Answer' ,object: StateObjects.learnPracticeSample,);
Widget _buildActualResponseWrapper() =>
Expanded(child: _buildActualResponse());
Widget _buildActualResponse() =>
const LearnPracticeAnswerCard(title: 'Your Answer',object: StateObjects.learnPracticeAnswer,);
}

View File

@ -1,13 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/practice_result_card.dart'; import 'package:yimaru_app/ui/widgets/learn_practice_result_card.dart';
import '../common/app_colors.dart'; import '../common/app_colors.dart';
class PracticeResultsWrapper extends StatelessWidget { class LearnPracticeResultsWrapper extends StatelessWidget {
final List<Map<String, dynamic>> data; final Map<String, dynamic> data;
const PracticeResultsWrapper({super.key, required this.data}); const LearnPracticeResultsWrapper({super.key, required this.data});
@override @override
Widget build(BuildContext context) => _buildContainer(); Widget build(BuildContext context) => _buildContainer();
@ -28,7 +28,7 @@ class PracticeResultsWrapper extends StatelessWidget {
); );
List<Widget> _buildColumnChildren() => List<Widget> _buildColumnChildren() =>
[_buildTitle(), verticalSpaceSmall, _buildResults()]; [_buildTitle(), verticalSpaceSmall, if (data.isNotEmpty) _buildResult()];
Widget _buildTitle() => Text( Widget _buildTitle() => Text(
'Conversation Review', 'Conversation Review',
@ -36,15 +36,5 @@ class PracticeResultsWrapper extends StatelessWidget {
textAlign: TextAlign.center, textAlign: TextAlign.center,
); );
Widget _buildResults() => ListView.separated( Widget _buildResult() => LearnPracticeResultCard(data: data);
shrinkWrap: true,
itemCount: data.length,
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildResult(index),
separatorBuilder: (context, index) => verticalSpaceSmall,
);
Widget _buildResult(int index) =>
PracticeResultCard(index: index + 1, data: data[index]);
} }

View File

@ -72,11 +72,7 @@ class LearnSubLevelTile extends ViewModelWidget<LearnLevelViewModel> {
Widget _buildTitle() => Text( Widget _buildTitle() => Text(
title, title,
style: const TextStyle( style: style16P600,
fontSize: 16,
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
),
); );
Widget _buildProgressStatus() => const ProgressStatus( Widget _buildProgressStatus() => const ProgressStatus(
@ -86,9 +82,7 @@ class LearnSubLevelTile extends ViewModelWidget<LearnLevelViewModel> {
Widget _buildContent() => Text( Widget _buildContent() => Text(
subtitle, subtitle,
style: const TextStyle( style: style14DG400,
color: kcDarkGrey,
),
); );
Widget _buildActionButtonWrapper(LearnLevelViewModel viewModel) => SizedBox( Widget _buildActionButtonWrapper(LearnLevelViewModel viewModel) => SizedBox(
@ -119,18 +113,17 @@ class LearnSubLevelTile extends ViewModelWidget<LearnLevelViewModel> {
); );
Widget _buildPracticeButtonWrapper(LearnLevelViewModel viewModel) => Expanded( Widget _buildPracticeButtonWrapper(LearnLevelViewModel viewModel) => Expanded(
child: _buildPracticeButton(viewModel), child: Container(),
); );
Widget _buildPracticeButton(LearnLevelViewModel viewModel) => Widget _buildPracticeButton(LearnLevelViewModel viewModel) =>
CustomElevatedButton( const CustomElevatedButton(
height: 15, height: 15,
text: 'Practice', text: 'Practice',
borderRadius: 12, borderRadius: 12,
backgroundColor: kcWhite, backgroundColor: kcWhite,
borderColor: kcPrimaryColor, borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor, foregroundColor: kcPrimaryColor
onTap: () async => await viewModel.navigateToLearnPractice() // onTap: () async => await viewModel.navigateToLearnPractice()
// onTap: () async => await viewModel.navigateToLearnLevel(),
); );
} }

View File

@ -40,25 +40,18 @@ class OverallLearnProgress extends StatelessWidget {
List<Widget> _buildProgressInfoChildren() => List<Widget> _buildProgressInfoChildren() =>
[_buildProgressInfo(), _buildProgress()]; [_buildProgressInfo(), _buildProgress()];
Widget _buildProgressInfo() => const Text( Widget _buildProgressInfo() => Text(
'Overall Progress', 'Overall Progress',
style: TextStyle( style: style16DG600,
fontSize: 16,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildProgress() => const Text( Widget _buildProgress() => Text(
'35%', '0%',
style: TextStyle( style: style14P400,
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
),
); );
Widget _buildProgressIndicator() => const CustomLinearProgressIndicator( Widget _buildProgressIndicator() => const CustomLinearProgressIndicator(
progress: 0.75, progress: 0.0,
activeColor: kcPrimaryColor, activeColor: kcPrimaryColor,
backgroundColor: kcVeryLightGrey, backgroundColor: kcVeryLightGrey,
); );

View File

@ -1,15 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_response_card.dart'; import 'package:yimaru_app/ui/widgets/custom_response_card.dart';
class PracticeResultCard extends StatelessWidget { class PracticeResultCard extends ViewModelWidget<LearnPracticeViewModel> {
final int index;
final Map<String, dynamic> data; final Map<String, dynamic> data;
const PracticeResultCard( const PracticeResultCard(
{super.key, required this.index, required this.data}); {super.key, required this.data});
@override @override
Widget build(BuildContext context) => _buildColumnWrapper(); Widget build(BuildContext context,LearnPracticeViewModel viewModel) => _buildColumnWrapper();
Widget _buildColumnWrapper() => SizedBox( Widget _buildColumnWrapper() => SizedBox(
height: 100, height: 100,
@ -27,7 +28,7 @@ class PracticeResultCard extends StatelessWidget {
[_buildQuestion(), verticalSpaceSmall, _buildRow()]; [_buildQuestion(), verticalSpaceSmall, _buildRow()];
Widget _buildQuestion() => Text( Widget _buildQuestion() => Text(
'$index. ${data['question']}', data['question_text'],
style: style14DG400, style: style14DG400,
); );

View File

@ -6,14 +6,22 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <audioplayers_linux/audioplayers_linux_plugin.h>
#include <file_selector_linux/file_selector_plugin.h> #include <file_selector_linux/file_selector_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h> #include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <record_linux/record_linux_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar);
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar); file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
g_autoptr(FlPluginRegistrar) record_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin");
record_linux_plugin_register_with_registrar(record_linux_registrar);
} }

View File

@ -3,8 +3,10 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_linux
file_selector_linux file_selector_linux
flutter_secure_storage_linux flutter_secure_storage_linux
record_linux
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@ -5,6 +5,7 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import audioplayers_darwin
import battery_plus import battery_plus
import connectivity_plus import connectivity_plus
import file_selector_macos import file_selector_macos
@ -14,11 +15,13 @@ import flutter_local_notifications
import flutter_secure_storage_darwin import flutter_secure_storage_darwin
import google_sign_in_ios import google_sign_in_ios
import package_info_plus import package_info_plus
import record_macos
import sqflite_darwin import sqflite_darwin
import video_player_avfoundation import video_player_avfoundation
import wakelock_plus import wakelock_plus
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
BatteryPlusMacosPlugin.register(with: registry.registrar(forPlugin: "BatteryPlusMacosPlugin")) BatteryPlusMacosPlugin.register(with: registry.registrar(forPlugin: "BatteryPlusMacosPlugin"))
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
@ -28,6 +31,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin")) FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))

View File

@ -57,6 +57,62 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.13.0" version: "2.13.0"
audioplayers:
dependency: "direct main"
description:
name: audioplayers
sha256: a72dd459d1a48f61a6fb9c0134dba26597c9236af40639ff0eb70eb4e0baab70
url: "https://pub.dev"
source: hosted
version: "6.6.0"
audioplayers_android:
dependency: transitive
description:
name: audioplayers_android
sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605"
url: "https://pub.dev"
source: hosted
version: "5.2.1"
audioplayers_darwin:
dependency: transitive
description:
name: audioplayers_darwin
sha256: c994b3bb3a921e4904ac40e013fbc94488e824fd7c1de6326f549943b0b44a91
url: "https://pub.dev"
source: hosted
version: "6.4.0"
audioplayers_linux:
dependency: transitive
description:
name: audioplayers_linux
sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001
url: "https://pub.dev"
source: hosted
version: "4.2.1"
audioplayers_platform_interface:
dependency: transitive
description:
name: audioplayers_platform_interface
sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656"
url: "https://pub.dev"
source: hosted
version: "7.1.1"
audioplayers_web:
dependency: transitive
description:
name: audioplayers_web
sha256: faa8fa6587f996a6f604433b53af44c57a1407d4fe8dff5766cf63d6875e8de9
url: "https://pub.dev"
source: hosted
version: "5.2.0"
audioplayers_windows:
dependency: transitive
description:
name: audioplayers_windows
sha256: bafff2b38b6f6d331887558ba6e0a01c9c208d9dbb3ad0005234db065122a734
url: "https://pub.dev"
source: hosted
version: "4.3.0"
battery_plus: battery_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@ -988,10 +1044,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.18" version: "0.12.19"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
@ -1296,6 +1352,70 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.0" version: "4.1.0"
record:
dependency: transitive
description:
name: record
sha256: d5b6b334f3ab02460db6544e08583c942dbf23e3504bf1e14fd4cbe3d9409277
url: "https://pub.dev"
source: hosted
version: "6.2.0"
record_android:
dependency: transitive
description:
name: record_android
sha256: "94783f08403aed33ffb68797bf0715b0812eb852f3c7985644c945faea462ba1"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
record_ios:
dependency: transitive
description:
name: record_ios
sha256: "8df7c136131bd05efc19256af29b2ba6ccc000ccc2c80d4b6b6d7a8d21a3b5a9"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
record_linux:
dependency: transitive
description:
name: record_linux
sha256: c31a35cc158cd666fc6395f7f56fc054f31685571684be6b97670a27649ce5c7
url: "https://pub.dev"
source: hosted
version: "1.3.0"
record_macos:
dependency: transitive
description:
name: record_macos
sha256: "084902e63fc9c0c224c29203d6c75f0bdf9b6a40536c9d916393c8f4c4256488"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
record_platform_interface:
dependency: transitive
description:
name: record_platform_interface
sha256: "8a81dbc4e14e1272a285bbfef6c9136d070a47d9b0d1f40aa6193516253ee2f6"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
record_web:
dependency: transitive
description:
name: record_web
sha256: "7e9846981c1f2d111d86f0ae3309071f5bba8b624d1c977316706f08fc31d16d"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
record_windows:
dependency: transitive
description:
name: record_windows
sha256: "223258060a1d25c62bae18282c16783f28581ec19401d17e56b5205b9f039d78"
url: "https://pub.dev"
source: hosted
version: "1.0.7"
rxdart: rxdart:
dependency: transitive dependency: transitive
description: description:
@ -1489,10 +1609,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.9" version: "0.7.10"
timezone: timezone:
dependency: transitive dependency: transitive
description: description:
@ -1653,6 +1773,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" version: "1.2.1"
waveform_flutter:
dependency: transitive
description:
name: waveform_flutter
sha256: "08c9e98d4cf119428d8b3c083ed42c11c468623eaffdf30420ae38e36662922a"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
waveform_recorder:
dependency: "direct main"
description:
name: waveform_recorder
sha256: "1ca0a19b143d1bdef2adfb3d28f0627c18aee5285235c8cf81a89bf29a0420e1"
url: "https://pub.dev"
source: hosted
version: "1.8.0"
web: web:
dependency: transitive dependency: transitive
description: description:

View File

@ -24,6 +24,7 @@ dependencies:
storage_info: ^1.0.0 storage_info: ^1.0.0
flutter_html: ^3.0.0 flutter_html: ^3.0.0
email_validator: any email_validator: any
audioplayers: ^6.6.0
video_player: ^2.10.1 video_player: ^2.10.1
firebase_core: ^4.4.0 firebase_core: ^4.4.0
in_app_update: ^4.2.5 in_app_update: ^4.2.5
@ -36,6 +37,7 @@ dependencies:
stacked_services: ^1.1.0 stacked_services: ^1.1.0
omni_datetime_picker: any omni_datetime_picker: any
json_serializable: ^6.8.0 json_serializable: ^6.8.0
waveform_recorder: ^1.8.0
permission_handler: ^12.0.1 permission_handler: ^12.0.1
firebase_messaging: ^16.1.1 firebase_messaging: ^16.1.1
cached_network_image: ^3.4.1 cached_network_image: ^3.4.1

View File

@ -14,6 +14,8 @@ import 'package:yimaru_app/services/image_downloader_service.dart';
import 'package:yimaru_app/services/notification_service.dart'; import 'package:yimaru_app/services/notification_service.dart';
import 'package:yimaru_app/services/smart_auth_service.dart'; import 'package:yimaru_app/services/smart_auth_service.dart';
import 'package:yimaru_app/services/course_service.dart'; import 'package:yimaru_app/services/course_service.dart';
import 'package:yimaru_app/services/audio_player_service.dart';
import 'package:yimaru_app/services/voice_recorder_service.dart';
// @stacked-import // @stacked-import
import 'test_helpers.mocks.dart'; import 'test_helpers.mocks.dart';
@ -38,6 +40,8 @@ import 'test_helpers.mocks.dart';
MockSpec<NotificationService>(onMissingStub: OnMissingStub.returnDefault), MockSpec<NotificationService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<SmartAuthService>(onMissingStub: OnMissingStub.returnDefault), MockSpec<SmartAuthService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<CourseService>(onMissingStub: OnMissingStub.returnDefault), MockSpec<CourseService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<AudioPlayerService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<VoiceRecorderService>(onMissingStub: OnMissingStub.returnDefault),
// @stacked-mock-spec // @stacked-mock-spec
], ],
) )
@ -57,6 +61,8 @@ void registerServices() {
getAndRegisterNotificationService(); getAndRegisterNotificationService();
getAndRegisterSmartAuthService(); getAndRegisterSmartAuthService();
getAndRegisterCourseService(); getAndRegisterCourseService();
getAndRegisterAudioPlayerService();
getAndRegisterVoiceRecorderService();
// @stacked-mock-register // @stacked-mock-register
} }
@ -197,6 +203,20 @@ MockCourseService getAndRegisterCourseService() {
locator.registerSingleton<CourseService>(service); locator.registerSingleton<CourseService>(service);
return service; return service;
} }
MockAudioPlayerService getAndRegisterAudioPlayerService() {
_removeRegistrationIfExists<AudioPlayerService>();
final service = MockAudioPlayerService();
locator.registerSingleton<AudioPlayerService>(service);
return service;
}
MockVoiceRecorderService getAndRegisterVoiceRecorderService() {
_removeRegistrationIfExists<VoiceRecorderService>();
final service = MockVoiceRecorderService();
locator.registerSingleton<VoiceRecorderService>(service);
return service;
}
// @stacked-mock-create // @stacked-mock-create
void _removeRegistrationIfExists<T extends Object>() { void _removeRegistrationIfExists<T extends Object>() {

File diff suppressed because it is too large Load Diff

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

View File

@ -6,14 +6,18 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <audioplayers_windows/audioplayers_windows_plugin.h>
#include <battery_plus/battery_plus_windows_plugin.h> #include <battery_plus/battery_plus_windows_plugin.h>
#include <connectivity_plus/connectivity_plus_windows_plugin.h> #include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <file_selector_windows/file_selector_windows.h> #include <file_selector_windows/file_selector_windows.h>
#include <firebase_core/firebase_core_plugin_c_api.h> #include <firebase_core/firebase_core_plugin_c_api.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h> #include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h> #include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <record_windows/record_windows_plugin_c_api.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
AudioplayersWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
BatteryPlusWindowsPluginRegisterWithRegistrar( BatteryPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("BatteryPlusWindowsPlugin")); registry->GetRegistrarForPlugin("BatteryPlusWindowsPlugin"));
ConnectivityPlusWindowsPluginRegisterWithRegistrar( ConnectivityPlusWindowsPluginRegisterWithRegistrar(
@ -26,4 +30,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
PermissionHandlerWindowsPluginRegisterWithRegistrar( PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
RecordWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
} }

View File

@ -3,12 +3,14 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_windows
battery_plus battery_plus
connectivity_plus connectivity_plus
file_selector_windows file_selector_windows
firebase_core firebase_core
flutter_secure_storage_windows flutter_secure_storage_windows
permission_handler_windows permission_handler_windows
record_windows
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST