- feat: Add localization according to the UAT comments

Merge branch 'release/0.1.16'
This commit is contained in:
BisratHailu 2026-05-14 16:00:40 +03:00
commit 3cc5753308
69 changed files with 1703 additions and 184 deletions

View File

@ -0,0 +1,85 @@
{
"welcome_back": "እንኳን በደህና ተመለሱ",
"dont_have_account": "መለያ የለዎትም? ይመዝገቡ",
"email": "ኢሜይል",
"password": "የይለፍ ቃል",
"forgot_password": "የይለፍ ቃል ረሱ?",
"cont": "ቀጥል",
"register": "ይመዝገቡ",
"login_with_google": "በጉግል ይግቡ",
"or": "ወይም",
"login_with_phone": "በስልክ ቁጥር ይግቡ",
"create_account": "አዲስ መለያ ይፍጠሩ",
"already_have_account": "መለያ አለዎት?",
"login": " ይግቡ ",
"register_with_google": "በጉግል ይመዝገቡ",
"register_with_phone": "በስልክ ቁጥር ይመዝገቡ",
"enter_phone_number": "የስልክ ቁጥርዎን ያስገቡ። የማረጋገጫ ኮድ እንልክልዎታለን።",
"login_with_email": "በኢሜይል ይግቡ",
"create_password": "የይለፍ ቃል ይፍጠሩ",
"confirm_password": "የይለፍ ቃል ያረጋግጡ",
"sign_up_agreement": "‘ይመዝገቡ’ የሚለውን ሲጫኑ በ‘አገልግሎት ውሎች’ እና ‘በግላዊነት ፖሊሲ’ ይስማማሉ።" ,
"reset_password": " የይለፍ ቃልን ይቀይሩ ",
"enter_email_reset_code": "ኢሜይልዎን ያስገቡ። የይለፍ ቃል መለወጫ ኮድ እንልክልዎታለን።" ,
"please_wait": "እባክዎ ይጠብቁ",
"reset_code_sent": "የመቀየሪያ ኮድ በተሳካ ሁኔታ ተልኳል" ,
"reset_code": " የመቀየሪያ ኮድ ",
"new_password": "አዲስ የይለፍ ቃል",
"logged_in_successfully": "በተሳካ ሁኔታ ገብተዋል",
"view_course": " ኮርሱን ይመልከቱ ",
"take_practice": " ልምምድ ያድርጉ ",
"your_current_level": "የአሁኑ ደረጃዎ",
"overall_progress": "አጠቃላይ እድገት",
"great_work": "በርቱ! በጣም ጥሩ እየሰሩ ነው ",
"view_module": "ሞጁሉን ይመልከቱ",
"progress": "እድገት",
"keep_going": " ይቀጥሉ - ከግማሽ በላይ ጨርሰዋል ",
"lessons_in_module": " በዚህ ሞጁል ውስጥ ያሉ ትምህርቶች ",
"practice": "ልምምድ",
"start": "ጀምር",
"in_progress": "በሂደት ላይ",
"hello": "ሰላም",
"ready_to_learn": " ዛሬ እንግሊዝኛ ለመማር ተዘጋጅተዋል? ",
"learn": "ይማሩ ",
"course": "ኮርስ",
"profile": " ፕሮፋይል ",
"speaking_partner": "የንግግር ጓደኛ ",
"practice_what_you_learned": "አሁን የተማሩትን እንለማመድ",
"practice_questions": "ጥቂት ጥያቄዎችን እጠይቃለሁ እና መልስ መስጠት ይችላሉ",
"start_practice": "ልምምድ ጀምር",
"almost_there": "ሊጨርሱ ተቃርበዋል",
"finish_session": "እድገትዎን ለማየት ክፍለ ጊዜውን ያጠናቅቁ",
"continue_practice": "ልምምዱን ይቀጥሉ",
"end_session": "ክፍለ ጊዜውን ያብቁ ",
"tap_start_to_listen": "ለማዳመጥ የጀምር ቁልፉን ይጫኑ",
"practice_speaking": "ንግግርን ይለማመዱ ",
"tap_microphone": "ለመናገር ማይክሮፎኑን ይጫኑ",
"reply": "እንደገና አዳምጥ",
"cancel": "ይቅር",
"you_are_speaking": "እየተናገሩ ነው",
"practice_completed": "ልምምዱ ተጠናቅቋል",
"great_improvement": "በዚህኛው በራስ መተማመንዎ ጨምሯል፤ ትልቅ መሻሻል ነው",
"practice_again": "እንደገና ይለማመዱ",
"conversation_review": "የንግግር ግምገማ ",
"result": "ውጤት",
"quick_tip": "ጠቃሚ ምክር",
"retry": "እንደገና ይሞክሩ",
"completed_a1": "እንኳን ደስ አለዎት! A1 ደረጃን አጠናቅቀዋል",
"analyzing_speaking": "የንግግር ችሎታዎን እየገመገምን ነው",
"view_profile": "ፕሮፋይሎን ይመልከቱ ",
"hi": "ሰላም",
"edit_profile": "መገለጫ ያስተካክሉ",
"first_name": "የመጀመሪያ ስም",
"last_name": "የአባት ስም",
"gender": "ፆታ",
"male": "ወንድ",
"female": "ሴት",
"phone_number": "የስልክ ቁጥር",
"country": "ሀገር",
"region": "ክልል",
"occupation": "የስራ መስክ ",
"save_changes": "ለውጦችን ያስቀምጡ"
}

View File

@ -0,0 +1,83 @@
{
"welcome_back": "Welcome back",
"dont_have_account": "Don't have an account? Register",
"email": "Email",
"password": "Password",
"forgot_password": "Forgot password?",
"cont": "Continue",
"register": "Register",
"login_with_google": "Login with Google",
"or": "Or",
"login_with_phone": "Login with phone number",
"create_account": "Create an account",
"already_have_account": "Already have an account?",
"login": "Login",
"register_with_google": "Register with Google",
"register_with_phone": "Register with phone number",
"enter_phone_number": "Enter your phone number. We will send you a confirmation code there.",
"login_with_email": "Login with email",
"create_password": "Create password",
"confirm_password": "Confirm password",
"sign_up_agreement": "By clicking Sign Up, you agree to our Terms of Service and Privacy Policy",
"reset_password": "Reset Password",
"enter_email_reset_code": "Enter your email. We will send you a reset code.",
"please_wait": "Please wait",
"reset_code_sent": "Reset code sent successfully",
"reset_code": "Reset code",
"new_password": "New password",
"logged_in_successfully": "Logged in successfully",
"view_course": "View course",
"take_practice": "Take practice",
"your_current_level": "Your current level",
"overall_progress": "Overall progress",
"great_work": "Keep up the great work! You're doing amazing",
"view_module": "View module",
"progress": "Progress",
"keep_going": "Let's keep going - you're more than half there",
"lessons_in_module": "Lessons in this module",
"practice": "Practice",
"start": "Start",
"in_progress": "In Progress",
"hello": "Hello",
"ready_to_learn": "Ready to keep learning English today",
"learn": "Learn",
"course": "Course",
"profile": "Profile",
"speaking_partner": "Speaking partner",
"practice_what_you_learned": "Let's practice what you just learnt",
"practice_questions": "I will ask you a few questions and you can respond",
"start_practice": "Start practice",
"almost_there": "You're almost there",
"finish_session": "Finish the session to see your progress",
"continue_practice": "Continue practice",
"end_session": "End session",
"tap_start_to_listen": "Tap the start button to listen",
"practice_speaking": "Practice speaking",
"tap_microphone": "Tap the microphone to speak",
"reply": "Reply",
"cancel": "Cancel",
"you_are_speaking": "You're speaking",
"practice_completed": "Practice completed",
"great_improvement": "You sound more confident this time, great improvement",
"practice_again": "Practice again",
"conversation_review": "Conversation review",
"result": "Result",
"quick_tip": "Quick tip",
"retry": "Retry",
"completed_a1": "Yay, you've completed A1",
"analyzing_speaking": "We're now analyzing your speaking skill",
"view_profile": "View profile",
"hi": "Hi",
"edit_profile": "Edit profile",
"first_name": "First name",
"last_name": "Last name",
"gender": "Gender",
"male": "Male",
"female": "Female",
"phone_number": "Phone number",
"country": "Country",
"region": "Region",
"occupation": "Occupation",
"save_changes": "Save changes"
}

View File

