Compare commits
3 Commits
c368404a83
...
9be914f884
| Author | SHA1 | Date | |
|---|---|---|---|
| 9be914f884 | |||
| 177b315f95 | |||
| 413502db11 |
|
|
@ -14,11 +14,11 @@
|
|||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "574860813475-glgnkruic7dflaomb59el8994b7hhfga.apps.googleusercontent.com",
|
||||
"client_id": "574860813475-3p3k63lkrfd113sn6jscgvdj0aigsg5s.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "com.yimaru.lms.app",
|
||||
"certificate_hash": "378836a3aa9f36958b6c6c69bc67e3195352f68d"
|
||||
"certificate_hash": "928ead08b5e39d6a861a55ae7cceb8c402d1ee7a"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
|||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
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
|
||||
|
|
|
|||
|
|
@ -19,9 +19,10 @@ pluginManagement {
|
|||
|
||||
plugins {
|
||||
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("com.google.gms.google-services") version("4.4.4") apply false
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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/ui/views/course_subcategory/course_subcategory_view.dart';
|
||||
import 'package:yimaru_app/ui/views/course/course_view.dart';
|
||||
import 'package:yimaru_app/services/audio_player_service.dart';
|
||||
import 'package:yimaru_app/services/voice_recorder_service.dart';
|
||||
// @stacked-import
|
||||
|
||||
@StackedApp(
|
||||
|
|
@ -104,6 +106,8 @@ import 'package:yimaru_app/ui/views/course/course_view.dart';
|
|||
LazySingleton(classType: NotificationService),
|
||||
LazySingleton(classType: SmartAuthService),
|
||||
LazySingleton(classType: CourseService),
|
||||
LazySingleton(classType: AudioPlayerService),
|
||||
LazySingleton(classType: VoiceRecorderService),
|
||||
// @stacked-service
|
||||
],
|
||||
bottomsheets: [
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import 'package:stacked_services/src/navigation/navigation_service.dart';
|
|||
import 'package:stacked_shared/stacked_shared.dart';
|
||||
|
||||
import '../services/api_service.dart';
|
||||
import '../services/audio_player_service.dart';
|
||||
import '../services/authentication_service.dart';
|
||||
import '../services/course_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/smart_auth_service.dart';
|
||||
import '../services/status_checker_service.dart';
|
||||
import '../services/voice_recorder_service.dart';
|
||||
|
||||
final locator = StackedLocator.instance;
|
||||
|
||||
|
|
@ -50,4 +52,6 @@ Future<void> setupLocator({
|
|||
locator.registerLazySingleton(() => NotificationService());
|
||||
locator.registerLazySingleton(() => SmartAuthService());
|
||||
locator.registerLazySingleton(() => CourseService());
|
||||
locator.registerLazySingleton(() => AudioPlayerService());
|
||||
locator.registerLazySingleton(() => VoiceRecorderService());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -445,8 +445,15 @@ class StackedRouter extends _i1.RouterBase {
|
|||
);
|
||||
},
|
||||
_i23.LearnLessonView: (data) {
|
||||
final args = data.getArgs<LearnLessonViewArguments>(nullOk: false);
|
||||
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,
|
||||
);
|
||||
},
|
||||
|
|
@ -457,8 +464,13 @@ class StackedRouter extends _i1.RouterBase {
|
|||
);
|
||||
},
|
||||
_i25.LearnLessonDetailView: (data) {
|
||||
final args = data.getArgs<LearnLessonDetailViewArguments>(nullOk: false);
|
||||
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,
|
||||
);
|
||||
},
|
||||
|
|
@ -469,6 +481,7 @@ class StackedRouter extends _i1.RouterBase {
|
|||
key: args.key,
|
||||
title: args.title,
|
||||
subtitle: args.subtitle,
|
||||
practices: args.practices,
|
||||
buttonLabel: args.buttonLabel),
|
||||
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 {
|
||||
const LearnPracticeViewArguments({
|
||||
this.key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.practices,
|
||||
required this.buttonLabel,
|
||||
});
|
||||
|
||||
|
|
@ -618,11 +720,13 @@ class LearnPracticeViewArguments {
|
|||
|
||||
final String subtitle;
|
||||
|
||||
final List<Map<String, dynamic>> practices;
|
||||
|
||||
final String buttonLabel;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '{"key": "$key", "title": "$title", "subtitle": "$subtitle", "buttonLabel": "$buttonLabel"}';
|
||||
return '{"key": "$key", "title": "$title", "subtitle": "$subtitle", "practices": "$practices", "buttonLabel": "$buttonLabel"}';
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -631,6 +735,7 @@ class LearnPracticeViewArguments {
|
|||
return other.key == key &&
|
||||
other.title == title &&
|
||||
other.subtitle == subtitle &&
|
||||
other.practices == practices &&
|
||||
other.buttonLabel == buttonLabel;
|
||||
}
|
||||
|
||||
|
|
@ -639,6 +744,7 @@ class LearnPracticeViewArguments {
|
|||
return key.hashCode ^
|
||||
title.hashCode ^
|
||||
subtitle.hashCode ^
|
||||
practices.hashCode ^
|
||||
buttonLabel.hashCode;
|
||||
}
|
||||
}
|
||||
|
|
@ -1133,14 +1239,27 @@ extension NavigatorStateExtension on _i41.NavigationService {
|
|||
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,
|
||||
bool preventDuplicates = true,
|
||||
Map<String, String>? parameters,
|
||||
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
|
||||
transition,
|
||||
]) async {
|
||||
}) async {
|
||||
return navigateTo<dynamic>(Routes.learnLessonView,
|
||||
arguments: LearnLessonViewArguments(
|
||||
key: key,
|
||||
title: title,
|
||||
topics: topics,
|
||||
subtitle: subtitle,
|
||||
practices: practices,
|
||||
description: description),
|
||||
id: routerId,
|
||||
preventDuplicates: preventDuplicates,
|
||||
parameters: parameters,
|
||||
|
|
@ -1161,14 +1280,23 @@ extension NavigatorStateExtension on _i41.NavigationService {
|
|||
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,
|
||||
bool preventDuplicates = true,
|
||||
Map<String, String>? parameters,
|
||||
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
|
||||
transition,
|
||||
]) async {
|
||||
}) async {
|
||||
return navigateTo<dynamic>(Routes.learnLessonDetailView,
|
||||
arguments: LearnLessonDetailViewArguments(
|
||||
key: key,
|
||||
title: title,
|
||||
practices: practices,
|
||||
description: description),
|
||||
id: routerId,
|
||||
preventDuplicates: preventDuplicates,
|
||||
parameters: parameters,
|
||||
|
|
@ -1179,6 +1307,7 @@ extension NavigatorStateExtension on _i41.NavigationService {
|
|||
_i36.Key? key,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required List<Map<String, dynamic>> practices,
|
||||
required String buttonLabel,
|
||||
int? routerId,
|
||||
bool preventDuplicates = true,
|
||||
|
|
@ -1191,6 +1320,7 @@ extension NavigatorStateExtension on _i41.NavigationService {
|
|||
key: key,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
practices: practices,
|
||||
buttonLabel: buttonLabel),
|
||||
id: routerId,
|
||||
preventDuplicates: preventDuplicates,
|
||||
|
|
@ -1645,14 +1775,27 @@ extension NavigatorStateExtension on _i41.NavigationService {
|
|||
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,
|
||||
bool preventDuplicates = true,
|
||||
Map<String, String>? parameters,
|
||||
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
|
||||
transition,
|
||||
]) async {
|
||||
}) async {
|
||||
return replaceWith<dynamic>(Routes.learnLessonView,
|
||||
arguments: LearnLessonViewArguments(
|
||||
key: key,
|
||||
title: title,
|
||||
topics: topics,
|
||||
subtitle: subtitle,
|
||||
practices: practices,
|
||||
description: description),
|
||||
id: routerId,
|
||||
preventDuplicates: preventDuplicates,
|
||||
parameters: parameters,
|
||||
|
|
@ -1673,14 +1816,23 @@ extension NavigatorStateExtension on _i41.NavigationService {
|
|||
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,
|
||||
bool preventDuplicates = true,
|
||||
Map<String, String>? parameters,
|
||||
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
|
||||
transition,
|
||||
]) async {
|
||||
}) async {
|
||||
return replaceWith<dynamic>(Routes.learnLessonDetailView,
|
||||
arguments: LearnLessonDetailViewArguments(
|
||||
key: key,
|
||||
title: title,
|
||||
practices: practices,
|
||||
description: description),
|
||||
id: routerId,
|
||||
preventDuplicates: preventDuplicates,
|
||||
parameters: parameters,
|
||||
|
|
@ -1691,6 +1843,7 @@ extension NavigatorStateExtension on _i41.NavigationService {
|
|||
_i36.Key? key,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required List<Map<String, dynamic>> practices,
|
||||
required String buttonLabel,
|
||||
int? routerId,
|
||||
bool preventDuplicates = true,
|
||||
|
|
@ -1703,6 +1856,7 @@ extension NavigatorStateExtension on _i41.NavigationService {
|
|||
key: key,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
practices: practices,
|
||||
buttonLabel: buttonLabel),
|
||||
id: routerId,
|
||||
preventDuplicates: preventDuplicates,
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class DefaultFirebaseOptions {
|
|||
projectId: 'yimaru-lms-e834e',
|
||||
storageBucket: 'yimaru-lms-e834e.firebasestorage.app',
|
||||
androidClientId:
|
||||
'574860813475-glgnkruic7dflaomb59el8994b7hhfga.apps.googleusercontent.com',
|
||||
'574860813475-3p3k63lkrfd113sn6jscgvdj0aigsg5s.apps.googleusercontent.com',
|
||||
iosBundleId: 'com.yimaru.lms.app',
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,12 +25,16 @@ class Practice {
|
|||
@JsonKey(name: 'shuffle_questions')
|
||||
final bool? shuffleQuestions;
|
||||
|
||||
|
||||
|
||||
const Practice({
|
||||
this.id,
|
||||
this.title,this.status,this.setType,this.persona,this.ownerId,this.ownerType,this.description,this.shuffleQuestions
|
||||
});
|
||||
const Practice(
|
||||
{this.id,
|
||||
this.title,
|
||||
this.status,
|
||||
this.setType,
|
||||
this.persona,
|
||||
this.ownerId,
|
||||
this.ownerType,
|
||||
this.description,
|
||||
this.shuffleQuestions});
|
||||
|
||||
factory Practice.fromJson(Map<String, dynamic> json) =>
|
||||
_$PracticeFromJson(json);
|
||||
|
|
|
|||
|
|
@ -512,20 +512,19 @@ class ApiService {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// Course practices
|
||||
Future<List<Practice>> getCoursePractices(Map<String,dynamic> data) async {
|
||||
Future<List<Practice>> getCoursePractices(Map<String, dynamic> data) async {
|
||||
try {
|
||||
List<Practice> coursePractices = [];
|
||||
|
||||
final Response response = await _service.dio.get(
|
||||
'$kBaseUrl/$kPracticeBaseUrl/$kCoursePractice',data: data);
|
||||
final Response response = await _service.dio
|
||||
.get('$kBaseUrl/$kPracticeBaseUrl/$kCoursePractice', data: data);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
var data = response.data;
|
||||
var decodedData = data['data'] as List;
|
||||
coursePractices = decodedData.map(
|
||||
(e) {
|
||||
(e) {
|
||||
return Practice.fromJson(e);
|
||||
},
|
||||
).toList();
|
||||
|
|
@ -537,20 +536,19 @@ class ApiService {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// Course practic questions
|
||||
Future<List<PracticeQuestion>> getCoursePracticeQuestions(int id) async {
|
||||
try {
|
||||
List<PracticeQuestion> coursePracticeQuestions = [];
|
||||
|
||||
final Response response = await _service.dio.get(
|
||||
'$kBaseUrl/$kPracticeBaseUrl/$id/$kCoursePracticeQuestions');
|
||||
final Response response = await _service.dio
|
||||
.get('$kBaseUrl/$kPracticeBaseUrl/$id/$kCoursePracticeQuestions');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
var data = response.data;
|
||||
var decodedData = data['data'] as List;
|
||||
coursePracticeQuestions = decodedData.map(
|
||||
(e) {
|
||||
(e) {
|
||||
return PracticeQuestion.fromJson(e);
|
||||
},
|
||||
).toList();
|
||||
|
|
|
|||
41
lib/services/audio_player_service.dart
Normal file
41
lib/services/audio_player_service.dart
Normal 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);
|
||||
}
|
||||
35
lib/services/voice_recorder_service.dart
Normal file
35
lib/services/voice_recorder_service.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,6 @@ String kSubcoursesUrl = 'sub-courses';
|
|||
|
||||
String kCompleteLessonUrl = 'complete';
|
||||
|
||||
|
||||
String kResetPassword = 'resetPassword';
|
||||
|
||||
String kCourseCategoryUrl = 'categories';
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
// Login method
|
||||
enum LoginMethod { phone, email, google }
|
||||
|
||||
// Response status
|
||||
enum ResponseStatus { success, failure }
|
||||
|
||||
// Sign-up method
|
||||
enum SignUpMethod { phone, email, google }
|
||||
|
||||
// Response status
|
||||
enum ResponseStatus { success, failure }
|
||||
// Voice recording state
|
||||
enum VoiceRecordingState { pending, recording }
|
||||
|
||||
// Levels
|
||||
enum ProficiencyLevels { a1, a2, b1, b2, none }
|
||||
|
|
@ -18,6 +21,7 @@ enum DuolingoAssessmentType { speaking, reading, writing, listening }
|
|||
|
||||
// State object
|
||||
enum StateObjects {
|
||||
none,
|
||||
courses,
|
||||
homeView,
|
||||
register,
|
||||
|
|
@ -37,5 +41,10 @@ enum StateObjects {
|
|||
courseCategories,
|
||||
profileCompletion,
|
||||
registerWithGoogle,
|
||||
learnPracticeSample,
|
||||
learnPracticeAnswer,
|
||||
loginWithPhoneNumber,
|
||||
learnPracticeQuestion,
|
||||
recordLearnPracticeAnswer,
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,3 +43,28 @@ Color getColor() {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -237,6 +237,8 @@ class AssessmentViewModel extends BaseViewModel {
|
|||
}
|
||||
|
||||
// Navigation
|
||||
void pop() => _navigationService.back();
|
||||
|
||||
Future<void> navigateToLanguage() async =>
|
||||
await _navigationService.navigateToLanguageView();
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
isLoading: viewModel.isBusy,
|
||||
isEmpty: viewModel.assessments.isEmpty,
|
||||
onTap: () async => await viewModel.getAssessments(),
|
||||
onPop: viewModel.assessments.isEmpty ? viewModel.pop : null,
|
||||
);
|
||||
|
||||
Widget _buildAssessmentScreensWrapper(AssessmentViewModel viewModel) =>
|
||||
|
|
|
|||
|
|
@ -8,9 +8,14 @@ import '../../../widgets/refresh_button.dart';
|
|||
class AssessmentLoadingScreen extends StatelessWidget {
|
||||
final bool isEmpty;
|
||||
final bool isLoading;
|
||||
final GestureTapCallback? onPop;
|
||||
final GestureTapCallback? onTap;
|
||||
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
|
||||
Widget build(BuildContext context) => _buildScaffoldWrapper();
|
||||
|
|
@ -35,7 +40,8 @@ class AssessmentLoadingScreen extends StatelessWidget {
|
|||
|
||||
List<Widget> _buildColumnChildren() => [_buildAppBar(), _buildBody()];
|
||||
|
||||
Widget _buildAppBar() => const LargeAppBar(
|
||||
Widget _buildAppBar() => LargeAppBar(
|
||||
onPop: onPop,
|
||||
showBackButton: true,
|
||||
showLanguageSelection: true,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -50,8 +50,7 @@ class CourseCategoryViewModel extends ReactiveViewModel {
|
|||
if (await _statusChecker.checkConnection()) {
|
||||
_categories = await _apiService.getCourseCategories();
|
||||
|
||||
rebuildUi();
|
||||
|
||||
rebuildUi();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ class CourseLessonDetailView extends StackedView<CourseLessonDetailViewModel> {
|
|||
);
|
||||
|
||||
Widget _buildPracticeButton(CourseLessonDetailViewModel viewModel) =>
|
||||
CustomElevatedButton(
|
||||
CustomElevatedButton(
|
||||
height: 55,
|
||||
text: 'Practice',
|
||||
borderRadius: 12,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class CoursePracticeViewModel extends BaseViewModel {
|
|||
final _navigationService = locator<NavigationService>();
|
||||
|
||||
// Course practices
|
||||
List<Practice> _coursePractices = [];
|
||||
List<Practice> _coursePractices = [];
|
||||
|
||||
List<Practice> get coursePractices => _coursePractices;
|
||||
|
||||
|
|
@ -32,14 +32,10 @@ class CoursePracticeViewModel extends BaseViewModel {
|
|||
|
||||
Future<void> _getCoursePractice(int id) async {
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
Map<String,dynamic> data = {
|
||||
'owner_id':id,
|
||||
'owner_type':'SUB_COURSE'
|
||||
};
|
||||
Map<String, dynamic> data = {'owner_id': id, 'owner_type': 'SUB_COURSE'};
|
||||
_coursePractices = await _apiService.getCoursePractices(data);
|
||||
|
||||
rebuildUi();
|
||||
|
||||
rebuildUi();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,8 +41,7 @@ class CourseSubcategoryViewModel extends BaseViewModel {
|
|||
if (await _statusChecker.checkConnection()) {
|
||||
_subcategories = await _apiService.getCourseSubcategories(id);
|
||||
|
||||
rebuildUi();
|
||||
|
||||
rebuildUi();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ class DuolingoView extends StackedView<DuolingoViewModel> with $DuolingoView {
|
|||
);
|
||||
|
||||
Widget _buildBody(DuolingoViewModel viewModel) => IndexedStack(
|
||||
index: viewModel.currentIndex, children: _buildScreens(viewModel));
|
||||
index: viewModel.currentPage, children: _buildScreens(viewModel));
|
||||
|
||||
List<Widget> _buildScreens(DuolingoViewModel viewModel) => [
|
||||
_buildDuolingoAssessmentsScreen(),
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ class DuolingoViewModel extends FormViewModel {
|
|||
bool get isSpeaking => _isSpeaking;
|
||||
|
||||
// In-app navigation
|
||||
int _currentIndex = 0;
|
||||
int _currentPage = 0;
|
||||
|
||||
int get currentIndex => _currentIndex;
|
||||
int get currentPage => _currentPage;
|
||||
|
||||
// Assessments
|
||||
Map<String, dynamic> _selectedAssessment = {
|
||||
|
|
@ -200,15 +200,15 @@ class DuolingoViewModel extends FormViewModel {
|
|||
|
||||
// In-app navigation
|
||||
void goTo(int page) {
|
||||
_currentIndex = page;
|
||||
_currentPage = page;
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
void goBack() {
|
||||
if (_currentIndex == 0) {
|
||||
if (_currentPage == 0) {
|
||||
pop();
|
||||
} else {
|
||||
_currentIndex--;
|
||||
_currentPage--;
|
||||
rebuildUi();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ class HomeView extends StackedView<HomeViewModel> {
|
|||
|
||||
@override
|
||||
void onViewModelReady(HomeViewModel viewModel) async {
|
||||
// Removable
|
||||
await _init(viewModel);
|
||||
super.onViewModelReady(viewModel);
|
||||
}
|
||||
|
|
@ -37,7 +38,7 @@ class HomeView extends StackedView<HomeViewModel> {
|
|||
Widget _buildStartUpView() => const StartupView(label: 'Checking user info');
|
||||
|
||||
Widget _buildScaffold(HomeViewModel viewModel) => Scaffold(
|
||||
body: getViewForIndex(viewModel.currentIndex),
|
||||
body: getViewForIndex(viewModel.currentPage),
|
||||
bottomNavigationBar: _buildBottomNav(viewModel),
|
||||
);
|
||||
|
||||
|
|
@ -47,7 +48,7 @@ class HomeView extends StackedView<HomeViewModel> {
|
|||
selectedItemColor: kcPrimaryColor,
|
||||
backgroundColor: kcBackgroundColor,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
currentIndex: viewModel.currentIndex,
|
||||
currentIndex: viewModel.currentPage,
|
||||
);
|
||||
|
||||
List<BottomNavigationBarItem> _buildNavBarItems() => [
|
||||
|
|
|
|||
|
|
@ -33,13 +33,13 @@ class HomeViewModel extends ReactiveViewModel {
|
|||
UserModel? get user => _user;
|
||||
|
||||
// Bottom navigation
|
||||
int _currentIndex = 0;
|
||||
int _currentPage = 0;
|
||||
|
||||
int get currentIndex => _currentIndex;
|
||||
int get currentPage => _currentPage;
|
||||
|
||||
// Bottom navigation
|
||||
void setCurrentIndex(int index) {
|
||||
_currentIndex = index;
|
||||
_currentPage = index;
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,19 +23,19 @@ class LearnViewModel extends ReactiveViewModel {
|
|||
final List<Map<String, dynamic>> _learnLevels = [
|
||||
{
|
||||
'title': 'Beginner',
|
||||
'status': ProgressStatuses.completed,
|
||||
'status': ProgressStatuses.started,
|
||||
'subtitle': 'Start your journey with the basics of English.',
|
||||
},
|
||||
{
|
||||
'title': 'Intermediate',
|
||||
'status': ProgressStatuses.started,
|
||||
'subtitle': 'Practice real conversations and expand vocabulary.',
|
||||
},
|
||||
{
|
||||
'title': 'Advanced',
|
||||
'status': ProgressStatuses.pending,
|
||||
'subtitle': 'Achieve fluency and master complex topics.',
|
||||
},
|
||||
// {
|
||||
// 'title': 'Intermediate',
|
||||
// 'status': ProgressStatuses.started,
|
||||
// 'subtitle': 'Practice real conversations and expand vocabulary.',
|
||||
// },
|
||||
// {
|
||||
// 'title': 'Advanced',
|
||||
// 'status': ProgressStatuses.pending,
|
||||
// 'subtitle': 'Achieve fluency and master complex topics.',
|
||||
// },
|
||||
];
|
||||
|
||||
List<Map<String, dynamic>> get learnLevels => _learnLevels;
|
||||
|
|
|
|||
|
|
@ -12,12 +12,30 @@ import '../../widgets/small_app_bar.dart';
|
|||
import 'learn_lesson_viewmodel.dart';
|
||||
|
||||
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
|
||||
LearnLessonViewModel viewModelBuilder(
|
||||
BuildContext context,
|
||||
) =>
|
||||
LearnLessonViewModel viewModelBuilder(BuildContext context) =>
|
||||
LearnLessonViewModel();
|
||||
|
||||
@override
|
||||
|
|
@ -26,27 +44,38 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
|
|||
LearnLessonViewModel viewModel,
|
||||
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,
|
||||
body: _buildScaffold(viewModel),
|
||||
body: _buildScaffold(context: context, viewModel: viewModel),
|
||||
);
|
||||
|
||||
Widget _buildScaffold(LearnLessonViewModel viewModel) =>
|
||||
SafeArea(child: _buildBody(viewModel));
|
||||
Widget _buildScaffold(
|
||||
{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),
|
||||
child: _buildColumn(viewModel),
|
||||
child: _buildColumn(context: context, viewModel: viewModel),
|
||||
);
|
||||
|
||||
Widget _buildColumn(LearnLessonViewModel viewModel) => Column(
|
||||
Widget _buildColumn(
|
||||
{required BuildContext context,
|
||||
required LearnLessonViewModel viewModel}) =>
|
||||
Column(
|
||||
children: [
|
||||
verticalSpaceMedium,
|
||||
_buildAppBar(viewModel),
|
||||
verticalSpaceMedium,
|
||||
_buildLevelsColumnWrapper(viewModel),
|
||||
_buildLevelsColumnWrapper(context: context, viewModel: viewModel),
|
||||
],
|
||||
);
|
||||
|
||||
|
|
@ -55,62 +84,99 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
|
|||
showBackButton: true,
|
||||
);
|
||||
|
||||
Widget _buildLevelsColumnWrapper(LearnLessonViewModel viewModel) =>
|
||||
Expanded(child: _buildLevelsColumnScrollView(viewModel));
|
||||
Widget _buildLevelsColumnWrapper(
|
||||
{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(
|
||||
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,
|
||||
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,
|
||||
_buildTitle(),
|
||||
verticalSpaceTiny,
|
||||
_buildSubtitle(),
|
||||
verticalSpaceSmall,
|
||||
_buildModuleProgress(),
|
||||
verticalSpaceMedium,
|
||||
_buildContinueButton(),
|
||||
verticalSpaceMedium,
|
||||
_buildMotivationCard(),
|
||||
verticalSpaceMedium,
|
||||
_buildHeader(),
|
||||
verticalSpaceMedium,
|
||||
_buildListView(viewModel),
|
||||
verticalSpaceMedium
|
||||
_buildTopics(),
|
||||
verticalSpaceSmall,
|
||||
// _buildModuleProgress(),
|
||||
// verticalSpaceMedium,
|
||||
// _buildContinueButton(),
|
||||
// verticalSpaceMedium,
|
||||
// _buildMotivationCard(),
|
||||
// verticalSpaceMedium,
|
||||
//_buildHeader(),
|
||||
//verticalSpaceMedium,
|
||||
// _buildListView(viewModel),
|
||||
getPadding(context),
|
||||
_buildStartButton(viewModel),
|
||||
verticalSpaceSmall,
|
||||
_buildPracticeButton(viewModel)
|
||||
];
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
'Module 1: Greetings & Introductions',
|
||||
title,
|
||||
style: style16DG600,
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
'Learn how to introduce yourself, talk about your surroundings, and start simple conversations.',
|
||||
style: style14DG400,
|
||||
subtitle,
|
||||
style: style14DG600,
|
||||
);
|
||||
|
||||
Widget _buildTopics() => Text(
|
||||
topics,
|
||||
style: style14DG500,
|
||||
);
|
||||
|
||||
Widget _buildModuleProgress() => const ModuleProgress();
|
||||
|
||||
Widget _buildContinueButton() => const CustomElevatedButton(
|
||||
Widget _buildStartButton(LearnLessonViewModel viewModel) =>
|
||||
CustomElevatedButton(
|
||||
height: 55,
|
||||
borderRadius: 12,
|
||||
text: 'Start $title',
|
||||
foregroundColor: kcWhite,
|
||||
text: 'Continue Lesson 1.3',
|
||||
backgroundColor: kcPrimaryColor,
|
||||
onTap: () async => await viewModel.navigateToLearnLessonDetail(
|
||||
title: title, practices: practices, description: description),
|
||||
);
|
||||
|
||||
Widget _buildPracticeButton(LearnLessonViewModel viewModel) =>
|
||||
CustomElevatedButton(
|
||||
height: 55,
|
||||
borderRadius: 12,
|
||||
text: 'Practice',
|
||||
backgroundColor: kcWhite,
|
||||
borderColor: kcPrimaryColor,
|
||||
foregroundColor: kcPrimaryColor,
|
||||
onTap: () async =>
|
||||
await viewModel.navigateToLearnPractice(practices));
|
||||
|
||||
Widget _buildMotivationCard() => const MotivationCard();
|
||||
|
||||
Widget _buildHeader() => Text(
|
||||
'Module 1: Greetings & Introductions',
|
||||
title,
|
||||
style: style18DG700,
|
||||
);
|
||||
|
||||
|
|
@ -122,9 +188,9 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
|
|||
title: viewModel.lessons[index]['title'],
|
||||
status: viewModel.lessons[index]['status'],
|
||||
thumbnail: viewModel.lessons[index]['thumbnail'],
|
||||
onLessonTap: () async =>
|
||||
await viewModel.navigateToLearnLessonDetail(),
|
||||
onPracticeTap: () async => await viewModel.navigateToLearnPractice(),
|
||||
onLessonTap: () async => await viewModel.navigateToLearnLessonDetail(
|
||||
title: title, practices: practices, description: description),
|
||||
// onPracticeTap: () async => await viewModel.navigateToLearnPractice(),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -32,14 +32,19 @@ class LearnLessonViewModel extends BaseViewModel {
|
|||
// Navigation
|
||||
void pop() => _navigationService.back();
|
||||
|
||||
Future<void> navigateToLearnLessonDetail() async =>
|
||||
await _navigationService.navigateToLearnLessonDetailView();
|
||||
Future<void> navigateToLearnLessonDetail(
|
||||
{required String title,
|
||||
required List<Map<String, dynamic>> practices,
|
||||
required String description}) async =>
|
||||
await _navigationService.navigateToLearnLessonDetailView(
|
||||
title: title, practices: practices, description: description);
|
||||
|
||||
Future<void> navigateToLearnPractice() async =>
|
||||
Future<void> navigateToLearnPractice(
|
||||
List<Map<String, dynamic>> practices) async =>
|
||||
await _navigationService.navigateToLearnPracticeView(
|
||||
buttonLabel: 'Start Practice',
|
||||
title: 'Let \'s practice what you just learnt!',
|
||||
subtitle:
|
||||
'I’ll ask you a few questions, and you can respond naturally.',
|
||||
practices: practices,
|
||||
title: 'Let’s Practice',
|
||||
buttonLabel: 'Begin Lesson Practice',
|
||||
subtitle: 'Let’s quickly review what you’ve learned in this lesson!',
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,19 +11,28 @@ import '../../widgets/small_app_bar.dart';
|
|||
import 'learn_lesson_detail_viewmodel.dart';
|
||||
|
||||
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 {
|
||||
await viewModel.pause();
|
||||
await viewModel.navigateToLearnPractice();
|
||||
await viewModel.navigateToLearnPractice(practices);
|
||||
}
|
||||
|
||||
// @override
|
||||
// void onDispose(LearnLessonDetailViewModel viewModel) {
|
||||
// print('DISPOSED');
|
||||
// viewModel.dispose();
|
||||
// super.onDispose(viewModel);
|
||||
// }
|
||||
@override
|
||||
void onDispose(LearnLessonDetailViewModel viewModel) {
|
||||
print('DISPOSED');
|
||||
viewModel.close();
|
||||
super.onDispose(viewModel);
|
||||
}
|
||||
|
||||
@override
|
||||
void onViewModelReady(LearnLessonDetailViewModel viewModel) async {
|
||||
|
|
@ -116,7 +125,7 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
|
|||
);
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
'1.3 Common Greetings',
|
||||
title,
|
||||
style: style16DG600,
|
||||
);
|
||||
|
||||
|
|
@ -149,7 +158,7 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
|
|||
);
|
||||
|
||||
Widget _buildDescription() => Text(
|
||||
'In this lesson, you’ll explore how to start simple conversations by greeting others in polite and friendly ways. You’ll practice different greetings for morning, afternoon, and evening, as well as casual and formal situations. By the end, you’ll know how to confidently say hello, ask how someone is, and respond naturally.',
|
||||
description,
|
||||
style: style14DG600,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -55,18 +55,18 @@ class LearnLessonDetailViewModel extends BaseViewModel {
|
|||
await _chewieController?.pause();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
void close() {
|
||||
_videoPlayerController?.dispose();
|
||||
_chewieController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Navigation
|
||||
void pop() => _navigationService.back();
|
||||
|
||||
Future<void> navigateToLearnPractice() async =>
|
||||
Future<void> navigateToLearnPractice(
|
||||
List<Map<String, dynamic>> practices) async =>
|
||||
await _navigationService.navigateToLearnPracticeView(
|
||||
practices: practices,
|
||||
buttonLabel: 'Start Practice',
|
||||
title: 'Let \'s practice what you just learnt!',
|
||||
subtitle:
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ class LearnLevelViewModel extends BaseViewModel {
|
|||
'current': true,
|
||||
'subtitle': 'Start your journey with the basics of English.',
|
||||
},
|
||||
{
|
||||
'title': 'A2',
|
||||
'current': false,
|
||||
'subtitle': 'Build upon your foundational knowledge.',
|
||||
},
|
||||
// {
|
||||
// 'title': 'A2',
|
||||
// 'current': false,
|
||||
// 'subtitle': 'Build upon your foundational knowledge.',
|
||||
// },
|
||||
];
|
||||
|
||||
List<Map<String, dynamic>> get learnSubLevels => _learnSubLevels;
|
||||
|
|
@ -27,11 +27,4 @@ class LearnLevelViewModel extends BaseViewModel {
|
|||
|
||||
Future<void> navigateToLearnModule() async =>
|
||||
_navigationService.navigateToLearnModuleView();
|
||||
|
||||
Future<void> navigateToLearnPractice() async =>
|
||||
await _navigationService.navigateToLearnPracticeView(
|
||||
title: 'Let’s Practice Level 1',
|
||||
buttonLabel: 'Begin Level Practice',
|
||||
subtitle: 'Let’s quickly review what you’ve learned in this level! ',
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,17 +94,25 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
|
|||
itemBuilder: (context, index) => _buildTile(
|
||||
title: viewModel.modules[index]['title'],
|
||||
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({
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required ProgressStatuses status,
|
||||
}) =>
|
||||
Widget _buildTile(
|
||||
{required String title,
|
||||
required String topics,
|
||||
required String subtitle,
|
||||
required String description,
|
||||
required ProgressStatuses status,
|
||||
required List<Map<String, dynamic>> practices}) =>
|
||||
LearnModuleTile(
|
||||
title: title,
|
||||
status: status,
|
||||
topics: topics,
|
||||
subtitle: subtitle,
|
||||
practices: practices,
|
||||
description: description,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,26 +11,337 @@ class LearnModuleViewModel extends BaseViewModel {
|
|||
// Modules
|
||||
final List<Map<String, dynamic>> _modules = [
|
||||
{
|
||||
'status': ProgressStatuses.completed,
|
||||
'title': 'Module 1: Greetings & Introductions',
|
||||
'subtitle':
|
||||
'Learn how to introduce yourself, talk about your surroundings, and start simple conversations.',
|
||||
'status': ProgressStatuses.started,
|
||||
'title': 'Lesson 1.1',
|
||||
'subtitle': 'Start Speaking English Today! Greetings & Introductions',
|
||||
'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,
|
||||
'title': 'Module 2: Everyday Basics',
|
||||
'subtitle': 'Learn numbers, colors, and common objects.',
|
||||
'title': 'Lesson 1.2',
|
||||
'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',
|
||||
},
|
||||
{
|
||||
'question_text': 'Ethiopia is beautiful! What city are you from?',
|
||||
'sample_answer':
|
||||
'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'
|
||||
},
|
||||
{
|
||||
'question_text': 'I see. And where does your family live now?',
|
||||
'question_audio_url':
|
||||
'https://drive.google.com/file/d/19XAbHL3HqTpPcolvOQUVPSGef-Ythusu/view?usp=drive_link',
|
||||
'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 "I’m 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': 'Module 3: At the Cafe',
|
||||
'status': ProgressStatuses.pending,
|
||||
'subtitle': 'Practice ordering food and drinks confidently.',
|
||||
'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.""",
|
||||
},
|
||||
{
|
||||
'progress': 0,
|
||||
'status': ProgressStatuses.pending,
|
||||
'title': 'Module 4: Asking for Directions',
|
||||
'subtitle': 'Learn numbers, colors, and common objects.',
|
||||
'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 "I’m a teacher" and "I’m 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':
|
||||
"""You’ve shared your name and your home—now it’s 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" secret—the most important grammar rule for talking about other people—and 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':
|
||||
'That’s 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 you’ve 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
|
||||
void pop() => _navigationService.back();
|
||||
|
||||
Future<void> navigateToLearnLesson() async =>
|
||||
await _navigationService.navigateToLearnLessonView();
|
||||
Future<void> navigateToLearnLesson(
|
||||
{required String title,
|
||||
required String topics,
|
||||
required String subtitle,
|
||||
required String description,
|
||||
required List<Map<String, dynamic>> practices}) async =>
|
||||
await _navigationService.navigateToLearnLessonView(
|
||||
title: title,
|
||||
topics: topics,
|
||||
subtitle: subtitle,
|
||||
practices: practices,
|
||||
description: description);
|
||||
|
||||
Future<void> navigateToLearnPractice() async =>
|
||||
Future<void> navigateToLearnPractice(
|
||||
List<Map<String, dynamic>> practices) async =>
|
||||
await _navigationService.navigateToLearnPracticeView(
|
||||
title: 'Let’s Practice Module 1',
|
||||
buttonLabel: 'Begin Module Practice',
|
||||
subtitle: 'Let’s quickly review what you’ve learned in this module! ',
|
||||
practices: practices,
|
||||
title: 'Let’s Practice',
|
||||
buttonLabel: 'Begin Lesson Practice',
|
||||
subtitle: 'Let’s quickly review what you’ve learned in this lesson!',
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/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/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/speak_to_learn_practice_listener_screen.dart';
|
||||
import 'package:yimaru_app/ui/views/learn_practice/screens/start_learn_practice_screen.dart';
|
||||
|
||||
import '../../common/app_colors.dart';
|
||||
import '../../widgets/cancel_learn_practice_sheet.dart';
|
||||
import 'learn_practice_viewmodel.dart';
|
||||
|
||||
class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final String buttonLabel;
|
||||
final List<Map<String, dynamic>> practices;
|
||||
|
||||
const LearnPracticeView(
|
||||
{Key? key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.practices,
|
||||
required this.buttonLabel})
|
||||
: 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
|
||||
LearnPracticeViewModel viewModelBuilder(BuildContext context) =>
|
||||
LearnPracticeViewModel();
|
||||
|
|
@ -33,36 +64,46 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
|
|||
LearnPracticeViewModel viewModel,
|
||||
Widget? child,
|
||||
) =>
|
||||
_buildPracticeScreensWrapper(viewModel);
|
||||
_buildPracticeScreensWrapper(context: context, viewModel: viewModel);
|
||||
|
||||
Widget _buildPracticeScreensWrapper(LearnPracticeViewModel viewModel) =>
|
||||
Widget _buildPracticeScreensWrapper(
|
||||
{required BuildContext context,
|
||||
required LearnPracticeViewModel viewModel}) =>
|
||||
PopScope(
|
||||
canPop: true,
|
||||
onPopInvokedWithResult: (value, data) {
|
||||
if (!value) return;
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((_) => viewModel.goBack());
|
||||
},
|
||||
canPop: viewModel.currentPage == 0 ? true : false,
|
||||
onPopInvokedWithResult: (value, data) async =>
|
||||
await _pop(context: context, viewModel: 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(
|
||||
backgroundColor: kcBackgroundColor,
|
||||
body: _buildBody(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildBody(LearnPracticeViewModel viewModel) =>
|
||||
IndexedStack(index: viewModel.currentIndex, children: _buildScreens());
|
||||
IndexedStack(index: viewModel.currentPage, children: _buildScreens());
|
||||
|
||||
List<Widget> _buildScreens() => [
|
||||
_buildLearnPracticesScreen(),
|
||||
_buildLearnPracticeIntroScreen(),
|
||||
_buildStartLearnPracticeScreen(),
|
||||
_buildListenLearnPracticeSpeakerScreen(),
|
||||
_buildSpeakToLearnPracticeListenerScreen(),
|
||||
_buildInteractLearnPracticeScreen(),
|
||||
_buildFinishLearnPracticeScreen(),
|
||||
_buildLearnPracticeResultScreen(),
|
||||
_buildLearnPracticeCompletionScreen()
|
||||
];
|
||||
|
||||
Widget _buildLearnPracticesScreen() => LearnPracticesScreen(
|
||||
practices: practices,
|
||||
);
|
||||
|
||||
Widget _buildLearnPracticeIntroScreen() => LearnPracticeIntroScreen(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
|
|
@ -71,11 +112,8 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
|
|||
|
||||
Widget _buildStartLearnPracticeScreen() => const StartLearnPracticeScreen();
|
||||
|
||||
Widget _buildListenLearnPracticeSpeakerScreen() =>
|
||||
const ListenLearnPracticeSpeakerScreen();
|
||||
|
||||
Widget _buildSpeakToLearnPracticeListenerScreen() =>
|
||||
const SpeakToLearnPracticeListenerScreen();
|
||||
Widget _buildInteractLearnPracticeScreen() =>
|
||||
const InteractLearnPracticeScreen();
|
||||
|
||||
Widget _buildFinishLearnPracticeScreen() => const FinishLearnPracticeScreen();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,36 +1,200 @@
|
|||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:stacked/stacked.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 '../../../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 _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
|
||||
int _currentIndex = 0;
|
||||
int _currentPage = 0;
|
||||
|
||||
int get currentIndex => _currentIndex;
|
||||
int get currentPage => _currentPage;
|
||||
|
||||
// Practice results
|
||||
final List<Map<String, dynamic>> _practiceResults = [
|
||||
{'question': 'What is your name?'},
|
||||
{'question': 'Where are you from?'},
|
||||
{'question': 'Where are you from?'}
|
||||
];
|
||||
// Practice
|
||||
Map<String, dynamic> _selectedPractice = {};
|
||||
|
||||
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
|
||||
void goTo(int page) {
|
||||
_currentIndex = page;
|
||||
_currentPage = page;
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
void goBack() {
|
||||
if (_currentIndex == 0) {
|
||||
if (_currentPage == 0) {
|
||||
pop();
|
||||
} else {
|
||||
_currentIndex--;
|
||||
_currentPage--;
|
||||
rebuildUi();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class FinishLearnPracticeScreen
|
|||
);
|
||||
|
||||
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
|
||||
showBackButton: false,
|
||||
showBackButton: true,
|
||||
onTap: viewModel.goBack,
|
||||
title: 'Practice Speaking',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,19 +1,38 @@
|
|||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_spinkit/flutter_spinkit.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/widgets/cancel_learn_practice_sheet.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 '../../../common/app_colors.dart';
|
||||
import '../../../common/enmus.dart';
|
||||
import '../../../common/ui_helpers.dart';
|
||||
import '../../../widgets/custom_column_button.dart';
|
||||
import '../../../widgets/small_app_bar.dart';
|
||||
|
||||
class ListenLearnPracticeSpeakerScreen
|
||||
class InteractLearnPracticeScreen
|
||||
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(
|
||||
{required BuildContext context,
|
||||
|
|
@ -40,28 +59,34 @@ class ListenLearnPracticeSpeakerScreen
|
|||
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),
|
||||
);
|
||||
SafeArea(child: _buildBodyStack(context: context, viewModel: viewModel));
|
||||
|
||||
Widget _buildBodyStack(
|
||||
{required BuildContext context,
|
||||
required LearnPracticeViewModel viewModel}) =>
|
||||
Stack(
|
||||
children: [
|
||||
_buildBodyColumn(context: context, viewModel: viewModel),
|
||||
_buildProgressIndicatorWrapper()
|
||||
_buildBodyColumnWrapper(context: context, viewModel: viewModel),
|
||||
_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(
|
||||
{required BuildContext context,
|
||||
required LearnPracticeViewModel viewModel}) =>
|
||||
|
|
@ -90,28 +115,79 @@ class ListenLearnPracticeSpeakerScreen
|
|||
|
||||
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
|
||||
showBackButton: true,
|
||||
onTap: viewModel.goBack,
|
||||
title: 'Practice Speaking',
|
||||
onTap: () async => await _cancel(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildSpeakingIndicatorWrapper(LearnPracticeViewModel viewModel) =>
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: _buildSpeakingIndicatorChildren(),
|
||||
children: _buildSpeakingIndicatorChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildSpeakingIndicatorChildren() =>
|
||||
[_buildSpeakerLabel(), verticalSpaceMedium, _buildSpeakingIndicator()];
|
||||
List<Widget> _buildSpeakingIndicatorChildren(
|
||||
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...',
|
||||
style: style14P400,
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
|
||||
Widget _buildSpeakingIndicator() =>
|
||||
WaveWrapper(height: 200, child: _buildSpinner());
|
||||
Widget _buildSpeakingLabel() => Text(
|
||||
'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(
|
||||
size: 20,
|
||||
|
|
@ -168,28 +244,61 @@ class ListenLearnPracticeSpeakerScreen
|
|||
{required BuildContext context,
|
||||
required LearnPracticeViewModel viewModel}) =>
|
||||
[
|
||||
_buildReplyButtonWrapper(),
|
||||
_buildReplyButtonWrapper(viewModel),
|
||||
_buildMicButtonWrapper(viewModel),
|
||||
_buildCancelButtonWrapper(context: context, viewModel: viewModel)
|
||||
];
|
||||
|
||||
Widget _buildReplyButtonWrapper() => Expanded(child: _buildReplyButton());
|
||||
Widget _buildReplyButtonWrapper(LearnPracticeViewModel viewModel) =>
|
||||
Expanded(child: _buildReplyButton(viewModel));
|
||||
|
||||
Widget _buildReplyButton() => const CustomColumnButton(
|
||||
icon: Icons.replay, label: 'Reply', color: kcPrimaryColor);
|
||||
Widget _buildReplyButton(LearnPracticeViewModel viewModel) =>
|
||||
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) =>
|
||||
Expanded(child: _buildMicButton(viewModel));
|
||||
|
||||
Widget _buildMicButton(LearnPracticeViewModel viewModel) => ElevatedButton(
|
||||
onPressed: () => viewModel.goTo(3),
|
||||
style: const ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(CircleBorder()),
|
||||
padding: WidgetStatePropertyAll(EdgeInsets.all(15)),
|
||||
shadowColor: WidgetStatePropertyAll(kcPrimaryColor),
|
||||
backgroundColor: WidgetStatePropertyAll(kcPrimaryColor),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
viewModel.player.state == PlayerState.playing ||
|
||||
viewModel.busy(StateObjects.recordLearnPracticeAnswer)
|
||||
? kcVeryLightGrey
|
||||
: kcPrimaryColor,
|
||||
),
|
||||
shadowColor: const WidgetStatePropertyAll(kcWhite),
|
||||
shape: const WidgetStatePropertyAll(CircleBorder()),
|
||||
padding: const WidgetStatePropertyAll(EdgeInsets.all(15)),
|
||||
),
|
||||
child: _buildMicIcon(),
|
||||
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(
|
||||
|
|
@ -217,18 +326,33 @@ class ListenLearnPracticeSpeakerScreen
|
|||
|
||||
Widget _buildSheet(LearnPracticeViewModel viewModel) =>
|
||||
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,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: _buildProgressIndicator(),
|
||||
child: _buildProgressIndicatorSpacer(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildProgressIndicator() => const CustomLinearProgressIndicator(
|
||||
progress: 0.7,
|
||||
activeColor: kcPrimaryColor,
|
||||
backgroundColor: kcVeryLightGrey);
|
||||
Widget _buildProgressIndicatorSpacer(LearnPracticeViewModel viewModel) =>
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: _buildProgressIndicator(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildProgressIndicator(LearnPracticeViewModel viewModel) =>
|
||||
CustomLinearProgressIndicator(
|
||||
activeColor: kcPrimaryColor,
|
||||
progress: viewModel.progress,
|
||||
backgroundColor: kcVeryLightGrey);
|
||||
}
|
||||
|
|
@ -77,7 +77,7 @@ class LearnPracticeCompletionScreen
|
|||
borderRadius: 12,
|
||||
text: 'Continue',
|
||||
foregroundColor: kcWhite,
|
||||
onTap: () => viewModel.pop(),
|
||||
onTap: () => viewModel.goTo(0),
|
||||
backgroundColor: kcPrimaryColor,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ class LearnPracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
|
|||
borderRadius: 12,
|
||||
text: buttonLabel,
|
||||
foregroundColor: kcWhite,
|
||||
onTap: () => viewModel.goTo(1),
|
||||
onTap: () => viewModel.goTo(2),
|
||||
backgroundColor: kcPrimaryColor,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ 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/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/ui_helpers.dart';
|
||||
|
|
@ -71,7 +71,7 @@ class LearnPracticeResultScreen
|
|||
];
|
||||
|
||||
Widget _buildResultsSection(LearnPracticeViewModel viewModel) =>
|
||||
PracticeResultsWrapper(data: viewModel.practiceResults);
|
||||
LearnPracticeResultsWrapper(data: viewModel.selectedPractice);
|
||||
|
||||
Widget _buildLearnPracticeTipSection() => const LearnPracticeTipSection();
|
||||
|
||||
|
|
|
|||
100
lib/ui/views/learn_practice/screens/learn_practices_screen.dart
Normal file
100
lib/ui/views/learn_practice/screens/learn_practices_screen.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -10,6 +10,10 @@ import '../../../widgets/small_app_bar.dart';
|
|||
class StartLearnPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
|
||||
const StartLearnPracticeScreen({super.key});
|
||||
|
||||
void _start(LearnPracticeViewModel viewModel) {
|
||||
viewModel.playQuestionAudio();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
|
||||
_buildScaffoldWrapper(viewModel);
|
||||
|
|
@ -47,8 +51,8 @@ class StartLearnPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
|
|||
);
|
||||
|
||||
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
|
||||
onTap: viewModel.goBack,
|
||||
showBackButton: true,
|
||||
onTap: viewModel.goBack,
|
||||
title: 'Practice Speaking',
|
||||
);
|
||||
|
||||
|
|
@ -56,9 +60,10 @@ class StartLearnPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
|
|||
child: _buildStartButtonContainer(viewModel),
|
||||
);
|
||||
|
||||
|
||||
Widget _buildStartButtonContainer(LearnPracticeViewModel viewModel) =>
|
||||
GestureDetector(
|
||||
onTap: () => viewModel.goTo(2),
|
||||
onTap: () => _start(viewModel),
|
||||
child: _buildStartButton(),
|
||||
);
|
||||
|
||||
|
|
@ -129,7 +134,7 @@ class StartLearnPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
|
|||
];
|
||||
|
||||
Widget _buildActionLabel() => Text(
|
||||
'Tap the microphone to speak',
|
||||
'Tap the start button to listen',
|
||||
style: style14DG400,
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
|
|
@ -149,27 +154,28 @@ class StartLearnPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
|
|||
Widget _buildReplyButtonWrapper() => Expanded(child: _buildReplyButton());
|
||||
|
||||
Widget _buildReplyButton() => const CustomColumnButton(
|
||||
icon: Icons.replay, label: 'Reply', color: kcPrimaryColor);
|
||||
icon: Icons.replay, label: 'Reply', color: kcLightGrey);
|
||||
|
||||
Widget _buildMicButtonWrapper(LearnPracticeViewModel viewModel) =>
|
||||
Expanded(child: _buildMicButton(viewModel));
|
||||
|
||||
Widget _buildMicButton(LearnPracticeViewModel viewModel) => ElevatedButton(
|
||||
onPressed: () => viewModel.goTo(2),
|
||||
style: const ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(CircleBorder()),
|
||||
padding: WidgetStatePropertyAll(EdgeInsets.all(15)),
|
||||
shadowColor: WidgetStatePropertyAll(kcPrimaryColor),
|
||||
backgroundColor: WidgetStatePropertyAll(kcPrimaryColor),
|
||||
),
|
||||
child: _buildMicIcon(),
|
||||
);
|
||||
onPressed: null,
|
||||
style: const ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(CircleBorder()),
|
||||
padding: WidgetStatePropertyAll(EdgeInsets.all(15)),
|
||||
shadowColor: WidgetStatePropertyAll(kcPrimaryColor),
|
||||
backgroundColor: WidgetStatePropertyAll(kcVeryLightGrey),
|
||||
),
|
||||
child: _buildMicIcon(),
|
||||
);
|
||||
|
||||
|
||||
Widget _buildMicIcon() => const Icon(
|
||||
Icons.mic,
|
||||
size: 35,
|
||||
color: kcWhite,
|
||||
);
|
||||
Icons.mic,
|
||||
size: 35,
|
||||
color: kcWhite,
|
||||
);
|
||||
|
||||
Widget _buildEmptySpace() => Expanded(child: Container());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,13 +44,13 @@ class LoginView extends StackedView<LoginViewModel> with $LoginView {
|
|||
_buildLoginScreensWrapper(viewModel);
|
||||
|
||||
Widget _buildLoginScreensWrapper(LoginViewModel viewModel) => PopScope(
|
||||
canPop: viewModel.currentIndex == 0 ? true : false,
|
||||
canPop: viewModel.currentPage == 0 ? true : false,
|
||||
onPopInvokedWithResult: (value, data) => WidgetsBinding.instance
|
||||
.addPostFrameCallback((_) => viewModel.goBack()),
|
||||
child: _buildBody(viewModel));
|
||||
|
||||
Widget _buildBody(LoginViewModel viewModel) =>
|
||||
IndexedStack(index: viewModel.currentIndex, children: _buildScreens());
|
||||
IndexedStack(index: viewModel.currentPage, children: _buildScreens());
|
||||
|
||||
List<Widget> _buildScreens() => [
|
||||
_buildLoginWithEmailScreen(),
|
||||
|
|
|
|||
|
|
@ -37,9 +37,9 @@ class LoginViewModel extends ReactiveViewModel
|
|||
GoogleSignInAccount? get googleUser => _googleUser;
|
||||
|
||||
// In-app navigation
|
||||
int _currentIndex = 0;
|
||||
int _currentPage = 0;
|
||||
|
||||
int get currentIndex => _currentIndex;
|
||||
int get currentPage => _currentPage;
|
||||
|
||||
// Email
|
||||
bool _focusEmail = false;
|
||||
|
|
@ -139,16 +139,16 @@ class LoginViewModel extends ReactiveViewModel
|
|||
|
||||
// In app navigation
|
||||
void goTo(int page) {
|
||||
_currentIndex = page;
|
||||
_currentPage = page;
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
void goBack() {
|
||||
if (_currentIndex == 1) {
|
||||
_currentIndex = 0;
|
||||
if (_currentPage == 1) {
|
||||
_currentPage = 0;
|
||||
rebuildUi();
|
||||
} else if (_currentIndex == 2) {
|
||||
_currentIndex = 1;
|
||||
} else if (_currentPage == 2) {
|
||||
_currentPage = 1;
|
||||
rebuildUi();
|
||||
} else {
|
||||
_navigationService.back();
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ class StartupViewModel extends BaseViewModel {
|
|||
await _authenticationService.getUser();
|
||||
await _navigationService.replaceWithHomeView();
|
||||
} else {
|
||||
// Removable
|
||||
await _navigationService.replaceWithLoginView();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
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 '../common/app_colors.dart';
|
||||
|
|
@ -6,32 +8,37 @@ import '../common/ui_helpers.dart';
|
|||
import 'custom_bottom_sheet.dart';
|
||||
import 'custom_elevated_button.dart';
|
||||
|
||||
class CancelLearnPracticeSheet extends StatelessWidget {
|
||||
final GestureTapCallback? onTap;
|
||||
class CancelLearnPracticeSheet extends ViewModelWidget<LearnPracticeViewModel> {
|
||||
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
|
||||
Widget build(BuildContext context) => _buildSheetWrapper();
|
||||
Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
|
||||
_buildSheetWrapper(viewModel);
|
||||
|
||||
Widget _buildSheetWrapper() => CustomBottomSheet(
|
||||
height: 500, onTap: onTap, child: _buildColumnWrapper());
|
||||
Widget _buildSheetWrapper(LearnPracticeViewModel viewModel) =>
|
||||
CustomBottomSheet(
|
||||
height: 500, onTap: onClose, child: _buildColumnWrapper(viewModel));
|
||||
|
||||
Widget _buildColumnWrapper() => Padding(
|
||||
Widget _buildColumnWrapper(LearnPracticeViewModel viewModel) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: _buildColumn(),
|
||||
child: _buildColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildColumn() => Column(
|
||||
Widget _buildColumn(LearnPracticeViewModel viewModel) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: _buildSheetChildren(),
|
||||
children: _buildSheetChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildSheetChildren() => [
|
||||
List<Widget> _buildSheetChildren(LearnPracticeViewModel viewModel) => [
|
||||
verticalSpaceLarge,
|
||||
_buildImage(),
|
||||
verticalSpaceMedium,
|
||||
_buildMessage(),
|
||||
_buildMessage(viewModel),
|
||||
_buildSubtitle(),
|
||||
verticalSpaceLarge,
|
||||
_buildContinueButton(),
|
||||
|
|
@ -42,10 +49,10 @@ class CancelLearnPracticeSheet extends StatelessWidget {
|
|||
radius: 45,
|
||||
);
|
||||
|
||||
Widget _buildMessage() => Text.rich(
|
||||
Widget _buildMessage(LearnPracticeViewModel viewModel) => Text.rich(
|
||||
TextSpan(text: 'You’re almost there,', style: style18DG700, children: [
|
||||
TextSpan(
|
||||
text: ' Johnny!',
|
||||
text: ' ${viewModel.user?.firstName ?? ''}!',
|
||||
style: style18P600,
|
||||
)
|
||||
]),
|
||||
|
|
@ -59,7 +66,7 @@ class CancelLearnPracticeSheet extends StatelessWidget {
|
|||
|
||||
Widget _buildContinueButton() => CustomElevatedButton(
|
||||
height: 55,
|
||||
onTap: onTap,
|
||||
onTap: onContinue,
|
||||
borderRadius: 12,
|
||||
foregroundColor: kcWhite,
|
||||
text: 'Continue Practice',
|
||||
|
|
@ -68,7 +75,7 @@ class CancelLearnPracticeSheet extends StatelessWidget {
|
|||
|
||||
Widget _buildEndButton() => CustomElevatedButton(
|
||||
height: 55,
|
||||
onTap: onTap,
|
||||
onTap: onCancel,
|
||||
borderRadius: 12,
|
||||
text: 'End Session',
|
||||
backgroundColor: kcWhite,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:iconsax/iconsax.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:yimaru_app/ui/views/learn_module/learn_module_viewmodel.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> {
|
||||
final String title;
|
||||
final String topics;
|
||||
final String subtitle;
|
||||
final String description;
|
||||
final ProgressStatuses status;
|
||||
final List<Map<String, dynamic>> practices;
|
||||
|
||||
const LearnModuleTile({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.status,
|
||||
required this.subtitle,
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
const LearnModuleTile(
|
||||
{super.key,
|
||||
required this.title,
|
||||
required this.topics,
|
||||
required this.status,
|
||||
required this.subtitle,
|
||||
required this.practices,
|
||||
required this.description});
|
||||
|
||||
Future<void> _showSheet(
|
||||
{required BuildContext context,
|
||||
|
|
@ -101,25 +91,19 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
|
|||
child: _buildIcon(),
|
||||
);
|
||||
|
||||
Widget _buildIcon() => Icon(
|
||||
_getIcon(),
|
||||
Widget _buildIcon() => const Icon(
|
||||
Icons.lightbulb_outline,
|
||||
color: kcPrimaryColor,
|
||||
);
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: kcPrimaryColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: style16P600,
|
||||
);
|
||||
|
||||
Widget _buildContent() => Text(
|
||||
subtitle,
|
||||
style: const TextStyle(
|
||||
color: kcDarkGrey,
|
||||
),
|
||||
style: style14DG400,
|
||||
);
|
||||
|
||||
List<Widget> _buildExpansionTileChildren(
|
||||
|
|
@ -141,8 +125,8 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
|
|||
{required BuildContext context,
|
||||
required LearnModuleViewModel viewModel}) =>
|
||||
[
|
||||
_buildProgressRow(),
|
||||
verticalSpaceSmall,
|
||||
// _buildProgressRow(),
|
||||
// verticalSpaceSmall,
|
||||
_buildActionButtonWrapper(context: context, viewModel: viewModel)
|
||||
];
|
||||
|
||||
|
|
@ -198,7 +182,12 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
|
|||
text: 'View Lessons',
|
||||
foregroundColor: kcWhite,
|
||||
backgroundColor: kcPrimaryColor,
|
||||
onTap: () async => await viewModel.navigateToLearnLesson(),
|
||||
onTap: () async => await viewModel.navigateToLearnLesson(
|
||||
title: title,
|
||||
topics: topics,
|
||||
subtitle: subtitle,
|
||||
practices: practices,
|
||||
description: description),
|
||||
);
|
||||
|
||||
Widget _buildPracticeButtonWrapper(
|
||||
|
|
@ -218,9 +207,7 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
|
|||
backgroundColor: kcWhite,
|
||||
borderColor: kcPrimaryColor,
|
||||
foregroundColor: kcPrimaryColor,
|
||||
onTap: () async => status == ProgressStatuses.completed
|
||||
? await viewModel.navigateToLearnPractice()
|
||||
: await _showSheet(context: context, viewModel: viewModel),
|
||||
onTap: () async => await viewModel.navigateToLearnPractice(practices),
|
||||
);
|
||||
|
||||
Widget _buildSheet(LearnModuleViewModel viewModel) => FinishPracticeSheet(
|
||||
|
|
|
|||
81
lib/ui/widgets/learn_practice_answer_card.dart
Normal file
81
lib/ui/widgets/learn_practice_answer_card.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
62
lib/ui/widgets/learn_practice_card.dart
Normal file
62
lib/ui/widgets/learn_practice_card.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
61
lib/ui/widgets/learn_practice_result_card.dart
Normal file
61
lib/ui/widgets/learn_practice_result_card.dart
Normal 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,);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import 'package:flutter/material.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';
|
||||
|
||||
class PracticeResultsWrapper extends StatelessWidget {
|
||||
final List<Map<String, dynamic>> data;
|
||||
class LearnPracticeResultsWrapper extends StatelessWidget {
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
const PracticeResultsWrapper({super.key, required this.data});
|
||||
const LearnPracticeResultsWrapper({super.key, required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _buildContainer();
|
||||
|
|
@ -28,7 +28,7 @@ class PracticeResultsWrapper extends StatelessWidget {
|
|||
);
|
||||
|
||||
List<Widget> _buildColumnChildren() =>
|
||||
[_buildTitle(), verticalSpaceSmall, _buildResults()];
|
||||
[_buildTitle(), verticalSpaceSmall, if (data.isNotEmpty) _buildResult()];
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
'Conversation Review',
|
||||
|
|
@ -36,15 +36,5 @@ class PracticeResultsWrapper extends StatelessWidget {
|
|||
textAlign: TextAlign.center,
|
||||
);
|
||||
|
||||
Widget _buildResults() => ListView.separated(
|
||||
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]);
|
||||
Widget _buildResult() => LearnPracticeResultCard(data: data);
|
||||
}
|
||||
|
|
@ -72,11 +72,7 @@ class LearnSubLevelTile extends ViewModelWidget<LearnLevelViewModel> {
|
|||
|
||||
Widget _buildTitle() => Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: kcPrimaryColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: style16P600,
|
||||
);
|
||||
|
||||
Widget _buildProgressStatus() => const ProgressStatus(
|
||||
|
|
@ -86,9 +82,7 @@ class LearnSubLevelTile extends ViewModelWidget<LearnLevelViewModel> {
|
|||
|
||||
Widget _buildContent() => Text(
|
||||
subtitle,
|
||||
style: const TextStyle(
|
||||
color: kcDarkGrey,
|
||||
),
|
||||
style: style14DG400,
|
||||
);
|
||||
|
||||
Widget _buildActionButtonWrapper(LearnLevelViewModel viewModel) => SizedBox(
|
||||
|
|
@ -119,18 +113,17 @@ class LearnSubLevelTile extends ViewModelWidget<LearnLevelViewModel> {
|
|||
);
|
||||
|
||||
Widget _buildPracticeButtonWrapper(LearnLevelViewModel viewModel) => Expanded(
|
||||
child: _buildPracticeButton(viewModel),
|
||||
child: Container(),
|
||||
);
|
||||
|
||||
Widget _buildPracticeButton(LearnLevelViewModel viewModel) =>
|
||||
CustomElevatedButton(
|
||||
const CustomElevatedButton(
|
||||
height: 15,
|
||||
text: 'Practice',
|
||||
borderRadius: 12,
|
||||
backgroundColor: kcWhite,
|
||||
borderColor: kcPrimaryColor,
|
||||
foregroundColor: kcPrimaryColor,
|
||||
onTap: () async => await viewModel.navigateToLearnPractice()
|
||||
// onTap: () async => await viewModel.navigateToLearnLevel(),
|
||||
foregroundColor: kcPrimaryColor
|
||||
// onTap: () async => await viewModel.navigateToLearnPractice()
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,25 +40,18 @@ class OverallLearnProgress extends StatelessWidget {
|
|||
List<Widget> _buildProgressInfoChildren() =>
|
||||
[_buildProgressInfo(), _buildProgress()];
|
||||
|
||||
Widget _buildProgressInfo() => const Text(
|
||||
Widget _buildProgressInfo() => Text(
|
||||
'Overall Progress',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: style16DG600,
|
||||
);
|
||||
|
||||
Widget _buildProgress() => const Text(
|
||||
'35%',
|
||||
style: TextStyle(
|
||||
color: kcPrimaryColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
Widget _buildProgress() => Text(
|
||||
'0%',
|
||||
style: style14P400,
|
||||
);
|
||||
|
||||
Widget _buildProgressIndicator() => const CustomLinearProgressIndicator(
|
||||
progress: 0.75,
|
||||
progress: 0.0,
|
||||
activeColor: kcPrimaryColor,
|
||||
backgroundColor: kcVeryLightGrey,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.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';
|
||||
|
||||
class PracticeResultCard extends StatelessWidget {
|
||||
final int index;
|
||||
class PracticeResultCard extends ViewModelWidget<LearnPracticeViewModel> {
|
||||
final Map<String, dynamic> data;
|
||||
const PracticeResultCard(
|
||||
{super.key, required this.index, required this.data});
|
||||
{super.key, required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _buildColumnWrapper();
|
||||
Widget build(BuildContext context,LearnPracticeViewModel viewModel) => _buildColumnWrapper();
|
||||
|
||||
Widget _buildColumnWrapper() => SizedBox(
|
||||
height: 100,
|
||||
|
|
@ -27,7 +28,7 @@ class PracticeResultCard extends StatelessWidget {
|
|||
[_buildQuestion(), verticalSpaceSmall, _buildRow()];
|
||||
|
||||
Widget _buildQuestion() => Text(
|
||||
'$index. ${data['question']}',
|
||||
data['question_text'],
|
||||
style: style14DG400,
|
||||
);
|
||||
|
||||
|
|
@ -45,11 +46,11 @@ class PracticeResultCard extends StatelessWidget {
|
|||
Expanded(child: _buildSampleResponse());
|
||||
|
||||
Widget _buildSampleResponse() =>
|
||||
const CustomResponseCard(title: 'Sample Answer', subtitle: '0:54');
|
||||
const CustomResponseCard(title: 'Sample Answer', subtitle: '0:54');
|
||||
|
||||
Widget _buildActualResponseWrapper() =>
|
||||
Expanded(child: _buildActualResponse());
|
||||
|
||||
Widget _buildActualResponse() =>
|
||||
const CustomResponseCard(title: 'Sample Answer', subtitle: '0:54');
|
||||
const CustomResponseCard(title: 'Sample Answer', subtitle: '0:54');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,14 +6,22 @@
|
|||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <audioplayers_linux/audioplayers_linux_plugin.h>
|
||||
#include <file_selector_linux/file_selector_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) {
|
||||
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 =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) record_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin");
|
||||
record_linux_plugin_register_with_registrar(record_linux_registrar);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@
|
|||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
audioplayers_linux
|
||||
file_selector_linux
|
||||
flutter_secure_storage_linux
|
||||
record_linux
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import audioplayers_darwin
|
||||
import battery_plus
|
||||
import connectivity_plus
|
||||
import file_selector_macos
|
||||
|
|
@ -14,11 +15,13 @@ import flutter_local_notifications
|
|||
import flutter_secure_storage_darwin
|
||||
import google_sign_in_ios
|
||||
import package_info_plus
|
||||
import record_macos
|
||||
import sqflite_darwin
|
||||
import video_player_avfoundation
|
||||
import wakelock_plus
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
|
||||
BatteryPlusMacosPlugin.register(with: registry.registrar(forPlugin: "BatteryPlusMacosPlugin"))
|
||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
|
|
@ -28,6 +31,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
|
||||
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
|
||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
|
||||
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||
|
|
|
|||
144
pubspec.lock
144
pubspec.lock
|
|
@ -57,6 +57,62 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -988,10 +1044,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
|
||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.18"
|
||||
version: "0.12.19"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1296,6 +1352,70 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1489,10 +1609,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.9"
|
||||
version: "0.7.10"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1653,6 +1773,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ dependencies:
|
|||
storage_info: ^1.0.0
|
||||
flutter_html: ^3.0.0
|
||||
email_validator: any
|
||||
audioplayers: ^6.6.0
|
||||
video_player: ^2.10.1
|
||||
firebase_core: ^4.4.0
|
||||
in_app_update: ^4.2.5
|
||||
|
|
@ -36,6 +37,7 @@ dependencies:
|
|||
stacked_services: ^1.1.0
|
||||
omni_datetime_picker: any
|
||||
json_serializable: ^6.8.0
|
||||
waveform_recorder: ^1.8.0
|
||||
permission_handler: ^12.0.1
|
||||
firebase_messaging: ^16.1.1
|
||||
cached_network_image: ^3.4.1
|
||||
|
|
|
|||
|
|
@ -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/smart_auth_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
|
||||
|
||||
import 'test_helpers.mocks.dart';
|
||||
|
|
@ -38,6 +40,8 @@ import 'test_helpers.mocks.dart';
|
|||
MockSpec<NotificationService>(onMissingStub: OnMissingStub.returnDefault),
|
||||
MockSpec<SmartAuthService>(onMissingStub: OnMissingStub.returnDefault),
|
||||
MockSpec<CourseService>(onMissingStub: OnMissingStub.returnDefault),
|
||||
MockSpec<AudioPlayerService>(onMissingStub: OnMissingStub.returnDefault),
|
||||
MockSpec<VoiceRecorderService>(onMissingStub: OnMissingStub.returnDefault),
|
||||
// @stacked-mock-spec
|
||||
],
|
||||
)
|
||||
|
|
@ -57,6 +61,8 @@ void registerServices() {
|
|||
getAndRegisterNotificationService();
|
||||
getAndRegisterSmartAuthService();
|
||||
getAndRegisterCourseService();
|
||||
getAndRegisterAudioPlayerService();
|
||||
getAndRegisterVoiceRecorderService();
|
||||
// @stacked-mock-register
|
||||
}
|
||||
|
||||
|
|
@ -197,6 +203,20 @@ MockCourseService getAndRegisterCourseService() {
|
|||
locator.registerSingleton<CourseService>(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
|
||||
|
||||
void _removeRegistrationIfExists<T extends Object>() {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
11
test/services/audio_player_service_test.dart
Normal file
11
test/services/audio_player_service_test.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:yimaru_app/app/app.locator.dart';
|
||||
|
||||
import '../helpers/test_helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('AudioPlayerServiceTest -', () {
|
||||
setUp(() => registerServices());
|
||||
tearDown(() => locator.reset());
|
||||
});
|
||||
}
|
||||
11
test/services/voice_recorder_service_test.dart
Normal file
11
test/services/voice_recorder_service_test.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:yimaru_app/app/app.locator.dart';
|
||||
|
||||
import '../helpers/test_helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('VoiceRecorderServiceTest -', () {
|
||||
setUp(() => registerServices());
|
||||
tearDown(() => locator.reset());
|
||||
});
|
||||
}
|
||||
|
|
@ -6,14 +6,18 @@
|
|||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <audioplayers_windows/audioplayers_windows_plugin.h>
|
||||
#include <battery_plus/battery_plus_windows_plugin.h>
|
||||
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <firebase_core/firebase_core_plugin_c_api.h>
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||
#include <record_windows/record_windows_plugin_c_api.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
AudioplayersWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
|
||||
BatteryPlusWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("BatteryPlusWindowsPlugin"));
|
||||
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
||||
|
|
@ -26,4 +30,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||
RecordWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@
|
|||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
audioplayers_windows
|
||||
battery_plus
|
||||
connectivity_plus
|
||||
file_selector_windows
|
||||
firebase_core
|
||||
flutter_secure_storage_windows
|
||||
permission_handler_windows
|
||||
record_windows
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user