@ -58,6 +58,7 @@ import 'package:yimaru_app/ui/views/arif_pay/arif_pay_view.dart';
import 'package:yimaru_app/services/learn_service.dart';
import 'package:yimaru_app/ui/views/course_catalog/course_catalog_view.dart';
import 'package:yimaru_app/ui/views/course_unit/course_unit_view.dart';
import 'package:yimaru_app/services/localization_service.dart';
// @stacked-import
@StackedApp(
@ -124,6 +125,7 @@ import 'package:yimaru_app/ui/views/course_unit/course_unit_view.dart';
LazySingleton(classType: UrlLauncherService),
LazySingleton(classType: PhoneCallerService),
LazySingleton(classType: LearnService),
LazySingleton(classType: LocalizationService),
// @stacked-service
],
bottomsheets: [

View File

@ -22,6 +22,7 @@ import '../services/image_downloader_service.dart';
import '../services/image_picker_service.dart';
import '../services/in_app_update_service.dart';
import '../services/learn_service.dart';
import '../services/localization_service.dart';
import '../services/notification_service.dart';
import '../services/permission_handler_service.dart';
import '../services/phone_caller_service.dart';
@ -63,4 +64,5 @@ Future<void> setupLocator(
locator.registerLazySingleton(() => UrlLauncherService());
locator.registerLazySingleton(() => PhoneCallerService());
locator.registerLazySingleton(() => LearnService());
locator.registerLazySingleton(() => LocalizationService());
}

View File

@ -6,11 +6,12 @@
// **************************************************************************
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:flutter/material.dart' as _i39;
import 'package:flutter/material.dart';
import 'package:flutter/material.dart' as _i39;
import 'package:stacked/stacked.dart' as _i1;
import 'package:stacked_services/stacked_services.dart' as _i46;
import 'package:stacked_services/stacked_services.dart' as _i47;
import 'package:yimaru_app/models/course.dart' as _i44;
import 'package:yimaru_app/models/course_catalog.dart' as _i46;
import 'package:yimaru_app/models/course_lesson.dart' as _i45;
import 'package:yimaru_app/models/learn_course.dart' as _i40;
import 'package:yimaru_app/models/learn_lesson.dart' as _i42;
@ -664,11 +665,10 @@ class StackedRouter extends _i1.RouterBase {
);
},
_i38.CourseUnitView: (data) {
final args = data.getArgs<CourseUnitViewArguments>(
orElse: () => const CourseUnitViewArguments(),
);
final args = data.getArgs<CourseUnitViewArguments>(nullOk: false);
return _i39.MaterialPageRoute<dynamic>(
builder: (context) => _i38.CourseUnitView(key: args.key),
builder: (context) =>
_i38.CourseUnitView(key: args.key, catalog: args.catalog),
settings: data,
);
},
@ -1586,28 +1586,33 @@ class CourseCatalogViewArguments {
}
class CourseUnitViewArguments {
const CourseUnitViewArguments({this.key});
const CourseUnitViewArguments({
this.key,
required this.catalog,
});
final _i39.Key? key;
final _i46.CourseCatalog catalog;
@override
String toString() {
return '{"key": "$key"}';
return '{"key": "$key", "catalog": "$catalog"}';
}
@override
bool operator ==(covariant CourseUnitViewArguments other) {
if (identical(this, other)) return true;
return other.key == key;
return other.key == key && other.catalog == catalog;
}
@override
int get hashCode {
return key.hashCode;
return key.hashCode ^ catalog.hashCode;
}
}
extension NavigatorStateExtension on _i46.NavigationService {
extension NavigatorStateExtension on _i47.NavigationService {
Future<dynamic> navigateToHomeView({
_i39.Key? key,
int? routerId,
@ -2216,6 +2221,7 @@ extension NavigatorStateExtension on _i46.NavigationService {
Future<dynamic> navigateToCourseUnitView({
_i39.Key? key,
required _i46.CourseCatalog catalog,
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
@ -2223,7 +2229,7 @@ extension NavigatorStateExtension on _i46.NavigationService {
transition,
}) async {
return navigateTo<dynamic>(Routes.courseUnitView,
arguments: CourseUnitViewArguments(key: key),
arguments: CourseUnitViewArguments(key: key, catalog: catalog),
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
@ -2838,6 +2844,7 @@ extension NavigatorStateExtension on _i46.NavigationService {
Future<dynamic> replaceWithCourseUnitView({
_i39.Key? key,
required _i46.CourseCatalog catalog,
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
@ -2845,7 +2852,7 @@ extension NavigatorStateExtension on _i46.NavigationService {
transition,
}) async {
return replaceWith<dynamic>(Routes.courseUnitView,
arguments: CourseUnitViewArguments(key: key),
arguments: CourseUnitViewArguments(key: key, catalog: catalog),
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,

View File

@ -7,7 +7,8 @@ import 'package:yimaru_app/app/app.locator.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/services/notification_service.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:yimaru_app/ui/common/translations/codegen_loader.g.dart';
import 'firebase_options.dart';
Future<void> main() async {
@ -15,26 +16,42 @@ Future<void> main() async {
await setupLocator();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
await locator<NotificationService>().initialize();
await EasyLocalization.ensureInitialized();
setupDialogUi();
setupBottomSheetUi();
runApp(const MainApp());
runApp(
EasyLocalization(
supportedLocales: const [
Locale('en'),
Locale('am'),
],
path: 'assets/translations',
startLocale: const Locale('en'),
assetLoader: const CodegenLoader(),
fallbackLocale: const Locale('en'),
child: const MainApp(),
),
);
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) => _buildMaterialWrapper();
Widget build(BuildContext context) => _buildMaterialWrapper(context);
Widget _buildMaterialWrapper() => ToastificationWrapper(
child: _buildMaterialApp(),
Widget _buildMaterialWrapper(BuildContext context) => ToastificationWrapper(
child: _buildMaterialApp(context),
);
Widget _buildMaterialApp() => MaterialApp(
Widget _buildMaterialApp(BuildContext context) => MaterialApp(
locale: context.locale,
initialRoute: Routes.startupView,
theme: ThemeData(fontFamily: 'Aeonik'),
navigatorKey: StackedService.navigatorKey,
supportedLocales: context.supportedLocales,
onGenerateRoute: StackedRouter().onGenerateRoute,
navigatorObservers: [StackedService.routeObserver],
localizationsDelegates: context.localizationDelegates,
);
}

View File

@ -0,0 +1,48 @@
import 'package:json_annotation/json_annotation.dart';
part 'course_module.g.dart';
@JsonSerializable()
class CourseModule {
final int? id;
final String? name;
final String? icon;
final String? thumbnail;
final String? description;
@JsonKey(name: 'unit_id')
final int? unitId;
@JsonKey(name: 'sort_order')
final int? sortOrder;
@JsonKey(name: 'has_practice')
final bool? hasPractice;
@JsonKey(name: 'lessons_count')
final int? lessonsCount;
@JsonKey(name: 'practices_count')
final int? practice;
const CourseModule(
{this.id,
this.icon,
this.name,
this.unitId,
this.practice,
this.thumbnail,
this.sortOrder,
this.hasPractice,
this.description,
this.lessonsCount});
factory CourseModule.fromJson(Map<String, dynamic> json) =>
_$CourseModuleFromJson(json);
Map<String, dynamic> toJson() => _$CourseModuleToJson(this);
}

View File

@ -0,0 +1,34 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'course_module.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CourseModule _$CourseModuleFromJson(Map<String, dynamic> json) => CourseModule(
id: (json['id'] as num?)?.toInt(),
icon: json['icon'] as String?,
name: json['name'] as String?,
unitId: (json['unit_id'] as num?)?.toInt(),
practice: (json['practices_count'] as num?)?.toInt(),
thumbnail: json['thumbnail'] as String?,
sortOrder: (json['sort_order'] as num?)?.toInt(),
hasPractice: json['has_practice'] as bool?,
description: json['description'] as String?,
lessonsCount: (json['lessons_count'] as num?)?.toInt(),
);
Map<String, dynamic> _$CourseModuleToJson(CourseModule instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'icon': instance.icon,
'thumbnail': instance.thumbnail,
'description': instance.description,
'unit_id': instance.unitId,
'sort_order': instance.sortOrder,
'has_practice': instance.hasPractice,
'lessons_count': instance.lessonsCount,
'practices_count': instance.practice,
};

View File

@ -0,0 +1,80 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:yimaru_app/models/course_module.dart';
part 'course_unit.g.dart';
@JsonSerializable()
class CourseUnit {
final int? id;
final String? name;
final String? thumbnail;
final String? description;
final List<CourseModule>? modules;
@JsonKey(name: 'sort_order')
final int? sortOrder;
@JsonKey(name: 'has_practice')
final bool? hasPractice;
@JsonKey(name: 'lessons_count')
final int? lessonsCount;
@JsonKey(name: 'modules_count')
final int? modulesCount;
@JsonKey(name: 'practices_count')
final int? practice;
@JsonKey(name: 'catalog_course_id')
final int? catalogCourseId;
const CourseUnit(
{this.id,
this.name,
this.modules,
this.practice,
this.thumbnail,
this.sortOrder,
this.hasPractice,
this.description,
this.modulesCount,
this.lessonsCount,
this.catalogCourseId});
factory CourseUnit.fromJson(Map<String, dynamic> json) =>
_$CourseUnitFromJson(json);
Map<String, dynamic> toJson() => _$CourseUnitToJson(this);
CourseUnit copyWith({
int? id,
String? name,
int? practice,
int? sortOrder,
bool? hasPractice,
int? lessonsCount,
int? modulesCount,
String? thumbnail,
String? description,
int? catalogCourseId,
List<CourseModule>? modules,
}) =>
CourseUnit(
id: id ?? this.id,
name: name ?? this.name,
modules: modules ?? this.modules,
practice: practice ?? this.practice,
thumbnail: thumbnail ?? this.thumbnail,
sortOrder: sortOrder ?? this.sortOrder,
hasPractice: hasPractice ?? this.hasPractice,
description: description ?? this.description,
lessonsCount: lessonsCount ?? this.lessonsCount,
modulesCount: modulesCount ?? this.modulesCount,
catalogCourseId: catalogCourseId ?? this.catalogCourseId,
);
}

View File

@ -0,0 +1,38 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'course_unit.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CourseUnit _$CourseUnitFromJson(Map<String, dynamic> json) => CourseUnit(
id: (json['id'] as num?)?.toInt(),
name: json['name'] as String?,
modules: (json['modules'] as List<dynamic>?)
?.map((e) => CourseModule.fromJson(e as Map<String, dynamic>))
.toList(),
practice: (json['practices_count'] as num?)?.toInt(),
thumbnail: json['thumbnail'] as String?,
sortOrder: (json['sort_order'] as num?)?.toInt(),
hasPractice: json['has_practice'] as bool?,
description: json['description'] as String?,
modulesCount: (json['modules_count'] as num?)?.toInt(),
lessonsCount: (json['lessons_count'] as num?)?.toInt(),
catalogCourseId: (json['catalog_course_id'] as num?)?.toInt(),
);
Map<String, dynamic> _$CourseUnitToJson(CourseUnit instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'thumbnail': instance.thumbnail,
'description': instance.description,
'modules': instance.modules,
'sort_order': instance.sortOrder,
'has_practice': instance.hasPractice,
'lessons_count': instance.lessonsCount,
'modules_count': instance.modulesCount,
'practices_count': instance.practice,
'catalog_course_id': instance.catalogCourseId,
};

View File

@ -14,6 +14,8 @@ import 'package:yimaru_app/services/dio_service.dart';
import 'package:yimaru_app/ui/common/app_constants.dart';
import '../app/app.locator.dart';
import '../models/course_module.dart';
import '../models/course_unit.dart';
import '../models/learn_course.dart';
import '../models/learn_module.dart';
import '../models/learn_question.dart';
@ -729,6 +731,54 @@ class ApiService {
}
}
// Get course units
Future<List<CourseUnit>> getCourseUnits(int id) async {
try {
List<CourseUnit> units = [];
final Response response = await _service.dio.get(
'$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kExamPrepUrl/$kCatalogCoursesUrl/$id/$kUnitsUrl');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data']['units'] as List;
units = decodedData.map(
(e) {
return CourseUnit.fromJson(e);
},
).toList();
return units;
}
return [];
} catch (e) {
return [];
}
}
// Get course modules
Future<List<CourseModule>> getCourseModules(int id) async {
try {
List<CourseModule> modules = [];
final Response response = await _service.dio.get(
'$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kExamPrepUrl/$kUnitsUrl/$id/$kModulesUrl');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data']['modules'] as List;
modules = decodedData.map(
(e) {
return CourseModule.fromJson(e);
},
).toList();
return modules;
}
return [];
} catch (e) {
return [];
}
}
/* TO BE MODIFIED*/
// Get courses

View File

@ -1,13 +1,78 @@
import 'package:flutter_html/flutter_html.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/app/app.locator.dart';
import 'package:yimaru_app/models/course_progress.dart';
import 'package:yimaru_app/services/api_service.dart';
import '../models/course_catalog.dart';
import '../models/course_detail.dart';
import '../models/course_module.dart';
import '../models/course_unit.dart';
class CourseService {
class CourseService with ListenableServiceMixin {
// Dependency injection
final _apiService = locator<ApiService>();
// Initialization
courseService() {
listenToReactiveValues([_catalogs]);
}
// Course catalogs
List<CourseCatalog> _catalogs = [];
List<CourseCatalog> get catalogs => _catalogs;
// Course units
List<CourseUnit> _units = [];
List<CourseUnit> get units => _units;
// Course modules
List<CourseModule> _modules = [];
List<CourseModule> get modules => _modules;
// Course catalogs
Future<void> getCourseCatalogs() async {
_catalogs = await _apiService.getCourseCatalogs();
_catalogs.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
notifyListeners();
}
// Course units
Future<void> getCourseUnits(int id) async {
_units = await _apiService.getCourseUnits(id);
_units.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
notifyListeners();
}
// Course modules
Future<void> getCourseUnitModule({
required int id,
required int index,
}) async {
List<CourseModule> modules = await _apiService.getCourseModules(id);
modules.sort(
(a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0),
);
final updatedUnit = _units[index].copyWith(
modules: modules,
);
_units[index] = updatedUnit;
notifyListeners();
}
Future<void> getCourseModules(int id) async {
_modules = await _apiService.getCourseModules(id);
_modules.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
notifyListeners();
}
// Get course detail
Future<List<CourseDetail>> getCoursesDetail(int id) async {
final courses = await _apiService.getCourses(id);

View File

@ -12,8 +12,8 @@ class LearnService with ListenableServiceMixin {
final _apiService = locator<ApiService>();
// Initialization
LearnLessonService() {
listenToReactiveValues([_programs, _lessons]);
learnService() {
listenToReactiveValues([_programs, _lessons, _modules]);
}
// Learn program

View File

@ -0,0 +1,47 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
class LocalizationService with ListenableServiceMixin {
// Initialization
localizationService() {
listenToReactiveValues([_selectedLanguage]);
}
// Languages
Map<String, dynamic> _selectedLanguage = {
'code': 'EN',
'language': 'English'
};
Map<String, dynamic> get selectedLanguage => _selectedLanguage;
final List<Map<String, dynamic>> _languages = [
{'code': 'አማ', 'language': 'አማርኛ'},
{'code': 'EN', 'language': 'English'},
];
List<Map<String, dynamic>> get languages => _languages;
bool isSelectedLanguage(String title) =>
_selectedLanguage['language'] == title;
Future<void> setSelectedLanguage(
{required BuildContext context,
required Map<String, dynamic> title}) async {
_selectedLanguage = title;
if (title['code'] == 'አማ') {
await setAmharicLanguage(context);
} else {
await setAmharicLanguage(context);
}
notifyListeners();
}
Future<void> setAmharicLanguage(BuildContext context) async =>
await context.setLocale(const Locale('am'));
Future<void> setEnglishLanguage(BuildContext context) async =>
await context.setLocale(const Locale('en'));
}

View File

@ -3,6 +3,8 @@ String kBaseUrl = 'https://api.yimaruacademy.com';
String kApiUrl = 'api';
String kUnitsUrl = 'units';
String kApiVersionUrl = 'v1';
String kLevelsUrl = 'levels';

View File

@ -32,6 +32,7 @@ enum StateObjects {
register,
verifyOtp,
resendOtp,
courseUnits,
assessments,
startupView,
learnLessons,
@ -39,6 +40,7 @@ enum StateObjects {
learnCourses,
profileImage,
learnPrograms,
courseModules,
courseLessons,
profileUpdate,
resetPassword,
@ -49,7 +51,6 @@ enum StateObjects {
loginWithGoogle,
loadLessonVideo,
loadCourseVideo,
learnSubmodules,
requestResetCode,
profileCompletion,
learnSubscription,

View File

@ -0,0 +1,182 @@
// DO NOT EDIT. This is code generated via package:easy_localization/generate.dart
// ignore_for_file: prefer_single_quotes, avoid_renaming_method_parameters, constant_identifier_names
import 'dart:ui';
import 'package:easy_localization/easy_localization.dart' show AssetLoader;
class CodegenLoader extends AssetLoader{
const CodegenLoader();
@override
Future<Map<String, dynamic>?> load(String path, Locale locale) {
return Future.value(mapLocales[locale.toString()]);
}
static const Map<String,dynamic> _am = {
"welcome_back": "እንኳን በደህና ተመለሱ",
"dont_have_account": "መለያ የለዎትም? ይመዝገቡ",
"email": "ኢሜይል",
"password": "የይለፍ ቃል",
"forgot_password": "የይለፍ ቃል ረሱ?",
"cont": "ቀጥል",
"register": "ይመዝገቡ",
"login_with_google": "በጉግል ይግቡ",
"or": "ወይም",
"login_with_phone": "በስልክ ቁጥር ይግቡ",
"create_account": "አዲስ መለያ ይፍጠሩ",
"already_have_account": "መለያ አለዎት?",
"login": " ይግቡ ",
"register_with_google": "በጉግል ይመዝገቡ",
"register_with_phone": "በስልክ ቁጥር ይመዝገቡ",
"enter_phone_number": "የስልክ ቁጥርዎን ያስገቡ። የማረጋገጫ ኮድ እንልክልዎታለን።",
"login_with_email": "በኢሜይል ይግቡ",
"create_password": "የይለፍ ቃል ይፍጠሩ",
"confirm_password": "የይለፍ ቃል ያረጋግጡ",
"sign_up_agreement": "‘ይመዝገቡ’ የሚለውን ሲጫኑ በ‘አገልግሎት ውሎች’ እና ‘በግላዊነት ፖሊሲ’ ይስማማሉ።",
"reset_password": " የይለፍ ቃልን ይቀይሩ ",
"enter_email_reset_code": "ኢሜይልዎን ያስገቡ። የይለፍ ቃል መለወጫ ኮድ እንልክልዎታለን።",
"please_wait": "እባክዎ ይጠብቁ",
"reset_code_sent": "የመቀየሪያ ኮድ በተሳካ ሁኔታ ተልኳል",
"reset_code": " የመቀየሪያ ኮድ ",
"new_password": "አዲስ የይለፍ ቃል",
"logged_in_successfully": "በተሳካ ሁኔታ ገብተዋል",
"view_course": " ኮርሱን ይመልከቱ ",
"take_practice": " ልምምድ ያድርጉ ",
"your_current_level": "የአሁኑ ደረጃዎ",
"overall_progress": "አጠቃላይ እድገት",
"great_work": "በርቱ! በጣም ጥሩ እየሰሩ ነው ",
"view_module": "ሞጁሉን ይመልከቱ",
"progress": "እድገት",
"keep_going": " ይቀጥሉ - ከግማሽ በላይ ጨርሰዋል ",
"lessons_in_module": " በዚህ ሞጁል ውስጥ ያሉ ትምህርቶች ",
"practice": "ልምምድ",
"start": "ጀምር",
"in_progress": "በሂደት ላይ",
"hello": "ሰላም",
"ready_to_learn": " ዛሬ እንግሊዝኛ ለመማር ተዘጋጅተዋል? ",
"learn": "ይማሩ ",
"course": "ኮርስ",
"profile": " ፕሮፋይል ",
"speaking_partner": "የንግግር ጓደኛ ",
"practice_what_you_learned": "አሁን የተማሩትን እንለማመድ",
"practice_questions": "ጥቂት ጥያቄዎችን እጠይቃለሁ እና መልስ መስጠት ይችላሉ",
"start_practice": "ልምምድ ጀምር",
"almost_there": "ሊጨርሱ ተቃርበዋል",
"finish_session": "እድገትዎን ለማየት ክፍለ ጊዜውን ያጠናቅቁ",
"continue_practice": "ልምምዱን ይቀጥሉ",
"end_session": "ክፍለ ጊዜውን ያብቁ ",
"tap_start_to_listen": "ለማዳመጥ የጀምር ቁልፉን ይጫኑ",
"practice_speaking": "ንግግርን ይለማመዱ ",
"tap_microphone": "ለመናገር ማይክሮፎኑን ይጫኑ",
"reply": "እንደገና አዳምጥ",
"cancel": "ይቅር",
"you_are_speaking": "እየተናገሩ ነው",
"practice_completed": "ልምምዱ ተጠናቅቋል",
"great_improvement": "በዚህኛው በራስ መተማመንዎ ጨምሯል፤ ትልቅ መሻሻል ነው",
"practice_again": "እንደገና ይለማመዱ",
"conversation_review": "የንግግር ግምገማ ",
"result": "ውጤት",
"quick_tip": "ጠቃሚ ምክር",
"retry": "እንደገና ይሞክሩ",
"completed_a1": "እንኳን ደስ አለዎት! A1 ደረጃን አጠናቅቀዋል",
"analyzing_speaking": "የንግግር ችሎታዎን እየገመገምን ነው",
"view_profile": "ፕሮፋይሎን ይመልከቱ ",
"hi": "ሰላም",
"edit_profile": "መገለጫ ያስተካክሉ",
"first_name": "የመጀመሪያ ስም",
"last_name": "የአባት ስም",
"gender": "ፆታ",
"male": "ወንድ",
"female": "ሴት",
"phone_number": "የስልክ ቁጥር",
"country": "ሀገር",
"region": "ክልል",
"occupation": "የስራ መስክ ",
"save_changes": "ለውጦችን ያስቀምጡ"
};
static const Map<String,dynamic> _en = {
"welcome_back": "Welcome back",
"dont_have_account": "Don't have an account? Register",
"email": "Email",
"password": "Password",
"forgot_password": "Forgot password?",
"cont": "Continue",
"register": "Register",
"login_with_google": "Login with Google",
"or": "Or",
"login_with_phone": "Login with phone number",
"create_account": "Create an account",
"already_have_account": "Already have an account?",
"login": "Login",
"register_with_google": "Register with Google",
"register_with_phone": "Register with phone number",
"enter_phone_number": "Enter your phone number. We will send you a confirmation code there.",
"login_with_email": "Login with email",
"create_password": "Create password",
"confirm_password": "Confirm password",
"sign_up_agreement": "By clicking Sign Up, you agree to our Terms of Service and Privacy Policy",
"reset_password": "Reset Password",
"enter_email_reset_code": "Enter your email. We will send you a reset code.",
"please_wait": "Please wait",
"reset_code_sent": "Reset code sent successfully",
"reset_code": "Reset code",
"new_password": "New password",
"logged_in_successfully": "Logged in successfully",
"view_course": "View course",
"take_practice": "Take practice",
"your_current_level": "Your current level",
"overall_progress": "Overall progress",
"great_work": "Keep up the great work! You're doing amazing",
"view_module": "View module",
"progress": "Progress",
"keep_going": "Let's keep going - you're more than half there",
"lessons_in_module": "Lessons in this module",
"practice": "Practice",
"start": "Start",
"in_progress": "In Progress",
"hello": "Hello",
"ready_to_learn": "Ready to keep learning English today",
"learn": "Learn",
"course": "Course",
"profile": "Profile",
"speaking_partner": "Speaking partner",
"practice_what_you_learned": "Let's practice what you just learnt",
"practice_questions": "I will ask you a few questions and you can respond",
"start_practice": "Start practice",
"almost_there": "You're almost there",
"finish_session": "Finish the session to see your progress",
"continue_practice": "Continue practice",
"end_session": "End session",
"tap_start_to_listen": "Tap the start button to listen",
"practice_speaking": "Practice speaking",
"tap_microphone": "Tap the microphone to speak",
"reply": "Reply",
"cancel": "Cancel",
"you_are_speaking": "You're speaking",
"practice_completed": "Practice completed",
"great_improvement": "You sound more confident this time, great improvement",
"practice_again": "Practice again",
"conversation_review": "Conversation review",
"result": "Result",
"quick_tip": "Quick tip",
"retry": "Retry",
"completed_a1": "Yay, you've completed A1",
"analyzing_speaking": "We're now analyzing your speaking skill",
"view_profile": "View profile",
"hi": "Hi",
"edit_profile": "Edit profile",
"first_name": "First name",
"last_name": "Last name",
"gender": "Gender",
"male": "Male",
"female": "Female",
"phone_number": "Phone number",
"country": "Country",
"region": "Region",
"occupation": "Occupation",
"save_changes": "Save changes"
};
static const Map<String, Map<String,dynamic>> mapLocales = {"am": _am, "en": _en};
}

View File

@ -0,0 +1,85 @@
// DO NOT EDIT. This is code generated via package:easy_localization/generate.dart
// ignore_for_file: constant_identifier_names
abstract class LocaleKeys {
static const welcome_back = 'welcome_back';
static const dont_have_account = 'dont_have_account';
static const email = 'email';
static const password = 'password';
static const forgot_password = 'forgot_password';
static const cont = 'cont';
static const register = 'register';
static const login_with_google = 'login_with_google';
static const or = 'or';
static const login_with_phone = 'login_with_phone';
static const create_account = 'create_account';
static const already_have_account = 'already_have_account';
static const login = 'login';
static const register_with_google = 'register_with_google';
static const register_with_phone = 'register_with_phone';
static const enter_phone_number = 'enter_phone_number';
static const login_with_email = 'login_with_email';
static const create_password = 'create_password';
static const confirm_password = 'confirm_password';
static const sign_up_agreement = 'sign_up_agreement';
static const reset_password = 'reset_password';
static const enter_email_reset_code = 'enter_email_reset_code';
static const please_wait = 'please_wait';
static const reset_code_sent = 'reset_code_sent';
static const reset_code = 'reset_code';
static const new_password = 'new_password';
static const logged_in_successfully = 'logged_in_successfully';
static const view_course = 'view_course';
static const take_practice = 'take_practice';
static const your_current_level = 'your_current_level';
static const overall_progress = 'overall_progress';
static const great_work = 'great_work';
static const view_module = 'view_module';
static const progress = 'progress';
static const keep_going = 'keep_going';
static const lessons_in_module = 'lessons_in_module';
static const practice = 'practice';
static const start = 'start';
static const in_progress = 'in_progress';
static const hello = 'hello';
static const ready_to_learn = 'ready_to_learn';
static const learn = 'learn';
static const course = 'course';
static const profile = 'profile';
static const speaking_partner = 'speaking_partner';
static const practice_what_you_learned = 'practice_what_you_learned';
static const practice_questions = 'practice_questions';
static const start_practice = 'start_practice';
static const almost_there = 'almost_there';
static const finish_session = 'finish_session';
static const continue_practice = 'continue_practice';
static const end_session = 'end_session';
static const tap_start_to_listen = 'tap_start_to_listen';
static const practice_speaking = 'practice_speaking';
static const tap_microphone = 'tap_microphone';
static const reply = 'reply';
static const cancel = 'cancel';
static const you_are_speaking = 'you_are_speaking';
static const practice_completed = 'practice_completed';
static const great_improvement = 'great_improvement';
static const practice_again = 'practice_again';
static const conversation_review = 'conversation_review';
static const result = 'result';
static const quick_tip = 'quick_tip';
static const retry = 'retry';
static const completed_a1 = 'completed_a1';
static const analyzing_speaking = 'analyzing_speaking';
static const view_profile = 'view_profile';
static const hi = 'hi';
static const edit_profile = 'edit_profile';
static const first_name = 'first_name';
static const last_name = 'last_name';
static const gender = 'gender';
static const phone_number = 'phone_number';
static const country = 'country';
static const region = 'region';
static const occupation = 'occupation';
static const save_changes = 'save_changes';
}

View File

@ -10,15 +10,25 @@ import '../../../app/app.locator.dart';
import '../../../app/app.router.dart';
import '../../../models/assessment.dart';
import '../../../services/api_service.dart';
import '../../../services/localization_service.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
class AssessmentViewModel extends BaseViewModel {
class AssessmentViewModel extends ReactiveViewModel {
// Dependency injection
final _apiService = locator<ApiService>();
final _dialogService = locator<DialogService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
final _localizationService = locator<LocalizationService>();
@override
List<ListenableServiceMixin> get listenableServices => [_localizationService];
// Languages
Map<String, dynamic> get _selectedLanguage => _localizationService.selectedLanguage;
Map<String, dynamic> get selectedLanguage => _selectedLanguage;
// In-app navigation
int _currentPage = 0;

View File

@ -67,7 +67,8 @@ class AssessmentIntroScreen extends ViewModelWidget<AssessmentViewModel> {
showBackButton: true,
onPop: viewModel.goBack,
showLanguageSelection: true,
onLanguage: () async => await viewModel.navigateToLanguage(),
language: viewModel.selectedLanguage['code'],
onLanguage: () async => await viewModel.navigateToLanguage(),
);
Widget _buildTitle() => Text(

View File

@ -35,7 +35,9 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
showBackButton: true,
onPop: viewModel.goBack,
showLanguageSelection: true,
onLanguage: () async => await viewModel.navigateToLanguage(),
language: viewModel.selectedLanguage['code'],
onLanguage: () async => await viewModel.navigateToLanguage(),
);
Widget _buildExpandedBody(AssessmentViewModel viewModel) =>

View File

@ -52,6 +52,8 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
showBackButton: true,
onPop: viewModel.goBack,
showLanguageSelection: true,
language: viewModel.selectedLanguage['code'],
onLanguage: () async => await viewModel.navigateToLanguage(),
);
Widget _buildExpandedBody(AssessmentViewModel viewModel) =>

View File

@ -34,11 +34,11 @@ class CourseViewModel extends ReactiveViewModel {
'description':
'Prepare for IELTS, TOEFL, or Duolingo with structured practice.'
},
{
/* {
'title': 'Skill-Based Courses',
'description':
'Learn English for the workplace, travel, and real-life communication.'
},
},*/
];
List<Map<String, dynamic>> get courses => _courses;

View File

@ -101,23 +101,25 @@ class CourseCatalogView extends StackedView<CourseCatalogViewModel> {
Widget _buildListView(CourseCatalogViewModel viewModel) => ListView.separated(
shrinkWrap: true,
itemCount: viewModel.courseCatalogs.length,
itemCount: viewModel.catalogs.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildTile(
courseCatalog: viewModel.courseCatalogs[index],
onCourseTap: () {},
onPracticeTap: () {}),
onPracticeTap: () {},
catalog: viewModel.catalogs[index],
onCourseTap: () async =>
await viewModel.navigateToCourseUnit(viewModel.catalogs[index]),
),
separatorBuilder: (context, index) => verticalSpaceSmall,
);
Widget _buildTile({
required CourseCatalog courseCatalog,
required CourseCatalog catalog,
required GestureTapCallback onCourseTap,
required GestureTapCallback onPracticeTap,
}) =>
CourseCatalogTile(
catalog: catalog,
onCourseTap: onCourseTap,
onPracticeTap: onPracticeTap,
courseCatalog: courseCatalog,
);
}

View File

@ -5,21 +5,25 @@ import 'package:yimaru_app/models/course_catalog.dart';
import '../../../app/app.locator.dart';
import '../../../app/app.router.dart';
import '../../../services/api_service.dart';
import '../../../services/course_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
class CourseCatalogViewModel extends BaseViewModel {
class CourseCatalogViewModel extends ReactiveViewModel {
// Dependency injection
final _apiService = locator<ApiService>();
final _courseService = locator<CourseService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
// Course catalogs
List<CourseCatalog> _courseCatalogs = [];
@override
List<ListenableServiceMixin> get listenableServices => [_courseService];
List<CourseCatalog> get courseCatalogs => _courseCatalogs;
// Learn lessons
List<CourseCatalog> get _catalogs => _courseService.catalogs;
List<CourseCatalog> get catalogs => _catalogs;
// Navigation
void pop() => _navigationService.back();
@ -27,19 +31,19 @@ class CourseCatalogViewModel extends BaseViewModel {
Future<void> navigateToCoursePractice(int id) async =>
_navigationService.navigateToCoursePracticeView(id: id);
// Future<void> navigateToSubcourse(Subcategory subcategory) async =>
// _navigationService.navigateToCourseView(subcategory: subcategory);
Future<void> navigateToCourseUnit(CourseCatalog catalog) async =>
await _navigationService.navigateToCourseUnitView(catalog: catalog);
// Remote api call
// Course catalogs
Future<void> getCourseCatalogs() async =>
await runBusyFuture(_getSubcategories(),
await runBusyFuture(_getCourseCatalogs(),
busyObject: StateObjects.courseCatalogs);
Future<void> _getSubcategories() async {
Future<void> _getCourseCatalogs() async {
if (await _statusChecker.checkConnection()) {
_courseCatalogs = await _apiService.getCourseCatalogs();
await _courseService.getCourseCatalogs();
}
}
}

View File

@ -3,6 +3,7 @@ import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
import '../../../app/app.router.dart';
import '../../../models/option.dart';
import '../../../models/assessment_question.dart';
import '../../../services/api_service.dart';
@ -153,6 +154,8 @@ class CoursePracticeQuestionViewModel extends FormViewModel {
// Navigation
void pop() => _navigationService.back();
// Remote api call
// Question navigation

View File

@ -43,8 +43,9 @@ class QuestionLoadingScreen extends StatelessWidget {
Widget _buildAppBar() => LargeAppBar(
onPop: onPop,
showBackButton: true,
showLanguageSelection: true,
);
showLanguageSelection: false,
);
Widget _buildBody() => Expanded(child: Container());

View File

@ -1,29 +1,148 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/models/course_catalog.dart';
import 'package:yimaru_app/ui/widgets/course_unit_tile.dart';
import '../../../models/course_unit.dart';
import '../../common/app_colors.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/course_module_banner.dart';
import '../../widgets/custom_circular_progress_indicator.dart';
import '../../widgets/custom_elevated_button.dart';
import '../../widgets/overall_progress.dart';
import '../../widgets/small_app_bar.dart';
import 'course_unit_viewmodel.dart';
class CourseUnitView extends StackedView<CourseUnitViewModel> {
const CourseUnitView({Key? key}) : super(key: key);
final CourseCatalog catalog;
const CourseUnitView({Key? key, required this.catalog}) : super(key: key);
@override
void onViewModelReady(CourseUnitViewModel viewModel) async {
await viewModel.getCourseUnits(catalog.id ?? 0);
super.onViewModelReady(viewModel);
}
@override
CourseUnitViewModel viewModelBuilder(BuildContext context) =>
CourseUnitViewModel();
@override
Widget builder(
BuildContext context,
CourseUnitViewModel viewModel,
Widget? child,
) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
body: Container(
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
child: const Center(child: Text("CourseUnitView")),
),
);
}
@override
CourseUnitViewModel viewModelBuilder(
BuildContext context,
) =>
CourseUnitViewModel();
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(CourseUnitViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(CourseUnitViewModel viewModel) =>
SafeArea(child: _buildBody(viewModel));
Widget _buildBody(CourseUnitViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel),
);
Widget _buildColumn(CourseUnitViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(viewModel),
verticalSpaceMedium,
_buildModulesColumnWrapper(viewModel),
],
);
Widget _buildAppBar(CourseUnitViewModel viewModel) => SmallAppBar(
onPop: viewModel.pop,
showBackButton: true,
title: 'Course Detail',
);
Widget _buildModulesColumnWrapper(CourseUnitViewModel viewModel) =>
Expanded(child: _buildLevelsColumnScrollView(viewModel));
Widget _buildLevelsColumnScrollView(CourseUnitViewModel viewModel) =>
SingleChildScrollView(
child: _buildLevelsColumn(viewModel),
);
Widget _buildLevelsColumn(CourseUnitViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildLevelsColumnChildren(viewModel),
);
List<Widget> _buildLevelsColumnChildren(CourseUnitViewModel viewModel) => [
verticalSpaceMedium,
_buildTitle(),
verticalSpaceMedium,
_buildCourseModuleBanner(),
verticalSpaceMedium,
_buildOverallProgress(),
verticalSpaceTiny,
_buildContinueButton(viewModel),
verticalSpaceMedium,
_buildListViewBuilder(viewModel)
];
Widget _buildTitle() => Text(
catalog.name ?? '',
style: style18P600,
);
Widget _buildCourseModuleBanner() => const CourseModuleBanner();
Widget _buildOverallProgress() => const OverallProgress(
progress: 0,
backgroundColor: Colors.transparent,
indicatorBackgroundColor: kcVeryLightGrey,
);
Widget _buildContinueButton(CourseUnitViewModel viewModel) =>
const CustomElevatedButton(
height: 55,
borderRadius: 12,
foregroundColor: kcWhite,
text: 'Continue Course',
backgroundColor: kcPrimaryColor);
Widget _buildListViewBuilder(CourseUnitViewModel viewModel) =>
viewModel.busy(StateObjects.courseUnits)
? _buildProgressIndicator()
: _buildListView(viewModel);
Widget _buildProgressIndicator() => const Center(
child: CustomCircularProgressIndicator(color: kcPrimaryColor),
);
Widget _buildListView(CourseUnitViewModel viewModel) => ListView.builder(
shrinkWrap: true,
itemCount: viewModel.units.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildTile(
index: index,
unit: viewModel.units[index],
onPracticeTap: () {},
onLessonTap: () {}),
);
Widget _buildTile({
required int index,
required CourseUnit unit,
required GestureTapCallback onLessonTap,
required GestureTapCallback onPracticeTap,
}) =>
CourseUnitTile(
unit: unit,
index: index,
onLessonTap: onLessonTap,
onPracticeTap: onPracticeTap,
);
}

View File

@ -1,3 +1,55 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/models/course_module.dart';
class CourseUnitViewModel extends BaseViewModel {}
import '../../../app/app.locator.dart';
import '../../../models/course_unit.dart';
import '../../../services/api_service.dart';
import '../../../services/course_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
class CourseUnitViewModel extends ReactiveViewModel {
// Dependency injection
final _courseService = locator<CourseService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
@override
List<ListenableServiceMixin> get listenableServices => [_courseService];
// Course units
List<CourseUnit> get _units => _courseService.units;
List<CourseUnit> get units => _units;
// Navigation
void pop() => _navigationService.back();
// Remote api call
// Course units
Future<void> getCourseUnits(int id) async =>
await runBusyFuture(_getCourseUnits(id),
busyObject: StateObjects.courseUnits);
Future<void> _getCourseUnits(int id) async {
if (await _statusChecker.checkConnection()) {
await _courseService.getCourseUnits(id);
}
}
Future<void> getCourseUnitModules(
{required int id, required int index}) async =>
await runBusyFuture(_getCourseUnitModules(id: id, index: index),
busyObject: StateObjects.courseModules);
Future<void> _getCourseUnitModules(
{required int id, required int index}) async {
if (await _statusChecker.checkConnection()) {
await _courseService.getCourseUnitModule(id: id, index: index);
}
}
}

View File

@ -4,17 +4,30 @@ import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
import '../../../app/app.router.dart';
import '../../../services/api_service.dart';
import '../../../services/localization_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
class ForgetPasswordViewModel extends FormViewModel {
class ForgetPasswordViewModel extends ReactiveViewModel
with FormStateHelper
implements FormViewModel {
final _apiService = locator<ApiService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
final _localizationService = locator<LocalizationService>();
@override
List<ListenableServiceMixin> get listenableServices => [_localizationService];
// Languages
Map<String, dynamic> get _selectedLanguage => _localizationService.selectedLanguage;
Map<String, dynamic> get selectedLanguage => _selectedLanguage;
// User data
final Map<String, dynamic> _userData = {};
@ -165,6 +178,10 @@ class ForgetPasswordViewModel extends FormViewModel {
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToLanguage() async =>
await _navigationService.navigateToLanguageView();
Future<void> replaceWithLogin() async =>
await _navigationService.clearStackAndShow(Routes.loginView);

View File

@ -88,7 +88,11 @@ class RequestCodeScreen extends ViewModelWidget<ForgetPasswordViewModel> {
showBackButton: true,
showLanguageSelection: true,
onPop: () => _inAppPop(viewModel),
);
language: viewModel.selectedLanguage['code'],
onLanguage: ()async => await viewModel.navigateToLanguage(),
);
Widget _buildExpandedBody(
{required BuildContext context,

View File

@ -86,7 +86,11 @@ class ResetPasswordScreen extends ViewModelWidget<ForgetPasswordViewModel> {
showBackButton: true,
showLanguageSelection: true,
onPop: () => _inAppPop(viewModel),
);
language: viewModel.selectedLanguage['code'],
onLanguage: ()async => await viewModel.navigateToLanguage(),
);
Widget _buildExpandedBody(ForgetPasswordViewModel viewModel) =>
Expanded(child: _buildColumnScroller(viewModel));

View File

@ -22,48 +22,72 @@ class LanguageView extends StackedView<LanguageViewModel> {
LanguageViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
_buildScaffoldWrapper(context: context, viewModel: viewModel);
Widget _buildScaffoldWrapper(LanguageViewModel viewModel) => Scaffold(
Widget _buildScaffoldWrapper(
{required BuildContext context,
required LanguageViewModel viewModel}) =>
Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
body: _buildScaffold(context: context, viewModel: viewModel),
);
Widget _buildScaffold(LanguageViewModel viewModel) =>
SafeArea(child: _buildBodyWrapper(viewModel));
Widget _buildScaffold(
{required BuildContext context,
required LanguageViewModel viewModel}) =>
SafeArea(
child: _buildBodyWrapper(context: context, viewModel: viewModel));
Widget _buildBodyWrapper(LanguageViewModel viewModel) => Column(
Widget _buildBodyWrapper(
{required BuildContext context,
required LanguageViewModel viewModel}) =>
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildBodyChildren(viewModel),
children: _buildBodyChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildBodyChildren(LanguageViewModel viewModel) => [
List<Widget> _buildBodyChildren(
{required BuildContext context,
required LanguageViewModel viewModel}) =>
[
verticalSpaceMedium,
_buildAppBarWrapper(viewModel),
_buildExpandedBody(viewModel)
_buildExpandedBody(context: context, viewModel: viewModel)
];
Widget _buildExpandedBody(LanguageViewModel viewModel) =>
Expanded(child: _buildColumnWrapper(viewModel));
Widget _buildExpandedBody(
{required BuildContext context,
required LanguageViewModel viewModel}) =>
Expanded(
child: _buildColumnWrapper(context: context, viewModel: viewModel));
Widget _buildColumnWrapper(LanguageViewModel viewModel) => Padding(
Widget _buildColumnWrapper(
{required BuildContext context,
required LanguageViewModel viewModel}) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel),
child: _buildColumn(context: context, viewModel: viewModel),
);
Widget _buildColumn(LanguageViewModel viewModel) => Column(
Widget _buildColumn(
{required BuildContext context,
required LanguageViewModel viewModel}) =>
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildColumnChildren(viewModel),
children: _buildColumnChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildColumnChildren(LanguageViewModel viewModel) => [
List<Widget> _buildColumnChildren(
{required BuildContext context,
required LanguageViewModel viewModel}) =>
[
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubtitle(),
verticalSpaceMedium,
_buildLanguages(viewModel)
_buildLanguages(context: context, viewModel: viewModel)
];
Widget _buildAppBarWrapper(LanguageViewModel viewModel) => Padding(
@ -87,16 +111,19 @@ class LanguageView extends StackedView<LanguageViewModel> {
style: style14MG400,
);
Widget _buildLanguages(LanguageViewModel viewModel) => ListView.builder(
Widget _buildLanguages(
{required BuildContext context,
required LanguageViewModel viewModel}) =>
ListView.builder(
shrinkWrap: true,
itemCount: viewModel.languages.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildLanguage(
title: viewModel.languages[index]['language'],
onTap: () => viewModel.setSelectedLanguage(
context: context, title: viewModel.languages[index]),
selected: viewModel
.isSelectedLanguage(viewModel.languages[index]['language']),
onTap: () =>
viewModel.setSelectedLanguage(viewModel.languages[index]),
),
);

View File

@ -1,34 +1,38 @@
import 'package:flutter/cupertino.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/services/localization_service.dart';
import '../../../app/app.locator.dart';
class LanguageViewModel extends BaseViewModel {
class LanguageViewModel extends ReactiveViewModel {
// Dependency injection
final _navigationService = locator<NavigationService>();
final _localizationService = locator<LocalizationService>();
@override
List<ListenableServiceMixin> get listenableServices => [_localizationService];
// Languages
Map<String, dynamic> _selectedLanguage = {
'code': 'EN',
'language': 'English'
};
Map<String, dynamic> get selectedLanguage => _selectedLanguage;
final List<Map<String, dynamic>> _languages = [
{'code': 'አማ', 'language': 'አማርኛ'},
{'code': 'EN', 'language': 'English'},
];
List<Map<String, dynamic>> get _languages => _localizationService.languages;
List<Map<String, dynamic>> get languages => _languages;
Map<String, dynamic> get _selectedLanguage => _localizationService.selectedLanguage;
Map<String, dynamic> get selectedLanguage => _selectedLanguage;
// Languages
bool isSelectedLanguage(String title) =>
_selectedLanguage['language'] == title;
_localizationService.isSelectedLanguage(title);
void setSelectedLanguage(Map<String, dynamic> title) {
_selectedLanguage = title;
rebuildUi();
}
Future<void> setSelectedLanguage(
{required BuildContext context,
required Map<String, dynamic> title}) async =>
await _localizationService.setSelectedLanguage(
context: context, title: title);
// Navigation
void pop() => _navigationService.back();

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/models/learn_module.dart';
import 'package:yimaru_app/ui/widgets/learn_module_tile.dart';
import 'package:yimaru_app/ui/widgets/overall_learn_progress.dart';
import 'package:yimaru_app/ui/widgets/overall_progress.dart';
import '../../../models/learn_course.dart';
import '../../common/app_colors.dart';
@ -96,7 +96,7 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
style: style14P400,
);
Widget _buildOverallProgress() => OverallLearnProgress(
Widget _buildOverallProgress() => OverallProgress(
indicatorBackgroundColor: kcWhite,
progress: course.access?.progressPercent ?? 0,
backgroundColor: kcPrimaryColor.withOpacity(0.1),

View File

@ -9,6 +9,7 @@ import 'package:yimaru_app/models/user.dart';
import '../../../services/api_service.dart';
import '../../../services/authentication_service.dart';
import '../../../services/google_auth_service.dart';
import '../../../services/localization_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
@ -24,16 +25,23 @@ class LoginViewModel extends ReactiveViewModel
final _googleAuthService = locator<GoogleAuthService>();
final _localizationService = locator<LocalizationService>();
final _authenticationService = locator<AuthenticationService>();
@override
List<ListenableServiceMixin> get listenableServices => [_googleAuthService];
List<ListenableServiceMixin> get listenableServices => [_googleAuthService,_localizationService];
// Google user
GoogleSignInAccount? get _googleUser => _googleAuthService.googleUser;
GoogleSignInAccount? get googleUser => _googleUser;
// Languages
Map<String, dynamic> get _selectedLanguage => _localizationService.selectedLanguage;
Map<String, dynamic> get selectedLanguage => _selectedLanguage;
// In-app navigation
int _currentPage = 0;
@ -158,9 +166,14 @@ class LoginViewModel extends ReactiveViewModel
Future<void> navigateToRegister() async =>
await _navigationService.navigateToRegisterView();
Future<void> navigateToLanguage() async =>
await _navigationService.navigateToLanguageView();
Future<void> navigateToForgetPassword() async =>
await _navigationService.navigateToForgetPasswordView();
Future<void> replaceWithStartUp() async =>
await _navigationService.clearStackAndShow(Routes.startupView);

View File

@ -80,10 +80,14 @@ class LoginOtpScreen extends ViewModelWidget<LoginViewModel> {
_buildExpandedBody(context: context, viewModel: viewModel)
];
Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar(
Widget _buildAppBar(LoginViewModel viewModel) => LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
);
language: viewModel.selectedLanguage['code'],
onLanguage: ()async => await viewModel.navigateToLanguage(),
);
Widget _buildExpandedBody(
{required BuildContext context, required LoginViewModel viewModel}) =>

View File

@ -1,5 +1,7 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
import 'package:yimaru_app/ui/views/login/login_view.form.dart';
import 'package:yimaru_app/ui/widgets/obscure_password.dart';
@ -76,9 +78,11 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
_buildExpandedBody(context: context, viewModel: viewModel)
];
Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar(
Widget _buildAppBar(LoginViewModel viewModel) => LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
language: viewModel.selectedLanguage['code'],
onLanguage: () async => await viewModel.navigateToLanguage(),
);
Widget _buildExpandedBody(
@ -140,7 +144,7 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
];
Widget _buildTitle() => Text(
'Welcome Back',
LocaleKeys.welcome_back.tr(),
style: style25DG600,
);
@ -150,10 +154,10 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
Widget _buildEmailFormField(LoginViewModel viewModel) => TextFormField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
onTap: viewModel.setEmailFocus,
keyboardType: TextInputType.emailAddress,
decoration: inputDecoration(
hint: 'Email',
hint: LocaleKeys.email.tr(),
focus: viewModel.focusEmail,
filled: emailController.text.isNotEmpty),
);
@ -173,7 +177,7 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
onTap: viewModel.setPasswordFocus,
obscureText: viewModel.obscurePassword,
decoration: inputDecoration(
hint: 'Password',
hint: LocaleKeys.password.tr(),
focus: viewModel.focusPassword,
suffix: _buildObscureButton(viewModel),
filled: passwordController.text.isNotEmpty),
@ -207,7 +211,7 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
);
Widget _buildForgetPasswordText() => Text(
'Forgot Password?',
LocaleKeys.forgot_password.tr(),
style: style14P400,
);
@ -227,9 +231,9 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
Widget _buildContinueButton(LoginViewModel viewModel) => CustomElevatedButton(
height: 55,
safe: false,
text: 'Continue',
borderRadius: 12,
foregroundColor: kcWhite,
text: LocaleKeys.cont.tr(),
onTap: emailController.text.isNotEmpty &&
passwordController.text.isNotEmpty
? () async => await _login(viewModel)
@ -245,9 +249,9 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
height: 55,
borderRadius: 12,
backgroundColor: kcWhite,
text: 'Login with Google',
borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor,
text: LocaleKeys.login_with_google.tr(),
leadingImage: 'assets/icons/google.png',
onTap: () async => await viewModel.signInWithGoogle(),
);
@ -263,7 +267,7 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
borderColor: kcPrimaryColor,
onTap: () => viewModel.goTo(1),
foregroundColor: kcPrimaryColor,
text: 'Login with Phone Number',
text: LocaleKeys.login_with_phone.tr()
);
Widget _buildLoginWithEmailState(LoginViewModel viewModel) =>

View File

@ -76,7 +76,11 @@ class LoginWithPhoneNumberScreen extends ViewModelWidget<LoginViewModel> {
showBackButton: true,
showLanguageSelection: true,
onPop: () => viewModel.goTo(0),
);
language: viewModel.selectedLanguage['code'],
onLanguage: ()async => await viewModel.navigateToLanguage(),
);
Widget _buildExpandedBody(
{required BuildContext context, required LoginViewModel viewModel}) =>

View File

@ -4,6 +4,7 @@ import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import '../../../app/app.locator.dart';
import '../../../services/google_auth_service.dart';
import '../../../services/localization_service.dart';
class OnboardingViewModel extends ReactiveViewModel
with FormStateHelper
@ -14,14 +15,24 @@ class OnboardingViewModel extends ReactiveViewModel
final _googleAuthService = locator<GoogleAuthService>();
final _localizationService = locator<LocalizationService>();
@override
List<ListenableServiceMixin> get listenableServices => [_googleAuthService];
List<ListenableServiceMixin> get listenableServices => [_googleAuthService,_localizationService];
// Google user
GoogleSignInAccount? get _googleUser => _googleAuthService.googleUser;
GoogleSignInAccount? get googleUser => _googleUser;
// Languages
Map<String, dynamic> get _selectedLanguage => _localizationService.selectedLanguage;
Map<String, dynamic> get selectedLanguage => _selectedLanguage;
// Navigation
int _currentPage = 0;
@ -207,20 +218,6 @@ class OnboardingViewModel extends ReactiveViewModel
List<String> get topics => _topics;
// Languages
final List<Map<String, dynamic>> _languages = [
{'code': 'አማ', 'language': 'አማርኛ'},
{'code': 'EN', 'language': 'English'},
];
List<Map<String, dynamic>> get languages => _languages;
Map<String, dynamic> _selectedLanguage = {
'code': 'EN',
'language': 'English'
};
Map<String, dynamic> get selectedLanguage => _selectedLanguage;
// User data
final Map<String, dynamic> _userData = {};
@ -548,15 +545,6 @@ class OnboardingViewModel extends ReactiveViewModel
bool isSelectedTopic(String value) => _selectedTopic == value;
// Language
void setSelectedLanguage(Map<String, dynamic> value) {
_selectedLanguage = value;
rebuildUi();
}
bool isSelectedLanguage(String value) =>
_selectedLanguage['language'] == value;
// Add user data
void addUserData(Map<String, dynamic> data) {
_userData.addAll(data);

View File

@ -88,7 +88,9 @@ class AgeGroupFormScreen extends ViewModelWidget<OnboardingViewModel> {
showBackButton: true,
showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(),
language: viewModel.selectedLanguage['code'],
onLanguage: () async => await viewModel.navigateToLanguage(),
);
Widget _buildTitle() => Text(

View File

@ -102,7 +102,9 @@ class ChallengeFormScreen extends ViewModelWidget<OnboardingViewModel> {
showBackButton: true,
showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(),
language: viewModel.selectedLanguage['code'],
onLanguage: () async => await viewModel.navigateToLanguage(),
);
Widget _buildTitle() => Text(

View File

@ -112,7 +112,8 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
showBackButton: true,
showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(),
language: viewModel.selectedLanguage['code'],
onLanguage: () async => await viewModel.navigateToLanguage(),
);
Widget _buildTitle() => Text(

View File

@ -88,7 +88,8 @@ class EducationalBackgroundFormScreen
showBackButton: true,
showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(),
language: viewModel.selectedLanguage['code'],
onLanguage: () async => await viewModel.navigateToLanguage(),
);
Widget _buildTitle() => const Text(

View File

@ -88,7 +88,8 @@ class FullNameFormScreen extends ViewModelWidget<OnboardingViewModel> {
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
onLanguage: () async => await viewModel.navigateToLanguage(),
language: viewModel.selectedLanguage['code'],
onLanguage: () async => await viewModel.navigateToLanguage(),
);
Widget _buildTitle() => const Text(

View File

@ -80,7 +80,9 @@ class GenderFormScreen extends ViewModelWidget<OnboardingViewModel> {
showBackButton: true,
showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(),
language: viewModel.selectedLanguage['code'],
onLanguage: () async => await viewModel.navigateToLanguage(),
);
Widget _buildTitle() => Text(

View File

@ -103,7 +103,8 @@ class LanguageGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
showBackButton: true,
showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(),
language: viewModel.selectedLanguage['code'],
onLanguage: () async => await viewModel.navigateToLanguage(),
);
Widget _buildTitle() => Text(

View File

@ -98,7 +98,9 @@ class LearningGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
showBackButton: true,
showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(),
language: viewModel.selectedLanguage['code'],
onLanguage: () async => await viewModel.navigateToLanguage(),
);
Widget _buildTitle(OnboardingViewModel viewModel) => Text.rich(

View File

@ -86,7 +86,9 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
showBackButton: true,
showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(),
language: viewModel.selectedLanguage['code'],
onLanguage: () async => await viewModel.navigateToLanguage(),
);
Widget _buildTitle() => Text(

View File

@ -58,7 +58,9 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
showBackButton: true,
showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(),
language: viewModel.selectedLanguage['code'],
onLanguage: () async => await viewModel.navigateToLanguage(),
);
Widget _buildExpandedBody(OnboardingViewModel viewModel) =>

View File

@ -11,6 +11,7 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart';
import '../../../app/app.locator.dart';
import '../../../models/user.dart';
import '../../../services/google_auth_service.dart';
import '../../../services/localization_service.dart';
import '../../../services/status_checker_service.dart';
class RegisterViewModel extends ReactiveViewModel
@ -24,16 +25,24 @@ class RegisterViewModel extends ReactiveViewModel
final _googleAuthService = locator<GoogleAuthService>();
final _localizationService = locator<LocalizationService>();
final _authenticationService = locator<AuthenticationService>();
@override
List<ListenableServiceMixin> get listenableServices => [_googleAuthService];
List<ListenableServiceMixin> get listenableServices => [_googleAuthService,_localizationService];
// Google user
GoogleSignInAccount? get _googleUser => _googleAuthService.googleUser;
GoogleSignInAccount? get googleUser => _googleUser;
// Languages
Map<String, dynamic> get _selectedLanguage => _localizationService.selectedLanguage;
Map<String, dynamic> get selectedLanguage => _selectedLanguage;
// Navigation
int _currentPage = 0;
@ -260,14 +269,19 @@ class RegisterViewModel extends ReactiveViewModel
void pop() => _navigationService.back();
Future<void> navigateToTermsAndConditions() async =>
await _navigationService.navigateToTermsAndConditionsView();
Future<void> replaceToLogin() async =>
await _navigationService.replaceWithLoginView();
Future<void> navigateToLanguage() async =>
await _navigationService.navigateToLanguageView();
Future<void> navigateToPrivacyPolicy() async =>
await _navigationService.navigateToPrivacyPolicyView();
Future<void> replaceToLogin() async =>
await _navigationService.replaceWithLoginView();
Future<void> navigateToTermsAndConditions() async =>
await _navigationService.navigateToTermsAndConditionsView();
Future<void> replaceWithStartUp() async =>
await _navigationService.clearStackAndShow(Routes.startupView);

View File

@ -68,7 +68,11 @@ class CreatePasswordScreen extends ViewModelWidget<RegisterViewModel> {
showBackButton: true,
onPop: viewModel.goBack,
showLanguageSelection: true,
);
language: viewModel.selectedLanguage['code'],
onLanguage: ()async => await viewModel.navigateToLanguage(),
);
Widget _buildExpandedBody(RegisterViewModel viewModel) =>
Expanded(child: _buildColumnScroller(viewModel));

View File

@ -81,7 +81,11 @@ class RegisterWithEmailScreen extends ViewModelWidget<RegisterViewModel> {
showBackButton: true,
onPop: viewModel.goBack,
showLanguageSelection: true,
);
language: viewModel.selectedLanguage['code'],
onLanguage: ()async => await viewModel.navigateToLanguage(),
);
Widget _buildExpandedBody(
{required BuildContext context,

View File

@ -84,7 +84,11 @@ class RegisterWithPhoneNumberScreen extends ViewModelWidget<RegisterViewModel> {
showBackButton: true,
onPop: viewModel.goBack,
showLanguageSelection: true,
);
language: viewModel.selectedLanguage['code'],
onLanguage: ()async => await viewModel.navigateToLanguage(),
);
Widget _buildExpandedBody(
{required BuildContext context,

View File

@ -95,6 +95,8 @@ class RegistrationOtpScreen extends ViewModelWidget<RegisterViewModel> {
showBackButton: true,
onPop: viewModel.goBack,
showLanguageSelection: true,
language: viewModel.selectedLanguage['code'],
onLanguage: () async => await viewModel.navigateToLanguage(),
);
Widget _buildExpandedBody(

View File

@ -47,16 +47,20 @@ class StartupViewModel extends ReactiveViewModel {
}
// Navigation
Future<void> replaceWithHome() async =>
await _navigationService.replaceWithHomeView();
Future<void> replaceWithFailure() async =>
await _navigationService.replaceWithFailureView(
label: 'Check you internet connection',
onTap: () async => await _getProfileStatus());
Future<void> replaceWithOnboarding() async =>
await _navigationService.replaceWithOnboardingView();
Future<void> replaceWithHome() async =>
await _navigationService.replaceWithHomeView();
// Remote api calls

View File

@ -43,7 +43,7 @@ class AssessmentLoadingScreen extends StatelessWidget {
Widget _buildAppBar() => LargeAppBar(
onPop: onPop,
showBackButton: true,
showLanguageSelection: true,
showLanguageSelection: false,
);
Widget _buildBody() => Expanded(child: Container());

View File

@ -7,7 +7,7 @@ import '../common/ui_helpers.dart';
import 'custom_elevated_button.dart';
class CourseCatalogTile extends StatelessWidget {
final CourseCatalog courseCatalog;
final CourseCatalog catalog;
final GestureTapCallback? onCourseTap;
final GestureTapCallback? onPracticeTap;
@ -15,7 +15,7 @@ class CourseCatalogTile extends StatelessWidget {
super.key,
this.onCourseTap,
this.onPracticeTap,
required this.courseCatalog,
required this.catalog,
});
@override
@ -57,7 +57,7 @@ class CourseCatalogTile extends StatelessWidget {
];
Widget _buildTitle() => Text(
courseCatalog.name ?? '',
catalog.name ?? '',
style: style16P600,
);

View File

@ -3,11 +3,12 @@ import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
class CourseTopicTile extends StatelessWidget {
class CourseModuleTileSmall extends StatelessWidget {
final String title;
final ProgressStatuses status;
const CourseTopicTile({super.key, required this.title, required this.status});
const CourseModuleTileSmall(
{super.key, required this.title, required this.status});
@override
Widget build(BuildContext context) => _buildTile();
@ -27,7 +28,9 @@ class CourseTopicTile extends StatelessWidget {
Widget _buildTitle() => Text(
title,
style: style16DG600,
maxLines: 1,
softWrap: false,
style: style14DG600,
);
Widget _buildLeadingWrapper() => CircleAvatar(

View File

@ -0,0 +1,267 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/models/course_unit.dart';
import 'package:yimaru_app/models/submodule.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/widgets/course_module_tile_small.dart';
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
import 'package:yimaru_app/ui/widgets/finish_practice_sheet.dart';
import '../common/app_colors.dart';
import '../common/ui_helpers.dart';
import '../views/course_unit/course_unit_viewmodel.dart';
import 'custom_circular_progress_indicator.dart';
import 'custom_elevated_button.dart';
class CourseUnitTile extends ViewModelWidget<CourseUnitViewModel> {
final int index;
final CourseUnit unit;
final GestureTapCallback? onLessonTap;
final GestureTapCallback? onPracticeTap;
const CourseUnitTile({
super.key,
this.onLessonTap,
this.onPracticeTap,
required this.unit,
required this.index,
});
Future<void> _getCourseModules({
required bool expanded,
required CourseUnitViewModel viewModel,
}) async {
if (!expanded) return;
// Prevent duplicate API calls
if ((unit.modules?.isNotEmpty ?? false)) return;
await viewModel.getCourseUnitModules(index: index, id: unit.id ?? 0);
}
Future<void> _showSheet(
{required BuildContext context,
required CourseUnitViewModel viewModel}) async =>
await showModalBottomSheet(
context: context,
backgroundColor: kcTransparent,
builder: (_) => _buildSheet(viewModel),
);
@override
Widget build(BuildContext context, CourseUnitViewModel viewModel) =>
_buildExpansionTileCard(context: context, viewModel: viewModel);
Widget _buildExpansionTileCard(
{required BuildContext context,
required CourseUnitViewModel viewModel}) =>
Container(
margin: const EdgeInsets.only(bottom: 15),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
border: Border.all(color: kcVeryLightGrey),
),
child: _buildTileStack(context: context, viewModel: viewModel),
);
Widget _buildTileStack(
{required BuildContext context,
required CourseUnitViewModel viewModel}) =>
Stack(
children: [
_buildExpansionTile(context: context, viewModel: viewModel),
// _buildContainerShaderState()
],
);
Widget _buildExpansionTile(
{required BuildContext context,
required CourseUnitViewModel viewModel}) =>
ExpansionTile(
enabled: true,
title: _buildTitle(),
textColor: kcDarkGrey,
showTrailingIcon: true,
initiallyExpanded: false,
subtitle: _buildSubtitle(),
// key: Key(unit.id.toString()),
collapsedIconColor: kcDarkGrey,
collapsedTextColor: kcDarkGrey,
backgroundColor: kcBackgroundColor,
shape: Border.all(color: kcTransparent),
expandedAlignment: Alignment.centerLeft,
collapsedBackgroundColor: kcBackgroundColor,
controlAffinity: ListTileControlAffinity.trailing,
expandedCrossAxisAlignment: CrossAxisAlignment.start,
tilePadding: const EdgeInsets.symmetric(horizontal: 15),
onExpansionChanged: (bool expanded) async => await _getCourseModules(
expanded: expanded,
viewModel: viewModel,
),
// enabled: status != ProgressStatuses.pending,
// showTrailingIcon: status != ProgressStatuses.pending ? true : false,
//initiallyExpanded: status == ProgressStatuses.started ? true : false,
children:
_buildExpansionTileChildren(context: context, viewModel: viewModel),
);
Widget _buildTitle() => Text(
unit.name ?? '',
maxLines: 1,
softWrap: false,
style: style16P600,
overflow: TextOverflow.ellipsis,
);
Widget _buildSubtitle() => Text(
'0% completed',
style: style14DG500,
);
List<Widget> _buildExpansionTileChildren(
{required BuildContext context,
required CourseUnitViewModel viewModel}) =>
[_buildExpansionTileItem(context: context, viewModel: viewModel)];
Widget _buildExpansionTileItem(
{required BuildContext context,
required CourseUnitViewModel viewModel}) =>
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildExpansionTileItemChildren(
context: context, viewModel: viewModel),
);
List<Widget> _buildExpansionTileItemChildren(
{required BuildContext context,
required CourseUnitViewModel viewModel}) =>
[
_buildProgressRowWrapper(),
verticalSpaceSmall,
_buildActionButtonWrapper(context: context, viewModel: viewModel),
verticalSpaceMedium,
_buildCourseModulesState(viewModel)
];
Widget _buildProgressRowWrapper() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildProgressRow(),
);
Widget _buildProgressRow() => Row(
mainAxisSize: MainAxisSize.min,
children: _buildProgressChildren(),
);
List<Widget> _buildProgressChildren() =>
[_buildProgressStatusWrapper(), horizontalSpaceSmall, _buildProgress()];
Widget _buildProgressStatusWrapper() => Expanded(
child: _buildProgressStatus(),
);
Widget _buildProgressStatus() => const CustomLinearProgressIndicator(
progress: 0,
activeColor: kcPrimaryColor,
backgroundColor: kcVeryLightGrey);
Widget _buildProgress() => const Text(
'0/0',
style: TextStyle(color: kcDarkGrey),
);
Widget _buildActionButtonWrapper(
{required BuildContext context,
required CourseUnitViewModel viewModel}) =>
Container(
height: 40,
margin: const EdgeInsets.symmetric(horizontal: 15),
child: _buildActionButtons(context: context, viewModel: viewModel),
);
Widget _buildActionButtons(
{required BuildContext context,
required CourseUnitViewModel viewModel}) =>
Row(
children: [
_buildLessonButtonWrapper(viewModel),
horizontalSpaceSmall,
_buildPracticeButtonWrapper(context: context, viewModel: viewModel),
],
);
Widget _buildLessonButtonWrapper(CourseUnitViewModel viewModel) => Expanded(
child: _buildLessonButton(viewModel),
);
Widget _buildLessonButton(CourseUnitViewModel viewModel) =>
CustomElevatedButton(
height: 15,
borderRadius: 12,
onTap: onLessonTap,
text: 'View Module',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
);
Widget _buildPracticeButtonWrapper(
{required BuildContext context,
required CourseUnitViewModel viewModel}) =>
Expanded(
child: _buildPracticeButton(context: context, viewModel: viewModel),
);
Widget _buildPracticeButton(
{required BuildContext context,
required CourseUnitViewModel viewModel}) =>
CustomElevatedButton(
height: 15,
borderRadius: 12,
onTap: onPracticeTap,
text: 'View Practices',
backgroundColor: kcWhite,
borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor,
);
Widget _buildSheet(CourseUnitViewModel viewModel) => FinishPracticeSheet(
onTap: viewModel.pop,
);
Widget _buildCourseModulesState(CourseUnitViewModel viewModel) =>
viewModel.busy(StateObjects.courseModules)
? _buildProgressIndicator()
: _buildCourseModules(viewModel);
Widget _buildProgressIndicator() => const Center(
child: CustomCircularProgressIndicator(color: kcPrimaryColor),
);
Widget _buildCourseModules(CourseUnitViewModel viewModel) => ListView.builder(
shrinkWrap: true,
itemCount: unit.modules?.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) =>
_buildCourseModuleCard(unit.modules?[index].name ?? ''),
);
Widget _buildCourseModuleCard(String title) =>
CourseModuleTileSmall(title: title, status: ProgressStatuses.completed);
// Widget _buildContainerShaderState() => status == ProgressStatuses.pending
// ? _buildContainerShaderWrapper()
// : Container();
Widget _buildContainerShaderWrapper() => Positioned.fill(
child: _buildContainerShader(),
);
Widget _buildContainerShader() => Container(
decoration: BoxDecoration(
color: kcWhite.withOpacity(0.5),
borderRadius: BorderRadius.circular(5),
),
);
}

View File

@ -4,6 +4,7 @@ import 'package:yimaru_app/ui/widgets/app_bar_pattern.dart';
import 'package:yimaru_app/ui/widgets/language_button.dart';
class LargeAppBar extends StatelessWidget {
final String? language;
final bool showBackButton;
final GestureTapCallback? onPop;
final bool showLanguageSelection;
@ -14,6 +15,7 @@ class LargeAppBar extends StatelessWidget {
{super.key,
this.onPop,
this.onClose,
this.language,
this.onLanguage,
required this.showBackButton,
required this.showLanguageSelection});
@ -72,8 +74,8 @@ class LargeAppBar extends StatelessWidget {
: _buildCloseButton());
Widget _buildLanguageSelector() => LanguageButton(
language: 'EN',
onTap: onLanguage,
language: language ?? '',
);
Widget _buildCloseButton() => IconButton(

View File

@ -1,6 +1,8 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import '../common/app_colors.dart';
import '../common/translations/locale_keys.g.dart';
import '../common/ui_helpers.dart';
class OptionTextDivider extends StatelessWidget {
@ -22,9 +24,9 @@ class OptionTextDivider extends StatelessWidget {
Widget _buildDivider() => const Divider(color: kcVeryLightGrey);
Widget _buildOrText() => const Text(
'or',
Widget _buildOrText() => Text(
LocaleKeys.or.tr(),
style: style14MG400,
textAlign: TextAlign.center,
style: TextStyle(color: kcMediumGrey),
);
}

View File

@ -3,11 +3,11 @@ import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
class OverallLearnProgress extends StatelessWidget {
class OverallProgress extends StatelessWidget {
final int progress;
final Color backgroundColor;
final Color indicatorBackgroundColor;
const OverallLearnProgress(
const OverallProgress(
{super.key,
required this.progress,
required this.backgroundColor,
@ -63,8 +63,8 @@ class OverallLearnProgress extends StatelessWidget {
backgroundColor: indicatorBackgroundColor,
);
Widget _buildSubtitle() => const Text(
Widget _buildSubtitle() => Text(
'Keep up the great work! You\'re doing amazing.',
style: TextStyle(color: kcDarkGrey),
style: style14DG500,
);
}

View File

@ -1,4 +1,6 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
import '../common/app_colors.dart';
import '../common/ui_helpers.dart';
@ -18,9 +20,9 @@ class RegisterForAccount extends StatelessWidget {
],
);
Widget _buildLeadingText() => const Text(
'Dont have an account? ',
style: TextStyle(color: kcMediumGrey),
Widget _buildLeadingText() => Text(
'${LocaleKeys.dont_have_account.tr()} ',
style: style14MG400,
);
Widget _buildRegisterTextButton() => TextButton(
@ -31,8 +33,8 @@ class RegisterForAccount extends StatelessWidget {
child: _buildRegisterText(),
);
Widget _buildRegisterText() => const Text(
'Register',
style: TextStyle(color: kcPrimaryColor),
Widget _buildRegisterText() => Text(
LocaleKeys.register.tr(),
style:style14P400 ,
);
}

View File

@ -17,6 +17,7 @@ import flutter_secure_storage_darwin
import google_sign_in_ios
import package_info_plus
import record_macos
import shared_preferences_foundation
import sqflite_darwin
import url_launcher_macos
import video_player_avfoundation
@ -35,6 +36,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))

View File

@ -377,6 +377,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.2"
easy_localization:
dependency: "direct main"
description:
name: easy_localization
sha256: "2ccdf9db8fe4d9c5a75c122e6275674508fd0f0d49c827354967b8afcc56bbed"
url: "https://pub.dev"
source: hosted
version: "3.0.8"
easy_logger:
dependency: transitive
description:
name: easy_logger
sha256: c764a6e024846f33405a2342caf91c62e357c24b02c04dbc712ef232bf30ffb7
url: "https://pub.dev"
source: hosted
version: "0.0.2"
email_validator:
dependency: "direct main"
description:
@ -654,6 +670,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.1"
flutter_localizations:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
flutter_native_splash:
dependency: "direct main"
description:
@ -1472,6 +1493,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.28.0"
shared_preferences:
dependency: transitive
description:
name: shared_preferences
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
url: "https://pub.dev"
source: hosted
version: "2.5.5"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
url: "https://pub.dev"
source: hosted
version: "2.4.23"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
url: "https://pub.dev"
source: hosted
version: "2.5.6"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shelf:
dependency: transitive
description:

View File

@ -1,5 +1,5 @@
name: yimaru_app
version: 0.1.15+17
version: 0.1.16+18
publish_to: 'none'
description: A new Flutter project.
@ -9,6 +9,7 @@ environment:
dependencies:
flutter:
sdk: flutter
http: any
intl: any
dio: ^5.9.0
path: ^1.9.1
@ -37,6 +38,7 @@ dependencies:
json_annotation: ^4.9.0
flutter_spinkit: ^5.2.2
stacked_services: ^1.1.0
easy_localization: ^3.0.8
omni_datetime_picker: any
json_serializable: ^6.8.0
waveform_recorder: ^1.8.0
@ -54,7 +56,6 @@ dependencies:
flutter_local_notifications: ^20.1.0
internet_connection_checker_plus: ^2.9.1+2
http: any
dev_dependencies:
flutter_test:
sdk: flutter
@ -74,6 +75,7 @@ flutter:
assets:
- assets/icons/
- assets/images/
- assets/translations/
fonts:
- family: Aeonik
fonts:

View File

@ -21,6 +21,7 @@ import 'package:yimaru_app/services/vimeo_service.dart';
import 'package:yimaru_app/services/url_launcher_service.dart';
import 'package:yimaru_app/services/phone_caller_service.dart';
import 'package:yimaru_app/services/learn_service.dart';
import 'package:yimaru_app/services/localization_service.dart';
// @stacked-import
import 'test_helpers.mocks.dart';
@ -56,6 +57,7 @@ import 'test_helpers.mocks.dart';
MockSpec<LearnLessonService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<LearnService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<LearnService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<LocalizationService>(onMissingStub: OnMissingStub.returnDefault),
// @stacked-mock-spec
],
)
@ -86,6 +88,7 @@ void registerServices() {
getAndRegisterLearnLessonService();
getAndRegisterLearnService();
getAndRegisterLearnService();
getAndRegisterLocalizationService();
// @stacked-mock-register
}
@ -275,6 +278,13 @@ MockLearnService getAndRegisterLearnService() {
locator.registerSingleton<LearnService>(service);
return service;
}
MockLocalizationService getAndRegisterLocalizationService() {
_removeRegistrationIfExists<LocalizationService>();
final service = MockLocalizationService();
locator.registerSingleton<LocalizationService>(service);
return service;
}
// @stacked-mock-create
void _removeRegistrationIfExists<T extends Object>() {

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('LocalizationServiceTest -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}